前言

本文翻译自https://makefiletutorial.com/#makefile-cookbook

我之所以编写这本指南,是因为我始终无法理解 Makefile。它们似乎充斥着隐藏的规则和深奥的符号,问一些简单的问题却得不到简单的答案。为了解决这个问题,我花了几个周末坐下来,阅读了所有关于 Makefile 的资料。我把最关键的知识浓缩到了这本指南中。每个主题都有简要说明和一个自带的示例,你可以自己运行。

如果你基本了解 Make,可以考虑查看 Makefile Cookbook,其中有一个中等规模项目的模板,对 Makefile 的每个部分的作用都有大量注释。

祝你好运,我希望你能战胜 Makefile 这个令人困惑的世界!

一. Getting Started

1.1 为什么需要Makefile

Makefile 用于帮助决定大型程序的哪些部分需要重新编译。在绝大多数情况下,编译的是 C 或 C++ 文件。其他语言通常也有自己的工具,其作用与 Make 类似。除了编译之外,Make 还可以用于根据文件变化运行一系列指令。本教程将重点介绍 C/C++ 编译用例。

下面是一个依赖关系图的示例,您可以用 Make 构建这个依赖关系图。如果任何文件的依赖关系发生变化,该文件将被重新编译:

dependency_graph

1.2 除了Makefile还有什么其他选择

流行的 C/C++ 替代构建系统有 SCons、CMake、Bazel 和 Ninja。一些代码编辑器(如 Microsoft Visual Studio)也有自己的内置构建工具。Java 有 Ant、Maven 和 Gradle。其他语言(如 Go、Rust 和 TypeScript)也有自己的构建工具。

Python、Ruby 和原始 Javascript 等解释型语言不需要类似 Makefile 的工具。Makefile 的目标是根据已更改的文件编译需要编译的文件。但是,当解释型语言中的文件发生变化时,就不需要重新编译了。程序运行时,将使用文件的最新版本。

1.3 运行示例

要运行这些示例,你需要一个终端并安装 “make”。将每个示例的内容放入一个名为 Makefile 的文件中,然后在该目录下运行 make 命令。让我们从最简单的 Makefile 开始:

1
2
hello:
echo "Hello, World"

这里是运行后的输出:

1
2
3
zrf@debian:/tmp$ make
echo "Hello, World"
Hello, World

就是这样!如果你还有些困惑,这里有一段视频,详细介绍了这些步骤,以及 Makefile 的基本结构。

https://youtu.be/zeEMISsjO38

1.4 Makefile语法

Makefile 由一系列规则组成。规则一般是这样的

1
2
3
4
targets: prerequisites
command
command
command
  • targets是文件名,以空格分隔。通常情况下,每条规则只有一个目标。
  • command是用于创建targets的一系列步骤。这些命令需要以制表符开头,而不是空格。
  • prerequisites(先觉条件)也是文件名,以空格分隔。在运行目标的命令之前,这些文件必须存在。这些文件也称为依赖项

1.5 Make的本质

让我们以hello world作为示例:

1
2
3
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."

这里已经有很多东西需要了解。让我们来分解一下:

  • 我们有一个名为 hello 的目标
  • 这个目标有两个命令
  • 该目标没有先决条件

然后运行 make hello。只要 hello 文件不存在,命令就会运行。如果 hello 文件存在,则不会运行任何命令。

要知道,我所说的 hello 既是目标,也是文件。这是因为两者直接联系在一起。通常情况下,当运行目标文件时(也就是运行目标文件的命令时),命令会创建一个与目标文件同名的文件。在当前的例子中,hello 目标并不会创建 hello 文件。

让我们创建一个更典型的 Makefile–一个编译单个 C 文件的 Makefile。在此之前,先创建一个名为 blah.c 的文件,其内容如下:

1
2
// blah.c
int main() { return 0; }

然后创建一个Makefile

1
2
blah:
cc blah.c -o blah

这次,请尝试直接运行 make。由于 make 命令的参数中没有提供目标文件,因此会运行第一个目标文件。在本例中,只有一个目标(blah)。第一次运行时,blah 将被创建。第二次运行时,你会看到 make: ‘blah’ 是最新的。这是因为 blah 文件已经存在。但有一个问题:如果我们修改了 blah.c,然后运行 make,什么也不会重新编译。

