I often use Makefiles in some of my projects.
I really like the flexibility it gives, and I often find myself writing a Makefile instead of a simple shell script to automatize tasks.
So here's a little crash course.
I'll obviously only cover the basics, but I hope this will give you a good idea on how you could improve your workflows using Makefiles.
Make was developed in 1976 mainly as a build automation tool, to produce executable files or libraries from source code.
While it excels as a build system, it can also be used for a lot of different things.
If you do write shell scripts to automatize certain tasks, you'll be able to use Makefiles instead.
As we're going to see, a Makefile can have several advantages over a regular shell script.
This tutorial will only be focused on the GNU version of Make, as it's the most widely used and the most powerful.
First of all, when you invoke the make
command, it will look for a file named Makefile
in the current working directory.
This is the default, but note that a specific Makefile can be used with the -f
flag, followed by the file name or path.
If such a file is found, it will by default execute the all
target.
Make is target-based system.
Your Makefile can specify multiple targets, and targets may be executed individually when invoking make
. But more on this later.
For now, we'll just start by creating a basic hello world example.
In some directory, create a file called Makefile
with the following content:
all: echo "hello, world"
all
is the target name. Target definitions are followed by a colon sign.
As mentioned earlier, make
will by default look for a target called all
. So this is our main entry point.
Inside the target, you'll simply execute shell commands.
Here, we print the hello, world string, using the shell's builtin echo
command.
Note that target commands need to be indented with at least a single tab.
While spaces can be used elsewhere for indentation, tabulation is mandatory inside a target.
Now from a command prompt, cd
to that directory and type make
.
make
will read the Makefile
, and execute the all
target, giving the following output:
echo "hello, world" hello, world
As you can see, make
will first print the full command, before printing any output.
This can be disabled by using an @
sign before the command:
all: @echo "hello, world"
Now the output is simply:
hello, world
You can define as many targets as you want.
For instance:
all: @echo "hello, world" foo: @echo "hello, foo" bar: @echo "hello, bar"
While invoking make
will still only execute the all
target, the foo
or bar
targets can be executed individually by specifying their names:
$ make hello, world $ make foo hello, foo $ make bar hello, bar
A target may depend on another target, or on multiple other targets.
This is called a prerequisite.
Prerequisites follows the target name:
foo: bar @echo "hello, foo"
Here, the foo
target depends on bar
. This means that when foo
is about to be executed, bar
will be executed first.
Multiple prerequisites are simply separated by a space:
foo: bar all @echo "hello, foo"
Here, upon executing foo
, make
will start by executing bar
, then all
, and finally foo
.
And obviously, chaining works too:
all: foo @echo "hello, world" foo: bar @echo "hello, foo" bar: @echo "hello, bar"
all
depends on foo
, which depends on bar
. So when invoking make
, you'll get the following output:
hello, bar hello, foo hello, world
And you can also manually execute foo
by typing make foo
, which will give:
hello, bar hello, foo
A very nice thing about make
is that it does error handling for you.
If a command returns a non-zero exit status, make
will report the error and abort execution.
This means that if a command fails inside some target (which may be a prerequisite of another target), the whole execution will stop.
So you don't have to do any manual error checking, as you would/should do with a shell script.
For instance:
all: foo @echo "hello, world" foo: bar @echo "hello, foo" bar: @echo "Executing false" @false @echo "hello, bar"
Note that in the bar
target, we execute the shell's false
command, which always returns a non-zero exit status.
Now if we invoke main
, we'll get the following output:
Executing false make: *** [bar] Error 1
make
will execute all
, which needs to execute foo
, which needs to execute bar
.
bar
will print the first message, and then execute the false
command.
As it returns a non-zero exit status, this is detected as an error, and execution is stopped.
The remaining message in bar
will not be printed, and the foo
and all
targets won't be executed.
Also note that you can obtain detailed informations about how make
reads your Makefile using the --debug
flag.
With the previous example:
$ make --debug Reading makefiles... Updating goal targets.... File `all' does not exist. File `foo' does not exist. File `bar' does not exist. Must remake target `bar'. Executing false make: *** [bar] Error 1
You can also define variables inside your Makefile.
Variables are defined outside targets, and can be referred to with a $
sign and parenthesis:
HELLO := hello, world all: @echo "$(HELLO)"
Variables may also be overridden when invoking make
, giving extra flexibility.
For instance, with the example above:
$ make HELLO="This is a test" This is a test
We'll cover more about variables later.
Now that we have covered the basics, let's take a more useful example.
We'll create a simple build system for the C programming language.
The goal is to compile C source files, and to produce an executable.
We'll start by a very simple build system, and work on it step by step to achieve a more generic one.
Here's the basic project structure:
build
(directory)Makefile
source
(directory)
main.c
We have a build
directory for the final executable and temporary files, the Makefile, and a source
directory with a single main.c
file.
The main.c
file is a basic hello world program:
#include <stdio.h> int main( void ) { printf( "hello, world\n" ); return 0; }
We'll start with a very simple Makefile
that invokes the clang
C compiler.
You can obviously replace it with gcc
if you want:
all: @clang -Wall -Werror source/main.c -o build/main
When invoking make
, it will compile the source/main.c
and produce an executable in build/main
.
Dead simple.
Now let's say we want to compile multiple C files to produce the executable.
We'll first create a function named hello
in the source/hello.c
file:
#include <stdio.h> #include "hello.h" void hello( void ) { printf( "hello, world\n" ); }
And we'll also add the corresponding header in source/hello.h
with the function prototype:
#ifndef HELLO_H #define HELLO_H void hello( void ); #endif
Our main.c
file will then call the hello
function:
#include "hello.h" int main( void ) { hello(); return 0; }
Now the Makefile
could simply be:
all: @clang -Wall -Werror source/hello.c source/main.c -o build/main
However, this is not really flexible, and this is usually not how individual files are compiled.
Instead, we'll produce an object file for each C source file, and link them together to produce the final executable:
all: @clang -Wall -Werror -c source/hello.c -o build/hello.o @clang -Wall -Werror -c source/main.c -o build/main.o @clang -Wall -Werror build/hello.o build/main.o -o build/main
Note the additional -c
flag, needed to tell the compiler to produce an unlinked object file, instead of an executable.
But we obviously want the compilation to happen in separate targets, so we'll create a specific target for each C source file.
The all
target will depend on these, and be responsible for linking the executable:
all: main hello @clang -Wall -Werror build/hello.o build/main.o -o build/main main: @clang -Wall -Werror -c source/main.c -o build/main.o hello: @clang -Wall -Werror -c source/hello.c -o build/hello.o
Also notice that the compiler flags (-Wall -Werror
) are now repeated in each target.
Time to create a variable:
CFLAGS := -Wall -Werror all: main hello @clang $(CFLAGS) build/hello.o build/main.o -o build/main main: @clang $(CFLAGS) -c source/main.c -o build/main.o hello: @clang $(CFLAGS) -c source/hello.c -o build/hello.o
This is obviously better, and it also mean we can now override the compiler flags when invoking make
:
$ make CFLAGS=-Weverything
It might also be a good idea to create a variable for the compiler itself:
CC := clang CFLAGS := -Wall -Werror all: main hello @$(CC) $(CFLAGS) build/hello.o build/main.o -o build/main main: @$(CC) $(CFLAGS) -c source/main.c -o build/main.o hello: @$(CC) $(CFLAGS) -c source/hello.c -o build/hello.o
So if you want to use gcc
instead of clang
, you can simply use:
$ make CC=gcc
And we should also add some output:
CC := clang CFLAGS := -Wall -Werror all: main hello @echo "Linking executable" @$(CC) $(CFLAGS) build/hello.o build/main.o -o build/main main: @echo "Compiling main.c" @$(CC) $(CFLAGS) -c source/main.c -o build/main.o hello: @echo "Compiling hello.c" @$(CC) $(CFLAGS) -c source/hello.c -o build/hello.o
When invoking make
, we'll now get:
Compiling main.c Compiling hello.c Linking executable
But there's an issue here. If we run make
again, all files will be recompiled.
Ideally, we want to compile the files only if it's necessary.
Fortunately, make
makes this very easy, as it supports targets that are based on real files.
If the name of a target specifies a file name, the target will only be executed if the file does not already exist.
This is what we need.
We only want to compile the C files if the object files don't already exist in the build directory.
So we can change our Makefile
the following way:
CC := clang CFLAGS := -Wall -Werror all: build/main.o build/hello.o @echo "Linking executable" @$(CC) $(CFLAGS) build/hello.o build/main.o -o build/main build/main.o: @echo "Compiling main.c" @$(CC) $(CFLAGS) -c source/main.c -o build/main.o build/hello.o: @echo "Compiling hello.c" @$(CC) $(CFLAGS) -c source/hello.c -o build/hello.o
Notice how we replaced the main
and foo
target names with the expected produced files.
Now if we run make
again, the build/main.o
and build/hello.o
targets won't be executed, because these files already exist.
For make
, it means that the prerequisites are already satisfied.
And obviously we can do the same with the executable, to avoid linking it every time:
CC := clang CFLAGS := -Wall -Werror all: build/main @echo "Build successful" build/main: build/main.o build/hello.o @echo "Linking executable" @$(CC) $(CFLAGS) build/hello.o build/main.o -o build/main build/main.o: @echo "Compiling main.c" @$(CC) $(CFLAGS) -c source/main.c -o build/main.o build/hello.o: @echo "Compiling hello.c" @$(CC) $(CFLAGS) -c source/hello.c -o build/hello.o
Here we created an additional build/main
target for the executable, on which all
now depends.
As always, we can see what's going on with the --debug
flag:
$ make Reading makefiles... Updating goal targets.... File `all' does not exist. File `build/main' does not exist. File `build/main.o' does not exist. Must remake target `build/main.o'. Compiling main.c Successfully remade target file `build/main.o'. File `build/hello.o' does not exist. Must remake target `build/hello.o'. Compiling hello.c Successfully remade target file `build/hello.o'. Must remake target `build/main'. Linking executable Successfully remade target file `build/main'. Must remake target `all'. Build successful Successfully remade target file `all'.
This is fine, but what if we want to force a full compilation again?
It is common practice to define a clean
target that will remove temporary build files.
Nothing difficult here, we'll just remove the files from the build directory
:
clean: @echo "Removing build files" @rm -rf build/*
We can also add a test
target that will run the executable:
test: all @./build/main
Our Makefile
is looking good so far.
But we can already see an upcoming issue.
If we want to add more C files, we'll have to add additional targets, which is not convenient at all.
Fortunately, make
supports targets that match a specific pattern. You can think of it as a kind of wildcard.
The generic part is denoted with the %
character in the target name.
It is called the stem.
This means we can create a single target named build/%.o
, that will match every .o
file in the build directory:
build/%.o: @echo "Compiling ???" @$(CC) $(CFLAGS) -c ??? -o ???
But we also need to retrieve the actual file name, so we can replace the ???
in the example above with the correct values.
For this purpose, make
has predefined variables, such as:
$@
The full name of the target, with the stem (%
) expanded.$*
The value of the stem (%
).
Using these variables, we can create a generic target that will compile C files from the source
directory into the build
directory:
CC := clang CFLAGS := -Wall -Werror all: build/main @echo "Build successful" clean: @echo "Removing build files" @rm -rf build/* test: all @./build/main build/main: build/main.o build/hello.o @echo "Linking executable" @$(CC) $(CFLAGS) build/hello.o build/main.o -o build/main build/%.o: @echo "Compiling $*.c" @$(CC) $(CFLAGS) -c source/$*.c -o $@
Now every time we execute a target with a build/
prefix and a .o
suffix, such as build/main.o
, it will compile the corresponding C file ($*
) from the source
directory into the destination file, which is the target name ($@
).
And we can do the same for the executable target:
CC := clang CFLAGS := -Wall -Werror all: build/main @echo "Build successful" clean: @echo "Removing build files" @rm -rf build/* test: all @./build/main build/%.o: @echo "Compiling $*.c" @$(CC) $(CFLAGS) -c source/$*.c -o $@ build/%: build/main.o build/hello.o @echo "Linking executable" @$(CC) $(CFLAGS) $^ -o $@
Notice that we also moved the build/%
target after build/%.o
. The target order is important in the way make
consider targets.
As build/%
can match any file in the build
directory, we want to give a higher priority to the build/%.o
target, so its considered first.
We also introduced a new variable: $^
This contains the full list of the target's prerequisites.
When invoking main with the last example, we can notice a small difference:
$ make Compiling main.c Compiling hello.c Linking executable Build successful rm build/main.o build/hello.o
Notice the last line. make
is now automatically removing the .o
files from the build
directory after a successful build.
Why this sudden change?
It's because the build/%.o
target is now called from a target that also contains a stem (%
) - build/%
.
make
considers that files produced by a target with a stem called from a target with a stem are temporary.
And its default behavior is to remove them upon completion.
We can instruct make
to keep these files by declaring the target as precious:
CC := clang CFLAGS := -Wall -Werror .PRECIOUS: build/%.o all: build/main @echo "Build successful" clean: @echo "Removing build files" @rm -rf build/* test: all @./build/main build/%.o: @echo "Compiling $*.c" @$(CC) $(CFLAGS) -c source/$*.c -o $@ build/%: build/main.o build/hello.o @echo "Linking executable" @$(CC) $(CFLAGS) $^ -o $@
The .o
files will no longer be deleted.
Now what if we make changes to a C file and run make
again?
$ make clean && make Compiling main.c Compiling hello.c Linking executable Build successful $ touch source/hello.c $ make Build successful
This doesn't work.
We can obviously manually run make clean
before, but this will recompile everything.
Ideally, we want to recompile only the changed files.
With make
, this is really easy to achieve.
As we saw earlier, targets may represent existing files.
This also means we can use files as target prerequisites:
build/%.o: source/%.c @echo "Compiling $*.c" @$(CC) $(CFLAGS) -c $< -o $@
Here we added source/%.c
as a prerequisite, which means that .o
files in the build
directory now depends on their corresponding C file in the source
directory.
And as the C file is now a prerequisite, we no longer need to specify it manually in the clang
invocation.
Instead, we use the $<
variable, which contains the first prerequisite of the target.
Let's try that again:
$ make clean && make Compiling main.c Compiling hello.c Linking executable Build successful $ touch source/hello.c $ make Compiling hello.c Linking executable Build successful
We can see that hello.c
was recompiled, and that the executable was also relinked.
The build/%
target was automatically executed, because one of its prerequisite needed to be executed again.
This is exactly what we want!
Our build system is quite nice so far, but we still have to manually specify the files we want to build:
build/%: build/main.o build/hello.o
Let's start by making a variable for this; it will already be a little more convenient:
CC := clang CFLAGS := -Wall -Werror FILES_O := build/main.o build/hello.o .PRECIOUS: build/%.o all: build/main @echo "Build successful" clean: @echo "Removing build files" @rm -rf build/* test: all @./build/main build/%.o: source/%.c @echo "Compiling $*.c" @$(CC) $(CFLAGS) -c $< -o $@ build/%: $(FILES_O) @echo "Linking executable" @$(CC) $(CFLAGS) $^ -o $@
We defined FILES_O
, and we use it now as a prerequisite.
But we can make it better.
make
provide several functions that can process text.
For instance, we have a function called addprefix
that adds a prefix to a variable.
As an example:
TEXT := world HELLO := $(addprefix hello ,$(TEXT))
Here we add the hello
prefix to the TEXT
variable.
The HELLO
variable now contains hello world
.
And obviously, there is also a function called addsuffix
.
make
can also do text replacement with the subst
function:
TEXT := hello world HELLO := $(subst world,universe,$(TEXT))
Here we replace the world
string by universe
in the TEXT
variable.
make
also provides a replacement function that works with patterns on whitespace separated strings: patsubst
FILES_C := hello.c main.c FILES_O := $(patsubst %.c,%.o,$(FILES_C))
Here FILES_O
now contains hello.o main.o
. For every string in FILES_C
, we replaced the .c
extension with .o
.
And we can also combine with addprefix
to add the build
directory on each file, since addprefix
also works on string lists:
FILES_C := hello.c main.c FILES_O := $(addprefix build/,$(patsubst %.c,%.o,$(FILES_C)))
This would be a nice enhancement, be we can do even better.
make
is also able to get a file list from a directory, with the wildcard
function:
$(wildcard source/*.c)
This will get every .c
file in the source
directory.
If we have multiple directories, we can use the wildcard
function as above in a foreach
:
DIR_SRC := source other FILES_C := $(foreach dir,$(DIR_SRC),$(wildcard $(dir)/*.c))
Here, for each string in DIR_SRC
, the foreach
function will declare a variable called dir
and pass it to its last argument, $(wildcard $(dir)/*.c)
, which gets all C file in the directory.
This way, we no longer have to specify the files we want to compile.
We can just add them to the source
directory, and they will be compiled.
Our final example is:
CC := clang CFLAGS := -Wall -Werror EXEC := main DIR_SRC := source DIR_BUILD := build FILES_C := $(foreach dir,$(DIR_SRC),$(wildcard $(dir)/*.c)) FILES_O := $(addprefix $(DIR_BUILD)/,$(patsubst $(DIR_SRC)/%.c,%.o,$(FILES_C))) .PRECIOUS: $(DIR_BUILD)/%.o all: $(addprefix $(DIR_BUILD)/,$(EXEC)) @echo "Build successful" clean: @echo "Removing build files" @rm -rf $(DIR_BUILD)/* test: all @./$(DIR_BUILD)/$(EXEC) build/%.o: $(DIR_SRC)/%.c @echo "Compiling $*.c" @$(CC) $(CFLAGS) -c $< -o $@ build/%: $(FILES_O) @echo "Linking executable" @$(CC) $(CFLAGS) $^ -o $@
We also added a variable for the build directory and for the executable name.
We now have a pretty generic build system.
That's all I have for today. I really hope you found this article useful.
As you can see, Makefiles can be quite simple and very powerful.
While this article was focused on a build system, you can probably see how it can be applied to other tasks and conveniently replace shell scripts for day to day automation.
Finally, the last example is also available on my GitHub.
Feel free to use and adapt it as you want.