[C++ Advanced] Makefile Basics (1)

Makefile is actually just a command file that instructs the make program (hereafter referred to as make or sometimes called make command) how to work for us. When we say Makefile is actually talking about make, we must have a clear understanding of this. For our project, Makefile refers to the compilation environment of the software project. The most common work content in the coding phase of software product development is roughly:

  • Developers code based on outline design
  • The developer compiles the designed source code to produce an executable
  • Developers test software products to verify correctness of their functionality

The above three steps are an iterative process. If the correctness of the design is finally verified to meet the requirements, then the development of the coding phase is completed. If not, these three steps must be repeated until the design requirements are met.

Among the above steps, the second step is the most related to the Makefile, so what influence does the quality of the Makefile have on the project development? With a well-designed Makefile, when we recompile, we only need to compile those files that have been modified since the last successful compilation, that is to say, a delta is compiled instead of the entire project. Conversely, if there is a bad Makefile environment, it may be necessary to clean for each compilation, and then recompile the entire project. The difference between the two cases is obvious, the latter will consume a lot of time for developers to compile, which means low efficiency. For small projects, the inefficiency may not be obvious, but for relatively large projects, it is very obvious. Developers may do ten compilations a day (or even less) and have no time for coding and testing (debugging). That's why usually a large project will have a small team dedicated to maintaining the Makefile
to support product development.

The most important thing is to master two concepts, one is target and the other is dependency. The goal refers to what to do, or what is generated after running make, and the dependency tells make how to do it to achieve the goal. In a Makefile, targets and dependencies are expressed through rules. What we are most familiar with is to use make to compile the code of software products, but it can be used to do many things, and we will give some examples of not using make to compile code later. To control Makefile, the most important thing is to learn to use goals and dependencies to think about the problems to be solved.


insert image description here

  • Targets: The targets in the Makefile refer to the files that need to be generated or the operations that need to be performed. A target can be a file, a command, or a sequence of operations.
  • Dependencies: The dependencies in the Makefile refer to the files or commands that the target depends on. If dependent files are changed, then the target also needs to be regenerated.
  • Commands: The commands in the Makefile refer to the sequence of operations that need to be performed to generate the target. These operations can be compilation, linking, copying, packaging, etc. Commands must start with a tab or multiple spaces, otherwise they will be treated as comments.

insert image description here
Makefile is a text file that contains rules and instructions that describe how to compile and link one or more source code files to generate executable programs or library files.

The Makefile works as follows:

  1. Makefiles define target files, dependencies, and commands. Object files are usually executable programs or library files, dependent files are source code files, header files, or other dependencies, and commands are the operations of compiling, linking, and generating object files.

  2. When the make command is executed, the rules in the Makefile will be parsed, and a dependency graph will be generated according to the dependencies to determine which files need to be recompiled.

  3. The Make program recursively performs the operations of compiling, linking, and generating object files according to the dependency graph and rules, ensuring that all dependencies are compiled and linked to generate the final object file.

  4. If some dependencies have not changed, there is no need to recompile and link, which improves compilation efficiency.

  5. Makefile also supports advanced features such as variables, conditional statements, and loop statements, and can be compiled and linked according to different conditions to generate different target files.

1. Environment

The environment requirements for using the makefile are as follows:

  1. Operating system: makefiles can be used on most operating systems, including Linux, Unix, Mac OS X, Windows, etc.
  2. Compiler: makefile requires a compiler that supports GNU make syntax, such as GNU make, BSD make, etc.
  3. Object files: makefiles require compilable source code files, such as C, C++, Java, etc.
  4. Environment variables: Makefile needs some environment variables to specify the compiler, compilation options, etc., such as CC, CFLAGS, LDFLAGS, etc.
  5. Editor: Makefile needs an editor to write and edit makefiles, such as Vim, Emacs, etc.
  6. make tool: makefile requires a make tool to execute the makefile, such as GNU make.

Steps for usage:

  1. Install the GNU Make tool: Make is a command-line tool used to automate the process of building software. You can download and install the Make tool from the GNU official website.
  2. Create a Makefile: A Makefile is a text file that contains a series of rules and instructions that describe how to build software. A file named Makefile can be created in the project root directory.
  3. Write Makefile rules: Makefile rules consist of targets, dependencies, and commands. The target refers to the file to be generated or the operation to be performed; the dependency refers to the prerequisite for generating the target; the command refers to the specific operation of generating the target.
  4. Run the Make command: enter the root directory of the project on the command line, enter the make command to execute the rules defined in the Makefile, generate target files or perform operations.

Enter the command line make -v, if the version information similar to the figure below appears, it means that make is already available in your environment:
insert image description here

Precautions:

  1. When writing Makefile, variables and functions should be used as much as possible to improve the readability and maintainability of the code.
  2. Commands in a Makefile must begin with the Tab key, not a space bar.
  3. Dependencies in a Makefile should be as explicit as possible in order to correctly determine which rules need to be executed.
  4. Before executing the Make command, you should ensure that all dependent files already exist, otherwise the build will fail.

