Introduction to GNU Make
Filed under: tools build scripting
There are 0 comments on this article.
Introduction
Make is the original build scripting tool. Created by Stuart Feldman at Bell Labs as long ago as 1977, make was developed to support dependency tracking and archiving for applications with large numbers of source files. It became popular very quickly and over time has been delivered with practically every BSD/UNIX operating system variant. Unfortunately, most vendors have also taken the opportunity to "enhance" make for their own bespoke environments; consequently there is really no single, industry recognised implementation. Probably the most common however is GNU make - an open source variant maintained by the GNU Project. It is therefore this variant of make which we will be discussing in this article.
In its simplest form, make can be used to create and coordinate a sequence of operating system scripts or commands. However, it is more typically used to help developers keep track of which files are needed to build a particular program or application and to sort out the dependency relationships between files; for example which soure code files, header files and libraries make up a specific executable. Make helps implement a simple form of build avoidance, in that it looks at the date/time stamps of individual files, and the dependency relationships that you have defined, to work out what (if any) of the files from your complete application it needs to re-build.
Key Concepts
target - the name of the object you are trying to create.
dependency - the relationship between a target and the other objects that are needed to construct it.
commands - the operating system commands, utilities or scripts that need to be executed to bring a target up to date.
The input to make is a textual file, by default called makefile or Makefile (although any filename can be used). A complete application can be built using one or many makefiles (which are commonly located at different component/directory levels). Each makefile describes a set of targets, which are the objects that can be made. Targets are usually the name of an executable or library but can also be the name of a specific "activity", for example "clean" or "release" - such targets are often called phony targets since they do not map to a physical file. By default, a target “exists” if it is present on disk (i.e., there is a file with that name).
Each target is constructed using a set of commands - which are usually calls to compilers, linkers, pre-processors and so on, but can actually be any operating system script or utility. A command or set of commands is executed by make to bring a target up to date.
Each target can also have dependencies. A dependency is a relationship between a target and the other objects that are needed to construct it. For example, an executable depends on object files and libraries, while an object file depends on source code and headers. Another name for a dependency is a “prerequisite”.
An example of calling make from the command line to execute a "release" target would be as follows:
Here the "-f" option is used to specify the name of the makefile that we wish to use (atm.mak) and the target which we will be executing is the "release" target.
One of the most important things to note is that make in itself is not a compiler, rather it invokes your compiler either using the rules that you specify or using its own built-in rules.
An Example
An example of a GNU makefile to compile a set of C++ classes (for an ATM application) and produce a releasable ".tar" file is given below:
CXX := g++ -c CXXFLAGS := -O2 LD := g++ -o LIBS := -lstdc++ RM := rm -r ARCHIVE := tar -cf COMPRESS := gzip PROGRAM := atm SOURCES := account.cc customer.cc transaction.cc atm.cc HEADERS := account.h customer.h transaction.h OBJECTS := $(SOURCES:%.cc=%.o) .PHONY: all all: $(PROGRAM) # redefine compilation procedure for .cc files .SUFFIXES: .SUFFIXES: .cc .o .cc.o: $(CXX) $(CXXFLAGS) $< # create the executable $(PROGRAM): $(OBJECTS) $(LD) $@ $(OBJECTS) $(LIBS) # dependencies (without makedepend) $(OBJECTS): $(HEADERS) # create a release .PHONY: release release: $(RM) $(PROGRAM).tar.gz $(ARCHIVE) $(PROGRAM).tar $(PROGRAM) $(SRC) $(HEADERS) Makefile $(COMPRESS) $(PROGRAM).tar # create a clean environment .PHONY: clean clean: $(RM) $(PROGRAM) $(OBJECTS)
If this makefile was used to build the ATM application then the output result would look similar to the following:
>make -f atm.mak
g++ -O2 -c -o account.o account.cc
g++ -O2 -c -o customer.o customer.cc
g++ -O2 -c -o transaction.o transaction.cc
g++ -O2 -c -o atm.o atm.cc
g++ -o atm account.o customer.o transaction.o atm.o -lstdc++
>make -f atm.mak release
rm -f atm.tar.gz
tar -cf atm.tar atm account.h transaction.h ... Makefile
gzip atm.tar
In this makefile an executable called atm is being built, it consists of four C++ source files (as listed on line 9) and three headers file (as listed on line 10). Lines 1-11 define some make macros to be used throughout the makefile. A macro in make is a variable, it can either be user defined (as for example PROGRAM in our example) or internally pre-defined (such as $@). There are a large number of pre-defined macros (which can be listed by running "make -p"). Of particular note is line 11 which uses a macro string substitution to create the macro OBJECTS which is similar to SOURCES but with the ".cc" extension replaced with ".o". Line 14 defines the default "all" target of the makefile - which will build the application and then create a release from it. The "all" target is marked as a .PHONY target (on line 13) to indicate that it is a special internal only target - otherwise make would try to check for the existence of thistarget on the file system.
Lines 17 to 20 define how we will be creating object files from C++ source files via use of suffix rules. In practice there is no need to define such rules, as make has a large number of internal rules for most source files types. However in this case, we are overriding the rule so that it is clear how C++ object files are constructed (and to help you with your understanding). The "$<" string is one of make's special dynamic macros that can be used to refer to the name of the dependency file (i.e. the C++ source file being compiled). Note that one of the most annoying issues with make is that it insists all indentation is carried out using tabs not spaces - this is true for the command script of every target.
In lines 23-24 a target is defined to link the application and lists the object files that it is dependent upon. The command on line 24 defines how to link the object files together to produce the resultant application executable. As well as listing the dependencies for object files, on line 27 we also list the dependencies of the object files on the header files which are used in their compilation. The reason for this is that if a header file is updated we want make to see that the date/time-stamp of the header file has changed and therefore automatically work out which object files it needs to rebuild.
Lines 31-34 defines a target to create a releasable ".tar" file (including our sources and compiled application). Finally lines 37-39 define a target that can be called to "clean up" (i.e. remove) any pre-built objects and application files in order to be able to return to a known state.
Managing Dependencies
Manually managing the header file dependencies for an application such as this is relatively straightforward, however in large applications (potentially consisting of thousands of files) this is not practical. Instead it is better to let make and your compiler manage these dependencies for you. Most modern compilers take a command line argument to indicate it that source files should be parsed for dependency information and the results output. For the GNU C/C++ compilers this is the "-M" for full or "-MM" for project specific dependencies (i.e. only the header files that you have developed - not operating system header files). A simple method of managing dependencies is to add a target to create a specific dependency file and then include the ouput of this file directly within your makefile. An example of achieving this is illustrated in the following makefile:
CXX := g++
CXXFLAGS := -O2
LD := g++ -o
LIBS := -lstdc++
RM := rm -f
ARCHIVE := tar -cf
COMPRESS := gzip
PROGRAM := atm.exe
SOURCES := ${wildcard *.cc}
OBJECTS := $(SOURCES:%.cc=%.o)
.PHONY: all
all: $(PROGRAM)
-include depend
# create the executable
$(PROGRAM): depend $(OBJECTS)
$(LD) $@ $(OBJECTS) $(LIBS)
# create the dependency file
depend: $(SOURCES)
$(CXX) -MM $(CXXFLAGS) $^ > $@
# create the release
.PHONY: release
release: $(PROGRAM)
$(RM) $(PROGRAM).tar.gz
$(ARCHIVE) $(PROGRAM).tar $(PROGRAM) $(SRC) $(SOURCES) ${wildcard *.h} Makefile
$(COMPRESS) $(PROGRAM).tar
# create a clean environment
.PHONY: clean
clean:
$(RM) $(PROGRAM).tar.gz $(PROGRAM) $(OBJECTS)
# clean up dependency file
.PHONY: clean-depend
clean-depend: clean
$(RM) depend
In this example a new target called "depend" is added on lines 21-23 which takes the name of each source file (via the $^ dynamic macro and creates a file called depend. Notice that on line 15 an "-include" command is used to include this file into the makefile (the minus means ignore errors - in case the file does not exist yet). There is also an additional cleanup target called "clean-depend" on lines 38-40. The reason that we have a special target for this is that we make to manage this dependcy file, i.e. update it when its out of date, and not for it to be deleted as part of a build specific "clean" process
If we now used this makefile was to build the ATM application then the output result would look similar to the following:
>make -f atm.mak depend
g++ -MM -O2 account.cc atm.cc customer.cc transaction.cc > depend
>make -f atm.mak
g++ -O2 -c -o account.o account.cc
g++ -O2 -c -o customer.o customer.cc
g++ -O2 -c -o transaction.o transaction.cc
g++ -O2 -c -o atm.o atm.cc
g++ -o atm account.o customer.o transaction.o atm.o -lstdc++
>cat depend
account.o: account.cc account.h
atm.o: atm.cc account.h customer.h transaction.h
customer.o: customer.cc customer.h account.h
transaction.o: transaction.cc transaction.h
The last command line invocation is used to show the contents of the dependency file.
Note, that in a large code base we might also use one of GNU makes functions to work out the list of source files to build for us by looking in a specific directory (a typical example is shown on line 9 where the GNU make "${wildcard *.cc}" function is being used - see the GNU make reference manual for more detail).
Summary
There is significantly more to GNU make than is described in this article, but hopefully it has given you an introduction to how the tool can be used and its capabilities. Make is a powerful tool but can be somewhat cryptic, especially when used in large projects. Command scripts in particular can become hard to read if typical UNIX tools such as sed and awk are used. To help with this situation GNU make has a large number of functions, which can hopefully be used to implement the same functionality. It is therefore worth understanding these functions in more detail.
For more information on GNU make see the references listed below or other articles on this site.
References
- Managing Projects with GNU Make (Nutshell Handbooks)
- The GNU make manual
- Recursive make considered harmful
- John Graham-Cumming's website (with plenty of tips on GNU make)
