1.Makefile进阶
在进行复杂项目时,了解下Makefile 的⼀些基本需求,这些基本需求是:
(1)将所有的⽬标⽂件放⼊源程序所在⽬录的 objs ⼦⽬录中。
(2)将所有最终⽣成的可执⾏程序放⼊源程序所在⽬录的 exes ⼦⽬录中。
(3)将引⼊⽤户头⽂件来模拟复杂项⽬的情形。
创建⽬录
在编译项⽬之前希望⽤于存放⽂件的⽬录先准备好,当然,我们可以在编译之前通过⼿动来创建所需的⽬录,但这⾥我们希望采⽤⾃动的⽅式。虽然 complicated 项⽬现在还没有源⽂件,但这并不妨碍我们先写⼀个只有⽬录创建功能的 Makefile。下⾯我们想⼀想⽬录创建Makefile 的“依赖树”⻓得是什么样⼦。
all 是⼀个⽬标,如果 all 直接依赖 objs 和exes ⽬录的话,那如何创建⽬录呢?不管如何先写⼀个 Makefile 吧,如图 2.2 所示,其运⾏结果如图2.3 所示。在这个 Makefile 中我们定义了两个变量,⼀个是 MKDIR,另⼀个则是⽤于存放⽬录名的变量DIRS。从结果来看,验证了我所提出的问题,即,⽬录如何创建呢?
OBJS 变量即是⼀个依赖⽬标也是⼀个⽬录,在不同的场合其意思是不同的。⽐如,第⼀次 make 时,由于 objs 和 exes ⽬录都不存在,所以 all ⽬标将它们视作是⼀个先决条件或者说是依赖⽬标,接着 Makefile 先根据⽬录构建规则构建 objs 和 exes ⽬标,即Makefile 中的第⼆条规则就被派上了⽤场。构建⽬录时,第⼆条规则中的命令被执⾏,即真正的创建了objs 和 exes ⽬录。当我们第⼆次进⾏ make 时,此时,make 仍以 objs 和 exes 为⽬标,但从⽬录构建规则中发现,这两个⽬标并没有依赖关系,⽽且能从当前⽬录中找到 objs 和 exes ⽬录,即认为 objs和 exes ⽬标都是最新的,所以不⽤再运⾏⽬录构建规则中的命令来创建⽬录。图 2.7示例了 Makefile与“依赖树”之间的映射关系。
创建⼀个 clean ⽬标,专⻔⽤来删除所⽣成的⽬标⽂件和可执⾏⽂件。加 clean 规则还是相当的直观的,如图 2.8 所示,其中我们⼜增加了两个变量,⼀个是RM,另⼀个则是 RMFLAGS,这与 simple 项⽬中所使⽤的⽅法是⼀样的。运⾏ make clean 命令的结果如图 2.9 所示。
增加头⽂件
好了,⽬录的创建已经好了,接下来我们需要为我们的 complicated 项⽬在 simple 项⽬的基础之上增加头⽂件。图 2.10 是我们现在 complicated 项⽬的源程序⽂件。接下来要做的是,在complicated 项⽬的Makefile 中加⼊对于源程序进⾏编译的部分,如图 2.11 所示。
在 all ⽬标的后⾯再增加了对 EXE ⽬标的依赖。当⼀个规则中出来了多个先决条件时(这⾥的 all 规则就是),make会以从左到右的顺序来⼀个⼀个的构建⽬标。make 相关的操作结果如图 2.12 所示,从图中你会发现,所有的⽬标⽂件以及可执⾏⽂件都放在当前⽬录下,⽽并没有如我们所希望那样放到 objs 和exes ⽬录中去。图2.10和图2.11的内容合并到2.11⽬录。
将⽂件放⼊⽬录
将⽬标⽂件或是可执⾏程序分别放⼊所创建的 objs 和 exes ⽬录中,我们需要⽤到 Makefile中的⼀个函数 —— addprefix(参⻅ 1.7.1 )。现在,我们需要对 Makefile 进⾏⼀定的修改,以使⽬标⽂件都放⼊ objs ⽬录当中,更改后的 Makefile 如图 2.13 所示。
最⼤的变化除了增加了对于 addprefix 函数的运⽤为每⼀个⽬标⽂件加上“objs/”前缀外,还有⼀个很⼤的变化是,我们需要在构建⽬标⽂件的模式规则中的⽬标前也加上“objs/”前缀,即增加“$(DIR_OBJS)/”前缀。之所以要加上,是因为规则的命令中的-o 选项需要以它作为⽬标⽂件的最终⽣成位置,还有就是因为 OBJS 也加上了前缀,⽽要使得 Makefile 中的⽬标创建规则被运⽤,也需要采⽤相类似的格式,即前⾯有“objs/”。由于改动后的 Makefile 会将所有的⽬标⽂件放⼊ objs ⽬录当中,⽽我们的 clean 规则中的命令包含将 objs ⽬录删除的操作,所以我们可以去除命令中对 OBJS 中⽂件的删除。这导致的改动就是 Makefile 中的最后⼀⾏中删除了$(OBJS)。后⾯我们都采⽤在内容上加删除线的⽅式来表示这⼀内容需要被删除。更改以后的运⾏结果可以从图 2.14 看出,从其编译过程你可以看到,所⽣成的⽬标⽂件的确是放⼊了 objs ⼦⽬录。
采⽤同样的⽅法,我们也可以将 complicated 放⼊到 exes ⽬录当中去,且改动也是⾮常的⼩,更改后的 Makefile 如图 2.15 所示,图 2.16 则是这⼀改动后的运⾏结果。
假设我们对项⽬已经进⾏了⼀次编译(这⼀点⾮常重要,否则你看不到将要说的问题),接着对 foo.h⽂件进⾏了如图 2.17 所示的更改,其改动就是对 foo ()函数增加了⼀个 int 类型的参数,⽽不对 foo.c进⾏相应的更改。这样⼀改的话,由于声名与定义不相同,所以理论上编译时应当出错。
图 2.18 示例了 make 的结果,是不是很吃惊?make 告诉我们没有什么事好做!那如果我们先make clean,然后再 make ⼜是什么结果呢?答案从图 2.19 可以看出,的确是出错了!那为什么在没有进⾏make clean 之前,make 没有发现需要对项⽬的部分(或全部)进⾏重新构建呢?对于这样的 Makefile如果运⽤到现实项⽬中,那对于开发效率还是有影响的,因为每⼀次 make 之前都得进⾏ clean,太费时.
来分析⼀下此时的 Makefile 为什么会出现这⼀问题呢?图 2.20 是现有 Makefile 所表达的依赖关系树及与规则的映射关系图。前⾯的测试中,我们改动了 foo.h ⽂件,但从依赖关系图中你是否发现,其中并没有出现对 foo.h 的依赖关系,这就是为什么我们改动头⽂件时,make ⽆法发现的原因!
最为直接的改动是我们在构建⽬标⽂件的规则中,增加对于 foo.h 的依赖。改动后的 Makefile如图 2.21所示。其改动也是⾮常的⼩的,需要指出的是,在这个 Makefile 中我们使⽤了⾃动变量$<(参⻅ 1.5.1节)。前⾯我们说⼦,这个变量与$^的区别是,其只表示所有的先决条件中的第⼀个,⽽$^则表示全部先决条件。之所以要⽤$<是因为,我们不希望将 foo.h 也作为⼀个⽂件让 gcc 去编译,这样的话会出错。
将 foo.h 改回以前的状态,即去除 foo ()函数中的 int 参数,然后编译,这次编译当然是成功的,接着再加⼊ int 参数,再编译。你发现这次真的能发现问题了!更改后的依赖关系图如图 2.22所示。
编译器 ——gcc。图 2.23 列出了采⽤ gcc 的-M 选项和-MM 选项列出 foo.c 对其它⽂件的依赖关系的结果,从结果你可以看出它们会列出 foo.c 中直接或是间接包含的头⽂件。-MM 选项与-M 选项的区别是,-MM选项并不列出对于系统头⽂件的依赖关系,⽐如 stdio.h 就属于系统头⽂件。其道理是,绝⼤多数情况我们并不会改变系统的头⽂件,⽽只会对⾃⼰项⽬的头⽂件进⾏更改。
⽣成的⽬标⽂件是放在 objs⽬录当中的,因此,我们希望依赖关系中也包含这⼀⽬录信息,否则,在我们的 Makefile 中,根本没有办法做到将⽣成的⽬标⽂件放到 objs ⽬录中去,这在前⾯的 Makefile 中我们就是这么做的。在使⽤新的⽅法时,我们仍然需要实现同样的功能。这时,我们需要⽤到 sed ⼯具了,这是 Linux 中⾮常常⽤的⼀个字符串处理⼯具。图 2.24 是采⽤ sed 进⾏查找和替换以后的输出结果,从结果中我们可以看到,就是在foo.o 之前加上了“objs/”前缀。对于 sed 的⽤法说明可能超出了本⽂的范围,如果你不熟悉其功能,可以找⼀些资料看⼀看。
gcc 还有⼀个⾮常有⽤的选项是-E,这个命令告诉 gcc 只做预处理,⽽不进⾏程序编译,在⽣成依赖关系时,其实我们并不需要 gcc 去编译,只要进⾏预处理就⾏了。这可以避免在⽣成依赖关系时出现没有必要的 warning,以及提⾼依赖关系的⽣成效率。
显然,⾃动⽣成的依赖信息,不可能直接出现在我们的 Makefile 中,因为我们不能动态的改变 Makefile中的内容,那采⽤什么⽅法呢?先别急,第⼀步我们能做的是,为每⼀个源⽂件通过采⽤ gcc 和 sed⽣成⼀个依赖关系⽂件,这些⽂件我们采⽤.dep 后缀结尾。从模块化的⻆度来说,我们不希望.dep⽂件与.o ⽂件或是可执⾏⽂件混放在⼀个⽬录中。为此,创建⼀个新的 deps ⽬录⽤于存放依赖⽂件似乎更为合理。图 2.25中的 Makefile 增加了创建 deps ⽬录和为每⼀个源⽂件⽣成依赖关系⽂件。
图 2.25 中的 Makefile 存在以下更改:
(1)增加了 DIR_DEPS 变量,⽤于保存需要创建的 deps ⽬录名,以及将这⼀变量的值加⼊到DIR 变量中。
(2)删除了⽬标⽂件模式规则中对于 foo.h ⽂件的依赖,与此同时,我们将这个规则中的$<变成了$^。
(3)增加了 DEPS 变量⽤于存放依赖⽂件。
(4)为 all ⽬标增加了对于 DEPS 的依赖。
使⽤了 gcc 的-E和-MM 选项来获取依赖关系,为了最终⽣成依赖⽂件,中间采⽤了⼀个临时⽂件。为了增加可读性,在⽣成⼀个依赖⽂件时,会在终端上打印类似“Making foo.dep …”这样的提示信息。在这个规则中,set -e 的作⽤是告诉 BASH Shell 当⽣成依赖⽂件的过程中出现任何错误时,就直接退出。make 会告诉我们出错了,从⽽停⽌后⾯的 make⼯作。如果不进⾏这⼀设置,当构建依赖⽂件出现错误时,make 还会继续后⾯的⼯作,这是我们所不希望的。同样,你可以试着将 set -e 去掉,然后故意在 foo.c 或是 main.c 中植⼊⼀个错误,观察⼀下 make 此时的⾏为是什么。
这⾥我们⼜有⼀个知识点需要注意,对于规则中的每⼀个命令,make 都是在⼀个新的 Shell 上运⾏它的,如果希望多个命令在同⼀个 Shell 中运⾏,则需要⽤‘;’将这些命令连起来。当命令很⻓时,为了⽅便阅读,我们需要将⼀⾏命令分成多⾏,这需要⽤‘\’。为了理解,我们可以做⼀个实验。现在假设我们需要创建⼀个 test ⽬录,然后,在这个 test ⽬录下⾯再创建⼀个 subtest ⽬录,如果你不知道 make 是如何执⾏命令的,你可能会写如所图 2.26 示的⼀个 Makefile。
图 2.27 是运⾏结果,从最后的 ls 结果来看,make 在同个⽬录中创建了 test 和 subtest 两个⽬录,⽽不是我们所希望的。现在,将 Makefile 做⼀下修改,运⽤前⾯提到的知识点,修改后的 Makefile如图2.28 所示。
现在先删除 test 和 subtest ⽬录,然后再运⾏⼀下 make 看⼀看结果,操作及结果如图 2.29 所示。这次的结果与我们所期望的完全相同!
现在,回到我们的 complicated 项⽬的 Makefile 上来,图 2.30 是图 2.25 的 Makefile 的运⾏结果,从图中你可以看出,Makefile 会为我们⽣成新的⽬录 deps、创建 foo.c 和 main.c 的依赖⽂件,分别是deps/foo.dep 和 deps/main.dep。最后我们采⽤ cat 命令查看了⼀下 foo.dep 和 main.dep 中的内容,从内容上看来,的确是我们所需要的。
包含⽂件
在 Makefile 中加⼊对所有依赖⽂件的包含功能,更改后的 Makefile 如图 2.31 所示。
由于 make 在处理Makefile 的include 命令时,发现找不到 deps/foo.dep 和 deps/main.dep,所以出错了。如何理解这⼀错误呢?从这⼀错误我们可知,make 对于 include 的处理是先于 all ⽬标的构建的,这样的话,由于依赖⽂件是在构建 all ⽬标时才创建的,所以很⾃然 make 在处理 include 指令时,是找不到依赖⽂件的。我们说第⼀次 make 的确没有依赖⽂件,所以 include 出错也是正常的,那能不能让 make忽略这⼀错误呢?可以的,在 Makefile 中,如果在 include 前加上⼀个‘-’号,当 make 处理这⼀包含指示时,如果⽂件不存在就会忽略这⼀错误。除此之外,需要对于 Makefile 中的 include 有更为深⼊的了解。当 make 看到include 指令时,会先找⼀下有没有这个⽂件,如果有则读⼊。接着,make 还会看⼀看对于包含进来的⽂件,在 Makefile 中是否存在规则来更新它。如果存在,则运⾏规则去更新需被包含进来的⽂件,当更新完了之后再将其包含进来。在我们的这个 Makefile 中,的确存在⽤于创建(或更新)依赖⽂件的规则。那为什么 make 没有帮助我们去创建依赖⽂件,⽽只是抱怨呢?因为make 想创建依赖⽂件时,deps ⽬录还没有创建,所以⽆法成功的构建依赖⽂件。
需要对 Makefile 的依赖关系进⾏调整,即将 deps ⽬录的创建放在构建依赖⽂件之前。其改动就是在依赖⽂件的创建规则当中增加对 deps ⽬录的信赖,且将其当作是第⼀个先决条件。采⽤同样的⽅法,我们将所有的⽬录创建都放到相应的规则中去。更改后的 Makefile如图 2.33 所示。
图 2.33 的 Makefile 中,我们使⽤了 filter 函数(参⻅ 1.7.2 节)将所依赖的⽬录从先决条件中去除,否则的话会出现错误(你可以试试看,如果不改会出现什么错误)。正如前⾯所提及的,当make 看到include 指令时,会试图去构建所需包含进来的依赖⽂件,这样⼀来,我们并不需要让 all⽬录依赖依赖⽂件,也就是从 all 规则中去除了对 DEPS 的依赖。图 2.34 示列了修订后的 Makefile的运⾏结果。
再对源程序⽂件进⾏⼀定的修改,如图 2.36 所示。其中的改动包括:增加 define.h ⽂件并在其中定义⼀个 HELLO 宏。在 foo.h 中包含 define.h ⽂件。
在 foo.c 中增加对 HELLO 宏的使⽤。增加了这些改动以后,进⾏ make 操作,结果如图 2.37 所示。
So far so good!foo.dep 也如我们所想象的那样。现在我们得稍微改动⼀下代码,以便发现其它的问题。
在上⾯成功编译过的基础之上,我们再做⼀些改动。注意⼀定要编译过,⽽不要 clean 后再做下⾯的改动,只有这样做我们才能发现问题。改动如图 2.32 所示,这次增加了⼀个 other.h ⽂件并将以前在define.h 中定义的 HELLO 宏放到了这个⽂件当中,接着让 define.h 包含 other.h ⽂件。接下来,我们再进⾏⼀次 make 操作,结果如图 2.39 所示。从结果中你发现什么了吗?尽管 foo.c 和main.c 从新编译了,但依赖关系并没有重新构建!从运⾏ complicated 程序的结果来看,其打印的问候语也是 Hello,这正是我们程序所设计的。
现在对 other.h 进⾏更改,更改后内容如图 2.40 所示。改动就是将问候语 Hello 变成了 Hi,更改后⼜⼀次进⾏ make 操作,结果如图 2.41 所示。
问题出来了,程序并没有因为我们更改了 other.h ⽽重新编译,问题出在哪呢?从 foo.dep 和main.dep的内容来看,其中并没有指出 foo.o 和 main.o 依赖于 other.h ⽂件,所以当我们进⾏ make时,make程序没有发现 foo.o 和 main.o 需要重新编译。那如何解决呢?我们说,当我们进⾏ make时,如果此时make 能发现 foo.dep 和 main.dep 需要重新⽣成的话,此时会发现 foo.o 和 main.o都依赖 other.h ⽂件,那⾃然就会发现 foo.o 和 main.o 也需要重新编译。
我们也需要对依赖⽂件采⽤ foo.o 和 main.o 相类似的依赖规则,为此,我们希望在Makefile 中存在如图 2.42 所示的依赖关系。如果存在这样的依赖关系,当我们对 define.h 进⾏更改以增加对other.h ⽂件的包含时,通过这个依赖关系 make 就能发现需要重新⽣成新的依赖⽂件,⼀旦重新⽣成依赖⽂件,other.h 也就⾃然会成为 foo.o 和 main.o 的⼀个先决条件。如果这样的话,就不会出现前⾯所看到的依赖关系并不重新构建的问题了。
要增加这个依赖关系,在现有的 Makefile 上还是很简单的⼀件事。我们只需对 Makefile 进⾏⼩改就能做到。图 2.43 示例了改动的原理,其实,只要在依赖⽂件的构建规则中多增加依赖⽂件⾃身这个⽬标就⾏了。
现在看来,我们在⽣成依赖关系时,也需要将依赖⽂件作为⼀个⽬标。更改后的 Makefile 如图2.44 所示。
从这个 Makefile 中你可以看出,我们只需在相应的规则命令中增加⼀个$@就⾏了,因为这个表示的是⽬标,即在创建 deps/foo.dep 时,其代表的就是 deps/foo.dep。有了这样的改动后,你不能直接 make来观察其效果,⽽是必须先 make clean 然后依此重新做图 2.38 和图 2.40 中所列出的变化。有了这样的变化之后,你会发现不论你如何更改源程序,make 都能发现并且做出正确的构建操作,当然这一切还得归功于我们,因为 Makefile 是我们写的呀!⾃已试试看吧!
条件语法
当 make 看到条件语法时将⽴即对其进⾏分析,这包括 ifdef、ifeq、ifndef 和 ifneq 四种语句形式。这也说明⾃动变量(参⻅ 1.5.1 节)在这些语句块中不能使⽤,因为⾃动变量的值是在命令处理阶段才被赋值的。如果⾮得⽤条件语法,那得使⽤ Shell 所提供的条件语法⽽不是 Makefile 的。Makefile 中的条件语法有三种形式,如图 2.45 所示。其中的 conditional-directive 可以是 ifdef、ifeq、ifndef 和 ifneq 中的任意⼀个。
对于 ifeq 和 ifneq,可以采⽤如图 2.46 所示的格式。图 2.47 是使⽤ ifeq 和 ifneq 条件语法的⼀个例⼦,图 2.48 则显示了其运⾏结果。
fdef和ifndef的格式如图 2.49所示。图 2.50是使⽤ifdef和ifndef的⼀个示例Makefile,图 2.51则是其运⾏结果。
现在回到我们的 complicated 项⽬(图2.44,对应源码⽬录2.44),你可能发现了当我们进⾏⼀次
make,然后接着进⾏两次make clean 操作时,第⼆次的 make clean 与第⼀次有所不同,如图 2.52 所示。
看到了吗?当进⾏第⼆次 make clean 时,make 还会先构建依赖⽂件,接着再删除,这是因为我们进⾏make clean 也需要包含依赖⽂件的缘故。显然,其中构建依赖⽂件的动作有点多余,因为后⾯⻢上⼜被删除了。为了去除在 make clean 时不必要的依赖⽂件构建,我们可以⽤条件语法来解决这⼀问题。⽅法就是,当进⾏ make clean 时,我们不希望 complicated 项⽬的 Makefile 包含依赖⽂件进来,更改以后的 Makefile 如图 2.53 所示。这⾥的更改,我们⽤到了 MAKECMDGOALS 变量(参⻅ 1.5.2 节)。有了这⼀改动后,即使是进⾏连续的多次 make clean,make 也不会先构建依赖⽂件。
在这个项⽬中我们采⽤了如图 2.54 所示的⽬录结构。尽管这种单⼀的结构在现实项⽬中⽐较少⻅,但是,这个项⽬的 Makefile 已经是⾮常的完整了。毕竟,它能准确⽆误的发现哪些⽂件需要从新构建,⽽不是需要我们先 make clean,然后再make。当然,更不⽤担⼼我们对代码进⾏了更改,但 make 却没有对其进⾏编译,⽽我们认为所有的更改都被编译了。如果存在这种情况,那调程序时,就是抓破了头也想不明⽩!
本篇讲了复杂目录的Makefile,在接下来的文章会继续讲解在工程项目中的Makefile,也就是最值得关注的实战