Making binaries

Make

The workspace tool (dws) deals with inter-project dependency. Once it has generated a topological ordering of projects to be rebuilt, it invokes the unix make utility for each project in sequence. Thus intra-project dependencies are handled and updated through regular makefiles.

The GNU Make Reference Manual is very well written and contains the detailed information on how make works, thus the place to look when debugging Makefiles. Of course, as a reference manual, it has very little on writing makefiles for large maintainable source code bases.

Makefile patterns

We will focus here on patterns and anti-patterns and the art of writing makefiles. Building packages and managing project dependencies is left for another section.

Path variables

installTop
  • where distributed executables, libraries, etc. are installed

  • objDir
  • where intermediate files are created

  • srcDir
  • where the source files under revision control live

  • Those paths relate to the following commands issued at the shell prompt.

    
    
    cd *objDir*
    *srcDir*/configure --prefix=*installTop*
    make
    make install
          
    

    Writing dependencies

    Makefiles dependencies are written in terms of file path names. A first decision when writing the build infrastructure is thus to decide between using absolute paths, relative paths and relative to which directory.

    Makefile to be executed from anywhere

    
    
    $(objDir)/hello.o: $(srcDir)/hello.c
          
    

    Makefile to be executed from srcDir

    
    
    $(objDir)/hello.o: hello.c
          
    

    Makefile to be executed from objDir

    
    
    hello.o: $(srcDir)/hello.c
          
    

    or

    
    
    vpath %.c $(srcDir)
    
    hello.o: hello.c
          
    

    Trampoline

    In case the intermediate directory is implicit, we need a trampoline to change the directory to objDir before executing make.

    Create a Makefile in the objDir through a configure script

    
    
    cd ${objDir} && ${srcDir}/configure && make
          
    

    Use a pre-existing Makefile from the source tree

    
    
    cd ${objDir} && make -f ${srcDir}/Makefile
          
    

    Invoke make through a wrapper

    
    
    cd ${srcDir} && cmake
          
    

    Use of a trampoline target. This one is high voltige!

    
    
    # $(srcDir)/Makefile
    
    ifeq ($(MAKECMDGOALS),)
    MAKECMDGOALS := all
    endif
    
    ifeq ($(MAKELEVEL),0)
    $(MAKECMDGOALS): trampoline
    endif
    
    trampoline:
    	cd $(objDir) && $(MAKE) -f $(srcDir)/Makefile $(MAKECMDGOALS)
    	process=`ps | grep make | grep -v grep | cut -d ' ' -f 1` \
    	&& kill -3 $$process > /dev/null 2>&1
          
    
    
    
    cd ${srcDir} && make
          
    

    Makefile fragments

    A Makefile can be divided from top to bottom in four sections

    • default variables
    • overridden variables
    • specific rules
    • default rules
    Obviously in large build infrastructures containing multiple Makefiles, it is possible and desirable to factorize the sections containing default variables and rules together in as few files as possible.

    The first overall organization consists of putting the default variables and rules in a common.mk fragment file included in all subsequent Makefiles. The structure of such Makefile then looks like:

    
    
    #  $(srcDir)/Makefile
    
    APPNAME := hello
    
    SRCS    := hello.c
    
    include releng/common.mk
          
    
    
    
    #  releng/common.mk
    
    CC     := gcc
    OBJS   := $(SRCS:.c=.o)
    
    $(APPNAME): $(OBJS)
        $(CC) -o $@ $^
          
    

    This structure works well for projects where all Makefiles builds the same number and kinds of targets. Since the overridden variables appear before the included common.mk fragment, it is thus very important to understand the semantics of the assignment operators (=, :=, ?=). Otherwise some variables might always end up with the default value.

    A second overall organization consists of putting the default variables and rules in a single common Makefile and include a specifics.mk fragment from the source directory.

    
    
    # $(srcDir)/specifics.mk
    
    APPNAME := hello
    
    SRCS    := hello.c
          
    
    
    
    # releng/Makefile
    
    CC     :=  gcc
    
    include $(srcDir)/specifics.mk
    
    OBJS   := $(SRCS:.c=.o)
    
    $(APPNAME): $(OBJS)
        $(CC) -o $@ $^
          
    

    This scheme is very close to the previous one but does not suffer from the default vs. overridden variables order. There is also no need to have an include statement in each source directory fragment since the inclusion order has been reversed in this case.

    As it turns out, in a way or another, we really need two separate fragments, a prefix.mk and suffix.mk. Then it becomes quickly a very complex game of variable expansion and substitution when different Makefiles require to make slightly different targets. The third approach thus leaves the specification of dependencies to the source directory Makefile and gathers the default rules into the common suffix.mk fragment.

    
    
    # $(srcDir)/Makefile
    
    include releng/prefix.mk
    
    bins := hello
    
    hello: hello.o
    
    include releng/suffix.mk
          
    
    
    
    # releng/prefix.mk
    
    CC := gcc
    
    
    # releng/suffix.mk
    
    install: $(bins)
        $(installExecs) $^ $(binDir)
    
    %: %.o
        $(CC) -o $@ $^
          
    

    Default variables and rules

    There are already a lot of built-in variables and rules in make and these can be printed out with a command line option.

    
    
    $ make -p
          
    
    
    
    ...
    COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
    LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)
    ...
    %.o: %.cc
    #  commands to execute (built-in):
            $(COMPILE.cc) $(OUTPUT_OPTION) $<
    
    %: %.o
    #  commands to execute (built-in):
            $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@
    ...
          
    

    Missing dependencies on headers

    All dependencies need to be specified. If there is a missing or implicit dependency, make will fail to update the target when the prerequisite is updated.

    
    
    hello.o: hello.c
          
    

    rewrite as

    
    
    CFLAGS += -MMD
    
    hello.o: hello.c
    
    -include *.d
          
    

    Targets with side effects

    When a command updates multiple files, it creates an implicit dependency on the target which is not specified on the dependency line.

    
    
    A: B
       prog --update STATE -o A B
          
    

    rewrite as

    
    
    STATE: A
    
    A: B
       prog --update STATE -o A B
          
    

    As a corollary, updating the same file multiple times will break the expected static single assignment Makefile structure and definitely confuse make.

    
    
    A: B
       prog --update STATE -o A B
    
    B: C
       prog --update STATE -o B C
          
    

    rewrite as

    
    
    STATE: A
    
    A: B STATE_B
       cp STATE_B STATE
       prog --update STATE -o A B
    
    STATE_B: B
    
    B: C
       prog --update STATE_B -o B C
          
    

    Prerequisites and commands

    When a command input is not derived from the prerequisites, it is possible to get a drift over time and introduce implicit dependencies. As a matter of fact, the best way to prevent such issues popping up is to always specify commands in terms of prerequisites.

    
    
    hello.o: hello.c
       $(CXX) -o hello.o -c hello.c
          
    

    rewrite as

    
    
    hello.o: hello.c
       $(CXX) -o $@ -c $<
          
    

    Another example

    
    
    monthly_results: rent.sub bills.sub sales.add interests.add
       print -o $@ -col1 rent.sub bills.sub -col2 sales.add interests.add
          
    

    rewrite as

    
    
    monthly_results: rent.sub bills.sub sales.add interests.add
       print -o $@ -col1 $(filter %.sub,$^) -col2 $(filter %.add,$^)
          
    

    Yet another example

    
    
    test_unit.log: query clients $(wildcard *.idx)
         ./$< -o $@ --keys=clients $(filter %.idx,$^)
    
    query: query.c
          
    

    rewrite as

    
    
    test_unit.log: query clients $(wildcard *.idx)
         ./$< -o $@ --keys=$(word 2,$^) $(filter %.idx,$^)
    
    query: query.c
          
    

    Bailing out with targets partially built

    Some commands might start to produce a target and fail in the middle of execution, leaving a partial built target file. This is usually the case with commands redirecting output to $@. The first time make bails out with an error but on the second execution make finds an up-to-date file and keeps going happily with that partially built target. It is thus important to delete those partially built targets on errors.

    
    
    test_unit.log: test_unit
         ./$< > $@
          
    

    rewrite as

    
    
    test_unit.log: test_unit
         ./$< > $@ || rm $@
          
    

    Variables substitution and ifeq

    Variable substitutions are very powerful in make. Most times you do need conditional code to set variables based on enumerated values.

    
    
    ifeq ($(DEBUGSET),1)
    BLOCKS := A B C
    else
    BLOCKS := D E F
    endif
          
    

    rewrite as

    
    
    BLOCKS_1 := A B C
    BLOCKS_0 := D E F
    BLOCKS   := $(BLOCKS_$(DEBUGSET))
          
    

    For loops as shell scripts

    Makefiles specify dependencies in static single assignment form. Often times a command written as a for loop shell script is unnecessary and counter-productive in make.

    
    
    test_unit.log: test_unit1 test_unit2
         (for i in $^ ; do \
           ./$$i ; \
         done) > $@ || rm $@
          
    

    rewrite as

    
    
    test_unit.log: test_unit1.log test_unit2.log
        cat $^ > $@ || rm $@
    
    test_unit1.log: test_unit1
    test_unit2.log: test_unit2
    
    %.log: %
           ./$< > $@ || rm $@
          
    

    Substituting commas

    Commas are used as separator in make function calls. Substituting a comma is thus a challenge. Hopefully we can use make variable substitution to still perform the substitution operation.

    
    
    hello.h: block1 block2 block3
    	echo "int blocks[] = { 1, 2, 3 };" > $@ || rm $@
          
    

    rewrite as

    
    
    comma=,
    
    hello.h: block1 block2 block3
    	echo "int blocks[] = { $(patsubst %,%$(comma),$^) };" > $@ || rm $@
          
    

    drop prefix and suffix fragments

    As seen before, common practise to factor makefiles is to devide them in three parts: a prefix that contains some definitions, project-specific declarations and a suffix that contains implicit rules.

    Drop's prefix.mk and suffix.mk define variables and rules to provide compilation of executables and libraries, installation of different files on the local system, packaging of a project for various distributions and running unit tests.

    Using drop to write a project makefile requires to include drop's prefix.mk at the begining of the project makefile and drop's suffix at the end of the project makefile. In order to convieniently support multiple workspaces, it is also required to include dws.mk through a include $(shell dws context) statement as the first line of the project's makefile.

    The install target defined in prefix.mk depends on the content of the bins, scripts, libs and includes variables. The install target also defines the rules to install the generated files in the appropriate system directories.

    bins install -s -p -m 755 binDir
    scripts install -p -m 755 binDir
    libs install -p -m 644 libDir
    includes install -p -m 644 includeDir

    A typical Makefile using the drop prefix and suffixes would thus look like:

    
    
    # -*- Makefile -*-
    include $(shell dws context)
    include $(shareDir)/dws/prefix.mk
    
    bins  := example
    ...
    
    example: example.c
    
    include $(shareDir)/dws/suffix.mk
        
    

    Drop is especially relevent in large cross-platform build systems. By including crossprefix.mk and crosssuffix.mk instead of the usual prefix.mk and suffix.mk, each predefined variable (ie. CC, LD, etc.) is doubled with a host equivalent (ie. CC.host, LD.host, etc.) and the associated rules. The cross*.mk fragments enable building both target and host executables in the same makefile, a feature often required for development of virtual machines.

    The dws integrate command is particularly useful to integrate a patch (under source control) into a source package fetched from a remote machine. Accordingly, dws upstream is used to easily generate patches that can be submitted to upstream maintainers of third-party packages.