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程序的程序;默认为ccCXX:用于编译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/) |