你的非recursion的经验是什么?
几年前,我阅读了recursion考虑有害论文,并在我自己的构build过程中实现了这个想法。 最近,我读了另外一篇关于如何实现非recursion构造的文章 。 所以我有一些非recursion的数据点可以用于至less一些项目。
但我很好奇别人的经验。 你尝试过非recursion吗? 它是让事情变好还是变坏? 这是值得的时间?
我在我工作的公司使用非recursion的GNU Make系统。 这是基于米勒的论文,特别是您提供的“实现非recursion制作”链接。 我们已经设法将卑尔根的代码改进到子目录makefile中没有任何锅炉板的系统中。 总的来说,它工作正常,比我们以前的系统(用GNU Automake做的一个recursion的事情)要好得多。
我们支持所有“主要”操作系统(商业):AIX,HP-UX,Linux,OS X,Solaris,Windows甚至AS / 400主机。 我们为所有这些系统编译相同的代码,将依赖于平台的部分分离到库中。
在我们的树中,有大约2000个子目录和20000个文件中有200多万行C代码。 我们认真考虑使用SCons,但是不能使它工作得足够快。 在较慢的系统上,Python会用几秒钟的时间parsingSCONS文件,GNU Make在大约一秒钟完成同样的事情。 这是大约三年前,所以事情可能已经改变。 请注意,我们通常将源代码保留在NFS / CIFS共享上,并在多个平台上构build相同的代码。 这意味着构build工具扫描源代码树以进行更改甚至更慢。
我们的非recursionGNU Make系统不是没有问题的。 以下是您可能遇到的一些最大的障碍:
- 使它可移植,尤其是Windows,是很多工作。
- 虽然GNU Make几乎是一种可用的函数式编程语言,但它不适合在大型编程中使用。 特别是,没有命名空间,模块或类似的东西来帮助你隔离各个部分。 这可能会导致问题,尽pipe不如你想象的那样多。
我们以前的recursionmakefile系统的主要胜利是:
- 速度很快 检查整个树(2k目录,20k文件)需要大约两秒的时间,并且可以决定它是最新的还是开始编译。 旧的recursion的事情将花费一分多钟无所事事。
- 它正确处理依赖关系。 我们的旧系统依赖于子目录的build立顺序等等。正如你从阅读米勒的论文期望的那样,把整棵树看成一个单一的实体是解决这个问题的正确方法。
- 在我们投入的所有努力之后,它可以移植到我们所有支持的系统中。 它太酷了。
- 抽象系统允许我们写出非常简洁的makefile。 定义一个库的典型子目录只有两行。 一行给出了库的名称,另一行列出了这些依赖的库。
关于上面列表中的最后一项。 我们最终在构build系统中实现了一种macros观扩展工具。 子目录makefiles列出程序,子目录,库,以及像PROGRAMS,SUBDIRS,LIBS这样的variables中的其他常见事物。 然后,这些扩展成“真正的”GNU Make规则。 这使我们可以避免大部分的命名空间问题。 例如,在我们的系统中可以有多个同名的源文件,没问题。
无论如何,这最终成了很多工作。 如果你能得到SCons或类似的代码工作,我build议你先看看。
在阅读RMCH论文后,我着手为我当时正在开发的一个小型项目编写一个适当的非recursionMakefile。 完成之后,我意识到应该可以创build一个通用的Makefile“框架”,它可以用来简单明了地告诉你想要build立的最终目标,它们是什么types的目标(例如库或可执行文件)以及应该编译哪些源文件才能生成它们。
经过几次迭代之后,我最终创build了这样一个模板:一个包含约150行GNU Make语法的样板Makefile,它永远不需要任何修改 – 它适用于任何我喜欢使用它的项目,并且足够灵活多个具有足够粒度的不同types的目标,为每个源文件指定精确的编译标志(如果需要的话)以及每个可执行文件的精确链接器标志。 对于每个项目,我所需要做的就是为它提供小的,独立的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将完成你所期望的:构build名为“foo”的可执行文件,编译foo.c(使用CFLAGS = -std = c99)和baz.cpp(使用CXXFLAGS = -fstrict-aliasing)并在#include
searchpath中添加“./inc”和“/ usr / local / include / bar”,最终链接包括“libbar”库。 它也会注意到有一个C ++源文件,并知道使用C ++连接器而不是C连接器。 在这个简单的例子中,框架允许我指定更多的内容。
样板文件Makefile执行构build指定目标所需的所有规则构build和自动依赖关系生成。 所有生成生成的文件被放置在一个单独的输出目录层次结构中,所以它们不与源文件混合在一起(并且不使用VPATH,因此多个源文件具有相同的名称没有问题)。
我现在(重新)使用这个相同的Makefile至less有二十多个不同的项目,我一直在工作。 我最喜欢这个系统的一些东西(除了为任何新项目创build一个合适的 Makefile是多么容易):
- 速度很快 它几乎可以即时判断是否有任何过时的事情。
- 100%可靠的依赖关系。 平行构build的机会几乎没有任何突破,而且它总是build立起将所有内容都恢复到最新所需的最低限度。
- 我将永远不需要重写一个完整的makefile:D
最后,我只想提一下,在recursion制造中固有的问题,我不认为我有可能把它取消。 我可能注定要一遍又一遍地重写有缺陷的makefile,妄图创build一个实际上正常工作的文件。
让我强调米勒的论文的一个论点:当你开始手动解决不同模块之间的依赖关系,并且很难确保构build顺序时,你首先有效地重新实现了构build系统所要解决的逻辑。 构build可靠的recursion使构build系统非常困难。 现实生活中的项目有许多相互依存的部分,其构build顺序不是微不足道的,因此,这个任务应该留给构build系统。 但是,只有全面了解制度,才能解决这个问题。
此外,在多处理器/内核上同时构build时,recursion使构build系统容易崩溃。 虽然这些构build系统似乎可以在单个处理器上可靠地工作,但是直到您开始并行构build项目时,才会检测到许多缺失的依赖关系。 我已经使用了一个在最多四个处理器上工作的recursion构build系统,但是突然在两个四核的机器上崩溃。 然后我又面临另一个问题:这些并发问题几乎不可能debugging,最后我绘制出整个系统的stream程图,找出问题所在。
回到你的问题,我觉得很难想出为什么要使用recursion的好理由。 非recursionGNU Make构build系统的运行时性能很难被打败,相反,许多recursionmake系统有严重的性能问题(弱并行构build支持也是问题的一部分)。 有一篇论文评估了一个特定的(recursion)Make构build系统,并将其与SCons端口进行了比较。 性能结果并不具有代表性,因为构build系统非常不规范,但在这个特殊情况下,SCons端口实际上更快。
底线:如果你真的想使用Make来控制你的软件构build,那就去做非recursionMake吧,因为它使得你的生活从长远来看变得更加容易。 就个人而言,我宁愿使用SCons出于可用性的原因(或Rake – 基本上任何使用现代脚本语言的构build系统,并具有隐式依赖性支持)。
我在前一份工作中做了一个半心半意的尝试,使构build系统(基于GNU make)完全不recursion,但是遇到了一些问题:
- 工件(即构build的库和可执行文件)的源码分布在多个目录中,依靠vpath来查找
- 多个具有相同名称的源文件存在于不同的目录中
- 工件之间共享几个源文件,通常用不同的编译器标志编译
- 不同的工件往往有不同的编译器标志,优化设置等
GNU make的一个简化非recursion使用的特性是目标特定的variables值 :
foo: FOO=banana bar: FOO=orange
这意味着当build立目标“foo”时,$(FOO)将扩展为“banana”,但是在构build目标“bar”时,$(FOO)将扩展为“orange”。
其中一个限制是,不可能具有特定于目标的VPATH定义,即无法为每个目标单独定义VPATH。 这在我们的情况下是必要的,以便find正确的源文件。
为了支持非recursion,GNU make所缺less的主要function是它缺less名称空间 。 特定于目标的variables可以用有限的方式来“模拟”命名空间,但是你真正需要的是能够使用本地作用域将Makefile包含在子目录中。
编辑:在这种情况下GNU make的另一个非常有用的(而且经常被使用的)特性是macros扩展设施(例如参见eval函数)。 当你有几个具有类似的规则/目标的目标,但是不能用常规的GNU make语法expression的方式不同时,这是非常有用的。
我同意所引用的文章中的陈述,但是花了很长时间才find一个好的模板,它能够完成所有这些工作,并且仍然易于使用。
目前我正在研究一个小型的研究项目,我正在尝试持续集成。 在pc上自动进行unit testing,然后在(embedded式)目标上运行系统testing。 这是不平凡的,我已经寻找一个好的解决scheme。 发现这种做法仍然是便携式多平台构build的不错select我终于在http://code.google.com/p/nonrec-makefind了一个很好的起点;
这是一个真正的解脱。 现在我的makefile是
- 非常简单的修改(即使有限的知识)
- 快速编译
- 完全检查(.h)依赖关系,不费力气
我当然也会用它来做下一个(大的)项目(假设C / C ++)
我知道至less有一个大规模项目( ROOT ),它使用[Powerpoint链接]中的Recursive Make Considered Harmful中描述的机制进行广告 。 该框架超过了一百万行代码,编译相当巧妙。
而且,当然,我所使用的所有大型项目都使用recursion构build,编译起来非常缓慢。 ::叹::
我为一个中等规模的C ++项目开发了一个非recursion的make系统,该项目旨在用于类似unix的系统(包括mac)。 这个项目中的代码全部位于一个以src /目录为根的目录树中。 我想编写一个非recursion系统,在其中可以从顶级src /目录的任何子目录中键入“make all”,以便编译以工作目录为根的目录树中的所有源文件,如在一个recursion的make系统中。 因为我的解决scheme似乎与我见过的其他解决scheme稍有不同,所以我想在这里描述一下,看看是否有任何反应。
我的解决scheme的主要内容如下:
1)src / tree中的每个目录都有一个名为sources.mk的文件。 每个这样的文件都定义了一个makefilevariables,该variables列出了以该目录为根的树中的所有源文件。 这个variables的名字是[directory] _SRCS的forms,其中[directory]代表从顶层src /目录到该目录的规范化forms,反斜杠被下划线替代。 例如,src / util / param / sources.mk文件定义了一个名为util_param_SRCS的variables,其中包含src / util / param及其子目录中所有源文件的列表(如果有的话)。 每个sources.mk文件还定义了一个名为[directory] _OBJS的variables,其中包含相应的目标文件* .o目标的列表。 在包含子目录的每个目录中,sources.mk包含每个子目录的sources.mk文件,并连接[子目录] _SRCSvariables以创build自己的[目录] _SRCSvariables。
2)所有path都以sources.mk文件的forms表示为绝对path,其中src /目录由variables$(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都包含一个全局configuration文件,该文件将variables$(SRC_DIR)的值设置为根src /目录的绝对path。 我select使用绝对path的符号forms,因为这似乎是在多个目录中创build多个生成文件的最简单方法,它们将以相同方式解释依赖关系和目标path,同时仍然允许在需要时移动整个源树通过在一个文件中更改$(SRC_DIR)的值。 这个值是由一个简单的脚本自动设置的,当从git仓库下载或克隆软件包时,或者整个源码树被移动时,指示用户运行该脚本。
4)每个目录中的makefile包含该目录的sources.mk文件。 每个这样的Makefile的“all”目标将该目录的[directory] _OBJS文件列为依赖项,因此需要编译该目录及其子目录中的所有源文件。
5)编译* .cpp文件的规则为每个源文件创build一个依赖文件,后缀为* .d,作为编译的副作用,如下所述: http : //mad-scientist.net/make/ autodep.html 。 我select使用gcc编译器生成依赖关系,使用-M选项。 即使使用其他编译器来编译源文件,也使用gcc来生成依赖关系,因为gcc几乎总是在类Unix系统上可用,因为这有助于标准化构build系统的这一部分。 可以使用不同的编译器来实际编译源文件。
6)为_OBJS和_SRCSvariables中的所有文件使用绝对path,我需要编写一个脚本来编辑由gcc生成的依赖文件,从而创build具有相对path的文件。 我为此写了一个python脚本,但另一个人可能已经使用了sed。 生成的依赖文件中的依赖path是文字绝对path。 在这种情况下,这很好,因为依赖文件(与sources.mk文件不同)是在本地生成的,而不是作为包的一部分分发。
7)每个director中的Makefile包含来自同一目录的sources.mk文件,并且包含一行“-include $([directory] _OBJS:.o = .d)”,它试图为每个源文件包含一个依赖文件在目录及其子目录中,如上面给出的URL中所述。
我所看到的允许从任何目录调用“make all”的其他scheme之间的主要区别在于,当从不同的目录调用Make时,使用绝对path来允许一致地解释相同的path。 只要这些path用一个variables来表示顶级源代码目录,这并不妨碍移动源代码树,并且比实现相同目标的一些替代方法更简单。
目前,我的这个项目的系统总是做一个“in-place”的构build:编译每个源文件产生的目标文件和源文件放在同一个目录下。 通过改变编辑gcc依赖文件的脚本,以便通过variables$(BUILD_DIR)replacesrc / dirctory的绝对path,来代替构build目录每个目标文件的规则中的目标文件目标。
到目前为止,我发现这个系统易于使用和维护。 所需的makefile片段比较简单,比较容易让协作者理解。
我开发这个系统的项目是用完全独立的ANSI C ++编写的,没有外部的依赖关系。 我认为这种自制的非recursionmakefile系统是一个自包含的,高度可移植的代码的合理select。 我会考虑一个更强大的构build系统,比如CMake或者gnu autotools,但是对于任何对外部程序或库有依赖性的项目或者非标准的操作系统特性。
我写了一个不是很好的非recursionmake构build系统,从那以后,一个非常干净的模块化recursion为一个名为Pd-extended的项目构build系统。 它基本上就像一个包含一堆库的脚本语言。 现在我也在使用Android的非recursion系统,所以这是我对这个主题的思考的上下文。
对于两者之间的性能差异我没有太多的说明,我没有真正关注,因为完整的构build实际上只在构build服务器上完成。 我通常在核心语言或特定的图书馆工作,所以我只对构build整个包的子集感兴趣。 recursion制造技术具有将构build系统独立并整合到一个更大的整体中的巨大优势。 这对我们很重要,因为我们要为所有图书馆使用一个构build系统,无论它们是由外部作者集成还是由其写作。
我现在正在构build定制的Android内部版本,例如基于SQLCipherencryption的sqlite的Android的SQLite类的一个版本。 所以我必须编写非recursion的Android.mk文件,这些文件包装了各种奇怪的构build系统,比如sqlite的。 我无法弄清楚如何让Android.mk执行一个任意的脚本,而在传统的recursion制作系统中,这是很容易的,从我的经验来看。