为了解决修改源文件,目标文件不会重新编译的问题,我们为Makefile添加先觉条件:

1
2
blah: blah.c
cc blah.c -o blah

当我们再次运行make时,会执行这些步骤

  • 选择了第一个目标,因为第一个目标是默认目标
  • 它的前提条件是 blah.c
  • Make 决定是否运行 blah 目标。只有当 blah 不存在,或 blah.c 比 blah 新时,它才会运行

最后一步至关重要,是 make 的精髓所在。它要做的是判断自上次编译 blah 以来,blah 的先决条件是否发生了变化。也就是说,如果 blah.c 被修改了,运行 make 就应该重新编译该文件。反之,如果 blah.c 没有改变,则不应重新编译。

为了做到这一点,它会使用文件系统时间戳作为代理来判断是否有内容发生了更改。这是一种合理的启发式方法,因为文件时间戳通常只有在文件被修改时才会发生变化。但必须认识到,情况并非总是如此。例如,你可以修改一个文件,然后把该文件的修改时间戳改成旧的。如果你这样做了,Make 就会错误地认为该文件没有更改,从而忽略它。

请务必理解这一点。这是 Makefile 的关键所在,可能需要花几分钟时间才能正确理解。如果还是不明白,请参考上面的示例或观看上面的视频。

1.6 更多快速示例

下面的 Makefile 最终会运行所有三个目标。当你在终端运行 make 时,它会通过一系列步骤构建一个名为 blah 的程序:

  • make 选择目标 blah,因为第一个目标是默认目标
  • blah 需要 blah.o,所以 make 搜索 blah.o 目标
  • blah.o 需要 blah.c,所以 make 会搜索 blah.c 目标
  • blah.c 没有依赖关系,因此运行 echo 命令
  • 然后运行 cc -c 命令,因为所有 blah.o 的依赖关系都已完成
  • 运行 top cc 命令,因为所有 blah 的依赖关系都已完成
  • 就是这样:blah 是一个编译过的 c 程序
1
2
3
4
5
6
7
8
9
blah: blah.o
cc blah.o -o blah # Runs third

blah.o: blah.c
cc -c blah.c -o blah.o # Runs second

# Typically blah.c would already exist, but I want to limit any additional required files
blah.c:
echo "int main() { return 0; }" > blah.c # Runs first

如果删除 blah.c,所有三个目标都将重新运行。如果编辑它(从而将时间戳改为比 blah.o 新),则会运行前两个目标。如果运行 touch blah.o(从而将时间戳改为比 blah 更新),则只会运行第一个目标。如果什么都不改,则所有目标都不会运行。试试看

下一个示例没有做任何新内容,但仍然是一个很好的补充示例。它将始终运行两个目标,因为 some_file 依赖于 other_file,而 other_file 从未创建。

1
2
3
4
5
6
some_file: other_file
echo "This will always run, and runs second"
touch some_file

other_file:
echo "This will always run, and runs first"

1.7 Make clean

clean 经常被用作删除其他目标的输出的目标,但它在 Make 中并不是一个特殊的词。你可以运行 make 和 make clean 来创建和删除 some_file。

请注意,clean在这里做了两件新事情:

它不是第一目标(默认),也不是先决条件。这意味着除非明确调用 make clean,否则它永远不会运行。
它不是一个文件名。如果你碰巧有一个名为 clean 的文件,这个目标就不会运行,这不是我们想要的。如何解决这个问题,请参见本教程后面的 .PHONY 部分

1
2
3
4
5
some_file: 
touch some_file

clean:
rm -f some_file

1.8 变量

变量只能是字符串。通常要使用 :=,但 = 也可以。参见变量 Pt 2。

下面是一个使用变量的示例:

1
2
3
4
5
6
7
8
9
10
11
12
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file

file1:
touch file1
file2:
touch file2

clean:
rm -f file1 file2 some_file

单引号或双引号对 Make 没有任何意义。它们只是分配给变量的字符。不过,引号对 shell/bash 很有用,在 printf 等命令中需要用到它们。在本例中,两个命令的行为是一样的:

