A guide to Makefiles. Designed to be useful for students I TA at the University of British Columbia (UBC), but also hopefully more broadly applicable to other developers. (I've read a lot of Makefile tutorials, and most of them are a bit confusing. So, hopefully this rendition is a bit clearly for undergraduates.) The content here is mostly just a more accessible version of Chapter 2 of the make
manual, "An Introduction to Makefiles," with some extra tidbits here and there.
Now, without further ado,
Table of Contents
make
is a command line program that, in tandem with a Makefile
, allows one to 'compile' and 'link' small, medium, and large programs. If you've ever compiled a program, like
gcc main.c -o main; ./main
or perhaps
javac Main.java; java -cp . Main
or even
clang++ main.cpp -o main; ./main
you'll benefit from make
. make
can be used to add "shortcuts" for various common commands as well, such as cleaning up undesired files from your project. It also only recompiles what is necessary which, while never particularly relevant for assignments consisting of less than 1,000 lines of code, becomes extraordinarily relevant in codebases of 1,000,000+ lines.
Firstly, make sure you have make
actually installed. Note this is not the same thing as cmake
. (I'm not proud to admit it, but I thought these were equivalents for a very long time.) You should also have gcc
working (and ideally some kind of Java installed). Again, new-to-C Windows users might have issues here; I recommend using Chocolatey to fix those issues (or better yet, WSL).
Aside:
cmake
is a meta-make
(that is, likemake
formake
). It builds makefiles for various systems simultaneously. It has 'profiles' for different compilers and systems, and will automatically generate the correct flags, et cetera, on the fly. In short, it's for huge projects, and I won't be covering it in this guide.
To check if you have make
installed, run make --version
. I see GNU Make 4.2.1 [...]
on my machine. If you see an error like,
"'make' is not recognized as an internal or external command, operable program or batch file" (from PowerShell, on Windows.)
or
"make": command not found (from Linux, I believe.)
you need to install the software. Follow these steps; on Windows, choco
is your friend here.
Try running make
with no arguments. You should see the following.
$ make
> make: *** No targets specified and no makefile found. Stop.
make
is looking for a corresponding Makefile
and didn't find one. Let's start by creating one!
Makefiles are essential a set of different commands or "rules." They have no file extension, and are just written in plaintext. Each rule has roughly the following structure.
target ...: prerequisites ...
recipe
...
The target is the name of the file generated by the program. A prerequisite is dependency for the target, which should be another target itself. The recipe is the actual set of actions carried out. The target and recipe are required, but prerequisites are of course optional.
WARNING: Makefiles are picky about spacing, like Python. Each line in a recipe must be prefixed by a TAB
character. Four spaces don't work; it really needs to be tabs. Just a note.
Let's go back to the basic gcc main.c -o main
we all know and love. If you check out examples/basic
, we can see what this looks like in a Makefile. Begin by trying
$ gcc main.c -o main; ./main
> H3llo, w0rld!
which works as normal. Now run
$ make; ./main
> H3llo, w0rld!
our first Makefile! And it's really just one line:
main: main.c
kind-of cool isn't it? Though it's a bit unorthodox, we could even do this for a Java file. Running the examples/java
file in VS Code gave some horrible command like
$ /usr/bin/env /home/mdema/.sdkman/candidates/java/current/bin/java \
--enable-preview \
-XX:+ShowCodeDetailsInExceptionMessages \
-Dfile.encoding=UTF-8 \
-cp /home/mdema/.vscode-server/data/[...]/bin \
examples.java.Main
> Hello, world!
Imagine trying to memorize that! Instead, try
$ make
> Hello, world!
(Forgive the cd
s in the Makefile; this is just for demonstration.) Clearly, make
can be used to run any set of commands. Rules are at their core, just a set of instructions, a "recipe", that are just commands you would've had to run by hand in the shell.
Oftentimes, our programs are not just single files. What if we want to compile a bunch of .c
files and .h
files into a single executable? This is where object files (.o
) can truly benefit us. We can create rules to construct intermediate .o
files, and then create a "master" rule that combines (or better yet, links) a bunch of these object files together.
In sum, the template is something like this
main: something.o something_else.o something_else_other.o
gcc -o main something.o \
something_else.o \
something_else_other.o
something.o: something.c something.h defs.h
gcc -c something.c
something_else.o: something_else.c defs.h
gcc -c something_else.c
something_else_other.o: something_else_other.c constants.h defs.h
gcc -c something_else_other.c
To create main
, you would type make
(or make main
). Notice how main
is just that, and not suffixed with a .o
. We also use -o
instead of -c
to create an object file instead of an object file. This example nicely demonstrates the importance of prerequisites; if we edit constants.h
, recompiling main will only recompile something_else_other.o
before linkage, saving some computation time.
The first rule given is used by default, so it should be the "main" goal or executable produced by your program.
A few asides from the above example. Firstly, note that
make
itself is not picky about what goes in a recipe, and it's your task as the programmer to devise a recipe that really works. Second,gcc
is the GNU C compiler; you can see a brief guide to different compilers here.cc
is a more generally applicable option.
It can be a bit tedious re-writing all these file names. Instead, it's common practice to throw them into a variable. The above example becomes.
OBJECTS = something.o something_else.o something_else_other.o
main: $(OBJECTS)
gcc -o main $(OBJECTS)
# etc.
You'll also notice that we're manually stating gcc -o main ...
in each of these, but we did not do that in our basic example. In fact, make
has an implicit rule to use cc -c
to compile object files from .c
files. It's pretty nifty. Likewise, make
also allows us to skip adding the .c
file itself to its object file prerequisite. The above example simplifies to
OBJECTS = something.o something_else.o something_else_other.o
main: $(OBJECTS)
gcc -o main $(OBJECTS)
something.o: something.h defs.h
something_else.o: defs.h
something_else_other.o: constants.h defs.h
Notice how defs.h
is a pre-requisite to all of our object files? There's more room for improvement here. We can group entries by their pre-requisites instead. This yields
OBJECTS = something.o something_else.o something_else_other.o
main: $(OBJECTS)
gcc -o main $(OBJECTS)
$(OBJECTS): defs.h
something.o: something.h
something_else_other.o: constants.h
Last thing before we wrap upโrunning these commands will leave a lot of unwanted files hanging around. To fix this, let's add a "phony" command called clean
that will delete junk files. The reason it is called "phony" is that we don't have a file clean.c
, but rather we're looking to just automate some command line task.
We get
OBJECTS = something.o something_else.o something_else_other.o
main: $(OBJECTS)
gcc -o main $(OBJECTS)
$(OBJECTS): defs.h
something.o: something.h
something_else_other.o: constants.h
.PHONY: clean
clean:
rm main $(OBJECTS)
What if we want to add some command line options to our gcc
call? For example, running gcc main.c -std=gnu11 -Wall -o main
? To do this, we just parameterize the gcc
part of a recipe. Continuing our earlier example,
CC = gcc
CFLAGS = -I . -std=gnu11 -Wall
OBJECTS = something.o something_else.o something_else_other.o
main: $(OBJECTS)
$(CC) -o main $(OBJECTS)
# etc.
Notice that we must specify the $(CC)
and the CFLAGS
is added automatically by the compiler.
Remember the earlier example where we wanted to pull out defs.h
from each of our pre-requisites? There's a different, arguably better way of doing this that involves using macros. We can also create "generic" rules that apply to globs (e.g., all *.c
files). This example demonstrates this
CC = gcc
CFLAGS = -I . -std=gnu11 -Wall
OBJECTS = something.o something_else.o something_else_other.o
DEPS = defs.h
%.o: %.c $(DEPS)
$(CC) -c $(CFLAGS)
main: $(OBJECTS)
$(CC) -o main $(OBJECTS)
# etc.
This fancy %.o
rule says all object files depend on their respective .c
file along with all of the DEPS
.
For a final step, let's add directories. What if our .h
files are in an include/
directory, our source code lives in a src/
folder, we have local libraries in a lib/
folder, and we want to stuff our object files into an output directory. Here's what that would look like.
IDIR = ../include
ODIR = obj
LDIR = ../lib
CC = gcc
CFLAGS = -I $(IDIR) -std=gnu11 -Wall
LIBS = -lm
_DEPS = main.h
DEPS = $(patsubst %, $(IDIR)/%, $(_DEPS))
_OBJ = main.o other.o
OBJ = $(patsubst %, $(ODIR)/%, $(_OBJ))
$(ODIR)/%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
main: $(OBJ)
$(CC) -o $@ $^ $(CFLAGS) $(LIBS)
.PHONY: clean
clean: # core is for core dumps!
rm -f $(ODIR)/*.o *~ core $(IDIR)/*~
There are a few extra oddities in this snippet. There are a few automatic variables being used. $@
is the name specified on the left of the colon. $<
is the first item in the dependencies list. $^
is the names of all of the pre-requisites.
patsubst
is a helper function to replace words matching a specified pattern with some text. "%" acts as a wildcard character.
With the above information and the knowledge in the rest of this guide, try to break down what happens above for yourself as an exercise!
This file comes courtesy of Bruce Maxwell of Colby College. If you try to re-use it, you may need to tweak the file paths a bit!
That's all for this Makefiles guide. Hope you enjoyed!
- compile, meaning to transform source code into object files (e.g. running
gcc main.c
) - executable file, a file that when run causes a computer to perform a set of instructions (e.g., the
main
ingcc main.c -o main
) - link, meaning to transform many object files (i.e., the things made in compilation) into a single executable file
- machine code, a (super) low-level programming language that controls a CPU
- object files, a file that contain object code, which is machine code that is output from a compiler or assembler (e.g., our
.o
files); they cannot be run in the command line