2. Rules

We use Hello World to start learning the Makefile rules, write a Makefile as follows, and the storage directory of the file can be arbitrary:

all:
	echo "Hello World"

insert image description here

It should be noted that there must be only TAB in front of echo , and there must be at least one TAB, and spaces cannot be used instead.

The first very important concept in Makefile is the target. All in the above code is our target. The target is placed in front of: and its name can be composed of letters and underscores. echo “Hello World”It is the command to generate the target. These commands can be any commands running in your environment and functions defined by make, etc. Here, echo is a command in BASH Shell, and its function is to print strings to the terminal. The goal of all here is to print "Hello World" on the terminal, and sometimes the goal will be a more abstract concept. The definition of the all target actually defines how to generate the all target, which is called a rule, that is, the above Makefile defines a rule for generating the all target.

The following example shows three different ways of running and the results of each way:

  • The first way: as long as you run makethe command in the directory where the Makefile is located, two lines will be output on the terminal, the first line is actually the command we wrote in the Makefile, and the second line is the result of running the command
  • The second way: run make allthe command, which tells the make tool that I want to generate the target all, and the result is the same as the first way
  • The third way: run make test, instructs make to generate the test target for us. Since we didn't define the test target at all, the running result is predictable, make does report that the test target cannot be found

insert image description here
Now make a small change to the Makefile above, as shown below, adding the test rule to build the test target, so as to print "Just for test!" on the terminal:

all:
	echo "Hello World"
test:
	echo "Just for test!"

insert image description here
From the above output we can find:

  • Multiple targets can be defined in one Makefile
  • When calling makea command, we have to tell it what our target is, that is, what we want it to do. When no specific target is specified, then make uses the first target defined in the Makefile as the target for this run. This first goal is also called the default goal (it has nothing to do with whether it is all)
  • When make gets the target, it first finds the rules that define the target, and then runs the commands in the rules to achieve the purpose of building the target. In the Makefile shown now, there is only one command in each rule, but in the actual Makefile, each rule can contain many commands

As with the previous example, when run make, the commands in the Makefile are also printed to the terminal. Sometimes this is not desirable, as it can make the output look a little confusing. To make makethe command not to be printed out, just make a small modification, the modified Makefile is as follows, that is, add a before the command @. This symbol tells make, do not display this line of command at runtime:

all:
	@echo "Hello World"
test:
	@echo "Just for test!

insert image description here
Make a little change to the above code, :add the test target after the all target, as shown below

all: test
        @echo "Hello World"
test:
        @echo "Just for test!"

insert image description here


Let's explain the dependencies in the Makefile

In the above code, the test after the all target tells make that the all target depends on the test target, and this dependent target is also called a prerequisite in the Makefile. When such a target dependency occurs, the make tool will first build each target that the rule depends on in order from left to right. If you want to build the all target, then make will have to build the test target before building it, which is why it is called a prerequisite. The following class diagram expresses the dependencies of all targets:

insert image description here

So far, we understand the rules in the Makefile, the following is the text and UML of the rules. A rule is made up of targets, prerequisites, and commands. It should be pointed out that the expression between the target and the prerequisite is the dependency (dependency), which indicates that the prerequisite must be satisfied (or built) before the target is built; the prerequisite can be other targets, When a prerequisite is a target, it must first be built.

targets : prerequisites
	command

insert image description here
There can be multiple targets in a rule. When there are multiple targets and this rule is the first rule in the Makefile, if we run the make command without any target, the first target in the rule will be regarded as is the default target, as follows:

all test:
	@echo "Hello World"

insert image description here
The activity diagram of make processing a rule is shown in the figure below. The activity of building dependent target(s) (note that it is an activity, not an action) is to repeat the same activity as shown in the figure below. You Can be seen as a recursive call to the activity diagram below. The run command to build target (run command to build target) is an action, which is an action composed of commands. The difference between an activity and an action is that an action does only one thing (but can have multiple commands), while an activity can include multiple actions.
insert image description here

3. Principle

Next, we try to apply the rules to program compilation. Below we assume that there are two source program files used to create a simple executable file. We need to write a Makefile for creating a simple executable program. How should this Makefile be compiled? Write?
foo.c

#include <stdio.h>
void foo ()
{
    
    
	printf ("This is foo()\n");
}

main.c

extern void foo();
int main ()
{
    
    
	foo();
	
	return 0;
}

The first step in writing a Makefile is not to jump in and try to write a rule, but to use a dependency-oriented method to figure out what kind of dependencies the Makefile to write needs to express. This is very important. Through continuous practice, we can finally achieve a natural use of dependencies to think about problems. At that time, when you write Makefile again, your mind will be very clear about what you are writing and what you will write later. Now put aside the Makefile, let's take a look at what the dependencies of the simple program are.