1
2
3
4
5
a := one two # a is set to the string "one two"
b := 'one two' # Not recommended. b is set to the string "'one two'"
all:
printf '$a'
printf $b

使用 ${} 或 $() 引用变量

1
2
3
4
5
6
7
8
x := dude

all:
echo $(x)
echo ${x}

# Bad practice, but works
echo $x

二. Target

2.1 all target

制作多个目标,并希望所有目标都运行?制作一个全部目标。由于这是列出的第一条规则,因此如果调用 make 而未指定目标,它将默认运行。

1
2
3
4
5
6
7
8
9
10
11
all: one two three

one:
touch one
two:
touch two
three:
touch three

clean:
rm -f one two three

2.2 多目标

当一条规则有多个目标时,将针对每个目标运行命令。$@ 是一个自动变量,包含目标名称。

1
2
3
4
5
6
7
8
9
all: f1.o f2.o

f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o

三. 自动变量和通配符

3.1 * wildcard

在 Make 中,* 和 % 都被称为通配符,但它们的含义完全不同。* 在文件系统中搜索匹配的文件名。我建议你始终将其封装在wildcard函数中,否则你可能会陷入下面描述的一个常见陷阱。

1
2
3
# Print out file information about every .c file
print: $(wildcard *.c)
ls -la $?

* 可以在目标、先决条件或通配符功能中使用。

危险: * 不能在变量定义中直接使用。

危险:当 * 不能匹配任何文件时,它将保持原样(除非在通配符函数中运行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)

all: one two three four

# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)

# Stays as *.o if there are no files that match this pattern :(
two: *.o

# Works as you would expect! In this case, it does nothing.
three: $(thing_right)

# Same as rule three
four: $(wildcard *.o)

3.2 % Wildcard

% 确实很有用,但由于可用于多种情况,所以有点令人困惑。

  • 在 “匹配” 模式下使用时,它会匹配字符串中的一个或多个字符。这种匹配称为词干。
  • 在 “替换” 模式下使用时,它会将匹配到的词干替换到字符串中。
  • % 通常用于规则定义和某些特定功能中。

3.3 自动变量

自动变量有很多,但往往只有少数几个会出现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hey: one two
# Outputs "hey", since this is the target name
echo $@

# Outputs all prerequisites newer than the target
echo $?

# Outputs all prerequisites
echo $^

touch hey

one:
touch one

two:
touch two

clean:
rm -f hey one two

四. 规则

4.1 隐式规则

