CMake技术总结

虚幻大学 xuhss 137℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

在做算法部署的过程中,我们一般都是用C++开发,主要原因是C++的高效性,而构建维护一个大型C++工程的过程中,如何管理不同子模块之间的依赖、外部依赖库、头文件和源文件如何隔离、编译的时候又该如何相互依赖这些问题,直接用Makefile实现是比较麻烦的。这个时候,CMake的优势就显现出来了,简洁的命令大大简化了项目构建过程,而且其跨平台特性也方便了不同部署平台间的迁移。这里我想把工作这一年来,在实践过程中学到的CMake用法做个总结。这里会参考一篇在知乎写的非常不错的文章,但这里我只记录我认为比较重要的部分,从来不会用到的功能不去深究,毕竟只是个工具,够用就行。

一、CMake构建编译原理概述

  • 单个cpp文件可以通过gcc直接编译生成可执行文件,但当项目很大时,这种方式便不再适用,我们需要写Makefile或者CMake。
  • CMake构建C++工程其实是充当一个生成Makefile的媒介,以往直接写Makefile也是可以的,但是当工程越来越复杂的时候,Makefile就不那么好写了,目前也不要求自己学会写Makefile了;
  • cpp工程一般由头文件目录、源文件目录和第三方库目录三大块代码内容组成,CMake一般会在每个模块文件夹下都建立一个CMakelists.txt文件,而在最顶层的源文件目录下,会建立一个总的CMakelists.txt用于控制整个cmake流程,然后通过add_subdirectory()命令递归的访问每个模块目录执行cmake,最后在build目录下生成一个总的makefile用于编译源码。头文件目录存放最终SDK提供出去需要的头文件、以及一些需要源文件目录访问的接口类定义头文件,源文件下的代码存放实现类,大致如此。CMake中需要配置每个模块编译时头文件需要从哪里找、还有链接的时候库文件需要从哪里找。
  • gcc编译生成的目标文件分为三类,可执行文件、动态库和静态库。其中可执行文件在链接过程中会链接一些系统c运行时库等,需保证可执行文件对应的源码中main函数是存在的,不然会链接失败。动态库和静态库可以朴素的理解为就是一系列的cpp文件打包而成的,cpp文件中会定义一些类和函数可供调用,此外还有一些全局变量。

二、CMake用法总结

2.1 使用与设置系统环境变量与系统信息


|  | $ENV{Name} # 使用环境变量 |
|  | set(ENV{Name} value) # 写入环境变量, 这里没有`$`符号 |
|  | ­UNIX # Linux平台下该值为 TRUE |
|  | ­WIN32 # Windows平台下该值为 TRUE |

2.2 CMake预定义变量


|  | PROJECT\_SOURCE\_DIR # 工程的根目录,即根CMakefiles.txt文件所在目录 |
|  | PROJECT\_BINARY\_DIR # 运行 cmake 命令的目录,通常是 ${PROJECT\_SOURCE\_DIR}/build  |
|  | CMAKE\_CURRENT\_SOURCE\_DIR # 当前处理的 CMakeLists.txt 所在的路径 |
|  | CMAKE\_CURRENT\_BINARY\_DIR # target(包括可执行文件与库文件) 编译目录 |
|  | CMAKE\_CURRENT\_LIST\_DIR # CMakeLists.txt 的完整路径 |
|  | CMAKE\_MODULE\_PATH # 自己的 cmake 模块所在的路径,SET(CMAKE\_MODULE\_PATH ${PROJECT\_SOURCE\_DIR}/cmake) |
|  | EXECUTABLE\_OUTPUT\_PATH # 目标二进制可执行文件的存放位置 |
|  | LIBRARY\_OUTPUT\_PATH # 目标链接库文件的存放位置 |
|  |  |
|  | CMAKE\_CXX\_FLAGS # 设置 C++ 编译选项,如优化等级、c++版本等 |

2.3 常用命令

  • 基本命令

|  | cmake\_minimum\_required(VERSION3.20.1) #声明最低cmake版本要求,当执行cmake命令启动时检测到版本不符合要求时会提醒 |
|  | project(demo) #设置项目名称,在windows下camke会生成对应的VS sln文件  |
|  | option( "" [value]) #设置编译选项,用于控制选择编译方案 |
|  |  |
|  | add\_executable(demo demo.cpp) # 生成可执行文件 |
|  | add\_library(common SHARED util.cpp) # 生成动态库或共享库 |
|  | set\_property(TARGET common PROPERTY POSITION\_INDEPENDENT\_CODE ON) # 代表-fPIC,生成位置无关的动态库文件 |
  • set——设置变量

