一.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中包含了三个重要的要素,它们是:
- 目标(Target):目标是构建的结果,可以是可执行程序、静态库、对象文件等。每个目标都由一个或多个规则定义。例如:
hello: main.o utils.o
gcc -o hello main.o utils.o
在上述示例中,hello
是目标的名称,它依赖于 main.o
和 utils.o
这两个文件,通过执行 gcc
命令将它们链接在一起生成可执行文件 hello
。
- 依赖关系(Dependencies):依赖关系指定了目标所依赖的文件或其他目标。当依赖项发生变化时,
make
会自动重新构建相关的目标。例如:
hello: main.o utils.o
上述示例中,hello
这个目标依赖于 main.o
和 utils.o
这两个文件,当它们发生变化时,make
将会重新构建 hello
这个目标。
- 构建命令(Build Command):构建命令是构建目标所需的实际操作。它们通常是编译、链接和其他构建工具的命令。构建命令前需要以一个 Tab 键作为缩进。例如:
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),即正在生成的文件的名称。$<
:表示规则的第一个条件(dependent),即触发规则的文件的名称。$^
:表示规则的所有条件(dependents)的列表,使用空格分隔。
下面是一个示例:
all: hello_world
hello_world: main.o utils.o
gcc $^ -o $@
main.o: main.c
gcc -c $< -o $@
utils.o: utils.c
gcc -c $< -o $@
在上述示例中,$@
、$<
和$^
都是自动化变量。
在hello_world
目标的规则中,$@
表示hello_world
,$^
表示main.o utils.o
。
在main.o
和utils.o
目标的规则中,$@
分别表示main.o
和utils.o
,$<
表示main.c
和utils.c
。
自动化变量使得规则中的命令可以以通用的方式操作目标和条件,这样可以更灵活和简洁地编写Makefile规则。
六.模式匹配
在Makefile中,模式匹配(Pattern Matching)是一种功能强大的特性,用于匹配和处理文件名或目录名。
Makefile中的模式匹配使用通配符来表示规则中的文件名或目录名模式。以下是常用的通配符:
%
:表示匹配任意字符序列。*
:表示匹配零个或多个字符。?
:表示匹配一个字符。
使用模式匹配可以在规则中根据文件名或目录名的模式执行相应的操作,常用的模式匹配语法包括:
%.ext
:匹配所有以.ext
为扩展名的文件,如%.c
匹配所有以.c
结尾的C语言源文件。dir/%
:匹配指定目录下的任意文件名,如src/%.c
匹配src/
目录下的所有以.c
结尾的文件。
下面是一个示例:
# 匹配所有以.c为扩展名的文件
%.o: %.c
gcc -c $< -o $@
# 匹配src目录下的所有.c文件
src/%.o: src/%.c
gcc -c $< -o $@
在上述示例中,第一个规则使用了%.o: %.c
的模式匹配,匹配所有以.c
为扩展名的文件。当目标名为.o
文件时,会执行gcc -c $< -o $@
命令编译对应的.c
文件生成.o
文件。
第二个规则使用了src/%.o: src/%.c
的模式匹配,匹配src/
目录下的.c
文件。当目标名为src/
目录下的.o
文件时,会执行相应的命令。
通过使用模式匹配,可以更加灵活和通用地定义规则,减少重复的代码和规则数量。这样可以方便地适应多个文件或目录的构建需求。
七.Makefile条件分支
条件分支是Makefile中的一种功能,它允许根据不同的条件执行不同的操作。Makefile中的条件分支语句包括ifeq、ifneq、ifdef和ifndef。
- ifeq/ifeq “条件1” “条件2”
ifeq条件语句用于判断两个条件是否相等。如果条件1等于条件2,则执行后续的操作。
ifeq ($(TARGET), prog)
# 执行操作
else
# 执行其他操作
endif
在上述示例中,如果变量TARGET的值等于"prog",则执行"执行操作"的部分;否则执行"执行其他操作"的部分。
- ifneq/ifneq “条件1” “条件2”
ifneq条件语句用于判断两个条件是否不相等。如果条件1不等于条件2,则执行后续的操作。
ifneq ($(TARGET), lib)
# 执行操作
else
# 执行其他操作
endif
在上述示例中,如果变量TARGET的值不等于"lib",则执行"执行操作"的部分;否则执行"执行其他操作"的部分。
- ifdef “变量名”
ifdef条件语句用于判断指定的变量是否已定义。如果变量已定义,则执行后续的操作。
ifdef MY_VARIABLE
# 执行操作
else
# 执行其他操作
endif
在上述示例中,如果变量MY_VARIABLE已定义,则执行"执行操作"的部分;否则执行"执行其他操作"的部分。
- ifndef “变量名”
ifndef条件语句用于判断指定的变量是否未定义。如果变量未定义,则执行后续的操作。
ifndef MY_VARIABLE
# 执行操作
else
# 执行其他操作
endif
在上述示例中,如果变量MY_VARIABLE未定义,则执行"执行操作"的部分;否则执行"执行其他操作"的部分。
条件分支语句可以根据不同的条件执行不同的操作,使Makefile更加灵活和定制化。根据需要可以嵌套使用条件分支语句,以满足更复杂的条件判断和操作需求。
八.Makefile常用函数
1.patsubst
patsubst 是 Makefile 中的一个常用函数,用于将一组字符串列表中符合特定模式的字符串进行替换。
语法如下:
$(patsubst PATTERN, REPLACEMENT, TEXT)
参数解释:
- PATTERN: 需要匹配的模式,可以包含通配符 %,表示任意字符序列。
- REPLACEMENT: 替换匹配到的模式的字符串。
- TEXT: 需要进行替换操作的字符串列表。
返回值:将 TEXT 中符合 PATTERN 的部分替换为 REPLACEMENT 后的结果。
使用示例:
# 将 FILE_LIST 中的 ".c" 后缀替换为 ".o"
FILE_LIST = main.c helper.c utils.c
OBJ_FILES = $(patsubst %.c, %.o, $(FILE_LIST))
# OBJ_FILES 的结果为 "main.o helper.o utils.o"
在上述示例中,我们使用 patsubst 函数将 FILE_LIST 中的 “.c” 后缀替换为 “.o”,并存储到 OBJ_FILES 变量中。最终,OBJ_FILES 的结果为 “main.o helper.o utils.o”。
patsubst 通常用于生成源文件和目标文件之间的对应关系,或者进行一些文件重命名操作。通过灵活使用 patsubst 函数,可以简化 Makefile 中的规则定义和文件操作。
2.notdir
notdir 是 Makefile 中的一个常用函数,用于从一个文件路径中提取出文件名部分,去除路径部分。
语法如下:
$(notdir NAME)
参数解释:
- NAME: 需要提取文件名的路径字符串。
返回值:提取出的文件名部分。
使用示例:
# 提取文件名
SRC_PATH = src/main.c
SRC_FILE = $(notdir $(SRC_PATH))
# SRC_FILE 的结果为 "main.c"
在上述示例中,我们使用 notdir 函数从 SRC_PATH 中提取出文件名部分,并将结果存储到 SRC_FILE 变量中。最终,SRC_FILE 的结果为 “main.c”。
notdir 函数通常用于获取文件路径中的文件名部分,方便在 Makefile 中操作文件时使用。使用 notdir 函数可以简化路径处理,并提取出需要的文件名部分,使得构建过程更加灵活和可读性更高。
3.wildcard
wildcard 是 Makefile 中的一个常用函数,用于获取符合指定模式的文件名列表。
语法如下:
$(wildcard PATTERN)
参数解释:
- PATTERN: 需要匹配的模式,可以包含通配符 “*” 和 “?”,用于匹配任意字符。
返回值:符合 PATTERN 匹配条件的文件名列表。
使用示例:
# 获取所有 ".c" 文件列表
C_FILES = $(wildcard *.c)
# 获取所有以 "test_" 开头的文件列表
TEST_FILES = $(wildcard test_*)
# 获取所有在 "src/" 目录下的 ".c" 文件列表
SRC_C_FILES = $(wildcard src/*.c)
在上述示例中,我们使用 wildcard 函数获取不同模式下的文件名列表:
$(wildcard *.c)
获取当前目录下的所有 “.c” 文件列表。$(wildcard test_*)
获取当前目录下以 “test_” 开头的文件列表。$(wildcard src/*.c)
获取 “src/” 目录下的所有 “.c” 文件列表。
使用 wildcard 函数可以方便地获取符合特定模式的文件列表,可以在 Makefile 中灵活使用这些文件名列表进行规则定义和操作。
4.foreach
foreach 是 Makefile 中的一个常用函数,用于迭代一个列表,并对每个元素执行相应的操作。
语法如下:
$(foreach VAR, LIST, TEXT)
参数解释:
- VAR: 迭代时的变量名,用于表示 LIST 中的每个元素。
- LIST: 需要迭代的列表。
- TEXT: 针对每个元素执行的操作。
返回值:执行操作后的结果。
使用示例:
# 输出每个元素
FRUITS = apple banana cherry
$(foreach fruit, $(FRUITS), $(info Fruit: $(fruit)))
# 生成目标依赖
OBJECTS = file1.o file2.o file3.o
$(foreach obj, $(OBJECTS), $(eval $(obj): $(obj:.o=.c)))
# 清理文件
FILES = file1.txt file2.txt file3.txt
clean:
rm -f $(foreach file, $(FILES), $(file))
在上述示例中,我们使用 foreach 函数对列表中的每个元素进行迭代:
- 第一个示例使用 foreach 打印 FRUITS 列表中每个元素的名称。
- 第二个示例使用 foreach 生成目标依赖规则,将 OBJECTS 中的 .o 文件转换为对应的 .c 文件作为依赖。
- 第三个示例使用 foreach 在 clean 目标中清理 FILES 列表中的文件。
通过使用 foreach 函数,可以方便地对列表进行迭代操作,并根据需要执行相应的操作。这在 Makefile 中非常有用,可以生成规则、处理文件列表等。
5.| 符号
在 Makefile 中,竖线符号 “|” 有多种用途。
-
伪目标(PHONY target):在规则前使用 “|” 符号表示该规则是一个伪目标,即无论目标文件是否存在,每次都会执行所定义的命令。
例如:
.PHONY: clean clean: rm -rf *.o | echo "Clean complete"
在上述示例中,使用 “.PHONY: clean” 声明了一个伪目标 “clean”,在执行 “make clean” 命令时,无论是否存在 clean 文件,将总是执行 “rm -rf *.o” 命令,并输出 “Clean complete”。
-
强制规则(Order-only Prerequisites):在依赖项之前使用 “|” 符号,可以将其标记为强制规则。这样,在执行规则时,只有当依赖项的时间戳旧于目标文件的时间戳时,才会执行命令。如果依赖项的时间戳是最新的,Makefile 不会重新执行命令。
例如:
obj_files: | obj_dir gcc -c -o obj_files main.c obj_dir: mkdir obj_dir
在上述示例中,通过在依赖项 “obj_files” 前使用 “|” 符号,将依赖项 “obj_dir” 标记为强制规则。这意味着只有当目录 “obj_dir” 不存在时,才会创建该目录,然后再执行编译命令。如果目录已经存在,则不会重新执行命令。
-
逻辑或(Logical OR):在命令行中使用 “|” 符号可实现多个命令逻辑上的或关系,即只要其中一个命令执行成功,整个命令被认为执行成功。
例如:
clean: rm -rf *.o | true
在上述示例中,如果某些文件已被删除,将返回非零的退出码,但在命令行中使用 “|” 符号和 “true” 命令,即使删除文件失败,整个命令被认为执行成功,不会导致 make 命令停止执行。
这些是竖线符号 “|” 在 Makefile 中的常见用法,具体使用方法会根据具体情况而定。
九.Makefile解决头文件依赖
在 Makefile 中解决头文件依赖的常用方法是使用自动化变量和自动生成依赖关系。这样可以确保在头文件修改后,只重新编译受影响的源文件,而不是整个项目。
以下是一种常见的 Makefile 配置来解决头文件依赖:
CC = gcc
CFLAGS = -Wall -Wextra -Iinclude
SRC_DIR = src
OBJ_DIR = obj
# 查找源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(addprefix $(OBJ_DIR)/, $(notdir $(SRCS:.c=.o)))
# 自动生成依赖关系
DEPS = $(OBJS:.o=.d)
-include $(DEPS)
# 声明主目标
TARGET = app
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
clean:
rm -rf $(OBJ_DIR)/*.o $(OBJ_DIR)/*.d $(TARGET)
上述示例中的关键点解释如下:
wildcard
函数用于查找源文件(.c 文件)。notdir
函数用于获取源文件的文件名,去除路径。addprefix
函数用于添加目标文件夹路径前缀作为输出目录。- 自动生成的依赖关系文件使用了
-MMD -MP
编译选项,这样会在编译源文件时自动生成对应的 .d 文件,并包含在 Makefile 中,实现头文件依赖的自动化。 -include
用于包含所有的依赖文件,这样可以确保在第一次构建时生成的 .d 文件也能够被正确地包含进来。- 每个目标文件的规则中,使用了
-MMD -MP
编译选项,以便自动生成依赖关系文件。同时,也使用了-c
选项来只进行编译而不进行链接操作。 clean
目标用于清理生成的目标文件,依赖文件和目标应用程序。
通过以上的配置,Makefile 将会根据源文件的修改和头文件的依赖关系,自动检测出需要重新编译的目标文件,并只编译那些受影响的文件,从而提高编译效率并减少不必要的重新编译。