现代 CMake 指南

引言

构建工具是 C/C++ 软件开发的必备工具。使用构建工具,可以轻松实现项目的自动化编译和测试,给项目的开发工作带来极大便利。主流的构建工具有 makeMSBuildNinja 等。

要使用这些构建工具,必须在项目中添加一个配置文件。不同的构建工具需要不同格式的配置文件,比如 make 的配置文件通常是 Makefile,Ninja 的配置文件通常是 build.ninja

由于不同平台上的主流构建工具不同,开发跨平台项目时需要维护多个配置文件。为了解决跨平台项目构建难的问题,CMake 应运而生。

CMake 是跨平台的构建系统生成器,它可以在不同的平台上为项目生成构建系统,并用构建工具完成项目的构建。有了 CMake,开发者只需了解 CMake 的用法,而无需了解每个构建工具的细节,就可以利用构建工具完成项目的构建。

快速开始

准备一个文件夹,作为项目根目录。

1
2
$ mkdir cmake_demo
$ cd cmake_demo

添加一个源文件 main.cpp 作为主模块:

1
2
3
4
5
6
7
#include <iostream>

int main()
{
std::cout << "Hello, World!\n";
return 0;
}

添加 CMake 的配置文件,文件名必须是 CMakeLists.txt

1
2
3
4
5
6
# 版本要求
cmake_minimum_required(VERSION 3.21.0)
# 项目信息
project(myproj VERSION 0.1.0 LANGUAGES C CXX)
# 构建信息
add_executable(myapp main.cpp)

每一行位于 # 之后的内容视为注释。

第一行的 cmake_minimum_required() 命令指示项目对 CMake 的最低版本要求。

第二行的 project() 命令指示项目的名称、版本号、使用的编程语言,C++ 用 CXX 表示。

第三行的 add_executable() 命令指示项目要生成的可执行文件和它的源文件。

项目要生成的可执行文件或库统称为 目标add_executable() 命令是为数不多的能够添加目标的命令之一。它用于添加产物为可执行文件的目标。在上面的例子中,第三行的 add_executable() 命令添加了一个名为 myapp 的目标。目标的名称也是最终生成的可执行文件的文件名或库的库名。构建目标的源文件在名称之后列出。

生成配置

准备一个子文件夹作为 构建目录,习惯上命名为 build。用 cmake 命令在该目录中生成构建工具的配置。

1
2
3
4
5
6
7
8
$ mkdir build
$ cd build
$ cmake ..
-- Building for: Visual Studio 16 2019
...
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/cmake_demo/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
2
3
4
5
6
7
8
9
10
$ cmake -h
Generators

The following generators are available on this platform (* marks default):
Visual Studio 17 2022 = Generates Visual Studio 2022 project files.
Use -A option to specify architecture.
* Visual Studio 16 2019 = Generates Visual Studio 2019 project files.
Use -A option to specify architecture.
MinGW Makefiles = Generates a make file for use with
mingw32-make.

构建项目

在构建目录生成构建工具的配置后,紧接着就可以调用构建工具开始构建项目了。要调用构建工具,用 --build 选项。选项之后要跟上构建目录的路径。在本示例中,如果是在构建目录中执行该命令,就是当前目录 .

1
$ cmake --build .

执行该命令时,带上 -v 选项,此时 CMake 会显示实际执行的每个编译和链接命令。

在 Linux 上使用 make 或者在 Windows 上使用 MinGW-w64 时,生成的可执行文件位于构建目录中;在 Windows 上使用 Visual Studio 时,生成的可执行文件位于构建目录的子文件夹 Debug 中。

1
2
$ ./Debug/myapp
Hello, World!

在构建大型项目时,为了加快编译速度,常常向构建工具传递 -j4-m:4 选项来启用 多线程编译

1
$ cmake --build . -- -m:4

在实际开发中,常常借助脚本使构建流程自动化。在项目根目录添加一个脚本,习惯上命名为 build.sh

1
2
3
4
5
6
7
8
#!/bin/bash
set -e

mkdir -p build
cd build

cmake ..
cmake --build . --target myapp --config Release

--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
2
3
4
5
6
#include "math.hpp"

int add(int a, int b)
{
return a + b;
}

main.cpp

1
2
3
4
5
6
7
8
#include <iostream>
#include "math.hpp"

int main()
{
std::cout << "1 + 1 = " << add(1, 1);
return 0;
}

在配置文件 CMakeLists.txt 中用 add_executable() 命令添加构建目标 myapp,在目标名之后列出各个模块的头文件和源文件,在本示例中为 main.cpp math.hpp math.cpp

CMakeLists.txt

1
2
3
4
cmake_minimum_required(VERSION 3.21.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)

add_executable(myapp main.cpp math.hpp math.cpp)

add_executable() 命令中列出头文件不是必须的,但这样做可以提高 IDE 的用户体验。

运行脚本 build.sh 完成项目的构建并调用生成的可执行文件 myapp

1
2
3
$ ./build.sh
$ ./build/Release/myapp
1 + 1 = 2

生成静态库

可以单独编译算术模块,生成一个静态库,再让主模块编译而成的可执行文件链接到该静态库。要添加产物为库的构建目标,用 add_library() 命令。它的参数和 add_executable() 命令类似。不同的是,它需要用于指示静态库或动态库的关键字 STATICSHARED 作为第二个参数。

1
add_library(math STATIC math.hpp math.cpp)

添加源文件

目标的源文件也可以通过 target_sources() 命令添加:

1
2
3
4
add_library(math STATIC math.hpp math.cpp)
# 相当于
add_library(math STATIC)
target_sources(math PRIVATE math.hpp math.cpp)

