1.简介
CMake是一个开源的、跨平台的C++构建工具,通过平台和编译器无关的配置文件来声明构建目标,支持Make、ninja、MSBuild等多种底层构建工具,大多数IDE(例如CLion、Visual Studio、Visual Studio Code等)也都支持CMake。
- 网站:https://cmake.org/
- 官方文档:CMake Reference Documentation
- 官方教程:CMake Tutorial
2.安装
- 下载链接
- 安装指引
2.1 Windows
在Windows上安装CMake有以下几种方式:
(1)下载MSI安装文件(例如cmake-3.25.2-windows-x86_64.msi)并按照指引安装。
(2)下载压缩文件(例如cmake-3.25.2-windows-x86_64.zip),解压后将其中的bin目录添加到PATH环境变量。
(3)如果安装了Visual Studio,可以通过Visual Studio Installer安装“适用于Windows的C++ CMake工具”,如下图所示。
2.2 Linux
在Linux上安装CMake有以下几种方式:
(1)下载压缩文件(例如cmake-3.25.2-linux-x86_64.tar.gz),解压后将其中的bin目录添加到PATH环境变量。
(2)下载安装脚本(例如cmake-3.25.2-linux-x86_64.sh)并按照指引安装。
(3)从源代码构建:下载源代码(例如cmake-3.25.2.tar.gz),解压后依次执行以下命令:
./bootstrap
make
make install
2.3 macOS
下载镜像文件(例如cmake-3.25.2-macos-universal.dmg)或压缩文件(例如cmake-3.25.2-macos-universal.tar.gz),将CMake.app拷贝到/Applications目录下,运行后按照 “How to Install For Command Line Use” 菜单项中的指引安装命令行工具,如下图所示。
安装成功后应该能够在命令行中执行cmake命令:
$ cmake
Usage
cmake [options] <path-to-source>
cmake [options] <path-to-existing-build>
cmake [options] -S <path-to-source> -B <path-to-build>
Specify a source directory to (re-)generate a build system for it in the
current working directory. Specify an existing build directory to
re-generate its build system.
Run 'cmake --help' for more information.
3.示例
下面使用CMake创建一个简单的C++ Hello World项目。
3.1 创建工作目录
首先创建项目根目录cmake-demo,并在根目录下创建一个文件CMakeLists.txt和一个子目录hello:
mkdir cmake-demo && cd cmake-demo
touch CMakeLists.txt
mkdir hello
其中项目根目录cmake-demo可以在任意位置。
CMake通过名为CMakeLists.txt的文件声明构建目标和依赖关系。 根目录下的CMakeLists.txt内容如下:
cmake_minimum_required(VERSION 3.20)
project(cmake-demo)
set(CMAKE_CXX_STANDARD 14)
其中,cmake_minimum_required()
命令指定该项目要求的最低CMake版本,project()
命令设置项目名称,set()
命令设置CMake变量的值,CMAKE_CXX_STANDARD
变量指定C++标准版本。
3.2 实现hello库
在hello目录下创建hello.h和hello.cpp两个文件,内容如下:
hello.h
#pragma once
#include <string>
void hello(const std::string& to);
hello.cpp
#include "hello.h"
#include <iostream>
void hello(const std::string& to) {
std::cout << "Hello, " << to << "!\n";
}
这两个文件构成了一个函数库hello。
在hello目录下创建另一个CMakeLists.txt文件,在其中声明hello库:
add_library(hello hello.cpp)
target_include_directories(hello INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
其中,add_library()
命令添加了一个函数库构建目标,名字为 “hello”、源文件为hello.cpp;target_include_directories()
命令表示所有链接到hello库的构建目标都需要将当前源代码目录(即cmake-demo/hello)添加到头文件包含目录,从而可以直接#include "hello.h"
。
之后在项目根目录下的CMakeLists.txt结尾添加:
add_subdirectory(hello)
从而将子目录hello中的构建目标包含进来。
3.3 实现hello_world程序
下面创建一个hello_world程序,在main()
函数中调用hello库提供的函数来打印信息。
在项目根目录下创建源文件hello_world.cpp
,内容如下:
#include "hello.h"
int main() {
hello("world");
return 0;
}
在项目根目录下的CMakeLists.txt结尾添加:
add_executable(hello_world hello_world.cpp)
target_link_libraries(hello_world hello)
其中,add_executable()
命令添加了一个可执行程序构建目标,名字为 “hello_world”、源文件为hello_world.cpp;target_link_libraries()
命令声明了构建目标之间的依赖关系:hello_world依赖hello,即hello_world程序需要链接到hello库。
3.4 构建和运行
为了构建hello_world程序,在项目根目录下执行以下命令:
$ cmake -S . -B cmake-build
-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
...
-- Build files have been written to: .../cmake-demo/cmake-build
$ cmake --build cmake-build
[ 25%] Building CXX object hello/CMakeFiles/hello.dir/hello.cpp.o
[ 50%] Linking CXX static library libhello.a
[ 50%] Built target hello
[ 75%] Building CXX object CMakeFiles/hello_world.dir/hello_world.cpp.o
[100%] Linking CXX executable hello_world
[100%] Built target hello_world
其中,第一步是配置CMake,第二步是生成构建目标。构建完成后将在cmake-build目录下生成可执行程序hello_world,直接执行即可:
$ cmake-build/hello_world
Hello, world!
完整的项目目录结构如下:
cmake-demo/
CMakeLists.txt
hello_world.cpp
hello/
CMakeLists.txt
hello.h
hello.cpp
cmake-build/ # CMake自动创建
...
hello_world # 可执行程序
hello/
...
libhello.a # hello库
4.CMake命令行工具
CMake命令行工具cmake
的文档见cmake(1)。Linux或macOS系统也可以通过cmake --help
或man cmake
查看。
cmake命令的常用功能包括配置和构建。
4.1 配置
配置(configure)是指根据CMakeLists.txt声明的构建目标(build target)和依赖关系(dependencies),针对特定的底层构建工具生成构建系统(buildsystem)(由一系列构建文件组成),所使用的底层构建工具叫做生成器(generator)。
配置命令的用法如下:
cmake [<options>] -S <path-to-source> -B <path-to-build>
其中,-S
选项指定源代码目录(source directory),-B
选项指定构建目录(build/binary directory),构建目录用于存放底层构建工具的构建文件(例如Makefile)和构建目标的输出(例如库文件和可执行文件)。如果仅指定了二者之一,则另一个默认为当前工作目录。
当构建目标或依赖关系发生变化(即CMakeLists.txt文件发生变化)时,需要重新配置。
注:在实际项目中,通常将构建目录与源代码目录区分开,从而可以方便地从Git中排除。
4.1.1 生成器
生成器是CMake使用的底层构建工具,用于指定CMake配置命令生成哪种构建文件、使用哪种编译器和链接器:
生成器 | 底层构建工具 | 构建文件 | 编译器 |
---|---|---|---|
Unix Makefiles | GNU Make | Makefile | GCC |
MinGW Makefiles | MinGW Make | Makefile | GCC |
Visual Studio | MSBuild | .sln和.vcxproj | MSVC |
CMake支持不同平台上的多种生成器,详见cmake-generators(7)。可通过配置命令的-G
选项指定要使用的生成器,如果未指定则使用当前平台的默认生成器。CMake在当前平台上支持的生成器和默认生成器可通过cmake --help
命令查看。
例如,在Linux系统上可以使用Unix Makefiles生成器:
cmake -G "Unix Makefiles" -S . -B cmake-build
在Windows系统上可以使用Visual Studio生成器(需要安装对应版本的Visual Studio):
cmake -G "Visual Studio 17 2022" -A x64 -B cmake-build
4.2 构建
配置完成后,就可以根据构建系统生成构建目标。
构建命令的用法如下:
cmake [<options>] --build <path-to-build>
构建目标将被输出到构建目录中对应的子目录下。如果只有源代码发生变化,CMakeLists.txt文件没有变化,则只需重新构建,不需要重新配置。
构建命令的常用选项:
-t <name>
,--target <name>
:仅构建指定的目标及其上游依赖。可以指定多个目标,用空格分隔。如果未指定则构建所有目标。目标clean
用于清除构建输出。--config <cfg>
:对于多配置的构建工具指定使用的配置,例如Debug或Release。
5.CMake命令
CMakeLists.txt文件本身也是一种语言,叫做“CMake语言”,语法说明见cmake-language(7)。
CMake语言的核心是命令(command),这些命令用于声明构建目标和依赖关系、指定编译和链接选项、设置CMake变量的值等。完整列表见cmake-commands(7)。
CMake命令的语法格式如下:
command_name(arg1 arg2 ...)
command_name(KEYWORD1 arg1 KEYWORD2 arg2 ...)
下面是一些常用的CMake命令(有些命令并未列出全部参数)。
5.1 脚本命令
cmake_minimum_required
指定项目要求的最低CMake版本。
cmake_minimum_required(VERSION <min>)
set
设置CMake变量的值。
set(<variable> <value>...)
可以通过${var}
的形式引用变量。如果指定了多个值,则变量的实际值是分号分隔的列表。例如:
set(srcs a.c b.c c.c) # sets "srcs" to "a.c;b.c;c.c"
message
输出日志消息。
message("message text" ...)
include
从指定的文件或模块加载并运行CMake命令。
include(<file|module>)
if
条件语句。
if(<condition>)
<commands>
elseif(<condition>) # optional block, can be repeated
<commands>
else() # optional block
<commands>
endif()
其中,if()
支持的表达式语法和逻辑运算符见if - Condition Syntax。例如:
if(WIN32)
set(output_file NUL)
else()
set(output_file /dev/null)
endif()
foreach
对列表中的每个值执行一组命令。
foreach(<loop_var> <items>)
<commands>
endforeach()
该命令有几种变体:
(1)遍历整数
foreach(<loop_var> RANGE <stop>)
foreach(<loop_var> RANGE <start> <stop> [<step>])
遍历0 ~ stop
或start
~ stop
之间的整数,包含上界。其中,start
、stop
和step
必须是非负整数,且stop
大于等于start
,step
默认为1。
例如:
foreach(i RANGE 1 3)
add_executable(prog${i} prog${i}.cpp)
endforeach()
等价于
add_executable(prog1 prog1.cpp)
add_executable(prog2 prog2.cpp)
add_executable(prog3 prog3.cpp)
(2)遍历列表
foreach(<loop_var> IN ITEMS <items>)
其中items
是分号分隔的列表。
例如:
foreach(i IN ITEMS foo;bar;baz)
add_executable(${i} ${i}.cpp)
endforeach()
等价于
add_executable(foo foo.cpp)
add_executable(bar bar.cpp)
add_executable(baz baz.cpp)
function
定义函数。
function(<name> [<arg1> ...])
<commands>
endfunction()
该命令定义了一个名为name
的函数,可以接受参数,在函数体中可以用${arg1}
引用参数arg1
。函数体中的命令只有在函数被调用时才会执行。
例如:
function(add_gui_executable name source)
add_executable(${name} ${source})
target_link_libraries(${name} GUI)
endfunction()
add_gui_executable(foo foo.cpp)
add_gui_executable(bar bar.cpp)
等价于
add_executable(foo foo.cpp)
target_link_libraries(foo GUI)
add_executable(bar bar.cpp)
target_link_libraries(bar GUI)
在函数体中,除了${arg1}
等形式参数,还可以使用以下变量:
ARGC
:实际参数的个数ARGV0
、ARGV1
、ARGV2
等:各实际参数的值ARGV
:所有参数的列表ARGN
:最后一个期望的参数之后所有参数的列表
这些变量可用于创建带有可选参数的函数。
例如,上面定义的函数add_gui_executable()
只能接受单个源文件参数source
。要接受多个源文件可修改为:
function(add_gui_executable name)
add_executable(${name} ${GUI_TYPE} ${ARGN})
target_link_libraries(${name} GUI)
endfunction()
add_gui_executable(foo foo.cpp bar.cpp)
注:function()
命令仅支持位置参数(positional parameter),使用cmake_parse_arguments()
命令可以实现关键字参数(keyword parameter)。
cmake_parse_arguments
解析函数参数。
cmake_parse_arguments(<prefix> <options> <one_value_keywords> <multi_value_keywords> <args>...)
其中,options
指定所有的选项关键字(即没有值的关键字参数),one_value_keywords
指定所有的单值关键字,multi_value_keywords
指定所有的多值关键字,<args>...
是要被处理的参数。解析结果将被保存到各关键字对应的变量中,命名格式为<prefix>_<keyword>
。
例如:
function(my_install)
set(options OPTIONAL FAST)
set(one_value_keywords DESTINATION RENAME)
set(multi_value_keywords TARGETS CONFIGURATIONS)
cmake_parse_arguments(MY_INSTALL "${options}" "${one_value_keywords}" "${multi_value_keywords}" ${ARGN})
# ...
endfunction()
如果像这样调用my_install()
:
my_install(TARGETS foo bar DESTINATION bin OPTIONAL blub CONFIGURATIONS)
则在函数体中调用cmake_parse_arguments()
后将定义以下变量:
MY_INSTALL_OPTIONAL = TRUE
MY_INSTALL_FAST = FALSE # was not used in call to my_install
MY_INSTALL_DESTINATION = "bin"
MY_INSTALL_RENAME <UNDEFINED> # was not used
MY_INSTALL_TARGETS = "foo;bar"
MY_INSTALL_CONFIGURATIONS <UNDEFINED> # was not used
MY_INSTALL_UNPARSED_ARGUMENTS = "blub" # nothing expected after "OPTIONAL"
MY_INSTALL_KEYWORDS_MISSING_VALUES = "CONFIGURATIONS"
5.2 项目命令
project
设置项目名称。
project(<name>)
该命令将设置以下变量:
PROJECT_NAME
:项目名称PROJECT_SOURCE_DIR
:项目源代码目录PROJECT_BINARY_DIR
:项目构建目录
add_executable
添加可执行程序构建目标。
add_executable(<name> <source>...)
可执行文件名为<name>
(Linux)或<name>.exe
(Windows)。
add_library
添加函数库构建目标。
add_library(<name> [STATIC|SHARED] <source>...)
可以指定库文件的类型:
STATIC
:静态链接库(默认),库文件名为lib<name>.a
(Linux)或<name>.lib
(Windows)SHARED
:动态链接库,库文件名为lib<name>.so
(Linux)或<name>.dll
(Windows)
add_test
添加测试,详见第7节。
target_include_directories
为给定的目标添加包含目录(相当于GCC编译器的-I
选项)。
target_include_directories(<target> <PUBLIC|INTERFACE|PRIVATE> <dir>...)
其中,PUBLIC
、INTERFACE
和PRIVATE
关键字用于指定该选项的作用域:
PUBLIC
:对该目标及其下游依赖均生效INTERFACE
:仅对该目标的下游依赖生效PRIVATE
:仅对该目标生效
例如:
add_library(foo foo.cpp)
target_include_directories(foo INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
定义了一个函数库foo,并指定foo的下游依赖要将当前源代码目录添加到包含目录,从而可以直接包含当前目录下的头文件。
target_link_directories
为给定的目标添加依赖库(相当于GCC编译器的-L
选项)。
target_link_directories(<target> <PUBLIC|INTERFACE|PRIVATE> <dir>...)
注:一般不需要使用该命令,直接使用target_link_libraries()
即可。
target_link_libraries
为给定的目标添加依赖库,即上游依赖(相当于GCC编译器的-l
选项)。构建可执行文件时,依赖库将参与链接。
target_link_libraries(<target> <PUBLIC|INTERFACE|PRIVATE> <item>...)
target_link_libraries(<target> <item>...)
其中,第二种形式等价于PUBLIC
,即为给定的目标及其下游依赖均添加依赖库,从而依赖关系具有传递性。
例如:
add_library(foo foo.cpp)
add_library(bar bar.cpp)
add_library(baz baz.cpp)
target_link_libraries(bar foo)
target_link_libraries(baz INTERFACE foo)
定义了三个库foo、bar和baz,bar及其下游依赖均依赖foo,baz的下游依赖均依赖foo,但baz本身不依赖foo。
target_link_options
为给定的目标添加链接选项。
target_link_options(<target> <PUBLIC|INTERFACE|PRIVATE> <item>...)
add_subdirectory
将指定的子目录加入构建。
add_subdirectory(<dir>)
如果dir
是相对路径,则相对于当前目录。CMake执行该命令时将立即处理子目录中的CMakeLists.txt文件。
5.3 生成器表达式
cmake-generator-expressions(7)
6.CMake内置变量
CMake提供了很多内置变量,完整列表见cmake-variables(7)。下面是一些常用的变量。
6.1 路径相关变量
CMAKE_COMMAND
:cmake命令的完整路径CMAKE_GENERATOR
:构建项目使用的生成器CMAKE_SOURCE_DIR
:顶层源代码目录CMAKE_BINARY_DIR
:顶层构建目录CMAKE_CURRENT_SOURCE_DIR
:当前源代码目录CMAKE_CURRENT_BINARY_DIR
:当前构建目录PROJECT_NAME
:最近调用project()
命令的项目名称PROJECT_SOURCE_DIR
:最近调用project()
命令的项目源代码目录PROJECT_BINARY_DIR
:最近调用project()
命令的项目构建目录
6.2 系统相关变量
LINUX
:如果目标系统是Linux则设置为TRUE
WIN32
:如果目标系统是Windows则设置为TRUE
APPLE
:如果目标系统是macOS则设置为TRUE
6.3 语言相关变量
CMAKE_C_STANDARD
:默认C标准版本,可选的值为90、99、11、17、23等CMAKE_CXX_STANDARD
:默认C++标准版本,可选的值为98、11、14、17、20、23、26等
7.测试
CMake通过CTest模块提供了测试支持。
首先在项目根目录下的CMakeLists.txt中调用enable_testing()
命令,之后可以在任意的CMakeLists.txt中通过add_test()
命令添加测试。
7.1 添加测试
add_test()
命令的用法如下:
add_test(
NAME <name>
COMMAND <command> [<arg>...]
[WORKING_DIRECTORY <dir>])
其中,command
指定测试命令,如果是一个可执行文件目标,将会被自动替换为构建生成的可执行文件路径。如果命令的返回码为0则认为测试通过,否则测试失败。
7.2 运行测试
用于运行测试的命令行工具是ctest
,文档见ctest(1)。
添加测试并配置、构建完成后,在构建目录下直接执行ctest
命令即可。
注:CTest本身不提供任何断言或比较功能,如何执行测试完全由测试命令确定。
7.3 GoogleTest
GoogleTest是一个常用的C++测试框架,CMake通过GoogleTest模块提供了对GoogleTest的支持。
示例见GoogleTest使用教程。
8.模块
CMake自带一些提供额外功能的模块(module),例如前面提到的CTest
和GoogleTest
,可以通过include()
命令加载。完整列表见cmake-modules(7)。
8.1 FetchContent
FetchContent模块提供了在配置时自动加载外部项目的功能,主要命令是FetchContent_Declare()
和FetchContent_MakeAvailable()
。
示例:
- GoogleTest: GoogleTest使用教程 3.2节
- FLTK: 《C++程序设计原理与实践》笔记 第12章 一个显示模型 12.8.2节