The first dependency graph that springs to mind, where the simple executable is obviously produced by compiling and linking main.c and foo.c at the end. Through this dependency graph, a Makefile can actually be written. The Makefile written by such dependencies is not very feasible in reality, that is, you have to put all the source programs in one line and let GCC compile them for us: the following figure is a more precise expression of the dependencies of the simple program
insert image description here
. Which added the object file. For a simple executable program, the figure below shows its "dependency tree". The next thing to do is to express each of the dependencies, that is, each of the dotted lines with arrows, with the rules in the Makefile:
insert image description here

all: main.o foo.o
        gcc main.o foo.o -o simple
main.o: main.c
        gcc main.c -c
foo.o: foo.c
        gcc foo.c -c

.PHONY:clean
clean:
        rm -f main.o foo.o simple

In this Makefile, I also added a pseudo-target to delete generated files, including object files and simple executable programs, which are very common in real projects.
insert image description here
What happens if we recompile without changing the code? The figure below shows the results. Notice that the second compilation does not have the action of building the target file, but why is there an action of building the simple executable program?
insert image description here
Makefile will judge whether the file needs to be rebuilt according to the timestamp of the file (that is, the last modification time). If a file has an older timestamp than the files that depend on it, the file needs to be rebuilt. Therefore, if you execute the make command multiple times, even if the source and header files have not changed, the executable's timestamp will be updated, causing a rebuild. If you want to avoid this, you can use make's incremental build feature, which only rebuilds the necessary files.

Let's verify that if we make a change to foo.c, it will rebuild. For the make tool, whether a file is changed is not based on the size of the file, but its timestamp. Under Linux, you only need to use the touch command to change the timestamp of the file, which is equivalent to simulating an edit to the file without actually editing it. As shown in the figure, make finds the change of foo.c, and recompiled it:
insert image description here

4. False targets

In Makefile, a pseudo-target is a special target that does not represent an actual file, but is used to complete a specific task or organize the execution sequence of other targets.

Suppose we have a C language project, including the following files: main.c, foo.c, bar.c, foo.h, bar.h. We need to compile this project to produce an executable my_program. A simple Makefile might look like this:

my_program: main.o foo.o bar.o
	gcc -Wall -g -o my_program main.o foo.o bar.o

main.o: main.c foo.h bar.h
	gcc -Wall -g -c main.c

foo.o: foo.c foo.h
	gcc -Wall -g -c foo.c

bar.o: bar.c bar.h
	gcc -Wall -g -c bar.c

clean:
	rm -f *.o my_program

In this Makefile, we have a cleantarget named . It does not depend on other targets, nor does it represent an actual file. Its role is to delete all intermediate files ( .ofiles) and generated executable files ( my_program), which is a typical pseudo-target.

Main features and uses of false targets:

  • Does not represent an actual file: The pseudo-target does not correspond to any actual file, it exists only to complete a specific task
  • Avoid name collisions: Since pseudo-targets do not represent actual files, we can avoid errors caused by file and target names being the same
  • Better organize Makefiles: With pseudo-targets, we can separate different tasks and operations, making Makefiles clearer and easier to read
  • Enforcing: Using pseudo-targets, we can enforce a certain task regardless of whether the file exists or has been updated

In the Makefile, we can use `.PHONY`` to declare a pseudo-target to explicitly tell make that the target is not an actual file. For example, we could add the following declaration to the above example:

.PHONY: clean

The advantage of this is that even if there is a file named make in the current directory clean, make will know that cleanit is a pseudo-target and not an actual file.

Of course, in addition to the above-mentioned clean pseudo-targets, there are other common pseudo-targets. The following are some pseudo-targets that are often used in Makefiles:

  1. all: This pseudo-target is usually used to compile the entire project. When the user executes make or make all, it will automatically compile and generate all required targets.
    .PHONY: all
    all: my_program
    
  2. install: This pseudo target is used to install the compiled program to the directory specified by the system. Typically, this requires administrator privileges, as it involves creating or modifying files in system directories.
    .PHONY: install
    install: my_program
    	cp my_program /usr/local/bin
    
  3. uninstall: This pseudo target is used to remove installed programs from the system. Like install, it usually requires administrator privileges.
    .PHONY: uninstall
    uninstall:
    	rm -f /usr/local/bin/my_program
    
  4. test: This pseudo-target is used to run the project's test cases, ensuring that various parts of the project work correctly.
    .PHONY: test
    test: my_program
    	./test_script.sh
    
  5. help: This pseudo-target is used to display the instructions of the Makefile to help users understand how to use the Makefile.
    .PHONY: help
    help:
    	@echo "Usage:"
    	@echo "  make all      - Compile the project"
    	@echo "  make clean    - Remove compiled files and binaries"
    	@echo "  make install  - Install the program"
    	@echo "  make test     - Run tests"
    

Guess you like

Origin blog.csdn.net/weixin_52665939/article/details/130131272