您对非递归制作的体验是什么?

时间:2009-02-17 23:36:42

标签: build-process makefile

几年前,我阅读了Recursive Make Considered Harmful论文,并在我自己的构建过程中实现了这个想法。最近,我读了另一篇关于如何implement non-recursive make的想法的文章。所以我有一些数据点,非递归make适用于至少几个项目。

但我对别人的经历感到好奇。您是否尝试过非递归make?它让事情变得更好还是更糟?是值得的吗?

8 个答案:

答案 0 :(得分:52)

我们在我工作的公司中使用非递归GNU Make系统。它基于米勒的论文,特别是你给出的“实现非递归制作”链接。我们设法将Bergen的代码改进为一个系统,在子系统makefile中根本没有锅炉板。总的来说,它运行良好,并且比我们以前的系统(使用GNU Automake完成的递归操作)要好得多。

我们支持所有“主要”操作系统(商业):AIX,HP-UX,Linux,OS X,Solaris,Windows,甚至是AS / 400大型机。我们为所有这些系统编译相同的代码,平台相关的部分被隔离到库中。

在我们的树中,大约有2000个子目录和20000个文件中有超过200万行C代码。我们认真考虑使用SCons,但却无法让它足够快地运行。在较慢的系统上,Python只需要在SCons文件中解析几十秒,其中GNU Make在大约一秒钟内做同样的事情。这是大约三年前的事情,所以事情可能已经改变了。请注意,我们通常将源代码保留在NFS / CIFS共享上,并在多个平台上构建相同的代码。这意味着构建工具扫描源树以进行更改的速度更慢。

我们的非递归GNU Make系统并非没有问题。以下是您可能遇到的一些最大障碍:

  • 让它可移植,特别是对Windows来说,是一项很多工作。
  • 虽然GNU Make几乎是一种可用的函数式编程语言,但它不适合大型编程。特别是,没有名称空间,模块或类似的东西可以帮助您将各个部分隔离开来。这可能会导致问题,尽管没有您想象的那么多。

我们旧的递归makefile系统的主要胜利是:

  • 。检查整个树(2k目录,20k文件)大约需要两秒钟,并确定它是最新的还是开始编译。旧的递归事情需要一分多钟才能完成。
  • 正确处理依赖关系。我们的旧系统依赖于订单子目录的构建等。就像您期望阅读米勒的论文一样,将整棵树视为一个单一的实体是解决这个问题的正确方法。
  • 经过我们所有努力工作后,它可以移植到我们所有支持的系统中。这很酷。
  • 抽象系统允许我们编写非常简洁的makefile。仅定义库的典型子目录只有两行。一行给出了库的名称,另一行列出了这个库所依赖的库。

关于上面列表中的最后一项。我们最终在构建系统中实现了一种宏扩展工具。子目录makefile列出了程序,子目录,库以及PROGRAMS,SUBDIRS,LIBS等变量中的其他常见内容。然后将其中的每一个扩展为“真正的”GNU Make规则。这使我们可以避免很多命名空间问题。例如,在我们的系统中,可以有多个具有相同名称的源文件,没有问题。

无论如何,这最终成了很多工作。如果您可以为您的代码获得SCons或类似代码,我建议您首先查看它。

答案 1 :(得分:29)

在阅读了RMCH论文后,我的目标是为当时正在处理的小项目编写一个正确的非递归Makefile。在我完成之后,我意识到应该可以创建一个通用的Makefile“框架”,它可以非常简单和简洁地告诉你想要构建什么样的最终目标,它们是什么样的目标(例如库或可执行文件)并且应该编译哪些源文件来制作它们。

经过几次迭代后,我最终创建了这个:一个样板文件,大约150行GNU Make语法的Makefile,从不需要任何修改 - 它只适用于任何我喜欢使用它的项目,并且灵活足以构建具有足够粒度的不同类型的多个目标,以指定每个源文件的精确编译标志(如果需要)和每个可执行文件的精确链接器标志。对于每个项目,我需要做的就是为它提供小的,单独的Makefile,其中包含与此类似的位:

TARGET := foo

TGT_LDLIBS := -lbar

SOURCES := foo.c baz.cpp

SRC_CFLAGS   := -std=c99
SRC_CXXFLAGS := -fstrict-aliasing
SRC_INCDIRS  := inc /usr/local/include/bar

如上所述的项目Makefile将完全符合您的期望:构建名为“foo”的可执行文件,编译foo.c(使用CFLAGS = -std = c99)和baz.cpp(使用CXXFLAGS = -fstrict -alasing)并将“./inc”和“/ usr / local / include / bar”添加到#include搜索路径,最终链接包括“libbar”库。它还会注意到有一个C ++源文件,并且知道使用C ++链接器而不是C链接器。该框架允许我指定比这个简单示例中显示的更多的内容。

样板文件Makefile执行构建指定目标所需的所有规则构建和自动依赖关系生成。所有构建生成的文件都放在一个单独的输出目录层次结构中,因此它们不与源文件混合(这是在不使用VPATH的情况下完成的,因此使多个源文件具有相同名称没有问题。)

我现在(重新)在至少二十几个我工作过的不同项目中使用了相同的Makefile。我最喜欢这个系统的一些东西(除了为任何新项目创建正确 Makefile的容易程度之外)是:

  • 。它几乎可以立即判断出是否有任何过时。
  • 100%可靠的依赖关系。并行构建将不可能神秘地破坏,并且它总是完全构建使所有内容恢复到最新状态所需的最小值。
  • 从不需要再次重写完整的makefile:D

最后我要提一下,由于递归制作中固有的问题,我认为我不可能把它拉下来。我可能注定要一遍又一遍地重写有缺陷的makefile,试图创建一个实际工作正常的文件。

答案 2 :(得分:18)

让我强调Miller的论文中的一个论点:当您开始手动解决不同模块之间的依赖关系并且很难确保构建顺序时,您实际上是重新实现构建系统在第一个中解决的逻辑地点。 构建可靠的递归make构建系统非常困难。现实生活中的项目有许多相互依赖的部分,其构建顺序非常重要,因此,这个任务应留给构建系统。但是,如果它具有系统的全局知识,它只能解决该问题。

此外,在多个处理器/内核上同时构建时,递归make构建系统容易崩溃。虽然这些构建系统似乎可以在单个处理器上可靠地工作,但是在您开始并行构建项目之前,许多缺少的依赖项都未被检测到。我使用了一个递归的make build系统,它可以处理多达四个处理器,但突然在一台带有两个四核的机器上崩溃了。然后我面临另一个问题:这些并发问题几乎无法调试,我最终绘制了整个系统的流程图,以找出问题所在。

回到你的问题,我发现很难想出为什么人们想要使用递归make的好理由。非递归GNU Make构建系统的运行时性能很难被击败,相反,许多递归make系统都存在严重的性能问题(弱并行构建支持也是问题的一部分)。有一个paper,我在其中评估了一个特定的(递归的)Make构建系统,并将其与SCons端口进行了比较。性能结果不具代表性,因为构建系统非常不标准,但在这种特殊情况下,SCons端口实际上更快。

结论:如果你真的想使用Make来控制你的软件构建,那就选择非递归Make,因为从长远来看,它会让你的生活变得更加容易。就个人而言,我宁愿使用SCons来实现可用性(或者Rake - 基本上任何使用现代脚本语言的构建系统,并且具有隐式依赖支持)。

答案 3 :(得分:10)

我对我之前的工作做了一个半心半意的尝试,使构建系统(基于GNU make)完全不是递归的,但我遇到了许多问题:

  • 工件(即构建的库和可执行文件)的源代码分布在多个目录中,依赖于vpath来查找它们
  • 几个同名的源文件存在于不同的目录中
  • 工件之间共享了几个源文件,通常使用不同的编译器标志进行编译
  • 不同的工件通常有不同的编译器标志,优化设置等。

