If you’ve ever attempted to compile some software from source, it’s likely you’ve come across Makefiles. It’s also likely you’ve heard them described as “black magic”. This week, I hope to demystify them, at least a little bit, and explain how some of the magic works.
You can find the original slides for this talk here.
What are we trying to solve?
- Building software which consists of more than n files rapidly becomes tedious to do by hand
- Building software which includes any nature of logic in determining what/how to build rapidly becomes very complex
- Automation beats manual fiddling 9 times out of 10
Important corollary
- All build systems are terrible, but some also work
Simple example
Small C project
src/
|- program.c
|- foo.c
\- foo.h
- C program with a “main” source file, and header/implementation files
- Easy to compile by hand:
gcc -o program program.c foo.c - Gets trickier with more files, more flags, compilation order, etc.
First thing
Shell script
#!/bin/bash
gcc -o program program.c foo.c
- Write a shell script with our compile commands in
- Works ok for simple projects
- Has some downsides:
- Change one file and we have to recompile everything
- Update how to compile one file and we need to recompile everything
- Adding more files means either copy+pasting (with associated mistakes) or adding complex logic
- We have all those CPU cores sitting idle while we compile one file at a time…
make
- Like most veteran programming tools,
makewas created in the 1970s - Standard implementation on Linux is GNU
make makeis a declarative language, as opposed to imperative- You tell
makehow to do something, and it figures what to do - Basic form of a rule is:
target: prerequisites
recipe
- Note: that’s a literal tab before recipe. If you don’t use a tab, you’ll get a mysterious error message along the lines of
:makefile:4: *** missing separator. Stop.
- This most likely means you’ve used spaces instead of a tab in front of the recipe. See this StackOverflow answer for one way to hunt down missing tabs.
Advantages of make
makewill figure out what order things need doing inmakewill figure out what things actually need doing- Change a file, and
makewill rebuild that file and everything that depends on, and no more - These things are true of other build systems too!
- Second most popular is probably CMake
- Also whatever web developers are using this week
Simple example
Makefile
all:
gcc -o program program.c foo.c
- Now we can just run
makeand build our program! - By default,
makebuilds the first target in the makefile - By convention, the
alltarget builds the whole program
Simple example
Makefile
- Let’s get fancier:
all: program
program: program.o foo.o
gcc -o program program.o foo.o
program.o: program.c
gcc -c program.c
foo.o: foo.c
gcc -c foo.c
- Now if we only change
program.c,foo.cdoesn’t need to be recompiled make program.owill build justprogram.oand its prerequisites
Variables
Compile time configuration
- What if we sometimes want to compile with a different compiler?
# CC is the default name for the C compiler
CC = gcc
all: program
program: program.o foo.o
$(CC) -o program program.o foo.o
program.o: program.c
$(CC) -c program.c
foo.o: foo.c
$(CC) -c foo.c
- Note: the default/conventional names for the C++ compiler is
CXX, andFCfor the Fortran compiler
Variables
Flags and libraries
CC = gcc
# CFLAGS for C compile flags, CXXFLAGS for C++, FFLAGS for Fortran
CFLAGS = -g -Wall
# LDLIBS for libraries, LDFLAGS for linker flags (i.e. -L)
LDLIBS = -lm
all: program
program: program.o foo.o
$(CC) $(CFLAGS) -o program program.o foo.o $(LDLIBS)
program.o: program.c
$(CC) $(CFLAGS) -c program.c
foo.o: foo.c
$(CC) $(CFLAGS) -c foo.c
Cleaning up
The clean rule
- We often want to compile from a clean start
- The conventional target for this is
clean:
.PHONY: clean
clean:
rm -fv *.o *~
- We use
-fso thatrmdoesn’t error if a file doesn’t exist (more important if you use a variable here) - The
.PHONYrule tells make thatcleandoesn’t produce a file namedclean
Adding more files
Patterns and automatic variables
- We’re repeating ourselves a lot, specifying the source file in both the prerequisite and the actual recipe
- Also, all the recipes are identical, save for the filenames
- This is where patterns and automatic variables come in:
%.o: %.c
$(CC) $(CFLAGS) -o $@ $^
- The
%is a pattern:foo.omatches%.o- The part matching
%is called the stem and gets expanded on both sides - If
foo.omatches the target, then%.cexpands tofoo.c
- The part matching
Adding more files
Patterns and automatic variables
- We’re repeating ourselves a lot, specifying the source file in both the prerequisite and the actual recipe
- Also, all the recipes are identical, save for the filenames
- This is where patterns and automatic variables come in:
%.o: %.c
$(CC) $(CFLAGS) -o $@ $^
$@and$^are automatic variables:$@expands to the name of the target$^expands to the names of all the prerequisites, separated by spaces
Adding more files
Back to our example
CC = gcc
CFLAGS = -g -Wall
LDLIBS = -lm
all: program
program: program.o foo.o
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $^
clean:
rm -fv *.o *~
Adding more files
- Now when we want to add a new source file to the program, it’s a very simple change
- Let’s also list all the files in a variable
Adding more files
Back to our example
CC = gcc
CFLAGS = -g -Wall
LDLIBS = -lm
OBJECTS = program.o foo.o bar.o
all: program
program: $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $^
clean:
rm -fv *.o *~
Going further
Useful flags
--jobs=Ntries to run N recipes at once – very useful for larger projects!--load-average=Ndoesn’t start new jobs if the load is more than N--keep-goingtellsmaketo keep going even if there are errors – useful for finding as many errors as possible in one go--file=FILEuse FILE instead of the defaultmakefile--directory=DIRECTORYchange to DIRECTORY before doing anything--dry-runjust print the recipes, instead of running them
Going further
Fancier features
makeis the standard *nix build system for projects large and small, and hence has lots of features- More complicated features not covered here:
- Implicit rules and variables
- Searching other directories for prerequisites using
VPATH, useful to place compilation artefacts in a different directory to the source - Lots of functions for transforming text
- Conditionals:
# ARCHER specific compiler
ifeq ($(machine),archer)
FC = ftn
endif
Going further
Implicit rules and variables
makealready knows how to build certain types of files from other ones- Also has lots of “implicit” variables
- Our example makefile could be as simple as:
program: program.o foo.o
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
clean:
rm -fv *.o *~
Going further
Automatically making Fortran dependencies
https://github.com/ZedThree/fort_depend.py
pip install --user fortdepend
all: $(actual_executable) my_project.dep
.PHONY: depend
depend: my_project.dep
my_project.dep: $(OBJECTS)
fortdepend -w -o my_project.dep -f $(OBJECTS)
include my_project.dep
Going further
Automatically creating directories
OBJDIR := objdir
OBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o)
$(OBJDIR)/%.o : %.c
$(COMPILE.c) $(OUTPUT_OPTION) $<
all: $(OBJS)
# The | signifies an "order-only" prerequisite
$(OBJS): | $(OBJDIR)
$(OBJDIR):
mkdir $(OBJDIR)