paritybit.ca

Make

Make is a build system that was created to ease the pain of building larger software projects. It is useful for building different parts of a project in parallel so that it builds faster, and it allows you to change small parts of your project at a time without needing to recompile the whole project to test your changes.

Depending on what you're doing, a POSIX shell script that calls build tools directly might be preferable to a makefile. It's certainly simpler and generally more portable.

The make command will look for a file called makefile to find rules. If that file is not found, then it will look for Makefile.

It is also common to have most variable declarations in a config.mk file so that the makefile contains only rules for building the software.

A lot of Makefile syntax is unfortunately not standard. This is probably because the standard is extremely limited. This has led to different operating systems having different syntax and conventions, depending on which version of make they use by default. The three major implementations that you’ll likely need to concern yourself with is BSD make syntax, GNU make syntax, and Windows make syntax. The different BSDs do have their own extensions, but they’re usually not major. If you’re on a BSD, GNU make is usually called gmake. Likewise, if you’re on Linux, BSD make is usually called bmake.

These differences and limitations are what spawned large scripts that generate makefiles (e.g. the autoconf program or configure scripts) and other build systems such as cmake.

Makefiles consist of a set of rules and variables that describe how to build a software project.

In general, rules are formatted like so:

target: [prerequisites ...]
    [commands ...]

The target describes what to build, the prerequisites describe what is required to build that target, and the commands describe how to build the target.

Commands must be indented by a tab character.

There is a special target, all, which will run the specified prerequisites no matter what make is told to do:

all: target1 target2

There is also the .PHONY target directive which tells make to treat the specified target as just a name that refers to a set of commands instead of an actual file on the filesystem to build or look for. This is most commonly used for the clean target to list commands to clear out the stuff that was compiled:

.PHONY clean
clean:
    rm -f *.o program

Some will also include an install target to specify commands that will take a compiled program and put it and any supporting files like documentation and examples where they need to be on the filesystem. It’s hard to make this portable without some kind of OS-detection script though, so I tend to leave it out.

A really simple makefile to compile a project with the source files main.c, hello.c, and hello.h would look something like:

hello: hello.o main.o
    cc hello.o main.o -o hello

hello.o: hello.c
    cc -c hello.c

main.o: main.c
    cc -c main.c

.PHONY clean
clean:
    rm -f *.o hello

Which will create main.o, hello.o, and finally the executable hello.

This can be made much more general with the use of variables and make’s builtin rules.

In general, variables are set as you would expect:

var = value

Although there are a number of variable assignment operators which do different things:

Variables are used as part of rules or in variable assignments by surrounding them with braces or parentheses and putting a dollar sign in front, like so:

var2 := $(VAR)

There are a number of standard variables used in Makefiles:

These variables are used by the automatic rules which you can examine with the command make -p. They contain default behaviour which means we could take the makefile example above and condense it down to just:

hello: hello.o main.o

In fact, if all you had was a main.c, you could even compile your executable by just calling:

$ make main

on the command line, which will make an executable called main from the main.c file.

These variables are also expected by and helpful for maintainers trying to port your software to different systems, since they can override values by specifying them on the command line. Using these expected variables instead of making up your own makes this easier for them. For example:

$ make CC=clang

Will tell the makefile to explicitly build the project using clang.

Check the documentation for the version of make you're using for more information on its particular extensions and syntax.

All that being said, here is a relatively simple makefile for compiling C programs that makes use of a lot of what was discussed above in order to be generally useful:

PROG = foo

CFLAGS = -std=c89 -Wall -Wextra -Wpedantic -Wshadow -Wvla\
     -Walloca -Wconversion -Wdeprecated -Wpointer-arith\
     -Wstrict-prototypes -Wmissing-prototypes -Wdouble-promotion\
     -fsanitize=undefined,address -fsanitize-trap=undefined

SRCS != find src -name '*.c'

$(PROG): $(SRCS)
    $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(LDLIBS) $(SRCS) -o $@

.PHONY: clean
clean:
    rm -f $(PROG)

This is the one I use by default for many of my projects.

This makefile will compile a program with name foo using all the *.c files found under the src directory. This skips making separate object files for each translation unit—which means that everything will be re-compiled even if one file changes—but it greatly simplifies the makefile and is suitable for small-to-medium size projects.

See Clang Diagnostic Flags for information about the various warnings defined in CFLAGS.

Advanced Makefile - BSD Make

Here is an example of a more advanced makefile using BSD make syntax:

PROG = $(.CURDIR:T)

CFLAGS = -std=c89 -Wall -Wextra -Wpedantic -Wshadow -Wvla\
     -Walloca -Wconversion -Wdeprecated -Wpointer-arith\
     -Wstrict-prototypes -Wmissing-prototypes -Wdouble-promotion\
     -fsanitize=undefined,address -fsanitize-trap=undefined

.ifmake debug
CFLAGS += -O0 -g3
CPPFLAGS += -DDEBUG
.else
CFLAGS += -Oz
.endif

SRCS != find ../src -name '*.c'
OBJS := $(SRCS:%.c=%.o)

$(PROG): $(OBJS)
    $(CC) $(LDFLAGS) $(LDLIBS) $(OBJS:../src/%=%) -o $@

.o:
    $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

.PHONY: debug
debug: $(PROG)

.PHONY: clean
clean:
    rm -f ./*

This makefile compiles object files and the executable in a separate directory. This directory must exist before running make By default it is ./obj/, but this can be changed with the environment variable MAKEOBJDIR. Additionally, the .ifmake debug section allows you to type make debug in order to compile the program with debugging options enabled. Also, instead of specifying PROG manually, we use the builtin variable .CURDIR combined with a substitution to automatically give our executable the same name as the directory the makefile is in.

BSD systems also tend to have a collection of macros under /usr/share/mk/ which can be relied upon for a much simpler makefile, though this is obviously not very portable.

PROG = foo

.include <bsd.prog.mk>

Advanced Makefile - GNU Make

The equivalent to the previous section for GNU make would look something like:

PROG = foo

CFLAGS = -std=c89 -Wall -Wextra -Wpedantic -Wshadow -Wvla\
     -Walloca -Wconversion -Wdeprecated -Wpointer-arith\
     -Wstrict-prototypes -Wmissing-prototypes -Wdouble-promotion\
     -fsanitize=undefined,address -fsanitize-trap=undefined

SRCS != find src -name '*.c'
OBJS := $(SRCS:%=obj/%.o)

obj/$(PROG): $(OBJS)
    $(CC) $(LDFLAGS) $(LDLIBS) $(OBJS) -o $@

obj/%.c.o: %.c
    $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

.PHONY: debug
debug: CFLAGS += -O0 -g3
debug: CPPFLAGS += -DDEPENDS
debug: $(PROG)

.PHONY: clean
clean:
    rm -rf obj/*

Which requires the ./obj/src directory to exist and will create object files under that directory with the executable itself under the ./obj/ directory. Also, object files will have a .c.o extension.

There is most certainly a nicer/more idiomatic way to do this but I’ll leave that as an exercise to the reader (I intend to avoid using GNU make).

Makefiles are certainly much simpler if you don’t care about doing out-of-source builds (i.e. object files and the executable are in the same directory as the source files).

Other Resources