MakeFile最全实践3

编程入门 行业动态 更新时间:2024-10-17 19:29:21

MakeFile<a href=https://www.elefans.com/category/jswz/34/1768819.html style=最全实践3"/>

MakeFile最全实践3

项目结构如下图:

图 3.1

huge 项⽬是上层有两个⽬录,⼀个是 build ⽬录,⽽另⼀个则是source ⽬录。前者⽤于存放共公的 make.rule,以及编译整个项⽬的的 Makefile。此外在 build ⽬录中还会在编译时⾃动⽣成 libs 和 exes 两个⼦⽬录。libs ⽬录⽤于存放编译出来的库⽂件,⽽ exes ⽤于存放编译出来的可执⾏⽂件。source ⽬录则⽤于存放项⽬的源程序,其下将按各个软件模块分成不同的⼦⽬录。在现在的 huge 项⽬中,包括 foo 库和 huge 主程序,所以在 source⽬录下分别创建了 foo 和huge ⼦⽬录。对于每⼀个软件模块⼦⽬录,⼜分为⽤于存放源程序的 src⼦⽬录和⽤于存放头⽂件的 inc⼦⽬录。当进⾏编译时,我们希望 Makefile 在 src ⽬录下⾯创建⼀个 deps ⽬录,⽤于存放依赖⽂件,这⼀点与 compliated 项⽬中的 deps ⽬录是⼀样的;另⼀个⽬录objs,也同 complicated 项⽬⼀样,⽤于存放编译过程中⽣成的⽬标⽂件。另外,在每⼀个 src ⽬录中都会有⼀个 Makefile,这⼀ Makefile的作⽤是只⽤于构建所在⽬录中的源程序,可以想像的是,在 build ⽬录下⾯的 Makefile 将调⽤每⼀个软件模块 src ⽬录下的 Makefile,从⽽实现整个项⽬的构建。为了实现各软件模块 src ⽬录中 Makefile之间最⼤程度的复⽤,我们可以让它们共⽤的部分放在⼀个独⽴的⽂件中 —— 这就是 build ⽬录下的make.rule 的作⽤。

创建⽬录

并不关⼼项⽬的源程序,源程序可以在需要时再放⼊。请先根据图 3.1 创建好那些需要⼿动创建的⽬录,采⽤图3.2 所示的命令能完成这些⼿动⽬录的创建⼯作。

先创建位于 source/foo/src 下⾯的 Makefile,这个 Makefile 我们可以基于complicated 项⽬最终版本的 Makefile 进⾏⼀定的更改⽽得到,如图 3.3 所示。

图 3.3 中的 Makefile 与 complicated 项⽬的最终版本相⽐,包含如下的更改:

(1)增加了 AR 和 ARFLAGS 两个变量,⽤途是⽤以⽣成静态库。

(2)给 DIR_EXES 变量赋了正确的值,⽤于表示 exes ⽬录的实际位置。现在,还是采⽤相对路径。

(3)在 DIRS 变量中增加了 DIR_LIBS 变量中的内容,以便在⽣成库⽂件之前创建 build/libs ⽬录

(4)新增了 RMS 变量,⽤于表示需要删除的⽬录和(或)⽂件。由于这个 Makefile 只是针对构建libfoo.a 库的,所以当我们运⾏ make clean 时,我们不能将位于 build ⽬录下的 exes和 libs ⽬录全部删除,这与前⾯的 complicated 项⽬是完全不同的。

(5)删除了将 compliated赋值给 EXE 变量,同时增加了 ifneq 条件语句⽤于判断 EXE 变是否被定义。因为后⾯在设置 all ⽬标的依赖关系时,需要判断EXE 变量是否有值,如果没有值,我们并不需要让 all ⽬标依赖$(EXE)。如果 EXE 没有值,我们也不需要为其调⽤ addprefix 函数增加前缀,否则这会打破我们后⾯判断 EXE 变量是否有值,从⽽决定是否让 all ⽬录依赖于它这⼀⽅法。

(6)如果 EXE 有值那么我们应当将其值加⼊到 RMS 变量中,以便在我们调⽤ make clean时清除它。

(7)新增了 LIB 变量,⽤于存放库名,在这⾥库的名称就是 libfoo.a。同样采⽤处理 EXE 变量的⽅法增加了条件语句。

(8)对 all ⽬标增加条件语句以决定是否将$(EXE)或是$(LIB)作为它的先决条件。

