Make and makefiles
n previous posts we have been compiling and linking projects using bash commands, we had to write all commands explicitly to build the projects. This is ok for small projects with few source files but oftentimes in large projects we need to compile hundreds of source files and link them to different libraries which makes the build complex. This is what make
was invented for.
The GNU make utility was written in 1977 by Stuart Feldman at Bell Labs and its purpose was to automate the build process, replacing manual shell scripts for compiling and linking large projects. Other (more modern) build systems for C++ projects are SCons, CMake, Bazel, and Ninja. Even though make
is old is still widely used in the industry, specially for small projects.
In this post we will learn how make works as always illustrating it with an example. The entire example can be found in my github repository blogging-code, in the subdirectory cpp-makefile.
TLDR
With the filestructure defned in the following section, the makefile can be
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Compiler and flags
CXX = g++
CXXFLAGS = -std=c++17 -Iinclude
# Directories
SRC_DIR = src
INCLUDE_DIR = include
BUILD_DIR = build
OBJ_DIR = $(BUILD_DIR)/obj
BIN_DIR = $(BUILD_DIR)/bin
# Target executable name
TARGET = $(BIN_DIR)/main
# # Find all source files and corresponding object files
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp, $(OBJ_DIR)/%.o, $(SRCS))
# Default target
all:
@echo "Available options:"
@echo " build - Build the project"
@echo " clean - Remove all build files"
@echo " help - Show this message"
# Build target
build: $(TARGET)
# Rule to build the executable
$(TARGET): $(OBJS)
@mkdir -p $(BIN_DIR)
$(CXX) $(CXXFLAGS) -o $@ $^
# Rule to build object files
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(OBJ_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Clean up build files
clean:
rm -rf $(BUILD_DIR)
# Help target
help: all
# Phony targets
.PHONY: all clean build help
That can be executed with
1
2
3
4
make
make clean
make build
./build/bin/main
that displays the helper, builds the program and executes the main
File structure
As always for our example we define here the file structure and contents so that you can copy-paste the example and run it yourself. The file structure is
1
2
3
4
5
6
7
.
├── Makefile
├── include
│ └── matmul.h
└── src
├── main.cpp
└── matmul.cpp
with matmul.h
:
1
2
3
4
5
6
7
#ifndef MATMUL_H
#define MATMUL_H
void matmul(const int* A, const int* B, int* C, int M, int N, int K);
void printmatrix(const int* A, int M, int N);
#endif
and matmul.cpp
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include "matmul.h"
// Matrices are indexed row-major in this example. E.g. if A is [M x N]
// If i,j are the row and column indices, the element A[i, j] is
// A[i, j] = A[i * N + j] // if row-index
// A[i, j] = A[j * M + i] // if column-index
void matmul(const int* A, const int* B, int* C, int M, int N, int K){
// Matrix multiplication, C[M x K] = A[M x N] * B[N x K]
// Multiplication is $\sum_n A[m, n] * B[n, k]$
for(int m=0; m<M; m++){
for(int k=0; k<K; k++){
C[m * K + k] = 0;
for(int n=0; n<N; n++){
C[m * K + k] += A[m * N + n] * B[n * K + k];
}
}
}
}
void printmatrix(const int* A, int M, int N) {
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
std::cout << A[i * N + j] << " ";
}
std::cout << "\n";
}
}
and finally main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include "matmul.h"
int main(void){
// A[M x N]
int M = 2;
int N = 3;
int* A = new int[M * N];
for(int i=0; i < M * N; i++){
A[i] = i;
}
std::cout << std::endl << "A:" << std::endl;
printmatrix(A, M, N);
// B[N x K]
int K = 4;
int* B = new int[N * K];
for(int i=0; i < N * K; i++){
B[i] = i;
}
std::cout << std::endl << "B:" << std::endl;
printmatrix(B, N, K);
// C[M x K]
int* C = new int[M * K];
matmul(A, B, C, M, N, K);
std::cout << std::endl << "C = A x B: " << std::endl;
printmatrix(C, M, K);
return 0;
}
We have used this example before, this is just a matrix multiplication example. The idea is to compile the main as an executable using make
.
Make basics
The process of compilation can be thought of as a graph: to generate a executable or library we need first to compile all the source files to objects and then link all the objects into the executable (or library). In more complex projects there could be even more compilation steps. Make has also the advantage of compiling only the files that have changed, this is crucial for large projects as it could take several minutes to compile the code again from scratch while there is no need if the source file hasn’t changed. Check the compilation time of OpenCV in a previous post.
To run make
we create a file named Makefile
with the compilation instructions and then run make
:
1
2
touch Makefile
make
Since the Makefile
is empty (there are no rules) make
will complain with make: *** No targets. Stop.
. Let’s add a rule in the file
1
2
hello:
echo "Hi there"
Now run make hello
and it will print into your screen the “Hi there”. In essence the makefile contains rules, a rule has the following syntax
1
2
3
4
targets: prerequisites
command
command
command
The targets are normally files to be compiled (object files) and the prerequisites the source files, but before jumping to that, let’s understand the essence of the graph calculation. Modify the makefile to contain
1
2
3
4
5
6
7
8
9
10
11
calculate_1:
echo "calculate_1"
calculate_2:
echo "calculate_2"
calculate_3: calculate_1
echo "calculate_3"
calculate_4: calculate_1 calculate_2
echo "calculate_4"
Running make calculate_1
will just print “calculate_1”, similarly for make calculate_2
. These two rules don’t depend on any other rule. However if we run make calculate_3
it will print first “calculate_1” and then “calculate_3” as the third calculation depends on the first (by the prerequisites in the rule). A similar case will happen in make calculate_4
, this time it will print first “calculate_1” and then “calculate_2” before printing “calculate_4”. This describes the nature of makefile, you can nest this as much as you want to generate a direct acyclic graph of your bash commands.
But make is more than just instructions, it is intrinsically linked to files. Let me explain this with an example. Create a file “calculate_1” and try to run the calculate_1 rule from the previous makefile.
1
2
touch calculate_1
make calculate_1
you will be prompted with make: 'calculate_1' is up to date.
. Indeed!, make interprets that since there is a file in this directory named calculate_1
it has already been “compiled” and there is nothing to do for this rule. This is very useful when you have compilation errors in certain files, the files that are compiled successfully won’t be compiled again if you re-run make.
Simple make: build and run the example
Let’s write a simple makefile to compile and link the program:
1
2
3
4
5
6
7
8
matmul.o:
g++ -std=c++17 -Iinclude -c src/matmul.cpp -o matmul.o
main.o:
g++ -std=c++17 -Iinclude -c src/main.cpp -o main.o
compile: matmul.o main.o
g++ matmul.o main.o -o main
Now run
1
2
make compile
./main
to compile and run the program. See that we have specified a graph here. To run compile
we need to have the files matmul.o
and main.o
. See that the files will be generated in the current directory, we can modify that by writing the path in the rule build/obj/matmul.o
for instance.
Automatic variables
There are some special characters defined in the make documentation called automatic variables. These are very useful but rarely explained through examples. Before jumping to a complete makefile we’ll explain some of them
1
2
3
4
5
6
output.txt: input1.txt input2.txt input1.txt
echo "Target: $@" > $@
echo "First prerequisite: $<" >> $@
echo "Updated prerequisites: $?" >> $@
echo "All prerequisites (unique): $^" >> $@
echo "All prerequisites (with duplicates): $+" >> $@
input1.txt
, input2.txt
, and input1.txt
are listed as prerequisites. Notice that input1.txt
is repeated to demonstrate $^
(unique) vs. $+
(duplicates included).
$@
: Refers to the target,output.txt
.$<
: Refers to the first prerequisite,input1.txt
.$?
: Lists all prerequisites that are newer than the target. This is dynamic and depends on file timestamps.$^
: Lists all unique prerequisites (input1.txt input2.txt
).$+
: Lists all prerequisites, including duplicates (input1.txt input2.txt input1.txt
).
As an example create the prerequisites with
1
2
echo Hello from input1! > input1.txt
echo Hello from input2! > input2.txt
and run make output.txt
, the result in output.txt
will be:
1
2
3
4
5
Target: output.txt
First prerequisite: input1.txt
Updated prerequisites: input1.txt input2.txt
All prerequisites (unique): input1.txt input2.txt
All prerequisites (with duplicates): input1.txt input2.txt input1.txt
We will use some of these automatic variables to write a more robust makefile.
A more complete makefile
Let’s write a proper makefile this time, now that we understand the basics.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Compiler and flags
CXX = g++
CXXFLAGS = -std=c++17 -Iinclude
# Directories
SRC_DIR = src
INCLUDE_DIR = include
BUILD_DIR = build
OBJ_DIR = $(BUILD_DIR)/obj
BIN_DIR = $(BUILD_DIR)/bin
# Target executable name
TARGET = $(BIN_DIR)/main
# # Find all source files and corresponding object files
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp, $(OBJ_DIR)/%.o, $(SRCS))
# Default target
all: $(TARGET)
# Rule to build the executable
$(TARGET): $(OBJS)
@mkdir -p $(BIN_DIR)
$(CXX) $(CXXFLAGS) -o $@ $^
# Rule to build object files
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(OBJ_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Clean up build files
clean:
rm -rf $(BUILD_DIR)
# Phony targets
.PHONY: all clean
I know, this is a lot… let’s explain line by line.
In the first two lines we define the compiler g++
and the flags used to compile the files -std=c++17 -Iinclude
, the C++ standard 17 and the include directories.
Next we define the paths, src
, include
and the build directories as usual build/obj
and build/bin
(this time we won’t compile any library).
The target is the variable target, the file build/bin/main
. This is the main rule we are going to execute.
The sources and object files could be specified explicitly with
1
2
SRCS = src/matmul.cpp src/main.cpp
OBJS = build/obj/matmul.o build/obj/main.o
but, it’s better to use the wildcard
and patsubst
commands. The first command finds all the files in the SRC_DIR
that end with .cpp
, the second is used to substitute the names of the files ending with .cpp
to end with .o
, so we construct the object paths and names. This is very convenient if our sources are in the same directory, if we have a nested directory we can do this operation several times, one per path.
Next the all
rule. This one is the rule that is going to be executed when calling make
without specifying any rule. The default target.
The $(TARGET)
rule is (by substituting the variable) build/bin/main
but this rule has the requirements of the object files $(OBJ)
(that’s build/obj/matmul.o build/obj/main.o
). To build the objects first, the target needs to build $(OBJ_DIR)/%.o
, that expands all the objects paths. This rule creates the objects directory first and then compiles the objects (recall the -c
flag for compilation to object). The command $<
, as explained previously, represents the all the input (source files, prerequisites in this case) and $@
the name of the target output.
Going back to the rule $(TARGET)
after the objects have been compiled, we create first the binary directory and then link the objects. For that the -o
flag with $@
representing the input (build/bin/main
) and $^
all the prerequisites (all the object files).
Finally we write a clean
rule that removes the build dir. See that we define .PHONY
targets to be all
and clean
. A phony target is not associated with any actual file; instead, it represents an action or a command. By declaring a target as phony, you ensure that make
will always execute the associated recipe, regardless of whether a file with the same name as the target exists in the filesystem. Why use phony then? if a file named clean
exists in the directory, running make clean
without .PHONY
would check the timestamp of the clean
file and conclude that the target is “up to date.” This prevents the clean
recipe from running. Declaring clean
as a phony target ensures the recipe is executed regardless of such a file’s presence.
Another nice addition is a helper menu. Make the default all
to print the targets on screen. To do that let me modify part of the makefile. For simplicity I include the entire new makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Compiler and flags
CXX = g++
CXXFLAGS = -std=c++17 -Iinclude
# Directories
SRC_DIR = src
INCLUDE_DIR = include
BUILD_DIR = build
OBJ_DIR = $(BUILD_DIR)/obj
BIN_DIR = $(BUILD_DIR)/bin
# Target executable name
TARGET = $(BIN_DIR)/main
# # Find all source files and corresponding object files
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp, $(OBJ_DIR)/%.o, $(SRCS))
# Default target
all:
@echo "Available options:"
@echo " build - Build the project"
@echo " clean - Remove all build files"
@echo " help - Show this message"
# Build target
build: $(TARGET)
# Rule to build the executable
$(TARGET): $(OBJS)
@mkdir -p $(BIN_DIR)
$(CXX) $(CXXFLAGS) -o $@ $^
# Rule to build object files
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(OBJ_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Clean up build files
clean:
rm -rf $(BUILD_DIR)
# Help target
help: all
# Phony targets
.PHONY: all clean build help
Again run
1
2
3
4
make
make clean
make build
./build/bin/main
to display the help, clean the directory and build again (compile) the directory.
Wrap up
We showed how to compile an executable using make and a makefile. This tutorial could be expanded with building libraries (static, dynamic) and linking them. But this is just an extension of the logic presented here. Developers still use makefiles, they are easy to comprehend, widely adopted and efficient but cmake
(and other tools like bazel
) is used for bigger projects, we’ll show how to use this tool in a follow up post. But till then, congratulations! Now you can build C++ programs with make
!.