PRIVATE 关键字的含义见高级篇。

添加附加依赖项

要让可执行文件链接到静态库,用 target_link_libraries() 命令:

1
2
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE math)

target_link_libraries() 命令也指明了两个目标之间的 依赖关系。在构建项目时,无需列出每个需要构建的目标,CMake 会根据目标之间的依赖关系自动找出每个需要构建的目标。

最终 CMakeLists.txt 的内容如下:

CMakeLists.txt

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.21.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)

add_library(math STATIC math.hpp math.cpp)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE math)

运行脚本 build.sh 即可得到由算术模块编译而成的静态库:

1
2
3
4
5
6
7
$ ./build.sh

$ ls ./build/Release
math.lib myapp.exe*

$ ./build/Release/myapp
1 + 1 = 2

生成动态库

要将算术模块编译成一个动态库,需要修改算术模块的头文件:在编译算术模块时,函数声明要用 __declspec(dllexport) 修饰;在编译主模块时,函数声明要用 __declspec(dllimport) 修饰。习惯上,函数声明的变化常常借助预编译指令 #ifdef...#else...#endif 实现。

math.hpp

1
2
3
4
5
6
7
#ifdef MATH_EXPORT
#define DECL_API __declspec(dllexport)
#else
#define DECL_API __declspec(dllimport)
#endif

DECL_API int add(int a, int b);

要生成动态库,add_library() 命令的第二个参数要修改成 SHARED

1
add_library(math SHARED math.hpp math.cpp)

添加宏定义

要在编译算术模块时定义 MATH_EXPORT 宏,可用 target_compile_definitions() 命令:

1
2
target_compile_definitions(math PRIVATE MATH_EXPORT)
# 相当于 `#define MATH_EXPORT`

最终 CMakeLists.txt 的内容如下:

CMakeLists.txt

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.21.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)

add_library(math SHARED math.hpp math.cpp)
target_compile_definitions(math PRIVATE MATH_EXPORT)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE math)

运行脚本 build.sh 即可得到由算术模块编译而成的动态库 math.dll 及其导入库 math.lib

1
2
3
4
5
6
7
$ ./build.sh

$ ls ./build/Release
math.dll* math.exp math.lib myapp.exe*

$ ./build/Release/myapp
1 + 1 = 2

生成显式加载的动态库

显式加载动态库是指可执行文件在运行时根据需要选择性从硬盘加载动态库到内存中。可执行文件无需链接到显式加载的动态库,在它的源文件中也无需包含动态库的头文件,但需要调用一些函数来加载动态库(在 Windows 上要包含头文件 windows.h 并使用 LoadLibrary()GetProcAddress();在 Linux 上要包含头文件 dlfcn.h 并使用 dlopen()dlsym())。

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <windows.h>

int main()
{
auto hDll = LoadLibrary("math.dll");
if (!hDll) {
std::cerr << "LoadLibrary() failed\n";
return -1;
}

auto add = reinterpret_cast<int (*)(int, int)>(GetProcAddress(hDll, "add"));
if (!add) {
std::cerr << "GetProcAddress() failed\n";
return -1;
}

std::cout << "1 + 1 = " << add(1, 1);
return 0;
}

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
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.21.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)

add_library(math MODULE math.hpp math.cpp)
target_compile_definitions(math PRIVATE MATH_EXPORT)

add_executable(myapp main.cpp)

此时如果直接执行脚本 build.sh,就会发现 math 没有被构建。这是因为脚本 build.sh 中的构建命令用 --target 选项指定的构建目标只有 myapp 一个。在前面的例子中,math 也会被构建是因为它和 myapp 之间存在依赖关系。现在删除了 target_link_libraries() 命令,myappmath 之间的依赖关系也就不复存在。为了构建 math,要修改构建命令,在 --target 选项之后追加 math

build.sh

1
2
3
4
5
6
7
8
#!/bin/bash
set -e

mkdir -p build
cd build

cmake ..
cmake --build . --target myapp math --config Release

如果不使用 --target 选项,所有目标都会被构建,除了那些使用关键字 EXCLUDE_FROM_ALL 作为命令的参数之一添加的目标。

基础篇

语法基础

布尔变量

add_library() 命令的第二个参数可以省略,它的取值取决于 布尔变量 BUILD_SHARED_LIBS:当 BUILD_SHARED_LIBS 的值为 OFF 时,取值为 STATIC;当 BUILD_SHARED_LIBSON 时,取值为 SHARED。该变量的默认值为 OFF。变量的值可用 set() 命令设置:

1
2
3
4
5
6
set(BUILD_SHARED_LIBS ON)

# 此时
add_library(math math.hpp math.cpp)
# 相当于
add_library(math SHARED math.hpp math.cpp)

将变量名要放在 ${} 中间,即可得到变量的值。例如,要打印变量的值,可借助 message() 命令:

1
message(STATUS "BUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}")

message() 命令的第一个参数用于指示信息的类型,它会影响 CMake 的行为;第二个参数是作为信息内容的字符串。常用的信息类型有下列三种:

  1. NOTICE 表示需要留意的信息(默认值)
  2. STATUS 表示一般的信息,是信息数量最多的类型,输出时带有 -- 前缀
  3. FATAL_ERROR 表示发生致命错误的信息,会导致 CMake 终止配置的生成

三种类型的信息的输出格式如下所示:

1
2
3
4
5
6
7
8
9
10
11
message("This is the NOTICE message")
This is the NOTICE message

message(STATUS "This is the STATUS message")
-- This is the STATUS message