简化非递归使用的GNU make的一个特性是特定于目标的变量值

foo: FOO=banana
bar: FOO=orange

这意味着当构建目标“foo”时,$(FOO)将扩展为“banana”,但是当构建目标“bar”时,$(FOO)将扩展为“orange”。

这样做的一个限制是不可能具有特定于目标的VPATH定义,即无法为每个目标单独唯一地定义VPATH。在我们的例子中,这是必要的,以便找到正确的源文件。

为支持非递归而需要GNU make的主要缺失功能是缺少命名空间。特定于目标的变量可以以有限的方式用于“模拟”命名空间,但您真正需要的是能够使用本地范围在子目录中包含Makefile。

编辑:在这种情况下,GNU make的另一个非常有用(通常未充分利用)的特性是宏扩展功能(例如,请参阅eval函数)。当你有几个目标具有相似的规则/目标但在使用常规GNU make语法无法表达的方式上有所不同时,这非常有用。

答案 4 :(得分:9)

我同意所引用文章中的陈述,但我花了很长时间才找到一个好的模板来完成所有这些并且仍然易于使用。

当前我正在从事一个小型研究项目,我正在尝试持续集成;在PC上自动进行单元测试,然后在(嵌入式)目标上运行系统测试。这在制作中是非常重要的,我已经寻找了一个很好的解决方案。发现make仍然是便携式多平台构建的一个很好的选择,我终于在http://code.google.com/p/nonrec-make

找到了一个很好的起点

这真是一种解脱。现在我的makefile是

  • 修改非常简单(即使知识有限)
  • 快速编译
  • 毫不费力地完全检查(.h)依赖关系

我当然也会将它用于下一个(大)项目(假设是C / C ++)

答案 5 :(得分:7)

我知道至少有一个大型项目(ROOT),其中advertises using [powerpoint link]机制在Recursive Make Considered Harmful中描述。该框架超过一百万行代码并且编写得非常巧妙。


当然,我使用使用递归make的所有大项目都很难编译。 ::叹气::

答案 6 :(得分:7)

我为一个中型C ++项目开发了一个非递归make系统,该系统适用于类Unix系统(包括mac)。此项目中的代码全部位于以src /目录为根的目录树中。我想编写一个非递归系统,在该系统中可以键入" make all"从顶级src /目录的任何子目录中,以编译以工作目录为根的目录树中的所有源文件,如在递归make系统中。因为我的解决方案似乎与我看到的其他解决方案略有不同,我想在此描述一下,看看我是否得到任何反应。

我的解决方案的主要内容如下:

1)src / tree中的每个目录都有一个名为sources.mk的文件。每个这样的文件都定义了一个makefile变量,该变量列出了以该目录为根的树中的所有源文件。此变量的名称的格式为[directory] ​​_SRCS,其中[directory]表示从顶级src /目录到该目录的路径的规范化形式,反斜杠替换为下划线。例如,文件src / util / param / sources.mk定义了一个名为util_param_SRCS的变量,该变量包含src / util / param及其子目录中所有源文件的列表(如果有)。每个sources.mk文件还定义了一个名为[directory] ​​_OBJS的变量,该变量包含相应目标文件* .o目标的列表。在包含子目录的每个目录中,sources.mk包含每个子目录中的sources.mk文件,并连接[子目录] _SRCS变量以创建自己的[目录] _SRCS变量。

2)所有路径都在sources.mk文件中表示为绝对路径,其中src /目录由变量$(SRC_DIR)表示。例如,在文件src / util / param / sources.mk中,文件src / util / param / Componenent.cpp将列为$(SRC_DIR)/util/param/Component.cpp。 $(SRC_DIR)的值未在任何sources.mk文件中设置。

