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.
Makefiles are also 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).