makefile自动依赖生成

自动依赖生成

基于make的构建环境要正确工作, 一个很重要(也很烦人)的任务是, 在makefile中正确列
举依赖.

这个文档将介绍了一个非常有用的让make自身来创建和维护这些依赖的方法.

文章来源

所有的make程序都需要知道, 某个特定的target依赖的文件有哪些, 以便确认它(target)
会在必要的时候进行rebuild.

手动更行这个清单不仅仅是让人乏味, 而且非常容易出错. 多数系统(不论大小)都偏向与
提供自动提取这个信息的自动化工具. 传统的工具的是makedepend程序, 其会读取c源代
码, 并以可以include至makefile中的__目标-依赖__模式生成头文件清单.

如果使用更加强大一点的编译器或者预处理器, 更加现代话的解决方案是让编译器或者预
处理器来生成这个信息.

这篇文章的意图不是专门讨论依赖信息获得的方式的(尽管有涉及到), 而是, 介绍一些有
用的将这些工具的调用,输出和gnu make组合, 来确保依赖信息总是正确和最新的, 衔接越
紧密(且越高效)越好.

这些方法依赖gnu make提供的特性. 可能可以通过修改它们来在其他版本的make上应用.
那就等你自己尝试啦. 但是, 在尽心那个尝试之前请看哈paul的makefile第一原则

gcc方案

如果有谁已近不耐烦了, 这是一个完整的最佳的实践方案. 这个方案需要你的编译器的支
持: 默认你使用gcc作为编译器(或者提供了和gcc兼容的预处理选项的编译器). 如果你的
编译器不满足这个条件, 请看另外的方案.

将这个加入到你的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都需要额外调用一次
之类的, 除去依赖生成工具自身的开销不说. 还有, 它会检查每个文件的依赖, 即使是没
有改变的文件

我们会看看到我们如何可以做到更好.

gnu 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

先解决上面的第一个问题: 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))

避免"no rule to make target..."的错误

这个要更加***钻一些. 但是, 我们可以通过在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变量的方式.

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不会在文件不存在的时候fail
  • include ...: 引入存在的依赖文件. 使用wildcard来避免因为不存在的文件而失败.

处理特殊情况

..:

  • 如果构建在某个不恰当的时间被kill了, 某个依赖文件可能会损坏. 可能会导致之后的
    调用由于语法错误而失败. 要解决这个问题必须手动删除文件
  • 眸子额情况, gcc会不恰当地设置生成的依赖文件时间戳. 使得依赖文件比对象文件更新
    . 这种情况会无限rebuild对象文件.
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文件的放置

通常你也会想要将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))

.....

posted on 2020-01-31 22:31  jakio6  阅读(1037)  评论(6编辑  收藏  举报