Makefile详解

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 和 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 无法正确解析文件并报错。

下面是运行结果:

$ make
echo "Hello, world!"
Hello, world!
Makefile 语法

Makefile 由一系列规则组成,每个规则通常以如下格式表示:

targets: prerequisites
	command
	command
	command

其中:

target: 这是你想要生成的文件(或其他结果)。它可以是单个文件,也可以是一组文件,使用通配符表示。
prerequisites: 这些是 target 生成的必要文件。如果任何 prerequisite 比 target 更新,则该规则需要运行以重新生成 target。
command: 这是实际用于生成 target 的命令列表。每个命令以一个 tab 字符开头。可以有多个命令组成 recipe。

例如,以下规则将编译一个 C++ 文件 main.cpp 并生成可执行文件 main:

main: main.cpp
g++ -o main main.cpp

在这个例子中:

target 是 main 可执行文件。
prerequisite 是 main.cpp 源文件。
recipe 是使用 g++ 编译器编译 main.cpp 并生成 main 可执行文件的命令。

接下来我们将运行 make main 命令。只要 main 文件不存在,就会执行相关命令。如果 main 文件已经存在,则不会执行任何命令。

需要注意的是,我将main同时作为目标和文件来讨论。这是因为两者是直接关联的。通常,当运行一个目标(即执行其命令)时,这些命令会创建一个与目标同名的文件。在本例中,main 目标会创建main文件。

让我们创建一个更典型的 Makefile,用于编译单个 C 文件。 但是,在这样做之前,请创建一个名为 blah.c 的文件,并添加以下内容:

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

然后创建一个名为 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 时,将发生以下一系列步骤:

  1. 选择第一个目标,因为它是默认目标。
  2. 检查先决条件:blah.c。
  3. Make 决定是否需要运行 blah 目标。只有当 blah 不存在,或者 blah.c 比 blah 更新时,才会运行。

这一步是关键,也是 Make 的核心所在。 它试图判断自上次编译 blah 以来,其先决条件(blah.c)是否已发生变化。也就是说,如果修改了 blah.c,运行 make 应该重新编译该文件。反之,如果 blah.c 没有改变,就不应该重新编译。

为了实现这一点,Make 使用文件系统时间戳作为判断文件是否改变的代理。 这是一个合理的方法,因为文件时间戳通常只有在文件被修改时才会改变。 但需要注意的是,并非总是如此。例如人为修改一个文件,然后将其修改时间戳改成一个旧的时间戳。在这种情况下,Make 会错误地猜测文件没有变化,因此可以忽略。

快速示例

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

  1. Make 选择目标 blah,因为它是第一个目标,也是默认目标。
  2. blah 依赖于 blah.o,所以 Make 会搜索 blah.o 目标。
  3. blah.o 依赖于 blah.c,所以 Make 会搜索 blah.c 目标。
  4. blah.c 没有依赖项,所以会运行 echo 命令。
  5. 然后运行 cc -c 命令,因为所有 blah.o 的依赖项都完成了。
  6. 最后运行顶部的 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
make clean

clean 通常用作清除其他目标输出的特殊目标,但它并不是 Make 中的特殊关键字。您可以运行 make 和 make clean 来创建和删除 some_file。

需要注意的是,clean 在这里做了两件新的事情:

  1. 它不是第一个目标(默认目标),也不是先决条件。这意味着除非您明确调用 make clean,否则它永远不会运行。
  2. 它不是一个文件名。如果您碰巧有一个名为 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 这样的命令中使用它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值