message(FATAL_ERROR "This is the FATAL_ERROR message")
CMake Error at CMakeLists.txt:5 (message):
This is the FATAL_ERROR message

-- Configuring incomplete, errors occurred!

列表变量

变量的值可以是一个列表,比如源文件的列表,这种变量称为 列表变量。列表变量可用 list() 命令设置:

1
2
3
4
list(APPEND sources main.cpp math.hpp math.cpp)
add_executable(myapp ${sources})
# 相当于
add_executable(myapp main.cpp math.hpp math.cpp)

APPEND 关键字表示向列表追加元素。

字符串变量

布尔变量和列表变量本质上都是字符串变量。所有变量的值都以字符串的形式存储。列表变量的值是用分号 ; 作为分隔符的字符串。

1
2
3
4
list(APPEND sources main.cpp math.hpp math.cpp)
# 相当于
set(sources "main.cpp;math.hpp;math.cpp")
message("sources=${sources}")
1
2
$ cmake -S . -B build
sources=main.cpp;math.hpp;math.cpp

用字符串作为命令的参数时,字符串两边的引号可以省略,只要字符串不含分号,否则字符串就会被展开成多个参数(可能造成语法错误)。

1
2
3
add_executable(myapp main.cpp;main.hpp)
# 相当于
add_executable(myapp main.cpp main.hpp)

环境变量

访问名为 TEMP 的环境变量的语法是 $ENV{TEMP}。可用 if 语句检查一个环境变量是否有定义:

1
2
3
if(DEFINED ENV{TEMP}) # 注意,此处无美元符号 `$`
message("TEMP=$ENV{TEMP}")
endif()

环境变量 TEMP 的值是临时文件夹的路径。

1
2
$ cmake -S . -B build
TEMP=C:\Users\<username>\AppData\Local\Temp

预定义变量

有很多预定义的变量可以直接使用。例如,开发环境的信息(如编译器的版本和路径)记录在一组预定义变量中。执行带有 --system-information 选项的 cmake 命令,就可以看到这些变量的值。还可将它们写入一个文件,只需在选项之后跟上文件名。

1
2
3
4
5
6
7
8
9
10
$ cmake -G "MinGW Makefiles" --system-information sysinfo.txt
$ cat sysinfo.txt
...
CMAKE_CXX_COMPILER "/path/to/g++.exe" # 编译器路径
CMAKE_CXX_COMPILER_ID "GNU" # 编译器标识
CMAKE_CXX_COMPILER_LOADED "1" # 编译器是否启用
CMAKE_CXX_COMPILER_VERSION "8.1.0" # 编译器版本号

WIN32 "1"
...

条件语句和运算符

将布尔变量和条件语句结合,可实现选择性构建。例如,仅当 BUILD_LIBSON 时,才生成 math 静态库。

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.21.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)

if(BUILD_LIBS)
add_library(math STATIC math.hpp math.cpp)
add_executable(myapp main.cpp)
target_link_libraries(myapp math)
else()
add_executable(myapp main.cpp math.hpp math.cpp)
endif()

其他变量可以和 NOTAND 等运算符结合构成一个布尔表达式:

  • NOT BUILD_LIBS 取反
  • A AND B
  • A OR B
  • A STREQUAL B 两个字符串 AB相等
  • EXIST PATH 路径为 PATH 的文件或目录存在
  • DEFINED VAR 变量 VAR 有定义

命令行设置变量

在执行 cmake 命令生成配置时,可用 -D 选项设置变量。

build.sh

1
2
3
4
5
6
7
8
#!/bin/bash
set -e

mkdir -p build
cd build

cmake .. -D BUILD_LIBS=ON # 将变量 `BUILD_LIBS` 的值设置为 `ON`
cmake --build . --target myapp --config Release

布尔变量的默认值

可以用 option() 命令给布尔变量设置默认值。参数依次是变量的变量名、注释和默认值。

1
option(BUILD_LIBS "Compile sources into a library" OFF)

目标的属性

目标的属性包含有关目标的一切信息。例如,目标的 SOURCES 属性是源文件的列表;COMPILE_DEFINITIONS 属性是宏定义的列表。

获取属性

属性的值可用 get_target_property() 命令获取。它能够将变量的值设置为属性的值。

1
2
3
4
5
6
7
add_library(math SHARED math.hpp math.cpp)
target_compile_definitions(math PRIVATE MATH_EXPORT=TRUE)

get_target_property(sources math SOURCES)
get_target_property(compile_definitions math COMPILE_DEFINITIONS)
message("sources=${sources}")
message("compile_definitions=${compile_definitions}")
1
2
3
$ cmake -S . -B build
sources=math.hpp;math.cpp
compile_definitions=MATH_EXPORT=TRUE

设置属性

目标的属性可以通过 set_target_properties() 命令设置:

1
2
3
set_target_properties(math PROPERTIES SOURCES "math.hpp;math.cpp")
# 相当于
target_sources(math PRIVATE math.hpp math.cpp)

模块

有些命令(也就是函数)由模块提供。在使用模块提供的函数之前,要用 include() 命令导入模块。例如,导入 CMake 内置的 CMakePrintHelpers 模块。

1
include(CMakePrintHelpers)

打印目标的属性

内置模块 CMakePrintHelpers 提供的cmake_print_properties() 函数用于打印目标的属性。例如,打印目标 mathTYPECOMPILE_DEFINITIONS 两个属性:

1
2
3
4
5
add_library(math SHARED math.hpp math.cpp)
target_compile_definitions(math PRIVATE MATH_EXPORT=TRUE)