(9)增加了⼀条⽤于构建库的规则,⽅法就是采⽤ crs 参数调⽤ ar 命令以⽣成库

(10)在 clean ⽬标命令中,采⽤删除 RMS 变量中的内容,⽽不是 DIRS 变量中的内容。这⼀点前⾯我们说过了,因为我们不希望在 foo模块中 make clean时将 build⽬录下的 libs 和 exes⽬录也删除。

现在需要试⼀试这个 Makefile 是否能⼯作,在试之前,我们需要先在 src ⽬录中增加⼀个源程序。这可以采⽤ touch 命令来创建⼀个空的 foo.c ⽂件,操作结果如图 3.4 所示。

从运⾏的结果来看,的确是在 build/libs ⽬录的下⾯⽣成了⼀个 libfoo.a 库⽂件。运⾏ make clean以后,也并没有将 build/libs 这个⽬录给删除,⽽只是删除了 libfoo.a ⽂件,这正是我们所设计的。接下来,我们需要考虑的是,将这个⽂件运⽤到 source/huge/src ⽬录下⾯去。最为直接的想法是,将 foo模块的 Makefile 拷⻉到 huge/src ⽬录下⾯去,然后做⼀些⼩的改动。在这样做之前,请等⼀下!Makefile 的设计与我们采⽤编程语⾔进⾏设计是⼀样的,我们需要考虑尽可能的复⽤,以提⾼可维护性。要做好这⼀点,这需要我们站在更⾼的层次去思考如何设计 Makefile。

提⾼复⽤性

在⼀开始,我们提到在 build 的⽬录下⾯将会放⼀个 make.rule ⽂件,这个⽂件的⽬的就是为了让所有位于各软件模块的 src ⽬录下⾯的 Makefile 能使⽤它以提⾼复⽤性。也就是说,我们需要考虑将 foo 模块的 Makefile 中的⼀部分内容放⼊到 make.rule 中。显然,只能将那些我们认为是公共的部分放⼊其中。那反过来,foo 模块的 Makefile 中,哪些是不能公⽤的呢?我想有以下⼏点:

(1)变量 EXE 和 LIB 的定义对于每⼀个软件模块是不同的。⽐如在我们的 huge 项⽬中,我们需要在source/foo/src ⽬录中的 Makefile ⾥将 LIB 变量的值设置为 libfoo.a,且 EXE 变量应当为空。但是,在 source/huge/src ⽬录中的 Makefile ⾥⾯,我们却要反过来,只定义EXE 变量的值为hug。

(2)DIR_EXES 变量和 DIR_LIBS 变量由于运⽤了相对路径,所以也是每个模块特有的。但是可以采⽤绝对路径的⽅式解决这个问题。⽐如,我们可以定义⼀个 ROOT 环境变量,其值设置为 huge 项⽬的根⽬录,这样的话 DIR_EXES 和 DIR_LIBS 就可以通过以 ROOT 为相对路径,从⽽使得其值对于所有的模块都相同。

考虑到复⽤性,现在 foo 模块的 Makefile 由两部分组成了,build ⽬录中的 make.rule 和source/foo/src ⽬录中的 Makefile,其内容如图 3.5 所示。

有了这⼀改动以后,你会发现 foo 模块的 Makfile 中 的内容很是简单,其中原来的⼤部分内容都被放到了 make.rule ⽂件中。如果要运⾏ make,那么必须先在 Shell 上 export 所需的 ROOT 变量,图 3.6示例了操作步骤。

