<b>我创建本指南是因为我一直无法完全理解 Makefiles。</b>它们似乎充满了隐藏的规则和深奥的符号,而提出简单的问题却得不到简单的答案。为了解决这个问题,我花了几个周末阅读了所有关于 Makefiles 的资料。我将最关键的知识浓缩到本指南中。每个主题都有一个简短的描述和一个可以自行运行的独立示例。

如果您对 Make 有大致的了解,可以查看 Makefile Cookbook,其中包含一个适用于中型项目的模板,并对 Makefile 的每个部分的作用进行了充分的注释。

祝您好运,希望您能征服 Makefiles 令人困惑的世界!

入门

为什么存在 Makefiles?

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

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

<div class='center'>
<img src="https://makefiletutorial.com/assets/dependency_graph.png"/>
</div>

Make 有哪些替代方案?

流行的 C/C++ 替代构建系统有 SConsCMakeBazelNinja。一些代码编辑器,如 Microsoft Visual Studio,有自己的内置构建工具。对于 Java,有 AntMavenGradle。其他语言,如 Go、Rust 和 TypeScript,有自己的构建工具。

像 Python、Ruby 和原始 Javascript 这样的解释型语言不需要 Makefiles 的类似物。Makefiles 的目标是根据文件更改编译需要编译的任何文件。但是当解释型语言中的文件更改时,不需要重新编译任何东西。当程序运行时,使用文件的最新版本。

Make 的版本和类型

Make 有多种实现,但本指南的大部分内容适用于您正在使用的任何版本。但是,它专门为 GNU Make 编写,这是 Linux 和 MacOS 上的标准实现。所有示例都适用于 Make 版本 3 和 4,除了某些深奥的差异外,它们几乎是等效的。

运行示例

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

hello:
    echo "Hello, World"

注意:Makefiles 必须使用 TAB 缩进,而不是空格,否则 make 将失败。

以下是运行上述示例的输出:

1
2
3
$ make
echo "Hello, World"
Hello, World

就是这样!如果您有点困惑,这里有一个视频,它将逐步介绍这些步骤,并描述 Makefiles 的基本结构。

<div class='yt-video'>
<iframe width="560" height="315" src="https://www.youtube.com/embed/zeEMISsjO38" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

Makefile 语法

Makefile 由一组规则组成。规则通常如下所示:

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

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 的文件,其内容如下:

// 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 不存在,或者 blah.c blah 时运行

最后一步至关重要,也是 make 的精髓。它试图做的是决定 blah 的先决条件自 blah 上次编译以来是否已更改。也就是说,如果 blah.c 被修改,运行 make 应该重新编译该文件。反之,如果 blah.c 没有更改,则不应重新编译。

为了实现这一点,它使用文件系统时间戳作为代理来确定是否发生了更改。这是一个合理的启发式方法,因为文件时间戳通常只会在文件被修改时才会更改。但重要的是要意识到情况并非总是如此。例如,您可以修改一个文件,然后将该文件的修改时间戳更改为旧的。如果您这样做,Make 将错误地猜测该文件没有更改,因此可以忽略。

哎呀,真是一大堆。确保您理解这一点。这是 Makefiles 的核心,可能需要几分钟才能正确理解。如果仍然感到困惑,请尝试上述示例或观看上面的视频。

更多快速示例

以下 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 的所有依赖项都已完成
  • 运行顶部的 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"

Make clean

clean 通常用作删除其他目标输出的目标,但它在 Make 中不是一个特殊词。您可以在此上运行 makemake clean 来创建和删除 some_file

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

  • 它不是第一个(默认)目标,也不是先决条件。这意味着除非您明确调用 make clean,否则它永远不会运行
  • 它不打算成为文件名。如果您碰巧有一个名为 clean 的文件,此目标将不会运行,这不是我们想要的。请参阅本教程后面的 .PHONY,了解如何解决此问题
1
2
3
4
5
some_file:
    touch some_file

clean:
    rm -f some_file

变量

变量只能是字符串。您通常会希望使用 :=,但 = 也可以。请参阅 变量第 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

单引号或双引号对 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

目标

all 目标

<!-- (Section 4.4) -->

要制作多个目标并希望它们全部运行?创建一个 all 目标。
由于这是列出的第一个规则,如果调用 make 时未指定目标,它将默认运行。

