我建立了这个指南,因为我以前总也搞不清Makefiles。 它们似乎充斥着隐藏的规则和深奥的符号,网上提问简单的问题也得不到简单的答案。 为了解决这个问题,我花了了几个周末,阅读了有关Makefiles的所有内容。 我已经将最关键的知识浓缩到了这本指南中。 每个主题都有一个简短的描述和一个你可以自己运行的独立示例。
如果你已经大体理解Make,可以考虑阅读 Makefile Cookbook ,其中有一个针对中型项目的模板,且包含有关Makefile每个部分所做工作的大量注释。
祝你好运,希望你能够摆平令人困惑的Makefiles世界!
入门
为什么需要Makefiles?
Makefiles用于帮助确定大型程序的哪些部分需要重新编译。 在绝大多数情况下,C或C++文件都是需要编译的。 其他语言通常都有自己的工具,它们的用途与Make类似。 在编译之外Make也可用于当你需要根据文件更改情况运行一系列指令时。 本教程将重点介绍C/C++编译用例。
下面是一个示例依赖关系图,你可能将Make用于类似的情况。 如果任何文件的依赖关系发生更改,则将重新编译该文件:
有什么替代方案?
流行的C/C++替代构建系统是 SCons , CMake , Bazel ,和 NINJA 。 一些代码编辑器,如 Microsoft Visual Studio 有自己的内置构建工具。 对于JAVA,有 Ant , Maven ,和 Gradle 。 像Go和Rust这样的其他语言都有自己的构建工具。
像Python、Ruby和Javascript这样的解释语言不需要类似Makefile的工具。 Makefiles的目标是根据更改的文件来编译需要编译的任何文件。 但当解释语言的文件发生变化时,不需要重新编译。 那些程序运行时,会直接使用文件的最新版本。
Make的版本和类型
有各种各样的实现,但本指南的大部分将适用于你使用的任何版本。 但这个教程还是专门为GNU Make编写的,GNU Make是Linux和MacOS上的标准实现。 所有的例子都适用于Make版本3和4,除了一些深奥的差异之外,它们几乎是等同的。
运行示例
要运行这些示例,你需要安装一个终端并安装好“make”。 对于每个示例,请将内容放在一个名为Makefile
的文件中,并在该目录中运行命令make
。 让我们从最简单的Makefiles开始:
hello:
echo "Hello, World"
注意:Makefile 必须 使用制表符而不是空格进行缩进,否则
make
将失败。
下面是运行上述示例的输出:
$ make
echo "Hello, World"
Hello, World
就这样! 如果你有点困惑,这里有一个视频,它演示了这些步骤,并描述了makefile的基本结构。
Makefile语法
Makefile由一组 rules 组成。 rule通常如下所示:
targets: prerequisites
command
command
command
- targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
- commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
- prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)
Make的精髓
让我们从一个hello world的例子开始:
hello:
echo "Hello, World"
echo "This line will always print, because the file hello does not exist."
这里已经包含很多东西了。 让我们分解一下:
- 我们有一个 targe 叫做‘hello`
- 这个目标有两个 commands
- 这个目标没有prerequisites(先决条件)
然后我们运行 make hello
。 只要hello
文件不存在,命令就会运行。 如果 hello
已存在,则不会运行任何命令。
需要注意的是,我所说的hello
既是一个目标,也是一个文件*。 那是因为两者直接绑在一起。 通常,当目标运行时(也就是运行目标的命令时),这些命令将创建一个与目标同名的文件。 目前hello
*target 还不会创建 hello
文件。
让我们创建一个更典型的Makefile - 一个编译单个C文件的Makefile。 但是在执行此操作之前,请创建一个名为 “blah.c” 的文件,其中包含以下内容:
// blah.c
int main() { return 0; }
然后创建Makefile(和往常一样称为Makefile
):
blah:
cc blah.c -o blah
这次,尝试简单地运行 “make”。 由于没有将目标作为参数提供给make
命令,因此会运行第一个目标。 在这种情况下,只有一个目标 (blah
)。 第一次运行此命令时,将创建blah
。 第二次运行,你会看到 make: 'blah' is up to date
。 这是因为blah
文件已经存在。 但是有一个问题: 如果我们修改 blah.c
,然后运行 make
,还是什么都不会被重新编译。
我们通过添加一个先决条件来解决这个问题:
blah: blah.c
cc blah.c -o blah
当我们再次运行 make
时,会发生以下一组步骤:
- 第一个目标被选中,因为第一个目标是默认目标
- 这有一个 “blah.c” 的先决条件
- make决定是否应该运行
blah
目标。 只有当blah.c
不存在,或者blah.c
比 *blah
更新时,它才会运行
这最后一步是至关重要的,也是 make的本质。 它试图做的是确定自上一次编译blah
以来,blah
的先决条件是否发生了变化。 也就是说,如果修改了 blah.c
,运行 make
应该重新编译文件。 反之,如果blah.c
没有变化,则无需重新编译。
为了实现这一目标,它使用文件系统时间戳作为代理来确定某些内容是否已更改。 这是一个合理的启发式方法,因为文件时间戳通常只有在文件被修改时才会更改。 但重要的是要意识到情况并不总是如此。 例如,你可以修改一个文件,然后将该文件修改后的时间戳更改为旧的时间戳。 如果这样做,Make则会错误地猜测文件没有更改,因此可以忽略。
哇,真是脑洞大开。请确保你理解这一点。 这是makefile的关键所在,可能需要你几分钟才能正确理解。 如果事情仍然令人困惑,请进行上述示例或观看上面的视频。
更多快速示例
以下Makefile最终运行所有三个目标。 当你在终端运行make
时,它会按一系列步骤构建一个名为blah
的程序:
- make选择目标
blah
,因为第一个目标是默认目标 blah
需要blah.o
, 因此搜索blah.o
目标blah.o
需要blah.c
,所以搜索blah.c
目标blah.c
没有依赖关系,所以运行echo
命令- 然后运行
cc -c
命令,因为所有blah.o
依赖关系都查询完成了 - 运行顶部的
cc
命令,因为所有的blah
依赖都已完成 - 就是这样得到:
blah
一个编译完成的c程序
blah: blah.o
cc blah.o -o blah # 第三位运行
blah.o: blah.c
cc -c blah.c -o blah.o # 第二位运行
# 通常情况下,blah.c已经存在,但我想限制任何其他所需的文件
blah.c:
echo "int main() { return 0; }" > blah.c # 写入blah.c文件,首先运行
如果删除 blah.c
,则将重新运行所有三个目标。 如果你对其进行编辑(从而将时间戳更改为比blah.o
更新),则前两个目标将运行。 如果你运行 'touch blah.o' (并因此将时间戳更改为比 'blah' 更新),那么只有第一个目标将运行。 如果不更改任何内容,则不会运行任何目标。 试试看!
下一个示例没有什么新意,但却是一个很好的附加示例。 它将始终运行两个目标,因为 some_file
依赖于从未创建的 other_file
。
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"
Make clean
clean
通常用作删除其他目标的输出的目标,但它在make中并不是一个专有的词。 你可以在此运行 make
和 make clean
来创建和删除 some_file
。
请注意,clean
在这里做了两件新的事情:
- 它不是第一个目标 (默认目标),也不是先决条件。 这意味着除非显式调用
make lean
,否则它永远不会运行 - 它不是一个文件名。 如果你碰巧有一个名为 “clean” 的文件,则此目标将无法运行,这不是我们想要的。 请参阅本教程后面的
.PHONY
,了解如何解决此问题
some_file:
touch some_file
clean:
rm -f some_file
变量
变量只能是字符串。 你通常会希望使用:=
,但=
也可以。 参见 [变量 第2部分](#变量 第2部分)。
以下是使用变量的示例:
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
使用 ${}
或 $()
引用变量
x := dude
all:
echo $(x)
echo ${x}
# 不好的做法,但有效
echo $x
目标
all目标
要制作多个目标,而你想让所有目标都运行? 你可以制作一个all
目标。
由于这是列出的第一个规则,因此如果调用 make
而未指定目标,则默认情况下它将运行。
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
多目标
当一个规则有多个目标时,将为每个目标运行命令。 $@
是包含目标名称的 自动变量 。
all: f1.o f2.o
f1.o f2.o:
echo $@
# 相当于:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o
自动变量和通配符
* 通配符
*
和 %
在 Make中都被称为通配符,但它们的含义完全不同。 *
会在你的文件系统中搜索匹配的文件名。 我建议你始终将其包装在wildcard
函数中,因为否则你可能会陷入下面描述的常见陷阱。
# 打印出每个.c文件的文件信息
print: $(wildcard *.c)
ls -la $?
“*” 可以在目标,先决条件或 wildcard
函数中使用。
危险:*
不能在变量定义中直接使用
危险: 当 *
不匹配任何文件时,它将维持原样 (除非在wildcard
函数中运行)
thing_wrong := *.o # 不要这样做!‘*’不会展开
thing_right := $(wildcard *.o)
all: one two three four
# 失败,因为$(thing_wrong)是字符串“*.o”
one: $(thing_wrong)
# 如果没有与此模式匹配的文件,则保持为 *.o :(
two: *.o
# 如你所料!在这种情况下,它什么也不做。
three: $(thing_right)
# 与规则three相同
four: $(wildcard *.o)
% 通配符
%
确实很有用,但由于它可以在多种情况下使用,所以有点令人困惑。
- 在“匹配”模式下使用时,匹配字符串中的一个或多个字符。 这种匹配被称为词干(stem)。
- 在“替换”模式下使用时,它会获取匹配的词干,并替换字符串中的词干。
%
最常用于规则定义和某些特定函数中。
有关使用它的示例,请参阅以下各节:
自动变量
有很多个 自动变量 ,但通常只有几个出现:
hey: one two
# 输出 “hey”,因为这是目标名称
echo $@
# 输出比目标更新的所有先决条件
echo $?
# 输出所有先决条件
echo $^
touch hey
one:
touch one
two:
touch two
clean:
rm -f hey one two
其它规则
隐式规则
Make偏爱c编译。 而每次它出现这种偏爱的时候,事情就会变得混乱。 也许Make中最令人困惑的部分是Make的魔术/自动规则(magic/automatic rules)。 Make会调用这些“隐含的”规则。 我个人不同意这个设计决定,也不建议使用它们,但是它们经常被使用,因此很有用。 以下是隐含规则的列表:
- 编译C程序: 从
n.c
自动生成n.o
,命令形式为$(CC) -c $(CPPFLAGS) $(CFLAGS)
- 编译C++程序:
n.o
由n.cc
或n.cpp
自动生成,命令形式为$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)
- 链接单个目标文件: 自动从
n.o
构造n
, 通过运行命令$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)
隐式规则使用的重要变量包括:
CC
: 编译C程序的程序;默认cc
CXX
: 编译C++程序的程序; 默认 “g++”CFLAGS
: 要提供给C编译器的额外标志CXXFLAGS
: 给C++编译器的额外标志CPPFLAGS
: 要提供给C预处理器的额外标志LDFLAGS
: 当编译器应该调用链接器时,会给他们额外的标志
让我们看看我们现在如何构建一个C程序,而无需明确地告诉制造如何进行编译:
CC = gcc # 隐式规则标志
CFLAGS = -g # 隐式规则的标志。打开调试信息
# 隐式规则 #1:blah是通过C链接器隐式规则构建的
# 隐式规则 #2: blah.o是通过C编译隐式规则构建的,因为blah.c存在
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
静态模式规则
静态模式规则是在Makefile中减少编写量的另一种方式,但我会说更有用,并且 “魔术” 技巧更少。 它们的语法如下:
targets...: target-pattern: prereq-patterns ...
commands
本质是给定的 target
与 target-pattern
匹配 (通过 %
通配符)。 任何匹配的东西都被称为 词干。 然后将词干替换为 “prereq-pattern”,以生成目标的prereq。
一个典型的用例是将.c
文件编译成.o
文件。 这里是 手动方式:
objects = foo.o bar.o all.o
all: $(objects)
# 这些文件通过上面隐式规则进行编译
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
这是使用静态模式规则的更高效的方式:
objects = foo.o bar.o all.o
all: $(objects)
# 这些文件通过隐式规则进行编译
# 语法 - targets ...: target-pattern: prereq-patterns ...
# 在第一个目标foo.o的情况下,目标模式与foo.o匹配,并将“词干”设置为“foo”。
# 然后用该词干替换prereq模式中的 “%”
$(objects): %.o: %.c
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
静态模式规则和过滤器
当我稍后介绍函数时,我将预示你可以使用它们来做什么。 filter
过滤器函数可以在静态模式规则中使用,以匹配正确的文件。 在本例中,我编造了.raw
和.result
扩展名。
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
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)
模式规则
模式规则经常被使用,但非常令人困惑。你可以从两个方面来看待它们:
- 定义自己的隐含规则的方法
- 静态模式规则的更简单形式
让我们首先从一个例子开始:
# 定义将每个.c文件编译为.o文件的模式规则
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
模式规则在目标中包含‘%’。 此 '%' 匹配任何非空字符串,其他字符匹配自己。 模式规则先决条件中的‘%’代表与目标中的‘%’匹配的同一词干。
这里是另一个例子:
# 定义一个在先决条件中没有模式的模式规则。
# 这只会在需要时创建空的.c文件。
%.c:
touch $@
双冒号规则
很少使用双冒号规则,但用它允许为同一目标定义多个规则。 如果这些规则是单冒号,则会打印一条警告,并且只运行这里的第二组命令。
all: blah
blah::
echo "hello"
blah::
echo "hello again"
命令和执行
命令回显/静默
在命令之前添加 @
以阻止其打印
你还可以运行带有-s
的make命令,等同于在每行前面添加一个@
all:
@echo "This make line will not be printed"
echo "But this will"
命令执行
每个命令都在一个新的shell中运行(或者至少效果是这样的)
all:
cd ..
# 上面的cd不会影响该行,因为每个命令都有效地在新的shell中运行
echo `pwd`
# 此cd命令会影响下一个命令,因为它们在同一行上
cd ..;echo `pwd`
# 同上
cd ..; \
echo `pwd`
默认Shell
默认Shell是 '/bin/sh'。你可以通过更改变量SHELL来更改此设置:
SHELL=/bin/bash
cool:
echo "Hello from bash"
-k
、-i
、-
错误处理
在运行时添加 -k
,即使面对错误也要继续运行。 如果你想一次查看Make的所有错误,这将非常有用。
在命令前添加 -
以抑制错误
添加-i
以使每个命令都会发生这种情况。
one:
# 此错误将被打印但被忽略,并且make将继续运行
-false
touch one
中断或杀死Make
备注:如果你对make按下ctrl+c
,它将删除它刚刚创建的较新的目标。
Make的递归使用
要递归调用makefile,请使用特殊的 $(MAKE)
而不是 make
,因为这样它才能为你传递make标志,并且本身不会受到它们的影响。
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
使用export进行递归make
export指令采用一个变量,并使sub-make命令可以访问它。 在本例中,导出了cooly
,以便subdir中的Makefile可以使用它。
注意: export具有与sh相同的语法,但它们不相关 (尽管在功能上相似)
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)
# 请注意,变量和导出。 它们在全局范围内被设置/影响。
cooly = "The subdirectory can see me!"
export cooly
# 这将使上面的行无效: 取消导出cooly
clean:
rm -rf subdir
你需要导出变量,以便让它们也在shell中运行。
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_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"
cooly = "The subdirectory can see me!"
# 这将使上面一行无效:取消导出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
make的参数
有一个很好的可以从Make运行的 options 列表。 查看 --dry-run
,--touch
,--old-file
。
你可以有多个目标来make,例如 make clean run test
,运行clean
,然后run
,然后test
。
变量 第2部分
类型和修饰
有两种类型的变量:
- 递归 (使用
=
) - 在使用命令时才会查找变量,而不在定义命令时查找变量。 - 简单展开 (使用
:=
) - 就像普通的命令式编程一样 -- 只有到目前为止定义的变量才会得到展开
# 递归变量。这将能在下面打印出 “later”
one = one ${later_variable}
# 简单展开变量。这无法在下面打印出 “later”
two := two ${later_variable}
later_variable = later
all:
echo $(one)
echo $(two)
简单的说展开(使用:=
)允许你追加到一个变量。 递归定义可能会给出无限循环错误。
one = hello
# one被定义为一个简单展开变量(:=),因此可以处理追加
one := ${one} there
all:
echo $(one)
?=
仅设置尚未设置的变量
one = hello
one ?= will not be set
two ?= will be set
all:
echo $(one)
echo $(two)
行尾的空格不会被去掉,但行首的空格会被去掉。 要使用单个空格制作变量,请使用 $(nullstring)
with_spaces = hello # with_spaces在 "hello" 之后有很多空格
after = $(with_spaces)there
nullstring =
space = $(nullstring) # 构造一个单个空格变量。
all:
echo "$(after)"
echo start"$(space)"end
未定义的变量实际上是空字符串!
all:
# 未定义的变量只是空字符串!
echo $(nowhere)
使用 +=
追加
foo := start
foo += more
all:
echo $(foo)
字符串替换 也是一种真正常见且有用的修改变量的方法。 另请查看 Text Functions 和 Filename Functions 。
命令行参数和覆盖
你可以使用override
覆盖来自命令行的变量。
在这里,我们用 make option_one=hi
运行 make
# 覆盖命令行参数
override option_one = did_override
# 未覆盖的命令行参数
option_two = not_override
all:
echo $(option_one)
echo $(option_two)
命令列表和define
“define”实际上只是一个命令列表。 它与函数无关。 请注意,这与命令之间的分号有点不同,因为每个命令都在单独的shell中运行,如预期的那样。
one = export blah="I was set!"; echo $$blah
define two
export blah=set
echo $$blah
endef
# one和two是不同的。
all:
@echo "这会打印 'I was set'"
@$(one)
@echo "这不会打印 'I was set' 因为每个command都在单独的shell中运行"
@$(two)
目标特定变量
可以为特定目标中赋值变量
all: one = cool
all:
echo one is defined: $(one)
other:
echo one is nothing: $(one)
模式特定变量
你可以为特定目标 模式 中分配变量
%.c: one = cool
blah.c:
echo one is defined: $(one)
other:
echo one is nothing: $(one)
Makefile的条件部分
条件if/else
foo = ok
all:
ifeq ($(foo), ok)
echo "foo equals ok"
else
echo "nope"
endif
检查变量是否为空
nullstring =
foo = $(nullstring) # 行尾; 这里有一个空格
all:
ifeq ($(strip $(foo)),)
echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
echo "nullstring doesn't even have spaces"
endif
检查是否定义了变量
ifdef不展开变量引用;它只查看是否定义了某些内容
bar =
foo = $(bar)
all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif
$(makeflags)
此示例向你展示了如何使用 findstring
和 MAKEFLAGS
测试make标志。 使用make-i
运行此示例,以查看它打印出echo语句。
bar =
foo = $(bar)
all:
# 搜索 “-i” 标志。 MAKEFLAGS只是一个单一字符的列表,每个标志一个字符。 所以在这种情况下寻找 “i”。
ifneq (,$(findstring i, $(MAKEFLAGS)))
echo "i was passed to MAKEFLAGS"
endif
函数
首个函数
函数主要用于文本处理。 用 $(fn, arguments)
或 ${fn, arguments}
调用函数。 你可以使用 call 内置函数来创建自己的代码。 Make有相当数量的 内置函数 。
bar := ${subst not, totally, "I am not superman"}
all:
@echo $(bar)
如果要使用变量替换空格或逗号
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))
all:
@echo $(bar)
第一个参数后不要包含空格。 否则这将被视为字符串的一部分。
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))
all:
#输出为 ", a , b , c"。 注意引入的空格
@echo $(bar)
字符串替换
$(patsubst pattern,replacement,text)
执行以下操作:
"在与pattern匹配的text中查找空格分隔的单词,并将其替换为replacement。 在这里,模式可以包含充当通配符的‘%’,匹配单词中任意数量的任何字符。 如果替换还包含 ‘%’,则将 ‘%’ 替换为与模式中的 ‘%’ 匹配的文本。 只有模式和替换中的第一个‘%’会以这种方式处理;任何后续的‘%’都不会改变。" (GNU docs)
替换引用 $(text:pattern=replacement)
是这方面的简写。
还有一个只替换后缀的简写:$(text:suffix=replacement)
。 这里不使用 %
通配符。
注意:不要为此简写添加额外的空格。 它将被视为搜索或替换术语。
foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# 这是上述内容的简写
two := $(foo:%.o=%.c)
# 这是仅后缀的简写,也等同于上述。
three := $(foo:.o=.c)
all:
echo $(one)
echo $(two)
echo $(three)
foreach函数
foreach函数如下所示: $(foreach var,list,text)
。 它将一个单词列表(由空格分隔)转换为另一个列表。 var
被设置为list中每个单词,同时text
是针对每一个单词的展开。
这里在每个单词后附加了一个感叹号:
foo := who are you
# 对于foo中的每个“word”,输出相同的单词,并在后面加一个感叹号
bar := $(foreach wrd,$(foo),$(wrd)!)
all:
# 输出是 "who! are! you!"
@echo $(bar)
if函数
if
检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个参数。
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)
all:
@echo $(foo)
@echo $(bar)
调用函数
Make支持创建基本函数。 你只需创建一个变量即可定义函数,但需要使用$(0)
、$(1)
等参数。 然后,你使用特殊的 'call' 函数调用该函数。 语法为$(call variable,param,param)
。 $(0)
是变量,而 $(1)
,$(2)
... 等是参数。
sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)
all:
# 输出 “Variable Name:sweet_new_fn First: go Second: tigers Empty Variable:”
@echo $(call sweet_new_fn, go, tigers)
shell函数
shell - 这调用shell,但它用空格替换换行符!
all:
@echo $(shell ls -la) # 很难看,因为换行不见了!
其他特性
包含Makefiles
include指令告诉make读取一个或多个其他多个makefiles。 一行makefile中makefile,看起来像这样:
include filenames...
当你使用诸如 -M
之类的编译器标志基于源创建Makefiles时,这特别有用。 例如,如果某些c文件包含头文件,则该头文件将被添加到由GCC编写的Makefile中。 我在 Makefile Cookbook 中有更多探讨
vpath指令
使用vpath指定一些先决条件的存在位置。 格式为vpath <pattern> <directories, space/colon separated>
<pattern>
可以有一个 %
,它匹配任何零个或多个字符。
你还可以使用变量VPATH在全局范围内执行此操作
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
多行
反斜杠("\")字符使我们能够在命令太长时使用多行
some_file:
echo This line is too long, so \
it is broken up into multiple lines
.phony
将 .PHONY
添加到目标将防止Make将phony(假)目标与文件名混淆。 在此示例中,如果创建了文件 “clean”,则仍将运行 make clean。 从技术上讲,我应该在每个带有all
或clean
的示例中使用它,但我没有保持clean示例。 此外,"phony" 目标通常具有很少是文件名的名称,实际上,许多人忽略了它。
some_file:
touch some_file
touch clean
.PHONY: clean
clean:
rm -f some_file
rm -f clean
.delete_on_error
如果命令返回非零退出状态,则make工具将停止运行规则(并将传播回先决条件)。
如果规则以这种方式失败,则 DELETE_ON_ERROR
将删除规则的目标。 这将发生在所有目标上,而不仅仅是它之前的那个PHONY目标。 最好始终使用它是一个好主意,即使make出于历史原因没有默认使用这个策略。
.DELETE_ON_ERROR:
all: one two
one:
touch one
false
two:
touch two
false
Makefile Cookbook
让我们来看看一个非常丰富的例子,它适用于中型项目。
这个Makefile的巧妙之处在于它会自动为你确定依赖项。 你所要做的就是将C/C++文件放在 “src/” 文件夹中。
# 感谢Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)
TARGET_EXEC := final_program
BUILD_DIR := ./build
SRC_DIRS := ./src
# 找到我们要编译的所有C和C++文件
# 请注意 * 表达式两边的单引号。 否则Make会在那里错误地展开。
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')
# 每个C/C++文件的字符串替换。
# 例如,hello.cpp变成./build/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
# 字符串替换(不带%的后缀版本)。
#例如,./build/hello.cpp.o变成./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)
# ./src中的每个文件夹将需要传递给GCC,以便它可以找到头文件
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# 为INC_DIRS添加前缀。所以moduleA会变成-ImoduleA。GCC会理解-I标志
INC_FLAGS := $(addprefix -I,$(INC_DIRS))
# -MMD和-MP标志一起为我们生成Makefiles!
# 这些文件将有.d而不是.o作为输出。
CPPFLAGS := $(INC_FLAGS) -MMD -MP
# 最后一个构建步骤。
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
$(CC) $(OBJS) -o $@ $(LDFLAGS)
# C源代码的构建步骤
$(BUILD_DIR)/%.c.o: %.c
mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
# C++源代码构建步骤
$(BUILD_DIR)/%.cpp.o: %.cpp
mkdir -p $(dir $@)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -r $(BUILD_DIR)
# 包含.d makefiles。
# 前面的 - 抑制缺少makefile的错误。
# 最初,所有的.d文件都将丢失,我们不希望出现这些错误。
-include $(DEPS)