Makefile 基础教程
本文内容来自对 isaacs/Makefile 的翻译、修改和补充。
Hello, 这是个 Makefile
的基础教程。
你将在这学到为什么 make
命令如此受欢迎。尽管它有着奇怪的语法,但实际上它的表达能力强而且高效,应对构建程序任务时它是个强有力的解决方案。
我们开始吧
你需要在含有 Makefile
文件的文件夹中使用 make
命令,也可以使用 make -f <makefile>
指定不同的文件名。
Makefile
是一堆规则(rules)的集合。每个规则表明了要做的事情,比如 grunt task
或者 npm package.json
之类的。
一条规则的语法如下:
|
|
目标(target)是必需的。前置条件(prerequisites)和命令(commands)都是可选,但两者之间至少存在一个。
下面是一个示例,用 make
命令试试看会发生什么:
|
|
一般来说,如果 make
没有指定目标,那么它会选择运行第一个目标,所以在这个情况下输入 make
和 make tutorial
是一样的结果。
默认情况下,命令在执行前命令本身会被先打印出来,这样你就能看到当前是什么命令在运行。虽然这背离了 “success should be silent” 的 UNIX 教条,但不这么做的话,你很难从构建日志中看出当前在执行的命令。
想要关掉这个默认输出的话,我们可以在每条命令前加上 @
符号。
每条命令都会分别调用 shell
,所以如果你在直接在上一个命令中设置了变量,这个变量将不会出现在下一个命令中。你可以试着输入 make var-lost
看下一个示例会发生什么:
|
|
你可能注意到了刚才的命令中我们用了两个 $
,这是因为每行 Makefile
都会被 make
转义后再传入 shell
。至于为什么转义 $
,下文会有解释~
我们可以通过在命令结尾加 \
来让两个命令同一个 shell
执行,试着输入 make var-kept
:
|
|
接下来让我们开始加入依赖吧。在这个示例中,我们将会依赖 source.txt 并创建一个新的文件 result.txt:
|
|
输入试试看 make result.txt
。Oops…我们出现了错误:
|
|
看起来问题来源于我们试图依赖 source.txt 创建 result.txt,但我们没有告诉 make
怎么去拿到 source.txt,而该文件也不在 make
运行的目录下(如果你没有事先创建的话~)。
我们可以加入一个目标作为依赖生成 source.txt:
|
|
输入 make result.txt
将会先创建 source.txt 然后复制出 result.txt,试着再一次执行 make result.txt
,你会发现什么都没发生。因为它的依赖 source.txt 没有发生改变,所以也就没有必要重新构建一次 result.txt
。
执行 touch source.txt
,并编辑它,你会发现 make result.txt
又会开始重新构建。
试想一下假如我们在一个项目中需要100个 .c 文件,编译成与之相对的 .o 文件,然后把这些 .o 链接到二进制文件中。(这个和将100个 .style 变成 .css,再将他们组合到一起成为 main.main.css)一条一条的创建规则简直就是一场灾难。
好在 make
可以简化这些。通过创建一条通用的规则匹配任何符合模式的文件。然后声明另外一种匹配模式来作转换。
自动变量(Automatic Variables)
我们可以使用一些特殊的语法来适配输入和输出文件。下面是一些自动变量(Automatic Variables),它们的值与当前规则有关:
$@
指代当前的目标,你可以把它当作shell
脚本中的$@
。@
就像是arguments
的首字母a
一样。当你输入make foo
,foo
就是参数。$<
指代第一个前置条件。你可以把<
当作shell
中的输入管道。就像head < foo.txt
把 foo.txt 的内容当作输入。$^
指代不止是指代第一个前置条件,而是全部的前置条件。你可以把它当作@^
来记忆,他们只是方向不一样(< 和 ^ 的区别~)。如果一个文件出于一些原因多次出现在前置条件中,在$^
中仍然只会显示一次。$?
指代比目标新的所有前置条件。$?
就像一个问题,”等一下,为什么你要这么干?什么文件发生了改动?“$$
指代字面意思上的$
符号,更多的美元符号等于更多的现金等于美元符号(有点拗口)$*
指代%
符号匹配到的部分。(下面会有示例具体说明)
你也可以使用特殊语法 $(@D)
和 $(@F)
分别指向 $@
的目录名和文件名。$(<D)
和 $(<F)
对于 $<
也是一样的意思。你可以把 D/F
这个技巧用在任何自动变量上。
还有一些其他的自动变量,不过那些变量大多你都用不到,先放在一边。
我们可以用这些变量,拿到我们想要的数据,比如目标和依赖条件的值:
|
|
虽然方便了许多,但比起一个一个的列出它们,我们可以用一点 shell 脚本生成他们,并放入变量中。
函数(Functions)
make
提供一些内置函数帮助我们完成更加复杂的任务,这边我们只列出几个,这也是本文例子中会用到或提及的:
shell
函数。 用来执行shell
命令wildcard
函数。 列出与模式相匹配的路径,像这样$(wildcard *.c)
patsubst
函数。 用于模式匹配的替换,语法为$(patsubst pattern,replacement,text)
同时,通常情况下你应该使用 $(wildcard src/*.txt)
因为一般来说项目中已经存在这些文件了。不过这是一个教程,而我们想用它来生成文件。
这将会用 shell
生成一个文件名列表:
|
|
那么我们怎么在 src 文件夹中创建这些文件呢?
我们可以使用 %
占位符来表示"所有文件路径为 src/*.txt 格式的文件名"然后将这些匹配到的文件名会被放入 $*
变量中。
|
|
试着运行 make src/00.txt
和 make src/01.txt
看看结果。文件被创建出来了。
不过为了不每个文件都 make
一次,我们应该定义一个"伪(phony)“目标依赖所有我们需要创建的源文件。(在使用伪目标时,或者依赖它时,make
就不会检查这个目标文件是否真正存在。毕竟如果目录下有个文件和目标相同的话,make
会认为没有必要重新构建,导致不会执行命令。)
这时我们运行 make source
会在 src/ 下创建所有的文件。首先它会把 srcfiles 变量中的路径当作前置条件,接着 src/%.txt
目标会与之匹配。于是便会执行匹配到的目标,创建 src/ 文件夹,并输出匹配的字段到文件中。试着运行 make source
看看结果把:
|
|
源文件有了,现在是时候创建结果文件了,当然,我们得先创建结果文件夹。可能你想匹配所有的源文件,然后用它来创建结果文件:
|
|
非常好,但这需要我们执行 make dest/#.txt
100次!唔。。我猜应该没人会想这么干。
我们还需要再补充点东西。 看起来我们应该创建一个伪目标依赖所有的结果文件,就像依赖所以的目标文件一样。
这一次我们可以使用内置的 patsubst
函数,它会把所有的源文件路径替换结果标文件路径,这样我们可以不用重建一个结果文件列表(就像 srcfiles 一样)。
|
|
既然 destination 不是一个真正的文件名,我们应该把它定义为伪目标。使用.PHONY
声明伪目标是一个好习惯。
现在,让我们把这些结果文件名合在一起"编译"把,使用 cat
命令来演示这个效果:
|
|
执行 make kitty
看看会发生什么。
每个结果文件都被创建了出来,而且 kitty 文明中出现了每个结果文件的名字。如果再运行 make kitty
,它会说 “kitty is up to date”。
如果你像这样 touch src/25.txt; make kitty
改动了某个源文件,然后执行 make kitty
,神奇的事情出现了!
你会发现 make
很聪明,它只会对更改过的源文件 25.txt 产生反应,重建与之对应的结果文件,并重新"编译"到 kitty 中。它不会每次都重写生成源文件,再重新生成结果文件。
在 Makefile
中写一个 test 目标是一个好习惯,因为大家会参与你的项目中,如果你的项目里有 Makefile
的话,他们会希望能用 make test
做一些事情。
当然没有 kitty 的话是不能跑测试的,所以我们需要依赖它:
|
|
最后,make clean
需要总是能移除你用 Makefile
创建出来的东西,这样我们就可以移除一些过期的"坏东西”。
|
|
如果出错的话会发生什么事情呢?打个比方你在构建东西,然后一条命令失败了,那么 make
会终止并且拒绝执行接下来的命令,返回一个非零的错误码。
为了示范这个操作,试着执行下面这段"坏猫猫"规则,它会退出且返回错误码为1。
|
|
有一些补充
make kitty
我把上面提到的一些示例整合到了一起,你可以直接试试 make kitty
~
|
|
赋值符
变量可以指向另外一个变量。
|
|
Makefile
提供四种赋值符,区别如下:
- VARIABLE = value 懒惰赋值。在执行的时候递归的获取值。也是赋值默认的行为。
- VARIABLE := value 立即赋值。定义变量时就赋好值,不会随着 value 的变更而发生变更
- VARIABLE ?= value 为空赋值。如果 VARIABLE 为空,赋值。
- VARIABLE += value 追加赋值。将值追加到变量的末尾。
下面是个示例展示他们的区别:
|
|
make echo
结果:
|
|
环境变量
你可以在 Makefile
中直接引入环境变量,也可以定义新的全局环境变量:
|
|
这个示例会创建一个全局的环境变量 FOO ,可以在任何命令中使用。不过由于每个命令都是新的 shell
,所以在命令中更改这些环境变量,其他的命令中是看不到更改效果的。
示例中引用的环境变量 PATH 如果不存在于调用 make
的 shell
中,也可以通过 make PATH="bar" env
来指明 PATH 的值。
小坑
Makefile
对 tab 很执着,如果缩进的时候用了空格会报错的哦
|
|
你可以用 cat -e -t -v Makefile
来检查用了空格还是 tab。
|
|
命令前面有 ^I 的就是 tab,没有的就是空格。
还有一件事
想必你已经大致的了解到了 Makefile
,以及它能干什么。想进一步了解 Makefile
?可以去看看它的官方手册GNU make~