all: one two three

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

clean:
    rm -f one two three

多个目标

<!-- (Section 4.8) -->

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

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

自动变量和通配符

* 通配符

<!-- (Section 4.2) -->

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

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

* 可用于目标、先决条件或 wildcard 函数中。

危险:* 不能直接用于变量定义中

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

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)

% 通配符

% 非常有用,但由于其在各种情况下的使用方式而有些令人困惑。

  • 在“匹配”模式下使用时,它匹配字符串中的一个或多个字符。此匹配称为词干。
  • 在“替换”模式下使用时,它获取匹配的词干并将其替换到字符串中。
  • % 最常用于规则定义和某些特定函数中。

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

自动变量

<!-- (Section 10.5) -->

有许多 自动变量,但通常只出现几个:

hey: one two
    # Outputs "hey", since this is the target name
    echo $@

    # Outputs all prerequisites newer than the target
    echo $?

    # Outputs all prerequisites
    echo $^

    # Outputs the first prerequisite
    echo $<

    touch hey

one:
    touch one

two:
    touch two

clean:
    rm -f hey one two

高级规则

隐式规则

<!-- (Section 10) -->

Make 喜欢 C 编译。每次它表达它的爱时,事情都会变得混乱。Make 最令人困惑的部分可能是它所做的魔法/自动规则。Make 将这些称为“隐式”规则。我个人不同意这种设计决策,我不建议使用它们,但它们经常被使用,因此了解它们很有用。以下是隐式规则的列表:

  • 编译 C 程序:n.o 自动从 n.c 生成,命令形式为 $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
  • 编译 C++ 程序:n.o 自动从 n.ccn.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:当编译器需要调用链接器时提供给编译器的额外标志

让我们看看现在如何构建一个 C 程序,而无需明确告诉 Make 如何进行编译:

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*

静态模式规则

<!-- (Section 4.10) -->

静态模式规则是 Makefile 中减少编写的另一种方式。以下是它们的语法:

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

其本质是给定的 target 通过 target-pattern(通过 % 通配符)匹配。匹配到的内容称为词干。然后将词干替换到 prereq-pattern 中,以生成目标的先决条件。

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

objects = foo.o bar.o all.o
all: $(objects)
    $(CC) $^ -o all

foo.o: foo.c
    $(CC) -c foo.c -o foo.o

bar.o: bar.c
    $(CC) -c bar.c -o bar.o

all.o: all.c
    $(CC) -c all.c -o all.o

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

# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.
%.c:
    touch $@

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

以下是更高效的方式,使用静态模式规则:

objects = foo.o bar.o all.o
all: $(objects)
    $(CC) $^ -o all

# 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
    $(CC) -c $^ -o $@

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

# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.
%.c:
    touch $@

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

静态模式规则和过滤器

<!-- (Section 4.10) -->

虽然我稍后会介绍 filter 函数,但它在静态模式规则中很常用,所以我会在这里提及。filter 函数可以在静态模式规则中使用,以匹配正确的文件。在此示例中,我创建了 .raw.result 扩展名。

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)

模式规则

模式规则经常使用,但相当令人困惑。您可以从两个方面来看待它们:

  • 定义自己的隐式规则的一种方式
  • 静态模式规则的更简单形式

让我们先看一个例子:

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

双冒号规则

<!-- (Section 4.11) -->

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

1
2
3
4
5
6
7
all: blah

blah::
    echo "hello"

blah::
    echo "hello again"

命令和执行

命令回显/静默

<!-- (Section 5.1) -->

在命令前添加 @ 以阻止其打印
您还可以使用 -s 运行 make,以便在每行前添加 @

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

命令执行

<!-- (Section 5.2) -->

每个命令都在一个新的 shell 中运行(或者至少效果是如此)

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

<!-- (Section 5.2) -->

默认 shell 是 /bin/sh。您可以通过更改变量 SHELL 来更改它:

1
2
3
4
SHELL=/bin/bash

cool:
    echo "Hello from bash"

双美元符号

如果您希望字符串中包含美元符号,可以使用 $$。这是在 bashsh 中使用 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)

错误处理与 -k-i-

<!-- (Section 5.4) -->

