Create a Makefile
Dependency management in Python is straightforward with
pip
. While people do distribute Python programs to other
people to install on their computers, most of the time we’re not
compiling Python programs.
We’re going to switch now to taking a look at building software (compiling source code into a runnable program), specifically in the context of building C programs.
There are many approaches to building C programs, and we’ve seen at least one way to do that in this course:
clang my-app.c -o my-app # compile my-app.c
This is fine when our program is exactly one file big, but as you design and grow your software, you’re very quickly going to find that single-file programs become unwieldy.
I’m not saying you can’t have a single file that’s got thousands of lines of code, but I am saying that it’s a “bad idea”™.
In practice you’re probably going to be decomposing
your application and problem into smaller functional units (you’re going
to have your main
function in one file, your
gui
code in a bunch of files in a separate folder, your
data structures in individual files, …).
You certainly can still compile these one at a time, but managing the dependencies between different units quickly gets painful (and tedious):
clang list.c -c -o list.o
clang set.c -c -o set.o
clang my-app.c list.o set.o -o my-app
### Did I remember to re-compile everything?
What even is make
?
make
is a program that can help you with situations like this: You tell it
what things you’re trying to compile, how to compile those things, and
what each unit depends on, and make
will figure out the
correct order of commands to run to make sure that you’re able to get
your program out at the end.
make
is going through the process of building a dependency
graph: a set of nodes and directed edges G
= (V, E), where the nodes are the compiled outputs and the
instructions for creating them, and the directed edges are the way that
one compiled output depends on the existence of another compiled
output.
In the example we had above (list.o
, set.o
,
and my-app
), the dependency graph would look something like
this:
We fortunately don’t have to draw a graph for make
to
create this graph. Instead, we list out “targets” (the things we’re
trying to build), their dependencies (what this unit depends on), and
the commands make
should use to create that target. This
all goes into a special file named Makefile
.
Running make
Re-download or copy hello.tar.gz
: https://toolsntechniques.ca/topic07/hello.tar.gz
If you’re downloading this file again, you should extract it:
tar -xf hello.tar.gz
The folder hello
has a file named Makefile
.
You can run the command make
in this directory to build the
targets:
cd hello
make
make
works by looking for a file in the current working
directory that has a special name, usually
Makefile
with an upper-case M
. By default,
make
will find the first “target” in the file (the first
thing that appears on the left side of a colon :
). In
hello
, the first line in the Makefile
that
matches this looks like:
all: HelloWorld.class hello ccrash
Our first “target” here is listing out all of the things we want to
build in this Makefile
as a “dependency”. This target is
often called all
, but this is also a “de facto” standard,
you can also call this first target cats
and
make
will behave the same way.
Assuming that everything’s working, you should see output similar to the following:
make
javac HelloWorld.java
clang -Wall -Wpedantic -Wextra -Werror -g hello.c -o hello
clang -Wall -Wpedantic -Wextra -Werror -g ccrash.c -o ccrash
This Makefile
has a clean
target, this is
another “de facto” standard for a target name that doesn’t create
anything new but deletes outputs from compilers:
make clean
You can also ask make
to build a specific target by
specifying the target name after make
:
make hello
Format of a Makefile
Let’s take a look at what Makefile
s look like in more
detail. The Makefile
in hello
is slightly more
complicated and we’ll see all of it soon, but the simplest format for a
Makefile
is straightforward:
target: dependencies
commands
This is a rule, and the rule consists of a target, the target’s dependencies, and a sequence of 0 or more commands that can be used to transform the dependencies into the target.
Whatever your stance is on tabs
vs spaces, make
doesn’t care what you think: the
whitespace before the commands
must be a
Tab (it cannot be spaces). Thankfully most text editors are
aware of this, and when you create or open a file named
Makefile
, they will very helpfully put tabs in instead of
spaces.
Here’s the Makefile
for our imaginary program above:
set.o: set.c # set.o depends on set.c
clang set.c -c -o set.o
list.o: list.c
clang list.c -c -o list.o
my-app: my-app.c list.o set.o
clang my-app.c list.o set.o -o my-app
Here are some general rules of thumb:
- The dependencies listed on the right side of the
:
should generally include your source code (.c
,.java
,.py
). - The targets listed on the left side of the
:
are always going to be what your compiler produces as output (.class
,.o
). - The commands should take as input the dependencies, and produce as output the target.
Variables
Similar to shell scripts, Makefile
s can also use variables.
We’re going to look at a specific kind of variable that can be used in
rules to help make sure we don’t have to name targets and dependencies
in our list of commands: automatic
variables. Here are some of the automatic variables you can use in
commands to generate a target from its dependencies:
Automatic variable | Description |
---|---|
$@ |
The target’s file name. |
$< |
The name of the first dependency. |
$^ |
The name of all dependencies. |
Let’s convert the Makefile
above for our imaginary
program to use these variables:
set.o: set.c
clang $< -c -o $@
list.o: list.c
clang $< -c -o $@
my-app: my-app.c list.o set.o
clang $^ -o $@
While this isn’t necessarily any easier to read, it does make sure that you’re including both the dependencies and the targets in the commands used to transform the dependencies into the targets.
Convert the Makefile
from hello
to use
these automatic
variables.
There’s only one target specified (HelloWorld.class
), so
that’s the only one to change.
Using GNU Make to build C programs
You might have noticed that the Makefile
in
hello
looks, uh, a little weird:
- It clearly compiles some C programs when you run
make
. - The names of the C programs are listed as a dependency in the
all
target. - There are no
clang
commands in thisMakefile
at all.
What gives?
GNU Make knows a lot about C programs (and other languages) in the form of implicit rules. One of the implicit rules that GNU Make has is for C programs:
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
%: %.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
There’s a bunch of unusual looking stuff here, but it’s straightforward:
LINK.c
is a variable definition, and it’s a string that gets built up by evaluating many other variables (CC
,CFLAGS
,CPPFLAGS
, …).%: %.c
is a target/dependency pair. The%
is a wildcard, matching anything on the left as a target that has a matching dependency on the right with an extension of.c
(this is what matcheshello
andhello.c
in ourMakefile
).- The command to build the target is a bunch of other variables, some
of which we’ve seen (
$^
and$@
).
Let’s look at these side-by-side:
The implicit rules and variables in GNU Make
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
%: %.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
Our Makefile
CC = clang
CFLAGS = -Wall -Wpedantic -Wextra -Werror -g
all: hello
While these two rule sets aren’t literally part of the same file, you
can imagine that the rules and variables on the left (the stuff that’s
defined in GNU Make) come first, and the rules and variables on the
right (the stuff that’s defined in our Makefile
) come
after:
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
%: %.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
CC = clang
CFLAGS = -Wall -Wpedantic -Wextra -Werror -g
all: hello
Variables that don’t have values will eventually turn into empty strings, and variables that have values (even if they’re declared and defined later in the file) will be replace with the values that they’re given.
Let’s step through this:
The first rule that has an actual target is our
all
target, so that’s the first thing that’s evaluated.all
depends onhello
.hello
then matches%: %c
; it matches the wildcard as a target on the left specifically because there is a file namedhello.c
in this directory (the dependency is satisfied).The command for transforming
hello.c
(the dependency) intohello
(the target) consists almost entirely of variables (everything except-o
), but$(LINK.c)
uses$(CC)
(which we defined asclang
) and$(CFLAGS)
(which we defined as-Wall...
), and uses the special variable$^
(all dependencies).The command turns into:
clang -Wall -Wpedantic -Wextra -Werror -g hello.c -o hello
(the extra spaces between
-g
andhello.c
are the spaces in the variableLINK.c
between the other variables, the spaces betweenhello.c
and-o
are from the empty variables between$^
and-o
).
Neat 📷.
Using make
to build…
stuff
make
is great at building code, but make
isn’t just about building code, it’s about identifying
targets and satisfying their
dependencies by running commands.
Let’s walk through an example of using make
to do
other stuff, like converting files from one format to another.
Gosh. That sounds like something we’ve done before…
Right, let’s use make
to convert files from Markdown to
other formats like pdf
or html
.
Here’s a Markdown-formatted document that you can place into your current working directory:
Hello!======
I'm a Markdown formatted file. We can write about
* Numbers and formulas $\frac{1}{2}$,
* Code: `System.out.println`
* Or *whatever*.
Copy this into a file named hello.md
in a directory
somewhere.
Now create a new file named Makefile
; let’s step through
writing it together:
We need to have a target for
all
, and we want that rule to be first. So let’s add anall
target. We’ll focus for now on converting our Markdown file to HTML.all: hello.html
Now we need to add a target for
hello.html
—make
doesn’t know how to convert.md
to.html
in the same way it does.c
to a compiled program, so let’s do that. We want to add a target that matches specificallyhello.html
(we’ll get fancier later, let’s keep it simple for now).hello.html
depends onhello.md
(we’re transforminghello.md
intohello.html
).all: hello.html hello.html: hello.md
Once we’ve got a target, we need to tell
make
how to do that transformation. Beneath our target forhello.html
, let’s add a command to convert it:all: hello.html hello.html: hello.md pandoc hello.md --standalone -o hello.html
Now run make
in your directory, and make
should transform hello.md
into hello.html
using pandoc
. Nice 🎉!
Now the important bit: make changes to hello.md
and
re-run make
. make
will detect changes in the
file (the modified date in the dependency is newer than the created date
on the target) and will automatically figure out which dependencies need
to be rebuilt.
Neat 📷.
Let’s use what we’ve learned to try and turn this into something that can do what we did with shell scripting:
Let’s switch our command to use the built-in variables for the names of targets
$@
and dependencies$<
:all: hello.html hello.html: hello.md pandoc $< --standalone -o $@
We don’t want to give specific names for targets (or dependencies), so let’s use wildcards:
all: hello.html %.html: %.md pandoc $< --standalone -o $@
We don’t want to list out the targets for
all
, we can use the wildcard function:MD = $(wildcard *.md) all: $(MD:md=html) # use the value of MD, but replace `md` with `html` %.html: %.md pandoc $< --standalone -o $@
Now you can add more Markdown files to this directory with any name and the files will be converted to HTML.
Look further at having this work for nested directories (you’ll have
to spend some more time looking at wildcards), and have this work for
multiple formats of output (either by making new targets with new
extensions, or by accepting
variable arguments to make
).
Running make
concurrently
One final and minor thing to add is that make
can try to
build targets that are unrelated concurrently. Your computer
has a processor with more than one core, and each core can be used by
make
to build unrelated dependencies for targets.
Here’s the dependency graph we had from above:
When make
builds set.o
and
list.o
, it can build them literally at the same time
because they don’t have common dependencies.
We can tell make
to run on multiple CPUs using the
option -j
for jobs.
You can pass -j
to make
for any
Makefile
, and if make
identifies targets it
can build independently of other targets, it will do that!
make -j10 # run 10 jobs concurrently (if possible)
This is helpful when you’re building things that aren’t trivial, or
if you’re using make
to transform files from one format to
another where that transformation is expensive (e.g., converting audio
or video from one format to another, converting images between
formats).
Don’t set -j
higher than the number of CPUs you have
installed on the system. A command you can use to find out how many CPUs
you have installed on a Linux system is nproc
, and you can
use that in conjunction with make
:
make -j$(nproc)
Further reading
There’s a lot here, and we’re just barely scratching the surface. You
can find more information about make
in a few different
places:
- If you really just want a place where you can quickly find, copy,
and paste a
Makefile
that will work for the situation that you want, or if you’re looking for a more in depth tutorial on how to build aMakefile
, look no further than https://makefiletutorial.com/. - The GNU Make
documentation is… comprehensive, it’s about 1MB (or one million
characters) of documentation about how to use
make
. It’s a lot, but if you have a question, you can almost certainly find the answer there.