Makefile Cheat Sheet
By Leif Wesche
Makefiles are tools that help us compile and maintain code locally. Make is a program that comes with the GNU compiler. Makefiles will save you lots of time typing and clicking. You are required to write them for your homework.
How does it work?
- When you type
make [target], the Make will look through your current directory for a Makefile. This file must be called
- Make will then look for the corresponding target in the makefile. If you don’t provide a target, Make will just run the first target it finds.
- If the target is found, the target’s dependencies will be run as needed, then the target commands will be run.
- Oftentimes these commands start with
g++, but they can be anything! You can run any command this way.
The target dependency format looks like this. Note the tab indent before the commands; these are required!
target1: dependency1 dependency2 ... command1 command2 ...
Targets in a Makefile can be named anything (though as you’ll see, certain names can incur certain behavior). Dependencies can either be other targets or file names; if a target depends on another target, it guarantees that target will be run prior, and if a target depends on a file, it will check to see if that file has changed to avoid executing redundantly. Finally, commands can be anything you could run on your command line.
This simple Makefile compiles a
It also includes a
clean target that removes our executable in case we want to clean up the directory.
hello: hello.cpp g++ -g -Wall hello.cpp -o hello clean: rm -f hello
If we’re in the same directory as our Makefile, we can run the following to compile
make # or make hello
And we can run the following to delete the file we just generated during compilation:
It’s important to note a target can be named after after a file. This is most commonly used to:
- Indicate that our target requires a file that must be compiled by another target.
- Only run our target when that dependency has changed to avoid doing extra work.
Most of your homeworks will require you to compile multiple files, then link them all at once.
To do this, we’ll use the
-c compiler flag, which compiles without linking.
Note that all the files we compile with
-c have target names that correspond to the object files we’re expecting out.
First, take a look at the imaginary file tree we’re basing this Makefile off of:
program/ main.cpp file1.cpp file1.h file2.cpp
And now, the Makefile:
all: program program: main.cpp file1.o file2.o g++ -g -Wall main.cpp file1.o file2.o -o program file1.o: file1.cpp file1.h g++ -g -Wall -c file1.cpp -o file1.o file2.o: file2.cpp g++ -g -Wall -c file2.cpp -o file2.o clean: rm -f *.o program
There are a couple things to note here:
- We added a dependency on
file1.otarget. This makes sure that if
file1.owill be recompiled.
*.o. This is simply shorthand for deleting all files that end with
.o, and is a more convenient way to clean up the objects we created while compiling
- The first target is
all. This is simply what the central or default task of a Makefile is customarily called; there’s no rule that requires you to do this. However, having a separate task in charge of orchestrating the overall compilation process can keep your Makefiles tidy.
When you start to write more complicated Makefiles, you’ll find yourself frequently repeating commands and arguments.
In order to alleviate this, Makefile offers variables.
Here’s an example Makefile where we’ve abstracted most of the
g++ calls into variables.
COMPILER=g++ OPTIONS=-g -std=c++17 -pedantic -Wall -Wextra -Werror -Wshadow -Wconversion -Wunreachable-code COMPILE=$(COMPILER) $(OPTIONS) program: main.cpp $(COMPILE) main.cpp -o program
Another way to keep your build process organized is to use a subdirectory for build artifacts.
This allows you to keep all of the compiled objects out of the way of the files you’re trying to edit.
It also improves the ergonomics of
clean; all you have to do is delete the directory you have all your objects in.
Note that because we still want Make to check file creation and edit times correctly, we also have to change target names.
Using a build directory might make your
Makefile look something like this:
BUILD=build $(BUILD)/file2.o: file2.cpp g++ -g -Wall -c file2.cpp -o $(BUILD)/file2.o build: mkdir -p $(BUILD)
There are a bunch of fancy things you can do with Make using wildcard and expanded symbols. Given the following snippet, here are a couple that might come in handy.
target1: dependency1 dependency2 command1 command2
$@expands to the target name, i.e.
$<expands to the first dependency, i.e.
$^expands to the complete list of dependencies, i.e.
You can use the wildcard operator,
%, to apply a rule to multiple files.
For example, the following rule compiles all
.cpp files in the directory to correspondingly named
%.o: %.cpp g++ -Wall -c $< -o $@
The following is a Makefile template that uses all of the strategies we’ve discussed above. Feel free to use it for your projects, just make sure to modify it to fit our assignment specifications.
COMPILER=g++ OPTIONS=-g -std=c++17 -pedantic -Wall -Wextra -Werror -Wshadow -Wconversion -Wunreachable-code COMPILE=$(COMPILER) $(OPTIONS) BUILD=build # Compile main by default all: program # $(BUILD)/*.o expands to all .o files in the $(BUILD) directory # In this case, we'll get $(BUILD)/file1.o $(BUILD)/file2.o program: main.cpp $(BUILD)/file1.o $(BUILD)/file2.o $(COMPILE) $< $(BUILD)/*.o -o $@ $(BUILD)/file1.o: file1.cpp file1.h build $(COMPILE) -c $< -o $@ $(BUILD)/file2.o: file2.cpp build $(COMPILE) -c $< -o $@ # Make the build directory if it doesn't exist build: mkdir -p $(BUILD) # Delete the build directory and program clean: rm -rf $(BUILD) program # These rules do not correspond to a specific file .PHONY: build clean