技术交流

好好学习,天天向上。

0%

一文搞懂Makefile

[toc]

本文是看这个教程的翻译。另外再推荐看一看这篇文章,《跟我一起学Make》

背景

我制作这个指南的原因是我永远无法完全掌握Makefile文件。他们似乎充斥着潜规则和深奥的符号,并且问简单的问题不会得到简单的答案。为了解决这个问题,我坐下来过了几个周末,阅读了所有关于Makefile文件的东西。我已经把最关键的知识浓缩到这本指南里了。每个主题都有一个简短的描述和一个独立的例子,你可以自己运行。

如果你对Make差不多都理解,那可以考虑看看Makefile Cookbook小节。它包含一个适用于中型项目的模板,里面有大量关于Makefile每个部分作用的注释。

祝你好运,我希望你能够杀死混乱的Makefiles世界!

开始

Makefile文件存在的意义

Makefile文件有助于大型程序的哪些部分需要重新编译。在绝大多数情况下,C或C++文件是需要重编译的。其他语言通常都有自己的与Make类似的工具。Make也可以在程序之外使用,当您需要基于文件变化来执行一系列命令的时候。本教程将关注C/C++编译的场景。

下图是一张依赖关系示意图,可以用make来解决其编译关系。如果任何文件的依赖关系发生变化,那么该文件将被重新编译:

和Make类似的软件

流行的C/C++替代构建系统有SCons、CMake、Bazel和Ninja。像微软Visual Studio这样的一些代码编辑器有自己的内置构建工具。对于Java语言,有Ant、Maven和Gradle。像Go和Rust这样的其他语言都有自己的构建工具。

像Python、Ruby和Javascript这样的解释语言不需要Make这样的工具。Makefiles的目标是根据哪些文件发生了变化来构建需要构建的文件,无论这个文件是什么。但对于解释语言的文件,其改变后不需要重新构建,只需要程序运行时使用文件的最新版本即可。

Make的版本和类型

Make有各种各样的实现,本指南的大部分内容对任何一个您可能使用的版本都是适用的。但还是需要强调,本文是针对GNU Make写的,GNU Make是Linux和MacOS上的标准Make实现。所有的例子都适用于Make 3和4版本,这两个版本除了一些细微的差异之外,其它特性几本等价。

运行例子

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

1
2
hello:
echo "hello world"

下面是运行上面例子的输出内容:

1
2
3
$ make
echo "hello world"
hello world

就是如此!如果您有哪里没看懂,这里有一个视频可以帮助您。其主要介绍了一些操作步骤以及Makefiles的基本结构。

Makefile语法

Makefile由一组规则组成。一个规则通常是这样的:

1
2
3
4
targets: prerequisites
command
command
command
  • targets由文件名组成,多个文件名用空格分隔。通常,每个规则只有一个target。
  • command通常是用于制作目标的一系列步骤。这些需要以tab开始,而不能是空格。
  • prerequisites同样由文件名组成,多个文件名用空格分隔。在运行command之前,列表中的文件需要存在。这些文件也被称为依赖项

新手示例

下面的Makefile有三个独立的规则。当您在终端中运行make blah时,它将通过一系列步骤构建一个名为blah的程序:

  • blah是第一个交给Make的target,所以Make将首先处理该target
  • blah 依赖 blah.o,所以make转而处理blah.o这个target
  • blah.o 依赖 blah.c,所以make转而处理blah.c 这个target
  • blah.c 没有依赖项,所以Make执行echo命令
  • 因为blah.o的依赖项都处理完了,所以紧接着执行 cc -c 命令
  • 因为blah 的依赖项目都处理完了,所以紧接着执行cc 命令
  • 处理接触,最终生成编译好的blah文件
1
2
3
4
5
6
7
8
blah: blah.o
cc blah.o -o blah # Runs third

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

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

下面这个makefile只有一个目标,叫做some_file。Makefile的第一个target就是项目的默认target,因此在这种情况下,将运行some_taget对应的命令。

1
2
some_file:
echo "This line will always print"

下面这个makefile第一次运行会生成some_file文件。第二次运行的时候,make会注意到some_file已经生成了,然后会打印”make: 'some_file' is up to date.”

