Makefile 简明指南
Makefile 简明指南
一. Makefile变量
在 Makefile 里,你可以通过不同方式来声明新变量 ne。在 Makefile 里,变量声明时等号(=、:=、+=、?=)两边的空格是可选的,加空格或者不加空格都不会影响变量的赋值。下面是常见的几种声明方式及其特点:
1. 递归展开变量赋值(=)
在 Makefile 里,递归展开变量是借助 = 符号或者 define 指令来定义的。这种变量赋值方式的显著特点是:在定义变量时,所指定的值会按原样保存,若其中包含对其他变量的引用,这些引用不会马上展开,而是在该变量被使用(也就是在扩展其他字符串的过程中进行替换)时才会展开,这一过程被称作递归展开。
基本的递归展开示例
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
@echo $(foo)
在这个例子中,foo 被定义为 $(bar),在定义时 $(bar) 不会展开。当 make 执行到 @echo $(foo) 时,$(foo) 会先展开为 $(bar),接着 $(bar) 展开为 $(ugh),最终 $(ugh) 展开为 Huh?。所以运行 make 后,输出结果为 Huh?。
优点:组合变量值
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
all:
@echo "CFLAGS: $(CFLAGS)"
此例展示了递归展开变量的一个优点。当在规则里展开 CFLAGS 时,$(include_dirs) 会展开为 -Ifoo -Ibar,所以 CFLAGS 最终展开为 -Ifoo -Ibar -O。运行 make 后,输出结果为 CFLAGS: -Ifoo -Ibar -O。
缺点:无限循环
CFLAGS = $(CFLAGS) -O
all:
@echo $(CFLAGS)
这体现了递归展开变量的一个主要缺点。CFLAGS 定义为 $(CFLAGS) -O,在展开 CFLAGS 时,由于它引用了自身,会造成无限循环。不过,make 能够检测到这种无限循环并报告错误。
递归展开变量虽然有组合变量值等优点,但也存在容易引发无限循环、函数多次执行导致结果不可预测和运行速度变慢等缺点。在编写 Makefile 时,需要根据具体需求谨慎使用递归展开变量。
2. Makefile 中的简单展开变量(:=)
在 Makefile 里,简单展开变量是通过 := 或 ::= 来定义的。其核心特性为:在变量定义阶段,就会对变量值里引用的其他变量和函数进行展开,并且展开完成后,变量值便固定下来,后续使用时不会再次展开。
下面通过几个示例来深入理解简单展开变量:
基本示例
x := foo
y := $(x) bar
x := later
all:
@echo "Value of y: $(y)"
在这个例子中,当定义 y := $(x) bar 时,$(x) 会立即展开为 foo,所以 y 的值是 foo bar。即便后续 x 被重新赋值为 later,y 的值也不会受到影响。运行 make 后,输出结果为 Value of y: foo bar。
结合函数的示例
files := $(wildcard *.c)
objects := $(patsubst %.c,%.o,$(files))
all:
@echo "Source files: $(files)"
@echo "Object files: $(objects)"
此例中,files := $(wildcard *.c) 会在定义时执行 wildcard 函数,将当前目录下所有 .c 文件的名称赋给 files 变量。接着,objects := $(patsubst %.c,%.o,$(files)) 会执行 patsubst 函数,把 files 里的 .c 文件名替换为 .o 文件名,然后赋给 objects 变量。后续使用 files 和 objects 时,它们的值不会再次展开。
避免无限循环示例
CFLAGS := $(CFLAGS) -O
all:
@echo "CFLAGS: $(CFLAGS)"
若使用递归展开变量(=),CFLAGS = $(CFLAGS) -O 会造成无限循环。但使用简单展开变量(:=),CFLAGS 初始为空,定义时展开后 CFLAGS 的值就是 -O,避免了无限循环问题。运行 make 后,输出结果为 CFLAGS: -O。
3. 追加赋值(+=)
若你要在已有变量值的基础上追加内容,就可以使用追加赋值(+=)。
ne = first
ne += second
all:
@echo $(ne)
在这个例子里,ne 变量最初的值是 first,使用 += 后追加了 second,最终执行 make all 会输出 first second。
4. 条件赋值(?=)
条件赋值(?=)只有在变量未被赋值时才会进行赋值操作。
ne ?= default
all:
@echo $(ne)
在这个例子中,由于 ne 之前未被赋值,所以它会被赋值为 default。若 ne 之前已经有了值,那么使用 ?= 就不会改变其原有的值。
二. 取消/删除Makefile变量
在 Makefile 中取消一个变量的声明(即清除变量的值),可以通过几种不同的方式来实现,下面为你详细介绍:
1. 使用空赋值
最简单的方式是将变量赋值为空字符串。这样变量虽然仍然存在,但它的值为空。
# 声明变量
MY_VAR = some_value
# 取消变量的值
MY_VAR =
all:
@echo "MY_VAR 的值为: $(MY_VAR)"
在上述代码中,首先给 MY_VAR 变量赋了值 some_value,之后通过 MY_VAR = 这一操作将其值清空。当执行 make all 时,输出的 MY_VAR 的值就是空的。
2. 使用 undefine 关键字
undefine 关键字可以彻底移除变量的定义。与空赋值不同,使用 undefine 后,变量就不再存在于 Makefile 的作用域中。
# 声明变量
MY_VAR = some_value
# 取消变量的定义
undefine MY_VAR
all:
@echo "MY_VAR 的值为: $(MY_VAR)"
这里,undefine MY_VAR 语句将 MY_VAR 变量的定义完全移除。当执行 make all 时,$(MY_VAR) 不会展开为任何内容,因为该变量已不存在。
3. 利用环境变量覆盖
如果变量是从环境变量中继承而来,你可以通过在 Makefile 中重新赋值为空或者使用 undefine 来处理。另外,在调用 make 命令时也可以通过传递空值来覆盖环境变量的值。
# 假设 MY_VAR 是从环境变量继承而来
all:
@echo "MY_VAR 的值为: $(MY_VAR)"
若要取消这个环境变量对 Makefile 的影响,可以这样调用 make 命令:
make MY_VAR=
这样,在 Makefile 执行过程中,MY_VAR 的值就为空。
4. 总结
- 空赋值:适合只是想清空变量的值,而保留变量的定义,后续还可能会重新赋值的情况。
undefine关键字:用于彻底移除变量的定义,若后续不再需要该变量,使用此方法更合适。- 环境变量覆盖:针对从环境变量继承的变量,可在调用
make时传递空值来取消其影响。
三. 访问makefile变量
1. 基本变量引用
使用 $(var) 或 ${var} 来引用变量 var 的值。这是最常见的变量引用方式。
CC = gcc
CFLAGS = -Wall
all:
$(CC) $(CFLAGS) -o test test.c
在这个例子中,$(CC) 引用了 CC 变量的值 gcc,$(CFLAGS) 引用了 CFLAGS 变量的值 -Wall。
2. 变量的嵌套引用
可以在一个变量引用中嵌套另一个变量引用,以实现更复杂的操作。
src = main.c utils.c
obj = $(src:.c=.o)
CFLAGS = -Wall
CC = gcc
all: $(obj)
$(CC) $(CFLAGS) -o my_program $(obj)
$(obj): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这里,$(obj) 本身是通过后缀替换引用得到的,然后又在规则中被引用,同时规则里还引用了 $(CC) 和 $(CFLAGS) 变量。后面会有很多这种例子
3. 自动变量引用(默认变量)
Makefile 提供了一些自动变量,用于在规则中引用目标文件、依赖文件等信息。
$@:表示当前规则的目标文件。$<:表示当前规则的第一个依赖文件。$^:表示当前规则的所有依赖文件,以空格分隔。$?:代表当前规则中所有比目标文件更新的依赖文件,以空格分隔。
src = main.c utils.c
obj = $(src:.c=.o)
CC = gcc
CFLAGS = -Wall
all: my_program
my_program: $(obj)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
在 my_program 规则中,$@ 代表 my_program,$^ 代表 main.o utils.o;在 %.o: %.c 规则中,$< 代表对应的 .c 文件,$@ 代表对应的 .o 文件。
4. 函数调用引用
Makefile 支持使用函数来处理变量,通过函数调用的方式引用变量。
$(wildcard pattern):用于查找符合指定模式的文件列表。
src = $(wildcard *.c)
obj = $(src:.c=.o)
CC = gcc
CFLAGS = -Wall
all: my_program
my_program: $(obj)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这里,$(wildcard *.c) 会查找当前目录下所有的 .c 文件,并将结果赋值给 src 变量。
$(patsubst pattern,replacement,text):用于对文本中的单词进行模式替换。
src = main.c utils.c
obj = $(patsubst %.c,%.o,$(src))
CC = gcc
CFLAGS = -Wall
all: my_program
my_program: $(obj)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
$(patsubst %.c,%.o,$(src)) 会将 src 变量中的 .c 后缀替换为 .o 后缀,效果和后缀替换引用类似。
5. 环境变量引用
Makefile 可以引用环境变量,使用 $(VAR) 或 ${VAR} 来引用名为 VAR 的环境变量。
all:
@echo $(HOME)
在这个例子中,$(HOME) 引用了系统环境变量 HOME 的值,通常是用户的主目录。
这些变量引用方式在 Makefile 中非常实用,可以帮助你更灵活地编写和管理项目的编译规则。
6. Makefile 后缀替换引用
Makefile 中用于批量替换变量里单词后缀的语法。
格式:$(var:a=b)
var:操作变量名a:待替换后缀b:替换后后缀
src = file1.c file2.c
obj = $(src:.c=.o)
all:
@echo $(obj)
解释:将 src 里 .c 后缀替换为 .o,输出 file1.o file2.o。
项目示例
src = main.c utils.c
obj = $(src:.c=.o)
CC = gcc
CFLAGS = -Wall
all: my_program
my_program: $(obj)
$(CC) $(CFLAGS) -o my_program $(obj)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(obj) my_program
解释:将 src 源文件列表转为 obj 目标文件列表,用于编译和链接生成 my_program 可执行文件。当然,%.o: %.c可以省略掉。
7. Makefile 中变量值的解析与处理
在 Makefile 中,SUBDIRS = subdir1 subdir2 这样的赋值会将 subdir1 和 subdir2 作为一个整体字符串赋值给变量 SUBDIRS,多个值之间用空格分隔。但在后续使用中,Makefile 会根据具体规则和上下文对其进行解析和处理:
- 静态模式规则:如在
all: $(SUBDIRS)中,$(SUBDIRS)会展开为"subdir1 subdir2",表明all目标依赖于subdir1和subdir2这两个目标。而在$(SUBDIRS):静态模式规则中,Makefile 会将其拆分成两个独立目标,$@自动变量会分别代表subdir1和subdir2,然后执行make -C $@命令。 - 循环处理:在类似
for dir in $(SUBDIRS); do \ make -C $$dir; \ done的 shell 循环中,$(SUBDIRS)展开为"subdir1 subdir2"后,shell 会按空格将其拆分成多个单词,依次将subdir1和subdir2赋值给变量dir,再执行make -C $dir命令。
总之,Makefile 不会将 SUBDIRS 中的多个值看作不可分割的整体,而是会根据不同场景按空格进行拆分和处理,以实现对多个目标或值的分别操作。
8. 临时变量
在 Makefile 里,$(0)、$(1)、$(2) 等临时变量和 call 函数配合使用,目的是在调用自定义参数化函数时传递和引用参数。
变量含义
$(0):存储call函数调用时的首个参数,也就是被调用的变量名。$(1):代表call函数调用时传入的第一个参数。$(2):代表call函数调用时传入的第二个参数。以此类推,可传入任意数量的参数。
工作机制
当使用 call 函数时,make 会把传入的参数依次赋值给这些临时变量。之后,在被调用的变量(宏)中,就能使用这些临时变量引用对应的参数,达成参数传递与处理的目的。
示例
- 简单参数反转
reverse = $(2) $(1)
foo = $(call reverse,a,b)
# 执行 `$(call reverse,a,b)` 时,`reverse` 赋值给 `$(0)`,`a` 赋值给 `$(1)`,`b` 赋值给 `$(2)`。
# `reverse` 宏里 `$(2) $(1)` 替换成 `b a`,所以 `foo` 值为 `b a`。
- 字符串拼接
concat = $(1)$(3)$(2)
result = $(call concat,Hello,World, )
# 执行 `$(call concat,Hello,World, )` 时,`concat` 赋值给 `$(0)`,`Hello` 赋值给 `$(1)`,`World` 赋值给 `$(2)`,空格 赋值给 `$(3)`。
# `concat` 宏里 `$(1)$(3)$(2)` 替换成 `Hello World`,所以 `result` 值为 `Hello World`。
四. Makefile 多种规则
在 Makefile 里,模式规则和普通规则,静态规则存在明显差异,下面从定义、语法、适用场景等方面详细介绍两者的区别:
1. 定义与概念
普通规则
明确指定一个或多个具体目标文件,以及这些目标文件所依赖的文件,同时给出用于生成目标文件的命令。简单来说,普通规则是针对特定的、具体的文件来定义的。
模式规则
使用通配符 % 来定义具有相似特征的一组目标文件及其依赖关系和生成命令。它可以匹配多个文件,通过 % 通配符捕获文件名的一部分,从而实现规则的复用。
静态模式规则
静态模式规则结合了普通规则和模式规则的特点。它会明确指定一组目标文件,然后使用模式来定义这些目标文件的依赖关系和生成命令。与模式规则不同的是,它只对明确列出的目标文件生效。
2. 语法形式
普通规则
目标文件: 依赖文件1 依赖文件2 ...
生成目标文件的命令
main.o: main.c
gcc -c main.c -o main.o
此规则明确指出 main.o 依赖于 main.c,并给出了将 main.c 编译成 main.o 的具体命令。
模式规则
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这里 %.o 表示任意以 .o 结尾的文件,%.c 表示同名的以 .c 结尾的文件。% 会匹配文件名中相同的部分,比如对于 main.o,% 匹配 main,对应的依赖文件就是 main.c。$< 是自动变量,代表第一个依赖文件,$@ 代表目标文件。
静态模式规则
objects = foo.o bar.o
$(objects): %.o: %.c
gcc -c $< -o $@
明确指定了目标文件列表 objects(包含 foo.o 和 bar.o),通过模式 %.o: %.c 定义了依赖关系和生成命令,只对 foo.o 和 bar.o 这两个目标文件生效。
3. 适用场景
普通规则
适用于目标文件较少且每个目标文件的生成规则有特殊要求的情况。例如,某个目标文件的编译选项与其他文件不同,或者生成过程需要额外的步骤,就可以使用普通规则为其单独定义。
special.o: special.c
gcc -O3 -c special.c -o special.o # 使用特殊的编译选项 -O3
模式规则
适合处理大量具有相似特征的文件。在项目中有很多源文件需要编译成目标文件时,使用模式规则可以避免为每个文件都编写重复的规则,大大减少 Makefile 的代码量,提高编写效率。例如,一个项目中有多个 .c 文件需要编译成 .o 文件,使用模式规则可以统一处理。
静态模式规则
适用于有一组特定的目标文件,它们有相似的依赖关系和生成方式,但又不想让规则应用到所有符合模式的文件。比如,项目中只有部分 .c 文件需要按照特定规则编译成 .o 文件,就可以使用静态模式规则指定这些目标文件。
4. 灵活性与扩展性
普通规则
灵活性较高,因为可以为每个具体的目标文件定制生成规则。但扩展性较差,当项目中的文件数量增加时,需要不断添加新的规则,会使 Makefile 变得冗长复杂。
模式规则
灵活性相对较低,因为所有匹配的文件都使用相同的规则。但扩展性好,无论项目中有多少符合模式的文件,都不需要额外添加规则,模式规则会自动处理这些文件。
静态模式规则
灵活性和扩展性处于普通规则和模式规则之间。它可以针对特定的一组目标文件定制规则,具有一定的灵活性;同时,当需要添加新的目标文件到这组特定文件中时,只需要修改目标文件列表,而不需要添加新的规则,具有一定的扩展性。
5. 后缀规则
Makefile 的后缀规则是一种较为传统的定义编译规则的方式,它通过文件的后缀名来确定如何处理不同类型的文件。以下是一些常见的后缀规则及其介绍:
.c.o 规则
- 描述:用于将 C 源文件编译为目标文件。
- 默认动作:使用 C 编译器(通常是
cc或gcc)将.c文件编译成.o文件。在编译过程中,会自动包含 Makefile 中定义的 CFLAGS 变量指定的编译选项。例如:
CFLAGS = -Wall -g
.c.o:
$(CC) $(CFLAGS) -c $< -o $@
这里 $(CC) 是 C 编译器,$< 表示依赖的 .c 文件,$@ 表示生成的 .o 目标文件。
.cpp.o 规则
- 描述:用于将 C++ 源文件编译为目标文件。
- 默认动作:使用 C++ 编译器(如
g++)将.cpp文件编译成.o文件,并应用 CXXFLAGS 变量指定的编译选项。示例如下:
CXXFLAGS = -std=c++11 -Wall -g
.cpp.o:
$(CXX) $(CXXFLAGS) -c $< -o $@
其中 $(CXX) 是 C++ 编译器,$< 和 $@ 分别代表源文件和目标文件。
这些后缀规则可以根据项目的实际需求进行自定义和扩展,以满足不同的编译和链接要求。不过,现代的 Makefile 更倾向于使用模式规则(如 %.o: %.c)来定义编译规则,因为它们更加灵活和直观。后缀规则相对较为传统,在一些旧的项目或者特定的环境中仍然会被使用。
https://www.gnu.org/software/make/manual/html_node/Suffix-Rules.html
五. 常见的隐含推导规则
1. C 源文件编译为目标文件
当目标是 .o 文件,依赖是同名的 .c 文件时,GNU Make 有默认的编译规则。其默认命令如下:
$(CC) $(CFLAGS) -c $< -o $@
这里,$(CC) 代表 C 编译器(默认是 cc),$(CFLAGS) 是编译选项,$< 为第一个依赖文件(也就是 .c 文件),$@ 是目标文件(即 .o 文件)。
示例:
CC = gcc
CFLAGS = -Wall
all: main.o
# 这里没有显式定义 main.o 的规则,会使用隐含推导规则
执行 make 时,GNU Make 会自动使用 gcc -Wall -c main.c -o main.o 来编译 main.c 文件。
2. C++ 源文件编译为目标文件
当目标是 .o 文件,依赖是同名的 .cpp 文件时,默认规则的命令为:
CC = gcc
CFLAGS = -Wall
all: main.o
# 这里没有显式定义 main.o 的规则,会使用隐含推导规则
其中,$(CXX) 是 C++ 编译器(默认是 g++),$(CXXFLAGS) 是 C++ 编译选项。
3. 链接目标文件生成可执行文件
若目标是可执行文件,依赖是多个 .o 文件,默认规则的命令是:
$(CC) $(CFLAGS) $(LDFLAGS) $^ -o $@
$(LDFLAGS) 是链接选项,$^ 代表所有的依赖文件(即 .o 文件)。
4. 隐含推导规则的使用场景
简化 Makefile:在小型项目或者简单的编译任务中,使用隐含推导规则可以显著减少 Makefile 的代码量。例如,一个只有几个源文件的 C 项目,可能只需要简单定义编译器和编译选项,就可以利用隐含推导规则完成编译。
CC = gcc
CFLAGS = -Wall
all: my_program
# 利用隐含推导规则编译 .c 文件和链接生成可执行文件
5. 隐含推导规则的注意事项
自定义编译选项:若默认的编译选项无法满足需求,就需要显式地定义规则。例如,需要指定特定的头文件路径或者宏定义时,就不能单纯依赖隐含推导规则。
%.o: %.c
$(CC) $(CFLAGS) -I/path/to/include -DDEBUG -c $< -o $@
6. 查看隐含推导规则
你可以使用 make -p 命令查看 GNU Make 的所有隐含推导规则和默认变量设置。这个命令会输出 Make 的内部规则和变量定义,有助于你了解默认的行为和进行自定义修改。
六. Makefile 隐式规则递归推导
1. 递归推导的概念
在 Makefile 中,隐式规则的递归推导是指当 Make 工具根据隐式规则处理目标文件时,若目标文件的依赖文件自身也需要通过隐式规则生成,Make 会继续对这些依赖文件的生成规则进行推导,这个过程会层层深入,直至找到可以直接处理的文件或者满足停止条件。
2. 触发递归推导的情形
间接依赖导致的推导
当一个目标文件(如 .o 文件)依赖于其他文件,而这些依赖文件又可以通过隐式规则生成时,就会触发递归推导。例如:
all: main.o
main.o: main.c header.h
# 没有显式规则,使用隐式规则
当执行 make all 时,Make 发现需要生成 main.o。根据隐式规则,它会检查 main.c 和 header.h 是否存在。若 header.h 是由另一个 .h.in 文件通过隐式规则生成的,Make 就会进一步推导如何生成 header.h。这个过程中,Make 会按照依赖关系逐层推导,先确定 main.o 的生成方式,再确定其依赖文件 header.h 的生成方式。
链式依赖引发的推导
在项目里,若存在多个 .o 文件,并且这些 .o 文件之间存在依赖关系,也会引发递归推导。比如:
all: program
program: main.o utils.o
main.o: main.c
utils.o: utils.c
# 没有显式规则,使用隐式规则
要生成 program,Make 会先推导怎样生成 main.o 和 utils.o。对于 main.o 和 utils.o,又会依据隐式规则去推导如何从 main.c 和 utils.c 生成它们。这就形成了一个链式的推导过程,从最终目标 program 开始,逐步深入到每个 .o 文件以及其对应的 .c 文件。
3. 防止无限递归的机制
虽然存在递归推导,但 Make 工具会通过一些机制避免无限递归。它会检查文件的实际情况,像文件是否存在、文件的时间戳等信息。当某个文件已经是最新的(即其修改时间晚于依赖它的文件),Make 就不会再去推导如何更新它。例如,若 header.h 文件的修改时间晚于 main.o,Make 就不会尝试重新生成 header.h,从而避免了不必要的推导和操作,确保递归推导能够在合理的范围内结束。
4. 隐含规则链与中间目标
隐含规则链
当一个目标文件的生成需要多个连续的隐含规则时,这些规则构成 “隐含规则链”。例如,.o文件的生成,可能先由 Yacc 的.y文件通过隐含规则生成.c文件,再由 C 编译器的隐含规则将.c文件编译为.o文件。若.c文件存在,直接调用 C 编译器隐含规则;若.c文件不存在但.y文件存在,则先调用 Yacc 隐含规则生成.c文件,再调用 C 编译隐含规则。
中间目标特性
中间目标在 Makefile 中有两个特殊之处:
- 触发条件:仅当中间目标不存在时,才会触发对应的中间规则。
- 文件处理:最终目标成功产生后,生成过程中产生的中间目标文件默认会被
rm -f删除 。
中间目标声明与保留
- 强制声明中间目标:使用伪目标
.INTERMEDIATE可强制声明文件为中间目标(如.INTERMEDIATE : mid)。 - 阻止自动删除:通过伪目标
.SECONDARY强制声明(如.SECONDARY : sec),可阻止 Make 自动删除指定中间目标。 - 保存中间文件:将目标以模式方式(如
%.o)指定为伪目标.PRECIOUS的依赖目标,可保存被隐含规则生成的中间文件。
避免无限递归
在 “隐含规则链” 中,禁止同一个目标出现两次或两次以上,以此防止 Make 自动推导时出现无限递归的情况。
规则优化
Make 会对部分特殊隐含规则进行优化,避免生成中间文件。例如从foo.c生成目标程序foo,理论上需先编译生成foo.o再链接,但实际可通过cc -o foo foo.c一条命令完成,此时优化后的规则不会生成foo.o这一中间文件。
5. 通配模式规则概述
当模式规则的目标仅为 % 时,可匹配任意文件名,此为通配规则。但 make 处理这类规则时,要对每个作为目标或先决条件的文件名考虑所有通配规则,会导致运行速度慢。例如,对于 foo.c,make 需考虑多种不合理的生成方式,如从 foo.c.o 链接、从 foo.c.c 编译链接等。
规则限制
为提升 make 运行速度,处理通配规则设置了两种限制,定义通配规则时需二选一。
终结规则
- 定义方式:使用双冒号
::定义通配规则。 - 适用条件:仅当先决条件实际存在时适用,由其他隐式规则生成的先决条件无效,即规则后不允许进一步的规则链操作。
- 示例:从 RCS 和 SCCS 文件提取源文件的内置隐式规则为终结规则。若
foo.c,v文件不存在,make不会考虑从foo.c,v.o或RCS/SCCS/s.foo.c,v将其作为中间文件生成,因为 RCS 和 SCCS 文件通常是最终源文件,无需从其他文件重新生成,可节省时间。
非终结规则
- 定义方式:未标记为终结规则的通配规则。
- 适用限制:不能应用于隐式规则的先决条件,也不能用于表示特定数据类型的文件名(若某个非通配隐式规则的目标与文件名匹配,则该文件名表示特定数据类型)。
- 示例:
foo.c与模式规则%.c : %.y(运行 Yacc 的规则)的目标匹配,无论该规则是否实际适用(有foo.y文件才适用),只要目标匹配,make就不会对foo.c考虑任何非终结通配规则,不会尝试从foo.c.o、foo.c.c、foo.c.p等将其作为可执行文件生成。
七. 条件判断
在 Makefile 中,是有条件判断语句的,类似于其他编程语言里的 if 语句,它能依据不同条件来执行不同的规则或者赋值操作。下面为你详细介绍 Makefile 里的条件判断语句:
1. ifeq 和 ifneq
ifeq:用于判断两个参数是否相等,若相等则执行ifeq和endif之间的内容。ifneq:用于判断两个参数是否不相等,若不相等则执行ifneq和endif之间的内容。
语法格式
ifeq (参数1, 参数2)
# 当参数1和参数2相等时执行的内容
else
# 当参数1和参数2不相等时执行的内容
endif
示例
DEBUG = 1
ifeq ($(DEBUG), 1)
CFLAGS = -g -Wall
else
CFLAGS = -O2 -Wall
endif
all:
@echo "CFLAGS: $(CFLAGS)"
在这个例子中,若 DEBUG 的值为 1,CFLAGS 会被赋值为 -g -Wall;若 DEBUG 的值不为 1,CFLAGS 会被赋值为 -O2 -Wall。
2. ifdef 和 ifndef
ifdef:用于判断一个变量是否已经定义,若已定义则执行ifdef和endif之间的内容。ifndef:用于判断一个变量是否未定义,若未定义则执行ifndef和endif之间的内容。
语法格式
ifdef 变量名
# 当变量已定义时执行的内容
else
# 当变量未定义时执行的内容
endif`
ifndef 变量名
# 当变量未定义时执行的内容
else
# 当变量已定义时执行的内容
endif`
示例
ifdef DEBUG
CFLAGS = -g -Wall
else
CFLAGS = -O2 -Wall
endif
all:
@echo "CFLAGS: $(CFLAGS)"
在这个例子中,若 DEBUG 变量已定义,CFLAGS 会被赋值为 -g -Wall;若 DEBUG 变量未定义,CFLAGS 会被赋值为 -O2 -Wall。
3. 注意事项
- 空格问题:在
ifeq、ifneq、ifdef、ifndef等关键字后面的括号内,参数之间的逗号前后不能有空格,否则会影响判断结果。 - 嵌套使用:条件判断语句可以嵌套使用,以实现更复杂的条件判断逻辑。
DEBUG = 1
PLATFORM = linux
ifeq ($(DEBUG), 1)
CFLAGS = -g -Wall
ifeq ($(PLATFORM), linux)
CFLAGS += -D LINUX
else
CFLAGS += -D OTHER_PLATFORM
endif
else
CFLAGS = -O2 -Wall
endif
all:
@echo "CFLAGS: $(CFLAGS)"
在这个嵌套使用的例子中,先根据 DEBUG 的值进行判断,然后在 DEBUG 为 1 的情况下,再根据 PLATFORM 的值进行进一步的判断。
综上所述,Makefile 中的条件判断语句能让你根据不同的条件来灵活控制编译过程,提高 Makefile 的通用性和可维护性。
八. Makefile 隐含规则与变量设置
1. 变量影响隐含规则
在 Makefile 里,隐含规则的命令大多运用预先设定的变量。这些变量可通过以下方式设置,且一旦设置,就会对隐含规则产生作用:
- 在 Makefile 中修改变量值。
- 在 make 命令行传入变量值。
- 在环境变量里设置变量值。
若要取消自定义变量对隐含规则的影响,可使用 make 的 -R 或 --no–builtin-variables 参数。
示例:编译 C 程序隐含规则
编译 C 程序的隐含规则命令为 $(CC) –c $(CFLAGS) $(CPPFLAGS) ,Make 默认编译命令是 cc。若将 $(CC) 重新定义为 gcc,$(CFLAGS) 重新定义为 -g,则隐含规则中的命令会以 gcc –c -g $(CPPFLAGS) 的形式执行。
2. 隐含规则使用的变量分类
隐含规则使用的变量可分为两类:
- 命令相关变量:例如
CC,用于指定编译器等命令。 - 参数相关变量:例如
CFLAGS,用于指定编译选项等参数。
关于命令的变量。¶
AR: 函数库打包程序。默认命令是arAS: 汇编语言编译程序。默认命令是asCC: C语言编译程序。默认命令是ccCXX: C++语言编译程序。默认命令是g++CO: 从 RCS文件中扩展文件程序。默认命令是coCPP: C程序的预处理器(输出是标准输出设备)。默认命令是$(CC) –EFC: Fortran 和 Ratfor 的编译器和预处理程序。默认命令是f77GET: 从SCCS文件中扩展文件的程序。默认命令是getLEX: Lex方法分析器程序(针对于C或Ratfor)。默认命令是lexPC: Pascal语言编译程序。默认命令是pcYACC: Yacc文法分析器(针对于C程序)。默认命令是yaccYACCR: Yacc文法分析器(针对于Ratfor程序)。默认命令是yacc –rMAKEINFO: 转换Texinfo源文件(.texi)到Info文件程序。默认命令是makeinfoTEX: 从TeX源文件创建TeX DVI文件的程序。默认命令是texTEXI2DVI: 从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是texi2dviWEAVE: 转换Web到TeX的程序。默认命令是weaveCWEAVE: 转换C Web 到 TeX的程序。默认命令是cweaveTANGLE: 转换Web到Pascal语言的程序。默认命令是tangleCTANGLE: 转换C Web 到 C。默认命令是ctangleRM: 删除文件命令。默认命令是rm –f
关于命令参数的变量¶
下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是空。
ARFLAGS: 函数库打包程序AR命令的参数。默认值是rvASFLAGS: 汇编语言编译器参数。(当明显地调用.s或.S文件时)CFLAGS: C语言编译器参数。CXXFLAGS: C++语言编译器参数。COFLAGS: RCS命令参数。CPPFLAGS: C预处理器参数。( C 和 Fortran 编译器也会用到)。FFLAGS: Fortran语言编译器参数。GFLAGS: SCCS “get”程序参数。LDFLAGS: 链接器参数。(如:ld)LFLAGS: Lex文法分析器参数。PFLAGS: Pascal语言编译器参数。RFLAGS: Ratfor 程序的Fortran 编译器参数。YFLAGS: Yacc文法分析器参数。¶
九. Makefile include指令
在 Makefile 里,include 指令是一个非常实用的功能,它能让你在一个 Makefile 中包含其他 Makefile 文件的内容,从而提升代码的可维护性和复用性。下面为你详细介绍 include 的相关内容:
基本语法
include 指令的基本语法如下:
include filename1 filename2 ...
这里的 filename1、filename2 等是要包含的 Makefile 文件的名称。你可以同时包含多个文件,文件名之间用空格分隔。
工作原理
当 Make 读取到 include 指令时,它会暂停当前 Makefile 的解析,转而读取并解析指定的文件内容。读取完成后,会将这些文件的内容插入到 include 指令所在的位置,然后继续解析当前的 Makefile。
示例
假设我们有两个 Makefile 文件:main.mk 和 utils.mk。
utils.mk 文件内容
CFLAGS = -Wall -O2
CC = gcc
clean:
rm -f *.o
main.mk 文件内容
include utils.mk
all: main.o
$(CC) $(CFLAGS) main.o -o main
main.o: main.c
$(CC) $(CFLAGS) -c main.c -o main.o
在这个例子中,main.mk 文件通过 include utils.mk 指令包含了 utils.mk 文件的内容。这样,main.mk 就可以使用 utils.mk 中定义的变量(如 CFLAGS 和 CC)和规则(如 clean 规则)。
1. 搜索路径
如果要包含的文件不在当前目录下,你可以通过设置 VPATH 变量或者 make 命令的 -I 选项来指定搜索路径。
使用 VPATH 变量
VPATH = path/to/include
include utils.mk
这里的 VPATH 变量指定了一个搜索路径,Make 会在这个路径下查找 utils.mk 文件。
使用 I 选项
make -I path/to/include -f main.mk
在执行 make 命令时,使用 -I 选项指定搜索路径,Make 会在该路径下查找包含的文件。
2. 处理文件不存在的情况
如果 include 指定的文件不存在,Make 默认会给出警告信息,但不会停止执行。你可以使用 sinclude 或者 -include 来忽略文件不存在的错误。
sinclude utils.mk
# 或者
-include utils.mk
使用 sinclude 或 -include 时,如果文件不存在,Make 不会给出警告,会继续执行后续的解析。
3. 适用场景
- 模块化开发:将不同功能的规则和变量分别放在不同的 Makefile 文件中,通过
include指令在主 Makefile 中组合这些文件,使代码结构更清晰,便于维护和管理。 - 代码复用:多个项目可能会有一些共同的编译规则和变量,将这些内容放在一个公共的 Makefile 文件中,其他项目的 Makefile 可以通过
include指令引用这个公共文件,避免代码重复编写。
十. Makefile 执行子目录 Makefile
1. 使用 make -C 命令
示例代码
SUBDIRS = subdir1 subdir2
all: $(SUBDIRS)
$(SUBDIRS):
make -C $@
clean:
for dir in $(SUBDIRS); do \
make -C $$dir clean; \
done
代码解释
SUBDIRS变量:定义包含子目录名称的列表。all目标:依赖于$(SUBDIRS),执行make all时依次处理各子目录。$(SUBDIRS)规则:make -C $@切换到子目录并执行其中的 Makefile,$@代表当前子目录名。clean目标:使用for循环遍历子目录,执行make -C $dir clean清理文件。
2. 使用递归调用
示例代码
SUBDIRS = subdir1 subdir2
all:
@for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done
clean:
@for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done
代码解释
all目标:for循环遍历子目录,$(MAKE) -C $dir递归调用make执行子目录的 Makefile,$(MAKE)是特殊变量,代表当前make命令。clean目标:同理,遍历子目录执行$(MAKE) -C $dir clean清理文件。
3. 传递变量和参数
示例代码
SUBDIRS = subdir1 subdir2
CFLAGS = -Wall -O2
all:
@for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir CFLAGS="$(CFLAGS)"; \
done
clean:
@for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done
代码解释
通过 $(MAKE) -C $dir CFLAGS="$(CFLAGS)" 将 CFLAGS 变量传递给子目录的 Makefile 用于编译。
4. 注意事项
- 错误处理:递归调用
make时要注意错误处理,如某个子目录 Makefile 执行失败,需考虑处理方式(停止或继续构建)。 - 并行执行:若子目录无依赖关系,可使用
make的j选项并行执行提高构建速度。
十一. Makefile 转义符号笔记
1.$$转义$
- 作用:在 Makefile 中,
$用于引用变量。当在 Makefile 的 shell 命令中需要使用$让 shell 解析变量时,使用$$进行转义,Makefile 会将$$解析为单个$传递给 shell。
示例
SUBDIRS = subdir1 subdir2
all:
for dir in $(SUBDIRS); do \
make -C $$dir; \
done
在上述代码中,$(SUBDIRS)被 Makefile 展开为subdir1 subdir2,$$dir经解析后传递给 shell 的是$dir,shell 在循环中会将$dir依次赋值为subdir1和subdir2。
二、\\转义\
- 作用:在 Makefile 中,反斜杠
\通常用于行延续。若要在字符串或命令中使用反斜杠本身,需用\\进行转义。
示例
PATH = C:\\Windows\\System32
这里\\被解析为单个反斜杠,PATH变量的值为C:\Windows\System32。
三、\转义换行符
- 作用:编写多行命令时,使用反斜杠
\转义换行符,使命令延续到下一行。
示例
all:
gcc -o program \
main.c \
utils.c
通过\将多行命令连接成一个完整的命令。
十二. Makefile 与 Shell 混合使用笔记
1. 基本原理
Makefile 用于自动化构建项目,通过定义规则描述文件依赖关系和构建步骤。Shell 是命令行解释器,提供丰富命令和功能。Makefile 允许在规则命令部分直接使用 Shell 命令,借助 Shell 功能完成复杂构建任务。
2. 具体体现
2.1. 直接嵌入 Shell 命令
在 Makefile 规则中可直接编写 Shell 命令,如:
all:
echo "Starting the build process..."
ls -l
mkdir build
执行 make all 时,Makefile 会依次执行这些 Shell 命令。
2.2. 使用 Shell 变量和语法
可在 Makefile 中使用 Shell 的变量和语法,例如使用 Shell 循环:
SUBDIRS = subdir1 subdir2
all:
for dir in $(SUBDIRS); do \
echo "Building in directory: $$dir"; \
make -C $$dir; \
done
for 循环是 Shell 语法,dir 是 Shell 变量,可对多个子目录进行批量操作。
2.3. 结合 Shell 脚本
除嵌入简单 Shell 命令,还能调用外部 Shell 脚本,如:
all:
./build_script.sh
Makefile 会执行 build_script.sh 脚本。
3. 注意事项
1. 变量解析冲突
Makefile 和 Shell 都用 $ 引用变量,可能导致解析冲突。在 Makefile 的 Shell 命令中,需用 $$ 转义 $,确保 Shell 正确解析其变量。
2. 命令执行环境
Makefile 中的 Shell 命令在新的 Shell 进程中执行,每个规则的命令默认在独立 Shell 中执行。若需在同一 Shell 中执行多个命令,可用反斜杠 \ 连接成一行,如:
all:
cd build; \
make
确保 make 命令在 build 目录下执行。
十三. makefile中函数调用
函数使你能够在 Makefile 中进行文本处理,以计算要操作的文件,或者确定在规则(recipe)中要使用的命令。你可以在函数调用中使用函数,在函数调用时,你需要给出函数的名称以及一些供函数进行处理的文本(即参数)。函数处理的结果会在调用函数的位置被替换到 Makefile 中,这就如同变量被替换一样。有点类似于Apache commoncollections
1. 函数调用语法笔记
- 调用形式:类似变量引用,有
$(function arguments)和${function arguments}两种形式。函数名是make内置或用call创建的自定义函数,参数与函数名用空格 / 制表符分隔,多参数用逗号分隔。参数中分隔符要成对,嵌套调用尽量用同一种分隔符。 - 参数扩展:参数在函数调用前按顺序扩展。
- 特殊字符:使用特殊字符(逗号、首参空格、不匹配括号等)作参数时,不能用反斜杠转义,可存入变量隐藏,如用
subst函数替换字符。
例如
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now ‘a,b,c’.
2. 字符串处理
1. 字符串替换函数
$(subst from,to,text):在text中,将所有from替换为to。例:$(subst ee,EE,feet on the street)输出fEEt on the strEEt。$(patsubst pattern,replacement,text):在text中,匹配pattern的单词替换为replacement,pattern中的%为通配符,replacement中的%会被pattern匹配内容替换 。例:$(patsubst %.c,%.o,x.c.c bar.c)输出x.c.o bar.o。- 替换引用语法:
$(var:pattern=replacement)等价于$(patsubst pattern,replacement,$(var)),$(var:suffix=replacement)等价于$(patsubst %suffix,%replacement,$(var))。
2. 字符串处理函数
$(strip string):去除string首尾空格,内部连续空格替换为单个空格。常用于条件判断前处理字符串 。$(findstring find,in):在in中查找find,找到返回find,否则返回空字符串。$(filter pattern…,text):返回text中匹配pattern的单词,去除不匹配项。$(filter-out pattern…,text):返回text中不匹配pattern的单词,与filter功能相反。$(sort list):按字典序排序list中的单词并去重。
3. 字符串提取函数
$(word n,text):返回text中第n个单词(n从 1 开始),越界返回空字符串。$(wordlist s,e,text):返回text中从第s个到第e个单词(包含s和e) 。$(words text):返回text中单词的数量。$(firstword names…):返回names中第一个单词。$(lastword names…):返回names中最后一个单词。
4. 应用示例
通过 subst 和 patsubst 处理 VPATH 变量,将目录列表转为 -I 编译选项添加到 CFLAGS 中:
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
上述代码先将 VPATH 中的冒号替换为空格,再将每个目录名转为 -I 标志并追加到 CFLAGS 中 。
3. 文件名处理函数
在 Makefile 中,有几个内置的展开函数专门用于处理文件名或文件名列表,这些函数对一系列以空格分隔的文件名(忽略首尾空格)进行特定转换,转换结果以单个空格连接。具体函数如下:
$(dir names…):提取文件名中的目录部分,即包含最后一个斜杠(/)及之前的所有内容。若文件名无斜杠,目录部分为./。如$(dir src/foo.c hacks)结果是src/ ./。$(notdir names…):提取文件名中除目录部分之外的内容。无斜杠的文件名不变;有斜杠的去掉最后斜杠及之前部分。以斜杠结尾的文件名会变为空字符串,可能导致结果文件名数量与参数不同。如$(notdir src/foo.c hacks)结果是foo.c hacks。$(suffix names…):提取文件名的后缀。若文件名有.,后缀是最后一个.及之后的内容;否则后缀为空字符串。结果可能比参数文件名数量少。如$(suffix src/foo.c src-1.0/bar.c hacks)结果是.c .c。$(basename names…):提取文件名中除后缀之外的内容。有.时,取最后一个.之前的部分(不包括.),目录部分的.会被忽略;无.时,basename 为整个文件名。如$(basename src/foo.c src-1.0/bar hacks)结果是src/foo src-1.0/bar hacks。$(addsuffix suffix,names…):将suffix添加到names中的每个文件名后面,结果用单个空格连接。如$(addsuffix .c,foo bar)结果是foo.c bar.c。$(addprefix prefix,names…):将prefix添加到names中的每个文件名前面,结果用单个空格连接。如$(addprefix src/,foo bar)结果是src/foo src/bar。$(join list1,list2):将list1和list2按单词逐个连接,第n个结果单词由两个参数的第n个单词连接而成。参数单词数量不同时,多出的单词原样复制到结果中。单词间原有的空格不保留,会被单个空格替换。可合并dir和notdir函数的结果。如$(join a b,.c .o)结果是a.c b.o。$(wildcard pattern):pattern是文件名模式(通常含通配符),返回与模式匹配且存在的文件名,以空格分隔。$(realpath names…):返回names中每个文件名的规范绝对名称,不包含.、..、重复路径分隔符(/)和符号链接。失败时返回空字符串,可能的失败原因参考realpath(3)文档。$(abspath names…):返回names中每个文件名的绝对名称,不包含.、..、重复路径分隔符(/)。与realpath不同,abspath不解析符号链接,也不要求文件名指向的文件或目录存在,可使用wildcard函数检测文件是否存在。
4. 条件判断函数
在 Makefile 中,有四个函数可实现条件展开,这些函数的关键特点是并非所有参数都会在初始时展开,只有那些需要展开的参数才会被展开。具体函数如下:
$(if condition,then-part[,else-part]):if函数在函数环境中提供条件展开支持(与 GNU make 的 Makefile 条件语句,如ifeq不同)。- 第一个参数
condition先去除首尾空格再展开。若展开后为非空字符串,条件为真;若为空字符串,条件为假。 - 条件为真时,计算第二个参数
then-part,其结果作为if函数的最终结果。 - 条件为假时,计算第三个参数
else-part(若存在),作为if函数的结果;若不存在第三个参数,if函数结果为空字符串。 then-part和else-part只会有一个被计算,可包含副作用操作(如 shell 函数调用等)。
- 第一个参数
$(or condition1[,condition2[,condition3…]]):or函数提供 “短路” 或运算。按顺序逐个展开参数,若某个参数展开后为非空字符串,处理停止,该展开结果作为or函数结果。若所有参数展开后都为假(空字符串),则函数结果为空字符串。$(and condition1[,condition2[,condition3…]]):and函数提供 “短路” 与运算。按顺序逐个展开参数,若某个参数展开后为空字符串,处理停止,函数结果为空字符串。若所有参数展开后都为非空字符串,则函数结果为最后一个参数的展开值。$(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]]):intcmp函数用于整数的数值比较,在 GNU make 的 Makefile 条件语句中无对应形式。- 先展开并将左右参数
lhs和rhs解析为十进制整数,其余参数的展开取决于左右参数的数值比较结果。 - 若无更多参数,若左右参数不相等,函数展开结果为空字符串;若相等,结果为它们的数值。
- 若
lhs严格小于rhs,intcmp函数结果为第三个参数lt-part的展开值;若相等,结果为第四个参数eq-part的展开值;若lhs严格大于rhs,结果为第五个参数gt-part的展开值。 - 若
gt-part缺失,默认取eq-part;若eq-part缺失,默认取空字符串。如$(intcmp 9,7,hello)和$(intcmp 9,7,hello,world,)结果为空字符串,$(intcmp 9,7,hello,world)(world后无逗号)结果为world。
- 先展开并将左右参数
例子
# $(if condition,then-part[,else-part])
# 定义变量
VAR := value
# 使用 if 函数进行条件判断
RESULT := $(if $(VAR),$(VAR) exists, $(VAR) does not exist)
all:
@echo $(RESULT)
# 在上述示例中,变量 VAR 有值,因此 condition 为真,then-part 会被计算,最终输出 value exists。
# $(or condition1[,condition2[,condition3…]])
# 定义变量
VAR1 :=
VAR2 := value2
VAR3 := value3
# 使用 or 函数进行条件判断
RESULT := $(or $(VAR1), $(VAR2), $(VAR3))
all:
@echo $(RESULT)
# 这里 VAR1 为空字符串,VAR2 为非空字符串,所以 or 函数在计算到 VAR2 时就停止,并返回 VAR2 的值,最终输出 value2。
# $(and condition1[,condition2[,condition3…]])
# 定义变量
VAR1 := value1
VAR2 := value2
VAR3 :=
# 使用 and 函数进行条件判断
RESULT := $(and $(VAR1), $(VAR2), $(VAR3))
all:
@echo $(RESULT)
# 在这个例子中,VAR3 为空字符串,and 函数在计算到 VAR3 时停止,由于 VAR3 为空,所以最终输出为空字符串。
# $(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]])
# 定义变量
NUM1 := 5
NUM2 := 3
# 使用 intcmp 函数进行数值比较
RESULT := $(intcmp $(NUM1), $(NUM2), less, equal, greater)
all:
@echo $(RESULT)
# 因为 NUM1 大于 NUM2,所以 intcmp 函数返回 gt-part 的值,即 greater。
5. let 函数
- 函数功能:
let函数用于限制变量作用域,在let表达式中对命名变量的赋值仅在该表达式的文本范围内有效,不影响外部作用域中同名变量。同时,let函数支持列表解包,可将未分配的值都赋给最后一个命名变量。 - 函数语法:
$(let var [var ...],[list],text)。先展开前两个参数var和list,最后一个参数text不一同展开。然后将list展开值的每个单词依次绑定到变量名var上,最后一个变量名绑定list展开后的剩余部分。若var变量名数量多于list单词数,剩余变量名设为空字符串;若var变量名数量少于list单词数,最后一个var设为list剩余所有单词。在let执行期间,var中的变量按简单展开变量进行赋值。 - 示例:
- 定义宏
reverse用于反转给定列表中单词顺序:
- 定义宏
reverse = $(let first rest,$1,\
$(if $(rest),$(call reverse,$(rest)) )$(first))
all: ; @echo $(call reverse,d c b a)
- 调用
reverse宏时,let先将$1展开为d c b a,将first赋值为d,rest赋值为c b a。接着展开if语句,因$(rest)非空,递归调用reverse函数处理c b a。递归中let又将first赋值为c,rest赋值为b a。递归持续到let处理只有一个值a时,此时first为a,rest为空,不再递归,直接展开$(first)为a并返回,逐步添加前面的值,最终输出a b c d。 reverse调用完成后,first和rest变量不再设置,若之前存在同名变量,不受reverse宏展开影响。
6. foreach 函数笔记
- 函数特点与功能:
foreach函数与let函数类似,但与其他函数差异较大。它能使一段文本被重复使用,每次对其进行不同的替换操作,类似于 shell 中sh的for命令和csh的foreach命令。可让文本按列表中单词数量多次展开,并将多次展开结果用空格连接作为foreach函数结果。 - 函数语法:
$(foreach var,list,text)。先展开前两个参数var和list,最后一个参数text不一同展开。然后对于list展开值的每个单词,将var展开值命名的变量设为该单词,并展开text,text中通常包含对该变量的引用,每次展开结果不同。 - 示例:
dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))
text 为 $(wildcard $(dir)/*),每次循环 dir 分别取值 a、b、c 等,依次执行 $(wildcard a/*)、$(wildcard b/*) 等,最终 files 为所有目录下文件列表,效果与 files := $(wildcard a/* b/* c/* d/*) 类似(除了 dirs 设置情况)。
- 提高可读性示例:
find_files = $(wildcard $(dir)/*)
dirs := a b c d
files := $(foreach dir,$(dirs),$(find_files))
通过定义变量 find_files 来提高复杂 text 的可读性,使用 = 定义递归展开变量,使 find_files 值包含实际函数调用,能在 foreach 控制下重新展开,简单展开变量无法实现此效果(因 wildcard 在定义 find_files 时只会调用一次)。4. 变量影响:与 let 函数类似,foreach 函数对变量 var 无永久影响,调用前后 var 的值和类型不变。从 list 中获取的值仅在 foreach 执行期间临时有效,执行期间 var 是简单展开变量。若调用前 var 未定义,调用后仍未定义。5. 注意事项:使用生成变量名的复杂变量表达式时需谨慎,因为很多奇怪的变量名虽有效但可能非预期,如 files := $(foreach Esta-escrito-en-espanol!,b c ch,$(find_files)) 这种情况可能是错误的,除非 find_files 引用的变量名确实为 Esta-escrito-en-espanol! 。
7. file 函数
- 函数功能:
file函数支持在 Makefile 中对文件进行读写操作。写操作有两种模式:覆盖(overwrite),即新文本写入文件开头,覆盖原有内容;追加(append),新文本写入文件末尾,保留原有内容。若文件不存在,两种模式下均会创建文件。写操作失败(如文件无法打开)将导致致命错误。写文件时,file函数返回空字符串。读文件时,函数返回文件内容(去除末尾换行符,若有),读取不存在的文件返回空字符串。 - 函数语法:
$(file op filename[,text])。函数执行时,先展开所有参数,再根据op指定的模式打开filename对应的文件。op为操作符,>表示覆盖写入,>>表示追加写入,<表示读取文件内容;filename为目标文件名;操作符与文件名间可包含空格。读文件时不能提供text参数;写文件时,text内容将写入文件,若text末尾无换行符会自动添加(text为空字符串时也会添加),不提供text则不写入内容 。 - 应用示例:当构建系统命令行长度受限,且命令支持从文件读取参数时,
file函数十分有用。许多命令约定以@开头的参数指定包含更多参数的文件。例如:- 简单写入并使用文件参数:
program: $(OBJECTS)
$(file >$@.in,$^)
$(CMD) $(CMDFLAGS) @$@.in
@rm $@.in
上述代码将 $^(所有先决条件)写入 $@.in 文件(覆盖原有内容),然后命令 $(CMD) 通过 @$@.in 读取参数执行,最后删除临时文件。
- 按行写入并使用文件参数:
program: $(OBJECTS)
$(file >$@.in) $(foreach O,$^,$(file >>$@.in,$O))
$(CMD) $(CMDFLAGS) @$@.in
@rm $@.in
此代码先清空创建 $@.in 文件,再通过 foreach 循环将每个先决条件逐行追加写入文件,后续命令执行和文件删除操作与上例类似。
8. call 函数
- 函数功能:
call函数可用于创建新的参数化函数。能将复杂表达式作为变量值,再用call函数传入不同值进行展开,实现类似自定义带参数函数的功能。 - 函数语法:
$(call variable,param,param,…)。make展开此函数时,会将每个param赋值给临时变量$(1)、$(2)等,$(0)包含variable。参数数量无最大和最小限制,但无参数调用无意义。然后在这些临时赋值的上下文中,variable作为make变量展开,variable值中对$(1)等的引用会解析为call调用时的对应参数。 - 使用要点:
variable是变量名,书写时一般不使用$或括号(若希望变量名不是常量,可在其中使用变量引用)。- 若
variable是内置函数名,即使存在同名make变量,也总是调用内置函数。 call函数在将param参数赋值给临时变量前会先展开它们,这可能导致包含foreach、if等有特殊展开规则的内置函数引用的变量值,运行结果与预期不符。
- 示例:
- 简单参数反转示例:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
foo 的值为 b a 。
- 在路径中搜索程序示例:
pathsearch = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH)))))
LS := $(call pathsearch,ls)
LS 变量包含类似 /bin/ls 的值。
- 函数嵌套(实现
map函数)示例:
map = $(foreach a,$(2),$(call $(1),$(a)))
o = $(call map,origin,o map MAKE)
o 最终包含类似 file file default 的值。
注意事项:给 call 函数参数添加空格时需谨慎,与其他函数一样,第二个及后续参数中的任何空格都会保留,可能导致意外结果。提供参数时,最好去除所有多余空格。
9. value 函数笔记
- 函数功能:
value函数提供了一种使用变量值而不进行展开的方式。但它无法撤销已经发生的展开,例如对于简单展开变量,其值在定义时已展开,此时value函数返回的结果与直接使用该变量相同。 - 函数语法:
$(value variable)。variable是变量名,书写时一般不使用$或括号(若希望变量名不是常量,可在其中使用变量引用)。 - 返回值特点:函数结果是一个包含
variable值的字符串,且不进行任何展开。例如在以下 Makefile 中:
FOO = $PATH
all:
@echo $(FOO)
@echo $(value FOO)
- 第一个
echo语句输出ATH,因为$P会被当作make变量展开($PATH中$P被错误展开)。 - 第二个
echo语句输出当前$PATH环境变量的值,因为value函数避免了对FOO值的展开,保留了$PATH原样。
使用场景:value 函数通常与 eval 函数结合使用(eval 函数相关内容见《The eval Function》) 。
10. eval 函数笔记
1. 函数功能
eval 函数非常特殊,它允许你定义非常量的新 Makefile 构造,这些构造是其他变量和函数求值的结果。eval 函数会先展开其参数,然后将展开结果按照 Makefile 语法进行解析,这些展开结果可以用来定义新的 Make 变量、目标、隐式或显式规则等。
2. 函数返回值
eval 函数的返回值始终为空字符串,因此它几乎可以放在 Makefile 中的任何位置而不会导致语法错误。
3. 双重展开特性
eval 函数的参数会经历两次展开:首先由 eval 函数进行第一次展开,然后展开的结果在被当作 Makefile 语法解析时会再次展开。这意味着在使用 eval 函数时,可能需要对 $ 字符进行额外的转义处理。在这种情况下,value 函数有时会很有用,可用于避免不必要的展开。
4. 示例及解释
示例代码
makefile
PROGRAMS = server client
server_OBJS = server.o server_priv.o server_access.o
server_LIBS = priv protocol
client_OBJS = client.o client_api.o client_mem.o
client_LIBS = protocol
# Everything after this is generic
.PHONY: all
all: $(PROGRAMS)
define PROGRAM_template =
$(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)
ALL_OBJS += $$($(1)_OBJS)
endef
$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))
$(PROGRAMS):
$(LINK.o) $^ $(LDLIBS) -o $@
clean:
rm -f $(ALL_OBJS) $(PROGRAMS)
代码解释
- 变量定义:
PROGRAMS定义了要构建的程序列表,这里是server和client。- 分别为
server和client定义了对应的目标文件列表(_OBJS)和库文件列表(_LIBS)。
PROGRAM_template模板定义:- 使用
define定义了一个模板PROGRAM_template,它接受一个参数$(1)。 $(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)定义了一个规则,目标是$(1),依赖是对应的目标文件和库文件。ALL_OBJS += $$($(1)_OBJS)将当前程序的目标文件添加到ALL_OBJS变量中。- 注意这里使用了
$$进行转义,以应对eval的双重展开。
- 使用
eval和foreach结合使用:$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))遍历PROGRAMS列表中的每个程序,调用PROGRAM_template模板并将程序名作为参数传递,然后使用eval函数对结果进行解析,动态生成规则。
- 通用规则和清理规则:
$(PROGRAMS):定义了一个通用的规则,用于链接生成最终的程序。clean规则用于清理生成的目标文件和程序。
使用 eval 的好处
- 模板定义可以非常复杂,使用
eval可以将复杂的部分封装起来,提高代码的可维护性。 - 可以将复杂的通用部分放在另一个 Makefile 中,然后在各个单独的 Makefile 中包含它,使各个单独的 Makefile 更加简洁明了。
11. origin 函数
1. 函数功能
origin 函数与大多数其他函数不同,它不操作变量的值,而是用于获取变量的相关信息,具体是告知变量的定义来源。
2. 函数语法
其语法为 $(origin variable),其中 variable 是要查询的变量名,并非对该变量的引用。通常书写时不用 $ 或括号(若希望变量名不是常量,可在其中使用变量引用)。
3. 函数返回值
函数返回一个字符串,表明变量的定义方式:
undefined:表示变量从未被定义过。default:意味着变量有默认定义,像CC等变量通常如此。不过若重新定义了默认变量,origin函数返回的是后续定义的来源。environment:说明变量是从传递给make的环境中继承而来。environment override:表示变量从传递给make的环境中继承,并且由于使用了e选项,它覆盖了 Makefile 中该变量的设置。file:表示变量是在 Makefile 中定义的。command line:说明变量是在命令行中定义的。override:意味着变量是在 Makefile 中使用override指令定义的。automatic:表示变量是为执行每个规则的命令脚本而定义的自动变量。
4. 实际应用
该信息除了满足用户的好奇心外,主要用于判断是否要采用变量的值。例如:
示例一
假设存在一个 Makefile foo 包含另一个 Makefile bar。希望在运行 make -f bar 时,若环境中已有 bletch 的定义,bar 也能对 bletch 进行定义;但如果 foo 在包含 bar 之前已定义 bletch,则不希望覆盖该定义。可以在 bar 中使用如下代码:
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
若 bletch 是从环境中定义的,此代码会对其重新定义。
示例二
若想在 bletch 来自环境(即便使用 -e 选项)时覆盖其之前的定义,可使用如下代码:
ifneq "$(findstring environment,$(origin bletch))" ""
bletch = barf, gag, etc.
endif
当 $(origin bletch) 返回 environment 或 environment override 时,就会进行重新定义。
12. flavor 函数
1. 函数功能
flavor 函数和 origin 函数类似,它并不对变量的值进行操作,而是用于获取变量的相关特性信息。具体来说,它能够告知变量的类型(参考《The Two Flavors of Variables》)。
2. 函数语法
其语法为 $(flavor variable),这里的 variable 指的是要查询的变量名,并非对该变量的引用。一般情况下,书写时不需要使用 $ 或括号。不过,要是你希望变量名不是常量,也可以在其中使用变量引用。
3. 函数返回值
该函数会返回一个字符串,用于标识变量的类型:
undefined:若变量从未被定义过,函数返回此结果。recursive:若变量是递归展开变量,函数返回该值。递归展开变量在定义时不会立即展开其引用的其他变量,而是在使用时才进行展开。simple:若变量是简单展开变量,函数返回此值。简单展开变量在定义时就会立即展开其引用的其他变量。
flavor 函数为我们提供了一种查看变量类型的方式,有助于我们在 Makefile 编写过程中更好地理解和处理不同类型的变量,确保变量的使用符合预期,避免因变量类型导致的意外错误。
浙公网安备 33010602011771号