|  | set(SRC\_LIST main.cpp) # 设置变量 |
|  | list(APPEND SRC\_LIST test.cpp) #追加文件到变量list |
|  | list(REMOVE\_ITEM SRC\_LIST main.cpp) #从变量列表中移除文件 |
  • if——条件判断

|  | if (expression) # expression 不为空(0,N,NO,OFF,FALSE,NOTFOUND)时为真 |
|  |  |
|  | #数字比较: |
|  | if (variable LESS number) # LESS 小于 |
|  | if (string LESS number) |
|  | if (variable GREATER number) # GREATER 大于 |
|  | if (string GREATER number) |
|  | if (variable EQUAL number) # EQUAL 等于 |
|  | if (string EQUAL number) |
|  |  |
|  | #字母表顺序比较: |
|  | if (variable STRLESS string) |
|  | if (string STRLESS string) |
|  | if (variable STRGREATER string) |
|  | if (string STRGREATER string) |
|  | if (variable STREQUAL string) |
|  | if (string STREQUAL string) |
  • 循环

|  | #while 循环 |
|  | while(condition) |
|  |  ... |
|  | endwhile() |
|  |  |
|  | # for循环 |
|  | # start 表示起始数,stop 表示终止数,step 表示步长 |
|  | foreach(loop\_var RANGE start stop [step]) |
|  |  ... |
|  | endforeach(loop\_var) |
  • function——函数

|  | # 定义一个简单的打印函数 |
|  | function(\_foo) |
|  | foreach(arg IN LISTS ARGN) |
|  | message(STATUS "this in function is ${arg}") |
|  | endforeach() |
|  | endfunction() |
|  |  |
|  | \_foo(a b c) |
|  | # this in function is a |
|  | # this in function is b |
|  | # this in function is c |
  • 指定当前编译需要包含的源文件

|  | aux\_source\_directory(dir VAR) 将目录下所有的源代码文件列表存储在一个变量中 |
|  | file(GLOB SRC\_LIST "*.cpp" "protocol/*.cpp") #按字符串匹配的文件设置变量 |
|  | file(GLOB\_RECURSE SRC\_LIST "*.cpp") # 递归搜索匹配 |
|  | add\_library(demo SHARED ${SRC\_LIST} ${SRC\_PROTOCOL\_LIST}) # 最后将所有源文件编译到一个动态库文件中,其中链接过程会对不同源文件中的定义式进行整合。 |
  • 查找指定的库文件或package

|  | find\_library(VAR name path) #查找指定名称的库文件,并将路径存储到VAR中,其中path是库文件所在目录。 |
|  | find\_package(<Name>) # 通过寻找 Find<name>.cmake文件引入其他包,具体搜索路径依次为:1. ${CMAKE\_MODULE\_PATH}中的所有目录;2. 再查看CMake自己的模块目录 /share/cmake-x.y/Modules/,通过$CMAKE\_ROOT可查看;3. 在~/.cmake/packages/或/usr/local/share/中的各个包目录中查找<库名字的大写>Config.cmake 或者 <库名字的小写>-config.cmake。 |
|  | 找到上述.cmake文件后,就会定义下述几个变量: |
|  | <NAME>\_FOUND #判断查找是否成功 |
|  | <NAME>\_INCLUDE\_DIRS or <NAME>\_INCLUDES #package的头文件包含目录 |
|  | <NAME>\_LIBRARIES or <NAME>\_LIBRARIES or <NAME>\_LIBS # package的库目录 |
|  | 然后就可以使用该库了。 |
  • 设置包含目录
    只有将包含目录设置了,在源文件中的include才能正确索引到头文件

|  | include\_directories(${CMAKE\_CURRENT\_SOURCE\_DIR}/include) |
  • 添加子目录并用CMake构建子目录
    CMake一个很好的功能就是,可以在个子目录设置单独的CMakelists.txt,然后再上一层的Cmakelists.txt中添加该子目录即可,例如:

|  | # 其中若设置EXCLUDE\_FROM\_ALL参数,则默认不编译该目录;binary\_dir指定编译target输出目录 |
|  | add\_subdirectory(source\_dir [binary\_dir] [EXCLUDE\_FROM\_ALL])  |
  • 设置链接库搜索目录

|  | link\_directories(${CMAKE\_CURRENT\_SOURCE\_DIR}/libs) |
  • 设置target需要链接的库文件

