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:
=: Assign the value to the variable. Any previous value is overridden.:=: Assign the value to the variable after expanding it (i.e. performing substitution rules, replacing variable names with their values, etc.)+=: Append the value to the current value of the variable.?=: Assign the value to the variable only if it is not already defined.!=: Expand the value, pass it to the shell for execution, and set the variable to the result of that execution.
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:
CC: The compiler to useCFLAGS: Flags passed to the C compilerCPPFLAGS: Flags passed to the compiler pre-processorCXXFLAGS: Flags passed to the C++ compilerLDFLAGS: Flags passed to the linkerLDLIBS: External libraries to link with your program
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).