Make 中最令人困惑的部分可能是那些神奇的自动规则。Make 将这些规则称为 “隐含 “规则。我个人并不同意这一设计决定,也不建议使用它们,但它们经常被使用,因此了解它们很有用。下面是隐含规则的列表:

  • 编译 C 程序:使用 $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@ 形式的命令从 n.c 自动生成 n.o。
  • 编译 C++ 程序:使用形式为 $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ 的命令,可自动从 n.cc 或 n.cpp 生成 n.o
  • 链接单个对象文件:通过运行 $(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@ 命令,可从 n.o 自动生成 n

隐式规则使用的重要变量有:

  • CC: 编译 C 程序的程序;默认为 cc
  • CXX: 用于编译 C++ 程序的程序;默认为 g++
  • CFLAGS:提供给 C 编译器的额外标志
  • CXXFLAGS:提供给 C++ 编译器的额外标志
  • CPPFLAGS:提供给 C 预处理器的额外标记
  • LDFLAGS:在编译器调用链接器时提供给编译器的额外标记

让我们看看如何在不明确告诉 Make 如何编译的情况下编译 C 程序:

1
2
3
4
5
6
7
8
9
10
11
12
CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info

# Implicit rule #1: blah is built via the C linker implicit rule
# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists
blah: blah.o

blah.c:
echo "int main() { return 0; }" > blah.c

clean:
rm -f blah*

4.2 静态模式规则

静态模式规则是在 Makefile 中减少编写内容的另一种方法,但我认为它更有用,而且不那么 “神奇”。下面是它们的语法:

1
2
targets...: target-pattern: prereq-patterns ...
commands

其实质是通过目标模式(通过 % 通配符)匹配给定目标。匹配到的内容称为词干。然后将词干代入先决条件模式,生成目标的先决条件。

一个典型的用例是将 .c 文件编译成 .o 文件。下面是手动方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

下面是使用静态模式规则的更有效方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

4.3 静态模式规则和过滤器

在稍后介绍函数的同时,我将先介绍一下可以用它们做些什么。过滤器函数可用于静态模式规则,以匹配正确的文件。在本例中,我制作了 .raw 和 .result 扩展名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all

# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $<"

# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
echo "target: $@ prereq: $<"

%.c %.raw:
touch $@

clean:
rm -f $(src_files)

4.4 Pattern Rules

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 $@

模式规则在目标中包含一个”%”。该’%’匹配任何非空字符串,其他字符则自行匹配。模式规则先决条件中的”%”代表与目标中的”%”匹配的同一词干。

4.5 双冒号规则

双冒号规则很少使用,但允许为同一目标定义多个规则。如果是单冒号,则会打印警告,并且只会运行第二组命令。

五. Commands and execution

5.1 命令行的输出/静默

在命令前添加 @ 以阻止命令被打印出来
您也可以使用 -s 运行 make,在每一行前添加 @

1
2
3
all: 
@echo "This make line will not be printed"
echo "But this will"

5.2 命令行的执行

每条命令都在一个新的 shell 中运行(至少效果是这样的)

1
2
3
4
5
6
7
8
9
10
11
all: 
cd ..
# The cd above does not affect this line, because each command is effectively run in a new shell
echo `pwd`

# This cd command affects the next because they are on the same line
cd ..;echo `pwd`

# Same as above
cd ..; \
echo `pwd`

5.3 默认shell

默认 shell 为 /bin/sh。你可以通过修改变量 SHELL 来改变它:

1
2
3
4
SHELL=/bin/bash

cool:
echo "Hello from bash"

5.4 双美元符号

如果希望字符串带有美元符号,可以使用 $$。这就是如何在 bash 或 sh 中使用 shell 变量。

请注意 Makefile 变量和 Shell 变量之间的区别。

1
2
3
4
5
6
7
make_var = I am a make variable
all:
# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
sh_var='I am a shell variable'; echo $$sh_var

# Same as running "echo I am a make variable" in the shell
echo $(make_var)

5.5 使用 -k、-i 和 - 处理错误

运行 make 时添加 -k,即使出现错误也能继续运行。如果你想一次性看到 Make 的所有错误,这很有用。
在命令前添加 - 来抑制错误
在 make 中添加 -i 可让每条命令都出错。

1
2
3
4
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one

5.6 中断或杀死

请注意:如果按 ctrl+c ,它将删除刚刚make的新目标。

5.7 make 的递归使用

要递归调用 makefile,请使用特殊的 $(MAKE) 而不是 make,因为它会为你传递 make 标志,而自身不会受其影响。

1
2
3
4
5
6
7
8
new_contents = "hello:\n\ttouch inside_file"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
cd subdir && $(MAKE)

clean:
rm -rf subdir

5.8 导出、环境和递归 make

当 Make 启动时,它会自动从执行时设置的所有环境变量中创建 Make 变量。

1
2
3
4
5
6
7
# Run this with "export shell_env_var='I am an environment variable'; make"
all:
# Print out the Shell variable
echo $$shell_env_var

# Print out the Make variable
echo $(shell_env_var)

The export directive takes a variable and sets it the environment for all shell commands in all the recipes:

1
2
3
4
5
shell_env_var=Shell env var, created inside of Make
export shell_env_var
all:
echo $(shell_env_var)
echo $$shell_env_var

因此,在 make 中运行 make 命令时,可以使用 export 指令使子 make 命令可以访问它。在本例中,cooly 被导出,以便子目录中的 makefile 可以使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_contents = "hello:\n\techo \$$(cooly)"

all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)

# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly

clean:
rm -rf subdir

您还需要导出变量,以便在 shell 中运行。

1
2
3
4
5
6
7
8
one=this will only work locally
export two=we can run subcommands with this

