1.16. Building a Static Library with GNU Make

Problem

You want to use GNU make to build a static library from a collection of C++ source files, such as those listed in Example 1-1.

Solution

First, create a makefile in the directory where you want your static library to be created, and declare a phony target all whose single prerequisite is the static library. Next, declare your static library target. Its prerequisites should be the object files that the library will contain, and its command script should be a command line to build the library from the collection of object files, as demonstrated in Recipe 1.3. If you are using GCC or a compiler with similar command-line syntax, customize the implicit patterns rules, if necessary, by modifying one or more of the variables CXX, CXXFLAGS, etc. used in make's database of implicit rules, as shown in Recipe 1.15. Otherwise, write a pattern rule telling make how to compile .cpp files into object files, using the command lines from Table 1-4 and the pattern rule syntax explained in Recipe 1.16. Next, declare targets indicating how each of your library’s source files depends on the headers it includes, directly or indirectly. You can write these dependencies by hand or arrange for them to be generated automatically. Finally, add install and clean targets as demonstrated in Recipe 1.15.

For example, to build a static library from the source files listed in Example 1-2 using GCC on Unix, create a makefile in the directory johnpaul, as shown in Example 1-20.

Example 1-20. Makefile for libjohnpaul.a using GCC on Unix

# Specify extensions of files to delete when cleaning
CLEANEXTS   = o a 

# Specify the target file and the install directory
OUTPUTFILE  = libjohnpaul.a
INSTALLDIR  = ../binaries

# Default target
.PHONY: all
all: $(OUTPUTFILE)

# Build libjohnpaul.a from john.o, paul.o, and johnpaul.o
$(OUTPUTFILE): john.o paul.o johnpaul.o
    ar ru $@ $^
    ranlib $@

# No rule to build john.o, paul.o, and johnpaul.o from .cpp 
# files is required; this is handled by make's database of
# implicit rules

.PHONY: install
install:
    mkdir -p $(INSTALLDIR)
    cp -p $(OUTPUTFILE) $(INSTALLDIR)

.PHONY: clean 
clean:
    for file in $(CLEANEXTS); do rm -f *.$$file; done

# Indicate dependencies of .ccp files on .hpp files
john.o: john.hpp
paul.o: paul.hpp
johnpaul.o: john.hpp paul.hpp johnpaul.hpp

Similarly, to build a static library using Visual C++, your makefile might look as shown in Example 1-21.

Example 1-21. Makefile for libjohnpaul.lib using Visual C++

# Specify extensions of files to delete when cleaning
CLEANEXTS      = obj lib 

# Specify the target file and the install directory
OUTPUTFILE     = libjohnpaul.lib
INSTALLDIR     = ../binaries

# Pattern rule to build an object file from a .cpp file
%.obj: %.cpp
    "$(MSVCDIR)/bin/cl" -c -nologo -EHsc -GR -Zc:forScope -Zc:wchar_t \
            $(CXXFLAGS) $(CPPFLAGS) -Fo"$@" $<

# Default target
.PHONY: all
all: $(OUTPUTFILE)

# Build libjohnpaul.lib from john. obj, paul. obj, and johnpaul. obj
$(OUTPUTFILE): john.obj paul.obj johnpaul.obj
    "$(MSVCDIR)/bin/link" -lib -nologo -out:"$@" $^

.PHONY: install
install:
    mkdir -p $(INSTALLDIR)
    cp -p $(OUTPUTFILE) $(INSTALLDIR)

.PHONY: clean 
clean:
    for file in $(CLEANEXTS); do rm -f *.$$file; done

# Indicate dependency of .cpp files on .hpp files
john.obj: john.hpp
paul.obj: paul.hpp
johnpaul. obj: john.hpp paul.hpp johnpaul.hpp

Tip

In Example 1-21, I’ve expressed Visual C++’s link.exe command as "$(MSVCDIR)/bin/link“, using the environment variable MSVCDIR set by vcvars32.bat. This prevents confusion between the Visual C++ linker and the Unix link command, supported by Cygwin and MSYS. For consistency, I’ve also expressed Visual C++’s compile command using MSVCDIR.

Discussion

Let’s walk through Example 1-20. I start by defining variables to represent the output file, the install directory, and the extensions of files that should be deleted when the target clean is built. Next, I declare the default target all, as in Example 1-14.

The rule to build the static library looks like this:

$(OUTPUTFILE): john.o paul.o johnpaul.o
    ar ru $@ $^
    ranlib $@

