lt;" > > # 指定 "所有%.o文件" 依赖于其同名的%.cpp > %.o: %.cpp > @echo "Build $@ from lt;" > > # 对于下面两个模式规则, 必须有命令, 否则会报错(没有命令的话make将不进行构建, 从而%.o缺少依赖项, 报错无法构建%.o) > # 指定%.cpp文件为目标———当所需的%.c文件不存在时,将执行下面的命令 > %.cpp: > @echo "> Build: $@" > > # 指定%.cpp文件为目标———当所需的%.cpp文件不存在时,将执行下面的命令 > %.c: > @echo "> Build: $@" > ``` > > ### 模式规则 Pattern Rules 模式规则[^11] [^12]: - `target` 中**包含一个通配符 `%`**,表示**匹配任意非空字符串**。 - `prerequisites` 中**同时使用 `%`时** ,表示**与 `target` 匹配内容相同**。 ```Makefile # 模式规则: 每一个%.o文件都依赖于同名的%.c文件 %.o: %.c $(CC) -c $(CFLAGS) lt; -o $@ ``` > [!caution] 模式规则不会被视作为 "默认目标"! > [!caution] 模式规则必须带有命令,否则 make 将认为没有匹配的规则,不构建该目标。 > > ```makefile > .PHONY: all > > all: foo.c bar.c zoo.c > > # 模式规则. 若未指定命令而仅有`%.o`目标, 则将会报错——认为没有匹配的规则, 不会进行构建. > %.c: > @echo "Building $@" # 对于"模式规则", 必须有命令 > ``` > > #### 静态模式规则 为给定 `target` 指定**模式规则**,**要求每个 target 都必须能匹配 target-pattern,否则 make 将报错**[^10]。 ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-933331FFD0BD4556EB2FFD4E594A639B.png|735]] ```Makefile OBJS = foo.o bar.o zoo.c all: $(OBJS) # 静态模式规则: $(OBJS)中每个%.o文件依赖于%.c文件 $(OBJS): %.o: %.c $(CC) -c $(CFLAGS) lt; -o $@ ``` ### 隐式规则 Implicit Rules Makefile 中支持一些 "**隐式规则**"[^2],能够简化规则编写 例如**当目标为 `.o` 后缀的文件时**,会**自动添加同名的 `.c` 文件作为依赖项**。 ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-2FDA04FDB6F05266C61AAEA1FFC63B74.png|490]] ![[#内置变量]] ### 双冒号规则 双冒号规则使用 `::`,即格式为 `target:: reprequisites`。 make 要求,当**一个 `target` 同时==出现在多条规则==** 中时,这些规则**必须要么全是常规规则(`:`),要么全是双冒号规则(`::`)**: - 对于**常规规则** (`:`):`make` **==只会应用其中一条规则==**,不会合并,也不会分别执行。 - 对于**双冒号规则** (`::`):`make` 会 **==独立处理==每一条规则**,**分别执行**。 ```makefile # 运行make, 则下面两个规则都会被应用/执行 all: target target:: dep1 @echo "Building target from dep1" target:: dep2 @echo "Building target from dep2" dep1: dep2: ``` <br><br> ## 目标 target 目标可以是 "**实际文件名**" 或者 "**文件名的 pattern**" 或者 "**伪目标**"。 - **伪目标**:不生成实际文件,无依赖,仅用于**标识执行特定操作**,例如 `clean` 。 ### 特殊内置目标 make 支持的内置目标[^4],用以实现特定功能: - `.PHONY`:其依赖项将被明确标识为 "**伪目标**",因而**不会因同名文件的存在而阻碍构建**。 - `.DEFAULT`:其命令(recipe)**将应用于所有 "==无规则==" 的目标(即用作了其他目标的依赖项,但自己却不具有任何规则的目标)** > [!faq] ❓为什么要将目标 `clean` 声明为 `.PHONY` 的依赖项? > > 通过 `.PHONY` 明确标识 `clean`为伪目标,避免当目标中存在名为 `clean` 的文件时,阻碍 `make clean` 执行。 > > ```Makefile > .PHONY: clean > clean: > -rm edit $(objects) > ``` > > ^5fdqhy <br> ## 依赖项 Prerequisites ### 仅顺序依赖 Order-only Prerequisites > ❓ <font color="#c0504d">如何实现 "当依赖项更新后,不触发对 target 的更新?"</font> ^knfezf 规则中的**依赖项 `prerequisites`** 在新于**构建目标 `target`** 时会**触发 make 对 target 的更新构建**。 要取消这一依赖关系,使得 "**当依赖项更新后,不触发更新 target**",**可通过 "==Order-only-prerequisites==" 指定**[^7]: ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-0AF3CF710A1CEB6EA522166633812747.png|853]] 如上图,`|` 左侧的是常规依赖项,`|` 右侧的即为 "**==Order-only-prerequisites==**"。 target 需要依赖于 "Orde-only-prerequisites",当这些依赖项不存在时会递归构建。但是,当这些依赖项 "更新" 后,却不会触发对 "target" 的更新。 #### 使用场景 需要将各个目标文件存放在不同目录,且在首次执行 `make` 前目录均不存在的情况 ```Makefile OBJDIR := objdir BOJS := $(appendfix $(OBJDIR)/, foo.o bar.o baz.o) $(OBJDIR)/%.o : %.c $(COMPILE.c) $(OUTPUT_OPTION) lt; all: $(OBJS) $(OBJS): | $(OBJDIR) # 当目录的最后修改时间更新后, 不会触发对目标的更新 $(OBJDIR): mkdir $(OBJDIR) ``` ## 命令 recipe ##### 抑制命令回显 `make` 构建目标时会**打印目标对应的 "所有命令"**,通过**在命令前声明 `@` 指示抑制==命令内容的回显==**[^9]。 ```Makefile CC = gcc CFLAGS = -Wall -g SRC = main.c util.c OBJ = $(SRC:.c=.o) all: myprogram @echo "Build completed successfully" myprogram: $(OBJ) @echo "Linking objects..." $(CC) $(CFLAGS) -o $@ $^ %.o: %.c @echo "Compiling lt;..." $(CC) $(CFLAGS) -c lt; -o $@ # 伪目标 .PHONY: clean clean: @echo "Cleaning up..." rm -f $(OBJ) myprogram ``` > [!example] > > ```Makefile > all: > echo "This will be displayed" > @echo "This will not be displayed" > @echo "But this message will be" > ``` > > 上述示例执行 `make` 后,将输出如下内容,其中**后两条 "命令语句" 自身的回显被抑制了** > > ```text > echo "This will be displayed" > This will be displayed > This will not be displayed > But this message will be > ``` > > ## 通配符 Wildcard Makefile 中支持的通配符[^8](含义同 shell 中的通配符):`*`、`?`、`[...]`。 > [!NOTE] `%` 不是常规通配符,仅**用于==模式匹配==场景** > > `%` 表示匹配 "**==一个单词中==的任意多个字符**",成对出现时两个 `%` 表示 "**相同内容**"。 > > 1. **模式规则**中,例如 `%.o: %.c`,表示**每个 `".o"` 的依赖项为同名的 `".c"` 文件**。 > 2. **替换引用**中,例如 `$(var:%.c=%.o)`,表示将 `var` 中**每个单词**末尾的 `.c` 项替换为`%.o` > 3. `$(patsubstr, pattern,replacement,text)` 函数中,效果同上 > [!caution] > > 通配符 `*`、`?`、`[...]` 可用于 `rule` 中的 target、prerequisites、recipe 部分。 > 当用于 "**定义变量**" 时,必须搭配 `$(wildcard )` 函数使用,否则会视作为字符 `'*'` 本身。 > > ```Makefile > objects := $(wildcard *.o) > ``` > <br><br> ## 变量 make 中的**变量的值**均以 "**字符串**" 形式存在。 > [!NOTE] make 会自动继承环境变量 > > - **make 在启动时==可见的每个环境变量都==被转换为==具有相同名称和值的make变量==**。 > - **==makefile 中的显式赋值==将会覆盖环境变量值**。 > ### 变量定义&赋值 make 中有**四种类型**的变量**定义/赋值**[^13]: | 变量定义/赋值方式 | 语法 | 说明 | | --------- | -------------- | ------------------------------------------------------------------------------------------ | | 递归展开变量赋值 | `=` 或 `define` | **变量值**在该变量**每次被引用**时都会 "**重新计算**"(考虑该变量的"值"中引用了另一个变量) | | 简单展开变量赋值 | `:=` 或 `::=` | **变量值**在该定义时**一经确定,此后保持不变** <br>(例如 `LIBS := $(sort LIBS am klib)`,如果使用 `"="` 将导致**无限递归**) | | 立即展开变量赋值 | `:::=` | | | 条件变量赋值 | `?=` | 仅当**变量尚未定义**时才进行定义,否则保留原值,例如**已存在的环境变量**,**make 内置变量** | | 追加赋值 | `+=` | 追加赋值,会自动添加一个**空格分隔符** | > [!NOTE] 在 Makefile 中,**引用一个未被定义(赋值)的变量将得到 "==空字符串=="** > > 注意: make 内置了一些变量供 "**隐式规则**" 使用,这些变量是具有默认值的。 > [!caution] 递归展开变量**==不能引用其自身==** > > **递归展开变量的值**每次在其 "**==被引用时重新计算==**",因此 **==不支持引用其自身==**,会导致 **==无限递归==**。 > > 如果要**实现追加赋值**,可使用 `+=`,如下所示[^14]: > > ```Makefile > CFLAGS = $(include_dirs) -O > include_dirs = -Ifoo -Ibar > > CFLAGS = $(CFLAGS) -O # 错误, 会导致无线递归 > CFLAGS += -O # 正确, CFLAGS的值会在每次计算得到`$(include_dirs) -O`的值后再追加上`-O` > ``` ### 变量引用 引用变量值的方式: - **常规引用**:`$(var)` 或 `${var}` - **替换引用**:`$(var:a=b)` 或 `${var:a=b}`,会引用变量 `var` 的值,并**将其中每个==单词末尾==(空格分隔)的 `a` 替换为 `b`** > [!NOTE] 替换引用 `$(var:a=b)` 的效果等价于函数 `$(patsubst %a,%b,var)` > > ```Makefile > objects = foo.o bar.o baz.o > > tst = $(objects:.o=.c) > # 等价于 > tst = $(patsubst %.o, %.c, $(objects)) > ``` > ^3zwlmr ### 清除变量 清除变量有两种方式: 1. 将变量值**设置为空** 2. 使用 `undefine` 关键字声明**取消定义** 差别仅在于,当使用 `$(origin )` 或 `$(flavor )` 函数时,**第二种方式将打印 "`undefine`" 而不是空字符串**。 ### 自动变量 Automatic Variables 自动变量[^1]: **其值取自 rule 中目标或依赖项** - `$@`:target 的名称 - `lt;`:**第一个依赖项**的名称 - `$+`:所有依赖项的名称(未去重) - `$^`:所有依赖项的名称(去重后的) - `$?`:所有时间戳新于 target 的依赖项的名称 ### 内置变量 make 具有一些内置变量,主要**供隐式规则**使用,最常用的几项为[^3]: - `.DEFAULT_GOAL`:**==默认目标==** - `MAKECMDGOLAS`:调用 `make` 时,**命令行中==指定的构建目标==** - `CC`: Program for compiling C programs; default `cc` - `CXX`: Program for compiling C++ programs; default `g++` - `CFLAGS`: Extra flags to give to the C compiler - `CXXFLAGS`: Extra flags to give to the C++ compiler - `CPPFLAGS`: Extra flags to give to the C preprocessor - `LDFLAGS`: Extra flags to give to compilers when they are supposed to invoke the linker - `SHELL`:指定 make 所使用的默认 shell - `MAKE`:**make 工具自己** 这些**变量可被覆盖**。 <br><br> ## 函数 make 中提供了一系列内置的 "**文本转换函数**"[^15]。 **函数调用**语法:`$(func arg1 arg2 ...)` 或 `${function arg1 arg2 ...}` - `$(flavor var)`:返回**变量的类型**——`recursive`,`simple`, `undefined`(如果是环境变量,即未在 Makefile 中定义的,则会返回 undefined) #### 字符串替换与分析相关 - `$(subst from,to,text)`: 文本替换,将 `from` 全部替换为 `to` - `$(patsubst pattern,replacement,text)`:文本替换(对匹配上 pattern 的部分替换为 replacement) - `$(strip string)`: 剔除**前后空格** - `$(findstring find,in)`: 从`in` 中查找目标值 `find`,若找到返回 `find`,否则**返回空字符串** - `$(filter pattern..., text)` :返回 `text` 中 **==匹配任意一项==** `pattern` 的**单词们** - `$(filter-out, pattern..., text)`:返回 **==不匹配任何一项== `pattern` 的单词们** - `$(word n,text)`:返回 **`text` 中第 `n` 个字符** - `$(words text)`: 返回 **`text` 中的 `words` 数量** - `$(wordlist, s,e,text)`:返回 `text` 中**下标区间 `[s,e]` 区间在范围内的单词们** - `$(addprefix prefix, text)`:为 text 中每一项**添加前缀** - `$(addsuffix suffix, text)`:为 text 中每一项**添加后缀** - `$(join $(list1) $(list2))`:**两个列表中对应项进行拼接** - 例如 `list1 = a b c`, `list2 = 1 2 3`, 则 `$(join $(list1) $(list2))` 得到 `a1 b2 c3` ![[05-工具/GNU 工具/make 工具#^3zwlmr]] #### 文件名相关 - `$(wildcard pattern)`:遍历 `Makefile` 所在目录,将**根据 pattern 匹配所有文件名,输出==匹配上的文件名==**。 => 常用于**获取所有特定后缀的文件** - `$(dir names...)`:提取路径,去掉文件名. - `$(notdir names...)`:去掉路径,只保留文件名 - `$(abspath, names...)` - `$(realpath, names...)` > [!example] > > ```Makefile > objects := $(patsubst %.c,%.o,$(wildcard *.c)) > > foo: $(objects) > cc -o foo $(objects) > ``` > > #### 条件相关 - `$(if condition,then-part[,else-part])`:**若 `condition` 去掉首尾空白字符后为==非空字符串==,则条件为真** > [!example] > > > ```Makefile > CFLAGS_BUILD += $(if $(CONFIG_CC_DEBUG),-Og -ggdb3,) > ``` > #### 自定义函数 - `$(call variable,param,param,...)`: 用于**实现自定义函数**: - `variable` 是个 "**变量名**",该**变量的值**中可通过 `$(1)`、`$(2)` 来**引用 `$(call, )` 传递的各项参数** > [!example] > > ```Makefile > reverse = $(2) $(1) > > foo = $(call reverse,a,b) > ``` > #### make 控制相关函数 - `$(error text...)`: 以 text 为错误消息,**报告一个致命错误**,会**使得 make 退出**。 - `$(warning text...)`: 以 text 为警告消息进行报告,不会令 make 退出。 - `$(info text...)`: 打印 text 消息 #### 执行外部 shell 命令 - `$(shell commands)`:执行一个 shell 命令,并**以命令输出作为返回值** > [!example] > > ```Makefile > contents := $(shell cat foo) > files := $(shell echo *.c) > ``` > > > <br> ## 条件判断 Makefile 中支持下列**四种条件判断语句**: - `ifeq (arg1, arg2)` : 两参数相等时为 true - `ifneq (arg1, arg2)`: 两参数不等时为 true - `ifdef var` :变量`var` 的 **==值存在==** 时为 true(即使为 **==空值==**,也是存在) - `ifndef var`: 变量 `var` 的 **==值不存在==** 时为 true > [!caution] `ifdef` 与 `ifndef` 是根据 "**变量值是否存在**" 进行判断的,**而不是看 "变量值是否为空值"** > > 如果要判断变量值是否为空,应使用 `ifeq ($(var),)` ###### 使用示例 ```Makefile # `ifeq`条件判断 ifeq (arg1, arg2) ... else ... endif ``` ```makefile # `ifdef`条件判断 ifdef var ... ``` <br> ## 文件引入 Include 引入其他 Makefile 文件: - `include <filepath>...`:引入其他 Makefile 文件 - `-include <filepath>`:当指定文件不存在时,**直接忽略而不进行报错** > [!important] 引入说明 > > `make` 会将**被引入的文件内容视为主 `Makefile` 的一部分**(类似于 C/C++中的 `include`,会将**被引入文件的内容**插入)。 > > 如果未明确指定**构建目标**,则将以 "**==合并后的 Makefile== 中的==首个目标==**" 作为 **==默认构建目标==**。 > [!example] 使用示例: > > ```makefile title:makefile > OBJECTS = sample.exe, useQuote.exe > > # 引入自定义的模版文件, 模版文件中声明了除OBJECTS以外的构建规则. > include ../GNU_makefile_template > > sample.exe: sample.o Quote.o > $(CXX) $(CXXFLAGS) sample.o Quote.o -o $@ > > useQuote.exe: useQuote.o Quote.o > $(CXX) $(CXXFLAGS) useQuote.o Quote.o -o $@ > ``` > > 上述 makefile 引入的模版 makefile 文件如下: > > ```makefile title:GNU_makefil_template > CXX = g++ > CXXFLAGS = -std=c++0x -Wall -g > > all: $(OBJECTS) > > %.o: %.cc > $(CXX) $(CXXFLAGS) -c lt; -o $@ > > clean: > rm -rf $(OBJECTS) *.o > ``` > > 运行 `make` 时,`all` 将作为默认目标被构建。 ### 实现多目标构建 运行 `make` 时,只有**默认目标** 或**指定目标** 及其**依赖树上的目标会被构建**。 若要实现同时构建多个目标: - 方式一:**声明第一个目标==依赖多个目标项==** - 方式二:**在一条规则中==声明多个目标==** - 方式二:**目标名使用通配符匹配**,例如 `%.o: %.c` ##### 方式一 **声明第一个目标==依赖多个目标项==** ```Makefile all: t1 target2 target3 target .PHONY: all # 声明为伪目标, 避免存在名为all的文件时阻碍执行构建. target1: ... ... target2: ... ... target3: ... ... clean: rm -f target1 target2 target3 ``` ##### 方式二 在一条规则下声明多个目标,**多个目标将被一起构建, 且这多个目标的依赖项、构建命令均完全相同** ```Makefile t1 t2 t3: r1 r2 ... ``` ##### 方式三 **通配符匹配** ## 转义符 Makefile 中需要对 `
lt; -o $@ > > clean: > rm -rf $(OBJECTS) *.o > ``` > > 运行 `make` 时,`all` 将作为默认目标被构建。 > [!example] 简单示例 > > ```makefile > prog: hello.o > gcc -o prog hello.o > > hello.o: hello.c > gcc -c hello.c > > hello.c: > echo "int main() { return 0; }" > hello.c > echo "Hello World" > ``` > > [!example] 示例二 > > ```makefile > # 定义编译器 > CXX = g++ > # 定义编译器标志 > CXXFLAGS = -Wall -g > > # 定义目标 > TARGET = myprogram > > # 定义目标文件和依赖关系 > $(TARGET): main.o foo.o bar.o > $(CXX) $(CXXFLAGS) -o $(TARGET) main.o foo.o bar.o > > # 定义如何生成目标文件 > main.o: main.cpp foo.h bar.h > $(CXX) $(CXXFLAGS) -c main.cpp > > foo.o: foo.cpp foo.h > $(CXX) $(CXXFLAGS) -c foo.cpp > > bar.o: bar.cpp bar.h > $(CXX) $(CXXFLAGS) -c bar.cpp > > # 定义清理命令 > clean: > rm -f $(TARGET) *.o > ``` > 示例三: ```Makefile # 总结: # 该文件的作用是为tests目录下的每个%.c文件生成一个`Makefile.%`文件, # 并根据该`Makefile.%`文件中的规则进行编译. .PHONY: all run gdb clean latest $(ALL) RESULT = .RESULT $(shell > $(RESULT)) # 重定向输出到文件 # 定义变量颜色 COLOR_RED = \033[1;31m COLOR_GREEN = \033[1;32m COLOR_NONE = \033[0m # ALL默认为tests目录下所有.c文件的文件名(basename, 不包含路径名和.c后缀) ALL = $(basename $(notdir $(shell find tests/. -name "*.c"))) # 默认目标 # all目标: 依赖于所有*.c文件对应的同名Makefile文件 all: $(addprefix Makefile., $(ALL)) @echo "test list [$(words $(ALL))] item(s):" $(ALL) # .PHONY声明了$(ALL)为伪目标, 即为每个*.c文件都对应一个同名伪目标. # 此处指定$(ALL)中每个"目标名称%"依赖于对应的"Makefile.%"文件 $(ALL): %: Makefile.% # 此处规则的作用: # 1) 生成`Makefile.%`文件, 其中包含了对应的`%.c`文件的编译规则. # 2) 运行`make`并根据`Makefile.%`文件中的规则进行编译. # 3) 根据编译结果输出PASS或FAIL, 写入到文件$(RESULT); # 4) 删除`Makefile.%`文件. # 备注: # 1)`${AM_HOME}`中的首个`