1
2
3
some_file:
echo "This line will only print once"
touch some_file

下面这个makefile,目标some_file依赖于other_file。当我们运行make时,默认target(也就是some_file,因为它是第一个的target)将被解析。make将首先查看some_file依赖项列表,如果其中任何一个较旧,它将首先处理这些依赖项,最后处理some_file自身。第二次运行时,两个目标都不会运行,因为两个目标都存在。

1
2
3
4
5
6
7
some_file: other_file
echo "This will run second, because it depends on other_file"
touch some_file

other_file:
echo "This will run first"
touch other_file

下面这个makefile总是将两个target都解析,因为some_file依赖一个永远不会存在的文件。

1
2
3
4
5
some_file: other_file
touch some_file

other_file:
echo "nothing"

clean总以作为一个移除其他target所创文件的target,但实际上它不并是make中的关键字。

1
2
3
4
5
some_file: 
touch some_file

clean:
rm -f some_file

变量

make对变量的处理过程,类似于C/C++中预处理对宏的处理过程,也就是值替换。变量只能是字符串,下面是一个关于变量使用的例子:

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

可以通过${}或者$()引用变量

1
2
3
4
5
6
7
8
x = dude

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

# Bad practice, but works
echo $x

目标

多target构建

如果你想同时make多个target,那么可以使用伪target。伪target可以依赖其他目标,伪目标可以作为默认目标。

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

多个target

在一个规则中,如果存在多个target,那么make将为每个target独立运行规则中的命令。$@是个自动变量,其内容为每次命令独立运过程中的target名称。

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

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

自动变量和wildcard

*通配符

***%在Make中都被称为通配符,但它们的含义完全不同。***用于在文件系统中搜索匹配的文件名。我建议您总是wildcard函数封装它,否则您可能会陷入下面所描述的常见陷阱之中。

1
2
3
# Print out file information about every .c file
print: $(wildcard *.c)
ls -la $?
  • *可能会用在targetprerequisites以及wildcard函数中

  • 注意: * 不能直接用于定义变量

  • 注意:当 *没有匹配到任何一个文件的时候,它就作为*符号本身留在当前位置(除非将其用 wildcard函数封装起来)

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)

%通配符

%符号是真的好用,但它也会有一些让人疑惑的地方,因为其可用于各种不同的场景:

  • 当用在“matching”模式的时候,它匹配字符串中一个或者多个字符。
  • 当用在”replacing”模式的时候,它会被替换为pattern中匹配到的子串。
  • % 符号最常见的用法还是在一些特定函数中做规则定义。

请参阅以下部分,了解使用了%的示例:

自动变量

Make中有许多的自动变量,但常用的自动变量只占一小部分:

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 first target
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

上面的例子运行后打印如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost demo]# make 
# Outputs "hey", since this is the first target
echo hey
hey
# Outputs all prerequisites newer than the target
echo one two
one two
# Outputs all prerequisites
echo one two
one two
touch hey
[root@localhost demo]# make
make: 'hey' is up to date.

花哨的规则

隐式规则

make热爱编译C语言,但每次它表达爱意的时候,事情就会变得混乱。也许Make最令人困惑的部分是所制定的魔法/自动规则,Make称其为隐式规则。我个人不赞同这个设计决策,我不建议使用它们。但是它们太常见了,因此学习它们还是有必要的。这里有一个隐式规则的列表:

  • 编译C程序: n.o 文件会自动的用命令$(CC) -c $(CPPFLAGS) $(CFLAGS)n.c文件编译出来,不需要手动的在规则中写入这条命令。
  • 编译C++程序:n.o 文件会自动的用命令$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)n.cc文件或者n.cpp文件编译出来,不需要手动的在规则中写入这条命令。
  • 链接一个独立的目标文件:n 会自动的通过命令$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)n.o创建出来。

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

  • CC: Program for compiling C programs; default cc
  • CXX: Program for compiling C++ programs; default g++
  • CFLAGS: Extra flags to give to the C compiler
  • CXXFLAGS: Extra flags to give to the C++ compiler
  • CPPFLAGS: Extra flags to give to the C preprocessor
  • LDFLAGS: Extra flags to give to compilers when they are supposed to invoke the linker

