0x00 引言
一个完整的软件是由很多函数库组成的,那么编译程序或者可执行程序是如何知道该怎么调用和组织这些.cpp/.h/.so/.a等等后缀名的文件呢?通常我们会处在比较高层的位置,很多IDE软件都能帮我们自动完成这些任务,比如Windows平台上就会使用Visual Studio,在Linux平台上就会使用QT creator或者CLion等[1]。但再深一层次地说,编译时文件之间的组织关系都是由一个名叫makefile的文件来完成的,只不过IDE已经代替完成了这件复杂的任务,所以通常我们对该文件并不熟悉。
makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。但是makefile语言本身比较抽象,让程序猿来手写是件非常费时费力的事情,于是CMake便应运而生了。CMake是比makefile抽象层次更高的项目管理工具,CMake能够通过命令转换成makefile文件,而所有一切软件设计人猿需要实现的编译任务均写在名为CMakeLists.txt文件内。
本文将从两方面入手,来剖析和学习CMake。一个方向是从CMake的官方文档[2]入手,当然也可以看中文版文档[3],推荐看中文版资料,详细且循序渐进,代码[4]可在git上找到,该方向不再赘述;第二个方向是从一些比较实际的编译目标入手。
0x01 CMake可移植程序编译
由于程序经常需要在不同电脑之间使用,但是每次都需要重新搭建环境非常繁琐,现在希望通过CMake编译,能够将可执行程序用到的三方库打包,那么在统一个ubuntu系统下即可即插即用。
1 | 1 基础知识 |
1、基础知识
1.1、静态库与动态库
现在的软件基本不可能全部是自己写的函数或类,我们经常需要使用别人的函数或类,那么就有必要得到那些软件的三方库,而库又分为静态库和动态库,在Windows下分别为.lib/.dll后缀文件,在Linux下分别为.a/.so文件。所谓静态库就是编译时和源代码一起被打包入可执行程序的库,而动态库是一些可重复使用的模块,他们的具体区别可参考教程[5]。
1.2、CMake基础编译方式
看过基本的中文教程demo1可知,CMake实际上都在CmakeList.txt文件内描述完成,一旦写完,只需在顶层的CmakeList.txt文件目录下执行以下语句:
1 | #做配置 |
1.3、Ubuntu搜索路径方式
编译之后的可执行程序在其他电脑上无法运行,实质上最关键的问题就在于新的操作系统环境下,程序不知道去什么地方寻找依赖库。Ubuntu系统默认的.so库搜索方式[6]根据优先级依次为:1)-L;2)/usr/lib;3)/lib;4)LIBRARY_PATH。
第二三种方法,系统只会该目录下的文件,而不会遍历文件夹,若要识别文件夹下的文件,还需要修改/etc/ld.so.conf,并调用ldconfig命令,这里不再深究。
主要讲的是最后一种方法,修改LIBRARY_PATH,其实就是我们熟知的修改环境变量[7],所以ROS也是这样,要执行rospack find就需要添加环境变量(该文件实质上是命令行每次开启时执行的一段命令,所以这样修改环境变量来运行程序的方法必须是使用./程序,双击的话就无法运行,除非新建一个快捷方式并指定优先运行一段export),那么可以采用:
1 | #向/.bashrc中填入搜索路径 |
1.4、CMake RPATH简介
实际上,CMake在编译和安装后生成的可执行程序头部都会有一个RPATH参数[8],其用来表示搜索库的路径。操作系统本身搜索库路径的方式有环境变量和默认路径,但RPATH能跟随程序本身,所以不受程序所在环境的制约。
1.5、可执行程序链接库批量处理
make步骤之后生成的中间可执行程序,在Ubuntu下可通过指令ldd拿到所有用到的链接库,可通过制作.sh脚本的方式将链接库文件批量导入目标文件夹中,具体步骤如下:
1 | #1、在build文件夹下新建脚本test.sh |
2、CMake编译可移植程序方案
2.1、方案一:添加环境变量
具体思路为:1)可执行程序链接库批量处理至CmakeList.txt根目录下;2)CMake将批量处理得到的动态链接库install至目标文件夹下;3)在目标机器的~/.bashrc文件中添加第二步链接库install所在路径;
思路其实非常简单,但是关键的难点在于链接库取得的技巧,这里有必要叙述一下。第一点为取得可执行程序的所有动态链接库,第二点为分析取得所有未被ldd出来的动态链接库。
难点一已经在1.5节中指出,难点二这里以取得Qt所有动态链接库为例展开讨论。实现上述三个步骤以后,我们修改LIBRARY_PATH参数,在可执行程序根目录下输入命令行:
1 | echo "export LD_LIBRARY_PATH=目标路径" >>~/.bashrc |
发现程序报错,错误内容如下:
1 | This application failed to start because it could not find or load the Qt platform plugin "xcb" |
根据上述提示可知,缺少了名为xcb的一个链接库,查阅资料[9]可知我们需要在可执行目录下新建platforms文件夹,并添加libqxcb.so文件(该文件在qt的/Qt5.7.1/5.7/gcc_64/lib目录下,包括qt所有的链接库都在这个文件夹下面。如何修改该libqxcb.so链接库位置仍未知)。继续执行可执行程序,发现报出同样错误,为了找到错误原因,这里引入一个技巧[10],即设置命令行的环境变量参数QT_DEBUG_PLUGINS,并修改/.bashrc文件,使其在命令行开启前执行:
1 | echo "export QT_DEBUG_PLUGINS=1" >>~/.bashrc |
再次运行可执行程序,报错出现具体的错误原因:
1 | Found metadata in lib /home/cobot/PathImporter/build/platforms/libqxcb.so, metadata={ |
可见文件夹里实质上还缺少一个名为libQt5XcbQpa.so.5的动态链接库,那么我们只需从qt的lib下找到该库并放入我们的目标文件夹下。再次运行程序并报错:
1 | Cannot mix incompatible Qt library (version 0x50201) with this library (version 0x50701) |
大意是Qt版本5.2.1与5.7.1的链接库不匹配,无法融合。可知实际上我的系统里存在多版本的Qt,Qt实质上会在默认库路径/usr/lib下生成一些必要的库,这也是报错的原因。这里有一个解决的技巧,即将所有lib文件全部放入目标文件夹,并执行程序,最后发现程序正常运行,可见库中必然还存在一个特定的动态链接库,使用二分法,多次将不同文件放入目标文件夹运行,迭代排查后发现缺少链接库libQt5DBus.so.5链接库,将其放入目标文件夹,运行正常(这个方法很牛逼,我都佩服我自己,全网基本上没有资料说这个文件的)。
这里还有两个细节,第一点libQt5DBus.so.5是一个类似于快捷方式的文件,它会链接到libQt5DBus.so.5.7.1上(库里还有一个libQt5DBus.so.5.7,暂时不知道何用),所以最后只需要取出libQt5DBus.so.5.7.1并重命名为libQt5DBus.so.5即可,链接库libQt5XcbQpa.so.5同理。第二点,有必要猜测下这三个链接库存在的意义,libqxcb.so应该是qt的入口库,然后libQt5XcbQpa.so.5是实际运行的函数,并且链接到了5.7.1的子版本上(方便版本控制),而这里的libQt5DBus.so.5应该是做Qt不同版本之间的一个解释器。
2.2、方案二:安装至默认lib文件夹
方案一虽然可以顺利运行,但是每次要修改环境变量实际上是非常麻烦的事情,且并不是所有软件使用者都有这个觉悟,那么我们要考虑更加便捷的方法,即将库文件用CMake install至系统默认搜索的库,即在1.3节中已经叙述过的/usr/lib与/lib位置。在1.3节也讲过了,如果将所有库文件放入lib文件夹下的子文件夹内,则系统仍搜索不到链接库(如果要采取修改/etc/ld.so.conf以及添加ldconfig命令,则还不如方案一便捷)。唯一的办法就是将所有库文件全部install至lib文件夹下,运行程序完美报错:
1 | ./PathImporter: /usr/lib/x86_64-linux-gnu/libQt5Gui.so.5: no version information available (required by ./PathImporter) |
未知错误(应该是多版本Qt的原因,还害得我修改了lib库所有文件权限,导致sudo用不了、系统时间出错、U盘都不出等一系列尿频尿急症状T_T)等待填坑。
*2.3、方案三:CMake添加RPATH
在1.4节中已经叙述过CMake RPATH的用途,该方案可以说是最便捷和人性化的了,第一不用修改环境变量,第二在make与make install的过程中就能决定中间执行程序与安装执行程序的库链接路径。
根据1.4节中提到的博客[8],我们只需要在CmakeList.txt的install部分添加如下指令:
1 | #方法一:在CMakeLists.txt文件中修改 |
这里依旧分享两个小技巧
1 | # 使用指令readelf可以查看某一可执行程序的RPATH |
2.4、方案四:CMake静态编译
除了上述三种调控动态链接库位置与链接的方式之外,我们还可以用CMake实现静态编译,即将三方库的动态链接库或者静态链接库加上自己的程序编译在一起,得到不需要外接库的可执行程序,在1.1节之中已经分析过了这种程序的优缺点(小型项目还好,一旦做大型项目,不但编译慢而且运行的电脑也会被大量占用空间)。目前该方式只在网上搜索到设置CMake设置静态编译的一段命令:
1 | set(CMAKE_CXX_FLAGS "-static ${CMAKE_CXX_FLAGS}") |
该方法等待填坑(不了解意思,执行之后也会报错)。
0x02 链接库选择路径
由于链接库路径选择的重要性,这一章节着重分析一下Linux链接库选择路径顺序及其修改方式。
1 | 1 Linux动态库选择顺序 |
1、Linux 动态库选择顺序
1 | # gcc 编译程序时查找SO顺序 |
2、Linux如何添加链接库搜索路径
1 | # ===== 方案一 ===== |
3、如何修改Qt链接库路径
为了少踩编译中或编译后的坑,请注意以下事项:
1 | 1.编译Qt、DTK 的生产目录,一定不要设置在 /usr 目录,这样非常容易桌面系统崩溃 |
*4、CMake链接库三要素
- .so / .o 文件
- 库头文件
- 环境变量
0x03 CMake常用变量
1 | # 在CMake3.1之后设置编译器使用c++11的方法 |
0x04 CMake学习中获得优质且繁杂的资源
CMake中调用Qt模块[11]
CMake中add_library include_library以及target_link_libraries区别[12]
CMake构建动态、静态库[13]
CMake的一些变量[14]
CMake跨平台编译以及静态编译[15]
0x05 引用文献
[1]https://blog.csdn.net/caowei880123/article/details/52497550
[2]https://cmake.org/cmake-tutorial
[3]https://www.hahack.com/codes/cmake
[4]https://github.com/wzpan/cmake-demo
[5]https://www.runoob.com/w3cnote/cpp-static-library-and-dynamic-library.html
[6]https://blog.csdn.net/qq_16097611/article/details/53484724
[7]https://www.cnblogs.com/trying/archive/2013/06/07/3123577.html
[8]https://www.cnblogs.com/rickyk/p/3884257.html
[9]https://blog.csdn.net/u010168781/article/details/82150105
[10]https://blog.csdn.net/sinat_26106275/article/details/82778951
[11]https://www.jianshu.com/p/7eeb6f79a275
[12]https://blog.csdn.net/bigdog_1027/article/details/79113342
[13]https://www.cnblogs.com/zhoug2020/p/5904206.html
[14]https://cmake.org/cmake/help/v3.0/manual/cmake-variables.7.html
[15]https://zilongshanren.com/blog/2014-08-31-how-to-use-cmake-to-compile-static-library.html