1.18. Building a Complex Application with GNU make

Problem

You wish to use GNU make to build an executable which depends on several static and dynamic libraries.

Solution

Follow these steps:

  1. Create makefiles for the libraries used by your application, as described in Recipe 1.16 and Recipe 1.17. These makefiles should reside in separate directories.

  2. Create a makefile in yet another directory. This makefile can be used to build your application, but only after the makefiles in step 1 have been executed. Give this makefile a phony target all whose prerequisite is your executable. Declare a target for your executable with prerequisites consisting of the libraries which your application uses, together with the object files to be built from your application’s .cpp files. Write a command script to build the executable from the collection libraries and object files, as described in Recipe 1.5. If necessary, write a pattern rule to generate object files from .cpp files, as shown in Recipe 1.16. Add install and clean targets, as shown in Recipe 1.15, and machinery to automatically generate source file dependencies, as shown in Recipe 1.16.

  3. Create a makefile in a directory which is an ancestor of the directories containing all the other makefiles — let’s call the new makefile the top-level makefile and the others the subordinate makefiles. Declare a default target all whose prerequisite is the directory containing the makefile created in step 2. Declare a rule whose targets consists of the directories containing the subordinate makefiles, and whose command script invokes make in each target directory with a target specified as the value of the variable TARGET. Finally, declare targets specifying the dependencies between the default targets of the subordinate makefiles.

For example, to build an executable from the source files listed in Example 1-3 using GCC on Unix, create a makefile as shown in Example 1-23.

Example 1-23. Makefile for hellobeatles.exe using GCC

# Specify the source files, target files, the build directories, 
# and the install directory 
SOURCES         = hellobeatles.cpp
OUTPUTFILE     = hellobeatles
LIBJOHNPAUL     = libjohnpaul.a
LIBGEORGERINGO = libgeorgeringo.so
JOHNPAULDIR     = ../johnpaul
GEORGERINGODIR     = ../georgeringo
INSTALLDIR     = ../binaries

#
# Add the parent directory as an include path 
#
CPPFLAGS      += -I..

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

#
# Target to build the executable. 
#
$(OUTPUTFILE): $(subst .cpp,.o,$(SOURCES))  \
               $(JOHNPAULDIR)/$(LIBJOHNPAUL) \
               $(GEORGERINGODIR)/$(LIBGEORGERINGO)
    $(CXX) $(LDFLAGS) -o $@ $^

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

.PHONY: clean 
clean: 
    rm -f *.o
    rm -f $(OUTPUTFILE)

# 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 $@.$$$$

Next, create a top-level makefile in the directory containing johnpaul, georgeringo, hellobeatles, and binaries, as shown in Example 1-24.

Example 1-24. Top level makefile for the source code from Example 1-1, Example 1-2, and Example 1-3

# All the targets in this makefile are phony
.PHONY: all johnpaul georgeringo hellobeatles

# Default target
all: hellobeatles

# The targets johnpaul, georgeringo, and hellobeatles represent
# directories; the command script invokes make in each directory
johnpaul georgeringo hellobeatles:
    $(MAKE) --directory=$@ $(TARGET) 

# This rule indicates that the default target of the makefile
# in directory hellobeatles depends on the default targets of 
# the makefiles in the directories johnpaul and georgeringo
.PHONY: hellobeatles
hellobeatles: johnpaul georgeringo

To build hellobeatles, change to the directory containing the top-level makefile, and enter make. To copy the files libjohnpaul.a, libgeorgeringo.so, and hellobeatles to the directory binaries, enter make TARGET=install. To clean the project, enter make TARGET=clean.

Discussion

The approach to managing complex projects demonstrated in this recipe is known as recursive make. It allows you to organize a project into a collection of modules, each with its own makefile, and to specify the dependencies between the modules. It’s not limited to a single top-level makefile with a collection of child makefiles: the technique can be extended to handle multi-level tree structures. While recursive make was once the standard technique for managing large projects with make, there are other methods which are now considered superior. For details, refer once again to Managing Projects with GNU make, Third Edition, by Robert Mecklenburg (O’Reilly).

Example 1-23 is a straightforward application of the techniques demonstrated in Recipe 1.15, Recipe 1.16, and Recipe 1.17. There’s really just one interesting point. As illustrated in Recipe 1.15, when compiling hellobeatles.cpp from the command line it’s necessary to use the option -I.. so that the compiler can find the headers johnpaul.hpp and georgeringo.hpp. One solution would be to write an explicit rule for building hellobeatles.o with a command script containing the option -I.., like so:

hellobeatles.o: hello.beatles.cpp
    g++ -c -I.. -o hellobeatles.o hellobeatles.cpp

Instead, I’ve taken advantage of the customization point CPPFLAGS, described in Recipe 1.15, to specify that whenever an object file is compiled from a .cpp file, the option -I.. should be added to the command-line:

CPPFLAGS      += -I..

I’ve used the assignment operator +=, instead of =, so that the effect will be cumulative with whatever value of CPPFLAGS may have been specified on the command line or in the environment.

Now let’s look at how Example 1-24 works. The most important rule is the one which causes make to be invoked in each of the directories johnpaul, georgeringo, and hellobeatles:

johnpaul georgeringo hellobeatles:
    $(MAKE) --directory=$@ $(TARGET)

To understand this rule, you need to know three things. First, the variable MAKE expands to the name of the currently running instance of make. Usually this will be make, but on some systems it could be gmake. Second, the command line option —directory=<path> causes make to be invoked with <path> as its current directory. Third, a rule with several targets is equivalent to a collection of rules, each having one target, and having identical command scripts. So the above rule is equivalent to:

johnpaul:
    $(MAKE) --directory=$@ $(TARGET)

georgeringo:
    $(MAKE) --directory=$@ $(TARGET)

hellobeatles:
    $(MAKE) --directory=$@ $(TARGET)

This in turn is equivalent to:

johnpaul:
    $(MAKE) --directory=johnpaul $(TARGET)

georgeringo:
    $(MAKE) --directory=georgeringo $(TARGET)

hellobeatles:
    $(MAKE) --directory=hellobeatles $(TARGET)

The effect of the rule, therefore, is to invoke the makefiles in each of the directories johnpaul, georgeringo, and hellobeatles, with the value of the variable TARGET tacked onto the command line. As a result, you can build target xxx of each of the subordinate makefiles by executing the top-level makefile with the option TARGET=xxx.

The final rule of the makefile ensures that the subordinate makefiles are executed in the correct order; it simply declares that the target hellobeatles depends on the targets johnpaul and georgeringo:

hellobeatles: johnpaul georgeringo

In a more complex application, there might be many dependencies between the executable and its component libraries. For each such component, declare a rule indicating the other components on which it directly depends.

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.