Makefile Basics

Have you ever compiled something from source? If so, you have likely used a Makefile. Have you ever written a Makefile? Makefiles are extremely powerful, but not the most intuitive. In this post, I would like to talk a bit about the lessons I have learned after needing to become knowledgable about Makefiles. Read on to learn more!

Why Makefiles

While Makefiles can be used for many purposes, they are optimized for compiling code. The offer many benefits that traditional programming languages do not natively support (though could be built manually). Two big ones are:

  • Only recreate dependencies that are newer than the file be created
  • Makefiles are testable — they will stop if missing a step and you can delete and start fresh to confirm workflows

Let’s look at an example Makefile:

$ cat Makefile
CFLAGS=-O2

all: helloworld

helloworld: ui.o xml.o mailcomponent.o
   $(CC) $(CFLAGS) -o $@ [email protected] $^
$ make
cc -O2 -c ui.c
cc -O2 -c xml.c
cc -O2 -c mailcomponent.c
cc -O2 -o helloworld helloworld.c ui.o xml.o mailcomponent.o

In addition, if say compiling xml.c fails, the Makefile will stop there with an error. You could of course write similar functionality in the language of your choice, but then you would need to handle dependency checking, error handling and more yourself.

Basics

Targets

Let’s start simple. A Makefile is literally a file named Makefile in a directory. The file is made up of one or more targets. A target is a name that does not start with a period followed by a colon with the target definition defined starting with a tab (not space). For example:

$ cat Makefile
test:
   @echo "Hello world!"
$ make
Hello world!
$ make test
Hello world!

A couple of notes about targets:

  • The first defined target will be called if make is not passed any target name
  • It is common that a target named all be created which is responsible for building and publishing (not required, just a common practice)

Variables

If you want to define a variable, you can either do that outside of a target, meaning it is applicable to all targets in the Makefile or within a target meaning it is applicable to the line it was defined in within the target. For example:

$ cat Makefile
X=123

test:
   Y=abc
   @echo "X = ${X} and Y = ${Y}"
$ make test
X = 123 and Y =

Wait, why did the value of the Y variable not echo? Because the echo was not on the same line as the Y definition. You see, in a Makefile every line in a target is basically its own shell (this does not apply to things outside of a target like the X declaration). So how can you make variable declarations you can use within a target? You have two options:

  1. Write single line commands. For example:
    $ cat Makefile
    X=123
    
    test:
       Y=abc; \
       @echo "X = ${X} and Y = ${Y}"
    $ make test
    X = 123 and Y = abc
  2. Use multiple targets and pass variables between them
    $ cat Makefile
    X=123
    
    y:
       @echo "Y = ${Y}"
    
    test:
       @echo "X = ${X}"
       make y Y=abc
    $ make test
    X = 123
    Y = abc

One other thing to be aware of with variable declaration is that a variety of different operators are available. For example:

$ cat Makefile
A = 1
B ?= 2
C := 3

test:
   @echo "A = ${A} and B = ${B} and C = ${C}"
$ make test
A = 1 and B = 2 and C = 3

Equal defines the variable statically while question mark equal sets the variable only if empty or null while colon equal sets the variable based on its definition when the variable is called/expanded.

$ make test B=4
A = 1 and B = 4 and C = 3

You may have noticed that when calling variables I have wrapped them in curly brackets. Those familiar with shell scripting may know that wrapping variable names may be required when attempting to call a variable separate from say a string definition. In Makefiles, you should always use either curly brackets or parenthesis around variables when attempting to get their value. While the above examples will work without curly brackets or parenthesis, any variable name with more than one character will not work without them. For example:

$ cat Makefile
FOO=123

test:
   BAR=abc; \
   @echo "FOO = $FOO and BAR = $BAR"
$ make test
FOO = OO and BAR = AR

You may be wondering why there is an at sign before the echo command. The reason for this is because by default, Makefiles default to /bin/sh -x which prints everything it does before it does it. If the at sign were removed, the output of the previous example would look like:

$ cat Makefile
X=123

test:
   Y=abc; \
   echo "X = $X and Y = $Y"
$ make test
echo "X = $X and Y = $Y"
X = 123 and Y = abc

In general, printing a command before it is run is a best practice as it can help with troubleshooting and also provide an audit trail of what was done when run against a CI/CD system such as Jenkins.

Shell

One final thing to call out in the basics section is if you happen to want a different shell then /bin/sh, you can define this with the SHELL variable. For example:

$ cat Makefile
SHELL=/bin/bash

Intermediates

Non-target definitions

You can define loops and functions outside of targets using native Makefile constructs. For example:

$ cat Makefile
A = 123

ifeq (${A},123)
B = true
else
B = false
endif

define C
    echo ${B}
endef

test:
    @$(call C)
$ make
true

Special variables

There are some special variables to be aware of including (note there are seven, but I cover the most common three):

  • $@ the name of the target currently running in
  • $* the stem of the target filename
  • $< the name of any prerequisite targets

For example:

$ cat Makefile
YAML=service deployment

all: $(addprefix test-,${YAML})

prereq:
   @echo "Run prereq!"
   test-%: prereq
   @echo "Target = $@, YAML value = $* and prerequisite = $<"
$ make
Run prereq!
Target = test-service, YAML value = service and prerequisite = prereq
Target = test-deployment, YAML value = deployment and prerequisite = prereq

It is also worth calling out wildcards here. As you can see in the above examples, % is a stem wildcard match. Other more common shell wildcard matches exists as well. In addition, make has an internal wildcard function which can be used outside of targets (e.g. ifeq or define).

Special targets

There are several special targets (start with a period). Here are a few to be aware of:

  • .PHONY: Ignore any file by the same name as the target. For performance reasons you should always mark target names as PHONY.
  • .EXPORT_ALL_VARIABLES: By default, defined variables are not exported. For shell commands to be able to resolve Makefile variables you can either manually export them or automatically export all of them with this option.
  • .ONESHELL: As mentioned earlier, every line in a Makefile target represents a new shell. This option can be used to change the behavior.

Ignoring errors

A Makefile will stop if it experiences any errors (i.e. any exit code that is not zero). Sometimes, you want a Makefile to continue on error. This can be achieved by prefixing the command with the minus sign (you could also prepend || true instead). For example:

$ cat Makefile
all: test test2

test:
   -$$(cat blah)
   @echo "Still going"

test2:
   $$(cat blah)
   @echo "Never printed"
$ make test
$(cat blah)
Still going
cat: blah: No such file or directory
$ make test2
$(cat blah)
make: [test] Error 1 (ignored)
cat: blah: No such file or directory
make: *** [test2] Error 1

Summary

While I have only scratched the service of what is possible with Makefiles, the above information should be more than enough to get you started. In a future post, I will provide a cheat sheet with more information. The key takeaways are:

  • Makefiles default to /bin/sh -x. Commands can be prefixed with the at sign to prevent echoing before being run.
  • Variables can be defined outside of targets to apply globally to a Makefile. These variables can be statically defined, defined only if empty/null or expanded on definition.
  • Targets start with a name followed by a colon, may contain one or more prerequisites and/or variable definitions
  • Everything within a target is TAB indented (not space)
  • Every line within a target is its own shell. If you want to run multiple command in the same shell, it must be written as a single line command
  • Variables called within targets should be wrapped in curly brackets or parenthesis

© 2018, Steve Flanders. All rights reserved.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top