3)每个目录还包含一个Makefile。每个Makefile都包含一个全局配置文件,它将变量$(SRC_DIR)的值设置为根src /目录的绝对路径。我选择使用绝对路径的符号形式,因为这似乎是在多个目录中创建多个makefile的最简单方法,它们以相同的方式解释依赖关系和目标的路径,同时仍然允许移动整个源树,如果需要的话,通过在一个文件中更改$(SRC_DIR)的值。此值由一个简单的脚本自动设置,当从git存储库下载或克隆包时,或者在移动整个源树时,指示用户运行该脚本。

4)每个目录中的makefile包含该目录的sources.mk文件。 "所有"每个这样的Makefile的目标列出该目录的[directory] ​​_OBJS文件作为依赖项,因此需要编译该目录及其子目录中的所有源文件。

5)编译* .cpp文件的规则为每个源文件创建一个依赖文件,后缀为* .d,作为编译的副作用,如下所述:http://mad-scientist.net/make/autodep.html。我选择使用gcc编译器生成依赖项,使用-M选项。即使使用其他编译器来编译源文件,我也使用gcc进行依赖生成,因为gcc几乎总是在类似unix的系统上可用,并且因为这有助于标准化构建系统的这一部分。可以使用不同的编译器来实际编译源文件。

6)对_OBJS和_SRCS变量中的所有文件使用绝对路径要求我编写脚本来编辑gcc生成的依赖文件,这会创建具有相对路径的文件。我为此目的编写了一个python脚本,但另一个人可能已经使用了sed。生成的依赖项文件中的依赖项路径是文字绝对路径。这在这种情况下很好,因为依赖文件(与sources.mk文件不同)是在本地生成的,而不是作为包的一部分进行分发。

7)每个导演中的Makefile包含来自同一目录的sources.mk文件,并包含一行" -include $([directory] ​​_OBJS:.o = .d)"尝试在目录及其子目录中包含每个源文件的依赖项文件,如上面给出的URL中所述。

我见过的其他方案之间的主要区别是允许"使所有"从任何目录调用的是使用绝对路径,以允许在从不同目录调用Make时一致地解释相同的路径。只要使用变量表示这些路径来表示顶级源目录,这不会阻止移动源树,并且比实现相同目标的一些替代方法更简单。

目前,我的这个项目的系统始终是"就地" build:通过编译每个源文件生成的目标文件与源文件放在同一目录中。通过更改编辑gcc依赖项文件的脚本来直接启用场外构建,以便用表示表达式中的构建目录的变量$(BUILD_DIR)替换src / dirctory的绝对路径。每个目标文件的规则中的目标文件目标。

到目前为止,我发现这个系统易于使用和维护。所需的makefile片段很短,并且相对容易让协作者理解。

我开发此系统的项目是使用完全独立的ANSI C ++编写的,没有外部依赖性。我认为这种自制的非递归makefile系统是自包含,高度可移植代码的合理选择。我会考虑一个更强大的构建系统,如CMake或gnu autotools,但是,对于任何对外部程序或库或非标准操作系统功能具有重要依赖性的项目。

答案 7 :(得分:0)

我写了一个不太好的非递归make构建系统,从那时起,一个非常干净的模块化递归make构建系统,用于名为Pd-extended的项目。它基本上类似于包含一堆库的脚本语言。现在我也在使用Android的非递归系统,所以这是我对这个主题的思考的背景。

我真的不能说两者之间的性能差异,我没有真正关注,因为完全构建实际上只在构建服务器上完成。我通常使用核心语言或特定的库,所以我只对构建整个包的子集感兴趣。递归制作技术具有使构建系统既独立又集成到更大整体中的巨大优势。这对我们很重要,因为我们想要为所有库使用一个构建系统,无论它们是集成在外部作者中还是由外部作者编写。

我现在正致力于构建Android内部的自定义版本,例如基于SQLCipher加密的sqlite的Android的SQLite类版本。因此,我必须编写非递归的Android.mk文件,这些文件包含各种奇怪的构建系统,如sqlite。我无法弄清楚如何让Android.mk执行任意脚本,而根据我的经验,这在传统的递归make系统中很容易。