all:
@echo $(one)
@echo $$one
@echo $(two)
@echo $$two

.EXPORT_ALL_VARIABLES 会为您导出所有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"

cooly = "The subdirectory can see me!"
# This would nullify the line above: unexport cooly

all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)

clean:
rm -rf subdir

5.9 make的参数

有很多的选项列表,可以通过 make 运行。请查看–fry-run、–touch、–old=file。

你可以为 make 设置多个目标,例如,make clean run test 运行 clean 目标,然后运行,最后测试。

六. 变量 2

6.1 Flavors and modification

变量有两种:

  • 递归(使用 =)–只在使用命令时查找变量,而不是在定义变量时。
  • 简单扩展(使用 :=)–就像普通的命令式编程一样–只有到目前为止定义的变量才会被扩展
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
2
3
4
5
6
one = hello
# one gets defined as a simply expanded variable (:=) and thus can handle appending
one := ${one} there

all:
echo $(one)

?= 仅设置尚未设置的变量

1
2
3
4
5
6
7
one = hello
one ?= will not be set
two ?= will be set

all:
echo $(one)
echo $(two)

行尾的空格不会被删除,但行首的空格会被删除。要使用单空格创建变量,请使用 $(nullstring)

1
2
3
4
5
6
7
8
9
with_spaces = hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)there

nullstring =
space = $(nullstring) # Make a variable with a single space.

all:
echo "$(after)"
echo start"$(space)"end

未定义变量实际上是一个空字符串!

1
2
3
all: 
# Undefined variables are just empty strings!
echo $(nowhere)

使用 += 来扩展

1
2
3
4
5
foo := start
foo += more

all:
echo $(foo)

字符串替换也是修改变量的一种非常常见和有用的方法。还可以查看文本函数和文件名函数。

6.2 命令行参数和覆盖

使用 override 可以覆盖命令行变量。在这里,我们使用 make option_one=hi 运行 make

1
2
3
4
5
6
7
# Overrides command line arguments
override option_one = did_override
# Does not override command line arguments
option_two = not_override
all:
echo $(option_one)
echo $(option_two)

6.3 List of commands and define

define并不是一个函数,尽管它看起来像。我见过它的使用频率很低,所以就不细说了,但它主要用于定义canned recipes ,还能与 eval 函数很好地搭配使用。

define/endef 简单地创建一个变量,并将其设置为一系列命令。请注意,这与在命令之间使用分号有点不同,因为每个命令都会在单独的 shell 中运行。

1
2
3
4
5
6
7
8
9
10
11
12
one = export blah="I was set!"; echo $$blah

define two
export blah="I was set!"
echo $$blah
endef

all:
@echo "This prints 'I was set'"
@$(one)
@echo "This does not print 'I was set' because each command runs in a separate shell"
@$(two)

6.4 Target-specific variables

可为特定目标设置变量

1
2
3
4
5
6
7
all: one = cool

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

6.5 Pattern-specific variables

您可以为特定目标模式设置变量

1
2
3
4
5
6
7
%.c: one = cool

blah.c:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

七. 条件判断

7.1 if/else

1
2
3
4
5
6
7
8
foo = ok

all:
ifeq ($(foo), ok)
echo "foo equals ok"
else
echo "nope"
endif

7.2 检查变量是否为空

1
2
3
4
5
6
7
8
9
10
nullstring =
foo = $(nullstring) # end of line; there is a space here

all:
ifeq ($(strip $(foo)),)
echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
echo "nullstring doesn't even have spaces"
endif

7.3 检查变量是否已定义

ifdef 不会扩展变量引用;它只是查看是否定义了某个变量

1
2
3
4
5
6
7
8
9
10
bar =
foo = $(bar)

all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif

7.4 $(MAKEFLAGS)

本例演示如何使用 findstring 和 MAKEFLAGS 测试 make 标志。使用 make -i 运行此示例,即可看到它打印出 echo 语句。

1
2
3
4
5
all:
# Search for the "-i" flag. MAKEFLAGS is just a list of single characters, one per flag. So look for "i" in this case.
ifneq (,$(findstring i, $(MAKEFLAGS)))
echo "i was passed to MAKEFLAGS"
endif