|  | target\_link\_libraries(demo libface.a) |
  • 打印log信息
    有些时候我们需要在终端打印某个变量以确定是否符合预期

|  | message(STATUS ${PROJECT\_SOURCE\_DIR}"this is warnning message") # 状态信息,显示变量 |
|  | message(WARNING "this is warnning message") # 警告信息 |
|  | message(FATAL\_ERROR"this is error message") # 错误信息,终止生成 |
  • 文件操作

|  | #文件拷贝 |
|  | file({COPY | INSTALL} <file>... DESTINATION  [...]) |
|  | #文件夹创建 |
|  | file(MAKE\_DIRECTORY [...]) |

三、编译示例

假如有如下结构的示例工程:


|  | |-- build # 编译输出目录 |
|  | |-- cmake # 自定义命令目录 |
|  | | |-- utils\_function.cmake |
|  | | `-- utils\_macro.cmake |
|  | |-- CMakeLists.txt # root cmake脚本(1) |
|  | |-- include # 公共头文件目录 |
|  | | |-- config.h |
|  | | `-- public.h |
|  | |-- source # 源码目录 |
|  | | |-- CMakeLists.txt # cmake脚本(2) |
|  | | |-- mod\_1 |
|  | | | |-- CMakeLists.txt # cmake脚本 |
|  | | | |-- include |
|  | | | | `-- mod\_1.h |
|  | | | |-- src |
|  | | | | `-- mod\_1.cpp |
|  | | `-- mod\_2 |
|  | | | |-- CMakeLists.txt # cmake脚本(3) |
|  | | | |-- include |
|  | | | | `-- mod\_3.h |
|  | | | `-- src |
|  | | | `-- mod\_3.cpp |
|  | | |- test # 一般为单元测试代码 |
|  | | |-- CMakeLists.txt # 可执行文件cmake脚本 (4) |
|  | | `-- main\_total.cpp |
|  | | `-- test\_module1.cpp |
|  | |-- build.sh # 编译脚本 |
|  | |-- libs # 第三方依赖库 |
  • 一般的CPP工程都按照上述结构组织,源码只存放在source和include文件夹中,其中include存放公共头文件,一般是一些需要提供出去的虚接口类;source下也会按照模块分别有每个模块的.h头文件和.cpp源文件,分别存放class声明和成员函数实现。此外还有单元测试代码,长期看单元测试是十分必要的。C++的STL我们可以直接使用,但第三方库需要引入才能使用,较小的库可以随工程放入单独的文件夹内,例如libs或者3rdparty文件夹下可以将opencv放进去,但像cuda这种很大的库,一般还是会从系统安装目录动态链接过来。
  • 上述工程目录中的CMake脚本的工作逻辑是:先shell命令创建build目录,然后在cd到build目录后执行cmake ..,这样就搜索到了根目录下的Cmakelists.txt,然后按顺序执行其中的命令,这个cmake脚本中需要做的工作包括:①项目名称设定、option开关设定、CMAKE_CXX_FLAGS设定;②外部头文件包含目录设定;③第三方库文件引入(opencv和cuda);④添加需要编译的子目录。然后递归执行子目录中的cmake流程。
  • cmake过程中一般比较容易出现的问题是:库找不到。一般都是路径不对或者相关库未安装。
    make过程中一般出现的问题是:头文件找不到、重定义、链接失败等。这些问题也需要返回到cmake脚本中修复。

CMakelists.txt示例:
本来是要在windows的SWL上写一个demo的,手欠先升级到了SWL2,结果之前的子系统登不进去了,又得重新配置ubuntu编译环境。样例这块就先不实践了。后续写c++项目的时候,再对其中的CMake做一次解析。

四、小结

这次花了一天时间对cmake的相关内容回顾和总结了一下,但工具不用很快就忘记了,这类东西最好还是在工作实践过程主动去尝试去思考,平时一个工程如果架构构建完成后,cmakelist是很少去动的,所以从头写的机会比较少。那么就要在看到别人的cmakelists.txt的时候,多想想为什么他这样写,学习别人是如何写出简洁的cmakelists.txt文件的。cmake的思路其实和编译原理是相辅相成的,学会cmake对我们理解项目架构、解决库依赖问题很有帮助。

参考:

  1. https://zhuanlan.zhihu.com/p/470681241

转载请注明:xuhss » CMake技术总结

喜欢 (0)

您必须 登录 才能发表评论!