运行 make 时添加 -k,即使出现错误也继续运行。如果您想一次性查看 Make 的所有错误,这很有帮助。
在命令前添加 - 以抑制错误
向 make 添加 -i 以使每个命令都发生这种情况。

<!-- (Section 5.4) -->

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

中断或终止 make

<!-- (Section 5.5) -->

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

make 的递归使用

<!-- (Section 5.6) -->

要递归调用 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

导出、环境和递归 make

<!-- (Section 5.6) -->

当 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)

export 指令获取一个变量并将其设置为所有配方中所有 shell 命令的环境:

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 被导出,以便 subdir 中的 makefile 可以使用它。

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

<!-- (Section 5.6) -->

您需要导出变量才能在 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

<!-- (Section 5.6) -->

.EXPORT_ALL_VARIABLES 为您导出所有变量。

.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

make 的参数

<!-- (Section 9) -->

有一个很好的 选项列表 可以从 make 运行。查看 --dry-run--touch--old-file

您可以有多个目标要制作,即 make clean run test 运行 clean 目标,然后是 run,然后是 test

变量第 2 部分

风格和修改

<!-- (6.1, 6.2, 6.3) -->

变量有两种风格:

  • 递归(使用 =) - 仅在命令使用时查找变量,而不是在命令定义时查找。
  • 简单扩展(使用 :=) - 像正常的命令式编程一样 -- 只有到目前为止定义的变量才会被扩展
# 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)

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

命令行参数和覆盖

<!-- (Section 6.7) -->

您可以使用 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)

命令列表和定义

<!-- (Section 6.8) -->

define 指令 不是一个函数,尽管它可能看起来像那样。我很少看到它被使用,所以我不会详细介绍,但它主要用于定义 预设配方,并且与 eval 函数 配合得很好。

define/endef 只是创建一个变量,该变量设置为命令列表。请注意,这与命令之间用分号分隔略有不同,因为每个命令都在单独的 shell 中运行,正如预期的那样。

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)

目标特定变量

<!-- (Section 6.10) -->

可以为特定目标设置变量

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

all:
    echo one is defined: $(one)

other:
    echo one is nothing: $(one)

模式特定变量

<!-- (Section 6.11) -->

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

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

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

other:
    echo one is nothing: $(one)

Makefiles 的条件部分

条件 if/else

<!-- (Section 7.1) -->

1
2
3
4
5
6
7
8
foo = ok

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

检查变量是否为空

<!-- (Section 7.2) -->

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

检查变量是否已定义

<!-- (Section 7.2) -->

ifdef 不会扩展变量引用;它只是查看是否定义了任何内容

bar =
foo = $(bar)

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

$(MAKEFLAGS)

