制作makefile

一个大项目通常有很多.h .cpp等文件,各种附加包含目录和库,如果要从终端手动敲g++命令,那么复杂度可想而知。
所谓makefile,本质上就是把终端里输入编译器命令行的若干命令写到一个文本文件里,这样编译的时候直接调这个本文文件即可。省时省力。
于是要写一个makefile需要两部分的知识,一是编译器命令行的知识,二是makefile的书写规则。

本文档以介绍makefile部分为主线,出现了编译器命令行的知识进行现场介绍
makefile通常是在自己电脑上调试好,然后直接拷贝到远程服务器然后调用make命令。这里演示的都是在本人Mac上进行的。

一、概述

makefile的语法格式是:
Target : Prerequisites
(tab)Command

语义上解释是当Prerequisites符合时,执行Command生成Target
当Prerequisites不符合时,比如指定的项不存在或者旧了,就去找生成那个项的block然后执行,执行完再回到当前block继续执行

在这个意义上说,make就是一层一层地去找文件依赖关系

惯例上第一个指定Target的block是最终的可执行文件,剩下的若干block定义中间文件.o,最后一个block是一个clean

二、基础:单个源文件

先从最简单的入手。假设这里有一个源文件main.cpp,只使用标准库输出”Hello Make”。那么对应的makefile如下:

井号开头表示注释。

第一个block是最终的可执行文件,第二个block是编译源代码,第三个block是clean(rm表示remove)
注意Command必须是tab开头,许多文本编辑器会自动把tab替换为若干个space,这样是无效的makefile,会报错missing separator。建议使用操作系统自带的默认编辑器书写
简单说一下g++指令。-c表示编译源文件为.o文件,-o表示链接生成一个可执行文件,需要用到的.o文件依次写在可执行文件的后面

在目标文件夹打开终端执行操作

注意make的执行顺序是先编译然后链接,这就是一层层查找依赖关系完成后就能够看到.o和可执行文件出现了
使用make clean就可以删除这两个文件

三、进阶:多个源文件

真正的项目肯定有许多源文件,而且存在文件结构,如何处理呢?
假设这里有这样一个项目,shared/cUtility.h和shared/cUtility.cpp提供一些辅助函数,放在shared文件夹里。Rnd.h和Rnd.cpp提供了一个类,该类使用了cUtility.h里面的函数。main.cpp是入口,使用了Rnd类。

按照上述思路,makefile要如下写

需要多个文件,就依次在后面添加,空格分隔
注意这里的cUtility使用了c++17的标准,所以在编译时需要显式指明。那个-std=c++17叫做编译选项,后文将陆续介绍更多的。
还有最后的clean书写方式变了,这是更为稳健的写法。.PHONY表示clean是一个伪目标,-rm表示执行过程中如果遇到问题不要停继续进行

不过还有一些可以改进的地方
首先,我们可以添加makefile的搜索路径,使用VPATH = xxx:yyy:zzz(多个路径用:分隔)
指定这个后就可以干掉prerequisites里面的shared/了,这样书写更简单
但是给g++的命令里仍然需要出现shared/不然g++找不到目标文件
其次,注意到makefile里有部分内容是重复的,为了方便书写和维护,可以在makefile里定义变量
语法很简单,使用 var = xxx即可,使用该变量则写$(var),make时会将这个东西替换为变量里的内容。说是变量但是这个行为方式更像宏

四、使用第三方库

使用第三方库是很常见的事,如何在makefile里表示呢?
要明确库的头文件搜索和链接是属于编译过程的任务,即g++的事,所以体现在命令行参数里。但是makefile可以辅助配合从而更易于维护

现在假设main里使用了boost::ublas,只有头文件,所以暂时只用考虑增加boost搜索路径的事
修改makefile如下

推荐做法是将所有编译选项集中起来定义成一个变量,然后用makefile展开成实际内容传给g++

要给g++添加搜索路径使用-Idir即可(是大写的I,不是小写的L)

现在假如使用boost::filesystem,这个需要有库,那么可以这样写

继续在后面添加,-Ldir表示添加库目录,-lxxx表示链接时使用名字为libxxx.so(libxxx.a)的库。(是小写的L,不是大写的I)。由于linux里库的命名格式都是libxxx.y所以省略前三个字母lib。链接时优先使用动态链接库,所以这里最后一个参数会变成libboost_filesystem-mt.so。如果要用静态库则增加-static。
关于库还有一些注意事项。
-l还有另一种写法比如-l:libcryptopp.a,这个明确指明要用哪个库,这种写法要更加健壮。
库的链接应该放在.o文件的后面,不然会出现undefined reference to XXX。这与g++执行的link order有关系。严格来说,库链接应该出现在用到库符号的项的后面。
编译库和编译程序的重要选项应该一致,不然可能link不了,比如编译器版本应该一样。

五、更多编译选项

-Dmacro表示#define macro
-O表示优化,-O3是最高级别优化,但是GCC建议一般用O2为好。
https://gcc.gnu.org/onlinedocs/gnat_ugn/Optimization-Levels.html

另外一个要注意的地方是debug和release
使用makefile得到的成果本质上没有debug和release之分。为什么呢?
因为成果已经是二进制了,不存在调试的问题。于是debug和release的区别就是是否进行了优化。一般在makefile中使用-O2来指示优化级别。所以得到的输出可以认为就是release版。
在引用库的时候只要引用即可,不用管到底是debug还是release
那么问题是windows下为什么存在debug和release?除了优化外,更重要的是因为VS允许进入库源代码中打断点进行调试,这样一来必须为库区分版本,不然无法做到。而Xcode应该是不允许这样的,所以也就无所谓库的版本的。
当然Xcode的项目还是区分debug和release的,这是因为项目本身的代码可以调试并优化。

还可以手动增加其他不常用的选项。具体见编译器文档。

假设这里使用asio coroutine写了一个服务器,那么makefile就需要这么写
(注,这里是在MacOS上使用Xcode的编译写法,放在g++上写法不一样,见ASIO协程专题)

需要增加一堆编译选项,但是能够成功编译