在做算法部署的过程中,我们一般都是用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. 攻防世界NewsCenter

    NewsCenter 打开题目是一个搜索框我们首先尝试一下sql注入 判断了一下是使用''进行包裹的字符型sql注入 然后我们需要判断数据库列数 1' order by 3# 回显正常但by4的时候回 ...

  2. 二十二、导入DXF文件

    x

  3. PCB各层的含义

    阻焊层:solder mask,是指板子上要上绿油的部分:因为它是负片输出,所以实际上有solder mask的部分实际效果并不上绿油,而是镀锡,呈银白色! 助焊层:paste mask,是机器贴片时 ...

  4. HTML5打造原生应用——Ionic框架简介与Ionic Hello World

    试了试用Ionic框架打造了两个应用,然后在Google Play上架了. 程序语言答人 教你设计物联网 更有意思的是这是在一周的业余时间内完成的三个应用中的两个,接着让我们看看这个框架如何实现高效地 ...

  5. .map() vs .forEach() vs for() 如何选择?

    访问原文地址 .map() vs .forEach() vs for() 笔者说,自己基本没怎么用过for()来遍历,主要是用.forEach(). 但是总是会被很多朋友说,这些人认为for()的速度 ...

  6. 项目中常用到的布局 flex

    1. 没header,footer固定 html<div class="page"> <div class="top"> <div ...

  7. Python入门-pip模块管理工具

    安装 # 在线安装 pip install <包名> 安装后,该模块文件会在安装python环境目录:lib/packages目录下 # 安装本地安装包 pip install <目 ...

  8. 线性表(python实现)

    线性表 1 定义 线性表是由 \(n(n>=0)\)个数据元素(节点)\(a1.a2.a3.-.an\) 成的有限序列.该序列中的所有节点都具有相同的数据类型.其中,数据元素的个数 \(n\) ...

  9. Mycat管理

    Mycat 提供了类似数据库的管理监控方式,可以通过 mycat 端口 9066 执行相应的 SQL 语句进行管理,可以可以通过 navicat 软件进行远程连接管理,再使用 MySQL 命令行界面登 ...

  10. echarts饼图去除圈外指向横线

    series: [ { name: '实时人员信息', type: 'pie', radius: ['86%', '80%'], avoidLabelOverlap: false, label: { ...