0

备注这是问题“<a href="https://stackoverflow.com/q/26026331">在 Makefile 中单独链接目标文件的目的是什么?”的变体?由 user4076675 采取略有不同的观点。另见相应的META 讨论

让我们考虑一个 C 项目的经典案例。编译器gcc能够一步编译和链接程序。然后,我们可以使用 shell 脚本轻松描述构建例程:

case $1 in
    build)  gcc -o test *.c;;
    clean)  rm -f test;;
esac
# This script is intentionally very brittle, to keep
# the example simple.

但是,用 描述构建过程似乎是惯用的Makefile,涉及将每个编译单元编译为目标文件并最终链接这些文件的额外步骤。相应的 GNU Makefile 将是:

.PHONY: all

SOURCES=$(wildcard *.cpp)
OBJECTS=$(SOURCES:.cpp=.o)

%.o: %.cpp
    g++ -c -o $@ $<

all: default
default: $(OBJECTS)
    g++ -o test $^

clean:
    rm -rf *.o

可以说,第二种解决方案比我们之前编写的简单 shell 脚本更复杂。这也是一个缺点,因为它会使源目录与目标文件混为一谈。那么,为什么我们用 Makefiles 而不是 shell 脚本来描述构建过程呢?在前面的例子中,这似乎是一个无用的并发症。

4

1 回答 1

2

在我们编译和链接三个中等大小的文件的简单情况下,任何方法都可能同样令人满意。因此,我将考虑一般情况,但使用 Makefile 的许多好处只在大型项目中很重要。一旦我们学会了可以让我们掌握复杂案例的最佳工具,我们也希望在简单的案例中使用它。

make让我强调一下使用而不是简单的 shell 脚本进行编译作业的“好处” 。但首先,我想做一个无害的观察。

shell 脚本的程序范式对于类似编译的作业是错误的

编写 Makefile 类似于编写一个稍微改变视角的 shell 脚本。在 shell 脚本中,我们描述了一个问题的程序解决方案:我们可以开始使用未定义的函数以非常抽象的术语来描述整个过程,然后我们细化这个描述,直到我们达到最基本的描述级别,其中一个过程只是一个普通的 shell 命令。在 Makefile 中,我们没有引入任何类似的抽象,但我们专注于我们想要生成的文件以及如何生成它们。这很有效,因为在 UNIX 中,一切都是文件,因此每个处理都是由一个程序完成的,该程序从输入文件中读取其输入数据,进行一些计算并将结果写入一些输出文件中。

如果我们想计算一些复杂的东西,我们必须使用很多输入文件,这些输入文件被程序处理,这些程序的输出被用作其他程序的输入,依此类推,直到我们生成包含结果的最终文件。如果我们将准备最终文件的计划翻译成 shell 脚本中的一堆过程,那么处理的当前状态是隐含的:计划执行者知道“它在哪里”,因为它正在执行一个给定的过程,它隐含地保证这样那样的计算已经完成,也就是说,这样那样的中间文件已经准备好了。现在,哪些数据描述了“计划执行者在哪里”?

无害观察 描述“计划执行者在哪里”的数据正是已经准备好的中间文件的集合,而这正是我们在编写Makefile时明确的数据。

这种无害的观察实际上是 shell 脚本和 Makefile 之间的概念差异,它解释了 Makefile 在编译作业和类似作业中优于 shell 脚本的所有优点。当然,要充分发挥这些优势,我们必须编写 正确的Makefile,这对初学者来说可能很难。

Make 使继续中断的任务变得容易

当我们使用 Makefile 描述编译作业时,我们可以轻松地中断它并在以后恢复它。这是 无害观察的结果。只有在 shell 脚本中付出相当大的努力才能实现类似的效果,而它只是内置于 make.

Make 可以轻松处理项目的多个构建

您观察到 Makefile 会使源代码树与目标文件杂乱无章。但实际上可以对 Makefile 进行参数化以将这些目标文件存储在专用目录中。我为BSD Owl 宏并使用

MAKEOBJDIR='/usr/home/michael/obj${.CURDIR:S@^/usr/home/michael@@}'

以便所有目标文件都结束~/obj并且不会污染我的来源。有关更多详细信息,请参阅此 答案

高级 Makefile 允许我们同时拥有多个目录,其中包含具有不同编译选项的项目的多个构建。例如,启用不同的功能,或调试版本等。这也是 Makefile 实际上是围绕中间文件集表达的无害观察的结果。这种技术在BSD Owl 的测试套件中得到了说明。

Make 使构建并行化变得容易

我们可以轻松地并行构建程序,因为这是许多版本的标准功能make。这也是 无害观察的结果:因为“计划执行者所在的位置”是 Makefile 中的明确数据,因此可以对其make进行推理。在 shell 脚本中实现类似的效果需要付出很大的努力。

make只有正确指定了依赖项,任何版本的并行模式才能正常工作。这可能实现起来相当复杂,但是具有从字面上消除问题的功能。它被称为 META模式。它使用编译作业的第一次非并行传递,通过监视文件访问来计算 实际依赖关系,并在以后的并行构建中使用此信息。

Makefile 易于扩展

由于特殊的视角——也就是说,作为无害观察的另一个结果——用于编写 Makefile,我们可以通过挂钩到我们构建系统的各个方面来轻松扩展它们。

例如,如果我们决定所有的数据库 I/O 样板代码都应该由自动工具编写,我们只需在 Makefile 中写入自动工具应该使用哪些文件作为编写样板代码的输入。不多不少,不多不少。我们可以在我们喜欢的地方添加这个描述,make无论如何都会得到它。在 shell 脚本构建中做这样的扩展会比必要的更难。

这种可扩展性的便利性是对 Makefile 代码重用的巨大激励。

于 2014-12-04T11:41:28.800 回答