<!-- `(Section 7.3) -->

此示例向您展示如何使用 findstringMAKEFLAGS 测试 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

函数

第一个函数

<!-- (Section 8.1) -->

函数主要用于文本处理。使用 $(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)) # Watch out!

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

<!-- # 8.2, 8.3, 8.9 TODO do something about the fns

TODO 8.7 origin fn? Better in documentation?

-->

字符串替换

$(patsubst pattern,replacement,text) 执行以下操作:

“在文本中查找与模式匹配的以空格分隔的单词,并将其替换为 replacement。这里的模式可以包含一个 '%',它充当通配符,匹配单词中的任意数量的任意字符。如果 replacement 也包含一个 '%',则该 '%' 将被模式中匹配 '%' 的文本替换。只有模式和 replacement 中的第一个 '%' 以这种方式处理;任何后续的 '%' 保持不变。”(GNU 文档

替换引用 $(text:pattern=replacement) 是上述的简写。

还有另一种简写,只替换后缀:$(text:suffix=replacement)。这里不使用 % 通配符。

注意:不要为此简写添加额外的空格。它将被视为搜索或替换项。

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 函数

<!-- (Section 8.4) -->

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 函数

<!-- (Section 8.5) -->

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 函数

<!-- (Section 8.6) -->

Make 支持创建基本函数。您只需通过创建变量来“定义”函数,但使用参数 $(0)$(1) 等。然后使用特殊的 call 内置函数调用该函数。语法是 $(call variable,param,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)

shell 函数

<!-- (Section 8.8) -->

shell - 这会调用 shell,但它会将换行符替换为空格!

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

filter 函数

filter 函数用于从列表中选择与特定模式匹配的某些元素。例如,这将选择 obj_files 中所有以 .o 结尾的元素。

1
2
3
4
5
obj_files = foo.result bar.o lose.o
filtered_files = $(filter %.o,$(obj_files))

all:
    @echo $(filtered_files)

Filter 也可以以更复杂的方式使用:

  1. 过滤多个模式:您可以一次过滤多个模式。例如,$(filter %.c %.h, $(files)) 将从文件列表中选择所有 .c.h 文件。
  2. 否定:如果您想选择所有不匹配模式的元素,可以使用 filter-out。例如,$(filter-out %.h, $(files)) 将选择所有不是 .h 文件的文件。
  3. 嵌套过滤器:您可以嵌套过滤器函数以应用多个过滤器。例如,$(filter %.o, $(filter-out test%, $(objects))) 将选择所有以 .o 结尾但不以 test 开头的对象文件。

其他功能

包含 Makefiles

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

include filenames...

当您使用 -M 等编译器标志根据源文件创建 Makefiles 时,这尤其有用。例如,如果某些 C 文件包含一个头文件,该头文件将被添加到由 gcc 编写的 Makefile 中。我在 Makefile Cookbook 中对此进行了更多讨论。

vpath 指令

<!-- (Section 4.3.2) -->

使用 vpath 指定某些先决条件存在的位置。格式为 vpath <pattern> <directories, space/colon separated>
<pattern> 可以包含一个 %,它匹配任意零个或多个字符。
您也可以使用变量 VPATH 全局地执行此操作

vpath %.h ../headers ../other-directory

# Note: vpath allows blah.h to be found even though blah.h is never in the current directory
some_binary: ../headers blah.h
    touch some_binary

../headers:
    mkdir ../headers

# We call the target blah.h instead of ../headers/blah.h, because that's the prereq that some_binary is looking for
# Typically, blah.h would already exist and you wouldn't need this.
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 将虚假目标与文件名混淆。在此示例中,如果创建了文件 cleanmake clean 仍将运行。从技术上讲,我应该在每个带有 allclean 的示例中使用它,但我想保持示例的简洁。此外,“虚假”目标的名称通常很少是文件名,实际上许多人会跳过此步骤。

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

<!-- (Section 5.4) -->

如果命令返回非零退出状态,make 工具将停止运行规则(并将传播回先决条件)。
DELETE_ON_ERROR 将在规则以这种方式失败时删除规则的目标。这将适用于所有目标,而不仅仅是它之前的目标,例如 PHONY。始终使用此功能是个好主意,尽管出于历史原因 make 没有这样做。

.DELETE_ON_ERROR:
all: one two

one:
    touch one
    false

two:
    touch two
    false

Makefile Cookbook

让我们来看一个非常棒的 Make 示例,它非常适用于中型项目。

这个 makefile 的巧妙之处在于它会自动为您确定依赖关系。您所要做的就是将 C/C++ 文件放入 src/ 文件夹中。

# 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)

<!--
TODO: This example fails initially because blah.d doesn't exist. I'm not sure how to fix this example, there are probably better ones out there..

Generating Prerequisites Automatically (Section 4.12)

Example requires: blah.c
Generating prereqs automatically
This makes one small makefile per source file
Notes:

  1. $$ is the current process id in bash. $$$$ is just $$, with escaping. We use it to make a temporary file, that doesn't interfere with others if there is some parallel builds going on.
  2. cc -MM outputs a makefile line. This is the magic that generates prereqs automatically, by looking at the code itself
  3. The purpose of the sed command is to translate (for example):
    main.o : main.c defs.h
    into:
    main.o main.d : main.c defs.h
  4. Running make clean will rerun the rm -f ... rule because the include line wants to include an up to date version of the file. There is such a target that updates it, so it runs that rule before including the file.
    # Run make init first, then run make
    # This outputs
    all: blah.d
    
    clean:
        rm -f blah.d blah.c blah.h blah.o blah
    
    %.d: %.c
        rm -f $@;    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$;     sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;     rm -f $@.$$$$
    
    init:
        echo "#include \"blah.h\"; int main() { return 0; }" > blah.c
        touch blah.h
    
    sources = blah.c
    
    include $(sources:.c=.d)
    

-->

Edit
Pub: 21 Jun 2025 08:53 UTC
Views: 85