让我们看看我们现在如何在不明确告诉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*

静态模式规则

静态模式规则相对隐式规则而言,是用得比较少的。但我认为它更有用,也不那么“奇怪”。以下是它的语法:

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

其本质就是要让target-pattern(通过一个 %通配符) 去匹配target中的元素。target中被匹配到的任何东西,称为stem。然后stem就被替换进prereq-pattern里面,构成target的依赖项。

一个典型的使用场景就是将.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
18
19
	
# Here's the more efficient way, using a static pattern rule:
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

静态模式规则以及过滤器

我稍后会向您介绍函数,现在我要提前向您展示,您可以如何使用它们。过滤器功能可用于静态模式规则,以匹配正确的文件。在这个例子中,我编写了.raw.result扩展。

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

.PHONY: all
all: $(obj_files)

$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $<"
$(filter %.result,$(obj_files)): %.result: %.raw
echo "target: $@ prereq: $<"

%.c %.raw:
touch $@

clean:
rm -f $(src_files)

模式规则

模式规则经常使用,但是十分复杂。你可以以下面两种方式看待它们:

  • 一个定义你自己隐式规则的方式
  • 一个简化的静态模式规则

先来看一个例子:

1
2
3
# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

模式规则的target包含一个%符号,这个%符号匹配任意的非空字符串。target中的其它符号保持原样匹配。模式规则先决条件中的%符号代表与target%所匹配到的同一词干。

下面是另外一个例子:

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

双冒号规则

双冒号规则很少使用,其允许为同一个target定义多个规则。如果同名target的重复规则后面是单个冒号,make会打印一条警告,并且只运行第二条规则。

1
2
3
4
5
6
7
all: blah

blah::
echo "hello"

blah::
echo "hello again"

命令以及执行

命令回显/静音

你可以命令前添加一个@符号来阻止命令打印,也可以在运行make时添加-s参数,这相当于在每行都添加一个@符号

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

命令执行