注意:在 export 所需的 ROOT 变量时,除了先要进⼊ huge项⽬的根⽬录外,pwd 命令前后的字符是“`”⽽不是“’”,这个字符是键盘上“!”键左边的那⼀个。从运⾏结果来看,我们为了提⾼复⽤性的努⼒有⼀点进展!

接下来需要考虑 sourc/huge/src ⽬录中的 Makefile 了,我们希望在这个⽬录中放的程序能⽣成⼀个可执⾏⽂件,在测试 Makefile 时,需要在⽬录中放置⼀个 main.c ⽂件,其中的内容如图 3.7所示,⽽Makefile 的内容如图 3.8 所示。

需要进⼊ source/huge/src ⽬录进⾏ make,以检验其下的 Makefile 是否能正常⼯作,结果如图 3.9 所示。你⼀定会对这⼀运⾏的结果感到满意!

加⼊源程序⽂件

现在我们的 Makefile 系统已经有了初步的成果了,下⾯要做的是将源程序加⼊到⽬录中。所需的程序内容可以从 complicated 项⽬中得来,如图 2.10 所示。现在让我们试⼀试进⼊ source/foo/src⽬录中进⾏编译,运⾏结果如图 3.10 所示。

在构建 foo.dep 依赖⽂件时,make 就发现了错误,但是还是继续进⾏编译,直到编译foo.c 时才因为错误⽽终⽌。这种⾏为并不是我们所期望的,理想的情况是,make ⼀旦发现错误就应当⽴即退出,这有利于我们在第⼀时间知道第⼀个出错点,从⽽不容易隐藏错误。那这是为什么呢?前⾯我们说了我们在构建依赖⽂件的规则中运⽤了 set -e 命令,以告诉 Shell ⼀发现错误就退出,可是为什么没有退出呢?想⼀想我们是如何包含依赖⽂件的?我们在 include 指令的前⾯放了⼀个‘-’,告诉 make 在include 出现错误时,不要报错。正是这⼀原因导致了即使 make 发现了错误,仍进⾏后续的构建操作。改动很简单,将 make.rule 中 include 语句前的‘-’去掉,去掉之后的 make 结果如图 3.11 所示。

这⼀次虽然 make 会报告找不到 foo.dep,但继续进⾏ foo.dep 的⽣成操作,这是我们所希望的⾏为。接着,在⽣成 foo.dep 时,由于找不到 foo.h 头⽂件所以出错了,这⼀次 make 直接就告诉了我们这⼀错误。

下⾯要做的是解决 foo.h 头⽂件找不到的问题。根据图 3.1 所示的 huge 项⽬的⽬录结构,你会发现,现在的 foo.c 源⽂件和 foo.h 头⽂件是分别被存放在不同的⽬录中的,当进⾏依赖⽂件构建时,我们需要采⽤⼀种形式告诉编译器到指定的⽬录中查找头⽂件。看来,我们得对 Makefile 进⾏⼀定的改造了,改造的基本原理是,利⽤ gcc 的-I(‘i’的⼤写)选项。更改后的 Makefile 如图 3.12 所示

从改动来看就是增加了⼀个⽤于存放各模块所需⽤到的全部头⽂件的⽬录的变量 ——INC_DIRS,这⼀变量可以存放多个⽬录,这完全是根据软件模块的需要的。此外,在 make.rule 中增加了⼀个条件语句块,即当 INC_DIRS 中的值不为空时,先采⽤ strip 函数去除多余的空格,然后再⽤ addprefix 函数为所有的⽬录的前⾯加上“-I”前缀。最后的改动就是,让⽬标⽂件⽣成规则和依赖⽂件⽣成规则中增加对INC_DIRS 变量的引⽤,以告诉 gcc 到哪去找头⽂件。增加这些改动之后,让我们再看看编译 libfoo.a的结果是怎样的,如图 3.13 所示。

这次 libfoo.a 能被成功构建出来了,那 huge 可执⾏程序呢?进⼊ source/huge/src ⽬录,运⾏make 的结果如图 3.14 所示。出错了!

可以看出 main.o ⽬标⽂件被正确的⽣成了,但在连接时找不到_foo 的引⽤,即找不到 foo ()函数。由于foo ()函数的实现是放在 libfoo.a 库中的,⽽我们⼜没有告诉编译器,在连接时应当到 libfoo.a中找所需的函数。其实,现在要做的与前⾯头⽂件⽬录的指定⼯作是相类似,只是这⼀次要⽤到 gcc的-l(⼩写的L)和-L 选项,更改后的 Makefile 如图 3.15 所示。

这次的改动包含:

(1)在 make.rule ⽂件中增加了 LINK_LIBS 变量(的引⽤),这⼀变量⽤来存放所有需要在连接时⽤到的库。

(2)在 make.rule 中将 DIR_LIBS 变量加⼊到连接器的搜索⽬录中去,这通过使⽤ gcc 的-L 选项做到。由于我们采⽤将所有的库⽂件都放⼊$(DIR_LIBS)⽬录中,现在看来这种⽅式能简化 Makefile 的设计,因为我们不需要指定多个⽬录。

(3)在各模块的 src ⽬录中的 Makefile 中,增加了 LINK_LIBS 变量(的定义),且在source/huge/src/Makefile 中对 LINK_LIBS赋值为 foo。在 Linux 中,⼀个库名的格式为libxxxx.a(或.so),其中的 xxxx 就是我们采⽤ gcc 的-l 选项时所需给的名。在这个 Makefile中,LINK_LIBS 变量的 foo 值就是表示 libfoo.a 库。

图 3.16 是更改后的编译结果,现在 huge 可执⾏程序能正确的⽣成了。在结果的最后,你也可以看到运⾏huge 可执⾏程序的输出结果。

⾄此,我们已经完成了各模块 src ⽬录下⾯的 Makefile ⽂件的创建⼯作。此时,假设我们想往huge 项⽬中增加⼀个 libbar.a 库,或说 bar 模块。让我们看⼀看对于新增模块的 Makfile 需要做些什么⼯作,以此,也可以检验我们的编译系统设计得如何。假设我们在图 3.1 的⽬录结构的基础上增加如图 3.17 所示的⽬录结构,⽤于存放 bar 模块的源程序。bar 模块的源程序及 Makefile 则如图3.18 所示。

为了构建 libbar.a,我们需要进⼊ source/bar/src ⽬录运⾏ make 命令,结果如图 3.19 所示。不可思议吧!当需要增加⼀个软件模块时,我们在 Makefile ⽅⾯需要做的⼯作⾮常的少。只需将已存在的⼀个 Makefile 拷⻉过来,然后⼩改⼀下就好了,这是⼀个好的程序编译环境应当具备的⼀个特征,这⼀点 huge 项⽬做到了!

 

 

在进⼊ source/huge/src ⽬录之后进⾏编译之前,我们需要对main.c和其 Makefile进⾏⼀点改动,以使其使⽤新增加的 bar库。更改后的 main.c和 Makefile如图3.20 所示。

 

图 3.21 示例了 huge 可执⾏程序的编译和运⾏结果。回过头来看⼀看图 3.20 中 Makefile 的改动,你会发现为了给 huge 可执⾏程序增加⼀个库,改动⾮常的⼩。这⼜⼀次证明了 huge 项⽬的编译环境具有很好的可维护性。

 

简化操作

为了编译 huge 项⽬,我们需要进⼊不同的⽬录进⾏make(优化这类操作),这并不是⼀件容易事。下⾯,我们得考虑如何简化 huge 项⽬的编译,还记得图 3.1中我们所规划的 build ⽬录下⾯的 Makefile 吗?我们现在就是要完成这个 Makefile 的内容,从⽽简化 huge 项⽬的编译⼯作。

不打算⼀步⼀步的介绍这个 Makefile 是如何设计出来的的,⽽是直接列出在图 3.22。以我们现在的⽔平,我想你可以明⽩这个 Makefile 在⼲什么。其中有⼀点需要指出的是,在这个 Makefile 中,使⽤了 Shell 中的 for 语句,⽤于遍历变量 DIRS 中的每⼀个⽬录,并进⼊⽬录运⾏make 命令。在 1.5.2节中我们提到了 MAKE 特殊变量,在这个 Makefile 中我们就⽤到了它。之所以⽤它,⽽不直接使⽤make 是为了更好的移植性。还有⼀点需要注意的是,由于库必须⽐可执⾏程序先构建出来,所以在 DIRS 变量中,我们必需将库⽬录放在可执⾏程序的⽬录之前,因为 Makefile 是根据⽬录的先后顺序来进⾏构建⼯作的。

 

个 Makefile 的结果如何?图 3.23 给出了答案。结果证明,这个 Makefile 的确是简化了我们的⼯作,我们只要以在 build ⽬录下运⾏ make 命令就可以构建整个项⽬。此外,也可以在各个软件模块的 src ⽬录下单独构建各个模块。你可能会问,我们这⾥所有的库都是静态库,每⼀个库的重新构建都需要重新⽣成 huge 可执⾏⽂件,因此,单独构建某⼀个模块似乎没有意义。是的,但是如果我们的库采⽤的是动态库。那么重新构建某个单独的模块就很有意义了,尤其是⼤型的项⽬。因为,在很多情况下,动态库是可以单独构建的,对其的重新构建并不需要对主程序进⾏重构。

 

 

⼩结

huge 项⽬中的 Makefile 是⼀个真正实⽤的实现,其完全可以被运⽤到⼤型项⽬中去。到此,你已经学会了如何⼀步⼀步的构建⼀个真正实⽤的软件编译环境了!

更多推荐

MakeFile最全实践3

本文发布于:2024-02-10 22:51:06,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1677768.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:最全   MakeFile

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!