八. 函数

8.1 First function

函数主要用于文本处理。使用 $(fn, arguments) 或 ${fn, arguments} 调用函数。Make 有大量内置函数。

1
2
3
bar := ${subst not,totally, "I am not superman"}
all:
@echo $(bar)

如果要替换空格或逗号,请使用变量

1
2
3
4
5
6
7
8
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
@echo $(bar)

在第一个参数之后的参数中不要包含空格。空格将被视为字符串的一部分。

1
2
3
4
5
6
7
8
9
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
# Output is ", a , b , c". Notice the spaces introduced
@echo $(bar)

8.2 字符串替换

$(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 docs)

替换引用 $(text:pattern=replacement) 就是这种情况的简写。

还有一种只替换后缀的速记方法:$(text:sufficiency=replacement)。这里不使用 % 通配符。

注意:不要为这种速记法添加额外的空格。它会被视为搜索或替换词。

1
2
3
4
5
6
7
8
9
10
11
foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)

all:
echo $(one)
echo $(two)
echo $(three)

8.3 循环函数

foreach 函数看起来像这样:$(foreach var,list,text).它将一个单词列表(用空格分隔)转换为另一个单词列表。var 设置为列表中的每个单词,text 则为每个单词扩展。

这会在每个单词后添加一个感叹号:

1
2
3
4
5
6
7
foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)

all:
# Output is "who! are! you!"
@echo $(bar)

8.4 if函数

if 检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个参数。

1
2
3
4
5
6
7
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
@echo $(foo)
@echo $(bar)

8.5 call函数

Make 支持创建基本函数。您只需通过创建一个变量来 “define” 函数,但要使用 $(0)、$(1) 等参数。然后使用特殊的调用内置函数调用该函数。语法是 $(调用变量,参数,param)。$(0)是变量,$(1)、$(2)等是参数。

1
2
3
4
5
sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
# Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
@echo $(call sweet_new_fn, go, tigers)

8.6 shell函数

shell - 调用 shell,但会用空格替换换行符!

1
2
all: 
@echo $(shell ls -la) # Very ugly because the newlines are gone!

九. 其他特性

9.1 包含 Makefile

include 指令告诉 make 读取一个或多个其他 makefile。这是 makefile 中的一行,看起来像这样:

1
include filenames...

这在使用 -M 等编译器标志时特别有用,这些标志会根据源代码创建 Makefile。例如,如果某些 c 文件包含一个头文件,那么该头文件就会被添加到由 gcc 编写的 Makefile 中。我将在 Makefile Cookbook 中详细介绍这一点

十. Makefile Cookbook

让我们来看看一个非常实用的 Make 示例,它非常适合中等规模的项目。

这个 makefile 最棒的地方在于它会自动为你确定依赖关系。你只需将 C/C++ 文件放入 src/ 文件夹即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)
TARGET_EXEC := final_program

BUILD_DIR := ./build
SRC_DIRS := ./src

# Find all the C and C++ files we want to compile
# Note the single quotes around the * expressions. The shell will incorrectly expand these otherwise, but we want to send the * directly to the find command.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# Prepends BUILD_DIR and appends .o to every src file
# As an example, ./your_dir/hello.cpp turns into ./build/./your_dir/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

# String substitution (suffix version without %).
# As an example, ./build/hello.cpp.o turns into ./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)

# Every folder in ./src will need to be passed to GCC so that it can find header files
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# Add a prefix to INC_DIRS. So moduleA would become -ImoduleA. GCC understands this -I flag
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# The -MMD and -MP flags together generate Makefiles for us!
# These files will have .d instead of .o as the output.
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# The final build step.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
$(CXX) $(OBJS) -o $@ $(LDFLAGS)

# Build step for C source
$(BUILD_DIR)/%.c.o: %.c
mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# Build step for C++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
mkdir -p $(dir $@)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean
clean:
rm -r $(BUILD_DIR)

# Include the .d makefiles. The - at the front suppresses the errors of missing
# Makefiles. Initially, all the .d files will be missing, and we don't want those
# errors to show up.
-include $(DEPS)