%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 [[#make 构建流程说明|❓make 解析 Makefile 文件进行构建的流程是什么?]] ![[05-工具/GNU 工具/make 工具#^y1pvfk]] ![[05-工具/GNU 工具/make 工具#^knfezf]] ![[05-工具/GNU 工具/make 工具#^5fdqhy]] #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # make 工具 make 是 Unix/Linux 下的一个**自动化构建工具**,其会读取名为 "**==Makefile==**"(或 **makefile**)的文本文件 ,**根据其中指定的规则与依赖关系自动执行构建步骤**,完成编译构建过程。 make 工具的主要特点及作用: - **自动化构建**:`make` 可以自动化编译过程,从而减少重复性工作。 - **依赖管理**:`make` 管理文件之间的依赖关系。如果一个文件被更新,**`make` 将重新编译依赖于该文件的所有文件。** - **增量编译**:**`make` 仅重新编译自上次构建以来已更改的文件,而不是整个项目**,没有修改过的文件不用重新编译,显著提高编译效率。 - **可定制**:`Makefile` 提供了高度的可定制性,可以用于定义复杂的构建逻辑和多种构建目标。 - 支持**多线程并发操作**:会极大缩短编译时间。 > [!NOTE] 除了 Linux 下的 GNU make 工具外,大多数 IDE 也都提供了类同 make 的命令,例如 Delphi 的`make`,Visual C++的 `nmake` 等 > [!info] Windows 下 MinGW 工具链提供的 make 程序名为 `mingw32-make` <br> ## make 运行命令 - `make [OPTION]... [target]...`: **构建目标** - `[target]`:需要构建的目标。 未指定时,默认构建 "**`Makefile` 文件内的==首个目标==**" - `-f <filename>`: **读取指定文件**。未指定时,make 工具**会自动查找==当前目录中的 `Makefile`==** - `-n | --just-print`: **打印==构建时将被执行的命令==,但不会实际执行** - `-B | --always-make`: **强制==重新构建所有目标==**,无论其是否已是最新的 - `-s | --silent`:**不输出指令** - `-C <dir>`:改变**当前工作目录** - `var=value`: 为**变量`var`** 赋值 ```shell make # 根据当前目录下的 Makefile 文件进行构建, 构建其中声明的第一个目标 make clean # 根据当前目录下的 Makefile 文件进行构建, 构建`clean`目标 ``` <br> ## make 的变量优先级 make 在运行时,会检查多个**来源的变量**,其中**变量优先级**如下: - **==命令行变量==**:优先级**最高**,覆盖所有其他来源的变量定义。 - **==环境变量==**:其次优先级,如果在命令行或 `Makefile` 中没有定义,则使用环境变量的值。 - **==`Makefile` 中定义的变量==**:如果没有在命令行和环境中定义,则使用 `Makefile` 中定义的变量。 > [!example] 通过**命令行指定变量** > > ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-E4B8EC153894215DF46F1DA9798047C1.png|869]] <br> ## make 构建流程说明 `make` 命令会**读取当前工作目录下 `Makefile` 文件**,其执行以下的**两阶段工作流**[^5]: 1. 第一阶段: 1. **读取所有 MakefIle 文件**,包括通过 `include` 引入的。 2. **解析变量**: `make` 解析文件中的所有变量定义及值,保存于内存中; 3. **解析规则**: `make` 解析**每个目标及其依赖关系**,并生成 **==依赖关系树==**。 2. 第二阶段: 1. **确定==最终目标==**: `make` **==默认==** 选择 `Makefile` 文件中声明的 **==第一个目标==作为最终目标**(可通过**命令行参数**或 **`.DEFAULT_GOAL` 指定其他目标**) 2. **递归==构建依赖项==**:`make` 会 **"递归"检查其==依赖项==**,**若依赖项不存在则==执行指定命令进行构建==** 3. **构建最终目标**:在递归构建完成所有依赖项之后,**构建最终目标(默认目标或指定目标)** > [!NOTE] > > 一个 `Makefile` 文件可以**包含任意多个目标(targets)**。 > 运行 `make` 时,只有**默认目标** 或**指定目标** 及其**依赖树上的目标会被构建**。 > ^j6v45t > [!info] Makefile 中变量、函数、规则的解析时机[^5] > > ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-1EE4A41A8827D1997C58C004B5837A5E.png|599]] > > <br> ## make 构建时的过期更新判断 > ❓<font color="#c0504d">make 是如何判断 target 是否过期(out of date)的?</font> ^y1pvfk 对于 `Makefile` 中的一个目标 `target`,处理方式为: 1. `make` **检查==当前目录下名为 `target` 的文件是否存在==**; 2. `make` **递归检查 `target` 的各个==依赖项文件==是否存在**; 3. 当**保证 `target` 的==所有依赖项存在==** 后(原先不存在的也完成构建),**检查 `target` 与其各个依赖项文件的 =="last-modified" 时间戳==**。 当 `target` 不存在,或者 **`target` 文件的时间戳小于其依赖项文件时**(意味着其依赖项已更新),则**将重新构建生成 `target` 文件**[^6]。 <br> ## make 错误码处理 当执行每个命令时,**`make` 会通过系统调用==获取该命令的返回值==(或退出状态)**,以确定命令是否成功。该返回值通被称为 **“错误码”或“退出状态码”**。 > [!Info] Unix/Linux 系统中,命令执行后会返回一个退出状态码——整数,通常为 0 表示成功,而非 0 表示失败。 `make` 执行命令时,**如果命令的退出状态码是非 0,`make` 将认为该命令失败,默认情况下会==停止执行接下来的所有命令==**。 有几种方式可以忽略执行失败的命令,**不终止 make 而继续执行剩余命令**: 1. 在 Makefile 中**单条命令前添加 `-` 符号**,表示忽略该命令的退出状态。 2. 在 Makefile 中单条命令后添加 `|| true`; 3. 在 Makefile 中使用 `.IGNORE` 伪目标,表示当该目标的命令出错时忽略; 4. 使用 `make -i` 或 `make --ignore-errors` 选项运行,**忽略所有命令的错误**; > [!example] > 方式一: > > ```Makefile > rm_log: > @echo "This is a successful command." > -rm debug* > -rm *log* > -rm core* > @echo "This will be printed because the previous command's error was ignored." > ``` > > 方式二: > > ```Makefile > rm_log: > @echo "This is a successful command." > rm debug* || true > rm *log* || true > rm core* || true > @echo "This will be printed because the previous command's error was ignored." > ``` > > 方式三: > > ```makefile > .IGNORE: rm_log: > > rm_log: > rm debug* > rm *log* > rm core* > ``` > > --- <br><br> # Makefile 文件 Makefile 是在 Linux 平台下使用 `make` 工具实现 **自动化构建** 所需编写的配置文件。 **`make` 工具**将根据 Makefile 文件中定义的规则**对整个项目自动化进行编译构建**,**生成可执行文件**。 <br> # Makefile 模块化构建 在一个工程项目中,可**将==不同模块的构建规则==分离到不同的 Makefile 文件中**(**以 `.mk` 作为后缀进行标识**), **再由==主 Makefile 文件==中通过 `include`引入**,从而使得**整个项目的构建过程更加 "模块化"、易于维护**。 ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-906DAB3C23829C97F270AE077EF15DD8.png|668]] # Makefile 文件编写说明 > [!NOTE] 注释 > Makefile 中以 `#` 作为单行注释,但**不支持在 "target" 的命令部分添加行内注释!** > > 建议的注释写法:通过 `@` 印制命令回显作为注释; > > ```Makefile > build: > @# 此行可作为一个行内注释 > @echo 测试打印 > ``` > > ## 规则 Rule 规则是 Makefile 的基本构成单元,每条**规则**包括三部分:**目标**、**依赖项**、**命令**。 一条规则描述了 "**==一个目标的依赖项==以及 "==构建该目标时所需执行的命令=="**": 1. **目标(Target)**: 1. 要==**构建的文件**==,例如**可执行文件**或**可重定位目标**文件。 2. **==伪目标==**:不生成实际文件,无依赖,仅用于**标识执行特定操作**,例如 `clean`; 2. **依赖(Prerequisites)**:**目标所依赖的==文件或其他目标==**。 3. **命令(recipe)**: **构建该 target 所需执行的命令**(**任意 shell 命令**),通常为 **==生成目标文件的 shell 命令==**,如**编译命令或链接命令**。 ![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-91424AADB12285614B44478CC2F2E3E0.png|720]] > [!caution] Makefile 文件中要求每条规则中的命令(recipe) 必须以 `<TAB>` 作为缩进!否则无法识别 > [!caution] 当为**同一个目标**分别指定了多条 "**常规规则**" 时,**==make 只会应用其中一条规则==**,不会合并规则。 > > make 会**依次检查各条规则**: > > - 对于 "**模式规则**",似乎会优先应用 "**最佳匹配项**"(**所依赖文件的时间戳最新的一项规则**),其次是 "**最先声明的**" > - 对于 "**一般规则**",似乎会应用 "**后声明的项**"。 > > ```makefile > .PHONY: all > > # 获取当前目录下所有的.cpp, .c文件 > OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp)) > OBJS += $(patsubst %.c, %.o, $(wildcard *.c)) > OBJS += foo.o bar.o > > # 指定目标all依赖于$(OBJS)中的所有.o文件 > all: $(OBJS) > $(info $(OBJS)) > > # 下面两条`%.o`的模式规则只会应用其中一条, 优先应用应用 "最佳匹配"(所依赖文件的时间戳最新的一项规则),其次是 "最先声明的" > # 指定 "所有%.o文件" 依赖于其同名的%.c > %.o: %.c > @echo "Build $@ from 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 中需要对 `
进行转义,转义符同为`
——**即 `$` 表示实际字符 `
**, **`$$` 表示实际字符 `$`**。 ```makefile EXEC: $(SRCS) $(CXX) $(CFLAGS) -o $@ ``` <br><br><br> # Makefile 文件示例 > [!example] 模版示例 ⭐ > > ```makefile > CXX := g++ > CXXFLAGS := -std=c++11 -Wall -g > > # 指定头文件目录 > INCLUDES := ./src/include > CXXFLAGS += $(addprefix -I, $(INCLUDES)) > # 指定源文件目录 > SRC_DIRS := ./src/channel ./src/epoller ./src/eventloop > # 获取所有.cpp源文件 > SRCS := $(wildcard $(addsuffix /*.cpp, $(SRC_DIRS))) > # 需对应生成的.o目标文件 > OBJS := $(SRCS:.cpp=.o) > > # 目标可执行文件 > EXEC := a.out > > > $(EXEC): $(OBJS) > $(CXX) $(CXXFLAGS) -o $@ $^ > > %.o: %.cpp > $(CXX) $(CXXFLAGS) -c lt; -o $@ > > clean: > rm -f $(EXEC) $(OBJS) > > .PHONY: clean > ``` > > > [!example] 模版示例: > > ```makefile title:makefile > OBJECTS = sample.exe, useQuote.exe > > # 引入自定义的模版文件, 模版文件中声明了除 OBJECTS 以外的构建规则. > include ../GNU_makefile_template > > INCLUDES = -I./include -I../include > > sample.exe: sample.o Quote.o > $(CXX) $(CXXFLAGS) $(INCLUDES) sample.o Quote.o -o $@ > > useQuote.exe: useQuote.o Quote.o > $(CXX) $(CXXFLAGS) $(INCLUDES) useQuote.o Quote.o -o $@ > ``` > > 上述 makefile 引入的模版 makefile 文件如下: > > ```makefile title:GNU_makefil_template > CXX = g++ > CXXFLAGS = -std=c++0x -Wall -g > INCLUDES = > > all: $(OBJECTS) > > %.o: %.cpp > $(CXX) $(CXXFLAGS) $(INCLUDES) -c 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}`中的首个`
是转义字符, 用于转义后一个`
字符, 使其不被解释为变量引用. # `AM_HOME`为环境变量, 其值是`*/ics2023/abstract-machine` # 2) `ARCH`变量来自命令行参数, 用于指定编译器架构. Makefile.%: tests/%.c latest @/bin/echo -e "NAME = $*\nSRCS = lt;\ninclude ${AM_HOME}/Makefile" > $@ @if make -s -f $@ ARCH=$(ARCH) $(MAKECMDGOALS); then \ printf "[%14s] $(COLOR_GREEN)PASS$(COLOR_NONE)\n" $* >> $(RESULT); \ else \ printf "[%14s] $(COLOR_RED)***FAIL***$(COLOR_NONE)\n" $* >> $(RESULT); \ fi -@rm -f Makefile.$* run: all @cat $(RESULT) @rm $(RESULT) gdb: all clean: rm -rf Makefile.* build/ latest: ``` <br><br><br> # 参考资料 #### 官方文档 - [GNU Make Manual - GNU Project](https://www.gnu.org/software/make/manual/) - 阅读建议:![[_attachment/05-工具/GNU 工具/make 工具.assets/IMG-make 工具-7F4A730E38F5434651D52C923D5DCDA6.png|637]] - [Quick Reference (GNU make)](https://www.gnu.org/software/make/manual/html_node/Quick-Reference.html) ⭐ cheat sheet - [Complex Makefile (GNU make)](https://www.gnu.org/software/make/manual/html_node/Complex-Makefile.html) ⭐ 一个复杂样例 #### 快速入门 - [Makefile Tutorial By Example](https://makefiletutorial.com/#variables) # Footnotes [^1]: [Automatic Variables (GNU make)](https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html) [^2]: [Implicit Rules (GNU make)](https://www.gnu.org/software/make/manual/html_node/Implicit-Rules.html) [^3]: [Implicit Variables (GNU make)](https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html) [^4]: [Special Targets (GNU make)](https://www.gnu.org/software/make/manual/html_node/Special-Targets.html) [^5]: [Reading Makefiles (GNU make)](https://www.gnu.org/software/make/manual/html_node/Reading-Makefiles.html) [^6]: [Rule Example (GNU make)](https://www.gnu.org/software/make/manual/html_node/Rule-Example.html) [^7]: [Prerequisite Types (GNU make)](https://www.gnu.org/software/make/manual/html_node/Prerequisite-Types.html) [^8]: [Wildcards (GNU make)](https://www.gnu.org/software/make/manual/html_node/Wildcards.html) [^9]: [Echoing (GNU make)](https://www.gnu.org/software/make/manual/html_node/Echoing.html) [^10]: [Static Usage (GNU make)](https://www.gnu.org/software/make/manual/html_node/Static-Usage.html) [^11]: [Pattern Rules (GNU make)](https://www.gnu.org/software/make/manual/html_node/Pattern-Rules.html) [^12]: [Pattern Intro (GNU make)](https://www.gnu.org/software/make/manual/html_node/Pattern-Intro.html) [^13]: [Flavors (GNU make)](https://www.gnu.org/software/make/manual/html_node/Flavors.html) [^14]: [Appending (GNU make)](https://www.gnu.org/software/make/manual/html_node/Appending.html) [^15]: [Functions (GNU make)](https://www.gnu.org/software/make/manual/html_node/Functions.html)