每条命令都是在一个新的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`

默认shell

默认的shell是/bin/sh。你可以通过修改SHELL环境变量来修改默认shell

1
2
3
4
SHELL=/bin/bash

cool:
echo "Hello from bash"

利用-k-i以及-进行错误处理

  • -k(或者--keep-going)参数是make的一个参数。如果添加了-k参数,那么运行的时候即使遇到错误,也继续运行。如果你想一次看到Make的所有错误,这个参数就很有用。
  • -是写在Makefile文件的command前面的。其含义是告知make,不管该command出不出错都认为是成功的。如果要让这个条件全局应用,可以使用make参数-i(或者--ignore-errors)
1
2
3
4
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one

打断或者杀死make

注意:如果对make使用ctrl+c,它将删除刚刚创建的较新目标。

Make的递归使用

要递归调用makefile,请使用特殊的$(make)而不是make,区别在于对待各种flag的方式。

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

递归make场景下使用export

export指令接受一个变量并使该变量可被下级的各个make访问。在下面这个例子中,coolyexport指令处理,这样下级目录中的makefile就可以使用它。 注意:export的语法与sh相同,但它们并不相关(虽然功能相似)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
######################################
# 顶层makefile文件
######################################
all:
@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
######################################
# subdir中的makefile文件
######################################
hello:
echo $(cooly)

如果你希望变量在shell中有效,你也需要export它们。

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 可以为你export所有的变量。

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
echo $(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

make的参数

这里有一个很好的make可以支持的选项列表,建议查看 --dry-run, --touch, --old-file

你可以有一次构建多个target。举例来说:make clean run test会先处理clean这个target,然后goal,然后 run,然后处理test

变量,第二部分

风格和编辑

有两种类型的变量:

  • 递归型(用=符号赋值)——只在使用变量时对变量做解析,而不是在定义时解析。
  • 简单展开型(用:=赋值)——就像普通的命令式编程一样,在赋值那一刻为止,定义过的变量才会被扩展。
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)

字符串替换函数 也是一种编辑变量值的方式,可以查看[文本函数]](https://www.gnu.org/software/make/manual/html_node/Text-Functions.html#Text-Functions) 和文件名函数小节。

命令行参数以及override

您可以使用override覆盖来自命令行的变量。下面这个例子,我们给make一个预定义变量option_one= hi

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)

命令列表和define

“define”实际上只是一个命令列表,和函数无关。请注意,这与用分号分割的一组命令有点不同。因为正如预期的那样,“define”中的每个命令都在一个单独的shell中运行。

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

define two
export blah=set
echo $$blah
endef

# One and two are different.

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)

与特定Target有关的变量

变量可以被指定给特定的target

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

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

与特定Pattern有关的变量

变量可以被指定给特定的pattern

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

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

other:
echo one is nothing: $(one)

条件分支

if/else条件

1
2
3
4
5
6
7
8
foo = ok

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

检查变量是否为空

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

检查变量是否定义过

ifdef不展开变量引用,它只是看看某个东西是否被定义了

1
2
3
4
5
6
7
8
9
10
11
# bar的赋值语句后面啥也没有,就理解为变量没有定义。注意,赋值右边内容的开头空格会被删掉
bar =
foo = $(bar)

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

$(makeflags)

这个例子展示了如何用findstringMAKEFLAGS测试make的各种flag。用make -i运行这个例子,看它打印出echo语句。

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

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

函数

首次函数

函数主要用于文本处理,可以通过 $(fn, arguments) 或者 ${fn, arguments}调用函数。你可以自行利用Make的call內建函数。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)

字符串替换

$(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文档)

替换引用$(text:pattern=replacement) 是上面这个句型的简写版本。

还有另外一种简写版本,其只替换后缀$(text:suffix=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)

foreach函数

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)

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)

call函数

make支持创建基本的函数。你可以通过创建一个变量来“定义” 一个函数,但是要使用参数$(0), $(1)等等。你之后就可以使用call函数调用这个函数了。其语法是$(call variable,param,param)$(0)define的变量值,而$(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)

shell函数

shell函数会调用系统的shell,但是它会用空格替代参数中的换行

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

其他特性

引入其他makefile

include指令告知make,取读取一个或多个其它的makefile文件。其用法示例如下,就是makefile文件中的一句话:

1
include filenames...

你可以使用编译器的-M参数,让其自动的基于源码为你生成makefile。举例来说,如果C文件包含了一个头文件,那么该文件名将被追加到gcc创建的Makefile文件中。我会在 Makefile Cookbook中更多的讨论这一点。

The vpath Directive

使用vpath指定某组先决条件存在的位置。其格式为 vpath <pattern> <directories, space/colon separated><pattern> 可以包含一个%符号,其匹配0个或者或更多的字符。
你也可以使用VPATH来全局的应用这个规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vpath %.h ../headers ../other-directory

some_binary: ../headers blah.h
touch some_binary

../headers:
mkdir ../headers

blah.h:
touch ../headers/blah.h

clean:
rm -rf ../headers
rm -f some_binary

Multiline

反斜杠”\“可以连接分行写的单条句子

1
2
3
some_file: 
echo This line is too long, so \
it is broken up into multiple lines

.PHONY

添加.PHONY可以防止伪target名称和文件名混淆。在下面这个例子中,如果一个名为clean的文件存在了,make clean同样会运行。.PHONY非常好用,但为了简单起见,在其他的示例中我都省略了它。

1
2
3
4
5
6
7
8
some_file:
touch some_file
touch clean

.PHONY: clean
clean:
rm -f some_file
rm -f clean

.DELETE_ON_ERROR

如果某个命令返回非零的退出状态,make工具将停止该规则的运行。如果添加DELETE_ON_ERROR标签,任何一个发生类似错误的规则,make都会删除该规则对生成的目标文件。建议总是添加这个标签,尽管出于历史原因,make不这样做。

1
2
3
4
5
6
7
8
9
10
.DELETE_ON_ERROR:
all: one two

one:
touch one
false

two:
touch two
false

Makefile Cookbook

Makefile加工厨房

让我们来看一个非常有趣的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. Make will incorrectly expand these otherwise.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# String substitution for every C/C++ file.
# As an example, hello.cpp turns into ./build/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)
$(CC) $(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)