Makefile 入门
在 Linux( Unix )环境下使用 GNU 的 make
工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为 Makefile
文件的编写。
所要完成的 Makefile
文件描述了整个工程的编译、连接等规则。其中包括:
- 工程中的哪些源文件需要编译以及如何编译
- 需要创建哪些库文件以及如何创建这些库文件
- 如何最后产生我们想要的可执行文件。
- …
尽管看起来可能是很复杂的事情,但是为工程编写 Makefile
的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile
。编译整个工程你所要做的事就是在 shell
提示符下输入 make
命令。整个工程完全自动编译,极大提高了效率。
make 是一个命令工具,它解释 Makefile
中的指令。在 Makefile
文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile
有自己的书写格式、关键字、函数。像 C 语言有自己的格式、关键字和函数一样。而且在 Makefile
中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile
在绝大多数的 IDE 开发环境中都在使用,已经成为一种工程的编译方法。
Makefile 基本规则
一个 Makefile
的基本语法结构如下:
1 | target ... : prerequisites ... |
其中:
target
可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。prerequisites
: 生成该 target 所依赖的文件command
: 该target要执行的命令(任意的shell命令)prerequisites
和command
都是可选的, 但是二者至少存在一个.- 没有
command
的规则也叫伪目标
简单地说,上面这个语句规定了一个 target
目标的生成方式. 首先需要满足 prerequisites
, 也就是说需要先完成 prerequisites
的内容, 然后执行 command
指令实现 target
的生成.
另一方面, 这个语句也表明了一种依赖关系, 也就是说:
1 | prerequisites 中如果有一个以上的文件比 target 文件要新的话,command 所定义的命令就会被执行。 |
而整个的 Makefile
就是这样的一条条规则语句组成的文件. 下面是一个简单的例子.
1 | # 使用 `#` 进行注释 |
之后, 在命令行执行:
1 | make # 等同于 make a.txt |
如果 make
命令运行时没有指定目标,默认会执行 Makefile 文件的第一个目标. 也就是 a.txt
, 之后由于 a.txt
需要 b.txt
和 c.txt
, make
命令会继续查找相应的命令生成这两个文件, 最后将其一共复制到 a.txt
中.
再执行:
1 | make clean |
便会执行清理操作, 删除所有的 txt
文件
target
一个目标(target
)就构成一条规则。目标通常是文件名,指明 Make
命令所要构建的对象,也可以是多个文件名,之间用空格分隔。除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target
)。
比如上面示例中的 clean
. 对于伪目标, 存在一种情况是, 如果当前目录下刚好存在一个名为 clean
的文件, 那么会导致 clean
这个规则不会被执行. 因为Make发现clean文件已经存在, 就认为没有必要重新构建了, 就不会执行指定的rm命令.
为了避免这种情况,可以明确声明 clean
是"伪目标",写法如下。
1 |
|
类似于 .PHONY
这样的内置目标名可以查看make 手册。
如果 Make
命令运行时没有指定目标,默认会执行 Makefile
文件的第一个目标。
prerequisites
前置条件通常也是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的时间戳比较新),"目标"就需要重新构建。
上面代码中,构建 a.txt 的前置条件是 b.txt
和 c.txt
。如果当前目录中,b.txt
和 c.txt
已经存在,那么make a.txt
可以正常运行,否则必须再写一条规则,来生成 b.txt
和 c.txt
。
如果一个目标后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件不存在,每次调用make b.txt
,它都会生成。
1 | make a.txt |
上面命令连续执行两次make a.txt
。第一次执行会先新建 b.txt
和 c.txt
,然后再新建 a.txt
。第二次执行,Make发现 b.txt
和 c.txt
没有变动(时间戳晚于 a.txt
),就不会执行任何操作,a.txt
也不会重新生成。
如果需要生成多个文件,往往采用下面的写法。
1 | source: file1 file2 file3 |
上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。
1 | make source |
执行make source
命令后,就会一次性生成 file1,file2,file3 三个文件。这比下面的写法要方便很多。
1 | make file1 |
commands
命令(commands
)表示如何更新目标文件,由一行或多行的Shell命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。需要注意的是,每行命令在一个单独的shell中执行。这些 Shell 之间没有继承关系。比如:
1 | var-lost: |
上面代码执行后(make var-lost
),取不到foo的值。因为两行命令在两个不同的进程执行。解决办法有:
- 将两行命令写在一行,中间用分号分隔
- 在换行符前加反斜杠转义
- 加上
.ONESHELL:
命令
1 | var-kept: |
每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX
声明。比如使用大于号(>)替代tab键:
1 | .RECIPEPREFIX = > |
Makefile 语法
回声(echoing)
正常情况下,make会打印出每条将要执行的命令,然后再执行,这就叫做回声(echoing)。在命令的前面加上@,就可以关闭回声。
1 | test: |
执行上面的规则,会得到下面的结果, 只打印了没有关闭回声的指令。
1 | $ make test |
由于在构建过程中,需要了解当前在执行哪条命令,所以通常只在注释和纯显示的echo命令前面加上@。
通配符(wildcard)
通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*
)、问号(?
)和 [...]
。比如, *.o
表示所有后缀名为 o
的文件。
*
: 匹配0个或者是任意个字符?
: 匹配任意一个字符[...]
: 我们可以指定匹配的字符放在[]
中
通配符不仅可以使用在规则的命令中,还可以使用在规则中, 但是如果通配符使用在依赖的规则中的话一定要注意这个问题:不能通过引用变量的方式来使用, 如果想要引用变量, 需要使用一个函数 wildcard
, 看下面这个例子.
1 | # 正常 |
模式匹配
Makefile
中有一个和通配符 “*
” 相类似的字符,这个字符是 “%
”,也是匹配任意个字符,使用在我们的的规则当中。使用匹配符 %
,可以将大量同类型的文件,只用一条规则就完成构建。
1 | %.o: %.c |
等同于下面的写法。
1 | f1.o: f1.c |
“%.o
” 把我们需要的所有的 “.o
” 文件组合成为一个列表,从列表中挨个取出的每一个文件,“%
” 表示取出来单个文件的文件名(不包含后缀),然后找到文件中和 "%
"名称相同的 “.c
” 文件,然后执行下面的命令,直到列表中的文件全部被取出来为止。
变量和赋值符
Makefile 允许使用等号自定义变量。调用时,变量需要放在 $( )
之中。
1 | txt = Hello World |
调用 Shell 变量,需要在美元符号前,再加一个美元符号,这是因为 Make
命令会对美元符号转义。
1 | test: |
有时,变量的值可能指向另一个变量。
1 | v1 = $(v2) |
上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。
为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow。
1 | VARIABLE = value |
内置变量(Implicit Variables)
Make命令提供一系列内置变量,比如,$(CC)
指向当前使用的编译器,$(MAKE)
指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见Make 手册。
1 | output: |
自动变量(Automatic Variables)
Make命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。
$@
$@
指代当前目标,就是Make命令当前构建的那个目标。比如,make foo
的 $@
就指代foo。
$(@D)
和 $(@F)
分别指向 $@
的目录名和文件名。
比如,$@
是 src/input.c
,那么 $(@D)
的值为 src
,$(@F)
的值为 input.c
。
1 | a.txt b.txt: |
$<
$<
指代第一个前置条件。比如,规则为 t: p1 p2
,那么 $<
就指代 p1
。
$(<D)
和 $(<F)
分别指向 $<
的目录名和文件名。
1 | a.txt: b.txt c.txt |
$?
$?
指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2
,其中 p2
的时间戳比 t
新,$?
就指代 p2
。
$^
$^
指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2
,那么 $^
就指代 p1 p2
。
$*
$*
指代匹配符 %
匹配的部分, 比如 %
匹配 f1.txt
中的 f1
,$*
就表示 f1
。
所有的自动变量清单,请看Make 手册。下面是自动变量的一个例子。
1 | dest/%.txt: src/%.txt |
上面代码将 src 目录下的 txt 文件,拷贝到 dest 目录下。首先判断 dest 目录是否存在,如果不存在就新建,然后,$<
指代前置文件(src/%.txt), $@
指代目标文件(dest/%.txt)。
判断和循环
Makefile
使用 Bash
语法,完成判断和循环。
1 | ifeq ($(CC),gcc) |
上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。
1 | LIST = one two three |
上面代码的运行结果。
1 | one |
函数
Makefile 还可以使用函数,格式如下。
1 | $(function arguments) |
Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。
shell 函数
shell 函数用来执行 shell 命令
1 | srcfiles := $(shell echo src/{00..99}.txt) |
wildcard 函数
wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。
1 | srcfiles := $(wildcard src/*.txt) |
subst 函数
subst 函数用来文本替换,格式如下。
1 | $(subst from,to,text) |
下面的例子将字符串"feet on the street"替换成"fEEt on the strEEt"。
1 | $(subst ee,EE,feet on the street) |
下面是一个稍微复杂的例子。
1 | comma:= , |
patsubst函数
patsubst 函数用于模式匹配的替换,格式如下。
1 | $(patsubst pattern,replacement,text) |
下面的例子将文件名"x.c.c bar.c",替换成"x.c.o bar.o"。
1 | $(patsubst %.c,%.o,x.c.c bar.c) |
替换后缀名
替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则。它实际上patsubst函数的一种简写形式。
1 | min: $(OUTPUT:.js=.min.js) |
上面代码的意思是,将变量OUTPUT中的后缀名 .js 全部替换成 .min.js 。
Makefile 的实例
执行多个目标
1 |
|
上面代码可以调用不同目标,删除不同后缀名的文件,也可以调用一个目标(cleanall),删除所有指定类型的文件。
编译C语言项目
1 | edit : main.o kbd.o command.o display.o |
Makefile 入门