[toc]
本文是看这个教程的翻译。另外再推荐看一看这篇文章,《跟我一起学Make》。
背景
我制作这个指南的原因是我永远无法完全掌握Makefile文件。他们似乎充斥着潜规则和深奥的符号,并且问简单的问题不会得到简单的答案。为了解决这个问题,我坐下来过了几个周末,阅读了所有关于Makefile文件的东西。我已经把最关键的知识浓缩到这本指南里了。每个主题都有一个简短的描述和一个独立的例子,你可以自己运行。
如果你对Make差不多都理解,那可以考虑看看Makefile Cookbook小节。它包含一个适用于中型项目的模板,里面有大量关于Makefile每个部分作用的注释。
祝你好运,我希望你能够杀死混乱的Makefiles世界!
开始
Makefile文件存在的意义
Makefile文件有助于大型程序的哪些部分需要重新编译。在绝大多数情况下,C或C++文件是需要重编译的。其他语言通常都有自己的与Make类似的工具。Make也可以在程序之外使用,当您需要基于文件变化来执行一系列命令的时候。本教程将关注C/C++编译的场景。
下图是一张依赖关系示意图,可以用make来解决其编译关系。如果任何文件的依赖关系发生变化,那么该文件将被重新编译:

和Make类似的软件
流行的C/C++替代构建系统有SCons、CMake、Bazel和Ninja。像微软Visual Studio这样的一些代码编辑器有自己的内置构建工具。对于Java语言,有Ant、Maven和Gradle。像Go和Rust这样的其他语言都有自己的构建工具。
像Python、Ruby和Javascript这样的解释语言不需要Make这样的工具。Makefiles的目标是根据哪些文件发生了变化来构建需要构建的文件,无论这个文件是什么。但对于解释语言的文件,其改变后不需要重新构建,只需要程序运行时使用文件的最新版本即可。
Make的版本和类型
Make有各种各样的实现,本指南的大部分内容对任何一个您可能使用的版本都是适用的。但还是需要强调,本文是针对GNU Make写的,GNU Make是Linux和MacOS上的标准Make实现。所有的例子都适用于Make 3和4版本,这两个版本除了一些细微的差异之外,其它特性几本等价。
运行例子
要运行这些示例,您需要安装一个终端和“make”。对于每个示例,将内容放在一个名为Makefile的文件中,并在该目录中运行make命令。让我们从最简单的Makefiles开始:
1 | hello: |
下面是运行上面例子的输出内容:
1 | make |
就是如此!如果您有哪里没看懂,这里有一个视频可以帮助您。其主要介绍了一些操作步骤以及Makefiles的基本结构。
Makefile语法
Makefile由一组规则组成。一个规则通常是这样的:
1 | targets: prerequisites |
- targets由文件名组成,多个文件名用空格分隔。通常,每个规则只有一个target。
- command通常是用于制作目标的一系列步骤。这些需要以tab开始,而不能是空格。
- prerequisites同样由文件名组成,多个文件名用空格分隔。在运行command之前,列表中的文件需要存在。这些文件也被称为依赖项
新手示例
下面的Makefile有三个独立的规则。当您在终端中运行make blah时,它将通过一系列步骤构建一个名为blah的程序:
- blah是第一个交给Make的target,所以Make将首先处理该target
blah
依赖blah.o
,所以make转而处理blah.o
这个targetblah.o
依赖blah.c
,所以make转而处理blah.c
这个targetblah.c
没有依赖项,所以Make执行echo命令- 因为
blah.o
的依赖项都处理完了,所以紧接着执行cc -c
命令 - 因为
blah
的依赖项目都处理完了,所以紧接着执行cc
命令 - 处理接触,最终生成编译好的
blah
文件
1 | blah: blah.o |
下面这个makefile只有一个目标,叫做some_file
。Makefile的第一个target就是项目的默认target,因此在这种情况下,将运行some_taget
对应的命令。
1 | some_file: |
下面这个makefile第一次运行会生成some_file
文件。第二次运行的时候,make会注意到some_file
已经生成了,然后会打印”make: 'some_file' is up to date.”
1 | some_file: |
下面这个makefile,目标some_file
依赖于other_file
。当我们运行make时,默认target(也就是some_file
,因为它是第一个的target)将被解析。make将首先查看some_file
依赖项列表,如果其中任何一个较旧,它将首先处理这些依赖项,最后处理some_file
自身。第二次运行时,两个目标都不会运行,因为两个目标都存在。
1 | some_file: other_file |
下面这个makefile总是将两个target都解析,因为some_file
依赖一个永远不会存在的文件。
1 | some_file: other_file |
clean
总以作为一个移除其他target所创文件的target,但实际上它不并是make中的关键字。
1 | some_file: |
变量
make对变量的处理过程,类似于C/C++中预处理对宏的处理过程,也就是值替换。变量只能是字符串,下面是一个关于变量使用的例子:
1 | files = file1 file2 |
可以通过${}
或者$()
引用变量
1 | x = dude |
目标
多target构建
如果你想同时make多个target,那么可以使用伪target。伪target可以依赖其他目标,伪目标可以作为默认目标。
1 | all: one two three |
多个target
在一个规则中,如果存在多个target,那么make将为每个target独立运行规则中的命令。$@
是个自动变量,其内容为每次命令独立运过程中的target名称。
1 | all: f1.o f2.o |
自动变量和wildcard
*通配符
***和%在Make中都被称为通配符,但它们的含义完全不同。***用于在文件系统中搜索匹配的文件名。我建议您总是wildcard
函数封装它,否则您可能会陷入下面所描述的常见陷阱之中。
1 | # Print out file information about every .c file |
*
可能会用在target
、prerequisites
以及wildcard
函数中注意:
*
不能直接用于定义变量注意:当
*
没有匹配到任何一个文件的时候,它就作为*
符号本身留在当前位置(除非将其用wildcard
函数封装起来)
1 | thing_wrong := *.o # Don't do this! '*' will not get expanded |
%通配符
%
符号是真的好用,但它也会有一些让人疑惑的地方,因为其可用于各种不同的场景:
- 当用在“matching”模式的时候,它匹配字符串中一个或者多个字符。
- 当用在”replacing”模式的时候,它会被替换为pattern中匹配到的子串。
%
符号最常见的用法还是在一些特定函数中做规则定义。
请参阅以下部分,了解使用了%
的示例:
自动变量
Make中有许多的自动变量,但常用的自动变量只占一小部分:
1 | hey: one two |
上面的例子运行后打印如下:
1 | [root@localhost demo]# make |
花哨的规则
隐式规则
make热爱编译C语言,但每次它表达爱意的时候,事情就会变得混乱。也许Make最令人困惑的部分是所制定的魔法/自动规则,Make称其为隐式规则。我个人不赞同这个设计决策,我不建议使用它们。但是它们太常见了,因此学习它们还是有必要的。这里有一个隐式规则的列表:
- 编译C程序:
n.o
文件会自动的用命令$(CC) -c $(CPPFLAGS) $(CFLAGS)
从n.c
文件编译出来,不需要手动的在规则中写入这条命令。 - 编译C++程序:
n.o
文件会自动的用命令$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)
从n.cc
文件或者n.cpp
文件编译出来,不需要手动的在规则中写入这条命令。 - 链接一个独立的目标文件:
n
会自动的通过命令$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)
从n.o
创建出来。
隐式规则使用的重要变量有:
CC
: Program for compiling C programs; defaultcc
CXX
: Program for compiling C++ programs; defaultg++
CFLAGS
: Extra flags to give to the C compilerCXXFLAGS
: Extra flags to give to the C++ compilerCPPFLAGS
: Extra flags to give to the C preprocessorLDFLAGS
: Extra flags to give to compilers when they are supposed to invoke the linker
让我们看看我们现在如何在不明确告诉Make如何编译的情况下构建一个C程序:
1 | CC = gcc # Flag for implicit rules |
静态模式规则
静态模式规则相对隐式规则而言,是用得比较少的。但我认为它更有用,也不那么“奇怪”。以下是它的语法:
1 | targets ...: target-pattern: prereq-patterns ... |
其本质就是要让target-pattern
(通过一个 %
通配符) 去匹配target
中的元素。target
中被匹配到的任何东西,称为stem
。然后stem
就被替换进prereq-pattern
里面,构成target
的依赖项。
一个典型的使用场景就是将.c文件编译为.o文件。下面手册中举的例子:
1 | objects = foo.o bar.o all.o |
下面是上方代码更有效率的版本,通过静态模式规则实现:
1 |
|
静态模式规则以及过滤器
我稍后会向您介绍函数,现在我要提前向您展示,您可以如何使用它们。过滤器功能可用于静态模式规则,以匹配正确的文件。在这个例子中,我编写了.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 |
模式规则的target
包含一个%
符号,这个%
符号匹配任意的非空字符串。target
中的其它符号保持原样匹配。模式规则先决条件中的%
符号代表与target
中%
所匹配到的同一词干。
下面是另外一个例子:
1 | # Define a pattern rule that has no pattern in the prerequisites. |
双冒号规则
双冒号规则很少使用,其允许为同一个target
定义多个规则。如果同名target
的重复规则后面是单个冒号,make会打印一条警告,并且只运行第二条规则。
1 | all: blah |
命令以及执行
命令回显/静音
你可以命令前添加一个@
符号来阻止命令打印,也可以在运行make时添加-s
参数,这相当于在每行都添加一个@
符号
1 | all: |
命令执行
每条命令都是在一个新的shell里面执行的(或者说运行效果就是这样的)
1 | all: |
默认shell
默认的shell是/bin/sh
。你可以通过修改SHELL
环境变量来修改默认shell
1 | SHELL=/bin/bash |
利用-k
,-i
以及-
进行错误处理
-k(或者--keep-going)
参数是make的一个参数。如果添加了-k
参数,那么运行的时候即使遇到错误,也继续运行。如果你想一次看到Make的所有错误,这个参数就很有用。-
是写在Makefile文件的command前面的。其含义是告知make,不管该command出不出错都认为是成功的。如果要让这个条件全局应用,可以使用make参数-i(或者--ignore-errors)
1 | one: |
打断或者杀死make
注意:如果对make使用ctrl+c,它将删除刚刚创建的较新目标。
Make的递归使用
要递归调用makefile,请使用特殊的$(make)
而不是make
,区别在于对待各种flag的方式。
1 | new_contents = "hello:\n\ttouch inside_file" |
递归make场景下使用export
export
指令接受一个变量并使该变量可被下级的各个make
访问。在下面这个例子中,cooly
被export
指令处理,这样下级目录中的makefile就可以使用它。 注意:export的语法与sh相同,但它们并不相关(虽然功能相似)
1 | ##################################### |
如果你希望变量在shell中有效,你也需要export它们。
1 | one=this will only work locally |
.EXPORT_ALL_VARIABLES
可以为你export所有的变量。
1 | .EXPORT_ALL_VARIABLES: |
make的参数
这里有一个很好的make可以支持的选项列表,建议查看 --dry-run
, --touch
, --old-file
。
你可以有一次构建多个target
。举例来说:make clean run test
会先处理clean
这个target
,然后goal
,然后 run
,然后处理test
。
变量,第二部分
风格和编辑
有两种类型的变量:
- 递归型(用
=
符号赋值)——只在使用变量时对变量做解析,而不是在定义时解析。 - 简单展开型(用
:=
赋值)——就像普通的命令式编程一样,在赋值那一刻为止,定义过的变量才会被扩展。
1 | # Recursive variable. This will print "later" below |
简单展开型(用:=
赋值)允许你追加一个变量的内容。如果是递归类型,则会出现一个无限循环错误。
1 | one = hello |
?=
只在变量没有被定义过的时候,才会对变量赋值
1 | one = hello |
变量赋值是,行尾的空格不会被删除,但行首的空格会被删除。要创建一个只有一个空格的变量,请使用$(nullstring)
1 | with_spaces = hello # with_spaces has many spaces after "hello" |
一个未定义的变量事实上是一个空字符串(就是什么也没有)!
1 | all: |
可以使用+=
追加变量的值
1 | foo := start |
字符串替换函数 也是一种编辑变量值的方式,可以查看[文本函数]](https://www.gnu.org/software/make/manual/html_node/Text-Functions.html#Text-Functions) 和文件名函数小节。
命令行参数以及override
您可以使用override
覆盖来自命令行的变量。下面这个例子,我们给make一个预定义变量option_one= hi
1 | # Overrides command line arguments |
命令列表和define
“define”实际上只是一个命令列表,和函数无关。请注意,这与用分号分割的一组命令有点不同。因为正如预期的那样,“define”中的每个命令都在一个单独的shell中运行。
1 | one = export blah="I was set!"; echo $$blah |
与特定Target有关的变量
变量可以被指定给特定的target
1 | all: one = cool |
与特定Pattern有关的变量
变量可以被指定给特定的pattern
1 | %.c: one = cool |
条件分支
if/else条件
1 | foo = ok |
检查变量是否为空
1 | nullstring = |
检查变量是否定义过
ifdef
不展开变量引用,它只是看看某个东西是否被定义了
1 | # bar的赋值语句后面啥也没有,就理解为变量没有定义。注意,赋值右边内容的开头空格会被删掉 |
$(makeflags)
这个例子展示了如何用findstring
和MAKEFLAGS
测试make的各种flag
。用make -i
运行这个例子,看它打印出echo语句。
1 | bar = |
函数
首次函数
函数主要用于文本处理,可以通过 $(fn, arguments)
或者 ${fn, arguments}
调用函数。你可以自行利用Make的call
內建函数。Make有相当多的內建函数。
1 | bar := ${subst not, totally, "I am not superman"} |
如果你想在使用变量时,替换其中的空格或者逗号
1 | comma := , |
调用函数时,不要在首个空格之后再引入其它空格,否则该空格将被视为字符串的一部分
1 | comma := , |
字符串替换
$(patsubst pattern,replacement,text)
语句做以下事情:
“Finds whitespace-separated words in text that match pattern and replaces them with replacement. Here pattern may contain a ‘%’ which acts as a wildcard, matching any number of any characters within a word. If replacement also contains a ‘%’, the ‘%’ is replaced by the text that matched the ‘%’ in pattern. Only the first ‘%’ in the pattern and replacement is treated this way; any subsequent ‘%’ is unchanged.” (来自GNU文档)
替换引用$(text:pattern=replacement)
是上面这个句型的简写版本。
还有另外一种简写版本,其只替换后缀$(text:suffix=replacement)
。注意这里没有使用%
通配符。
注意:不要在简写版本中添加额外的空格,其会被当做入参的一部分。
1 | foo := a.o b.o l.a c.o |
foreach函数
foreach函数看起来就像这样 $(foreach var,list,text)
。它会将一组以空格分隔的词汇转成另外一组。var
会被设置成每次迭代流程中的那个变量,而text
会为每个变量都展开一次。
下面这个例子会在每个单词后面追加一个感叹号:
1 | foo := who are you |
if函数
if
检查首个参数是否为非空,如果是,则执行第二个参数,否则执行第三个参数。
1 | foo := $(if this-is-not-empty,then!,else!) |
call函数
make支持创建基本的函数。你可以通过创建一个变量来“定义” 一个函数,但是要使用参数$(0)
, $(1)
等等。你之后就可以使用call
函数调用这个函数了。其语法是$(call variable,param,param)
。 $(0)
是define
的变量值,而$(1)
和`$(2)就是参数。
1 | sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3) |
shell函数
shell函数会调用系统的shell,但是它会用空格替代参数中的换行
1 | all: |
其他特性
引入其他makefile
include
指令告知make,取读取一个或多个其它的makefile文件。其用法示例如下,就是makefile文件中的一句话:
1 | include filenames... |
你可以使用编译器的-M
参数,让其自动的基于源码为你生成makefile。举例来说,如果C文件包含了一个头文件,那么该文件名将被追加到gcc创建的Makefile文件中。我会在 Makefile Cookbook中更多的讨论这一点。
The vpath Directive
使用vpath指定某组先决条件存在的位置。其格式为 vpath <pattern> <directories, space/colon separated>
,<pattern>
可以包含一个%
符号,其匹配0个或者或更多的字符。
你也可以使用VPATH
来全局的应用这个规则。
1 | vpath %.h ../headers ../other-directory |
Multiline
反斜杠”\“可以连接分行写的单条句子
1 | some_file: |
.PHONY
添加.PHONY
可以防止伪target
名称和文件名混淆。在下面这个例子中,如果一个名为clean的文件存在了,make clean
同样会运行。.PHONY
非常好用,但为了简单起见,在其他的示例中我都省略了它。
1 | some_file: |
.DELETE_ON_ERROR
如果某个命令返回非零的退出状态,make工具将停止该规则的运行。如果添加DELETE_ON_ERROR
标签,任何一个发生类似错误的规则,make都会删除该规则对生成的目标文件。建议总是添加这个标签,尽管出于历史原因,make不这样做。
1 | .DELETE_ON_ERROR: |
Makefile Cookbook
Makefile加工厨房
让我们来看一个非常有趣的Make例子,它非常适合中型项目。
这个makefile的好处是它会自动为您确定依赖关系。你所要做的就是把你的C/C++文件放到src/文件夹里。
1 | # Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/) |