Solved Makefiles and $$ syntax

In my quest to create a universal Makefile, I'm puzzled with the $$(foo) syntax. For example,

Makefile:
OSNAME = $$(uname)

target:
    echo $(OSNAME)

make displays the result of the uname command. But it is not possible to use the $OSNAME variable with the include directive.

Makefile:
OSNAME = $$(uname)
include $(OSNAME).Mk

target:
    echo $(OSNAME)

Bash:
$make
make: "/tmp/Makefile" line 3: Could not find $(uname).Mk
make: Fatal errors encountered -- cannot continue
make: stopped in /tmp

Of course, the file to include exist (eg. FreeBSD.Mk).

Apparently I'm misuse the $$(foo) syntax, but I don't understand where is the problem. I have the same behavior on Aix, FreeBSD, Linux, macos and OpenSolaris. For once, they are all compatible.

Regards
 
VARIABLE ASSIGNMENTS
Variables in make are much like variables in the shell, and, by
tradition, consist of all upper-case letters.

Variable assignment modifiers
The five operators that can be used to assign values to variables are as
follows:

= Assign the value to the variable. Any previous value is
overridden.

+= Append the value to the current value of the variable.

?= Assign the value to the variable if it is not already defined.

:= Assign with expansion, i.e. expand the value before assigning it
to the variable. Normally, expansion is not done until the
variable is referenced. NOTE: References to undefined variables
are not expanded. This can cause problems when variable
modifiers are used.

!= Expand the value and pass it to the shell for execution and
assign the result to the variable. Any newlines in the result
are replaced with spaces.
Yes, I know about that. gmake (Linux, macos) use the following syntax:

Code:
OSNAME = $(shell uname)

FreeBSD's syntax (compatible with gmake since 4.0.0) is :
Code:
OSNAME != uname

AIX and OpenSolaris (OpenIndiana) use the following syntax
Code:
OSNAME:sh = uname

My concern is about $$(foo) that works with every one, so my post...
 
Your problem is not syntax, it is semantics.

In BSD make, when you say variable = value, then the string "value" becomes the content of the variable. In your case, the variable OSNAME is set to the string "$$(uname)". Not to the string "FreeBSD". Now, the meaning of the string $$(uname) is fascinating. Because in make syntax, the string "$$" actually means "$", except that the parser knows that sometimes $$ means "execute". But only in certain contexts. Look at the man page for FreeBSD make, and search for $$ ... it is described extensively.

Then, a moment later, what you try to do is "include $$(uname).Mk". That is not legal syntax. Make does not automatically concatenate strings across variable names. You really need to read the long section about variable expansion in the FreeBSD make man page.

As a general word of advice: I think creating a single makefile that can be correctly interpreted by both BSD make and Gnu gmake is de-facto impossible. The differences in syntax and semantics are too large. Here is what I would do: The only common part of the makefile is the list of targets, and the explicit dependencies. Those use syntax and semantics that is indeed common to both makes. Then, the setup (like what files to include) and the execution (build rules) are strictly separate in separate files. For your main makefile, you have to versions: BSDmakefile and GNUmakefile. Those then include the make-specific setups, then include the common part, and then include the make-specific production rules.

Even better idea: Give up on make. It is a horrible tool, it has always been a horrible tool, and the massive incompatibility between the common versions has not made it any better. Recently, I've been pretty happy with Bazel as a make replacement, but I understand that there are many other solutions.
 
In a (Free)BSD make file the contents is handled in various ways. Technically speaking: all the text characters are scanned and subsequently parsed. What adds to the complexity is that sometimes "stuff" gets output to the shell. The shell in turn interprets that "stuff" that make(1) handed over and, after processing, the shell hands it back to make(1). Sometimes make(1) pre-processes it before handing it over to the shell.

Both syntax and semantics matter. Maybe you have been trying to get the value of the /bin/sh shell variable uname (also in bash):
sh:
# echo $(uname)
FreeBSD

make(1) uses a (sub)shell of /bin/sh to execute shell commands. (note: it does not rely on the shell from which the make command is issued.) However, there is the shell command uname that makes things more readily accessible:
sh:
# uname
FreeBSD

The easiest and most straightforward solution, as mentioned in earlier messages, is the use of the command uname in your make file using the syntax "!=":
Code:
OSNAME2 != uname

If you really want to use the shell variable* uname into your make file then you can let the shell handle the expansion of the shell variable:
Code:
OSNAME3 != echo $$(uname)

You can even use your initial setup to make this happen:
Code:
OSNAME = $$(uname)
OSNAME4 != echo $(OSNAME)

