基于make的构建环境要正确工做, 一个很重要(也很烦人)的任务是, 在makefile中正确列
举依赖.html
这个文档将介绍了一个很是有用的让make自身来建立和维护这些依赖的方法.node
文章来源shell
全部的make程序都须要知道, 某个特定的target依赖的文件有哪些, 以便确认它(target)
会在必要的时候进行rebuild.函数
手动更行这个清单不只仅是让人乏味, 并且很是容易出错. 多数系统(不论大小)都偏向与
提供自动提取这个信息的自动化工具. 传统的工具的是makedepend
程序, 其会读取c源代
码, 并以能够include至makefile中的__目标-依赖__模式生成头文件清单.工具
若是使用更增强大一点的编译器或者预处理器, 更加现代话的解决方案是让编译器或者预
处理器来生成这个信息.post
这篇文章的意图不是专门讨论依赖信息得到的方式的(尽管有涉及到), 而是, 介绍一些有
用的将这些工具的调用,输出和gnu make组合, 来确保依赖信息老是正确和最新的, 衔接越
紧密(且越高效)越好.ui
这些方法依赖gnu make提供的特性. 可能能够经过修改它们来在其余版本的make上应用.
那就等你本身尝试啦. 可是, 在尽心那个尝试以前请看哈paul的makefile第一原则.net
若是有谁已近不耐烦了, 这是一个完整的最佳的实践方案. 这个方案须要你的编译器的支
持: 默认你使用gcc做为编译器(或者提供了和gcc兼容的预处理选项的编译器). 若是你的
编译器不知足这个条件, 请看另外的方案.unix
将这个加入到你的makefile环境中,(蓝色的部分是对gnu make提供的内建内容的改动). 当
然, 你能够却略不符合你须要的模式规则(或者添加你须要的, whatever).
(固然我这里并无蓝色...whatever)调试
depdir := .deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.d compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c %.o : %.c %.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
要注意, include
这一行须要出如今初始, 默认target以后, 不然引入的依赖会取代你的
默认target配置. 将这个加到makefile末尾是很好的(或者放在一个单独的makefile文件里
并include他)
还这, 这里认为srcs变量包含全部你想要跟踪依赖的源文件(不是头文件)
若是你只是先要知道这些改动的意义的话, 而且考虑一些问题和对它们的解决方案, 能够(看原文,..)
make depend
方法一个由来已久的处理依赖生成的方式是, 在makefiles中提供一个特殊的target, 一般是
depend, 其能够用于建立依赖信息. 这个target的命令会对xx文件调用一些依赖跟踪工具
..生成makefile格式的依赖信息.
若是你的make版本支持include, 你能够将它们(依赖输出)重定向到一个文件, 而后
include这个文件. 若是不支持的话, 一般还须要利用shell来将依赖列表追加到makefile
文件末尾...
这样虽然很简单, 可是存在很严重的问题. 首先也是最重要的是, 依赖只在使用者明确要
求更新的时候才更新, 若是使用者并无常常运行make depend
, 依赖可能会严重果实,
make就不能正确得rebuild target.. 所以, 咱们无法说这是无缝且正确的.
第二个问题是, 运行make depend
是不高效的, 特别是第一次. 由于它会修改makefile,
一般须要做为一个单独的构建步骤, 也就是在每一个子目录的每次make都须要额外调用一次
之类的, 除去依赖生成工具自身的开销不说. 还有, 它会检查每一个文件的依赖, 即便是没
有改变的文件
咱们会看看到咱们如何能够作到更好.
include
指令多数版本的make都支持某种类型的include指令(实际上, include
是最新的posix规范中
明确要求的).
你立刻就会看到为何这个会有用, 就好比避免上面的追加依赖信息到makefile中. 而在
gnu make的include处理中有更多有趣的能力...gnu make会尝试rebuild引入的makefile.
若是成功rebuild, gnu make会从新执行它本身类读入新版本的makefile.
这个自动重建的特性能够用于避免使用单独的make depend
步骤: 若是你将全部的源文件
做为包含依赖的文件先决条件, 而后将那个文件include到你的makefile, 则它会在每次有
源文件变更的时候重建. 这样的结果是, 依赖信息老是最新的, 使用者不须要明确运行
make depend
固然, 这意味每次有文件变更的时候全部的文件的依赖信息都会从新计算, 很遗憾. 咱们
还能够作得更好.
关于gnu make的自动重建特性的详细信息, 能够看gnu make的用户手册中"how makefiles are remade"一节
gnu make的用户手册中generating dependencies automatically
一节中介绍了一种处理自动依赖的方式.
在这个方式中, 或为每一个源文件建立一个单独的依赖文件(在咱们的例子中咱们会使用
basename加上.d
后缀做为文件). 这个文件包含了从那个源文件建立的target的一条依赖
, 提供生成target的先决条件.
这些依赖文件以后都会被makefile引入. 提供了一条描述依赖文件如何建立的隐式规则.
总的来讲, 差很少就是这样:
srcs = foo.c bar.c ... %.d : %.c $(makedepend) include $(srcs:.c=.d)
在这个例子中, 我会使用变量$(makedepend)
来表明你选择的用于建立依赖文件的方式.
这个变量的一些可能的值以后会介绍.
生成的依赖文件的格式是什么呢? 在这个简单的例子中, 咱们须要声明对象文件和依赖文
件都有相同的先决条件: 源文件和全部的头文件, 所以foo.d
文件可能会包含这个:
foo.o foo.d: foo.c foo.h bar.h baz.h
当gnu make读取这个makefile的时候, 在进行别的事情以前, 会尝试重建引入的makefile,
在这个例子中是后缀.d
的文件. 咱们有一条用于构建它们的规则, 而且依赖和构建.o
文件的依赖同样. 所以, 当任何改动致使原来的target过期的时候, 也会致使.d
文件被
重建.
所以, 当任何源文件或者引入的文件变更的时候, make或重建.d
文件, 从新执行它本身
来读入新的makefile, 而后继续构建, 此次用的是最新的, 正确的依赖列表.
这里咱们解决了前面的方案的两个问题. 首先, 使用者不须要作任何工做来更新依赖列表,
make本身会完成. 第二, 只更新实际改动的文件的依赖列表, 而非目录中的全部文件.
可是, 又有了三个新的问题. 首先是, 仍然不够高效, 虽然咱们只从新检查了改动的文件,
咱们仍然会在有变更的时候从新执行make, 对于大的构建系统会很慢.
第二个问题是仅仅是烦人: 当你新添加一个文件或者第一次构建, 不存在.d
文件. 当
make试图include的时候会发现它不存在, 他会生成一个warning. 以后gnu make会继续重
建.d
文件, 而后从新调用自身, 不致命, 可是烦人.
第三个问题更加严重: 若是你移除或者重命名了一个先决文件(好比c的.h
文件), make会
以至命错误推出, 抱怨target不存在:
make: *** no rule to make target 'bar.h', needed by 'foo.d'. stop.
这是由于.d
文件有make找不到的依赖. 没有先决文件的话无法重建.d
文件, 而它在重
建.d
文件以前不知道它不须要这个先决条件.
惟一的解决方案是手动介入并移除任何引用了缺失的文件的.d
文件, 一般所有移除会更
简单, 甚至能够建立一个clean-deps
目标或者相似的来自动作这个(..).说来这个确实是
够恼人的, 可是若是文件爱呢移除或者重命名不常发生, 可能就不是致命的了.
上面介绍的基础的方式是由tom tromey策划的, 他使用其做为fsf的automake工具的标准依
赖生成方式. 我(不是我)对其进行了一些改动来让它能够用于一个更加通常化的构建环境
中.
先解决上面的第一个问题: make的从新调用. 若是你想想的话, 这个从新调用真的是没
有必要的. 由于咱们知道target的一些先决条件变更了, 咱们必须重建构建target, 更新
依赖列表也不会影响这个决定. 咱们真正须要作的是确保先决条件列表在make的下次调用,
咱们再次须要决定是不是最新的时候.
由于在这个构建中不须要最新的先决条件列表, 咱们实际上能够彻底能够避免从新调用
make: 咱们可让先决条件列表在target重建的时候build. 换句话说, 咱们能够该百纳
target的构建规则来加入更新依赖文件的命令.
在这个例子中, 咱们必须很是当心, 咱们没有提供规则来自动都见依赖: 若是咱们提供了,
make仍然会尝试从新构建它们并从新执行: 这不是咱们想要的
如今咱们不关心不存在的依赖文件, 解决第二个问题(多余的warning)就很是简单了: 直接
使用gnu make的wildcard
函数, 不存在的依赖文件不会致使错误
看一个简单例子:
srcs = foo.c bar.c ... %.o : %.c @$(makedepend) $(compile.c) -o $@ $< include $(wildcard $(srcs:.c=.d))
这个要更加刁钻一些. 可是, 咱们能够经过在makefile中仅仅将文件做为target来讲服
make不要fail. 若是target存在, 可是没有命令(隐式或者显式)或者先决条件, 则make总
是认为它是最新的. 这就是正常的状况, 它会像咱们期待的那样工做.
在出现上述错误的例子中, target并不存在. 而根据gnu make用户手册"rules without
recipes or prerequisties":
若是一个规则没有先决条件或者recipe, 而且规则的target是不存在的文件, 那么每次
在它的规则运行的时候, make会认为这个target已近更新了. 这意味着全部依赖于这个
target的target老是会执行其recipe(生成这个target的命令组)
棒极了. 这确保了make不会丢出错误, 由于它知道如何处理那个不存在的文件, 它会确保
任何l以爱那个target的文件rebuild, 这也是咱们想要的.
(???)
所以, 咱们须要作的就是, 修改这个依赖文件输出, 使得每一个先决条件(源文件和头文件)
定义为没有命令和先决条件的target. 因此makedepend脚本的输出因该生成一个内容像这
样的foo.d
文件:
foo.o: foo.c foo.h bar.h baz.h foo.c foo.h bar.h baz.h:
所以.d
文件包含最开始的先决条件定义, 而后添加每一个源文件做为一个显式的target
这个配置还有一个问题: 若是使用者删除了一个依赖文件, 而没有更新任何源文件, make
不会发现任何问题, 而且不会从新建立依赖文件, 直到因为其余的缘由决定从新构建对应
的对象文件. 同时, make会缺失这些target的依赖信息(好比, 修改头文件而不改动源文件
不会致使对象文件重建)
这个问题稍微有点复杂, 由于咱们不想要依赖文件被看做是"真正的"target: 若是它们是,
则咱们使用include来引入它们, make会重建它们, 而后从新执行它本身. 这并不致命, 但
是是多余的, 咱们选择拒绝.
automake的方式并无解决则和个问题, 之前我提供了一个"just don't do that"的方案,
加上将依赖文件放到一个单独的目录来使得不那么容易碰巧删除了它们.
可是lukas waymann提供了一个简洁的解决方案: 将依赖文件做为target的依赖, 而后给它
建立一个空的recipe:
srcs = foo.c bar.c ... %.o : %.c %.d @$(makedepend) $(compile.c) -o $@ $< %.d: ; include $(wildcard $(srcs:.c=.d))
这很是好地解决了问题: 当make检查target的时候, 他会将依赖文件爱呢看做是一个先决
条件, 而后尝试rebuild它. 若是它存在, 什么都不会作, 由于依赖文件没有先决条件. 如
果它不存在, 则会被标记为过期, 由于它的recipe是空的, 这会致使object target被重建
(其重建过程当中会建立一个新的依赖文件)
当make试图重建引入的文件的时候, 他会找到依赖的隐式规则而后使用它. 可是, 因为规
则并无更新target文件, 没有引入的文件会被更新, make不会从新执行自身.
上面的一个问题是, make会认为.d
文件是中间文件, 会删除它们. 我经过将它们定义为
显式的target而非使用模式规则来解决:
depfiles := $(srcs:.c=.d) $(depfiles): include $(wildcard $(depfiles))
你可能不想将全部的.d
文件放在源文件目录下. 你很容易就可让makefile将它们放到
别的地方. 这是一个例子. 固然, 这里认为你以及修改了你的makedepend只来生成输出到
这个位置, 以及知道在写入这个目录以前可能会须要建立它....:
srcs = foo.c bar.c ... depdir = .deps %.o : %.c $(depdir)/%.d @$(makedepend) $(compile.c) -o $@ $< depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
这里我会讨论一些可能的定义上面使用的makedepend变量的方式.
makedepend = /usr/lib/cpp
or cc -e
最简单的生成依赖的方式是使用c预处理其. 这须要一点对预处理其输出格式的了解, 幸运
的是多数unix预处理器都有相似咱们意图须要的输出. 为了编译器错误消息和调试信息的
编号信息, 预处理其在每次jump到一个#include
文件以及从中返回的时候都必须提供行
号和文件名的信息(__line__
,__file__
). 这些输出行能够用于搞清楚引入了哪些文件.
多数unix预处理其会在输出中插入这个格式的特殊行:
# lineno "filename" extra
咱们关心的是filename
处的值. 有了这个, 咱们就可使用这个命令以咱们想要的格式生成.d
文件..:
makedepend = $(cpp) $(cppflags) $< \ | sed -n 's,^\# *[0-9][0-9]* *"\([^"<]*\)".*,$@: \1\n\1:,p' \ | sort -u > $*.d
....
上面的一个问题是咱们须要对源文件进行两次预处理: 一次是makedepend
命令, 一次是在编译过程当中.
若是你在使用gcc(或者提供了等价选项的编译器(clang)),你能够同时生成对象文件和依赖
文件, 节省很多实践, 由于这些编译器能够以编译反作用的形式生成依赖文件. 这是一个实现示例, 从tl;dr一节中复制的:
depdir := .deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.d compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c %.o : %.c %.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
过一遍吧:
depdir = ...
: 将依赖文件放到一个叫作.deps
的子目录depflags = ...
: gcc特定的flags, 告诉编译器生成依赖文件
-mt $@
: 设置在生成的依赖文件中target的名称-mmd
: 编译之余, 生成依赖信息. 这个版本省去系统头文件, 若是想要系统-md
-mp
: 给每一个先决条件添加一个target, 比买在删除文件的时候的错误.-mf $(depdir)/$*.d
: 将生成依赖文件$(depdir)/$*.d
%o : %.c
: 删除内建的从.c
文件构建.o
文件的规则, 以使用咱们提供的规则... $(depdir/%.d
: 将生成的依赖文件声明为target的一个先决条件, 以便在它缺失的时候, rebuilt target... | $(depdir)
: 将依赖目录声明为. target的一个order only的先决条件,以便在须要的时候建立它.$(depdir): ; @mkdir -p $@
: 声明一个在依赖目录不存在的时候建立它的规则depfiles := ...
: 生成一个可能存在的全部依赖文件的列表$(depfiles):
: 将全部依赖文件做为target说起, 以使得make不会在文件不存在的时候failinclude ...
: 引入存在的依赖文件. 使用wildcard
来避免由于不存在的文件而失败...:
depdir := .deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.td postcompile = mv -f $(depdir)/$.td $(depdir)/$.d && touch $@ compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c %.o : %.c %.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(postcompile) $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
一般你也会想要将object文件放到一个单独的位置, 而不只仅是依赖文件. 这里是一个例子:
objdir := obj depdir := $(objdir)/.deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.d compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c $(objdir)/%.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
.....