include(CMakePrintHelpers)
cmake_print_properties(TARGETS math PROPERTIES "TYPE;COMPILE_DEFINITIONS")
1
2
3
4
5
6
$ cmake -S . -B build
-- Selecting Windows SDK version 10.0.19041.0 to target Windows 10.0.19044.
--
Properties for TARGET math:
math.TYPE = "SHARED_LIBRARY"
math.COMPILE_DEFINITIONS = "MATH_EXPORT=TRUE"

TYPESHARED_LIBRARY 说明构建该目标将得到一个动态库。

打印变量

CMakePrintHelpers 模块提供的另一个函数 cmake_print_variables() 用于打印变量。例如,要打印 BUILD_SHARED_LIBSsources 两个变量:

1
2
3
set(BUILD_SHARED_LIBS ON)
list(APPEND sources main.cpp math.hpp math.cpp)
cmake_print_variables(BUILD_SHARED_LIBS sources)
1
2
$ cmake -S . -B build
-- BUILD_SHARED_LIBS="ON" ; sources="main.cpp;math.hpp;math.cpp"

构建配置

构建配置会影响目标的编译和链接选项。常用的构建配置有 DebugRelease 两种,默认值为 Debug

构建工具可分为单配置构建工具和多配置构建工具。常用的单配置构建工具有 make、MinGW-w64 和 Ninja。常用的多配置构建工具有 Visual Studio 和 Xcode。使用多配置构建工具构建项目时,可以在不同的构建配置之间快速切换。

单配置构建工具

使用单配置构建工具时,构建配置在生成配置时就已经确定,它由变量 CMAKE_BUILD_TYPE 指示。不同构建配置的编译选项记录在一组预定义变量中:

1
2
3
CMAKE_CXX_FLAGS ""
CMAKE_CXX_FLAGS_DEBUG "-g"
CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG"

CMAKE_CXX_FLAGS 变量包含通用编译选项。

当构建配置为 Release 时,NDEBUG 宏会被定义。

main.cpp

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main()
{
#ifdef NDEBUG
std::cout << "Release";
#else
std::cout << "Debug";
#endif
return 0;
}

CMakeLists.txt

1
2
3
cmake_minimum_required(VERSION 3.20.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)
add_executable(myapp main.cpp)

在生成配置时,用 -G 选项选择单配置构建工具,用 -D 选项设置变量 CMAKE_BUILD_TYPE 的值,然后运行程序,观察程序的输出:

1
2
3
4
5
6
7
$ cd build
$ cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
$ cmake --build . && ./main.exe
Release
$ cmake .. -G "MinGW Makefiles" ..
$ cmake --build . && ./main.exe
Debug

多配置构建工具

使用多配置构建工具时,可用的构建配置在生成配置时列出,实际使用的构建配置在构建项目时指定。具体来说,可用的构建配置是由列表变量 CMAKE_CONFIGURATION_TYPES 决定的。例如,只需 DebugRelease 两种构建配置。

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
2
3
4
5
6
7
#include <iostream>

int main()
{
std::cout << "Hello, World!\n";
return 0;
}

子源目录也需要一个 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
2
3
4
5
$ cd build
$ cmake .. -G "MinGW Makefiles"
$ cmake --build .
$ ls hello_build/
cmake_install.cmake CMakeFiles/ hello.exe* Makefile

可执行文件 hello.exe 可在构建目录 build 的子文件夹 hello_build 中找到。

源目录的路径

子源目录中可能又有子源目录。子源目录还有可能是 子项目 的根目录。因此,源目录的路径并不简单。不同源目录的绝对路径记录在一组预定义变量中:

  • CMAKE_CURRENT_SOURCE_DIR 当前源目录的绝对路径
  • CMAKE_SOURCE_DIR 根源目录的绝对路径
  • PROJECT_SOURCE_DIR 项目源目录的绝对路径

在每个 CMakeLists.txt 文件中都可以使用上述三个变量。比较容易混淆的是后两个变量。简单来说,项目源目录既有可能是根源目录,也有可能是介于根源目录和当前源目录中间的某一级子源目录。某一级子源目录是不是项目源目录,取决于该源目录中的 CMakeLists.txt 文件是否使用了 project() 命令。由此可见,项目源目录也就是项目根目录。

下面是一个具有四级源目录的示例:

1
2
3
4
5
6
7
8
9
$ tree cmake_demo
cmake_demo
|-- CMakeLists.txt # 使用了 `project()` 命令
`-- a
|-- CMakeLists.txt
`-- b
|-- CMakeLists.txt # 使用了 `project()` 命令
`-- c
`-- CMakeLists.txt

每一级源目录中的 CMakeLists.txt 文件都使用 cmake_print_variables() 函数打印上述三个变量的值。不同的是,只有 cmake_democmake_demo/a/b 两级源目录是项目源目录。

cmake_demo/CMakeLists.txt

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.21)
project(myproj VERSION 0.1.0 LANGUAGES C CXX)

include(CMakePrintHelpers)
cmake_print_variables(CMAKE_CURRENT_SOURCE_DIR CMAKE_SOURCE_DIR PROJECT_SOURCE_DIR)
add_subdirectory(a)

cmake_demo/a/CMakeLists.txt

1
2
3
include(CMakePrintHelpers)
cmake_print_variables(CMAKE_CURRENT_SOURCE_DIR CMAKE_SOURCE_DIR PROJECT_SOURCE_DIR)
add_subdirectory(b)

cmake_demo/a/b/CMakeLists.txt

1
2
3
4
5
project(mysubproj VERSION 0.1.0 LANGUAGES C CXX)

