在做算法部署的过程中,我们一般都是用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(<variable> "<help_text>" [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 <dir> [...])
#文件夹创建
file(MAKE_DIRECTORY [<dir>...])

三、编译示例

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

|-- 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

CMake技术总结的更多相关文章

  1. SLAM+语音机器人DIY系列:(二)ROS入门——2.ROS系统整体架构

    摘要 ROS机器人操作系统在机器人应用领域很流行,依托代码开源和模块间协作等特性,给机器人开发者带来了很大的方便.我们的机器人“miiboo”中的大部分程序也采用ROS进行开发,所以本文就重点对ROS ...

  2. 【转载】ROS系统整体架构

    目录 1.从文件系统级理解 2.从计算图级理解 3.从开源社区级理解 由于ROS系统的组织架构比较复杂,简单从一个方面来说明很难说清楚.按照ROS官方的说法,我们可以从3个方面来理解ROS系统整体架构 ...

  3. 编译哈工大语言技术平台云LTP(C++)源码及LTP4J(Java)源码

    转自:编译哈工大语言技术平台云LTP(C++)源码及LTP4J(Java)源码 JDK:java version “1.8.0_31”Java(TM) SE Runtime Environment ( ...

  4. (转) SLAM系统的研究点介绍 与 Kinect视觉SLAM技术介绍

          首页 视界智尚 算法技术 每日技术 来打我呀 注册     SLAM系统的研究点介绍 本文主要谈谈SLAM中的各个研究点,为研究生们(应该是博客的多数读者吧)作一个提纲挈领的摘要.然后,我 ...

  5. Cmake调用NSIS(一个可执行文件,其实就是一个编译器)编译NSIS脚本问题研究

    技术经理说,可以用Cmake当中的add_custom_command,add_custom_target命令来使用. 我初次研究了下,add_custom_command应该用官方文档中说明的第二种 ...

  6. 收藏的技术文章链接(ubuntu,python,android等)

    我的收藏 他山之石,可以攻玉 转载请注明出处:https://ahangchen.gitbooks.io/windy-afternoon/content/ 开发过程中收藏在Chrome书签栏里的技术文 ...

  7. 如何用cmake编译

    本文由云+社区发表 作者:工程师小熊 CMake编译原理 CMake是一种跨平台编译工具,比make更为高级,使用起来要方便得多.CMake主要是编写CMakeLists.txt文件,然后用cmake ...

  8. Clion+Cmake+Qt5+Qwt+msys2+MinGW在Windows下的安装配置使用教程

    摘要: CLion, a cross-platform C/C++ IDE. 本文主要介绍基于Clion作为IDE, MinGW作为编译器,CMake作为项目构建工具,开发基于Qt5.qwt的C++图 ...

  9. 【OCR技术系列之八】端到端不定长文本识别CRNN代码实现

    CRNN是OCR领域非常经典且被广泛使用的识别算法,其理论基础可以参考我上一篇文章,本文将着重讲解CRNN代码实现过程以及识别效果. 数据处理 利用图像处理技术我们手工大批量生成文字图像,一共360万 ...

随机推荐

  1. Atomic 的实现原理

    1.直接操作内存,使用Unsafe 这个类 2.使用 getIntVolatile(var1, var2) 获取线程间共享的变量 3.采用CAS的尝试机制(核心所在),代码如下: public fin ...

  2. 学习 Haproxy (一)

    haproxy是一个开源的.高性能的基于tcp和http应用代理的HA的.LB服务软件,它支持双机热备.HA.LB.虚拟主机.图形界面查看状态信息等功能,其配置简单.维护方便,而且后端RS的healt ...

  3. JavaScript的取值小技巧之“中括号[]取值法”

    一.简介 做下记录,今天看了一篇很有意思的文章,学到了这个取值的小技巧 正常的话我们一般都是用对象直接去'.'对应的属性名(也就是键值对的键)来获取对应的值 这里记录的是另一种取值方式,他是采用中括号 ...

  4. 每天坚持一个CSS——社会人

    每天一个CSS-社会人 实现效果 想法 之前看到一篇博客,使用python绘制出了小猪佩奇,所以自己想试一试,采用纯html + CSS绘制出低配版的小猪佩奇. 实现思路 使用上一篇,圆与边框实现.最 ...

  5. 使用Dropbox搭建静态网站详细教程

    DropBox是一款非常好用的免费网络文件同步工具,是Dropbox公司运行的在线存储服务,通过云计算实现因特网上的文件同步,用户可以存储并共享文件和文件夹.今天小z和大家分享一下如何使用dropbo ...

  6. 设计模式之:工厂方法模式FactoryMethodPattern的实现

    本例用到了配置文件.接口.反射.多态: 满足的设计原则: 通过工厂,实现创建对象和使用对象的分离,实现松耦合,满足迪米特法则: 通过配置文件指定创建对象类型,而不需更改源代码,满足开闭原则: 容易实现 ...

  7. 每日所学之自学习大数据的Linux环境配置2

    今天设置网络 出现报错 明天找时间解决 不用解决了 刚才试了以下 又能下载了 描述一下问题: cannot find a valid baseurl for repo:base/7/x86_64 如果 ...

  8. Myeclipse 中怎样更改web项目的访问名

    第一步:在要修改的项目名称上右击选择最下面一列的"prepertise"(属性),进入属性设置界面. 第二步:找到左侧菜单栏的"Myeclipse"中的web项 ...

  9. ztree详解

    1.添加样式.js <link rel="stylesheet" href="${ctx}/hollybeacon/resources/plugins/zTree_ ...

  10. javaScript设计模式:发布订阅模式

    发布订阅模式的思想是在观察者模式的基础上演变而来,在观察者模式中客户端监听到对象某个行为就触发对应任务程序.而在发布订阅模式中依然基于这个核心思想,所以有时候也会将两者认为是同一种设计模式.它们的不同 ...