Makefile教程1 快速入门

发布时间 2023-11-27 08:38:11作者: 磁石空杯

1 快速入门

1.1 为什么存在 Makefile?

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

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

1.2 Make有哪些替代?

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

Python、Ruby 和原始 avascript等解释性语言不需要与Makefile之类的东西。 Makefile的目标是根据已更改的文件来编译需要编译的任何文件。 解释语言中的文件发生更改时,不需要重新编译任何内容。 程序运行时,将使用该文件的最新版本。

1.3 Make的版本和类型

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

1.4 运行示例

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

hello:
	echo "Hello, World"

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

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

$ make
echo "Hello, World"
Hello, World

1.5 Makefile语法

生成文件语法
Makefile 由一组规则组成。 规则通常如下所示:

targets: prerequisites
	command
	command
	command
  • targets是文件名,以空格分隔。 通常每条规则只有一个。
  • 这些command是通常用于创建目标的一系列步骤。
  • prerequisites 也是文件名,以空格分隔。 在运行target的命令之前,这些文件需要存在。 这些也称为依赖项

1.6 Make的本质

让我们从hello world示例开始:

hello:
	echo "Hello, World"
	echo "This line will print if the file hello does not exist."
  • 我们有一个名为 hello 的目标
  • 该目标有两个命令
  • 该目标没有先决条件

然后我们将运行 make hello。 只要hello文件不存在,命令就会运行。 如果hello存在,则不会运行任何命令。

让我们创建更典型的Makefile:编译单个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会错误地猜测该文件没有更改,因此可以被忽略。

唷,真是拗口啊。 确保您理解这一点。 这是 Makefile 的关键,您可能需要几分钟才能正确理解。 如果事情仍然令人困惑,请尝试上面的示例或观看上面的视频。

参考资料

1.6 更多示例

以下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 # 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,所有三个目标都将重新运行。如果您运行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"

1.7 Make clean

clean经常被用作删除其他目标输出的目标,你可以运行make和make clean来创建和删除some_file。

clean在这里做了两件新事情:

  • 它不是第一目标(默认),也不是先决条件。这意味着除非你明确调用 make clean,否则它永远不会运行。
  • 它不是一个文件名。如果你碰巧有一个名为clean的文件,这个目标就不会运行,这不是我们想要的。
some_file: 
	touch some_file

clean:
	rm -f some_file

1.8 变量

变量只能是字符串。通常要使用 :=,但 = 也可以。

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

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