学会使用Makefile
本教程乃是makefiletutorial中文版
我建立了这个指南,因为我一直无法完全理解Makefile。它们似乎充斥着隐藏的规则和神秘的符号,问简单的问题也得不到简单的答案。为了解决这个问题,我坐下来花了几个周末的时间,尽可能阅读了关于Makefile的所有资料。我将最关键的知识内容总结到了这个指南中。每个主题都有简要的描述和可供你自行运行的独立示例。
如果你对Make有了较好的理解,可以考虑查看《Makefile Cookbook》,其中提供了一个中等规模项目的模板,以及关于Makefile各部分功能的充分注释。
祝你好运,希望你能够征服令人困惑的Makefile世界!
快速开始
为什么会存在Makefiles呢?
Makefile用于帮助确定哪些大型程序的部分需要重新编译。在绝大多数情况下,会编译C或C++文件。其他编程语言通常也有自己的工具,用于类似于Make的功能。Make还可以用于除了编译之外的其他用途,比如当需要一系列指令根据文件的变化情况而运行时。本教程将集中讨论C/C++编译的使用情况。
这里有一个可能使用Make构建的示例依赖图。如果任何文件的依赖关系发生变化,那么该文件将被重新编译。

Make有哪些替代方案?
流行的C/C++替代构建系统包括SCons、CMake、Bazel和Ninja。一些代码编辑器如Microsoft Visual Studio内建了其自己的构建工具。对于Java,有Ant、Maven和Gradle。其他语言如Go、Rust和TypeScript也有它们自己的构建工具。
像Python、Ruby以及原始JavaScript这样的解释型语言不需要类似于Makefile的东西。Makefile的目标是根据文件的更改情况编译需要编译的文件。但是对于解释型语言的文件,一旦发生更改,就无需重新编译。程序运行时会使用文件的最新版本。
Make的版本和类型
Make有各种实现版本,但本指南中的大部分内容适用于任何你使用的版本。但是,它是专门针对GNU Make编写的,这是Linux和MacOS上的标准实现。所有的示例都适用于Make的3和4版本,它们几乎是等效的,只是一些难以理解的细微差别。
运行示例
要运行这些示例,你需要一个终端和已安装的”make”。对于每个示例,将内容放入一个名为Makefile的文件中,在该目录下运行命令make。让我们从最简单的Makefile开始:
1 | hello: |
注意:Makefile 中的缩进必须使用制表符而不是空格,否则 make 命令会失败。
这是运行上述例子的结果
1 | $ make |
Makefile 语法
一个Makefile包含一组规则。一般规则看起来像这样:
1 | targets【目标】: prerequisites【先决条件】 |
targets目标是文件名,用空格分隔。通常,每条规则只有一个目标。
command命令是一系列步骤,通常用于生成目标文件。这些步骤需要以制表符开头,而不是空格。
prerequisites先决条件也是文件名,用空格分隔。这些文件需要在运行目标的命令之前存在。这些也被称为依赖项。
Make的实质
我们来分析一下Helloworld的例子
1 | hello: |
这里已经有很多内容需要理解。让我们逐步分解:
我们有一个名为hello的目标
这个目标有两个命令
这个目标没有先决条件
然后我们会运行make hello。只要hello文件不存在,命令就会运行。如果hello存在,就不会运行任何命令。
重要的是要意识到,我说的hello既是一个目标又是一个文件。这是因为这两者直接相关。通常情况下,当运行一个目标(也就是运行目标的命令)时,命令会创建一个与目标名称相同的文件。在这种情况下,hello目标不会创建hello文件。
在我们创建一个更典型的Makefile之前,让我们创建一个名为blah.c的文件,内容如下:
1 | // blah.c |
然后我们创建Makefile(文件名通常都为Makefile)
1 | blah: |
这次,尝试简单地运行 make 命令。由于没有向 make 命令提供目标作为参数,因此会运行第一个目标。在这种情况下,只有一个目标(blah)。第一次运行时,会创建 blah。第二次运行时,你会看到输出 make: ‘blah’ is up to date。这是因为 blah 文件已经存在。但是存在一个问题:如果我们修改 blah.c 然后运行 make,就不会重新编译任何东西。
我们通过添加一个先决条件来解决这个问题:
1 | blah: blah.c |
当我们再次运行 make 时,会发生以下一系列步骤:
选择第一个目标,因为第一个目标是默认目标
这个目标有一个先决条件,即 blah.c
Make 决定是否应该运行 blah 目标。只有在 blah 不存在,或者 blah.c 的修改时间比 blah 的新时才会运行
最后这一步至关重要,也是 make 的实质。其目的是判断自上次编译 blah 以来,blah 的先决条件是否发生了变化。也就是说,如果修改了 blah.c,运行 make 应该重新编译文件。反之,如果 blah.c 没有变化,就不应该重新编译。
为了实现这一点,它使用文件系统时间戳作为代理来确定是否发生了变化。这是一个合理的启发式方法,因为文件时间戳通常只会在文件被修改时改变。但重要的是要意识到,情况并非总是如此。例如,你可能修改了一个文件,然后将该文件的修改时间戳更改为旧的时间。如果这样做,Make 将错误地猜测文件没有发生变化,因此可以忽略。
哎呀,这真是一大段话。确保你理解了这一点。这是 Makefile 的关键,可能需要你花几分钟来正确理解。通过上面的例子进行实验,如果仍然感到困惑,可以观看上面的视频。
更多快速示例
以下的 Makefile 最终运行了所有三个目标。当你在终端中运行 make 时,它将按一系列步骤构建一个名为 blah 的程序:
Make 选择了目标 blah,因为第一个目标是默认目标
blah 需要 blah.o,make 于是寻找 blah.o 目标
blah.o 需要 blah.c,因此 make 寻找 blah.c 目标
blah.c 没有依赖,因此运行 echo 命令
然后运行 cc -c 命令,因为所有的 blah.o 依赖已经完成
接着运行顶层的 cc 命令,因为所有的 blah 依赖已经完成
至此,blah 是一个编译好的 C 程序
1 | blah: blah.o |
如果你删除 blah.c,所有三个目标都将重新运行。如果你编辑 blah.c(因此修改时间戳比 blah.o 要新),那么前两个目标将会运行。如果你运行 touch blah.o(从而修改时间戳比 blah 还要新),那么只有第一个目标会运行。如果啥也不改,那么这些目标都不会运行。试一下吧!
下一个示例并没有做什么新的,但它仍然是一个很好的附加例子。它始终会运行两个目标,因为 some_file 依赖于 other_file,而 other_file 从未被创建。【因为创建some_file目标的前提条件是other_file存在】
1 | some_file: other_file |
Make clean
clean通常被用作一个目标,用于删除其他目标的输出,但它并不是Make中的一个特殊单词。你可以在这个示例上运行 make 和 make clean 来创建和删除 some_file。
需要注意的是,clean 在这里有两个新的特点:
它是一个非首要(默认)的目标,也不是一个先决条件。这意味着它永远不会运行,除非你明确调用 make clean
它不是一个意图作为文件名的目标。如果你碰巧有一个名为 clean 的文件,这个目标就不会运行,这不是我们想要的。在本教程的后面部分中会介绍如何通过 .PHONY 来解决这个问题
1 | some_file: |
变量【Variables】
变量只能是字符串。通常你会想使用 :=,但=也可以。请参阅变量第二部分。
下面是一个使用变量的示例:
1 | files := file1 file2 |
单引号或双引号在Make中没有意义。它们只是赋给变量的字符。然而,在shell/bash中,引号是有用的,在诸如printf之类的命令中需要它们。在这个例子中,这两个命令的行为是相同的:
1 | a := one two # a is set to the string "one two" |
引用变量使用 ${}
or $()
1 | x := dude # :=表示变量立即赋值,即刻生效,相当于字符串常量,=是在使用变量的时候再赋值,变量值会改变。 |
Targets【目标】
The all target
要创建多个目标并且想让它们都运行吗?那就创建一个名为 all 的目标。因为这是列出的第一个规则,所以如果在没有指定目标的情况下调用 make,它将默认运行。
1 | all: one two three |
Multiple targets
当一个规则有多个目标时,命令将会对每个目标运行。$@ 是一个自动变量,包含了目标的名称。
$@就是一个迭代器,可以遍历目标数组中的每个目标
1 | all: f1.o f2.o |
自动变量和通配符【Automatic Variables and Wildcards】
* Wildcard通配符
在Make中,和%都被称为通配符,但它们表示完全不同的东西。在文件系统中搜索匹配的文件名。我建议您始终将其包装在通配符函数中,否则您可能会陷入下面描述的常见陷阱。
1 | # Print out file information about every .c file |
1 | make |
*
可以用在目标、先决条件中,或者用在通配符函数中。
注意:* 不能直接用在变量定义中
注意:当 * 没有匹配到任何文件时,它将保持原样(除非在通配符函数中运行)。
1 | thing_wrong := *.o # Don't do this! '*' will not get expanded |
执行结果
1 | make |
%通配符
百分号(%)非常有用,但由于它可用于多种情况,有时会显得有些令人困惑。
当在“匹配”模式中使用时,它会匹配字符串中的一个或多个字符。这种匹配被称为 stem。
当在“替换”模式中使用时,它会取代之前匹配的 stem,并替换字符串中相应的部分。
% 最常用于规则定义和一些特定函数中。
请查看以下章节,了解其用法示例:
- 静态模式规则
- 模式规则
- 字符串替换
- vpath 指令
自动变量
有许多自动变量,但通常只有少数几个会出现:
1 | hey: one two |
执行结果
1 | touch one |
高级规则【Fancy Rules】
隐式规则【Implicit Rules】
Make很喜欢用隐式的规则来编译C程序【称之为Make对C/C++独特的爱】。我个人不同意这个设计决定,我不建议使用它们,但它们经常被使用,因此了解它们很有用。下面是隐式规则的列表:
Compiling a C program:
n.o
is made automatically fromn.c
with a command of the form$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
Compiling a C++ program:
n.o
is made automatically fromn.cc
orn.cpp
with a command of the form$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
Linking a single object file:
n
is made automatically fromn.o
by running the command$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
编译一个程序:
n.o
文件会由n.c
文件自动生成,通过命令:$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
C++程序编译:
n.o
文件会由n.cc
或n.cpp
文件自动生成,通过命令:$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
链接单个目标文件:
n
文件会由n.o
或n.cpp
文件自动生成,通过命令:$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
隐式规则所使用的重要变量包括:
CC: 用于编译C程序的程序;默认为cc
CXX: 用于编译C++程序的程序;默认为g++
CFLAGS: 提供给C编译器的额外标志
CXXFLAGS: 提供给C++编译器的额外标志
CPPFLAGS: 提供给C预处理器的额外标志
LDFLAGS: 当编译器需要调用链接器时提供的额外标志
现在,让我们看看如何在不明确告诉Make如何进行编译的情况下构建一个C程序:
1 | CC = gcc # Flag for implicit rules |
静态模式规则【Static Pattern Rules】
静态模式规则是在Makefile中编写更少内容的另一种方式,我认为它们更实用,而且稍微不那么”神奇”。以下是它们的语法:
1 | targets...: target-pattern: prereq-patterns ... |
其本质是给定的目标通过目标模式(使用 % 通配符)进行匹配。匹配到的任何内容称为 stem。然后将此 stem 替换到前置模式中,以生成目标的先决条件。
一个典型的用例是将 .c 文件编译成 .o 文件。以下是手动方式:
1 | objects = foo.o bar.o all.o |
解析:
这个Makefile定义了一个变量objects,用于存储目标文件的列表。然后定义了一个名为all的目标,其依赖于objects中列出的所有目标文件。接下来,针对每个源文件(foo.c,bar.c,all.c),Makefile设置了编译每个文件的规则,即将每个.c文件编译为对应的.o文件。all.c文件使用了一个特殊的规则,用于生成一个简单的main函数。此外,还定义了一个通用的规则,指示当Makefile需要一个.c文件而该文件不存在时,通过touch命令创建一个空的.c文件。最后,定义了一个名为clean的目标,用于删除所有生成的.c文件和.o文件,以及生成的可执行文件all。
以下是更高效的方法,使用静态模式规则:根据静态模式生成目标文件
1 | objects = foo.o bar.o all.o |
$(objects): %.o: %.c这一行代码说明了如何将.c文件编译为.o文件。当定义了这样的规则后,对应的目标文件会根据文件名自动匹配并编译,无需显式地指定规则。
静态模式规则和过滤器【Static Pattern Rules and Filter】
虽然我稍后会介绍函数,但我会提前展示它们的用法。filter函数可用于静态模式规则,以匹配正确的文件。在本例中,我使用了.raw和.result这两个后缀。
1 | obj_files = foo.result bar.o lose.o |
模式规则
模式规则经常被使用,但也常常让人感到困惑。你可以将它们看作两种方式:
定义自己的隐含规则的一种方法
静态模式规则的一种更简单的形式
让我们先从一个示例开始:
1 | # Define a pattern rule that compiles every .c file into a .o file |
模式规则中的目标包含 ‘%’。这个 ‘%’ 匹配任何非空字符串,而其他字符则与它们自身匹配。在模式规则的先决条件中,“%”代表与目标中的“%”相匹配的相同 stem。
以下是另一个例子:
1 | # Define a pattern rule that has no pattern in the prerequisites. |
Double-Colon Rules “双冒号规则”
双冒号规则很少被使用,但允许为同一目标定义多个规则。如果使用单冒号,会打印警告,并且只有第二组命令会被执行。
1 | all: blah |
命令和执行 【Commands and execution】
命令回显/静音 【Command Echoing/Silencing 】
在命令前面加上@可以阻止其被打印出来
您也可以使用-s选项来运行make,在每行命令前面加上@
1 | all: |
Makefile中,使用@符号可以阻止该条命令本身被打印到屏幕上,但是命令的执行结果仍然会打印到屏幕上。
执行make:
1 | make |
执行
make
时使用了-s
选项,即make -s
,那么Makefile中的命令不会被打印到屏幕上,只会输出命令的执行结果。这个选项通常用于让输出更加清晰,只显示实际的构建信息,而不包括每个具体的命令。
命令执行【Command Execution】
每个命令在一个新的 shell 中运行(或至少产生了这样的效果)
1 | all: |
默认的Shell【Default Shell】
The default shell is /bin/sh
. You can change this by changing the variable SHELL:
1 | SHELL=/bin/bash |
1 | echo $BASH |
双美元符号
如果你想要字符串包含一个美元符号,你可以使用$$
. 这是在bash
或sh
中使用shell变量的方法。
请注意在接下来的例子中Makefile变量与Shell变量之间的差异。
1 | make_var = I am a make variable |
Error handling with -k
, -i
, and -
【错误处理】
在运行 make 时,使用 -k 选项可以在面对错误时继续执行。这对于希望一次看到 make 的所有错误非常有帮助。在命令之前添加 - 可以抑制该命令的错误。使用 -i 选项可以使这种特性对每个命令都生效。
Add -k when running make to continue running even in the face of errors. Helpful if you want to see all the errors of Make at once.
Add a - before a command to suppress the error
Add -i to make to have this happen for every command.
1 | one: |
注意:如果你使用 ctrl+c 终止 make,它会删除它刚创建的较新的目标。
Recursive use of make 【递归使用 make】
要递归调用一个 makefile,请使用特殊的 $(MAKE) 而不是 make,因为它将为你传递 make 标志,并且不会受到它们的影响。
1 | new_contents = "hello:\n\ttouch inside_file" |
Export, environments, and recursive make【导出、环境和递归 make】
当 Make 启动时,它会自动将所有在执行时设置的环境变量转换为 Make 变量。
1 | # Run this with "export shell_env_var='I am an environment variable'; make" |
export指令接受一个变量,并将其设置为所有配方中所有shell命令的环境变量:
1 | shell_env_var=Shell env var, created inside of Make |
export 导出的变量在子进程中是可见的,而在当前 shell 环境中是不可见的。
执行结果:
1 | make |
因此,当您在make命令内部运行make时,您可以使用export指令使其对子make命令可用。在这个例子中,cooly被导出,以便subdir中的makefile可以使用它。
1 | new_contents = "hello:\n\techo \$$(cooly)" |
$(MAKE)
就代表着执行make命令。在Makefile中,$(MAKE)
是一个特殊变量,用于在执行递归调用时引用 make 命令本身。使用$(MAKE)
而不是直接使用 make 的好处在于它可以传递 make 命令的参数和标志,使得递归调用的 make 命令能够继承当前 make 命令的设置,从而更加灵活和方便。
以下是一些常见的 Makefile 特殊变量:
- **$@**:表示规则中的目标文件名。
- $^:表示规则中所有的依赖文件列表,用空格分隔。
- **$<**:表示规则中的第一个依赖文件。
- **$?**:表示规则中所有比目标新的依赖文件列表。
- **$*、%**:表示通配符,匹配规则中 % 所代表的部分。
- **$(MAKE)**:表示 make 命令。
- **$(CURDIR)**:表示当前目录的路径。
- **$(wildcard pattern)**:展开匹配 pattern 的文件名。
- **$(subst from,to,text)**:将 text 中的 from 替换为 to。
- **$(patsubst pattern,replacement,text)**:根据 pattern 匹配 text 中的单词,将其替换为 replacement。
- **$(shell command)**:执行 shell 命令并返回结果。
- **$(foreach var,list,text)**:遍历 list 中的每个单词,并将其赋值给变量 var,然后展开 text。
- **$(if condition,then-part,else-part)**:如果 condition 成立,则展开 then-part;否则展开 else-part。
- **$(call variable,param,…)**:定义和调用一个函数。
- **$(addprefix prefix,names…)**:给 names 中的每个单词都加上前缀 prefix。
这些变量和函数在 Makefile 中经常被用来进行文件管理、条件判断、循环操作等。
执行结果:
1 | make |
你需要导出变量才能在shell中运行。
1 | one=this will only work locally |
输出结果:
1 | make |
.EXPORT_ALL_VARIABLES
会为你导出所有的变量。
1 | .EXPORT_ALL_VARIABLES: |
make的参数 【Arguments to make】
make有一系列很好用的选项。看一下–dry-run,–touch,–old-file。
你可以指定多个目标来运行make,比如 make clean run test 会依次运行clean目标,然后是run,最后是test。
Variables Pt. 2【变量】
Flavors and modification【变量的种类和修改】
变量有两种不同的类型:
递归(使用=)- 仅在命令被使用时查找变量,而不是在其定义时。【使用时赋值】
简单展开(使用:=)- 像普通的命令式编程一样,只有到目前为止定义的才会被展开。【直接赋值!!!】
There are two flavors of variables:
- recursive (use
=
) - only looks for the variables when the command is used, not when it’s defined. - simply expanded (use
:=
) - like normal imperative programming – only those defined so far get expanded
1 | # Recursive variable. This will print "later" below |
执行结果:
1 | make |
简单扩展(使用 :=)允许你向变量追加内容。递归定义会导致无限循环错误。
1 | one = hello |
?=
only sets variables if they have not yet been set
1 | one = hello |
输出结果:
1 | make |
行末的空格不会被去除,但行首的空格会被去除。要创建只含有一个空格的变量,使用 $(nullstring)。
1 | with_spaces = hello # with_spaces has many spaces after "hello" |
一个未定义的变量实际上是一个空字符串!
1 | all: |
Use +=
to append
1 | foo := start |
字符串替换是修改变量的一种常见而实用的方式。另外也可以查看文本函数和文件名函数。
命令行参数和覆盖 【Command line arguments and override】
你可以使用 “override” 来重写来自命令行的变量。在这里,我们使用 “make option_one=hi” 来运行 make
1 | # Overrides command line arguments |
输出结果:
1 | 加了override |
命令列表和定义 【List of commands and define】
define指令不是一个函数,尽管看起来可能像。我见过它的使用频率如此之低,所以我不会详细介绍,但它主要用于定义常规的操作步骤,而且与eval函数搭配使用效果良好。
define/endef 简单地创建一个变量,该变量被设置为一系列命令。需要注意的是,这与在命令之间使用分号有些不同,因为每个命令都会在单独的shell中执行,正如我们预期的那样。
1 | one = export blah="I was set!"; echo $$blah |
运行结果:
1 | make |
特定目标变量【Target-specific variables】
可以为特定的目标设置变量
1 | all: one = cool |
特定模式变量【Pattern-specific variables】
你可以为特定的目标模式设置变量
1 | %.c: one = cool |
Makefile的条件部分【# Conditional part of Makefiles】
Conditional if/else
1 | foo = ok |
检查变量是否为空【Check if a variable is empty】
1 | nullstring = |
检查变量是否已经定义【Check if a variable is defined】
ifdef不会展开变量引用;它只是检查某些东西是否被定义。
1 | bar = |
$(MAKEFLAGS)
$(MAKEFLAGS)
这个例子向你展示了如何使用findstring
和MAKEFLAGS
来测试make标志。使用make -i命令来运行这个例子,你将看到它打印出echo语句。
1 | all: |
Functions【函数】
首要的函数
函数主要用于文本处理。使用$(fn,arguments)
或${fn, arguments}
来调用函数。Make具有相当数量的内置函数。
Functions are mainly just for text processing. Call functions with $(fn, arguments)
or ${fn, arguments}
. Make has a decent amount of builtin functions.
1 | bar := ${subst not,totally, "I am not superman"} |
如果你想要替换空格或逗号,请使用变量。
1 | comma := , |
不要在第一个参数之后的参数中包含空格。那会被视为字符串的一部分。
1 | comma := , |
String Substitution【字符串替换】
$(patsubst pattern,replacement,text)
执行以下操作:
“在文本中查找与模式匹配的以空格分隔的单词,并用替换项替换它们。在这里,模式可能包含一个‘%’,它充当通配符,匹配单词内的任意数量的任何字符。如果替换项也包含‘%’,那么‘%’将被模式中匹配‘%’的文本所取代。只有模式和替换项中的第一个‘%’会被这样处理;任何后续的‘%’都不会改变。”(GNU 文档)
替换引用$(text:pattern=replacement)
是这个的简写。
还有另一个只替换后缀的简写:$(text:suffix=replacement)
。这里不使用%通配符。
注意:对于这个简写,不要添加额外的空格。它会被视为搜索或替换项。
1 | foo := a.o b.o l.a c.o |
在 Makefile 语法中,OBJECTS := $(SOURCES:.c=.o)
是一行用于变量赋值的语句,它使用了模式替换功能来从源文件列表(通常是 .c
后缀的 C 语言源文件)生成对应的目标文件列表(通常是 .o
后缀的对象文件)。
具体解释如下:
OBJECTS
是一个变量名,用于存储生成的对象文件列表。:=
是立即赋值操作符,它会在解析 Makefile 时立即计算并赋值给变量,而不是在每次引用该变量时都重新计算。$(SOURCES:.c=.o)
是模式替换表达式的应用,其中:$(SOURCES)
是一个已经定义的变量,通常包含一组.c
后缀的源文件。:.c=.o
是模式替换规则,指定将.c
后缀替换为.o
后缀。
因此,如果 SOURCES
包含了 file1.c file2.c
,那么 OBJECTS
将会被赋值为 file1.o file2.o
。这样,你就可以用 $(OBJECTS)
来引用这个新生成的对象文件列表,以便在后续的 Makefile 规则中使用它们。
例如,在编译 C 语言项目时,你可能会看到如下的规则:
1 | SOURCES := file1.c file2.c |
在这个例子中,%.o: %.c
是一个模式规则,用于指定如何从 .c
文件编译生成 .o
文件。all
和 program
是目标,program
依赖于 $(OBJECTS)
,即 file1.o
和 file2.o
。当执行 make
命令时,Make 会根据这些规则和依赖关系自动编译必要的文件来构建最终的程序。
The foreach function
foreach 函数的形式为:$(foreach var,list,text)
。它将一个由空格分隔的单词列表转换为另一个列表。var 被设定为列表中的每个单词,text 被依次扩展为每个单词。
以下是在每个单词后附加感叹号的示例:
1 | foo := who are you |
The if function【if 函数】
if 函数检查第一个参数是否非空。如果是,执行第二个参数;否则执行第三个参数。
1 | foo := $(if this-is-not-empty,then!,else!) |
The call function【回调函数】
Make supports creating basic functions. You “define” the function just by creating a variable, but use the parameters $(0)
, $(1)
, etc. You then call the function with the special call
builtin function. The syntax is $(call variable,param,param)
. $(0)
is the variable, while $(1)
, $(2)
, etc. are the params.
Make 支持创建基本函数。你只需通过创建一个变量来“定义”函数,但使用参数 $(0)
、$(1)
等。然后,通过特殊的 call 内置函数来调用这个函数。其语法为 $(callvariable,param,param)
。$(0)
是变量,而 $(1)
、$(2)
等是参数。
1 | sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3) |
The shell function
shell - 这将调用shell,但它会用空格替换换行符!
shell - This calls the shell, but it replaces newlines with spaces!
1 | all: |
origin函数
在Makefile中,origin函数用于检查一个变量是如何定义的。它的语法是$(origin variable),它返回变量的引用方式。
它返回的结果有以下可能:
- 如果变量是通过赋值语句定义的(比如
VARIABLE = value
),那么origin函数返回”file”。 - 如果变量是在命令行中设置的,比如
make VARIABLE=value
,那么origin函数返回”command line”。 - 如果变量是通过环境变量定义的,那么origin函数返回”environment”。
- 如果变量没有被定义,origin函数返回”undefined”。
这个函数在Makefile中通常用于检查变量的来源,以便根据不同情况采取相应的行动。
Other Features
Include Makefiles 【包含Makefiles】
The include directive tells make to read one or more other makefiles. It’s a line in the makefile that looks like this:
include 指令告诉 make 去读取一个或多个其他的 Makefile。在 Makefile 中,它是这样一行:
1 | include filenames... |
这在你使用类似 -M 的编译器标志时特别有用,该标志会基于源代码创建 Makefile。举个例子,如果一些 C 文件包含一个头文件,这个头文件将被添加到由 gcc 写入的 Makefile 中。我在《Makefile Cookbook》中有更多相关内容。
The vpath Directive【vpath指令】
使用vpath来指定某些先决条件存在的位置。格式为vpath <模式> <目录,以空格/冒号分隔> <模式> 可以包含%字符,用于匹配任意零个或多个字符。你也可以使用变量VPATH在全局范围内进行类似的操作。
Use vpath to specify where some set of prerequisites exist. The format is vpath <pattern> <directories, space/colon separated>
<pattern>
can have a %
, which matches any zero or more characters. You can also do this globallyish with the variable VPATH
1 | vpath %.h ../headers ../other-directory |
Multiline
The backslash (“”) character gives us the ability to use multiple lines when the commands are too long
反斜杠(“\”)字符使我们能够在命令过长时使用多行
1 | some_file: |
.phony【伪目标】
Adding .PHONY
to a target will prevent Make from confusing the phony target with a file name. In this example, if the file clean
is created, make clean will still be run. Technically, I should have used it in every example with all
or clean
, but I didn’t to keep the examples clean. Additionally, “phony” targets typically have names that are rarely file names, and in practice many people skip this.
在目标中添加 .PHONY 可以防止 Make 将虚拟目标与文件名混淆。在这个例子中,如果创建了文件 clean,运行 make clean 时仍然会执行对应的操作。严格来讲,在每个包含 all 或 clean 的示例中,我都应该使用它,但为了保持示例的简洁性,我没有这样做。此外,“虚拟”目标通常具有很少作为文件名的名称,在实践中,许多人会忽略这一点。
1 | some_file: |
.delete_on_error
The make tool will stop running a rule (and will propogate back to prerequisites) if a command returns a nonzero exit status.DELETE_ON_ERROR
will delete the target of a rule if the rule fails in this manner. This will happen for all targets, not just the one it is before like PHONY. It’s a good idea to always use this, even though make does not for historical reasons.
如果命令返回非零退出状态,make 工具将停止运行规则(并将向前传递至先决条件)。DELETE_ON_ERROR 将在规则以这种方式失败时删除目标。这将适用于所有目标,而不仅仅是像 PHONY 那样的单个目标。尽管由于历史原因 make 没有这样做,但始终使用这一功能是个好主意。
1 | .DELETE_ON_ERROR: |
Makefile 使用示例
通用模板
让我们来看一个非常实用的Make示例,非常适合中等规模的项目。
这个Makefile的精妙之处在于它会自动为你确定依赖关系。你所需做的就是把你的C/C++文件放在src/文件夹中。
1 | # Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/) |
include指令的使用方法
当使用 include
指令时,您可以将其他 Makefile 文件的内容包含到当前的 Makefile 中。这种方法非常有用,特别是当您希望将一个大型项目的构建过程分解为多个 Makefile 时。
假设您有以下项目结构:
1 | project/ |
现在让我们看看如何使用 include
指令,将这些文件包含到主 Makefile
中,以及它们各自的内容:
1. 主 Makefile(project/Makefile)
1 | # 包含常用变量和规则 |
2. 子系统1的 Makefile(project/sub_system1/Makefile)
1 | # 定义子系统1相关的变量 |
3. 子系统2的 Makefile(project/sub_system2/Makefile)
1 | # 定义子系统2相关的变量 |
4. 公共定义文件(project/common/common_defs.mk)
1 | # 定义常用变量和规则 |
在这个例子中,顶级的 Makefile
使用 include
指令引用了子系统1和子系统2目录下的各自的 Makefile
,同时也引用了公共的定义文件 common_defs.mk
。这样可以使得整个项目的构建过程更加清晰和模块化,并且允许在主 Makefile
中使用子系统和公共定义文件中定义的变量和规则。
补充芝士
在 Makefile 中,使用
-include
(或者等效的.include
)命令来引入文件的话,如果被包含的文件不存在,Make 不会报错,而是会继续执行后续的命令。这种行为与普通的
include
命令有所不同。普通的include
命令会在被包含的文件不存在时报错并停止 Make 的执行,而-include
命令会忽略文件不存在的情况,继续执行后续步骤。这种处理方式通常用于让 Makefile 在包含可选配置文件或模块时更加灵活,因为有时某些配置文件可能存在但并非必需,或者存在于特定情况下的文件。
日志打印调试
在Makefile中,info
函数通常用于打印消息或变量的值到标准输出。以下是一些常见的info
函数的用法示例:
输出固定消息:
1
$(info This is a message.)
输出变量的值:
1
2MY_VARIABLE := hello
$(info MY_VARIABLE is $(MY_VARIABLE))输出多个变量的值及消息:
1
2
3VARIABLE_1 := value1
VARIABLE_2 := value2
$(info Values: $(VARIABLE_1) and $(VARIABLE_2))输出带有换行符的消息:
1
$(info Line 1\nLine 2)
检测Makefile中某段代码是否执行:
1
$(info This line is running)$(eval DUMMY := $(shell echo Executing this line))
这些示例展示了如何使用info
函数在Makefile中输出消息、变量的值以及执行信息。这对于调试Makefile中的逻辑、变量传递等情况非常有用。
- 本文作者: CoderSong
- 本文链接: https://jack-song-gif.github.io/2024/04/11/学会Makefile/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!