Makefile简易教程
下图是一个典型的依赖关系,Makefile
可以用来构建这样的依赖关系
最简单的一个实例,使用 Makefile
在终端输出 Hello World!
:
1 | hello: |
Makefile 语法
1 | targets: prerequisites |
targets
是目标文件,可以是object file
,也可以是可执行文件,还可以是一个标签prerequisites
是生成target
所需要的文件或者目标,可以有多个,中间用空格分开command
是make
执行的命令,可以有多行,每行必须以tab
开头
比如,我们可以创建下面这样一个C文件:
1 | // blah.c |
然后使用 Makefile
来编译这个文件:
1 | blah: blah.c |
第一次运行时,将会创建 blah
文件。第二次运行时,您将看到 make: 'blah' is up to date
的提示。这是因为 blah
文件已经存在。但是存在一个问题:如果我们修改了 blah.c
文件然后运行 make
,没有任何内容被重新编译。通过添加一个 prequisites
,我们可以解决这个问题:
1 | blah: blah.c blah.h |
Make
决定是否运行 blah
目标。只有在 blah
不存在,或者 blah.c
的时间戳较新时,它才会运行。在文件被修改保存后,时间戳一般会更新,即修改日期会变成最新的。
更复杂的例子:
1 | blah: blah.o |
blah
依赖于blah.o
,因此make
搜索blah.o
目标。blah.o
依赖于blah.c
,因此make
搜索blah.c
目标。- 然后运行
cc -c
命令,因为所有blah.o
的依赖项都已完成。 - 最后运行顶层的
cc
命令,因为所有的blah
依赖项都已完成。 - 这就是结果:
blah
是一个已编译的C程序。
Make clean
clean
通常被用作一个目标,用于删除其他目标的输出:
1 | some_file: |
变量
变量只能是字符串。通常你会想使用 :=
,但 =
也可以使用。
1 | files := file1 file2 |
Targets
多个 targets
1 | all: f1.o f2.o |
自动变量和通配符
自动变量
1 | hey: one two |
*
通配符
1 | thing_wrong := *.o # Don't do this! '*' will not get expanded |
%
通配符
- 当以“匹配”模式使用时,它匹配字符串中的一个或多个字符。这个匹配称为“词干”。
- 当以“替换”模式使用时,它取得匹配到的词干并替换字符串中的内容。
%
最常用于规则定义和某些特定函数中。
Fancy Rules
隐性规则
- 编译
C
程序:n.o
会自动从n.c
生成,使用的命令形式为$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
- 编译
C++
程序:n.o
会自动从n.cc
或n.cpp
生成,使用的命令形式为$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
- 链接单个目标文件:
n
会自动从n.o
生成,使用的命令形式为$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
隐式规则使用的重要变量包括:
CC
:用于编译C
程序的程序;默认为cc
CXX
:用于编译C++
程序的程序;默认为g++
CFLAGS
:给C
编译器的额外标志CXXFLAGS
:给C++
编译器的额外标志CPPFLAGS
:给C
预处理器的额外标志LDFLAGS
:在需要调用链接器时给编译器的额外标志
1 | CC = gcc # Flag for implicit rules |
Static Pattern Rules
1 | targets...: target-pattern: prereq-patterns ... |
对于
1 | objects = foo.o bar.o all.o |
可以使用静态模式规则来简化:
1 | objects = foo.o bar.o all.o |
Static Pattern Rules and Filter
filter
可以在静态模式规则中用于匹配正确的文件。在这个例子中,我举例使用了 .raw 和 .result 这两个扩展名。
1 | obj_files = foo.result bar.o lose.o |
Pattern Rules
- 定义自己的隐式规则的方法
- 一种更简单的静态模式规则形式在模式规则的前提条件中,符号
1
2
3# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@%
代表与目标中的%
匹配的相同部分。1
2
3
4# Define a pattern rule that has no pattern in the prerequisites.
# This just creates empty .c files when needed.
%.c:
touch $@
Double-Colon Rules
双冒号规则很少被使用,但它允许为同一目标定义多个规则。如果这些规则是单冒号,会打印一个警告,并且只有第二组命令会运行。
1 | all: blah |
Commands and execution
在命令前加上 @
符号可以阻止其打印输出。你也可以在运行 make
命令时使用 -s
选项选择静默模式,相当于在每行前加上@符号。
1 | all: |
每个命令等效于在一个新的shell中运行(或者至少效果是如此)。
1 | all: |
如果你想让一个字符串含有美元符号,你可以使用 $$
。这是在 bash
或 sh
中使用 shell
变量的方法。请注意下面的示例中 Makefile
变量和 Shell
变量之间的区别。
1 | make_var = I am a make variable |
- 在运行
make
时添加-k
参数,即使出现错误也可以继续运行。如果您想一次查看所有make
的错误信息,这将非常有帮助。 - 在命令前添加
-
可以抑制错误。 - 添加
-i
参数到make
命令中,使得每个命令都会发生这种情况。
1 | one: |
make
递归调用
要递归调用一个 Makefile
,请使用 $(MAKE)
而不是 make
,因为 $(MAKE)
将为您传递 make
标志,并且不会受其影响。
1 | new_contents = "hello:\n\ttouch inside_file" |
在
Makefile
中,&&
用于连接多个命令,并且只有前一个命令成功执行(返回退出码为0)时,才会执行下一个命令。这种方式可以用于确保依赖关系的正确执行顺序。
Export, environments, and recursive make
1 | # Run this with "export shell_env_var='I am an environment variable'; make" |
当使用 export
命令导出一个变量时,该变量将在 Makefile
中的后续命令中可见,并且还可以在通过 $(MAKE)
调用的子进程中使用。这样可以确保变量的值在整个构建过程中始终可用。
unexport cooly
将会撤销之前通过export cooly
导出的变量。
.EXPORT_ALL_VARIABLES
exports all variables for you.
1 | .EXPORT_ALL_VARIABLES: |
There’s a nice list of options that can be run from make.
Variables Pt. 2
- 递归(使用
=
)- 仅在命令被使用时才查找变量,而不是在其定义时查找。 - 简单扩展(使用
:=
)- 类似于常规的命令式编程 - 仅展开到目前为止定义的变量。1
2
3
4
5
6
7
8
9
10# Recursive variable. This will print "later" below
one = one ${later_variable}
# Simply expanded variable. This will not print "later" below
two := two ${later_variable}
later_variable = later
all:
echo $(one)
echo $(two)
使用 :=
进行简单扩展允许您向变量追加内容。递归定义会导致无限循环错误。
1 | one = hello |
?=
仅在变量尚未设置时设置变量
1 | one = hello |
+=
用于向变量追加内容。如果变量尚未设置,则等效于 :=
。
1 | one = hello |
行末的空格不会被去除,但是行首的空格会被去除。要创建一个只包含一个空格的变量,请使用 $(nullstring)
。
1 | with_spaces = hello # with_spaces has many spaces after "hello" |
您可以使用 override
来覆盖来自命令行的变量。在这里,我们使用了 make option_one=hi
命令运行了 make
。
1 | # Overrides command line arguments |
define/endef
简单地创建一个变量,该变量被设置为一系列命令。需要注意的是,这与在命令之间使用分号有些不同,因为每个命令都在单独的 shell
中运行
1 | one = export blah="I was set!"; echo $$blah |
变量可以针对特定 targets
进行设置
1 | all: one = cool |
你可以为特定的 targets
模式设置变量
1 | %.c: one = cool |
条件判断
if/else
1 | foo = ok |
1 | nullstring = |
ifdef
1 | bar = |
$(MAKEFLAGS)
这个示例向您展示了如何使用 findstring
和 MAKEFLAGS
来测试制作标志。使用 make -i
运行此示例,以查看它打印出 echo
语句。
1 | all: |
函数
函数主要用于文本处理。通过 $(fn, arguments)
或 ${fn, arguments}
来调用函数。Make
具有相当数量的内置函数 。
文本替换
1 | bar := ${subst not, totally, "I am not superman"} |
1 | comma := , |
patsubst
1 | foo := a.o b.o l.a c.o |
foreach
foreach
函数的使用方式如下:$(foreach var,list,text)
。它将一个由空格分隔的单词列表转换为另一个列表。var
被设置为列表中的每个单词,而 text
在每个单词中被扩展。以下是在每个单词后追加感叹号的示例:
1 | foo := who are you |
if
检查第一个参数是否非空
1 | foo := $(if this-is-not-empty,then!,else!) |
Make
支持创建基本函数。通过创建变量来定义函数,但使用 $(0)
,$(1)
等调用参数。然后,使用特殊的内建函数来调用函数。语法是 $(call 变量,参数,参数)
。$(0)
是变量(函数名),而 $(1)
,$(2)
等是参数
1 | sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3) |
其他特性
include
include
用于包含其他 Makefile
。如果 Makefile
不存在,make
将会继续执行。如果 Makefile
存在,make
将会执行它。
1 | include some_file |
vpath
使用 vpath
指定一组prerequisites存在的位置。格式为 vpath <pattern> <directories, space/colon separated>
。模式可以包含 %
,表示匹配任意零个或多个字符。您还可以在全局范围内使用变量 VPATH
来实现这一点。
1 | vpath %.h ../headers ../other-directory |
.PHONY
.PHONY
目标用于声明一些常见的操作,例如 clean
(清理生成的文件)、all
(构建所有目标)、install
(安装程序)等。通过将这些操作声明为伪目标,可以确保它们在每次运行 Make
时都会被正确执行,而不需要检查文件的更新状态
1 |
|
.DELETE_ON_ERROR
.DELETE_ON_ERROR
是一个特殊的 Makefile
目标,用于在出现错误时删除生成的目标文件
1 | .DELETE_ON_ERROR: |
如果在编译 main.o
时发生错误,Make
会立即停止编译,并删除已经生成的目标文件 main.o
和 util.o
。这样可以确保下次构建时从干净的状态开始,避免残留文件导致的问题。
Makefile Cookbook
让我们来看一个非常精彩的 Make
示例,适用于中等规模的项目。
这个 Makefile
的好处是它可以自动为您确定依赖关系。您只需要将您的 C/C++
文件放在 src/
文件夹中即可
1 | # Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/) |