It’s a straightforward adaptation of the entry for GCC in Table 1-10. Here $(OUTPUTFILE) and $@ both expand to libjohnpaul.a, and $^ expands to the list of prerequisites john.o paul.o johnpaul.o.

The next two rules declare install and clean targets, as in Recipe 1.15. The only difference is that in Example 1-20 I use a shell looping construct to remove all files whose extension appears in the list o a — i.e., all object or static library files:

for file in $(CLEANEXTS); do rm -f *.$$file; done

I’ve used a double dollar sign to prevent make from expanding the variable $$file rather than passing it on to the shell.

The last three rules specify the dependency relationships between the library’s .cpp files and the headers they include. There’s one rule for each .cpp file; its target is the object file to be built from the .cpp file, and its prerequisites are the header files included—directly or indirectly—by the .cpp file:

john.o: john.hpp
paul.o: paul.hpp
johnpaul.o: john.hpp paul.hpp johnpaul.hpp

This can be understood as follows. If a .cpp file includes a header file—directly or indirectly—it must be rebuilt each time the header is modified. However, since the .cpp file exists and does not appear as the target of any rule, it is never out of date, as discussed in Recipe Recipe 1.15. Consequently, when the header is modified, no recompilation is triggered. The fix is to declare a rule making these dependencies explicit; whenever one of the headers in question is modified, the object file corresponding to the .cpp will become out of date, causing the .cpp file to be recompiled.

This solution is only adequate for very small projects, since it’s extremely difficult to keep the targets representing source file dependencies synchronized with a changing codebase. Fortunately, there are several methods for generating these dependencies automatically. For example, you can replace the last three rules in Example 1-20 with the following:

# Generate dependencies of .ccp files on .hpp files
include john.o paul.o johnpaul.o

%.d: %.cpp
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

This bit of code relies on the compiler option -M which causes GCC to output dependency information for inclusion in a makefile. For a detailed explanation of how it works—and why it’s sometimes inadequate—see Managing Projects with GNU make, Third Edition, by Robert Mecklenburg (O’Reilly).

Tip

Put the code to generate dependencies at the end of your makefile.

This method can be adapted to work with most toolsets, since most compilers provide an option similar to GCC’s -M; in fact, the option is usually either -M or -m. Visual C++, however, does not provide an option for generating makefile dependencies. If you use Visual C++, you have two choices. You can use the -Gm option, together with one of the options -Zi or -ZI, discussed in Recipe 1.16. The -Gm option tells the compiler to build a database, stored in a file with the extension idb, containing information about dependencies between source files. The .idb file is created when a .cpp file, or collection of .cpp files, is initially compiled. On subsequent compilations, only those source files which have been modified or which depend on headers which have been modified are recompiled.

Alternatively, you can use the -showIncludes option, together with the option -E. The -showIncludes option causes the compiler to output a message to standard error each time an include directive is encountered. The -E option tells the compiler to run the preprocessor and then exit, without building any binary files. Using a bit of shell scripting, you can use the output generated by -showIncludes to construct makefile dependencies:

include john.d paul.d johnpaul.d

%.d: %.cpp
    "$(MSVCDIR)/bin/cl" -E -showIncludes $< 2> $@.$$$$ > /dev/null; \
    sed -n 's/^Note: including file: *\(.*\)/$*.obj·$*.d:\1/gp' \
        < $@.$$$$ | sed 's:\\:/:g;s: :\\ :gp' > $@;             \                           
    rm -f $@.$$$$

In this example, the character · represents a Tab.

Let’s make one last improvement to Example 1-20. Currently, the sequence john paul johnpaul occurs in two places; in the prerequisites of the rule to build the static library, and in the include directive used to generate dependencies. If the list of source files changes, you’ll have to update the makefile in two locations. It’s better to define a variable SOURCES, and to replace both occurrences of the sequence john paul johnpaul with expressions involving SOURCES:

SOURCES = john.cpp paul.cpp johnpaul.cpp
...
# Build libjohnpaul.a from john.o, paul.o, and johnpaul.o
$(OUTPUTFILE): $(subst .cpp,.o,$(SOURCES))
    ar ru $@ $^
    ranlib $@
...
    
# Generate dependencies of .ccp files on .hpp files
include $(subst .cpp,.d,$(SOURCES))

%.d: %.cpp
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

Here I’m using the make function $(subst x,y,str), which replaces x with y everywhere in str.

Tip

GNU make supports a rich collection of functions for string and filename manipulation and more. It also supports user defined functions. As usual, for a thorough treatment, see Managing Projects with GNU make, Third Edition, by Robert Mecklenburg (O’Reilly).

Get C++ Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.