Fork me on GitHub

Makefile Quirks

make is a very old and basic task automation tool that has stood the test of time and has aged very well. I believe make offers the perfect balance between simplicity and functionality, and thus it should be the first choice for automation in software projects unless proven otherwise. But despite the initial simplicity, make has grown tremendously through the decades to address the increasing complexity of software projects. So much so that modern make incantations like gmake have quite a steep learning curve. This unplanned and ad-hoc growth has resulted in some quirkiness in make rules, which are specified in a Makefile. One might think it’s time to move on from the 70’s technology to modern automation and build systems, but make ubiquity combined with other tools having their own limitations has kept make attractive even today.

These are some of the quirks that I have encountered while using make. I hope this helps a fellow programmer save some time and frustration:

  • Missing the compulsory tab character in front of all commands in a target definition is a well-known pitfall while writing Makefiles, but I think it’s still worth a mention. A good text editor can help with that.

  • make allows silencing echoing of commands to terminal by prefixing them with an @ sign, but it should be put on the first command, otherwise it wouldn’t have any effect.

  • I usually depend on a shell (bash) to run complicated commands, and bash uses $ to refer to shell variables. But make also uses $ to refer to make variables, so all $ signs in bash variables need to be escaped in a Makefile. Escaping is done by prefixing the dollar sign with another dollar sign:

    echo_home:
            echo $$HOME
    
  • In a makefile we declare target and source files and the steps from which former can be created from the latter. Make enables defining task hierarchies by allowing sources to also be targets of other entries. The way make works is to create a dependency graph for the asked target and for every target run the steps if source files are newer than target files. This is the most important attribute of make since it removes unnecessary builds and speeds up the project build process. But it also means if a target does not create actual files on the file system, make won’t know what to do with it and occasionally skips running the steps altogether. In this scenario, the target needs to be marked with .PHONY pseudo-target to signal make that this target must be run independently and make should not look for a file with the name of the target.

    .PHONY:
    help:
            echo "such and such"
    
  • make allows defining patterned targets, but if they are used a lot, they could become a speed bottleneck, since for every target file, make has to construct all variations of patterns before it can decide what should be run for the target. This is a similar problem to recursive preprocessor definitions in the C language family. The solution is to use :: to define terminal targets if applicable.

    patterned_target: dependencies
    terminal_target:: dependencies
    
  • We can define variables in a Makefile that have newline characters using define directive and referred to using $ notation. Lines can be commented using # character. But if you comment a line that includes a reference to a multi-line variable, make first parses the variable and then the comment! So you might think commenting a line isolates any effects that line might have, but you’d be wrong and scratching your head, and losing precious time because there is a reference to a multiline variable on that line.

  • bash is not the default shell for running make target commands, so it needs to be explicitly defined using the SHELL make variable at the top of the Makefile.

    SHELL=/bin/bash
    
  • By default make runs each command in a separate instance of the shell, which is set by SHELL variable. In this mode make will also check the exit status of each and every command and stop the execution on first non-zero status code (- command modifier or -i command-line argument can be used to change that). This can lead to infinite frustration if you try to treat a Makefile as a shell script because all shell state is lost between consecutive commands. Complex shell logic is best put into an actual shell script and only invoked from the Makefile target.

  • Sometimes it’s desirable to use the .ONESHELL directive on a target to tell make that it needs to be run in a single instance of shell. An example that I can think of is when I need to activate a python virtualenv environment, before running a python command. But there is a catch in using this tempting directive. As these entire commands are passed to a single instance of the shell, make is no longer able to check for the successful execution of the sequence of commands. You have to either remove the .ONESHELL directive or manually check for the exit status of all commands that might fail. This is an example that I use for creating a Python virtualenv and keeping it updated after changes in requirements.txt file using make:

    venv: venv/bin/activate
    
    .ONESHELL:
    venv/bin/activate: requirements.txt
            test -d venv || virtualenv --python=`which python3` venv
            source venv/bin/activate
            pip install --upgrade -r requirements.txt
            touch venv/bin/activate
    

Social