include(CMakePrintHelpers)
cmake_print_variables(CMAKE_CURRENT_SOURCE_DIR CMAKE_SOURCE_DIR PROJECT_SOURCE_DIR)
add_subdirectory(c)

cmake_demo/a/b/c/CMakeLists.txt

1
2
include(CMakePrintHelpers)
cmake_print_variables(CMAKE_CURRENT_SOURCE_DIR CMAKE_SOURCE_DIR PROJECT_SOURCE_DIR)

执行 cmake 命令生成配置:

1
2
3
4
5
$ cmake -S . -B build
-- CMAKE_CURRENT_SOURCE_DIR="/path/to/cmake_demo" ; CMAKE_SOURCE_DIR="/path/to/cmake_demo" ; PROJECT_SOURCE_DIR="/path/to/cmake_demo"
-- CMAKE_CURRENT_SOURCE_DIR="/path/to/cmake_demo/a" ; CMAKE_SOURCE_DIR="/path/to/cmake_demo" ; PROJECT_SOURCE_DIR="/path/to/cmake_demo"
-- CMAKE_CURRENT_SOURCE_DIR="/path/to/cmake_demo/a/b" ; CMAKE_SOURCE_DIR="/path/to/cmake_demo" ; PROJECT_SOURCE_DIR="/path/to/cmake_demo/a/b"
-- CMAKE_CURRENT_SOURCE_DIR="/path/to/cmake_demo/a/b/c" ; CMAKE_SOURCE_DIR="/path/to/cmake_demo" ; PROJECT_SOURCE_DIR="/path/to/cmake_demo/a/b"

输出目录

输出目录是可执行文件和库的保存位置。默认输出目录是构建目录。使用多配置构建工具时,实际的输出目录还受构建配置的影响。比如,当构建配置为 Release 时,多配置构建工具会在输出目录中创建子文件夹 Release

预定义变量 PROJECT_BINARY_DIR 的值是构建目录的绝对路径。

1
cmake_print_variables(PROJECT_SOURCE_DIR PROJECT_BINARY_DIR)
1
2
$ cmake -S . -B build
-- PROJECT_SOURCE_DIR="/path/to/cmake_demo" ; PROJECT_BINARY_DIR="/path/to/cmake_demo/build"

有两种方式可以改变输出路径。后者是新版本的 CMake 推荐的方式。

通过变量设置

可以通过设置 EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH 两个变量改变可执行文件和库的输出路径。比如,将所有的可执行文件保存到项目根目录的子文件夹 bin 中;将所有的库保存到项目根目录的子文件夹 lib 中。

