cmake

CMake是用于构建,测试,和软件打包的开源跨平台工具

参阅书籍:<<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库名.alibxlog.a
  • 动态库为 lib库名.solibxlog.so 格式: libname.so.主版本号.次版本号.发行版本号

使用库的时候,对静态库和动态库其实是无感的,有静态库链接静态库,有动态库链接动态库。链接什么库,主要是跟库的路径相关

macOS

  • 静态库为 lib库名.alibxlog.a
  • 动态库为 lib库名.dyliblibxlog.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 makeapt install ninja-build
  • sudo apt install unzip
  • sudo apt install libssl-dev (openssl是个加解密工具,这里只安装他的库)

下载解压cmake源码并编译

  1. 下载wget https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1.tar.gz
  2. 解压tar -xvf cmake-3.2 1.tar.gz
  3. cd cmake-3.2 1
  4. 执行./configure生成makefile(几乎所有开源软件,如果不支持cmake都是使用这个)
  5. 编译源码: make -j32-j3232线程编译)

安装编译好的cmake

安装编译好的cmake sudo make install (默认安装路径在/usr/local/share/cmake-3.23

设置cmake的运行路径

  1. vi ~/.bash_profile
  2. 文件中添加 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如何执行编译功能

  1. 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查询

  2. cmake --build build --config Release [-j32] 通用的编译生成目标文件命令(替代统一make等的指令),build表示生成到build文件夹中,--config可以设置编译成Release版本或者Debug版本,-j32表示32线程编译

  3. cmake --install build 将已经构建好的程序、库或头文件等文件安装到指定的目录下。这个命令会自动根据 CMakeLists.txt 文件中的指令来安排需要安装哪些文件,以及将它们复制到哪个目录下。

windows中文件名大小写不敏感,而linux中文件名大小写敏感,但是cmake做了处理,统一大小写不敏感。
$$
标准的cmake文件名: CMakeLists.txt
$$

通用动态库头文件格式

由于windows中含有独有指令__declspec(dllexport)__declspec(dllimport),并且需要有__declspec(dllexport)标记的内容生成动态库时才会生成lib文件,而其他平台均不需要,因此头文件这么编写可以通用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//xlog.h
#pragma once
//__declspec(dllexport) 导出XLog类的函数到lib文件中,不加这个的话,生成动态库的时候不会附带生成lib文件(只在windows下有效)
//xlog库文件调用 dllexport
//test_xlog调用 应该是dllimport,不然也会生成test_xlog.lib
#ifndef _WIN32 //_WIN32实际上包含win32和win64
//不是win32为空
#define DECLSPEC_API
#else
#ifdef xlog_STATIC//xlog_STAITC需要由cmake手动传入
#define DECLSPEC_API
#else
#ifdef xlog_EXPORTS
#define DECLSPEC_API __declspec(dllexport)//库项目调用
#else
#define DECLSPEC_API __declspec(dllimport)//调用库项目调用
#endif
#endif
#endif
class DECLSPEC_API XLog
{
private:
/* data */
public:
XLog(/* args */);
};

CMake注释

  • 括号注释 cmake3.0开始引入的括号注释,格式:#[[ 注释内容 ]]
  • 行注释 #行注释,一直注释到行尾

CMake message

基础语法

格式:message([mode] arg1 arg2 arg3 ...) 空格分隔。输出会以拼接参数的形式进行输出,并在最后进行换行,例子如下:

如: message("参数1" "参数2" "参数3" "参数4") 输出为: 参数1参数2参数3参数4

日志级别

message用可省略的mode参数指定该内容显示的日志级别 如:

1
2
3
message(FATAL_ERROR "test fatal_error")  #指定该日志为FATAL_ERROR日志级别,该级别会停止cmake运行和生成
message("after FATAL_ERROR")
#只会打印test fatal_error,不会打印after FATAL_ERROR

调用cmake时可以添加日志打印参数 --log-level=<ERROR|WARNING|NOTICE|STATUS|VERBOSE|DEBUG|TRACE> ,不填写的话默认为STATUS级别

指定显示的日志级别为TRACE的例子:cmake -S . -B build --log-level=TRACE

日志级别由低到高的,指定高级别会同时打印比他低的级别

日志级别(由低到高)

  1. FATAL_ERROR 停止cmake运行和生成 打印到stderr
  2. SEND_ERROR cmake继续运行,生成跳过 打印到stderr
  3. WARNING 包含WARNING级别此处上面的级别会同时打印代码路径和行号 打印到stderr
  4. 不设置 或者 NOTICE 打印到stderr (此级别到TRACE级别不会同时打印代码路径和行号) 打印到stderr
  5. STATUS 项目用户可能感兴趣的信息 这个等级往下打印消息前会添加前缀-- 打印到stdout
  6. VERBOSE 针对项目用户的详细信息 从这里往下调用cmake不指定–log-level默认不显示消息 打印到stdout
  7. DEBUG 项目本身的开发人员使用的信息 打印到stdout
  8. TRACE 非常低级实现细节的细粒度信息 打印到stdout

如果执行cmake -S . -B build >log.txt是默认把标准输出stdout重定向到log.txt文件

  • 1 表示 stdout
  • 2 表示 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
2
3
4
5
6
7
8
9
10
11
message(CHECK_START "查找xcpp")
#设置message消息缩进
set(CMAKE_MESSAGE_INDENT "--")
#查找库的代码
#嵌套查找
message(CHECK_START "查找xlog")
#查找库的代码
message(CHECK_PASS "成功")
#取消message消息缩进
set(CMAKE_MESSAGE_INDENT "")
message(CHECK_FAIL "失败")

cmake变量

设置变量

set关键字

set(<variable> <value>) 将变量<variable>的值设置为<value>

如果没有指定 value,那么这个变量就会被撤销而不是被设置,也可以用 unset(<variable>)撤销变量

PARENT_SCOPE参数:用于在子目录中设置父目录中的变量: set(VAR1 "测试变量VAR1的值" PARENT_SCOPE)

注意这样设置不会影响本地作用域,本地作用域就相当于什么也没做,VAR1的值没动过

变量使用

使用方式 ${变量名}

  • 变量引用是值替换,如果未设置变量,返回空字符串
  • 变量引用可以嵌套并从内向外求值
  • 变量名大小写敏感
  • 普通变量的作用域是自身和子目录 p.s.子目录1中设置的变量,主目录和子目录2都无法访问
1
2
3
4
5
6
7
8
9
10
11
12
set(VAR1 "测试变量VAR1的值")
message("VAR1=" ${VAR1})
message("\${VAR1}=${VAR1}")
set(VAR2 "VAR1")
message("\${VAR2}=${${VAR2}}") #嵌套
unset(VAR1)
message("\${VAR1}=${VAR1}")
#输出结果如下
VAR1=测试变量VAR1的值
${VAR1}=测试变量VAR1的值
${VAR2}=测试变量VAR1的值
${VAR1}=

缓存变量跳转

通过变量让message输出不同的颜色

终端的颜色格式 : Esc的ASCII字符[显示方式;前景色;背景色m 内容 Esc的ASCII字符[m 中间的内容会被设置为对应颜色和显示方式

如:\033[1;31;40m 红色内容黑色背景 \033[m

显示方式值 对应的显示方式含义
0 终端默认设置
1 高亮显示
4 使用下划线
5 闪烁
7 反白显示
8 不可见
1
2
3
4
5
6
7
8
9
#红色:Esc[0;31m
string(ASCII 27 Esc)
set(R "${Esc}[0;31m")
#尾:Esc[m
set(E "${Esc}[m")
#高亮,蓝色字体,黑色背景
set(B "${Esc}[0;34;40m")
message("${R}红色内容${E}")
message("${B}蓝色内容${E}")

cmake内建变量

  • 提供信息的变量

    PROJECT_NAME project()设置的项目名称

  • 改变行为的变量

    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
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.0)
project(MyApp)
message("top: CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
message("top: CMAKE_BINARY_DIR = ${CMAKE_BINARY_DIR}")
message("top: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("top: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")
add_subdirectory(mysub)
message("top: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("top: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")

sub_dir/CMakeLists.txt

1
2
3
4
message("sub_dir: CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
message("sub_dir: CMAKE_BINARY_DIR = ${CMAKE_BINARY_DIR}")
message("sub_dir: CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")
message("sub_dir: CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_BINARY_DIR}")

打印如下:

image-20220112101913126

cmake include

1
2
3
include("cmake/test_cmake.cmake")
include("cmake/test_cmake.cmake" OPTIONAL)#OPTIONAL 可选,文件不存在不会报错
include("cmake/test_cmake.cmake" OPTIONAL RESULT_CARIABLE ret)#RESULT_CARIABLE 返回值会返回到ret变量中,成功会返回导入文件的绝对路径,失败会返回NOTFOUND

include导入的文件本质也就是文本替换

命令构建指定项目和清理

预处理 -> 编译 -> 汇编 -> 链接 -> 运行

cmake --build build --target help 可以查看所有目标,如下:

1
2
3
4
5
6
7
8
9
10
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... edit_cache
... rebuild_cache
... first_cmake
... first_cmake.o
... first_cmake.i
... first_cmake.s
  1. 预处理cmake --build build --target first_cmake.i
  2. 编译cmake --build build --target first_cmake.s
  3. 汇编: cmake --build build --target first_cmake.o

清理: cmake --build build --target clean 通用各种编译工具的清理目标文件的清理

调试打印生成的具体指令

1
2
#打印详细的生成指令的开关变量,默认是OFF
set(CMAKE_VERBOSE_MAKEFILE ON) #名字里虽然包含makefile,但是其实都可以

cmake --build . -v 可以直接打印详细的生成指令,相当于临时在CMakeLists.txt中添加上面的语句

设置输出路径

输出路径 控制变量 控制输出什么
库输出路径 CMAKE_LIBRAR_OUTPUT_DIRECTORY linux动态库.so (该变量在windows中是设置了也无效的)
归档输出路径 CMAKE_ARCHIVE_OUTPUT_DIRECTORY windos静态库.libwindows动态库地址.liblinux静态库(.a)静态库的PDB调试文件
执行程序输出路径 CMAKE_RUNTIME_OUTPUT_DIRECTORY 执行程序dll动态库可执行程序以动态库的PDB调试文件

库输出路径和归档输出路径一般设置为一个路径

1
2
3
4
5
#库输出路径:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/lib")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/lib")
#执行程序生成路径:
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/bin")

待解决问题:

  • 多个项目想要有不同的输出路径
  • Debug和Release不同输出
  • 一个项目同时要生成静态库和动态库,静态库需要传递宏-Dxlog_STATIC,动态库却不需要

CMake主要语法

if控制流程

1
2
3
4
5
6
7
8
if(<条件>)
<commands>
elseif(<条件>)#可选的块,可被重复
repeated
<commands>
else() #可选的块
<commands>
endif()

条件真假规则如下:

  • 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有如下几种,主要用于使得图形化界面可以提供对应的输入方式

  1. BOOL ON/OFF选择框
  2. FILEPATH 文件选择
  3. PATH 目录选择
  4. STRING 一行字符串
  5. INTERNAL 一行字符串不会开放给用户来设置的内部变量,图形化界面无法看到该选项
1
2
3
4
5
6
7
8
set(VAR1 "CACHE VAR1 VALUE" CACHE STRING "cache doc")#设置缓存变量,字符串类型,说明为:cache doc
set(VAR1 "CACHE VAR1 VALUE CHANGED" CACHE STRING "cache doc")#缓存变量第二次修改不生效
message("VAR1=${VAR1}" )
set(VAR1 "CACHE VAR1 VALUE CHANGED FORECE" CACHE STRING "cache doc" FORCE)#缓存变量强制修改
message("VAR1=${VAR1}" )
#输出如下:
VAR1=CACHE VAR1 VALUE
VAR1=CACHE VAR1 VALUE CHANGED FORECE

大部分场景使用的都是BOOL类型,cmake提供了更简单的方式: option(选项的键 “说明” ON/OFF) 如:option(OPT1 "opt1 doc" ON)

1
2
3
4
5
set(VAR1 "CACHE VAR1 VALUE" CACHE STRING "cache doc")#设置缓存变量,字符串类型,说明为:cache doc
set(VAR_BOOL "ON" CACHE BOOL "cache doc")
set(VAR_PATH "path" CACHE PATH "cache doc")
set(VAR_FILE "file path" CACHE FILEPATH "cache doc")
option(OPT1 "opt1 doc" ON)

在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属性也可以设置命令行传递参数给源文件

cmake属性

属性是作用域为目标的变量

属性就相当于成员变量

全局属性就是一个没有缓存的全局变量

属性语法

set_property

设置属性

语法: 方括号[]: 表示可选参数 尖括号<>: 表示必填参数

1
2
3
4
5
set_property(<GLOBAL | DIRECTORY [<dir>] | TARGET [<target1>...] | SOURCE [<src1>...][DIRECTORY <dirs>...]  [TARGET_DIRECTORY <targets>...] | INSTALL[<file1>...] | TEST [<test1>...] | CACHE [<entry1>...] > 
[APPEND]#以数组的方式追加,其实就是;分隔
[APPEND_STRING]#直接追加的文本,字符串拼接
PROPERTY <name> [<value1>...]
)

get_property

获取属性

1
2
3
get_property(<variable> <GLOBAL |DIRECTORY [<dir>] | TARGET <target> |SOURCE <source> [DIRECTORY <dir> |TARGET_DIRECTORY <target>] | INSTALL<file> |TEST <test> | CACHE <entry> |VARIABLE > 
PROPERTY <name>
[SET | DEFINED | BRIEF_DOCS | FULL_DOCS])

define_property

定义属性,可以设置说明.(不仅可以直接设置不存在的属性,也可以设置已有的属性)

1
2
3
4
5
define_property(<GLOBAL | DIRECTORY | TARGET | SOURCE |TEST | VARIABLE | CACHED_VARI BLE>
PROPERTY <name>[INHERITED]
[BRIEF_DOCS <brief-doc> [docs...]]
[FULL_DOCS <full-doc> [docs...]]
[INITIALIZE_FROM_VARIABLE <variable>])

如:

1
2
3
4
5
6
define_property(GLOBAL PROPERTY TEST_DEF BRIEF_DOCS "brief docs")
#获取属性概要说明
get_property(var GLOBAL PROPERTY TEST_DEF BRIEF_DOCS)
message("TEST_DEF=${var}")
#输出如下:
TEST_DEF=brief docs
属性分类
  • 全局属性

    无缓存全局

    1
    2
    3
    4
    5
    6
    set_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
    2
    set_property(DIRECTORY . PROPERTY DIR_VAR1 "dir_var1 001")#.表示该CMakeLists.txt所处的当前目录  DIR_VAR1为属性名
    get_property(var DIRECTORY . PROPERTY DIR_VAR1)
  • 文件属性

    文件属性只在该文件存在才有效(可以访问子目录中的文件属性)

    1
    2
    set_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
    3
    add_executable(${PROJECT_NAME} main.cpp)
    set_property(TARGET ${PROJECT_NAME} PROPERTY TVAR "tvar1")#目标名必须是已存在的
    get_property(var TARGET ${PROJECT_NAME} PROPERTY TVAR)

    命令行传递参数给源文件

    1
    2
    3
    set_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
2
get_property(var GLOBAL PROPERTY TEST_GLOBAL)
message("PROPERTY TEST_GLOBAL=${var}")

但也可以:**cmake_print_properties**

需要引入打印的模块: include(CMakePrintHelpers)

1
2
3
4
cmake_print_properties([TARGETS target1.. targetN]
[SOURCES source1.. sourceN] [DIRECTORIES dir1.. dirN] [TEST test1.. testN]
[CACHE_ENTRIES entry1.. entryN] PROPERTIES
prop1.. propN)

使用案例:

1
2
3
4
5
6
include(CMakePrintHelpers)
cmake_print_properties(TARGETS ${PROJECT_NAME} PROPERTIES COMPILE_DEFINITIONS TVAR)#打印COMPILE_DEFINITIONS和TVAR两个属性
#输出如下:
Properties for TARGET xlog:
xlog.COMPILE_DEFINITIONS = "PARA1="test_para1";PARA2="test_para2";PARA3="test_para3""
xlog.TVAR = <NOTFOUND>

math数学运算

1
2
3
4
5
6
7
math(EXPR <variable> "<expression>" [OUTPUT_FORMAT <format>])
#例子:
set(exp "5*(10+3)")
math(EXPR out ${exp} OUTPUT_FORMAT HEXADECIMAL)#结果存在out变量中
message("out=${out}")
#输出如下:
out=0x41
  • 表达式支持+ - * / % | & ^ ~ << >>
  • 结果必须是64位有符号整数
  • 输出格式
    • HEXADECIMAL 十六进制
    • DECIMAL 十进制

string字符串处理

  • 搜索和替换
  • 操作
  • 比较
  • 哈希值
  • 生成
  • 与JSON交互

比较常用的有头尾去无用字符,取子串,大小写转换

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#取出字符串"  begin test cmake string end   "中,begin和end中间的字符串,并去除两边空格转成大写打印
set(STR1 " begin test cmake string end ")
set(BSTR "begin")
string(FIND ${STR1} ${BSTR} bindex)#查找在字符串中begin的位置的下标存到bindex变量中
string(FIND ${STR1} "end" eindex)#查找在字符串中end的位置的下标存到eindex变量中
string(LENGTH ${BSTR} size)#获取BSTR变量中值的长度到size变量
math(EXPR bindex "${bindex}+${size}")#bindex=bindex+size
math(EXPR length "${eindex} - ${bindex}")#length = eindex-bindex
string(SUBSTRING ${STR1} ${bindex} ${length} substr)
string(STRIP ${substr} substr)#去掉头尾的空格,\t,\n,\r输出到substr变量
string(TOUPPER ${substr} substr)#转成大写存到substr中
message("substr = [${substr}]")
#输出如下:
substr = [TEST CMAKE STRING]

list基础语法

环境变量

环境变量设置语法: set(ENV{<variable>}{<value>}) 使用 $ENV{<variable>}

1
2
set(ENV{MYENV} "test env value")
message("MYENV=$ENV{MYENV}")

环境变量特性:

  • 只影响当前的CMake进程,不影响调用CMake的进程,也不影响整个系统环境,也不影响后续构建或测试进程的环境
  • 作用域是全局的,基本类似全局属性,但全局属性可以给图形界面加说明
  • 环境变量相比属性访问更简单
  • 可以直接通过 **$ENV{<variable>}**读取到系统的环境变量,如果对系统环境变量进行修改也只影响当前CMake进程,不会真正影响到系统环境变量 如:message("PATH=$ENV{PATH}")
  • 无缓存,和属性一样,不会和缓存变量一样可以持久存在

循环语句

foreach
while

CMake宏

CMake函数

测试相关

cmake中引入测试相关特殊语法主要包括

  • enable_testing:必须要有该语句,后续才能开启ctest功能,即 add_test

  • add_test:添加测试用例,格式如下

    1
    2
    3
    add_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
2
include(fileName [OPTIONAL] [RESULT_VARIABLE myVar] [NO_POLICY_SCOPE])#用于加载文件
include(module [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
2
3
4
5
f(DEFINED cool_stuff_include_guard)
return()
endif()
set(cool_stuff_include_guard 1)
# ...

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。 以下是两者的对照表。

img

director-oriented最大的不足在于:必须按照实际目录的方式来管理cmake的依赖关系。举个例子:假如你有两个平行的目录之间互相依赖,这样就变得十分麻烦。

target-oriented 的方式是可以忽略实际的文件夹层次的。target可以随便放置在任何文件夹当中。只要你设计好target的依赖关系,所有的依赖关系都理清了

下面开始介绍现代cmake这其中最重要的有三个概念:

  1. 编译target
  2. 编译target相应的属性
  3. 可见性(传递性) 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
2
3
4
5
6
7
8
9
10
11
12
#Eigen本身不会编译成任何东西
add_library(Eigen INTERFACE)

target_sources(Eigen INTERFACE
FILE_SET HEADERS
BASE_DIRS src
FILES src/eigen.h src/vector.h src/matrix.h
)

add_executable(exe1 exe1.cpp)
#实际上就是把上面指定的src/eigen.h src/vector.h src/matrix.h头文件传递给了exe1
target_link_libraries(exe1 Eigen)

INTERFACE的实现机制

在cmake内部,有两个变量:INCLUDE_DIRECTORIESINTERFACE_INCLUDE_DIRECTORIES

  • INCLUDE_DIRECTORIES当前目标搜索的头文件目录
  • INTERFACE_INCLUDE_DIRECTORIES下一个目标要搜索的头文件目录

当目标B去搜索头文件的时候,就会在INCLUDE_DIRECTORIES 中搜索。这是简单清晰的

当A引用了B(或者说目标A依赖于目标B),那么目标B的INTERFACE_INCLUDE_DIRECTORIES 中的路径就会赋给目标A的INCLUDE_DIRECTORIES

使用PRIVATE,PUBLICINTERFACE就能控制是否将当前搜索路径传递给下一个目标

  • PRIVATE就是不把当前的INCLUDE_DIRECTORIES 传递给INTERFACE_INCLUDE_DIRECTORIES
  • PUBLIC就是把当前的INCLUDE_DIRECTORIES 传递给INTERFACE_INCLUDE_DIRECTORIES
  • INTERFACE就是自己不使用当前的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 命令的作用是:

  1. --cflags: 返回编译 OpenCV 4 程序所需的编译器标志,包括头文件路径。在这个例子中返回的是 -I/opt/homebrew/opt/opencv/include/opencv4,表示 OpenCV 4 的头文件路径。
  2. --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
2
3
4
if(MSVC)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)#重点是这句
set(BUILD_SHARED_LIBS TRUE)
endif()

如果预先编译了一个静态库,再编译动态库,这个问题不会出现.

如果只编译动态库,就需要在add_library之前加上述代码

cmake通用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# CMake 最低版本要求
cmake_minimum_required(VERSION 3.16) # 设置 CMake 的最低版本要求为 3.16

# 项目名称和版本
project(MyModernCMakeProject VERSION 1.0 LANGUAGES CXX) # 定义项目名称为 MyModernCMakeProject,版本为 1.0,使用 C++ 语言

# 指定 C++ 标准
set(CMAKE_CXX_STANDARD 17) # 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求必须使用 C++17 标准
set(CMAKE_CXX_EXTENSIONS OFF) # 禁用编译器特定的 C++ 扩展

# 设置输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # 设置可执行文件的输出目录为 ${CMAKE_BINARY_DIR}/bin
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 设置库文件的输出目录为 ${CMAKE_BINARY_DIR}/lib
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 设置静态库文件的输出目录为 ${CMAKE_BINARY_DIR}/lib

# 包含头文件目录
include_directories(${PROJECT_SOURCE_DIR}/inc) # 包含项目源代码目录下的 inc 文件夹作为头文件目录

# 搜索 src 文件夹中的所有源文件
file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/src/*.cpp) # 递归搜索项目源代码目录下的 src 文件夹中的所有 .cpp 文件,并将结果存储在 SRC_FILES 变量中

# 添加可执行目标
add_executable(${PROJECT_NAME} ${SRC_FILES}) # 添加一个可执行目标,目标名称为项目名称,源文件为 SRC_FILES 中的文件

# 使用现代 CMake 推荐的 target_include_directories
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/inc) # 为目标 ${PROJECT_NAME} 添加头文件目录,作用域为 PRIVATE,目录为 ${PROJECT_SOURCE_DIR}/inc

# 为不同构建类型添加编译选项
if(CMAKE_BUILD_TYPE STREQUAL "Debug") # 如果构建类型为 Debug
target_compile_definitions(${PROJECT_NAME} PRIVATE DEBUG) # 为目标 ${PROJECT_NAME} 添加编译定义 DEBUG
target_compile_options(${PROJECT_NAME} PRIVATE -g) # 为目标 ${PROJECT_NAME} 添加编译选项 -g(生成调试信息)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release") # 如果构建类型为 Release
target_compile_definitions(${PROJECT_NAME} PRIVATE NDEBUG) # 为目标 ${PROJECT_NAME} 添加编译定义 NDEBUG
target_compile_options(${PROJECT_NAME} PRIVATE -O3) # 为目标 ${PROJECT_NAME} 添加编译选项 -O3(优化级别为 3)
endif()

# 打印调试信息
message(STATUS "项目名: ${PROJECT_NAME}") # 打印项目名称
message(STATUS "源文件列表: ${SRC_FILES}") # 打印源文件列表
message(STATUS "二进制文件输出路径: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") # 打印可执行文件的输出路径

# 静态库和动态库扫描
set(LIB_SEARCH_DIR ${PROJECT_SOURCE_DIR}/libs) # 指定库文件根目录

# 扫描静态库(*.a, *.lib)
file(GLOB STATIC_LIBS "${LIB_SEARCH_DIR}/static/*${CMAKE_STATIC_LIBRARY_SUFFIX}")

# 扫描动态库(*.so, *.dll, *.dylib)
file(GLOB DYNAMIC_LIBS "${LIB_SEARCH_DIR}/dynamic/*${CMAKE_SHARED_LIBRARY_SUFFIX}")

# 链接所有静态库
foreach(LIB_FILE ${STATIC_LIBS})
message(STATUS "Found static library: ${LIB_FILE}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${LIB_FILE})
endforeach()

# 链接所有动态库
foreach(LIB_FILE ${DYNAMIC_LIBS})
message(STATUS "Found dynamic library: ${LIB_FILE}")
target_link_libraries(${PROJECT_NAME} PRIVATE ${LIB_FILE})
endforeach()

# 启用 vcpkg 支持
if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) # 如果未定义 CMAKE_TOOLCHAIN_FILE
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" # 设置 CMAKE_TOOLCHAIN_FILE 为 vcpkg 的工具链文件路径
CACHE STRING "Vcpkg toolchain file") # 将该变量缓存为字符串类型
endif()

# 查找并链接第三方动态库(以 OpenSSL 和 zlib 为例)
find_package(OpenSSL REQUIRED) # 查找 OpenSSL 库,必须找到
find_package(ZLIB REQUIRED) # 查找 Zlib 库,必须找到

# 链接动态库(以下面库为案例)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB) # 为目标 ${PROJECT_NAME} 链接 OpenSSL 和 Zlib 库,作用域为 PRIVATE

# 安装目标(可选,将可执行文件安装到系统的 bin 目录)
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) # 安装目标 ${PROJECT_NAME} 的可执行文件到系统的 bin 目录

适用的目录结构:

1
2
3
4
5
6
7
8
MyProject/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
├── inc/
├── libs/
│ ├── static/ # 静态库目录,包含 .a 或 .lib
│ ├── dynamic/ # 动态库目录,包含 .so, .dylib, 或 .dll

可以通过cmake --install build将可执行文件安装到系统目录(如 /usr/local/bin)

通过设置 CMAKE_TOOLCHAIN_FILEvcpkg 提供的跨平台支持,能够轻松实现多平台动态库兼容性

  • 对于单配置生成器(如 Makefile 和 Ninja),可以通过 cmake -DCMAKE_BUILD_TYPE=<type> 来指定构建类型。

  • 对于多配置生成器(如 Visual Studio 或 Xcode),则使用 cmake --build <build-dir> --config <type>,而不是 CMAKE_BUILD_TYPE。

type设置为Debug或Release可以配置不同的编译选项

允许添加本地库,放在libs文件夹下,静态与动态库位置

如果需要配置平台相关动态库路径:(不确定是否可行)

1
2
3
4
5
6
7
8
# 配置平台相关动态库路径
if(WIN32)
set(DYNAMIC_LIB_PATH ${PROJECT_SOURCE_DIR}/libs/windows)
elseif(UNIX AND NOT APPLE)
set(DYNAMIC_LIB_PATH ${PROJECT_SOURCE_DIR}/libs/linux)
elseif(APPLE)
set(DYNAMIC_LIB_PATH ${PROJECT_SOURCE_DIR}/libs/macos)
endif()