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
: 函数库打包程序。默认命令是ar
AS
: 汇编语言编译程序。默认命令是as
CC
: C语言编译程序。默认命令是cc
CXX
: C++语言编译程序。默认命令是g++
CO
: 从 RCS文件中扩展文件程序。默认命令是co
CPP
: C程序的预处理器(输出是标准输出设备)。默认命令是$(CC) –E
FC
: Fortran 和 Ratfor 的编译器和预处理程序。默认命令是f77
GET
: 从SCCS文件中扩展文件的程序。默认命令是get
LEX
: Lex方法分析器程序(针对于C或Ratfor)。默认命令是lex
PC
: Pascal语言编译程序。默认命令是pc
YACC
: Yacc文法分析器(针对于C程序)。默认命令是yacc
YACCR
: Yacc文法分析器(针对于Ratfor程序)。默认命令是yacc –r
MAKEINFO
: 转换Texinfo源文件(.texi)到Info文件程序。默认命令是makeinfo
TEX
: 从TeX源文件创建TeX DVI文件的程序。默认命令是tex
TEXI2DVI
: 从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是texi2dvi
WEAVE
: 转换Web到TeX的程序。默认命令是weave
CWEAVE
: 转换C Web 到 TeX的程序。默认命令是cweave
TANGLE
: 转换Web到Pascal语言的程序。默认命令是tangle
CTANGLE
: 转换C Web 到 C。默认命令是ctangle
RM
: 删除文件命令。默认命令是rm –f
关于命令参数的变量¶
下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是空。
ARFLAGS
: 函数库打包程序AR命令的参数。默认值是rv
ASFLAGS
: 汇编语言编译器参数。(当明显地调用.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 编写过程中更好地理解和处理不同类型的变量,确保变量的使用符合预期,避免因变量类型导致的意外错误。