cmake
cmake
ZEROKO14CMake是用于构建,测试,和软件打包的开源跨平台工具
参阅书籍:<<cmake菜谱>>
Linux,Windows,和macOS系统中库的名称
Windows
windows下的库文件(下面非必须,只是大部分开源库的习惯)
库文件包含静态库和动态库,而windows版本的静态库和动态库又各自有Release
静态库
库名.lib是 release 模式下生成的库文件,用于发布版本;经过了优化的,并不包含调试信息库名_d.lib或库名d.lib是 debug 模式下生成的库文件,用于调试版本。debug 模式下生成的库文件包含了额外的调试信息,以方便调试程序时进行源码级别的跟踪
动态库
同时生成两个文件,二者是关联的
库名.lib(文件很小,不包含真正的源代码,只在编译阶段需要用到) 函数地址索引库名.dll函数二进制代码
在Windows平台上编译动态链接库时,通常会生成一个.lib文件和一个.dll文件。其中,.lib文件用于编译阶段的链接,动态链接库的实际代码和数据存放在.dll文件中。因此,.lib文件只在开发环境中使用,用于指示应用程序在运行时需要加载和使用哪个动态链接库,并提供符号和函数等信息。
在发布二进制程序包时,可以不必包含.lib文件,因为用户运行程序时已经没有必要再链接动态库了。可以将.dll文件单独打包或放在系统路径或应用程序路径下,供程序运行时动态链接使用。因此,可以直接删除动态库生成的.lib文件,而保留.dll文件即可。
Linux
linux/mac下静态库没有windows上Debug和Release的区分(LINUX包含Android,鸿蒙这种以linux为内核的系统)
- 静态库为
lib库名.a如libxlog.a - 动态库为
lib库名.so如libxlog.so格式:libname.so.主版本号.次版本号.发行版本号
使用库的时候,对静态库和动态库其实是无感的,有静态库链接静态库,有动态库链接动态库。链接什么库,主要是跟库的路径相关
macOS
- 静态库为
lib库名.a如libxlog.a - 动态库为
lib库名.dylib如libxlog.dylib(注意与linux不同)
大部分商业库在没有授权的情况下是不允许使用静态链接的,静态链接属于侵权。因为静态库看不出来用哪一个库。
静态库:缺点:程序会比较大,会涉及版权问题,会拖慢编译速度,仅windows涉及的一个问题(如下)
windows中线程库的静态和动态都有Debug和Release两个版本,你的库链接了线程静态库的Debug版本,别人程序本身链接了线程静态库Release版本,还链接了你的动态库会产生冲突问题 ,不是同一个Release/Debug版本时候可能会产生冲突。因为Debug和Release版本的线程库可能有不同的实现和接口,链接时也存在不同的调试符号和优化选项,如果将一个使用Debug版本的库和一个使用Release版本的库链接在一起,可能会导致程序出现各种难以预料的错误。
优点: 不需要环境提前具备动态库
Cmake
CMake是用于构建,测试,和软件打包的开源跨平台工具
持续集成
- 每次集成都通过自动化的制造(包括提交,发布,自动化测试)来验证,准确地发现集成错误
- 快速错误,每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易
- 各种不同的更新的主干,如果不经常集成,会导致集成的成本变大
- 让产品可以快速地通过,同时保持关键测试合格
- 自动化测试,只要有一个测试用例不通过就不能集成
- 集成并不能删除发现的错误,而是让它们很容易和改正
Cmake特性
- 自动搜索可能需要的程序,库和头文件的能力
- 独立的构建目录,可以安全清理
- 创建复杂的自定义命令(例如 qt moc uic)
- 配置时选择可选组件的能力
- 从简单的文本文件(CMakeLists.txt)自动生成工作区和项目的能力
- 在静态和共态构建之间轻松切换的能力
- 在大多数平台上自动生成文件依赖项并支持并行构建
- 每个IDE都支持CMake(CMake支持几乎所有IDE)
- 使用 CMake 的软件包比任何其他系统都多
安装
两种安装方式
- 源码编译安装
- 二进制文件直接安装
源码编译安装流程
ubuntu系统为例:
安装编译工具和依赖库
sudo apt install g++sudo apt install make或apt install ninja-buildsudo apt install unzipsudo apt install libssl-dev(openssl是个加解密工具,这里只安装他的库)
下载解压cmake源码并编译
- 下载
wget https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1.tar.gz - 解压
tar -xvf cmake-3.2 1.tar.gz cd cmake-3.2 1- 执行
./configure生成makefile(几乎所有开源软件,如果不支持cmake都是使用这个) - 编译源码:
make -j32(-j3232线程编译)
安装编译好的cmake
安装编译好的cmake sudo make install (默认安装路径在/usr/local/share/cmake-3.23)
设置cmake的运行路径
vi ~/.bash_profile- 文件中添加
export PATH = /usr/local/share/cmake-3.22:$PATH
运行cmake查看版本
cmake --version
可执行程序功能
cmake.exe用于生成- windows用
cmake-gui.exe图形化界面,linux用ccmake.exe在控制台下提供一个类图形化界面 cpack.exe用于打包ctest.exe用于测试
cmake如何执行编译功能
cmake -S . -B build [-G "可以指定使用nmake/ninja/Xcode等等其他编译工具"]生成编译需要的文件 ,-S表示source,源文件位置,-B表示生成的makefile,vs项目,ninja,nmake,xcode项目等文件生成的位置,指定的文件夹会自动生成(例子中是build目录)windows下生成的是vs项目,如果想使用nmake也可以。nmake用于windows(类似make),但只能在
x64 Native Tools Command Prompt for VS 2019程序中使用才能识别该指令mac下也可以通过
-G Xcode指定生成xcode项目,然后可以使用cmake --open 之前-B指定的位置方式用xcode打开项目。如果在执行cmake -S . -B build -G "Xcode"的情况下出现找不到C与C++项目,需要执行sudo xcode-select --switch /Applications/Xcode.app/,就不会再报该错误;还不行就执行cmake -S . -B build -DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++手动指定编译器路径,clang编译器路径使用which clang查询cmake --build build --config Release [-j32]通用的编译生成目标文件命令(替代统一make等的指令),build表示生成到build文件夹中,--config可以设置编译成Release版本或者Debug版本,-j32表示32线程编译cmake --install build将已经构建好的程序、库或头文件等文件安装到指定的目录下。这个命令会自动根据CMakeLists.txt文件中的指令来安排需要安装哪些文件,以及将它们复制到哪个目录下。
windows中文件名大小写不敏感,而linux中文件名大小写敏感,但是cmake做了处理,统一大小写不敏感。
$$
标准的cmake文件名: CMakeLists.txt
$$
通用动态库头文件格式
由于windows中含有独有指令__declspec(dllexport)和__declspec(dllimport),并且需要有__declspec(dllexport)标记的内容生成动态库时才会生成lib文件,而其他平台均不需要,因此头文件这么编写可以通用
1 | //xlog.h |
CMake注释
- 括号注释
cmake3.0开始引入的括号注释,格式:#[[ 注释内容 ]] - 行注释
#行注释,一直注释到行尾
CMake message
基础语法
格式:message([mode] arg1 arg2 arg3 ...) 空格分隔。输出会以拼接参数的形式进行输出,并在最后进行换行,例子如下:
如: message("参数1" "参数2" "参数3" "参数4") 输出为: 参数1参数2参数3参数4
日志级别
message用可省略的mode参数指定该内容显示的日志级别 如:
1 | message(FATAL_ERROR "test fatal_error") #指定该日志为FATAL_ERROR日志级别,该级别会停止cmake运行和生成 |
调用cmake时可以添加日志打印参数 --log-level=<ERROR|WARNING|NOTICE|STATUS|VERBOSE|DEBUG|TRACE> ,不填写的话默认为STATUS级别
指定显示的日志级别为TRACE的例子:cmake -S . -B build --log-level=TRACE
日志级别由低到高的,指定高级别会同时打印比他低的级别。
日志级别(由低到高)
FATAL_ERROR停止cmake运行和生成 打印到stderrSEND_ERRORcmake继续运行,生成跳过 打印到stderrWARNING包含WARNING级别此处上面的级别会同时打印代码路径和行号 打印到stderr不设置 或者 NOTICE打印到stderr (此级别到TRACE级别不会同时打印代码路径和行号) 打印到stderrSTATUS项目用户可能感兴趣的信息 这个等级往下打印消息前会添加前缀--打印到stdoutVERBOSE针对项目用户的详细信息 从这里往下调用cmake不指定–log-level默认不显示消息 打印到stdoutDEBUG项目本身的开发人员使用的信息 打印到stdoutTRACE非常低级实现细节的细粒度信息 打印到stdout
如果执行cmake -S . -B build >log.txt是默认把标准输出stdout重定向到log.txt文件
1表示stdout2表示stderr
cmake -S . -B build >log.txt 2>&1 可以同时把标准输出stdout和标准错误输出stderr重定向到log.txt,Windows/Linux/Mac通用
message Reporting checks查找库日志
Reporting checks 是一种常见的消息类型,用于报告检查的结果,例如 检查依赖项是否满足条件 或 检查编译器和构建环境 或 报告其他构建选项和参数 等
关键词
CHECK_START开始记录将要执行检查的消息CHECK_PASS记录检查的成功结果CHECK_FAIL记录检查的失败结果
案例:
1 | message(CHECK_START "查找xcpp") |
cmake变量
设置变量
set关键字
set(<variable> <value>) 将变量<variable>的值设置为<value>
如果没有指定 value,那么这个变量就会被撤销而不是被设置,也可以用 unset(<variable>)撤销变量
PARENT_SCOPE参数:用于在子目录中设置父目录中的变量:set(VAR1 "测试变量VAR1的值" PARENT_SCOPE)注意这样设置不会影响本地作用域,本地作用域就相当于什么也没做,VAR1的值没动过
变量使用
使用方式 ${变量名}
- 变量引用是值替换,如果未设置变量,返回空字符串
- 变量引用可以嵌套并从内向外求值
- 变量名大小写敏感
- 普通变量的作用域是自身和子目录 p.s.子目录1中设置的变量,主目录和子目录2都无法访问
1 | set(VAR1 "测试变量VAR1的值") |
通过变量让message输出不同的颜色
终端的颜色格式 : Esc的ASCII字符[显示方式;前景色;背景色m 内容 Esc的ASCII字符[m 中间的内容会被设置为对应颜色和显示方式
如:\033[1;31;40m 红色内容黑色背景 \033[m
| 显示方式值 | 对应的显示方式含义 |
|---|---|
| 0 | 终端默认设置 |
| 1 | 高亮显示 |
| 4 | 使用下划线 |
| 5 | 闪烁 |
| 7 | 反白显示 |
| 8 | 不可见 |
1 | #红色:Esc[0;31m |
cmake内建变量
提供信息的变量
PROJECT_NAMEproject()设置的项目名称改变行为的变量
BUILD_SHARED_LIBS设置为ON使add_library()默认构建动态库,设置为OFF默认构建静态库描述系统的变量
CMAKE_SYSTEM_NAME记录系统名控制构建过程的变量
CMAKE_COLOR_MAKEFILE生成的makefile是否有自带的颜色,ON/OFF控制(默认ON)
下面是常用变量:
| 变量名 | 描述 |
|---|---|
CMAKE_BINARY_DIR |
工程编译发生的根目录(一般是build文件夹) |
CMAKE_SOURCE_DIR |
工程的源码根目录 |
CMAKE_CURRENT_BINARY_DIR |
当前处理的CMakeLists.txt对应的二进制目录 |
CMAKE_CURRENT_SOURCE_DIR |
当前处理的CMakeLists.txt的路径 |
CMAKE_CURRENT_LIST_DIR |
当前处理的CMakeLists.txt文件所在的目录路径 |
CMAKE_CURRENT_LIST_FILE |
当前正在处理的文件的名称(文件的绝对路径,而不仅仅是文件名) |
CMAKE_CURRENT_LIST_LINE |
当前所在的行 |
CMAKE_PROJECT_NAME |
工程的名称 |
PROJECT_NAME |
最近一次调用project()命令时的工程名 |
CMAKE_C_COMPILER |
C编译器 |
CMAKE_CXX_COMPILER |
C++编译器 |
CMAKE_BUILD_TYPE |
构建类型(如Release或Debug) |
CMAKE_C_FLAGS |
设置C编译器的选项 |
CMAKE_CXX_FLAGS |
设置C++编译器的选项 |
CMAKE_TOOLCHAIN_FILE |
工具链文件的路径 |
CMAKE_PREFIX_PATH |
程序查找库文件(.so,.dll)和头文件(.h)的路径 |
CMAKE_MODULE_PATH |
程序查找自定义模块的路径 |
EXECUTABLE_OUTPUT_PATH |
可执行文件的输出路径 |
LIBRARY_OUTPUT_PATH |
库文件的输出路径 |
CMAKE_INCLUDE_PATH |
额外的头文件搜索路径 |
CMAKE_LIBRARY_PATH |
额外的库文件搜索路径 |
CMAKE_INSTALL_PREFIX |
安装路径前缀 |
CMAKE_RUNTIME_OUTPUT_DIRECTORY |
可执行目标二进制的输出目录 |
CMAKE_LIBRARY_OUTPUT_DIRECTORY |
非DLL库二进制的输出目录 |
CMAKE_ARCHIVE_OUTPUT_DIRECTORY |
静态库(archive)二进制的输出目录 |
CMAKE_DEBUG_POSTFIX |
附加在Debug库后缀的字符串 |
CMAKE_FIND_ROOT_PATH |
用于在交叉编译中指定搜索根路径 |
下面演示几个变量的区别,他们是由cmake维护的
Top level CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.0) |
sub_dir/CMakeLists.txt
1 | message("sub_dir: CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}") |
打印如下:
cmake include
1 | include("cmake/test_cmake.cmake") |
include导入的文件本质也就是文本替换
命令构建指定项目和清理
预处理 -> 编译 -> 汇编 -> 链接 -> 运行
cmake --build build --target help 可以查看所有目标,如下:
1 | The following are some of the valid targets for this Makefile: |
预处理:cmake --build build --target first_cmake.i编译:cmake --build build --target first_cmake.s汇编:cmake --build build --target first_cmake.o
清理: cmake --build build --target clean 通用各种编译工具的清理目标文件的清理
调试打印生成的具体指令
1 | #打印详细的生成指令的开关变量,默认是OFF |
cmake --build . -v 可以直接打印详细的生成指令,相当于临时在CMakeLists.txt中添加上面的语句
设置输出路径
| 输出路径 | 控制变量 | 控制输出什么 |
|---|---|---|
| 库输出路径 | CMAKE_LIBRAR_OUTPUT_DIRECTORY | linux动态库.so (该变量在windows中是设置了也无效的) |
| 归档输出路径 | CMAKE_ARCHIVE_OUTPUT_DIRECTORY | windos静态库.lib和windows动态库地址.lib和linux静态库(.a)和静态库的PDB调试文件 |
| 执行程序输出路径 | CMAKE_RUNTIME_OUTPUT_DIRECTORY | 执行程序和dll动态库和可执行程序以动态库的PDB调试文件 |
库输出路径和归档输出路径一般设置为一个路径
1 | #库输出路径: |
待解决问题:
- 多个项目想要有不同的输出路径
- Debug和Release不同输出
- 一个项目同时要生成静态库和动态库,静态库需要传递宏
-Dxlog_STATIC,动态库却不需要
CMake主要语法
if控制流程
1 | if(<条件>) |
条件真假规则如下:
- 1,ON,YES,TRUE,Y,或非零数(包括浮点数),则为真
- 0,OFF,NO,FLASE,N,IGNORE,NOTFOUND,空字符串,空,或以后缀结尾-NOTFOUND则为假
- 非假值常量为真,未定义和其他变量为假,环境变量总为假
- 字符串的值是常量真则为真,其他带引号的字符串始终计算为假
if判断语句
一元判断
EXISTS COMMAND DEFINED
1
if(DEFINED VAR_DEF)#判断VAR_DEF变量是否被定义
二元判断
只能判断数字或数字字符串:EQUAL LESS LESS_EQUAL GREATER GREATER_EQUAL
判断字符串(也能判断数字 ):STREQUAL STRLESS STRLESS_EQUAL STRGREATER STRGREATER_EQUAL
版本比较:VERSION_EQUAL VERSION_LESS VERSION_LESS_EQUAL VERSION_CREATER VERSION_CREATER_EQUAL
正则表达式匹配:MATCHES
1
if("abcd" MATCHES "^[a-z]+$")#判断abcd字符串是否全是小写字母
存在性检查
支持使用逻辑运算符:与或非,AND OR NOT
if语句的问题:
- 判断语句过长
- 无法嵌入到其他功能函数中
变量和缓存
缓存变量可以持久的存在
- 设置过后哪怕是设置语句去除也可以正常使用该缓存变量
- 缓存变量第二次修改不生效
- 普通变量的作用域是自身和子目录,缓存变量的作用域是全局的
- 缓存变量最大的作用是让用户可以选择一些变量的设置
缓存变量的语法: set(<variable><value>...CACHE<type><docstring>[FORCE])
FORCE表示强制修改,同一个缓存变量多次设置无效,该关键词可以让他强制修改
docstring:说明,鼠标放在项上显示的内容
type有如下几种,主要用于使得图形化界面可以提供对应的输入方式
BOOLON/OFF选择框FILEPATH文件选择PATH目录选择STRING一行字符串INTERNAL一行字符串不会开放给用户来设置的内部变量,图形化界面无法看到该选项
1 | set(VAR1 "CACHE VAR1 VALUE" CACHE STRING "cache doc")#设置缓存变量,字符串类型,说明为:cache doc |
大部分场景使用的都是BOOL类型,cmake提供了更简单的方式: option(选项的键 “说明” ON/OFF) 如:option(OPT1 "opt1 doc" ON)
1 | set(VAR1 "CACHE VAR1 VALUE" CACHE STRING "cache doc")#设置缓存变量,字符串类型,说明为:cache doc |
在windows中使用cmake-gui,如果在linux控制台下使用ccmake
ccmake build
访问缓存变量的特殊方式: $CACHE{变量名} 用于普通变量和缓存变量同名的情况下指定访问缓存变量
缓存变量覆盖策略设置
VERSION 3.21版本开始的新功能
设置cache变量覆盖策略: cmake_policy(SET CMP0126 NEW/OLD)
- 当次政策设置为NEW时,set设置cache变量时不会从当前范围中删除任何同名的普通函数
- OLD策略,set设置cache变量时会从当前范围中删除同名普通函数
命令行传递缓存变量
关键词 -D key=value
如: cmake -S . -B build -D PARA1=para001
如果是用该命令对已存在的缓存变量进行修改,相当于加上了FORCE强制修改
缓存变量的缺陷:一经设置,不强制修改的情况下无法修改,因此很容易出错
cmake属性
属性是作用域为目标的变量
属性就相当于成员变量
全局属性就是一个没有缓存的全局变量
属性语法
set_property
设置属性
语法: 方括号[]: 表示可选参数 尖括号<>: 表示必填参数
1 | set_property(<GLOBAL | DIRECTORY [<dir>] | TARGET [<target1>...] | SOURCE [<src1>...][DIRECTORY <dirs>...] [TARGET_DIRECTORY <targets>...] | INSTALL[<file1>...] | TEST [<test1>...] | CACHE [<entry1>...] > |
get_property
获取属性
1 | get_property(<variable> <GLOBAL |DIRECTORY [<dir>] | TARGET <target> |SOURCE <source> [DIRECTORY <dir> |TARGET_DIRECTORY <target>] | INSTALL<file> |TEST <test> | CACHE <entry> |VARIABLE > |
define_property
定义属性,可以设置说明.(不仅可以直接设置不存在的属性,也可以设置已有的属性)
1 | define_property(<GLOBAL | DIRECTORY | TARGET | SOURCE |TEST | VARIABLE | CACHED_VARI BLE> |
如:
1 | define_property(GLOBAL PROPERTY TEST_DEF BRIEF_DOCS "brief docs") |
属性分类
全局属性
无缓存全局
1
2
3
4
5
6set_property(GLOBAL PROPERTY TEST_GLOBAL "test global 001")
set_property(GLOBAL APPEND PROPERTY TEST_GLOBAL "123" )
get_property(var GLOBAL PROPERTY TEST_GLOBAL)#TEST_GLOBAL属性的值取到var变量中
message("PROPERTY TEST_GLOBAL=${var}")
#输出如下
PROPERTY TEST_GLOBAL=test global;123目录属性
目录属性只在当前目录有效(上级目录与下级目录均无效)
1
2set_property(DIRECTORY . PROPERTY DIR_VAR1 "dir_var1 001")#.表示该CMakeLists.txt所处的当前目录 DIR_VAR1为属性名
get_property(var DIRECTORY . PROPERTY DIR_VAR1)文件属性
文件属性只在该文件存在才有效(可以访问子目录中的文件属性)
1
2set_property(DIRECTORY main.cpp PROPERTY S1 "s1 value")#main.cpp表示源文件名 S1为属性名
get_property(var DIRECTORY . PROPERTY S1)命令行传递参数给源文件
**
COMPILE_DEFINITIONS**是传递预处理变量的预置属性1
set_property(SOURCE main.cpp PROPERTY COMPILE_DEFINITIONS "PARA1=1234")#编译的时候传递 -DPARA1 1234
目标属性
大部分情况下,都是使用目标属性(可以访问子目录中的目标属性)
1
2
3add_executable(${PROJECT_NAME} main.cpp)
set_property(TARGET ${PROJECT_NAME} PROPERTY TVAR "tvar1")#目标名必须是已存在的
get_property(var TARGET ${PROJECT_NAME} PROPERTY TVAR)命令行传递参数给源文件
1
2
3set_property(TARGET ${PROJECT_NAME} PROPERTY COMPILE_DEFINITIONS "PARA1=\"test_para1\"")#\表示转义符
set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY COMPILE_DEFINITIONS "PARA2=\"test_para2\"")#追加设置
set_property(TARGET ${PROJECT_NAME} APPEND_STRING PROPERTY COMPILE_DEFINITIONS "PARA3=\"test_para3\"")#追加设置
打印属性
打印属性可以用一下命令:
1 | get_property(var GLOBAL PROPERTY TEST_GLOBAL) |
但也可以:**cmake_print_properties**
需要引入打印的模块: include(CMakePrintHelpers)
1 | cmake_print_properties([TARGETS target1.. targetN] |
使用案例:
1 | include(CMakePrintHelpers) |
math数学运算
1 | math(EXPR <variable> "<expression>" [OUTPUT_FORMAT <format>]) |
- 表达式支持
+ - * / % | & ^ ~ << >> - 结果必须是64位有符号整数
- 输出格式
- HEXADECIMAL 十六进制
- DECIMAL 十进制
string字符串处理
- 搜索和替换
- 操作
- 比较
- 哈希值
- 生成
- 与JSON交互
比较常用的有头尾去无用字符,取子串,大小写转换
案例
1 | #取出字符串" begin test cmake string end "中,begin和end中间的字符串,并去除两边空格转成大写打印 |
list基础语法
环境变量
环境变量设置语法: set(ENV{<variable>}{<value>}) 使用 $ENV{<variable>}
1 | set(ENV{MYENV} "test env value") |
环境变量特性:
- 只影响当前的CMake进程,不影响调用CMake的进程,也不影响整个系统环境,也不影响后续构建或测试进程的环境
- 作用域是全局的,基本类似全局属性,但全局属性可以给图形界面加说明
- 环境变量相比属性访问更简单
- 可以直接通过 **
$ENV{<variable>}**读取到系统的环境变量,如果对系统环境变量进行修改也只影响当前CMake进程,不会真正影响到系统环境变量 如:message("PATH=$ENV{PATH}") - 无缓存,和属性一样,不会和缓存变量一样可以持久存在
循环语句
foreach
while
CMake宏
CMake函数
测试相关
cmake中引入测试相关特殊语法主要包括
enable_testing:必须要有该语句,后续才能开启ctest功能,即add_test。add_test:添加测试用例,格式如下1
2
3add_test(NAME <name> [CONFIGURATIONS [Debug|Release|...]]
[WORKING_DIRECTORY dir]
COMMAND <command> [arg1 [arg2 ...]])
就可以进入到build文件夹直接运行ctest命令测试测试用例
cmake子目录
在任何多目录项目中,两个基本的CMake命令是add_subdirectory()和include()。这些命令将来自另一个文件或目录的内容引入到构建中,允许构建逻辑分布在目录层次结构中,而不是强制所有内容都在最顶层定义。这样做有很多好处:
- 构建逻辑是本地化的,这意味着构建的特征可以在它们最相关的目录中定义。
- 构建可以由子组件组成,子组件的定义独立于使用它们的顶级项目。这对于使用git子模块或嵌入第三方源代码树的项目来说尤为重要。
- 因为目录可以是自包含的,所以仅仅通过选择是否在该目录中添加就可以打开或关闭构建的部分。
add_subdirectory()和include()具有非常不同的特征,因此了解两者的优缺点是很重要的。
add_subdirectory
add_subdirectory()命令允许项目将另一个目录带入构建。该目录必须有自己的CMakeLists.txt文件,该文件将在add_subdirectory()被调用的地方进行处理,并在项目的构建树中为它创建一个相应的目录。
1 | add_subdirectory(sourceDir [ binaryDir ] [ EXCLUDE_FROM_ALL ]) |
sourceDir不一定是源树中的子目录,尽管它通常是。可以添加任何目录,sourceDir可以指定为绝对路径或相对路径,后者相对于当前源目录。绝对路径通常只在添加主源代码树之外的目录时才需要。
通常,binaryDir不需要指定。省略时,CMake会在构建树中创建一个与sourceDir同名的目录。如果sourceDir包含任何路径组件,它们将被镜像到CMake创建的binaryDir中。或者,binaryDir可以显式地指定为绝对路径或相对路径,后者相对于当前二进制目录(稍后将更详细地讨论)求值。如果sourceDir是源树之外的一个路径,CMake需要指定binaryDir,因为相应的相对路径不能再被自动构造。
子目录的范围
调用add_subdirectory()的效果之一是,CMake为处理该目录的CMakeLists.txt文件创建了一个新的作用域。这个新的作用域就像调用作用域的子作用域,有很多效果:
$$
调用作用域\ \ ==add_subdirectory()==>\ \ 子作用域
$$
- 调用作用域中定义的所有变量对子作用域都是可见的,子作用域可以像读取其他变量一样读取它们的值。
- 在子作用域中创建的任何新变量对调用作用域都不可见。
- 对子作用域中的变量的任何更改都是该子作用域中的局部变量。即使该变量存在于调用作用域中,调用作用域的变量也保持不变。在子作用域中修改的变量就像一个新变量,在处理离开子作用域中时丢弃该变量。
换句话说,在进入子作用域时,它会接收到那个时间点上在调用作用域中定义的所有变量的副本。对子变量的任何更改都将在子变量的副本上执行,而不会改变调用者的变量
$$
重要特性:允许添加的目录更改它想要的任何变量,而不影响调用作用域中的变量
$$
但如果希望子目录中对变量的更改对调用者可见,这就是set命令中PARENT_SCOPE关键字的作用.使用该关键字设置的是父作用域中的变量,而不是当前作用域
include
CMake提供的另一个从其他目录中获取内容的方法是include()命令,它有以下两种形式:
1 | include(fileName [OPTIONAL] [RESULT_VARIABLE myVar] [NO_POLICY_SCOPE])#用于加载文件 |
第一种形式有点类似于add_subdirectory(),但有一些重要的区别:
- include()需要读取文件的名称,而add_subdirectory()需要一个目录,并在该目录中查找CMakeLists.txt文件。传递给include()的文件名通常扩展名为.cmake,但可以是任何名称。
- include()没有引入新的变量范围,而add_subdirectory()引入了。
- 默认情况下,这两个命令都引入了一个新的策略范围,但是可以使用NO_POLICY_SCOPE选项告诉include()命令不要这样做(add_subdirectory()没有这样的选项)。
- CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR变量的值在处理由include()命名的文件时不会改变,然而它们在add_subdirectory()中会改变。
所以被include的文件中只能使用CMAKE_CURRENT_LIST_DIR/CMAKE_CURRENT_LIST_FILE获取文件位置,参考cmake内建变量
return
在某些情况下,项目可能希望停止处理当前文件的剩余部分,并将控制权返回给调用者。return()命令可以完全用于此目的,但请注意,它不能向调用者返回值。它的唯一作用是结束当前作用域的处理。如果不是在函数内部调用,return()将结束对当前文件的处理,无论它是通过include()还是add_subdirectory()引入的。
项目的不同部分可能包括来自多个位置的相同文件。有时,最好检查这个文件,只包含该文件一次,并尽早返回后续包含的内容,以防止多次重新处理该文件。这与C/ C++头文件的情况非常相似,通常会看到类似形式的include guard被使用:
1 | f(DEFINED cool_stuff_include_guard) |
cmake的3.10或更高版本中:
1 | include_guard() |
使用这个👆🏻更好,因为他在内部处理保护变量的名称
现代cmake的依赖传递性
从 modern cmake(>=3.0) 开始,使用的范式从 director-oriented 转换到了 target-oriented。
在cmake的早期版本中(2.xx,新版本叫modern cmake)是使用directory-oriented的方式来管理这些属性的传递性的。当你定义了一个属性,就意味着当前文件夹和子文件夹会使用这些属性。
旧版本的cmake在2015年左右经历过一次大更新,全面升级为modern cmake。所有旧的命令都变为了target-oriented。 以下是两者的对照表。
director-oriented最大的不足在于:必须按照实际目录的方式来管理cmake的依赖关系。举个例子:假如你有两个平行的目录之间互相依赖,这样就变得十分麻烦。
target-oriented 的方式是可以忽略实际的文件夹层次的。target可以随便放置在任何文件夹当中。只要你设计好target的依赖关系,所有的依赖关系都理清了
下面开始介绍现代cmake这其中最重要的有三个概念:
- 编译target
- 编译target相应的属性
- 可见性(传递性) PUBLIC/PRIVATE/INTERFACE
编译target
编译目标,总共就3种
- 静态库
add_library - 动态库
add_library指定SHARED关键字 - 可执行文件
add_executable
编译target相应的属性
- 编译标志:使用
target_complie_option - 预处理宏标志:使用
target_compile_definitions - 头文件目录:使用
target_include_directories - 链接库:使用
target_link_libraries - 链接标志:使用
target_link_options
可见性(传递性)
所谓可见性就是上述这些属性在不同target之间的传递性.有三种:
- PRIVATE 属性不会传递,只给自己使用
- PUBLIC 属性不仅自己使用,还传递给依赖它的目标
- INTERFACE 属性不会自己使用,只传递给目标
上面提到的依赖实际上就是指的cmake中的
target_link_libraries命令,通过这个命令使用了哪个库就是依赖了谁
只有INTERFACE比较特别,值得细说
INTERFACE
可以这么理解: INTERFACE就是纯粹的利他注意,我自己不用,但我甘于奉献,让别人用.iNTERFACE只做个纯粹的接口.这类似于电话接线员.接线员不能听到任何内容,他们只是把信息转发给别人
为什么需要INTERFACE可见性?
因为有些目标是没有实质性内容的,比如header-only的库,他们没办法编译成静态库.因为它们是没有源码的,只有头文件.一旦编译就会报错.他们唯一的作用就是被别人引用
举个例子:
下面的Eigen库是个header-only库的线性代数运算库
1 | #Eigen本身不会编译成任何东西 |
INTERFACE的实现机制
在cmake内部,有两个变量:INCLUDE_DIRECTORIES 和 INTERFACE_INCLUDE_DIRECTORIES
INCLUDE_DIRECTORIES是当前目标搜索的头文件目录INTERFACE_INCLUDE_DIRECTORIES是下一个目标要搜索的头文件目录
当目标B去搜索头文件的时候,就会在INCLUDE_DIRECTORIES 中搜索。这是简单清晰的
当A引用了B(或者说目标A依赖于目标B),那么目标B的INTERFACE_INCLUDE_DIRECTORIES 中的路径就会赋给目标A的INCLUDE_DIRECTORIES
使用PRIVATE,PUBLIC和INTERFACE就能控制是否将当前搜索路径传递给下一个目标
PRIVATE就是不把当前的INCLUDE_DIRECTORIES传递给INTERFACE_INCLUDE_DIRECTORIESPUBLIC就是把当前的INCLUDE_DIRECTORIES传递给INTERFACE_INCLUDE_DIRECTORIESINTERFACE就是自己不使用当前的INCLUDE_DIRECTORIES,但是把当前的INCLUDE_DIRECTORIES传递给INTERFACE_INCLUDE_DIRECTORIES
回过头来了看现代cmake的思想,实际上就是吸收的面向对象设计的访问权限控制
设置属性的时候,可以设定这个是属性的可见性(传递性)
- 对于private的property,不会传递,只会自己用。
- 对于public的property,会传递,也自己用。
- 对于interface的property,会传递,但不会自己用。
也需要设置target的可见性(传递性)
- 对于private的target,所有属性不会传递,只会自己用。
- 对于public的target,public和interface 属性会传递,所有属性也会自己用。
- 对于interface的target,所有interface 和 public属性会传递,但不会自己用。
在依赖的时候也可以可以设置可见性(传递性):和target的可见性两个之间取最小传递性(和面向对象设计的访问权限如出一辙)
配合包管理工具使用
cmake与vcpkg配合使用
[[C++基础#与CMAKE配合使用|CMake使用vcpkg安装的开发包]]
cmake与homebrew配合使用
[[C++基础#配合cmake使用开发包|CMake使用homebrew安装的开发包]]
pkg-config工具
pkg-config 是一个用于管理编译和链接软件包的命令行工具。
它主要用于在编译和链接阶段查找软件包的头文件路径和库文件路径。
对于 OpenCV 这样的库,pkg-config 可以帮助我们快速获取编译和链接所需的编译器和链接器标志。
pkg-config 会读取软件包提供的 .pc 文件,这些文件包含了软件包的元信息,如版本号、头文件路径、库文件路径等。
pkg-config --cflags --libs opencv4 命令的作用是:
--cflags: 返回编译 OpenCV 4 程序所需的编译器标志,包括头文件路径。在这个例子中返回的是-I/opt/homebrew/opt/opencv/include/opencv4,表示 OpenCV 4 的头文件路径。--libs: 返回链接 OpenCV 4 程序所需的链接器标志,包括库文件路径和库名称。在这个例子中返回的是一长串以-l开头的库名称,如-lopencv_gapi、-lopencv_stitching等。这些都是 OpenCV 4 提供的各种功能模块的库文件。还包括-L/opt/homebrew/opt/opencv/lib这样的库文件路径。
总的来说,这个命令可以帮助我们快速获取编译和链接 OpenCV 4 程序所需的所有编译器和链接器标志,避免手动指定这些信息。在 CMake 等构建系统中,通常会使用这个命令来自动配置 OpenCV 的依赖关系。
cmake小问题记录
LNK1181
cmake在Windows平台链接动态库 error LNK1181: 无法打开输入文件
这是因为被编译成的动态库的符号不会被可执行文件看到,需要手动开启符号被别人可见
1 | if(MSVC) |
如果预先编译了一个静态库,再编译动态库,这个问题不会出现.
如果只编译动态库,就需要在add_library之前加上述代码
cmake通用
1 | # CMake 最低版本要求 |
适用的目录结构:
1 | MyProject/ |
可以通过cmake --install build将可执行文件安装到系统目录(如 /usr/local/bin)
通过设置 CMAKE_TOOLCHAIN_FILE 和 vcpkg 提供的跨平台支持,能够轻松实现多平台动态库兼容性
对于单配置生成器(如 Makefile 和 Ninja),可以通过
cmake -DCMAKE_BUILD_TYPE=<type>来指定构建类型。对于多配置生成器(如 Visual Studio 或 Xcode),则使用
cmake --build <build-dir> --config <type>,而不是 CMAKE_BUILD_TYPE。
type设置为Debug或Release可以配置不同的编译选项
允许添加本地库,放在libs文件夹下,静态与动态库位置
如果需要配置平台相关动态库路径:(不确定是否可行)
1 | # 配置平台相关动态库路径 |

