The most common way to manage programs is to use a variable for program names or paths that are likely to change. The variables can be defined in a simple block, as we have seen:
MV ?= mv -f RM ?= rm -f
or in a conditional block:
ifdef COMSPEC MV ?= move RM ?= del else MV ?= mv -f RM ?= rm -f endif
If a simple block is used, the values can be changed by resetting them on the command line, by editing the makefile , or (in this case because we used conditional assignment, ?= ) by setting an environment variable. As mentioned previously, one way to test for a Windows platform is to check for the COMSPEC variable, which is used by all Windows operating systems. Sometimes only a path needs to change:
ifdef COMSPEC OUTPUT_ROOT := d: GCC_HOME := c:/gnu/usr/bin else OUTPUT_ROOT := $(HOME) GCC_HOME := /usr/bin endif OUTPUT_DIR := $(OUTPUT_ROOT)/work/binaries CC := $(GCC_HOME)/gcc
This style results in a makefile in which most programs are invoked via make variables. Until you get used to it, this can make the makefile a little harder to read. However, variables are often more convenient to use in the makefile anyway, because they can be considerably shorter than the literal program name , particularly when full paths are used.
The same technique can be used to manage different command options. For instance, the built-in compilation rules include a variable, TARGET_ARCH , that can be used to set platform-specific flags:
ifeq "$(MACHINE)" "hpux-hppa" TARGET_ARCH := -mdisable-fpregs endif
When defining your own program variables, you may need to use a similar approach:
MV := mv $(MV_FLAGS) ifeq "$(MACHINE)" "solaris-sparc" MV_FLAGS := -f endif
If you are porting to many platforms, chaining the ifdef sections can become ugly and difficult to maintain. Instead of using ifdef , place each set of platform-specific variables in its own file whose name contains a platform indicator. For instance, if you designate a platform by its uname parameters, you can select the appropriate make include file like this:
MACHINE := $(shell uname -smo sed 's/ /-/g') include $(MACHINE)-defines.mk
Filenames with spaces present a particularly irritating problem for make . The assumption that whitespace separates tokens during parsing is fundamental to make . Many built-in functions such as word , filter , wildcard , and others assume their arguments are space-separated words. Nevertheless, here are some tricks that may help in small ways. The first trick, noted in Section 7.4.1 in Chapter 8, is how to replace spaces with another character using subst :
space = $(empty) $(empty) # $(call space-to-question,file-name) space-to-question = $(subst $(space),?,)
The space-to-question function replaces all spaces with the globbing wildcard question mark. Now, we can implement wildcard and file-exists functions that can handle spaces:
# $(call wildcard-spaces,file-name) wildcard-spaces = $(wildcard $(call space-to-question,)) # $(call file-exists file-exists = $(strip \ $(if ,,$(warning has no value)) \ $(call wildcard-spaces,))
The wildcard-spaces function uses space-to-question to allow the makefile to perform a wildcard operation on a pattern including spaces. We can use our wildcard-spaces function to implement file-exists . Of course, the use of the question mark may also cause wildcard-spaces to return files that do not correctly match the original wildcard pattern (e.g., "my document.doc" and "my-document.doc"), but this is the best we can do.
The space-to-question function can also be used to transform filenames with spaces in targets and prerequisites, since those allow globbing patterns to be used.
space := $(empty) $(empty) # $(call space-to-question,file-name) space-to-question = $(subst $(space),?,) # $(call question-to-space,file-name) question-to-space = $(subst ?,$(space),) $(call space-to-question,foo bar): $(call space-to-question,bar baz) touch "$(call question-to-space,$@)"
Assuming the file " bar baz " exists, the first time this makefile is executed the prerequisite is found because the globbing pattern is evaluated. But the target globbing pattern fails because the target does not yet exist, so $@ has the value foo?bar . The command script then uses question-to-space to transform $@ back to the file with spaces that we really want. The next time the makefile is run, the target is found because the globbing pattern finds the target with spaces. A bit ugly, but I have found these tricks useful in real makefile s.
7.3.1 Source Tree Layout
Another aspect of portability is the ability to allow developers freedom to manage their development environment as they deem necessary. There will be problems if the build system requires the developers to always place their source, binaries, libraries, and support tools under the same directory or on the same Windows disk drive, for instance. Eventually, some developer low on disk space will be faced with the problem of having to partition these various files.
Instead, it makes sense to implement the makefile using variables to reference these collections of files and set reasonable defaults. In addition, each support library and tool can be referenced through a variable to allow developers to customize file locations as they find necessary. For the most likely customization variables, use the conditional assignment operator to allow developers a simple way of overriding the makefile with environment variables.
In addition, the ability to easily support multiple copies of the source and binary tree is a boon to developers. Even if they don't have to support different platforms or compilation options, developers often find themselves working with several copies of the source, either for debugging purposes or because they work on several projects in parallel. Two ways to support this have already been discussed: use a "top-level" environment variable to identify the root of the source and binary trees, or use the directory of the makefile and a fixed relative path to find the binary tree. Either of these allows developers the flexibility of supporting more than one tree.