Cpp构建和编译笔记——5.CMake与CMakeLists
关于 CMake 的内容可能比较多,计划是分成以下几部分:
- Modern CMake 的基本使用
- CMake 基本语法与变量
- CMake 语法结构(条件,循环,函数,模块等)
- CMake 依赖管理(作为库的使用者)
- CMake 库的开发(作为库的开发者)
CMake 命令速查
完全CMake风格的四步命令:构建+编译+测试+安装 1
2
3
4cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/path/to/install/
cmake --build build
cmake --build build --target test
cmake --build build --target install
上述命令完全不依赖具体平台。
经典Linux风格的四步命令:构建+编译+测试+安装 1
2
3
4
5
6mkdir build
cd build
cmake ..
make -j8
ctest
make install
这里需要依赖make
命令,主要命令都在build/
中进行。
Windows平台使用MinGW风格的工具链,对应的四步命令:构建+编译+测试+安装
1
2
3
4cmake -S . -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/path/to/install/
cmake --build build -j8
cmake --build build --target test
cmake --build build --target install
Windows平台使用默认的VS2019的工具链,对应的四步命令:构建+编译+测试+安装
1
2
3
4cmake -S . -B build -G "Visual Studio 16 2019" -DCMAKE_INSTALL_PREFIX=/path/to/install/
cmake --build build --config Release
cmake --build build --target test
cmake --build build --target install
注意对于MSVC,在构建时指定模式是无效的,需要在编译时指定模式--config Release
。
CMake 介绍
CMake 是一个跨平台的构建工具,是 make 和 Makefile 的上层工具,它的目的是跨平台,可以根据同一个 CMakeLists,自动产生对应平台上的 Makefile 或其它的等价产物,并简化手写 Makefile 时的巨大工作量。
make 的执行依赖于对 Makefile 的解析,同样的,CMake 的执行也依赖于对 CMakeLists 的解析,我们的工作从手写 Makefile 变成了手写 CMakeLists。 Makefile 的语法是非常接近编译命令本身的,而经过 CMake 的封装,在 CMakeLists 中使用的语法已经相当贴近自然语言了。
严格来说,CMake 的语法其实更复杂,早期的语法(2.x)非常混乱,使用也不规范。 但是好消息是,CMake 的现代语法已经越来越合理了,完全基于面向对象的思想: 3.x 被称为 Modern CMake,3.12+被称为 More Modern CMake。 我们需要使用足够高的 CMake 版本来支持现代的语法,本文将使用的 CMake 版本为 3.23.0,并且要求 CMake 版本至少为 3.15。
极简的 CMakeLists 示例
这里给出一个单文件的项目,只有一个源文件 hello.cpp
1 |
|
项目结构为
1 | |-build |
最基本的 CMakeLists 如下
1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) |
逐行解释它们的含义
cmake_minimum_required
写在 CMakeLists 的第一行,表示这个 CMakeLists 需要的最低版本的 CMake,FATAL_ERROR
表示,如果达不到最低版本要求就报致命错误,停止执行(CMake 的语法变化非常大,因此有必要声明一下最低的版本要求)project(Demo VERSION 0.1)
Demo 是项目的名称,0.1 是项目的版本号,LANGUAGES CXX
表明项目使用的语言为 C++。对于 C/C++这个语句可以省略,因为默认语言就是 C 和 C++,注意如果只写 CXX 是不支持 C 文件的,需要写明LANGUAGES C CXX
,对于 Fortran 等其它语言不可省略set(CMAKE_CXX_STANDARD 17)
设置使用的 C++标准为 C++17(一个整数,例如 11,17)set(CMAKE_CXX_STANDARD_REQUIRED ON)
强制要求必须达到相应的 C++标准set(CMAKE_CXX_EXTENSIONS OFF)
设置不接受编译器提供的 C++扩展(便于跨编译器使用)add_executable(test)
添加一个可执行文件的 target,名称为 testtarget_sources(test PRIVATE hello.cpp)
给名称为 test 的 target 私有地添加源文件 hello.cpp,其中 PRIVATE 的作用见后文
当然这几行并不都是必须的,最简单的形式只需要三行
1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) |
注意:CMake 从前到后执行
CMakeLists,部分设置对出现的顺序是非常敏感的,例如对编译器的设定必须出现在
project 语句之前,否则无效。建议将下面三个语句放在 CMakeLists.txt
的开头部分,最好在project()
之前,CMake
会对编译器进行版本检测
1 | set(CMAKE_CXX_STANDARD 17) |
CMake 使用
CLI 操作
CMake 在命令行中的使用主要分成以下几步:
- 建立构建目录,例如项目根目录下的 build 子目录
- 生成构建系统(比如 make 工具对应的
Makefile),在这一步可以附加命令行参数
-D <var>=<value>
传递一些变量的定义,空格可省略 - 执行构建(比如 make),编译生成目标文件
- 执行测试、安装或打包等后续任务
CMake 会产生很多对使用者没有意义的杂项文件(包括一些缓存和中间文件),这可能会污染整个项目,因此建议使用单独的构建目录(通常命名为 build),和项目中的源文件隔离开,比如在项目根目录下新建 build 子目录,或者在 XX 项目的同级目录下新建 XX-build 目录,用于存放 CMake 产生的各种文件。
CMake 99%的错误都是缓存文件错误,可以通过直接删除 build 文件夹,重新生成编译解决。
第一种用法是传统方式
- 建立构建目录(build 子目录):
mkdir build
,进入 build 子目录:cd build
- 生成构建系统(Makefile):
cmake ..
,注意 cmake 需要指定的目录是项目的 CMakeLists 所在的位置,由于我们处于 build 子目录中,因此使用..
也就是上一级目录。 - 执行构建(使用 make 编译):
make
,注意此时我们仍然处在 build 子目录中
1 | mkdir build |
第二种用法是 modern cmake 推荐的,完全使用 CMake 自己的命令
- 生成构建系统:
cmake -S . -B build
-S
后接的位置是查找 CMakeLists 的位置,-S
选项可以整体缺省,默认会使用当前目录-B
后接的位置是生成的构建系统存放的位置,也就是 build 子目录(如果不存在,则会自动创建)
- 执行构建:
cmake --build build
,在 build 子目录下执行构建,或者可以进入 build 目录后,使用cmake --build .
,效果一样
1 | cmake -B build |
注:我们可以在 CMakeLists 中使用下列选项要求在 build 文件夹内仍然保持相应的文件夹结构
1 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) |
CMake 输出信息
单独使用cmake
不加任何路径或参数的话,会给出如下的提示信息
1 | Usage |
CMake 在生成构建系统的阶段可能输出如下形式的信息
1 | $ cmake -B build |
CMake 在执行构建系统的阶段可能输出如下形式的信息,包括百分比的编译进度提示
1 | $ cmake --build build |
指定构建系统
CMake 支持生成的构建系统并非局限于
Makefile,可以使用cmake --help
查看当前状态支持的构建系统(工具链),例如在
Windows 下,可能有下列构建系统
1 | Visual Studio 17 2022 = Generates Visual Studio 2022 project files. |
其中的*
代表默认选择的构建系统,我们可以在生成构建系统的时候,使用-G <generator-name>
指定一个构建系统,例如对于
Windows,可以指定使用 Mingw 而非默认的 VS,对于 Linux 默认的选项就是
Unix Makefiles。
1 | cmake .. -G "MinGW Makefiles" # 传统的方式 |
在生成构建系统时,CMake 可能会试图检查编译环境和编译器是否满足要求,对于版本较新的编译器,则会直接跳过检查。
设置变量
在生成构建系统时可以同时给 CMake 传递一些变量,这些变量的值可能触发
CMakeLists 内部不同的执行方式,例如 1
2
3cmake -S . -B build -DTYPE=1
# or
cmake -S . -B build -D TYPE=1
这里-D
后面接不接空格均可,如果手动定义的变量在 CMake
内部没有用到,则会警告一下,提示可能输错了参数。
CMake项目中最常见的变量包括:
CMAKE_BUILD_TYPE
:设置编译类型 Debug/ReleaseCMAKE_INSTALL_PREFIX
:设置安装目录前缀BUILD_TESTING
:是否开启测试(默认开启)
例如
1 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/path/to/install/ -DBUILD_TESTING=OFF |
这些变量在命令行中输入非常麻烦,但其实只需要设置一次,CMake
会把它们缓存下来(即 CMake
的缓存变量),下一次生成构建系统时无需重复输入。(也支持再次在命令行中使用-D
来修改)
指定编译器
除了构建系统,我们还可以指定编译使用的编译器,在Linux系统中很可能同时存在多个不同版本的gcc/g++,clang/clang++,我们可以明确告诉CMake使用哪些编译器:在构建时定义CMAKE_C_COMPILER
和CMAKE_CXX_COMPILER
变量,例如
1
/usr/bin/cmake -DCMAKE_C_COMPILER=/usr/bin/gcc -DCMAKE_CXX_COMPILER=/usr/bin/g++ -S . -B build -G Ninja
对于Linux系统,还可以直接设置CC
和CXX
环境变量,CMake也会识别并使用对应的编译器,优先级比CMAKE_C_COMPILER
和CMAKE_CXX_COMPILER
低,例如指定默认的gcc和g++
1
2export CC=/usr/bin/gcc
export CXX=/usr/bin/g++
指定使用Intel的新版编译器icx和icpx 1
2export CC=/path/to/icx
export CXX=/path/to/icpx
多核编译加速
在编译时,我们可以添加选项让它进行多核编译加速,例如-j8
选项(或者等价的--parallel 8
),注意是在编译时传递选项,并且不同的构建系统对此的支持不一样:似乎VS
的 MSBuild 不支持多核,Makefile 支持,但是效果上没有 Ninja 好。
1 | # (1) |
显示执行细节
我们可能希望显示 CMake 在编译时内部执行的具体指令,有以下几种方法可以实现(虽然变量名含有 MAKEFILE,但是同样支持 MSVC)
- 在生成构建系统时定义
CMAKE_VERBOSE_MAKEFILE
变量为真(直接在 CMakeLists 中设置也一样) - 在编译时添加参数
-v
或--verbose
,即
1 | cmake -Bbuild -DCMAKE_VERBOSE_MAKEFILE=ON |
如果在生成构建系统时使用--trace
命令,则会显示生成构建系统时的
CMake 调用命令的细节,比如 CMake 执行到了 CMakeLists
的哪一行,输出内容非常繁琐,建议重定向到文件中,用于调试 CMake 自身
1 | cmake -Bbuild -GNinja --trace-redirect=tmp.txt |
支持代码提示
对于 clangd 等静态检查或代码提示工具,需要 CMake 提供 compile_commands.json,这通常放置在 build/内,内容大致为如下形式
1 | [ |
CMake 很可能不会自动生成它,这会导致代码提示工具找不到头文件,可以使用下列选项开启(VScode 的 CMakeTools 插件会帮助开启这个选项)
1 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) |
注意:根据CMake官方文档,这个选项只对 Makefile Generators 和 Ninja Generators 有效,其他情况例如MSVC会忽略这个选项。
GUI 操作
在 Windows 下载 CMake 的同时会附带 cmake-gui.exe,支持直接在图形界面进行简单操作:
- 第一步,添加源代码目录(即包含项目 CMakeLists.txt
的目录);添加构建目录,通常选择源代码目录下的
build
子目录,如果目录不存在会提示创建。 - 第二步,点击
Configure
,此时可以选择 CMake 使用的工具链,包括生成器,编译器,构建类型等;此时在 GUI 界面会用红色显示新添加的、被更新的缓存变量(可以直接通过 GUI 修改),再次点击Configure
则红色消失。(再次确认是 GUI 特有的过程,命令行操作不包括这一步) - 第三步,点击
Generate
,生成构建系统(在 build 子目录中产生大量文件)。 - 第四步,对于使用 VS
构建的项目,可以点击
Open Project
直接打开 VS 项目(.sln
项目文件);其它情形下,需要手动进入项目,然后进行编译。
在 Linux 上除了 cmake 外也有 ccmake,它支持在命令行界面的简单可视化操作。
CMakeLists 基本语法
准备语法
- 支持
#
开头的单行注释,支持#[[
开头,]]
结尾的多行注释 - CMakeLists 的内置命令对大小写不敏感(建议全小写),但是其它部分例如变量名、文件名和编译选项等,显然都是大小写敏感的!
- 跨平台是 CMake 的卖点,但仍然避免不了字符编码的麻烦,建议尽量使用 ASCII 码,也就是纯英文脚本,换行符可以兼容 LF 和 CRLF
- 在 CMake 中的路径分隔符总是应当使用
/
,因为 CMake 会对字符串中的\
转义,CMake 在对接 VS 时会自动处理路径分隔符的替换问题
在较复杂的项目中,我们可以在不同的子目录下使用多个
CMakeLists.txt,在根目录下的 CMakeLists.txt
是最顶级的,例如可以使用add_subdirectory(source)
命令,进入
source 文件夹,然后自动执行 source 目录下的
CMakeLists.txt,执行完毕后返回上一级,还可以继续前往其它子目录执行相应的
CMakeLists.txt。
编译模式
CMake 支持四种编译模式,可以通过修改 CMAKE_BUILD_TYPE 来指定编译模式(默认的编译模式值为空,似乎相当于 Debug 模式)
- Release 模式:
-O3 -DNDEBUG
,发布模式,较高的优化,没有调试信息 - Debug 模式:
-g
,调试模式,附带调试信息 - MinSizeRel 模式:
-Os -DNDEBUG
,(较少见)多用于嵌入式,侧重于优化文件的体积 (Release 侧重于优化运行速度) - RelWithDebInfo 模式:
-O2 -g -DNDEBUG
,(较少见)在 Release 模式的基础上,加入一些调试信息
在使用 cmake 时往往需要指定编译模式,在讨论之前我们需要对构建系统进行分类:
- single configuration generator: Unix-Makefiles, MinGW Makefiles, Ninja ...
- multi-configuration generator: Visual Studio, Ninja Multi-Config ...
对于 single configuration generator,我们需要使用 CMAKE_BUILD_TYPE 进行设置,可以在 CMakeLists.txt 中设置,也可以在命令行参数中设置(命令行参数的优先级通常更高,除非在 CMakeLists.txt 修改缓存变量时使用 FORCE),总之是在生成构建系统时指定模式
1 | cmake -B build -DCMAKE_BUILD_TYPE=Release |
对于 multi-configuration generator,CMAKE_BUILD_TYPE 似乎是无效的(不会报错),我们需要在编译时指定模式
1 | cmake -B build |
对于 Visual Studio,如果编译时不使用--config
指定,则默认
Debug 模式。
通常在 CMakeLists.txt 的开头部分会添加下面的语句,确保默认情况下使用 Release 模式
1 | if(NOT CMAKE_BUILD_TYPE) |
注意这里设置的是同名的普通变量而非缓存变量(当然也可以使用缓存变量实现),此时 CMakeCache.txt 记录的 CMAKE_BUILD_TYPE 仍然是空。
输出目录
默认情况下,CMake 会把得到的可执行文件和库文件等仍然存放在 build 目录中,但是我们通常希望把得到的可执行文件和库文件放在 bin 和 lib 目录下,可以使用如下的 set 命令,指定不同编译模式下不同产物的输出目录
1 | # 设置不同模式下,编译后的输出目录 |
其中的路径变量PROJECT_SOURCE_DIR
是 CMake
的内置变量,代表项目的根目录,也就是最近一次使用project(...)
命令的
CMakeLists.txt
所在的目录。(对于含有子项目的情形,可能有多个project(...)
出现)
这一组命令的效果与编译环境有关:
- 对于 Linux,非常简单直观:可执行文件被放在 bin;静态库(.a)和动态库(.so)都被放在 lib
- 对于 Windows(MSVC),非常繁琐:可执行文件(.exe)和动态库的主要部分(.dll)被放在 bin;静态库(.lib)和动态库的辅助部分(.lib)被放在 lib
- 对于 Windows(Mingw),效果也类似:可执行文件(.exe)和动态库的主要部分(.dll)被放在 bin;静态库(.a)和动态库的辅助部分(.dll.a)被放在 lib
这是由于 Windows 和 Linux 在对待动态库时有着不同的处理,Windows 的动态库主要部分需要被放置在可执行文件同一目录。(辅助部分只是编译时需要,运行时不需要)
同时还要注意的是,关于动态库:
- 对于 Linux,CMake
可能在构建编译(不包括安装时)时自动在可执行文件中写入
rpath
参数,确保程序可以找到动态库 - 对于 Windows,不支持
rpath
类似的参数,需要修改 PATH 环境变量或者将动态库放在指定位置,否则可执行文件只能找到同一目录下的动态库(不是当前目录)
总的来说 Windows 平台对动态库的做法有很多麻烦:既有导出符号的问题,还有查找路径的问题。
还需要补充一下关于 MSVC 的特殊处理,由于我们调整了输出文件的位置,需要进行以下设置来确保 VS 的调试器正常工作,即对每个 target 修改相关属性
1 | if(MSVC) |
这里设置的就是 VS 使用调试器调试时的工作目录,尤其在我们修改了输出目录时,如果可执行文件需要进行文件读写,就需要关注启动时的工作目录。 在 3.27 的版本可以直接使用 CMAKE_VS_DEBUGGER_WORKING_DIRECTORY 进行全局设置。
注: 我们可以使用下列选项要求在 build 文件夹内仍然保持相应的文件夹结构
1 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) |
构建目标
在 modern cmake 的语法中,总是以面向对象的思路来编写 CMakeLists:
把生成的可执行文件,生成的静态动态库都统一成构建目标(target),围绕着
target
使用形如target_xxx
的命令添加源文件,添加依赖关系,添加属性等等。
target 大致有以下的类型:
- 可执行文件 target:使用源文件 test.cpp 编译得到可执行文件 test
1 | add_executable(test) |
- 静态库 target:使用源文件 static_fun.cpp 编译得到静态库 static_fun
1 | add_library(static_fun STATIC) |
- 动态库 target:使用源文件 shared_fun.cpp 编译得到动态库 shared_fun
1 | add_library(shared_fun SHARED) |
- CMake 还允许一些特殊的库,比如由.o 文件组成的 OBJECT 库(主要为了节约编译时间),或者仅仅由头文件组成的 INTERFACE 库(header-only),见下文
注:
add_library
可以缺省STATIC|SHARED
参数,此时默认为STATIC
全部生成静态库,但是也可以通过指定BUILD_SHARED_LIBS
为真,修改默认值为SHARED
全部生成动态库- 动态库目标会默认启动代码与位置无关的选项(POSITION_INDEPENDENT_CODE),相当于
GCC 的
-fPIC
选项,对于静态库则不会自动启用 - 支持对目标起一个别名,这通常是为了增加命名空间前缀,在被链接时和导入的第三方依赖的命名风格保持一致,例如
1 | add_library(demo SHARED) |
设置目标属性(一)
对于一个 target,我们引入以下两个概念
- build-requirements: 为了正确编译这个 target 我们需要的一切
- usage-requirements: 其它的 target 为了正确使用当前 target 所需要的一切,会自动被它的使用方 target 获取,而 build-requirements 则不会传播给别的 target。
target 通常需要设置如下属性:
- 源文件
- 头文件搜索路径
- 链接的库
- 库搜索路径
- 宏定义
- 编译选项
- 链接选项
- 其它编译特点,例如指定 C++标准
我们使用如下的三个修饰符来区分
- PRIVATE: 私有的,只是 build-requirements,不是 usage-requirements
- INTERFACE: 接口化的,只是 usage-requirements,不是 build-requirements
- PUBLIC: 公开的,既是 usage-requirements,又是 build-requirements
对于一个 target,具体有如下的命令选项,这里的 PRIVATE 换成 INTERFACE 和 PUBLIC 均可
1 | # 源文件 |
注意:
- CMake
中涉及到路径时,默认是相对路径,也可以是绝对路径,并且建议对路径统一使用
/
分隔符。 target_link_libraries
既支持链接到 CMake 的 target,也支持连接到一个已经存在的库文件中(只要找得到)- 建议总是加上这些修饰符,虽然有时候省略也是合法的语法,但不是 modern cmake 推荐的用法。
设置目标属性(二)
除了上述的target_xxx
命令,还有两个命令可以直接访问和修改
target 的属性。 属性其实有很多(参考官网文档),
当前已经设置的所有属性都可以访问和修改,并不局限于正在处理的
CMakeLists.txt。
1 | get_target_property(<VAR> target property) |
例如
1 | set_target_properties(demo PROPERTIES INTERFACE_INCLUDE_DIRECTORIES src/include) |
这里其实分别等价于使用修饰符INTERFACE
和PRIVATE
添加头文件目录
1 | target_include_directories(demo INTERFACE src/include) |
如果设置修饰符为PUBLIC
,则等于同时使用上述两个命令。
从这个角度,target_xxx
系列命令可以看作一组命令语法糖,实质上是在修改
CMake 内部维持的关于指定 target 的变量,包括
1 | INCLUDE_DIRECTORIES |
常见的 property 设置还有:
- 对于动态库,通常生成的是与位置无关的代码(相当于
-fPIC
),但是如果动态库需要链接一个静态库,我们就需要让静态库也具有这个性质,可以如下设置
1 | set_target_properties(demo PROPERTIES POSITION_INDEPENDENT_CODE ON) |
- 对于 GCC,可能存在自动剔除链接静态库中未使用部分的行为,这个行为受到下面属性的控制。(目前只支持 Linux 上的,不支持时会忽略)
1 | set_target_properties(demo PROPERTIES LINK_WHAT_YOU_USE OFF) |
- 如下的命令也可以设置属性,不过只能设置单个属性而非批量设置,例如
1 | set_property(TARGET demo PROPERTY POSITION_INDEPENDENT_CODE ON) |
添加源文件
对一个 target 添加源文件时,可以依次添加每一个源文件
1 | add_executable(demo) |
但是这样太过繁琐,一个很常见的需求是添加指定目录下的所有*.cpp
文件,此时可以使用下面的命令,先搜索所有匹配的源文件存储到
SRCS,然后再添加到 target。(注意相对路径问题)
1 | file(GLOB SRCS src/*.cpp include/*.h) |
这里其实没必要添加头文件,只是便于 VS 项目的生成,因为如果不添加头文件,则生成 VS 项目时会把头文件直接排除出去。
可以使用GLOB_RECURSE
进行递归搜索,此时的*
可以匹配到子文件中的src/test/a.cpp
,但是GLOB
不可以
1 | file(GLOB_RECURSE SRCS src/*.cpp include/*.h) |
建议开启的选项是CONFIGURE_DEPENDS
,因为 file
命令的查找通常只在生成时,而编译时往往会跳过,如果我们此时增减或重命名源文件,与缓存中的不一致会导致编译出错,使用这个选项会让编译时再次执行
file
命令进行校对,如果得到的结果不变则跳过,否则会提示GLOB mismatch!
,然后重新生成构建系统。
1 | file(GLOB_RECURSE SRCS CONFIGURE_DEPENDS src/*.cpp include/*.h) |
这样就组成了一个简单的三行 target
1 | file(GLOB_RECURSE SRCS CONFIGURE_DEPENDS src/*.cpp include/*.h) |
一个替代的命令是下面的aux_source_directory
,它可以自动搜集添加当前目录下的源文件(根据项目使用的语言确定源文件),然后存储到变量中,例如
1 | add_executable(Demo) |
但是这个命令并不支持等价于CONFIGURE_DEPENDS
的选项,因此也存在前文中的问题。
编译结果后缀
我们可以通过给不同编译模式下的结果加上不同的后缀来区分它们,最常见的是_d
后缀,代表
Debug 模式下的库文件。
1 | # libfunc |
对于不指定编译模式或者 Release 模式下的结果,通常不会加后缀。
注意这里的后缀设置是全局的,会自动设置到所有的库文件目标中,但是对于可执行文件目标并不会自动生效,如果希望可执行文件也带上后缀,需要单独设置 target 属性
1 | set_target_properties(demo |
常用片段模板
在项目根目录的 CMakeLists.txt
开头部分,最好在project()
之前,直接使用下面的模板设置一些必要选项
1 | cmake_minimum_required(VERSION 3.15 FATAL_ERROR) |
如下片段直接禁止在源码目录下生成构建系统(这会污染整个项目),可以避免很多误操作
1 | if(PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR) |
设置本地编译的输出目录
1 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin") |
输出一些信息,包括构建系统的具体细节,编译参数等
1 | message(STATUS ">> system = ${CMAKE_SYSTEM_NAME}") |
对于 QT 项目在编译时要进行额外的处理,CMake 需要开启/关闭对应的几个选项,可以使用下面两个函数进行简单的封装
1 | function(My_QtBegin) |
有时候我们需要对编译器的版本提出明确的要求,可以使用下面的片段
1 | if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") # Clang |
特殊目标
除了最常见的可执行文件以及静态库/动态库,CMake 还提供了几个特殊的 target 类型,下面简要介绍。
INTERFACE 库
INTERFACE 库指的是没有编译产物,即通常的 header-only 库,此时对它不需要执行编译,参考官方文档。
可以如下创建 INTERFACE 库
1 | add_library(Demo INTERFACE) |
注意,前文中的 target 属性只有INTERFACE
部分才会对
INTERFACE 库生效。
OBJECT 库
这个库相当于一堆.o
文件的集合,并不会进一步打包到一起得到静态库,通常用于节约编译时间。
OBJECT
库还可以避免静态库的一个问题,即链接时对于没用到的部分进行的自动剔除。
可以如下创建 OBJECT 库
1 | add_library(Demo OBJECT) |
注意,OBJECT 库被链接时,如果是
build-requirements,会把相应的.o
文件直接拿过来一起编译;如果是
usage-requirements,则视作 INTERFACE 库。
IMPORTED 目标
通常标记从第三方库中导入的 target 为 IMPORTED 库,作为使用者应当视其为不可修改的。 链接到 IMPORTED 库可以自动继承它的 usage-requirements。
IMPORTED
标记最好不要单独使用,而是作为库的标记。下面的语句通常在第三方库的导入过程中出现
1 | add_library(Demo STATIC IMPORTED) |
进阶
自定义目标
除了通常的可执行文件和库可以作为目标,CMake也支持将任意的命令通过add_custom_target()
命令打包为一个特殊的工具目标(utility
target), 例如生成代码、执行脚本或清理临时文件的命令操作。
add_custom_target
既可以设置命令的具体内容,也可以设置执行命令时的工作目录,便于跨平台进行统一操作。
例如我们可以在一个普通的可执行文件目标后面,将运行这个可执行文件的命令设置为工具目标
1
2
3
4
5
6add_executable(test main.cpp)
add_custom_target(run
COMMAND $<TARGET_FILE:test>
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)
这里添加了一个名为run
的工具目标,效果就是运行test
,通过下面的命令显式构建它
1
cmake --build build --target run
默认情况下,CMake并不会主动去构建工具目标(即使--target all
也会将其忽略),但是我们也可以加上可选参数ALL
,例如
1
2
3
4
5
6add_executable(test main.cpp)
add_custom_target(run ALL
COMMAND $<TARGET_FILE:test>
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)
这会导致使用--target all
构造所有目标时也执行它。
下面提供的例子是清理临时文件 1
2
3
4add_custom_target(clean_temp_files
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/temp_files
COMMENT "Cleaning temporary files"
)
如果这里需要执行的命令过于复杂,我们还可以将命令通过
add_custom_command()
进一步抽象出自定义命令。 这里的自定义目标和自定义命令都可以进一步嵌入CMake构建系统中,形成更复杂的依赖关系,或者在指定行为下自动触发。这些暂不讨论。
CMake 多文件项目示例
有了上述知识,我们就可以写出一个较复杂的多文件项目的 CMakeLists,这里暂不涉及对第三方库的依赖,不涉及一些编译的复杂逻辑,不涉及安装。项目在 Linux 平台上进行。
Demo 项目的文件结构如下
1 | |-bin |
Demo 项目包括:
- 单独生成可执行文件 test1
- 首先生成静态库 static_fun,然后生成可执行文件 test2,test2 调用静态库 static_fun
- 首先生成动态库 shared_fun,然后生成可执行文件 test3,test3 调用动态库 shared_fun
两个库的头文件分别为
1 | // static_fun.h |
两个库的源文件依次为
1 | // static_fun.cpp |
三个可执行文件的源文件依次为
1 | // test1.cpp |
项目根目录下的 CMakeLists.txt 如下
1 | # CMakeLists(0) |
各级子目录下的 CMakeLists.txt 依次为
1 | # CMakeLists(1) |
需要注意一下进入各个子目录的先后顺序,否则会链接到不存在的 target。
生成构建系统,编译完成后,我们会得到两个库和三个可执行文件,分别存放在 bin 和 lib 目录
1 | |-bin |