Makefiles 教程

入门指南

为什么会有 Makefile?

Makefile 的存在,是为了帮助判断一个大型程序中哪些部分需要重新编译。在绝大多数情况下,Makefile 用于编译 C 或 C++ 文件。其他编程语言通常都有类似于 Make 的工具来完成类似的工作。事实上,Make 不仅限于代码编译;当你需要根据文件变化来执行一系列操作时,它也非常有用。

本教程将聚焦于 Make 在 C/C++ 编译场景下的使用。

下面是一个你可能会用 Make 构建的依赖关系图示例。如果某个文件的依赖发生了变化,那么该文件就会被重新编译:

dependency_graph.png

Make 的替代方案有哪些?

在 C/C++ 领域,常见的替代构建系统包括 SCons、CMake、Bazel 和 Ninja。一些代码编辑器,比如 Microsoft Visual Studio,也内置了自己的构建工具。

对于 Java,有 Ant、Maven 和 Gradle 这样的构建工具。而其他语言,比如 Go、Rust 和 TypeScript,也都有各自专属的构建系统。像 Python、Ruby 和原生 JavaScript 这样的解释型语言则通常不需要类似 Makefile 的东西。Makefile 的主要目的是根据哪些文件发生了变化来决定需要编译哪些文件。但对于解释型语言来说,当文件发生变化时,并不需要重新编译——程序在运行时会自动使用最新的源文件。

Make 的版本和类型

Make 有多种不同的实现,但本指南中的内容基本适用于你正在使用的任何版本。不过,它是专门为 GNU Make 编写的,这是 Linux 和 macOS 上的标准实现。文中的所有示例都兼容 Make 的第 3 版和第 4 版,这两者在功能上几乎没有差别,只有一些少见的细节差异。

运行示例

要运行这些示例,你需要一个终端,并确保已安装 make 工具。对于每个示例,把内容保存到一个名为 Makefile 的文件中,然后在该目录下运行命令 make 即可。我们先从最简单的 Makefile 示例开始:

hello:
    echo "Hello, World"

tips: Makefile 中的缩进必须使用 Tab 键,不能使用空格,否则 make 命令会执行失败。

这是运行上面那个简单示例后的输出结果:

$ make
echo "Hello, World"
Hello, World

Makefile 语法简介

一个 Makefile 是由一组 规则(rules) 组成的。每条规则通常看起来像这样:

targets: prerequisites
    command
    command
    command
  • 目标是文件名,用空格分隔。通常每条规则只包含一个目标。
  • 命令是一系列通常用于生成目标的步骤。这些命令行的开头必须使用 Tab 字符,而不能使用空格。
  • 先决条件(prerequisites)也是文件名,用空格分隔。这些文件在执行目标的命令之前必须存在,它们也被称为依赖(dependencies)。

Make 的本质

我们从一个简单的 Hello World 示例开始:

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

这里有很多内容需要理解,我们来逐步解析:

  • 我们有一个名为 hello 的目标(target)。
  • 这个目标有两个命令。
  • 这个目标没有先决条件(prerequisites)。

接下来我们运行 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.cblah 文件更新时,Make 才会运行。

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

为了实现这一点,make 使用文件系统的时间戳作为判断是否有变更的依据。这是一种合理的启发式方法,因为文件的时间戳通常只有在文件被修改时才会更新。但需要注意的是,这并不总是准确的。比如你可以修改一个文件,然后将它的修改时间戳改成一个较早的时间。如果你这么做了,make 就会错误地判断该文件没有被修改,因此会忽略它。

呃呃呃,这一段信息量很大。请务必理解这一点——它是 Makefile 的核心内容,可能需要你花几分钟时间才能真正掌握。如果你还感到困惑,可以多尝试上面的例子。

更多快速示例

下面这个 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 程序。
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 从未被创建过。

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 来解决这个问题。
some_file: 
    touch some_file

clean:
    rm -f some_file

变量

变量在 Makefile 中只能是字符串。通常你会想使用 :=,但 = 也可以使用。详见后续的“变量(第二部分)”。

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

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 这样的命令中是需要引号的。

在这个示例中,这两个命令的行为是一样的:

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

引用变量时可以使用 ${}$() 这两种方式。

x := dude

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

    # Bad practice, but works
    echo $x 

目标(Targets)

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 $@
# Equivalent to:
# f1.o:
#    echo f1.o
# f2.o:
#    echo f2.o

自动变量和通配符

* 通配符

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

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

% 通配符

% 非常有用,但由于它可以在多种情况下使用,因此有些令人困惑。

  • 当用于“匹配”模式时,它匹配字符串中的一个或多个字符。这个匹配被称为“词干”(stem)。
  • 当用于“替换”模式时,它会将匹配到的词干替换到字符串中。
  • % 最常用于规则定义和一些特定的函数中。

自动变量

有很多自动变量,但通常只有一些会出现:

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

......持续更新

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容