
Click the blue text above to follow me~
01
Introduction
Today I took some time to study Makefile and organized the information I found for quick reference during future reviews, helping beginners like me save time.
02
Preparation
First, let’s assume we have the following code files: main.cpp functions.h function1.cpp function2.cpp
--- functions.h ---// functions.hvoid print_hello();int factorial(int n);--- function1.cpp ---// function1.cpp#include "functions.h"int factorial(int n){ if (n!=1) return n*factorial(n-1); else return 1;}--- function2.cpp ---//function2.cpp#include<iostream>#include "functions.h"void print_hello(){ std::cout << "hello world" << std::endl;}--- main.cpp ---//main.cpp# include<iostream># include "functions.h"
int main(){ print_hello(); std::cout << "this is main" << std::endl; std::cout << "The factorial of 5 is " << factorial(5) << std::endl; return 0;}
03
Compiling Without Makefile
If you don’t use Makefile, you need to compile the above code as follows:
g++ -c function1.cppg++ -c function2.cppg++ -c main.cppg++ -o hello main.o function1.o function2.o
Here, g++ -c function1.cpp will compile the source code into an object file named function1.o. If you don’t want to use the default naming, you can customize the filename, for example:
g++ -c function1.cpp -o fun1.o
You can also combine the compile and link steps into one command:
g++ -o hello main.cpp function1.cpp function2.cpp
This method has many drawbacks, such as:
-
You need to manually type many commands each time you compile and link.
-
When the project is large, compiling the entire project can take a long time. Often, we do not modify all source files each time, so we hope the program can automatically compile only the modified sources without wasting time recompiling the unchanged parts.
To solve the first problem, we can save all the commands needed for compilation in a file and execute them with one click. For the second problem, we want a software that automatically detects which source files have been modified and selectively compiles them. The make command determines whether to compile based on the timestamps of the code files.
04
Compiling with Makefile
First Version of Makefile
First, you need to determine the name of the Makefile; it should be set to Makefile or makefile, and cannot be any other variation (MakeFile, Make_file, makeFile,…). Secondly, note that Makefile is sensitive to indentation; you cannot just add spaces at the beginning of a line. Let’s look at the first version of the Makefile.
# Makefile (# indicates a comment)all: g++ -o hello main.cpp function1.cpp function2.cppclean: rm -rf *.o hello
(Note that the indentation in the code snippet above is a
In this, all and clean are terms for targets, and I can also specify a name like abc; the actual compilation is executed by the commands indented below it. We can see that this command is no different from what we manually typed in the command line. Therefore, with this simple Makefile, we can avoid the pain of typing commands every time: just type make and hit enter to complete the compilation.
clean indicates to clear the compilation results; the command below it is a normal command line delete file command. Typing make will execute the commands under the first target (i.e., all) by default; to perform a clean operation, you need to enter make clean to specify the execution of the commands under the clean target.
This Makefile, while saving the trouble of typing commands, cannot selectively compile source code. Because we have crammed all the source files into one command, we have to compile the entire project every time, which is a waste of time. The second version of the Makefile will solve this problem.
Second Version of Makefile
Since we want to be able to selectively compile source files, we cannot compile all source files in one command as we did in the previous section; instead, we need to write them separately:
all: hellohello: main.o function1.o function2.o g++ main.o function1.o function2.o -o hellomain.o: main.cpp g++ -c main.cppfunction1.o: function1.cpp g++ -c function1.cppfunction2.o: function2.cpp g++ -c function2.cpp
clean: rm -rf *.o hello
The above Makefile contains an important syntax:
Following the logic in the code:
-
When you type make in the command line, it will default to execute the all target;
-
And the all target depends on hello, which does not exist in the current directory, so the program starts to read the commands below… finally finds the hello target;
-
When it is about to execute the hello target, it finds that it depends on main.o, function1.o, and function2.o, which do not exist in the current directory, so the program continues to execute;
-
When it encounters the main.o target, it depends on main.cpp. Since main.cpp exists in the current directory, it can finally compile and generate the main.o object file. The other two functions will be compiled in turn, and after all are compiled, it returns to the hello target, links various binary files, and generates the hello file.
The first time you compile, the command line will output:
g++ -c main.cppg++ -c function1.cppg++ -c function2.cppg++ main.o function1.o function2.o -o hello
This proves that all source code has been compiled. If we make a slight modification to main.cpp and then re-run make (without running make clean first), the command line will only show:
g++ -c main.cppg++ main.o function1.o function2.o -o hello
Thus, we have demonstrated the selective compilation feature of Makefile. Next, we will introduce how to declare variables in Makefile.
Third Version of Makefile
We want to consolidate the commands that need to be repeatedly typed into variables so that we can use the corresponding variable when needed. This way, if we need to modify these commands in the future, we can just change one line of code where they are defined.
CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: main.o function1.o function2.o $(CC) $(LFLAGS) main.o function1.o function2.o -o hellomain.o: main.cpp $(CC) $(CFLAGS) main.cppfunction1.o: function1.cpp $(CC) $(CFLAGS) function1.cppfunction2.o: function2.cpp $(CC) $(CFLAGS) function2.cpp
clean: rm -rf *.o hello
In the Makefile above, three variables are defined at the beginning: CC, CFLAGS, and LFLAGS. Here, CC indicates the chosen compiler (it can also be changed to gcc); CFLAGS indicates the compilation options, where -c is used in g++, and -Wall indicates that all warnings encountered during compilation will be displayed; LFLAGS indicates linking options, which do not include -c. These names are user-defined; what really matters is the content they hold, so as long as the following code correctly references them, it doesn’t matter what they are named. It is easy to see that when referencing variable names, you need to enclose them in $() to indicate that this is a variable name.
Fourth Version of Makefile
The third version of the Makefile is still not concise enough; for example, the contents of our dependencies often repeat those in the g++ command:
hello: main.o function1.o function2.o $(CC) $(LFLAGS) main.o function1.o function2.o -o hello
We don’t want to type so much; can we make good use of
$@ ,$<,$^
For example, we have the target: dependencies pair: all: library.cpp main.cpp
$@ refers to all, which is the target
$< refers to library.cpp, which is the first dependency
$^ refers to library.cpp and main.cpp, which are all dependencies
Therefore, the Makefile snippet at the beginning of this section can be modified to:
hello: main.o function1.o function2.o $(CC) $(LFLAGS) $^ -o $@
And the fourth version of the Makefile looks like this:
CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: main.o function1.o function2.o $(CC) $(LFLAGS) $^ -o [email protected]: main.cpp $(CC) $(CFLAGS) $<function1.o: function1.cpp $(CC) $(CFLAGS) $<function2.o: function2.cpp $(CC) $(CFLAGS) $<
clean: rm -rf *.o hello
However, typing file names manually is still a bit cumbersome. Can we automatically detect all the cpp files in the directory? Additionally, since main.cpp and main.o differ only by one suffix, can we automatically generate the object file names by changing the suffix of the source file to .o?
Fifth Version of Makefile
To achieve automatic detection of cpp files and automatic replacement of file name suffixes, we need to introduce two new commands: patsubst and wildcard.
wildcard
wildcard is used to obtain file names that match specific rules, for example, the following code:
SOURCE_DIR = . # If it's the current directory, it can also be omittedSOURCE_FILE = $(wildcard $(SOURCE_DIR)/*.cpp)target: @echo $(SOURCE_FILE)
After running make, the output will be all the .cpp files in the current directory:
./function1.cpp ./function2.cpp ./main.cpp
Where @echo before @ is to avoid command echoing. In the previous text, make clean called rm -rf, which would output this command in the terminal; if you add @ before rm, it will not be output.
patsubst
patsubst is probably short for pattern substitution. It allows you to easily change the suffix of .cpp files to .o. Its basic syntax is: $(patsubst original pattern, target pattern, file list). Run the following example:
SOURCES = main.cpp function1.cpp function2.cppOBJS = $(patsubst %.cpp, %.o, $(SOURCES))target: @echo $(SOURCES) @echo $(OBJS)
The output will be:
main.cpp function1.cpp function2.cppmain.o function1.o function2.o
Combining the above two commands, we can upgrade to the fifth version of Makefile:
OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: $(OBJS) $(CC) $(LFLAGS) $^ -o [email protected]: main.cpp $(CC) $(CFLAGS) $< -o [email protected]: function1.cpp $(CC) $(CFLAGS) $< -o [email protected]: function2.cpp $(CC) $(CFLAGS) $< -o $@
clean: rm -rf *.o hello
However, this version of Makefile still has room for improvement. The main.o, function1.o, and function2.o all use the same template but with different names. The sixth version of Makefile will address this issue.
Sixth Version of Makefile
Here we will use Static Pattern Rule, whose syntax is:
targets: target-pattern: prereq-patterns
Where targets are no longer a single target file but a list of target files. The target-pattern indicates the characteristics of the target files. For example, if the target files all end with .o, it can be represented as %.o, and the prereq-patterns (prerequisites) indicate the characteristics of the dependency files, for example, if the dependency files all end with .cpp, it can be represented as %.cpp.
Using the above method, any element in the targets list can find its corresponding dependency file. For example, through the main.o in targets, we can lock onto main.cpp.
Here is the sixth version of the Makefile:
OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: $(OBJS) $(CC) $(LFLAGS) $^ -o $@$(OBJS):%.o:%.cpp $(CC) $(CFLAGS) $< -o $@
clean: rm -rf *.o hello
05
Others
I noticed that some Makefiles set the -lm flag, which is found to link the math library because the code may include:
#include<math.h>
For example:
g++ -o out fun.cpp -lm
CC = g++LIBS = -lmout: fun.cpp $(CC) -o $@ $^ $(LIBS)
/ The End /
This article introduces how to write Makefile, with the main knowledge points being:
-
Defining and referencing variables in Makefile
-
Meaning of $^, $@, $<
-
Usage of wildcard and patsubst
-
Static pattern rule: targets: target-pattern: prereq-patterns
Reply “Makefile” in the public account to get the download link for the source file of this article.
Recommended Reading:
Disclaimer: This article is reproduced from Zhihu; the copyright belongs to the original author. If there are any copyright issues with the work, please contact me to delete it.

Scan to Follow Us
See More Embedded Cases

If you like this content, please give us a thumbs up to see more
