GNU Make path handling

[article]
Summary:

Ask Mr. Make discusses how GNU Make handles paths.

The Basics: Target name matching

Take a look at the following example Makefile and suppose that ../foo is missing.  Does the Makefile manage to create it?

.PHONY: all
all: ../foo
.././foo:
    touch $@


If you run that Makefile with GNU Make you might be surprised to see the error

make: *** No rule to make target `../foo', needed by `all'.  Stop.

Now if you change the Makefile to:

.PHONY: all
all: ../foo
./../foo:
touch $@

you'll find that it works as expected and performs a touch ../foo.

The first Makefile fails because GNU Make doesn't do path manipulation in target names and so it sees two different targets called ../foo and .././foo and fails to make the connection between the two.   The second Makefile works because I lied in the last sentence. GNU Make does do a tiny big of path manipulation: it will strip leading ./ from target names.   So in the second Makefile both targets are ../foo and it works as expected.

In general the rule with GNU Make targets is that they are treated as literal strings without interpreting them in any way.   That means it's essential that when referring to a target in a Makefile you always make sure that the same string is used.

The Basics: Lists of paths
It bears repeating that GNU Make lists are just strings where any whitespace is considered to be the list seperator.  That means that paths with spaces in them are not recommended because it makes using many of GNU Make's built in functions impossible, and spaces in paths cause problems with targets. 

For example, suppose a target is /tmp/sub directory/target and we write a rule for it like this:

/tmp/sub directory/target:
    @do stuff

GNU Make will actually interpret that as two rules, one for /tmp/sub and one for directory/target, just as if you'd written

/tmp/sub:
    @do stuff
directory/target:
    @do stuff

You can work around that by escaping that space with \ but that escape is poorly  respected by GNU Make (it only works in target names and the $(wildcard) function).  

Unless you have to, avoid spaces in target names.

The Basics: Lists of paths in VPATH and vpath
Another place that lists of paths appear in GNU Make is when specifying the VPATH or in a vpath directive.   For example, it's possible to set the VPATH to search for source files to a list of : or whitespace separated paths:

VPATH = ../src:../thirdparty/src /src
vpath %c ../src:../thirdparty/src /src

GNU Make will split that path correctly at either colons or whitespaces.  On Windows system that native builds of GNU Make use ; as the path separator for VPATH (and vpath) because : is needed for drive letters.   In these paths GNU Make on Windows actually tries to be smart and will split on colons unless it looks like a drive letter (one letter followed by a colon).   This drive letter intelligence actually creates a problem if you have a directory
in the path whose name is a single letter: in that case you must use ; as the path separator otherwise GNU Make will think it's a drive.

VPATH = ../src;../thirdparty/src /src
vpath %c ../src;../thirdparty/src /src

On both POSIX and Windows systems a space in a path is a separator in a VPATH and vpath.   So the space is the best bet for cross-platform GNU Makeing.

/ or \

On POSIX systems / is the path separator and on Windows systems it's \.   It's common to see paths being built up in Makefiles like this:

SRCDIR := src

MODULE_DIR := module_1

MODULE_SRCS := $(SRCDIR)/$(MODULE_DIR)

It would be nice to remove the POSIX-only / there are replace it with something that would work with a different separator.   One way to do that is to define a variable called / (GNU Make lets you get away using almost anything as a variable name) and use it in place of /.

/ := /
SRCDIR := src
MODULE_DIR := module_1
MODULE_SRCS := $(SRCDIR)$/$(MODULE_DIR)


Of course, if that makes you uncomfortable just call is SEP:

SEP := /
SRCDIR := src
MODULE_DIR := module_1
MODULE_SRCS := $(SRCDIR)$(SEP)$(MODULE_DIR)

Now when switching to Windows you can just redefine / or SEP to \.  I use $(strip) to define / because it's hard to get a literal \ on its own (since GNU Make interprets it as a line continuation and it can't be escaped).

/ := $(strip \)
SRCDIR := src
MODULE_DIR := module_1
MODULE_SRCS := $(SRCDIR)$/$(MODULE_DIR)

Note, however, that the Windows builds of GNU Make will also accept / as a path separator, so paths like c:/src are legal.  Using those paths will simplify the Makefile but be careful when passing them to a native Windows tool that expects \ separaed paths.   If that's necessary then the follwing simple function will convert a forward slash path to a back slash path:

forward-to-backward = $(subst /,\,$1)

A Windows oddity: case insensitive but case preservingOn POSIX systems file names as case sensitive, on Windows they are not.  On Windows the files File, file and FILE are all the same physical file.  But Windows also has the oddity that the first time a file is accessed the specific case used is recorded and preserved.  Thus if we touch File it will appear as File in the file system (but can be accessed as FILE, file or any other combination of case).

By default GNU Make does case sensitive target comparisons and thus the following Makefile does not do what you might expect:

.PHONY: all
all: File
file:
    @touch $@

It is possible to compile GNU Make on Windows to do case insensitive comparisons (with the HAVE_CASE_INSENSITIVE_FS option).

This oddity is more likely to arise when a target is specified in a Makefile and also found using a wildcard search.  The target names may differ in case and that may cause an unexpected 'No rule to make' error.

GNU Make built in path functions and macros
It's possible to find out the current working directory in GNU Make using the built in CURDIR.  Note that CURDIR will follow symlinks so suppose you are in /foo but /foo is actually a symlink to /somewhwere/foo.   CURDIR will report the directory as /somewhere/foo.  If you need the non-symlink-followed directory name then call pwd via $(shell):

CURRENT_DIRECTORY := $(shell pwd)

The other directory that it's often interesting to find is the directory in which the current Makefile is stored.  This can be done using the MAKEFILE_LIST macro that was introduced in GNU Make 3.80.   At the start of a Makefile it's possible to extract its directory as follows:

CURRENT_MAKEFILE := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
MAKEFILE_DIRECTORY := $(dir $(CURRENT_MAKEFILE))

GNU Make has functions for splitting paths into components: dir, notdir, basename and suffix.

Consider a file name /foo/bar/source.c stored in the macro FILE.   The four functions above can be used to extract the directory, file name and suffix:

FunctionResult
dir/foo/bar/
notdirsource.c
basenamesource
suffix.c

You can see that the directory, the non-directory part, the suffix (or extension) and the non-directory part without the suffix have been extracted.   These four functions make file name manipulation easy.   If there was no directory specified then GNU Make assumes ./.  For example, suppose that FILE was just source.c:

FunctionResult
dir./
notdirsource.c
basenamesource
suffix.c

Since these functions are commonly used in conjunction with GNU Make's automatic macros (like $@) GNU Make provides a modifier syntax.  Appending D or F to any automatic macro is equivalent to calling $(dir) or $(notdir) on it.  For example, $(@D) is equivalent to $(dir $@) and $(@F) is the same as $(notdir $@).

GNU Make 3.81 added two very useful functions: abspath and realpath.

realpath is a GNU Make wrapper for the C library realpath function which removes ./, resolves ../, removes duplicated / and follows symlinks.  The argument to realpath must exist in the file system.   The path returned by realpath is absolute.  If the path does not exist then the function returns an empty string.

Note that you cannot use realpath as a file exists function because the final part of the path does not have to exist; to do file exists used $(wildcard FILE). For example, the full path of the current directory could be found like this:

         current := $(realpath ./)

abspath is similar but it does not follow symlinks and does not have to refer to an existing file or directory.

About the author

CMCrossroads is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.