Putting all the possible constructs together as an example, you get the following Makefile:
Makefile:
OSNAME = $$(uname)
OSNAME2 != uname
OSNAME3 != echo $$(uname)
OSNAME4 != echo $(OSNAME)
#include $(OSNAME).Mk
#include $(OSNAME2).Mk
#include $(OSNAME3).Mk
#include $(OSNAME4).Mk

target:
        echo "OSNAME - $(OSNAME)"
        echo "OSNAME2 - $(OSNAME2)"
        echo "OSNAME3 - $(OSNAME3)"
        echo "OSNAME4 - $(OSNAME4)"
As written with the comments in place, make will not complain. However, after removing the #-characters, make is not happy. The following sequence shows how that can be remied (commands are issued from a tcsh):
Code:
% make
make: "<path-to-make_file>/Makefile" line 5: Could not find $(uname).Mk
make: "<path-to-make_file>/Makefile" line 6: Could not find FreeBSD.Mk
make: "<path-to-make_file>/Makefile" line 7: Could not find FreeBSD.Mk
make: "<path-to-make_file>/Makefile" line 8: Could not find FreeBSD.Mk
make: Fatal errors encountered -- cannot continue
make: stopped in <path-to-make_file>
% touch \$\(uname\).Mk FreeBSD.Mk
% ls *.Mk
$(uname).Mk FreeBSD.Mk
% make
echo "OSNAME - $(uname)"
OSNAME - FreeBSD
echo "OSNAME2 - FreeBSD"
OSNAME2 - FreeBSD
echo "OSNAME3 - FreeBSD"
OSNAME3 - FreeBSD
echo "OSNAME4 - FreeBSD"
OSNAME4 - FreeBSD

You can see how the different options have been made to work. When using include $(OSNAME).Mk, you get include $$(uname).Mk; that is technically legal syntax but, it needs a rather special file and, most probably, that is not what is intended. This point is where semantics are important: at this particular location in a make file this is just NOT the 'sometimes "stuff" gets output to the shell'. Here make(1) only interprets the include $$(uname).Mk as follows. Usually a construction in a make file as $(uname) or ${uname} would be interpreted as a reference to a make file variable. However, because the $-character is preceded by another $-character, the second $-character does not have a special meaning and is instead interpreted just as a literal $-character. The first extra $-character functions as an escape character. The total string is: include $$(uname).Mk**. The big difference here is that in this particular position in a make file there is nothing output to the shell: the shell doesn't get to "see it". make(1) will now try to include the file $(uname).Mk; note the single $-character.

The following may also be helpful:

I'm not familiar with the intricacies of GNU make however, for (Free)BSD make(1) meta mode is pretty important. Additionally, if you're searching/experimenting for an all-encompassing build system, I too suggest that you look at other options. Besides the option mentioned by ralphbsz you might have a look at cmake. Note however that more often than not such alternatives, like cmake, are basically build generator systems that generate (make) code "sort of under the hood" and are easily tailored for a specific target make system. Such a build generator system as cmake is intended to abstract from the underlying make system for which it generates code and making your cmake files system independent. This means that by adding another layer of abstraction, you—mostly—won't have to deal with the intricacies of the various underlying make systems.

___
* by using the value of a shell variable you increase the dependance on particular properties of a certain shell. I'd consider using an external command a somewhat safer option.
** you should be able to find that in the output of make -d A but there is quite a lot of text in that debug output.
 
Last edited:
Hi
ralphbsz, Erichans, Thanks for your explanations. I agree that make is an horrible tool, but it's available on every *nix and I hate dependency :) Finaly, I found a solution thats works with gmake, bmake/make (FreeBSD) and make (AIX, Solaris).

In fact FreeBSD support the ":sh" syntax (just like AIX and Solaris):
Code:
OSNAME:sh = uname
So the following Makefile works on all platforms:
Code:
OSNAME:sh = uname
OSNAME     ?= $(shell uname)   # ignored by all non-gmake versions

include $(OSNAME).Mk

all:
    @echo $(OSNAME)

Of course, all the specific syntax goes to $(uname).Mk. That said, it's not tested on NetBSD or OpenBSD.
 
Hi
ralphbsz, Erichans, Thanks for your explanations. I agree that make is an horrible tool, but it's available on every *nix and I hate dependency :) Finaly, I found a solution thats works with gmake, bmake/make (FreeBSD) and make (AIX, Solaris).

In fact FreeBSD support the ":sh" syntax (just like AIX and Solaris):
Code:
OSNAME:sh = uname
So the following Makefile works on all platforms:
Code:
OSNAME:sh = uname
OSNAME     ?= $(shell uname)   # ignored by all non-gmake versions

include $(OSNAME).Mk

all:
    @echo $(OSNAME)

Of course, all the specific syntax goes to $(uname).Mk. That said, it's not tested on NetBSD or OpenBSD.

With no whitespace before `?=` works for me. Also on NetBSD.
 
Back
Top