1
2
set(EXECUTABLE_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/bin")
set(LIBRARY_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/lib")

该设置会影响项目的所有可执行文件和库,包括子源目录中的目标。

通过属性设置

对于可执行文件和库的输出路径,可以通过设置目标的下列三个属性实现更细粒度的控制:

  • RUNTIME_OUTPUT_DIRECTORY 属性是用 add_executable() 命令创建的可执行文件(.exe)的输出路径,在 Windows 平台上还包括用带有 SHARED 选项的 add_library() 命令创建的动态库(.dll);
  • ARCHIVE_OUTPUT_DIRECTORY 属性是用带有 STATIC 选项的 add_executable() 命令创建的静态库(.lib.a)的输出路径,在 Windows 平台上还包括用带有 SHAREDMODULE 选项的 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
2
3
4
5
6
7
#include <iostream>

int main()
{
std::cout << "Hello, World!\n";
return 0;
}

math.cpp

1
2
3
4
5
6
#include "math.hpp"

int add(int a, int b)
{
return a + b;
}

math.cpp

1
2
3
4
5
6
7
8
9
#if defined BUILD_STATIC
#define DECL_API
#elif defined BUILD_SHARED
#define DECL_API __declspec(dllexport)
#else
#define DECL_API extern "C" __declspec(dllexport)
#endif

DECL_API int add(int a, int b);

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmake_minimum_required(VERSION 3.21.0)
project(myproj VERSION 0.1.0 LANGUAGES CXX)


set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/exe")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/output/dll")


# executable
add_executable(app app.cpp)

# static
add_library(math_static STATIC math.cpp)
target_compile_definitions(math_static PRIVATE BUILD_STATIC)

# shared
add_library(math_shared SHARED math.cpp)
target_compile_definitions(math_shared PRIVATE BUILD_SHARED)

# module
add_library(math_module MODULE math.cpp)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cmake -S . -B build
$ cmake --build build --config Release
$ tree output
output
|-- exe
| `-- Release
| |-- app.exe # 可执行文件 .exe
| `-- math_shared.dll # 动态库 .dll
|-- dll
| `-- Release
| `-- math_module.dll # 显式加载的动态库 .dll
`-- lib
`-- Release
|-- math_module.lib # 显式加载的动态库的导入库 .lib
|-- math_shared.lib # 动态库的导入库 .lib
`-- math_static.lib # 静态库 .lib

附加包含目录

包含目录是构建工具在其中检索头文件(.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
2
3
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "/path/to/*.dll" "${PROJECT_BINARY_DIR}"
)

参数依次是针对的目标、命令执行的时机和命令的列表。命令执行的时机有三种,由三个不同的关键字表示:

  • 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 FilesSource Files 两个分组(筛选器),用来分开目标的头文件和源文件。

IDE

Source Files 分组是源文件的默认分组。要设置更多分组,可以使用 source_group() 命令。

1
2
3
4
5
6
list(APPEND headers main.h util.h)
list(APPEND sources main.cpp util.cpp)

source_group("Header Files" FILES ${headers})

add_executable(myapp ${sources} ${headers})

在上面的例子中,列表 headers 中的源文件都被添加到分组 Header Files 中。为源文件设置好分组后,IDE 就会分组列出目标的源文件。源文件的分组也可以在源文件被添加到目标之后进行,效果相同。

进阶篇

语法进阶

缓存变量

在构建目录中可以找到文件 CMakeCache.txt,它是 CMake 在生成配置时产生的,作用是记录一些变量的值。

缓存的主要作用是记住用户的选择,这样下次使用生成配置时,用户就不必再提供这些信息。例如,用 option() 命令创建的布尔变量就会被保存缓存起来。

1
2
option(USE_JPEG "Do you want to use the jpeg library" OFF)
message("USE_JPEG=${USE_JPEG}")

无论何时,缓存变量的值都可以用命令行选项 -D 重新设置。简单来说,缓存变量的值,要么是在命令行中用 -D 选项指定的值,要么是缓存的值。

1
2
3
4
$ cmake -D USE_JPEG=ON ..
USE_JPEG=ON
$ cmake ..
USE_JPEG=ON # 缓存的值

option() 命令只能用于设置并缓存布尔变量。要缓存其他类型的变量,要用带有 CACHE 选项的 set() 命令,它的行为和 option() 命令一致。

1
2
3
set(USE_JPEG OFF CACHE BOOL "include jpeg support?")
# 相当于
option(USE_JPEG "include jpeg support?" OFF)

参数依次是变量名、默认值、关键字 CACHE、变量的类型和注释。最后两个参数的作用是提高 GUI 的用户体验,常用的类型有下列四种:

  • BOOL GUI 将显示一个复选框
  • FILEPATH GUI 将显示一个「打开文件」对话框
  • PATH GUI 将显示一个「选择文件夹」对话框
  • STRING GUI 将显示一个文本框或下拉列表

要忽略并覆盖缓存的值,加上 FORCE 选项:

1
set(USE_JPEG OFF CACHE BOOL "include jpeg support?" FORCE)
1
2
3
4
$ cmake -D USE_JPEG=ON ..
USE_JPEG=ON
$ cmake ..
USE_JPEG=OFF

通过设置缓存变量的 STRINGS 属性,可以创建特殊的缓存变量。这种缓存变量的取值只能是给定选项之一。

1
2
set(COLOR "Red" CACHE STRING "Select a color")
set_property(CACHE COLOR PROPERTY STRINGS "Red" "Green" "Blue")

STRINGS 属性的值就是选项的列表。对于这种变量,GUI 将显示一个下拉列表。

自定义命令

可以自定义两种命令:函数和带参数的宏。

函数用 function() 命令定义,第一个参数是函数名,之后都是形参。

1
2
3
4
5
6
7
# 定义函数
function(say_hello name)
message("Hello ${name}")
endfunction()

# 调用函数
say_hello(John)
1
2
$ cmake ..
Hello John

带参数的宏用 macro() 命令定义。

1
2
3
4
5
6
7
# 定义带参数的宏
macro(say_hello name)
message("Hello ${name}")
endmacro()

# 调用带参数的宏
say_hello(John)
1
2
$ cmake ..
Hello John

函数和带参数的宏都可以使用下列预定义变量:

  • ARGC 参数的个数
  • ARGV 所有参数的列表
  • ARGN 除形参之外的所有参数的列表
  • ARGV0 第一个参数
  • ARGV1 第二个参数
  • ARGV2 第三个参数
1
2
3
4
5
6
7
8
9
10
function(fn a b c)
message("ARGC: ${ARGC}")
message("ARGV: ${ARGV}")
message("ARGN: ${ARGN}")
message("ARGV0: ${ARGV0}")
message("ARGV1: ${ARGV1}")
message("ARGV2: ${ARGV2}")
endfunction()

fn(a b c d e)
1
2
3
4
5
6
7
$ cmake ..
ARGC: 5
ARGV: a;b;c;d;e
ARGN: d;e
ARGV0: a
ARGV1: b
ARGV2: c

变量的作用域

每处理一个子源目录,或调用一个函数,就会形成一个子作用域。在父作用域中设置的变量会被复制到子作用域中。因此,在子作用域中可以使用在父作用域中设置的变量,但反过来不行。注意,子作用域中的 set() 命令默认修改的不是父作用域中的变量,而是它们在子作用域中的副本。

1
2
3
4
5
6
7
8
9
function(foo)
message(${test})
set(test 2)
message(${test}) # 子作用域中,`test` 修改为 2
endfunction()

set(test 1) # 父作用域中,`test` 为 1
foo()
message(${test}) # 父作用域中,`test` 仍然为 1
1
2
3
4
$ cmake ..
1
2
1

带有 PARENT_SCOPE 选项的 set() 命令修改的是父作用域中的变量,而不是它们在子作用域中的副本。

1
2
3
4
...
set(test 2 PARENT_SCOPE) # 父作用域中,`test` 修改为 2
message(${test}) # 子作用域中,`test` 仍然为 1
...
1
2
3
4
$ cmake ..
1
1
2

整合第三方库

有多种方式可以将第三方库整合到项目中,它们适用于不同的场景。

从网上获取源码的库

对于需要从网上获取源码并在本地构建的库,可借助内置模块 FetchContent 将其整合到项目中。

1
include(FetchContent)

使用内置模块 FetchContent 整合第三方库需要两个步骤:

  1. 先用 FetchContent_Declare() 命令声明库
  2. 再用 FetchContent_MakeAvailable() 命令启用声明的库

下面是整合日志库 glog 的示例。

1
2
3
4
5
6
7
8
9
FetchContent_Declare(glog
GIT_REPOSITORY git@github.com:google/glog.git
GIT_TAG v0.6.0
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/glog"
)
FetchContent_MakeAvailable(glog)

add_executable(myapp main.cpp)
target_link_libraries(myapp glog::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
2
3
4
5
6
7
8
9
ExternalProject_Add(gflags-2.2.2
PREFIX ${PROJECT_BINARY_DIR}/third_party/gflags-2.2.2

URL https://github.com/gflags/gflags/archive/v2.2.2.zip
DOWNLOAD_NAME gflags-2.2.2.zip

CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${PROJECT_BINARY_DIR}/third_party/gflags
-DBUILD_STATIC_LIBS=ON
)

PREFIX 选项指示第三方库相关文件的保存位置。和目录有关的选项有很多个,它们都有默认值:

1
2
3
4
5
6
7
TMP_DIR      = <prefix>/tmp              # 临时文件保存位置
STAMP_DIR = <prefix>/src/<name>-stamp # 时间戳文件保存位置
DOWNLOAD_DIR = <prefix>/src # 下载位置
SOURCE_DIR = <prefix>/src/<name> # 源文件保存位置
BINARY_DIR = <prefix>/src/<name>-build # 二进制文件保存位置
INSTALL_DIR = <prefix> # 安装位置
LOG_DIR = <STAMP_DIR> # 日志文件保存位置

DOWNLOAD_DIR 和第三方库的获取方式有关。常用的获取方式有 URL 下载和 Git 克隆两种。仅当采用 URL 下载时,才需要该选项。

若采用 URL 下载方式,则需指定 URL 选项。下载位置由上述 DOWNLOAD_DIR 选项指示。如果 URL 的末尾不适合作为文件名,可用 DOWNLOAD_NAME 指示新的文件名。

1
2
URL           https://github.com/gflags/gflags/archive/v2.2.2.zip
DOWNLOAD_NAME gflags-2.2.2.zip

若采用 Git 克隆,则需要指定 GIT_REPOSITORY 选项。如果需要代码库的特定版本,可以用 GIT_TAG 选项指示一个标签。

1
2
GIT_REPOSITORY https://github.com/gflags/gflags.git
GIT_TAG v2.2.2

生成配置的命令所需的选项可用 CMAKE_ARGS 选项指示,通常要包含 CMAKE_INSTALL_PREFIXBUILD_STATIC_LIBS 两个选项,前者指示第三方库的安装目录,后者指示是否构建静态库。

1
2
CMAKE_ARGS    -DBUILD_STATIC_LIBS=ON
-DCMAKE_INSTALL_PREFIX=${PROJECT_BINARY_DIR}/third_party/gflags

如有需要,可用下列选项自定义库的配置、构建和安装命令。

1
2
3
CONFIGURE_COMMAND
BUILD_COMMAND
INSTALL_COMMAND

通过包管理器安装的库

有些库可以用包管理器安装。比如在 Ubuntu 上可以用 apt 命令安装 OpenCV:

1
$ sudo apt install -y libopencv-dev

这种安装方式方便快捷。安装好后,可用 pkg-config 命令查看头文件和库的位置以及要链接的库:

1
2
pkg-config --cflags --libs openh264
-I/usr/local/include -L/usr/local/lib -lopenh264

实际上,通过包管理器安装的库都会提供一个 .pc 文件,里面记录了库的头文件和链接库的位置。

1
2
3
4
5
6
7
$ cat /usr/lib/x86_64-linux-gnu/pkgconfig/opencv4.pc
# Package Information for pkg-config

prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib/x86_64-linux-gnu
includedir=${prefix}/include/opencv4

--list-all 选项执行 pkg-config 命令,可查看本机安装的所有库:

1
2
$ pkg-config --list-all | grep opencv
opencv4 OpenCV - Open Source Computer Vision Library

在 CMake 中,要整合通过包管理器安装的库,要借助内部模块 FindPkgConfig

1
include(FindPkgConfig)

该模块被引入后会设置若干变量,如指示 pkg-config 命令是否找到 PKG_CONFIG_FOUND 变量和指示 pkg-config 命令可执行文件的路径的 PKG_CONFIG_EXECUTABLE 变量。

1
2
3
include(FindPkgConfig)
message("PKG_CONFIG_FOUND=${PKG_CONFIG_FOUND}")
message("PKG_CONFIG_EXECUTABLE=${PKG_CONFIG_EXECUTABLE}")
1
2
3
$ cmake ..
PKG_CONFIG_FOUND=TRUE
PKG_CONFIG_EXECUTABLE=/usr/bin/pkg-config

要得到库的信息,用 pkg_check_modules() 命令。

1
pkg_check_modules(<prefix> <moduleSpec>)

该命令会根据库名设置一系列变量。第一个参数指示变量名的前缀。最后一个参数指示库名。

1
2
3
4
5
pkg_check_modules(OpenCV opencv4)
message("OpenCV_FOUND=${OpenCV_FOUND}")
message("OpenCV_INCLUDE_DIRS=${OpenCV_INCLUDE_DIRS}")
message("OpenCV_LIBRARY_DIRS=${OpenCV_LIBRARY_DIRS}")
message("OpenCV_LIBRARIES=${OpenCV_LIBRARIES}")
1
2
3
4
5
$ cmake ..
OpenCV_FOUND=1
OpenCV_INCLUDE_DIRS=/usr/include/opencv4
OpenCV_LIBRARY_DIRS=/usr/lib/x86_64-linux-gnu
OpenCV_LIBRARIES=opencv_stitching;opencv_alphamat;...

若添加了 IMPORTED_TARGET 选项,则在 target_link_libraries() 命令中可以使用参数 PkgConfig::OpenCV

1
2
3
4
pkg_check_modules(OpenCV REQUIRED IMPORTED_TARGET opencv4)

add_executable(myapp main.cpp)
target_link_libraries(myapp PkgConfig::OpenCV)

预编译的软件包

预编译的软件包可用 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
2
3
4
5
find_package(OpenCV REQUIRED PATHS "${OPENCV_DIR}/build" NO_DEFAULT_PATH)

if(NOT OpenCV_FOUND)
message(FATAL_ERROR "OpenCV Not Found")
endif()

接下来,把依赖于 OpenCV 的构建目标链接到 OpenCV 即可。

1
2
add_executable(myapp main.cpp)
target_link_libraries(myapp ${OpenCV_LIBS})

预编译的库

对于预编译的库,可用带有 IMPORTED 选项的 add_library() 命令导入。

1
add_library(mkldnn SHARED IMPORTED)

库的路径由 IMPORTED_LOCATION 属性指示。对于静态库,IMPORTED_LOCATION 属性指示 .lib.a 文件的路径;对于动态库,IMPORTED_LOCATION 属性指示 .dll.so 文件的路径。在 Windows 平台上,对于动态库,还要用 IMPORTED_IMPLIB 属性指示动态库的导入库。

1
2
3
4
set_target_properties(mkldnn PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/path/to/include"
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/path/to/libmkldnn.so.0"
)

附加包含目录要用 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.libmsvcrtd.lib 都是静态库,不是动态库的导入库

在 Visual Studio 中,可以在项目的属性页中(C/C++ - 代码生成 - 运行库)选择不同的 CRT。

在低于 3.15 版本的 CMake 中,要让程序静态链接到 CRT 的 Release 版本,常用下面的宏将选项中的 /MD 统一替换成 /MT

1
2
3
4
5
6
7
8
9
macro(safe_set_static_flag)
foreach(flag_var
CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
if(${flag_var} MATCHES "/MD")
string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
endif(${flag_var} MATCHES "/MD")
endforeach(flag_var)
endmacro()

在新版本的 CMake 中,只需设置目标的 MSVC_RUNTIME_LIBRARY 属性即可:

1
2
3
4
5
6
7
8
9
10
11
# 静态链接到 CRT 的 Release 版本
set_property(TARGET myapp PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") # /MT

# 动态链接到 CRT 的 Release 版本
set_property(TARGET myapp PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") # /MD

# 静态链接到 CRT 的 Debug 版本
set_property(TARGET myapp PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") # /MTd

# 动态链接到 CRT 的 Debug 版本
set_property(TARGET myapp PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreadedDebugDLL") # /MDd

构建 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
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main(int argc, char const *argv[])
{
#ifdef _WIN64
std::cout << "_WIN64 defined\n";
#else
std::cout << "_WIN64 undefined\n";
#endif
return 0;
}

要在 Linux 上构建 32 位的程序,只需增加编译和链接选项 -m32

1
2
add_compile_options($<$<PLATFORM_ID:Linux>:-m32>)
add_link_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 既是构建条件,也是使用条件

在下面的例子中,第一条命令会设置目标 mylibINCLUDE_DIRECTORIES 属性;第二条命令会设置目标 mylibINTERFACE_INCLUDE_DIRECTORIES 属性;第三条命令是前两条命令的组合版本。

1
2
3
target_include_directories(mylib PRIVATE   ${PROJECT_SOURCE_DIR})
target_include_directories(mylib INTERFACE ${PROJECT_SOURCE_DIR})
target_include_directories(mylib PUBLIC ${PROJECT_SOURCE_DIR})

设置私有属性和接口属性的作用有很大不同。在下面的例子中,第一条命令表示构建目标 mylib 时,需要编译选项 -std=c++20;第二条命令表示链接目标 mylib 时,需要附加包含目录 ${CMAKE_CURRENT_SOURCE_DIR}

1
2
target_compile_features(mylib PRIVATE cxx_std_20)
target_include_directories(mylib INTERFACE ${PROJECT_SOURCE_DIR})

下列是常用的属性设置命令:

  • 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
2
target_compile_features(mylib PRIVATE cxx_std_20)
target_compile_options(mylib PRIVATE -std=c++20)

接口目标和导入目标

逻辑目标是在构建时不产生任何可执行文件、静态库或动态库的目标,分为接口目标和导入目标两种:接口目标用于声明一组使用条件,这是通过设置它的接口属性实现的;导入目标用来表示一个预编译的库,这是通过设置它的 IMPORTED_* 属性实现的。

导入目标的名称可以包含 ::。使用预编译的库常常需要满足一定的使用条件,因此,导入目标常常也是接口目标,它的接口属性指示预编译的库的使用条件。

在非 Windows 的平台上,动态库只有一个 .so 文件。若动态库具有 SONAME,则要设置 IMPORTED_SONAME 属性;若动态库没有 SONAME,而平台支持 SONAME,则要设置 IMPORTED_NO_SONAME 属性。

生成器表达式

在设置属性时,可以借助生成器表达式:

  • 布尔型
    • $<TARGET_EXISTS:tgt> 当目标 tgt 存在时为真
    • $<CONFIG:cfg> 当构建配置为 cfg 时为真
  • 字符串型
    • $<CONFIG> 构建配置,如 DebugRelease
    • $<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

安装

撰写中……

打包

撰写中……

参考文献