一.make和Makefile
当谈到 make
和 Makefile
时,通常是指构建工具 make
和用于描述编译和构建过程的文本文件 Makefile
。
make
是一个在类Unix系统中广泛使用的构建工具。它基于文件的时间戳比较,只编译发生了变化的文件,从而提高了编译效率。make
通过读取 Makefile
文件中的规则和命令来执行构建任务。
Makefile
是一个文本文件,其内容定义了一个或多个构建任务的规则。它包含了目标、依赖关系和构建命令。以下是 Makefile 文件中常用的元素:
目标(Target):目标是构建的结果,可以是可执行程序、静态库、对象文件等。每个目标都由一个或多个规则定义。
依赖关系(Dependencies):依赖关系指定了目标所依赖的文件或其他目标。当依赖项发生变化时,make
会自动重新构建相关的目标。
构建命令(Build Command):构建命令是构建目标所需的实际操作。它们通常是编译、链接和其他构建工具的命令。
Makefile
的规则由以下形式组成:
target: dependencies
build commands
其中:
target
是目标的名称,可以是一个文件名或一个逻辑名称。dependencies
是目标依赖的文件或其他目标。build commands
是构建目标所需的一系列命令。
例如,以下是一个简单的 Makefile
示例:
hello: main.o utils.o
gcc -o hello main.o utils.o
main.o: main.c
gcc -c main.c
utils.o: utils.c
gcc -c utils.c
上述 Makefile
中定义了一个目标 hello
,它依赖于 main.o
和 utils.o
这两个文件。对应的构建命令编译并链接这些文件以生成可执行文件 hello
。同时,Makefile
定义了 main.o
和 utils.o
这两个目标的依赖关系和构建命令。
当执行 make
命令时,它会读取当前目录下的 Makefile
文件并根据规则执行构建任务。make
会分析目标与依赖关系,并根据文件的时间戳判断是否需要重新构建目标。
通过使用 make
和 Makefile
,开发者可以方便地管理项目的编译过程,减少重复构建,提高开发效率。
![2023-09-17T05:58:27.png][1]
二.Makefile的重要性
Makefile具有以下几个重要的作用和重要性:
自动化构建:Makefile定义了项目的构建规则和依赖关系,使得构建过程可以自动化执行。通过执行make命令,构建工具会根据Makefile中的规则和文件的时间戳判断哪些文件需要重新构建,从而提高构建的效率。
管理复杂项目:对于大型和复杂的软件项目,构建过程可能涉及多个源文件、库文件和依赖项。Makefile可以清晰地定义这些依赖关系,确保所有的代码都按照正确的顺序编译和链接,避免遗漏和混乱。
跨平台构建:Makefile不仅适用于Unix和Linux系统,也广泛用于其他操作系统和平台。通过编写通用的Makefile,可以方便地实现不同平台上的构建,从而提高项目的可移植性和可扩展性。
灵活性和可定制性:Makefile提供了丰富的语法和功能,可以处理各种复杂的构建需求。开发者可以根据项目的需要,定制和调整Makefile中的规则和命令,以满足特定的构建要求。
版本控制和协作:Makefile通常与版本控制系统(如Git)结合使用,将构建过程的定义与代码一起进行版本管理。这样可以确保团队成员之间的构建环境一致,并提供一个统一的构建流程,方便项目的协作和迭代开发。
构建的可维护性:Makefile将项目的构建过程抽象出来,使其独立于具体的构建工具和平台。这样,在需要更改构建工具或切换平台时,只需要修改Makefile而不需要修改项目中的代码,提高了构建过程的可维护性和可持续性。
综上所述,Makefile在软件开发中扮演着重要的角色,它提供了一种规范和自动化的方式来管理和执行项目的构建过程,提高了开发效率、可维护性和可移植性,减少了错误和重复工作。
![2023-09-17T05:59:52.png][2]
三.Makefile三要素
Makefile中包含了三个重要的要素,它们是:
hello: main.o utils.o
gcc -o hello main.o utils.o
在上述示例中,hello
是目标的名称,它依赖于 main.o
和 utils.o
这两个文件,通过执行 gcc
命令将它们链接在一起生成可执行文件 hello
。
make
会自动重新构建相关的目标。例如:hello: main.o utils.o
上述示例中,hello
这个目标依赖于 main.o
和 utils.o
这两个文件,当它们发生变化时,make
将会重新构建 hello
这个目标。
hello: main.o utils.o
gcc -o hello main.o utils.o
在上述示例中,gcc -o hello main.o utils.o
是构建命令,它使用 gcc
编译器将 main.o
和 utils.o
这两个文件链接在一起生成可执行文件 hello
。
通过定义目标、依赖关系和构建命令,Makefile 提供了一种描述项目构建过程的方式,通过执行 make
命令,可以根据 Makefile 中的规则自动执行构建任务。这三个要素共同构成了 Makefile 的基本结构和功能。
四.伪目标
伪目标(Phony Target)是在Makefile中定义的一种特殊目标,它并不对应真实的文件,而是表示一个动作或一组命令。
通常情况下,目标在Makefile中的定义会与一个实际的文件名相关联。但是有些时候,我们可能需要定义一些仅用于执行一些操作或命令的特殊目标,这时就可以使用伪目标。
伪目标不会检查与其同名的文件是否存在或文件的时间戳,它每次都会执行定义的命令,无论依赖项是否改变。
定义伪目标时,需要使用 .PHONY
声明。例如:
.PHONY: clean
clean:
rm *.o
上述示例中,.PHONY
声明了 clean
为伪目标。当执行 make clean
命令时,Makefile会执行 rm *.o
命令,删除所有 .o
后缀的文件。
常见的伪目标包括 clean
(用于清理生成的文件)、all
(执行所有构建操作)等。
使用伪目标可以方便地扩展Makefile的功能,例如定义一些常用的操作或构建命令的集合。同时,它也提高了Makefile的可读性和维护性,使得构建过程更加灵活和易于管理。
五.Makefile变量
在Makefile中,系统变量是预定义的变量,用于访问系统相关的信息。这些变量可以用于配置编译器、链接器、编译选项等。
以下是常见的Makefile系统变量:
CC
:C编译器的路径和名称。默认为cc
。CXX
:C++编译器的路径和名称。默认为g++
。LD
:链接器的路径和名称。默认为cc
。AR
:静态库归档工具的路径和名称。默认为ar
。RM
:删除文件的命令。默认为rm
。CFLAGS
:C编译器的选项和标志。例如,CFLAGS=-O2 -Wall
可以设置编译器优化级别和显示警告。CXXFLAGS
:C++编译器的选项和标志。LDFLAGS
:链接器的选项和标志。可以用来指定库文件的路径和链接其他库等。CPPFLAGS
:C和C++编译器的预处理选项和标志。通常用于定义宏或包含路径。SHELL
:默认的Shell解释器。默认为系统的默认Shell。这些系统变量可以在Makefile中进行自定义,以满足项目的需求。通过设置这些变量的值,可以修改编译器、链接器和编译选项,从而灵活地控制项目的编译和构建过程。
例如,可以通过定义 CC
变量来指定使用的C编译器:
CC = clang
或者设置 CFLAGS
变量来指定编译选项:
CFLAGS = -O2 -Wall -I/path/to/includes
利用这些系统变量,可以轻松地管理项目的构建过程,并根据需要进行定制化调整。
2.自定义变量
2.1 = 延迟赋值
当使用 =
运算符进行赋值时,变量的值会在使用时
立即展开并计算。这意味着如果在后续的代码中修改了变量的值,那么之前使用该变量的地方也会立即反映这个改变。
例如:
VAR = foo
TARGET = $(VAR)
$(info TARGET: $(TARGET)) # 输出: TARGET: foo
VAR = bar
$(info TARGET: $(TARGET)) # 输出: TARGET: bar
在上述示例中,使用 =
运算符对 TARGET
进行赋值,初始时 VAR
的值为 foo
,因此 TARGET
的值也是 foo
。然后,修改了 VAR
的值为 bar
,因此在下一次使用 TARGET
时,它的值也会变为 bar
。
2.2 := 立即赋值
相反,当使用 :=
运算符进行赋值时,变量的值会立即计算并展开,并在定义时就固定下来,不会受其后定义的变量值的影响。
例如:
VAR := foo
TARGET := $(VAR)
$(info TARGET: $(TARGET)) # 输出: TARGET: foo
VAR := bar
$(info TARGET: $(TARGET)) # 输出: TARGET: foo
在上述示例中,使用 :=
运算符对 TARGET
进行赋值,初始时 VAR
的值为 foo
,因此 TARGET
的值也是 foo
。即使在后续将 VAR
的值修改为 bar
,TARGET
的值仍保持不变,仍然是最初展开时的 foo
。
通过选择适当的赋值运算符,可以控制变量的延迟赋值行为,从而灵活地管理变量的值。
2.3 ?= 空赋值
在Makefile中,?=
运算符用于进行空赋值。它的作用是在变量未定义时,将一个新的值赋给该变量。
当使用?=
运算符进行赋值时,如果该变量还未定义,那么它会被赋予指定的值;如果该变量已经定义了,那么赋值操作不会执行,变量的值保持不变。
以下是一个示例:
VAR ?= default_value
$(info VAR: $(VAR))
VAR := new_value
$(info VAR: $(VAR))
在上述示例中,首先使用?=
运算符进行赋值,将default_value
赋给VAR
。然后输出VAR
的值,结果为default_value
。
接着,使用:=
运算符进行赋值,将new_value
赋给VAR
。再次输出VAR
的值,结果为new_value
。
在这个示例中,?=
运算符只在变量未定义时执行赋值操作,因此它并不会覆盖已有的变量值。
使用?=
运算符可以用来设置默认值,确保变量在未定义时有一个合适的初始值。
需要注意的是,如果变量已经定义了,并且你希望在重新赋值时覆盖原有的值,那么应该使用:=
或=
运算符,而不是?=
运算符。
2.4 += 追加赋值
在Makefile中,+=
运算符用于进行追加赋值。它的作用是将一个新的值追加到变量的末尾。
当使用+=
运算符进行赋值时,它会将右边的值追加到左边的变量的末尾。
以下是一个示例:
VAR := initial_value
VAR += appended_value
$(info VAR: $(VAR))
在上述示例中,首先将VAR
赋值为initial_value
。然后使用+=
运算符,将appended_value
追加到VAR
的末尾。最后输出VAR
的值,结果为initial_value appended_value
。
通过使用+=
运算符,可以方便地将新的值追加到变量中,而不是覆盖原有的值。
需要注意的是,如果变量之前未定义,那么+=
运算符会与=
运算符的行为相同,即创建一个新的变量并赋予相应的值。
VAR += appended_value
$(info VAR: $(VAR))
在上述示例中,由于VAR
之前未定义,+=
运算符的行为等同于=
运算符,创建了一个新的变量VAR
并赋予值appended_value
。最后输出VAR
的值,结果为appended_value
。
因此,在使用+=
运算符之前,请确保变量已经定义。
3.自动化变量
在Makefile中,自动化变量(Automatic Variables)是一些特殊的变量,它们在规则(rule)的命令中具有特殊的含义,用于表示不同的上下文信息。当Makefile执行规则时,自动化变量会根据当前的规则上下文被展开为相应的值。
以下是一些常用的自动化变量:
$@
:表示规则的目标(target),即正在生成的文件的名称。$