首先我们知道,即使再大型的项目最后生成的都是一个可执行文件,只要是可执行文件就需要依赖各种目标文件,动态库,静态库;
静态库同样需要依赖其它目标文件;
动态库也可能依赖其它文件;
那么动态库,静态库是如何构建的呢 ?
总体原则就是:
- 先依次编译出一些小的目标文件,将这些目标文件链接成静态库,动态库方便使用。
- 然后再一步步连接目标文件以及各种库形成更大的库
- 最后将几个必要库以及目标文件进行链接从而生成最终可执行的文件。
💚💚💚💚
以上就是 构建大型项目的一个基本思想,程序员先驱们也是基于这种非常非常古老的方法进行程序编写,每个目标文件以及库都是自己手动编译链接出来的,然后再将它们链接成更大的库,直到最后生成可执行文件。
可是这样的方法看似非常简单,但是缺点也非常明显,俺就是非常繁琐,一旦某个源文件进行了改动,所有依赖此文件的库都需要从新编译,手动来完成这项工作是极其枯燥且容易出错的。
所以,为了解决这个这个问题,天才的程序员想出了一个小工具 make。
- 有个 make 这个工具从此编译链接这个过程就被 make 自动化,使得程序员得以从繁琐的编译链接中解放出来。
- 使用 make时我们只需要编写规则,也就是告诉make 最终的可执行文件依赖什么,为此需要做些什么。
- 当编写好这些规则后,然后简单的执行一个命令也就是 make 一下就可以了。如果某个源文件被修改了,也只需要简单的重新执行一下 make 命令,因为整个过程的规则并没有改变,而make 也会很聪明的只编译链接那些需要更新的目标文件,库,并重新进行可执行文件的生成,而对于那些没有改动的源文件,make 不会重新编译它们。
💚💚💚
好,现在就让我们来认识下 make 吧 。
make
target: prerequisites
recipe
💚
- target 类似我们的一个目标;
- prerequisites :先决条件,也就是依赖什么;
- recipe:这个类似菜谱,就是上面的事情需要怎么做。
- make中的规则保存在:叫做 Makefile 的文件中
- 当运行 make 命令时,make 程序会自动找到当前路径下的 Makefile 然后开始执行里面的规则。
- 这里 Makefile 其实就是脚本,而 make 就是读取这个脚本然后根据里面的内容来执行命令,所以 make 就是一个普通的程序而已, makefile 就是 默认的脚本名字。
编译一个 HW 的可执行文件
比如,我们写了一个 HelloWorld程序,将源文件命名为: HelloWorld.cpp ,我们想把该源文件编译成一个叫做 HW的可执行文件,那么一个最简单的 Makefile 就可以这样写:
HW:HelloWorld.o
gcc HelloWorld.o -o HW
HelloWorld.o:HelloWorld.cpp
gcc -c HelloWorld.cpp
💚
我们来分析下,这里最终的可执行文件 HW 依赖目标文件 HelloWorld.o
- 假设我们现在已经有 HelloWorld.o 这个 object 对象,那么我们就可以利用命令: gcc HelloWorld.o -o HW 生成可执行文件 HW
- 那么 HelloWorld.o 是如何获取的呢 ?我们可以看到第二条规则,Helloworld.o 依赖 HelloWorld.cpp , 由于 HelloWorld.cpp 源文件已经写好了,所以我们可以直接用命令 : gcc -c HelloWordl.cpp 来生成
- 这样完成上述前两个步骤后,整个目标就达成了。
💚
其实本质上我们使用的各种继承开发环境IDE ,其自动化编译工具背后的原理和 make 是一样多的。
有了上面的基本认识以后,我们开始分析:大型项目实如何被构建的 。
构建大型项目
大型项目通常会有成百上千甚至上万个源文件,这些源文件统一放在了一个文件夹中方便管理。如图所示:
- 圆形代表源文件,其他为文件夹
- 项目中的源码会放在 src 当中
- lib 用于存放一些工具性代码(如:网络通信以及字符串处理),通常情况下,lib 下的代码灰被编译成各种库,方便app使用
- app 存放各种需要可执行文件(程序)的代码。
通过make的嵌套能力,解决功能模块单独编译问题
network:
cd net && make
这句话的意思是告诉 make ,要想编译网络模块(network) 需要进到 net 文件夹并且执行 make命令,当 make进入到 net文件夹开始执行 make时,net 下的 Makefile 就开始被执行,通过这样一个简单的命令就可以实现 make 的嵌套执行, make的这项特性使得每个模块都可以当做独立项目来维护。
💚💚💚
所以,为了项目的模块化管理,使得项目丛每个模块都可以有独立的 编译脚本,那么每个模块中都会有单独的 makefile。(如上图中蓝色部分,白色部分是源码)。
下图是更清晰的 Makefile 组织方式。
这些脚本中定义了如何编译该模块,以及编译该模块需要依赖什么。
- 这些模块的父目录即 lib 文件夹下同样也有自己的 makefile, lib 下的 Makefile 会收集各个子模块的编译结果,然后将其链接成各种库。
- 对于 app 下面的子目录来说,这些子目录总就是各个可执行文件的源码,如:wechat文件下就是可执行程序微信的源码,如果wechat需要用到 lib 下的 net 功能,那么只需要 在 wechat 的 makefile 中加入 对 lib/net 库的依赖就可以了。
- 同理:wechat 的父目录app 中也有 Makefile , 这里的 makefile相对简单,只需要一次执行 QQ , wechat 中的 makefile 就可以了。
💚 - 所以:通过 make 嵌套功能,我们只需要在 src 的目录 下 执行 make 就可以将 QQ 和Wechat 编译出来。
make 执行过程
在上面的示例中 src 下的 Makefile 是整个编译过程的入口,因此我们进入 src 文件夹开始 执行make 命令。
-
在 src目录下,make 首先读取 src 下的makefile : ./src/Makefile 非常简单,该文件仅仅告诉 make 需要去 app 目录下执行 make 命令
-
make 来到 ./src/app 目录下,开始读取该目录下的 Makefile ,该文件定义了编译出 QQ,微信的规则, make 首先执行编译 QQ的规则,该规则告诉make 编译 QQ 需要到 ./src/app/QQ 目录下 执行make 命令
-
make 来到 ./src/app/QQ m目录下,开始读取目录下的 makefile ,该文件定义了 编译QQ程序的规则, make开始执行这些规则,其中一项就是需要依赖网络库,同时该规则告诉 make 如果想得到该网络库 ,需要进入到 ./src/lib 下 执行 make 命令
-
make 来到 ./src/lib 目录下,开始读取该目录的 Makefile , 该文件定义了编译出 网络库,字符串处理库的规则, make 首先执行编译网络库的规则,该规则告诉 make 如果想得到 网络库,则需要进入到 ./src/lib/net 目录下执行make命令。
-
make 来到了 ./src/lib/net 目录下,开始读取 该目录下 的 makefile ,该文件定义了编译网络库的规则,编译网络库不需要依赖任何其他库,make现在可以可以安心工作,不再跳转。
make 开始执行该目录下的 makefile ,将一个个源文件编译成目标文件,最后将这些目标文件链接成静态库(也可以是 动态库,这个取决于 编译规则), make 在 ./src/lib/net 完成任何后 跳转后 ./src/lib ,因为 make 会记住自己从哪个目录跳转到 当前目录的。
-
make再次回到 ./src/lib 下,因为 make 执行完网络库的编译规则需要往下执行,也就是 string 字符串的编译规则,该规则告诉make 如果想得到字符串库则需要进入 到 src/lib/strings 下执行 make 命令。
-
make 来到了 ./src/lib/strings 目录下,开始读取 该目录下 的 makefile ,该文件定义了编译网络库的规则,编译网络库不需要依赖任何其他库,make现在可以可以安心工作,不再跳转。
make 开始执行该目录下的 makefile ,将一个个源文件编译成目标文件,最后将这些目标文件链接成静态库(也可以是 动态库,这个取决于 编译规则), make 在 ./src/lib/strings 完成任何后 跳转后 ./src/lib ,因为 make 会记住自己从哪个目录跳转到 当前目录的。 -
make 回到 ./src/lib 如果该目录下的 makefile还有其他编译规则,则继续上面的过程,如果没有,则该模板下的编译任务执行完成,make 回到 ./src/app/QQ 。
-
make 回到 ./src/app/QQ 下继续执行被中断的规则,此时 QQ所依赖的库都已经编译完成,因此 make 可以直接进行链接了。 QQ程序编译链接完成, make 返回到 ./src/app 中
-
make 来到./src/app 下继续执行被中断的规则,make 开始执行 微信程序的编译规则,这里和 QQ 的编译时一样的,唯一的一点 区别是:如果 微信也需要依赖网络库和字符串库,那么当 make 跳转到 ./src/lib 下会发现这些库已经生成了,因此直接返回。
-
当 make执行完 ./src/app 下的编译规则后,QQ 和微信程序都编译完成,make 回到 ./src后,发现该目录下 的 makefile 执行完成,因此 make 程序退出,整个编译过程完成。