现代 CMake 指南
引言
构建工具是 C/C++ 软件开发的必备工具。使用构建工具,可以轻松实现项目的自动化编译和测试,给项目的开发工作带来极大便利。主流的构建工具有 make、MSBuild 和 Ninja 等。
要使用这些构建工具,必须在项目中添加一个配置文件。不同的构建工具需要不同格式的配置文件,比如 make 的配置文件通常是 Makefile
,Ninja 的配置文件通常是 build.ninja
。
由于不同平台上的主流构建工具不同,开发跨平台项目时需要维护多个配置文件。为了解决跨平台项目构建难的问题,CMake 应运而生。
CMake 是跨平台的构建系统生成器,它可以在不同的平台上为项目生成构建系统,并用构建工具完成项目的构建。有了 CMake,开发者只需了解 CMake 的用法,而无需了解每个构建工具的细节,就可以利用构建工具完成项目的构建。
快速开始
准备一个文件夹,作为项目根目录。
1 | mkdir cmake_demo |
添加一个源文件 main.cpp
作为主模块:
1 |
|
添加 CMake 的配置文件,文件名必须是 CMakeLists.txt
:
1 | # 版本要求 |
每一行位于 #
之后的内容视为注释。
第一行的 cmake_minimum_required()
命令指示项目对 CMake 的最低版本要求。
第二行的 project()
命令指示项目的名称、版本号、使用的编程语言,C++ 用 CXX
表示。
第三行的 add_executable()
命令指示项目要生成的可执行文件和它的源文件。
项目要生成的可执行文件或库统称为 目标。add_executable()
命令是为数不多的能够添加目标的命令之一。它用于添加产物为可执行文件的目标。在上面的例子中,第三行的 add_executable()
命令添加了一个名为 myapp
的目标。目标的名称也是最终生成的可执行文件的文件名或库的库名。构建目标的源文件在名称之后列出。
生成配置
准备一个子文件夹作为 构建目录,习惯上命名为 build
。用 cmake
命令在该目录中生成构建工具的配置。
1 | mkdir build |
要在 cmake
命令之后跟上 源目录(即源文件所在目录)的路径,此处为父目录 ..
。
提示:上面三条命令也可以用下面一条命令代替:
1 | cmake -S . -B build |
-S
选项指示源目录,在本示例中为当前目录 .
;-B
选项指示构建目录,在本示例中为子文件夹 build
。CMake 会自动创建 -B
选项指示的文件夹。
CMake 用于生成配置文件的部件称为 生成器。不同的生成器用于生成不同构建工具的配置。生成器可用 -G
选项选择,比如,在 Windows 平台可以使用 MinGW Makefiles
生成 MinGW-w64 的配置文件。
1 | cmake -S . -B build -G "MinGW Makefiles" |
要查看生成器的列表,用 cmake -h
命令(带 *
号的是默认的生成器)。
1 | cmake -h |
构建项目
在构建目录生成构建工具的配置后,紧接着就可以调用构建工具开始构建项目了。要调用构建工具,用 --build
选项。选项之后要跟上构建目录的路径。在本示例中,如果是在构建目录中执行该命令,就是当前目录 .
。
1 | cmake --build . |
执行该命令时,带上 -v
选项,此时 CMake 会显示实际执行的每个编译和链接命令。
在 Linux 上使用 make 或者在 Windows 上使用 MinGW-w64 时,生成的可执行文件位于构建目录中;在 Windows 上使用 Visual Studio 时,生成的可执行文件位于构建目录的子文件夹 Debug
中。
1 | ./Debug/myapp |
在构建大型项目时,为了加快编译速度,常常向构建工具传递 -j4
或 -m:4
选项来启用 多线程编译。
1 | cmake --build . -- -m:4 |
在实际开发中,常常借助脚本使构建流程自动化。在项目根目录添加一个脚本,习惯上命名为 build.sh
。
1 |
|
--target
或 -t
选项用于明确指定需要构建的目标。有多个构建目标时,用空格隔开。在 Windows 上使用 Visual Studio 作为构建工具时,可用 --config
选项选择构建配置。当构建配置为 Release
时,生成的可执行文件位于构建目录的子文件夹 Release
中。构建配置的详细介绍见后文。
基本用法
模块化
添加一个算术模块 math.cpp
,在主模块 main.cpp
中包含它的头文件 math.hpp
并调用它提供的函数 add()
。
math.hpp
1 | int add(int a, int b); |
math.cpp
1 |
|
main.cpp
1 |
|
在配置文件 CMakeLists.txt
中用 add_executable()
命令添加构建目标 myapp
,在目标名之后列出各个模块的头文件和源文件,在本示例中为 main.cpp math.hpp math.cpp
。
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21.0) |
在 add_executable()
命令中列出头文件不是必须的,但这样做可以提高 IDE 的用户体验。
运行脚本 build.sh
完成项目的构建并调用生成的可执行文件 myapp
:
1 | ./build.sh |
生成静态库
可以单独编译算术模块,生成一个静态库,再让主模块编译而成的可执行文件链接到该静态库。要添加产物为库的构建目标,用 add_library()
命令。它的参数和 add_executable()
命令类似。不同的是,它需要用于指示静态库或动态库的关键字 STATIC
或 SHARED
作为第二个参数。
1 | add_library(math STATIC math.hpp math.cpp) |
添加源文件
目标的源文件也可以通过 target_sources()
命令添加:
1 | add_library(math STATIC math.hpp math.cpp) |
PRIVATE
关键字的含义见高级篇。
添加附加依赖项
要让可执行文件链接到静态库,用 target_link_libraries()
命令:
1 | add_executable(myapp main.cpp) |
target_link_libraries()
命令也指明了两个目标之间的 依赖关系。在构建项目时,无需列出每个需要构建的目标,CMake 会根据目标之间的依赖关系自动找出每个需要构建的目标。
最终 CMakeLists.txt
的内容如下:
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21.0) |
运行脚本 build.sh
即可得到由算术模块编译而成的静态库:
1 | ./build.sh |
生成动态库
要将算术模块编译成一个动态库,需要修改算术模块的头文件:在编译算术模块时,函数声明要用 __declspec(dllexport)
修饰;在编译主模块时,函数声明要用 __declspec(dllimport)
修饰。习惯上,函数声明的变化常常借助预编译指令 #ifdef...#else...#endif
实现。
math.hpp
1 |
|
要生成动态库,add_library()
命令的第二个参数要修改成 SHARED
:
1 | add_library(math SHARED math.hpp math.cpp) |
添加宏定义
要在编译算术模块时定义 MATH_EXPORT
宏,可用 target_compile_definitions()
命令:
1 | target_compile_definitions(math PRIVATE MATH_EXPORT) |
最终 CMakeLists.txt
的内容如下:
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21.0) |
运行脚本 build.sh
即可得到由算术模块编译而成的动态库 math.dll
及其导入库 math.lib
:
1 | ./build.sh |
生成显式加载的动态库
显式加载动态库是指可执行文件在运行时根据需要选择性从硬盘加载动态库到内存中。可执行文件无需链接到显式加载的动态库,在它的源文件中也无需包含动态库的头文件,但需要调用一些函数来加载动态库(在 Windows 上要包含头文件 windows.h
并使用 LoadLibrary()
和 GetProcAddress()
;在 Linux 上要包含头文件 dlfcn.h
并使用 dlopen()
和 dlsym()
)。
main.cpp
1 |
|
GetProcAddress()
函数根据函数的符号名获取函数的地址。在 C 语言中,符号名就是函数名,但是在 C++ 中,符号名还跟函数的参数有关。习惯上,为了兼容 C 程序,动态库导出的函数的符号名常常以 C 语言的方式处理。要以 C 语言的方式处理函数的符号名,要用 extern "C"
修饰函数的声明:
math.hpp
1 | extern "C" DECL_API int add(int a, int b); |
要生成显式加载的动态库,add_library()
命令的第二个参数要修改成 MODULE
。要显式加载动态库,无需用 target_link_libraries()
命令让可执行文件链接到该动态库。
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21.0) |
此时如果直接执行脚本 build.sh
,就会发现 math
没有被构建。这是因为脚本 build.sh
中的构建命令用 --target
选项指定的构建目标只有 myapp
一个。在前面的例子中,math
也会被构建是因为它和 myapp
之间存在依赖关系。现在删除了 target_link_libraries()
命令,myapp
和 math
之间的依赖关系也就不复存在。为了构建 math
,要修改构建命令,在 --target
选项之后追加 math
。
build.sh
1 |
|
如果不使用 --target
选项,所有目标都会被构建,除了那些使用关键字 EXCLUDE_FROM_ALL
作为命令的参数之一添加的目标。
基础篇
语法基础
布尔变量
add_library()
命令的第二个参数可以省略,它的取值取决于 布尔变量 BUILD_SHARED_LIBS
:当 BUILD_SHARED_LIBS
的值为 OFF
时,取值为 STATIC
;当 BUILD_SHARED_LIBS
为 ON
时,取值为 SHARED
。该变量的默认值为 OFF
。变量的值可用 set()
命令设置:
1 | set(BUILD_SHARED_LIBS ON) |
将变量名要放在 ${
和 }
中间,即可得到变量的值。例如,要打印变量的值,可借助 message()
命令:
1 | message(STATUS "BUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}") |
message()
命令的第一个参数用于指示信息的类型,它会影响 CMake 的行为;第二个参数是作为信息内容的字符串。常用的信息类型有下列三种:
NOTICE
表示需要留意的信息(默认值)STATUS
表示一般的信息,是信息数量最多的类型,输出时带有--
前缀FATAL_ERROR
表示发生致命错误的信息,会导致 CMake 终止配置的生成
三种类型的信息的输出格式如下所示:
1 | message("This is the NOTICE message") |
列表变量
变量的值可以是一个列表,比如源文件的列表,这种变量称为 列表变量。列表变量可用 list()
命令设置:
1 | list(APPEND sources main.cpp math.hpp math.cpp) |
APPEND
关键字表示向列表追加元素。
字符串变量
布尔变量和列表变量本质上都是字符串变量。所有变量的值都以字符串的形式存储。列表变量的值是用分号 ;
作为分隔符的字符串。
1 | list(APPEND sources main.cpp math.hpp math.cpp) |
1 | cmake -S . -B build |
用字符串作为命令的参数时,字符串两边的引号可以省略,只要字符串不含分号,否则字符串就会被展开成多个参数(可能造成语法错误)。
1 | add_executable(myapp main.cpp;main.hpp) |
环境变量
访问名为 TEMP
的环境变量的语法是 $ENV{TEMP}
。可用 if
语句检查一个环境变量是否有定义:
1 | if(DEFINED ENV{TEMP}) # 注意,此处无美元符号 `$` |
环境变量 TEMP
的值是临时文件夹的路径。
1 | cmake -S . -B build |
预定义变量
有很多预定义的变量可以直接使用。例如,开发环境的信息(如编译器的版本和路径)记录在一组预定义变量中。执行带有 --system-information
选项的 cmake
命令,就可以看到这些变量的值。还可将它们写入一个文件,只需在选项之后跟上文件名。
1 | cmake -G "MinGW Makefiles" --system-information sysinfo.txt |
条件语句和运算符
将布尔变量和条件语句结合,可实现选择性构建。例如,仅当 BUILD_LIBS
为 ON
时,才生成 math
静态库。
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21.0) |
其他变量可以和 NOT
、AND
等运算符结合构成一个布尔表达式:
NOT BUILD_LIBS
取反A AND B
且A OR B
或A STREQUAL B
两个字符串A
和B
相等EXIST PATH
路径为PATH
的文件或目录存在DEFINED VAR
变量VAR
有定义
命令行设置变量
在执行 cmake
命令生成配置时,可用 -D
选项设置变量。
build.sh
1 |
|
布尔变量的默认值
可以用 option()
命令给布尔变量设置默认值。参数依次是变量的变量名、注释和默认值。
1 | option(BUILD_LIBS "Compile sources into a library" OFF) |
目标的属性
目标的属性包含有关目标的一切信息。例如,目标的 SOURCES
属性是源文件的列表;COMPILE_DEFINITIONS
属性是宏定义的列表。
获取属性
属性的值可用 get_target_property()
命令获取。它能够将变量的值设置为属性的值。
1 | add_library(math SHARED math.hpp math.cpp) |
1 | cmake -S . -B build |
设置属性
目标的属性可以通过 set_target_properties()
命令设置:
1 | set_target_properties(math PROPERTIES SOURCES "math.hpp;math.cpp") |
模块
有些命令(也就是函数)由模块提供。在使用模块提供的函数之前,要用 include()
命令导入模块。例如,导入 CMake 内置的 CMakePrintHelpers
模块。
1 | include(CMakePrintHelpers) |
打印目标的属性
内置模块 CMakePrintHelpers
提供的cmake_print_properties()
函数用于打印目标的属性。例如,打印目标 math
的 TYPE
和 COMPILE_DEFINITIONS
两个属性:
1 | add_library(math SHARED math.hpp math.cpp) |
1 | cmake -S . -B build |
TYPE
为 SHARED_LIBRARY
说明构建该目标将得到一个动态库。
打印变量
CMakePrintHelpers
模块提供的另一个函数 cmake_print_variables()
用于打印变量。例如,要打印 BUILD_SHARED_LIBS
和 sources
两个变量:
1 | set(BUILD_SHARED_LIBS ON) |
1 | cmake -S . -B build |
构建配置
构建配置会影响目标的编译和链接选项。常用的构建配置有 Debug
和 Release
两种,默认值为 Debug
。
构建工具可分为单配置构建工具和多配置构建工具。常用的单配置构建工具有 make、MinGW-w64 和 Ninja。常用的多配置构建工具有 Visual Studio 和 Xcode。使用多配置构建工具构建项目时,可以在不同的构建配置之间快速切换。
单配置构建工具
使用单配置构建工具时,构建配置在生成配置时就已经确定,它由变量 CMAKE_BUILD_TYPE
指示。不同构建配置的编译选项记录在一组预定义变量中:
1 | CMAKE_CXX_FLAGS "" |
CMAKE_CXX_FLAGS
变量包含通用编译选项。
当构建配置为 Release
时,NDEBUG
宏会被定义。
main.cpp
1 |
|
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.20.0) |
在生成配置时,用 -G
选项选择单配置构建工具,用 -D
选项设置变量 CMAKE_BUILD_TYPE
的值,然后运行程序,观察程序的输出:
1 | cd build |
多配置构建工具
使用多配置构建工具时,可用的构建配置在生成配置时列出,实际使用的构建配置在构建项目时指定。具体来说,可用的构建配置是由列表变量 CMAKE_CONFIGURATION_TYPES
决定的。例如,只需 Debug
和 Release
两种构建配置。
1 | set(CMAKE_CONFIGURATION_TYPES Debug Release) |
在构建项目时,用选项 --config
指定构建配置。
1 | cmake --build . --config Release |
当构建配置为 Debug
时,文件名常常带有后缀 d
,这可以通过设置变量 CMAKE_DEBUG_POSTFIX
实现。
1 | set(CMAKE_DEBUG_POSTFIX d) |
子源目录
可将部分源文件放置到子源目录(即源目录的子文件夹)中,再用 add_subdirectory()
命令指示 CMake 处理这个子文件夹。例如,在子文件夹 hello
中放置源文件 main.cpp
:
hello/main.cpp
1 |
|
子源目录也需要一个 CMakeLists.txt
文件:
hello/CMakeLists.txt
1 | add_executable(hello main.cpp) |
子源目录中的 CMakeLists.txt
只需列出要生成的可执行文件或库。父文件夹中的 CMakeLists.txt
要用 add_subdirectory()
命令指示子源目录和相应的 子构建目录。
CMakeLists.txt
1 | add_subdirectory(hello hello_build) |
构建项目:
1 | cd build |
可执行文件 hello.exe
可在构建目录 build
的子文件夹 hello_build
中找到。
源目录的路径
子源目录中可能又有子源目录。子源目录还有可能是 子项目 的根目录。因此,源目录的路径并不简单。不同源目录的绝对路径记录在一组预定义变量中:
CMAKE_CURRENT_SOURCE_DIR
当前源目录的绝对路径CMAKE_SOURCE_DIR
根源目录的绝对路径PROJECT_SOURCE_DIR
项目源目录的绝对路径
在每个 CMakeLists.txt
文件中都可以使用上述三个变量。比较容易混淆的是后两个变量。简单来说,项目源目录既有可能是根源目录,也有可能是介于根源目录和当前源目录中间的某一级子源目录。某一级子源目录是不是项目源目录,取决于该源目录中的 CMakeLists.txt
文件是否使用了 project()
命令。由此可见,项目源目录也就是项目根目录。
下面是一个具有四级源目录的示例:
1 | tree cmake_demo |
每一级源目录中的 CMakeLists.txt
文件都使用 cmake_print_variables()
函数打印上述三个变量的值。不同的是,只有 cmake_demo
和 cmake_demo/a/b
两级源目录是项目源目录。
cmake_demo/CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21) |
cmake_demo/a/CMakeLists.txt
1 | include(CMakePrintHelpers) |
cmake_demo/a/b/CMakeLists.txt
1 | project(mysubproj VERSION 0.1.0 LANGUAGES C CXX) |
cmake_demo/a/b/c/CMakeLists.txt
1 | include(CMakePrintHelpers) |
执行 cmake
命令生成配置:
1 | cmake -S . -B build |
输出目录
输出目录是可执行文件和库的保存位置。默认输出目录是构建目录。使用多配置构建工具时,实际的输出目录还受构建配置的影响。比如,当构建配置为 Release
时,多配置构建工具会在输出目录中创建子文件夹 Release
。
预定义变量 PROJECT_BINARY_DIR
的值是构建目录的绝对路径。
1 | cmake_print_variables(PROJECT_SOURCE_DIR PROJECT_BINARY_DIR) |
1 | cmake -S . -B build |
有两种方式可以改变输出路径。后者是新版本的 CMake 推荐的方式。
通过变量设置
可以通过设置 EXECUTABLE_OUTPUT_PATH
和 LIBRARY_OUTPUT_PATH
两个变量改变可执行文件和库的输出路径。比如,将所有的可执行文件保存到项目根目录的子文件夹 bin
中;将所有的库保存到项目根目录的子文件夹 lib
中。
1 | set(EXECUTABLE_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/bin") |
该设置会影响项目的所有可执行文件和库,包括子源目录中的目标。
通过属性设置
对于可执行文件和库的输出路径,可以通过设置目标的下列三个属性实现更细粒度的控制:
RUNTIME_OUTPUT_DIRECTORY
属性是用add_executable()
命令创建的可执行文件(.exe
)的输出路径,在 Windows 平台上还包括用带有SHARED
选项的add_library()
命令创建的动态库(.dll
);ARCHIVE_OUTPUT_DIRECTORY
属性是用带有STATIC
选项的add_executable()
命令创建的静态库(.lib
或.a
)的输出路径,在 Windows 平台上还包括用带有SHARED
或MODULE
选项的add_library()
命令创建的动态库的导入库.lib
;LIBRARY_OUTPUT_DIRECTORY
属性是用带有MODULE
选项的add_executable()
命令创建的动态库(.dll
或.so
)的输出路径,在 Linux 平台上还包括用带有SHARED
选项的add_library()
命令创建的动态库(.so
)。
上述三个属性的默认值分别由下列变量决定:
CMAKE_RUNTIME_OUTPUT_DIRECTORY
CMAKE_ARCHIVE_OUTPUT_DIRECTORY
CMAKE_LIBRARY_OUTPUT_DIRECTORY
因此,在多数情况下,要利用上述三个属性设置目标的输出路径,也是通过变量间接设置。
下面的示例共包含可执行文件 app
、静态库 math_static
、动态库 math_shared
和显式加载的动态库 math_module
四个目标。
app.cpp
1 |
|
math.cpp
1 |
|
math.cpp
1 |
|
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.21.0) |
1 | cmake -S . -B build |
附加包含目录
包含目录是构建工具在其中检索头文件(.h
或 .hpp
)的目录。构建工具默认只在源文件所在目录和系统包含目录中检索头文件。然而,在实际开发中,头文件和源文件通常被放在不同的子文件夹中。习惯上,将头文件放在子文件夹 include
中,将源文件放在子文件 src
中。要让构建工具在额外的包含目录中检索头文件,要用 target_include_directories()
命令添加附加包含目录。假设头文件位于子文件夹 include
中:
1 | target_include_directories(myapp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") |
附加库目录
库目录是构建工具在其中检索库(.lib
、.a
或 .so
)的目录。要让构建工具在额外的库目录中检索库,要用 target_link_directories()
命令添加附加库目录。假设预编译的第三方库位于子文件夹 3rd_party
中:
1 | target_link_directories(myapp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/3rd_party") |
自定义构建规则
在 Windows 上,当可执行文件使用了预编译的第三方动态库时,通常需要在生成可执行文件后,将第三方动态库复制到可执行文件所在目录中,以便进行测试,为此需要自定义构建规则。
自定义的构建规则实际上就是一组用 add_custom_command()
命令添加的命令:
1 | add_custom_command(TARGET myapp POST_BUILD |
参数依次是针对的目标、命令执行的时机和命令的列表。命令执行的时机有三种,由三个不同的关键字表示:
PRE_BUILD
构建之前PRE_LINK
编译之后、链接之前POST_BUILD
构建之后
添加的命令通常就是带有 -E
选项的 cmake
命令(CMAKE_COMMAND
变量的值是 cmake
命令的绝对路径)。-E
选项的参数是 CMake 提供的子命令,这些子命令在不同的平台有相同的行为。上面的示例添加了一条在目标 myapp
构建完成之后执行的 copy
命令,它的作用是将动态库 /path/to/*.dll
复制到 ${PROJECT_BINARY_DIR}
目录中。
源文件分组
将源文件分组是为了提高 IDE 的用户体验。例如,在 Visual Studio 的解决方案资源管理器中,目标(项目)通常都包含 Header Files
和 Source Files
两个分组(筛选器),用来分开目标的头文件和源文件。
Source Files
分组是源文件的默认分组。要设置更多分组,可以使用 source_group()
命令。
1 | list(APPEND headers main.h util.h) |
在上面的例子中,列表 headers
中的源文件都被添加到分组 Header Files
中。为源文件设置好分组后,IDE 就会分组列出目标的源文件。源文件的分组也可以在源文件被添加到目标之后进行,效果相同。
进阶篇
语法进阶
缓存变量
在构建目录中可以找到文件 CMakeCache.txt
,它是 CMake 在生成配置时产生的,作用是记录一些变量的值。
缓存的主要作用是记住用户的选择,这样下次使用生成配置时,用户就不必再提供这些信息。例如,用 option()
命令创建的布尔变量就会被保存缓存起来。
1 | option(USE_JPEG "Do you want to use the jpeg library" OFF) |
无论何时,缓存变量的值都可以用命令行选项 -D
重新设置。简单来说,缓存变量的值,要么是在命令行中用 -D
选项指定的值,要么是缓存的值。
1 | cmake -D USE_JPEG=ON .. |
option()
命令只能用于设置并缓存布尔变量。要缓存其他类型的变量,要用带有 CACHE
选项的 set()
命令,它的行为和 option()
命令一致。
1 | set(USE_JPEG OFF CACHE BOOL "include jpeg support?") |
参数依次是变量名、默认值、关键字 CACHE
、变量的类型和注释。最后两个参数的作用是提高 GUI 的用户体验,常用的类型有下列四种:
BOOL
GUI 将显示一个复选框FILEPATH
GUI 将显示一个「打开文件」对话框PATH
GUI 将显示一个「选择文件夹」对话框STRING
GUI 将显示一个文本框或下拉列表
要忽略并覆盖缓存的值,加上 FORCE
选项:
1 | set(USE_JPEG OFF CACHE BOOL "include jpeg support?" FORCE) |
1 | cmake -D USE_JPEG=ON .. |
通过设置缓存变量的 STRINGS
属性,可以创建特殊的缓存变量。这种缓存变量的取值只能是给定选项之一。
1 | set(COLOR "Red" CACHE STRING "Select a color") |
STRINGS
属性的值就是选项的列表。对于这种变量,GUI 将显示一个下拉列表。
自定义命令
可以自定义两种命令:函数和带参数的宏。
函数用 function()
命令定义,第一个参数是函数名,之后都是形参。
1 | # 定义函数 |
1 | cmake .. |
带参数的宏用 macro()
命令定义。
1 | # 定义带参数的宏 |
1 | cmake .. |
函数和带参数的宏都可以使用下列预定义变量:
ARGC
参数的个数ARGV
所有参数的列表ARGN
除形参之外的所有参数的列表ARGV0
第一个参数ARGV1
第二个参数ARGV2
第三个参数
1 | function(fn a b c) |
1 | cmake .. |
变量的作用域
每处理一个子源目录,或调用一个函数,就会形成一个子作用域。在父作用域中设置的变量会被复制到子作用域中。因此,在子作用域中可以使用在父作用域中设置的变量,但反过来不行。注意,子作用域中的 set()
命令默认修改的不是父作用域中的变量,而是它们在子作用域中的副本。
1 | function(foo) |
1 | cmake .. |
带有 PARENT_SCOPE
选项的 set()
命令修改的是父作用域中的变量,而不是它们在子作用域中的副本。
1 | ... |
1 | cmake .. |
整合第三方库
有多种方式可以将第三方库整合到项目中,它们适用于不同的场景。
从网上获取源码的库
对于需要从网上获取源码并在本地构建的库,可借助内置模块 FetchContent
将其整合到项目中。
1 | include(FetchContent) |
使用内置模块 FetchContent
整合第三方库需要两个步骤:
- 先用
FetchContent_Declare()
命令声明库 - 再用
FetchContent_MakeAvailable()
命令启用声明的库
下面是整合日志库 glog 的示例。
1 | FetchContent_Declare(glog |
声明库时要指定库名、仓库的地址和保存位置。SOURCE_DIR
属性指示构建工具将源码放置到 3rd_party/glog
文件夹中。注意,第三方库的获取发生在构建项目时,而不是在生成配置时。
第三方库很可能也利用该机制整合其他库。在网络连接不稳定的环境中,为了避免因第三方库源码获取失败而导致项目构建失败,可以通过设置变量 FETCHCONTENT_SOURCE_DIR_<uppercaseName>
指示构建工具使用事先准备好的源码。假设 glog 的源码已事先放置在 src/glog
文件夹中:
1 | set(FETCHCONTENT_SOURCE_DIR_GLOG "${CMAKE_CURRENT_SOURCE_DIR}/src/glog") |
FetchContent
模块实际上是 ExternalProject
模块的封装。ExternalProject
模块的接口更丰富。
1 | include(ExternalProject) |
使用 ExternalProject
模块整合第三方库,通常只需用 ExternalProject_Add()
函数指示第三方库如何获取、配置、构建、安装和测试。
1 | ExternalProject_Add(gflags-2.2.2 |
PREFIX
选项指示第三方库相关文件的保存位置。和目录有关的选项有很多个,它们都有默认值:
1 | TMP_DIR = <prefix>/tmp # 临时文件保存位置 |
DOWNLOAD_DIR
和第三方库的获取方式有关。常用的获取方式有 URL 下载和 Git 克隆两种。仅当采用 URL 下载时,才需要该选项。
若采用 URL 下载方式,则需指定 URL
选项。下载位置由上述 DOWNLOAD_DIR
选项指示。如果 URL 的末尾不适合作为文件名,可用 DOWNLOAD_NAME
指示新的文件名。
1 | URL https://github.com/gflags/gflags/archive/v2.2.2.zip |
若采用 Git 克隆,则需要指定 GIT_REPOSITORY
选项。如果需要代码库的特定版本,可以用 GIT_TAG
选项指示一个标签。
1 | GIT_REPOSITORY https://github.com/gflags/gflags.git |
生成配置的命令所需的选项可用 CMAKE_ARGS
选项指示,通常要包含 CMAKE_INSTALL_PREFIX
和 BUILD_STATIC_LIBS
两个选项,前者指示第三方库的安装目录,后者指示是否构建静态库。
1 | CMAKE_ARGS -DBUILD_STATIC_LIBS=ON |
如有需要,可用下列选项自定义库的配置、构建和安装命令。
1 | CONFIGURE_COMMAND |
通过包管理器安装的库
有些库可以用包管理器安装。比如在 Ubuntu 上可以用 apt
命令安装 OpenCV:
1 | sudo apt install -y libopencv-dev |
这种安装方式方便快捷。安装好后,可用 pkg-config
命令查看头文件和库的位置以及要链接的库:
1 | pkg-config --cflags --libs openh264 |
实际上,通过包管理器安装的库都会提供一个 .pc
文件,里面记录了库的头文件和链接库的位置。
1 | cat /usr/lib/x86_64-linux-gnu/pkgconfig/opencv4.pc |
带 --list-all
选项执行 pkg-config
命令,可查看本机安装的所有库:
1 | pkg-config --list-all | grep opencv |
在 CMake 中,要整合通过包管理器安装的库,要借助内部模块 FindPkgConfig
。
1 | include(FindPkgConfig) |
该模块被引入后会设置若干变量,如指示 pkg-config
命令是否找到 PKG_CONFIG_FOUND
变量和指示 pkg-config
命令可执行文件的路径的 PKG_CONFIG_EXECUTABLE
变量。
1 | include(FindPkgConfig) |
1 | cmake .. |
要得到库的信息,用 pkg_check_modules()
命令。
1 | pkg_check_modules(<prefix> <moduleSpec>) |
该命令会根据库名设置一系列变量。第一个参数指示变量名的前缀。最后一个参数指示库名。
1 | pkg_check_modules(OpenCV opencv4) |
1 | cmake .. |
若添加了 IMPORTED_TARGET
选项,则在 target_link_libraries()
命令中可以使用参数 PkgConfig::OpenCV
。
1 | pkg_check_modules(OpenCV REQUIRED IMPORTED_TARGET opencv4) |
预编译的软件包
预编译的软件包可用 find_package()
命令查找。预编译的软件包通常包含一个 *Config.cmake
文件。前缀 *
是包名。CMake 通过该文件获取软件包的信息。find_package()
命令的作用就是根据包名在特定路径检索 *Config.cmake
文件,然后用包名作为变量名的前缀设置一组变量。例如,在查找预编译的 OpenCV 时,若 CMake 能够找到 OpenCVConfig.cmake
,则变量 OpenCV_FOUND
为真,否则为假。在能够找到 OpenCV 的情况下,还会设置下列变量:
OpenCV_INCLUDE_DIRS
附加包含目录OpenCV_LIB_PATH
附加库目录OpenCV_LIBS
附加依赖项
find_package()
命令默认先搜索变量 CMAKE_MODULE_PATH
列出的目录。如果设置了 *_DIR
变量,还会该变量指示的目录。
在调用 find_package()
命令时,要用包名作为第一个参数。
1 | find_package(OpenCV REQUIRED PATHS "${OPENCV_DIR}/build" NO_DEFAULT_PATH) |
接下来,把依赖于 OpenCV 的构建目标链接到 OpenCV 即可。
1 | add_executable(myapp main.cpp) |
预编译的库
对于预编译的库,可用带有 IMPORTED
选项的 add_library()
命令导入。
1 | add_library(mkldnn SHARED IMPORTED) |
库的路径由 IMPORTED_LOCATION
属性指示。对于静态库,IMPORTED_LOCATION
属性指示 .lib
或 .a
文件的路径;对于动态库,IMPORTED_LOCATION
属性指示 .dll
或 .so
文件的路径。在 Windows 平台上,对于动态库,还要用 IMPORTED_IMPLIB
属性指示动态库的导入库。
1 | set_target_properties(mkldnn PROPERTIES |
附加包含目录要用 INTERFACE_INCLUDE_DIRECTORIES
属性指示。
最终会得到一个特殊的目标。要链接该库,只需链接该目标。
1 | target_link_libraries(myapp mkldnn) |
C 运行库
C 语言的标准包含两部分,一部分描述 C 语言的语法,一部分描述 C 标准库(C Standard Library)。
C 运行库(C Runtime Library,CRT)是 C 标准库在不同平台上的具体实现。使用最广泛的 C 运行库是 Linux 上的 glibc(GNU C Library)和 Windows 上的 MSVCRT(Microsoft Visual C++ Runtime)。
Windows 上的 CRT
由于 MSVCRT 有很多缺点,微软重构了 CRT。新的 CRT 随着 Visual Studio 2015 一同推出。在 Windows 10 上,新的 CRT 已经取代 MSVCRT 成为默认的 CRT。新的 CRT 由三组库构成:
- 通用 C 运行库(Universal C Runtime,UCRT)
- VC 运行库(VC Runtime Library)
- 基本 C 运行库(Basic C Runtime Library)
当程序包含了 C++ 的标准头文件(如 iostream
)时,还需要使用相应的 C++ 标准库(C++ Standard Library)。这些库的各个版本的库名以及使用它们的程序在构建时需要添加的编译和链接选项如下表所示:
UCRT | VC Runtime Library | Basic C Runtime library | C++ Standard Library | Options |
---|---|---|---|---|
libucrt.lib |
libvcruntime.lib |
libcmt.lib |
libcpmt.lib |
/MT |
libucrtd.lib |
libvcruntimed.lib |
libcmtd.lib |
libcpmtd.lib |
/MTd |
ucrtbase.dll (ucrt.lib ) |
vcruntime*.dll (vcruntime.lib ) |
msvcrt.lib |
msvcp*.dll (msvcprt.lib ) |
/MD |
ucrtbased.dll (ucrtd.lib ) |
vcruntime*d.dll (vcruntimed.lib ) |
msvcrtd.lib |
msvcp*d.dll (msvcprtd.lib ) |
/MDd |
注:
- 库名带有
d
后缀的是 Debug 版本,不带后缀的是 Release 版本 - 括号中是动态库的导入库,如动态库
ucrtbase.dll
的导入库是ucrt.lib
- 库名中的
*
是编译器的版本号,如140
- 基本 C 运行库
msvcrt.lib
和msvcrtd.lib
都是静态库,不是动态库的导入库
在 Visual Studio 中,可以在项目的属性页中(C/C++ - 代码生成 - 运行库)选择不同的 CRT。
在低于 3.15 版本的 CMake 中,要让程序静态链接到 CRT 的 Release 版本,常用下面的宏将选项中的 /MD
统一替换成 /MT
:
1 | macro(safe_set_static_flag) |
在新版本的 CMake 中,只需设置目标的 MSVC_RUNTIME_LIBRARY
属性即可:
1 | # 静态链接到 CRT 的 Release 版本 |
构建 32 位程序
在 64 位的 Windows 上,既可以运行 64 位的程序,也可以运行 32 位的程序,但是 64 位的程序不能加载 32 位的动态库,表现为找不到模块。
在 Windows 上生成配置时,可用 -A
选项设置目标平台。在 64 位的操作系统上,默认目标平台为 x64
,即构建 64 位的程序。若设置为 Win32
,则构建 32 位的程序:
1 | cmake .. -A Win32 |
在配置文件中,当前目标平台由变量 CMAKE_VS_PLATFORM_NAME
指示:
1 | message(FATAL_ERROR "CMAKE_VS_PLATFORM_NAME=${CMAKE_VS_PLATFORM_NAME}") |
在源文件中,只需检查 _WIN64
宏是否有定义,即可确定当前目标平台:
1 |
|
要在 Linux 上构建 32 位的程序,只需增加编译和链接选项 -m32
。
1 | add_compile_options($<$<PLATFORM_ID:Linux>:-m32>) |
高级篇
逻辑目标
构建条件和使用条件
每个目标都有两种属性:属性名不带 INTERFACE_
前缀的属性是私有属性,它们描述该目标的构建条件(Build-Requirements),即构建该目标需要满足的条件;属性名带有 INTERFACE_
前缀的属性是接口属性,它们描述该目标的使用条件(Usage-Requirements),即使用该目标需要满足的条件。简单来说,目标的使用条件,就是依赖该目标的其他目标的构建条件。
目标的属性既可以用 set_target_properties()
命令设置,也可以用 target_*()
命令设置。它们之间的区别在于,set_target_properties()
命令不区分私有属性和接口属性,因此 INCLUDE_DIRECTORIES
属性和 INTERFACE_INCLUDE_DIRECTORIES
属性是两个相互独立的属性;而 target_*()
命令会把目标的构建条件对应到目标的私有属性,把目标的使用条件对应到目标的接口属性。
在用 target_*()
命令设置目标的属性时,要用下列关键字指示条件的类型:
PRIVATE
仅构建条件INTERFACE
仅使用条件PUBLIC
既是构建条件,也是使用条件
在下面的例子中,第一条命令会设置目标 mylib
的 INCLUDE_DIRECTORIES
属性;第二条命令会设置目标 mylib
的 INTERFACE_INCLUDE_DIRECTORIES
属性;第三条命令是前两条命令的组合版本。
1 | target_include_directories(mylib PRIVATE ${PROJECT_SOURCE_DIR}) |
设置私有属性和接口属性的作用有很大不同。在下面的例子中,第一条命令表示构建目标 mylib
时,需要编译选项 -std=c++20
;第二条命令表示链接目标 mylib
时,需要附加包含目录 ${CMAKE_CURRENT_SOURCE_DIR}
。
1 | target_compile_features(mylib PRIVATE cxx_std_20) |
下列是常用的属性设置命令:
target_sources()
添加源文件target_include_directories()
添加附加包含目录target_link_directories()
添加附加库目录target_link_libraries()
添加附加依赖项target_compile_definitions()
添加宏定义target_compile_options()
添加通用编译选项target_link_options()
添加通用链接选项target_compile_features()
开启编译器特性
在用 target_compile_features()
命令开启编译器特性时,可以通过检查预定义变量 CMAKE_CXX_COMPILE_FEATURES
确定有哪些编译器特性可以使用。有些编译器特性是通过添加某个编译选项开启的,因此下面两条命令的作用相同:
1 | target_compile_features(mylib PRIVATE cxx_std_20) |
接口目标和导入目标
逻辑目标是在构建时不产生任何可执行文件、静态库或动态库的目标,分为接口目标和导入目标两种:接口目标用于声明一组使用条件,这是通过设置它的接口属性实现的;导入目标用来表示一个预编译的库,这是通过设置它的 IMPORTED_*
属性实现的。
导入目标的名称可以包含 ::
。使用预编译的库常常需要满足一定的使用条件,因此,导入目标常常也是接口目标,它的接口属性指示预编译的库的使用条件。
在非 Windows 的平台上,动态库只有一个 .so
文件。若动态库具有 SONAME
,则要设置 IMPORTED_SONAME
属性;若动态库没有 SONAME
,而平台支持 SONAME
,则要设置 IMPORTED_NO_SONAME
属性。
生成器表达式
在设置属性时,可以借助生成器表达式:
- 布尔型
$<TARGET_EXISTS:tgt>
当目标tgt
存在时为真$<CONFIG:cfg>
当构建配置为cfg
时为真
- 字符串型
$<CONFIG>
构建配置,如Debug
或Release
$<TARGET_FILE:tgt>
目标tgt
的可执行文件或动态库$<TARGET_FILE_DIR:tgt>
目标tgt
的可执行文件或动态库所在目录$<condition:true_string>
当条件condition
为真时,表达式为true_string
,否则为空$<IF:condition,true_string,false_string>
当条件condition
为真时,表达式为true_string
,否则为false_string
安装
撰写中……
打包
撰写中……