在我们编译和链接三个中等大小的文件的简单情况下,任何方法都可能同样令人满意。因此,我将考虑一般情况,但使用 Makefile 的许多好处只在大型项目中很重要。一旦我们学会了可以让我们掌握复杂案例的最佳工具,我们也希望在简单的案例中使用它。
make
让我强调一下使用而不是简单的 shell 脚本进行编译作业的“好处” 。但首先,我想做一个无害的观察。
shell 脚本的程序范式对于类似编译的作业是错误的
编写 Makefile 类似于编写一个稍微改变视角的 shell 脚本。在 shell 脚本中,我们描述了一个问题的程序解决方案:我们可以开始使用未定义的函数以非常抽象的术语来描述整个过程,然后我们细化这个描述,直到我们达到最基本的描述级别,其中一个过程只是一个普通的 shell 命令。在 Makefile 中,我们没有引入任何类似的抽象,但我们专注于我们想要生成的文件以及如何生成它们。这很有效,因为在 UNIX 中,一切都是文件,因此每个处理都是由一个程序完成的,该程序从输入文件中读取其输入数据,进行一些计算并将结果写入一些输出文件中。
如果我们想计算一些复杂的东西,我们必须使用很多输入文件,这些输入文件被程序处理,这些程序的输出被用作其他程序的输入,依此类推,直到我们生成包含结果的最终文件。如果我们将准备最终文件的计划翻译成 shell 脚本中的一堆过程,那么处理的当前状态是隐含的:计划执行者知道“它在哪里”,因为它正在执行一个给定的过程,它隐含地保证这样那样的计算已经完成,也就是说,这样那样的中间文件已经准备好了。现在,哪些数据描述了“计划执行者在哪里”?
无害观察 描述“计划执行者在哪里”的数据正是已经准备好的中间文件的集合,而这正是我们在编写Makefile时明确的数据。
这种无害的观察实际上是 shell 脚本和 Makefile 之间的概念差异,它解释了 Makefile 在编译作业和类似作业中优于 shell 脚本的所有优点。当然,要充分发挥这些优势,我们必须编写
正确的Makefile,这对初学者来说可能很难。
Make 使继续中断的任务变得容易
当我们使用 Makefile 描述编译作业时,我们可以轻松地中断它并在以后恢复它。这是
无害观察的结果。只有在 shell 脚本中付出相当大的努力才能实现类似的效果,而它只是内置于
make
.
Make 可以轻松处理项目的多个构建
您观察到 Makefile 会使源代码树与目标文件杂乱无章。但实际上可以对 Makefile 进行参数化以将这些目标文件存储在专用目录中。我为bsdmake使用BSD Owl
宏并使用
MAKEOBJDIR='/usr/home/michael/obj${.CURDIR:S@^/usr/home/michael@@}'
以便所有目标文件都结束~/obj
并且不会污染我的来源。有关更多详细信息,请参阅此
答案
。
高级 Makefile 允许我们同时拥有多个目录,其中包含具有不同编译选项的项目的多个构建。例如,启用不同的功能,或调试版本等。这也是
Makefile 实际上是围绕中间文件集表达的无害观察的结果。这种技术在BSD Owl 的测试套件中得到了说明。
Make 使构建并行化变得容易
我们可以轻松地并行构建程序,因为这是许多版本的标准功能make
。这也是
无害观察的结果:因为“计划执行者所在的位置”是 Makefile 中的明确数据,因此可以对其make
进行推理。在 shell 脚本中实现类似的效果需要付出很大的努力。
make
只有正确指定了依赖项,任何版本的并行模式才能正常工作。这可能实现起来相当复杂,但是bsdmake具有从字面上消除问题的功能。它被称为
META模式。它使用编译作业的第一次非并行传递,通过监视文件访问来计算
实际依赖关系,并在以后的并行构建中使用此信息。
Makefile 易于扩展
由于特殊的视角——也就是说,作为无害观察的另一个结果——用于编写 Makefile,我们可以通过挂钩到我们构建系统的各个方面来轻松扩展它们。
例如,如果我们决定所有的数据库 I/O 样板代码都应该由自动工具编写,我们只需在 Makefile 中写入自动工具应该使用哪些文件作为编写样板代码的输入。不多不少,不多不少。我们可以在我们喜欢的地方添加这个描述,make
无论如何都会得到它。在 shell 脚本构建中做这样的扩展会比必要的更难。
这种可扩展性的便利性是对 Makefile 代码重用的巨大激励。