Post

CMake构建工具使用教程

1.简介

CMake是一个开源的、跨平台的C++构建工具,通过平台和编译器无关的配置文件来声明构建目标,支持Make、ninja、MSBuild等多种底层构建工具,大多数IDE(例如CLion、Visual Studio、Visual Studio Code等)也都支持CMake。

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工具”,如下图所示。

Visual Studio Installer

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),解压后依次执行以下命令:

1
2
3
./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” 菜单项中的指引安装命令行工具,如下图所示。

macOS CMake

安装成功后应该能够在命令行中执行cmake命令:

1
2
3
4
5
6
7
8
9
10
11
12
$ 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:

1
2
3
mkdir cmake-demo && cd cmake-demo
touch CMakeLists.txt
mkdir hello

其中项目根目录cmake-demo可以在任意位置。

CMake通过名为CMakeLists.txt的文件声明构建目标和依赖关系。 根目录下的CMakeLists.txt内容如下:

1
2
3
4
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

1
2
3
4
5
#pragma once

#include <string>

void hello(const std::string& to);

hello.cpp

1
2
3
4
5
6
7
#include "hello.h"

#include <iostream>

void hello(const std::string& to) {
    std::cout << "Hello, " << to << "!\n";
}

这两个文件构成了一个函数库hello。

在hello目录下创建另一个CMakeLists.txt文件,在其中声明hello库:

1
2
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结尾添加:

1
add_subdirectory(hello)

从而将子目录hello中的构建目标包含进来。

3.3 实现hello_world程序

下面创建一个hello_world程序,在main()函数中调用hello库提供的函数来打印信息。

在项目根目录下创建源文件hello_world.cpp,内容如下:

1
2
3
4
5
6
#include "hello.h"

int main() {
    hello("world");
    return 0;
}

在项目根目录下的CMakeLists.txt结尾添加:

1
2
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程序,在项目根目录下执行以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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,直接执行即可:

1
2
$ cmake-build/hello_world 
Hello, world!

完整的项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 --helpman cmake查看。

4.1 配置

配置(configure)是指根据CMakeLists.txt声明的构建目标(build target)和依赖关系(dependencies),针对特定的底层构建工具生成构建系统(buildsystem)(由一系列构建文件组成),所使用的底层构建工具叫做生成器(generator)。

配置命令的用法如下:

1
cmake [<options>] -S <path-to-source> -B <path-to-build>

常用选项:

  • -S <path-to-source>:指定源代码目录(source directory)。
  • -B <path-to-build>:指定构建目录(build/binary directory),用于存放底层构建工具的构建文件(例如Makefile)和构建目标的输出(例如库文件和可执行文件)。
  • -D <var>=<value>:指定变量的值。可以省略中间的空格:-D<var>=<value>
  • -G <generator-name>:指定构建系统生成器。
  • -A <platform-name>:指定平台名称(如果生成器支持)。

其中,如果-S-B仅指定了二者之一,则另一个默认为当前工作目录。

当构建目标或依赖关系发生变化(即CMakeLists.txt文件发生变化)时,需要重新配置。

注:在实际项目中,通常将构建目录与源代码目录区分开,从而可以方便地从Git中排除。

4.1.1 生成器

生成器是CMake使用的底层构建工具,用于指定CMake配置命令生成哪种构建文件、使用哪种编译器和链接器。例如:

生成器底层构建工具构建文件编译器和链接器
Unix MakefilesGNU MakeMakefileGCC(gcc和ld)
MinGW MakefilesMinGW MakeMakefileGCC(gcc.exe和ld.exe)
Visual StudioMSBuild.sln和.vcxprojMSVC(cl.exe和link.exe)

CMake支持不同平台上的多种生成器,详见cmake-generators(7)。可通过配置命令的-G选项指定要使用的生成器,如果未指定则使用当前平台的默认生成器。CMake在当前平台上支持的生成器和默认生成器可通过cmake --help命令查看。

例如,在Linux系统上可以使用Unix Makefiles生成器:

1
cmake -G "Unix Makefiles" -S . -B cmake-build

在Windows系统上可以使用Visual Studio生成器(需要安装对应版本的Visual Studio):

1
cmake -G "Visual Studio 17 2022" -A x64 -B cmake-build

4.2 构建

配置完成后,就可以根据构建系统生成构建目标。

构建命令的用法如下:

1
cmake --build <dir> [<options>]

构建目标将被输出到构建目录中对应的子目录下。如果只有源代码发生变化,CMakeLists.txt文件没有变化,则只需重新构建,不需要重新配置。

常用选项:

  • --build <dir>:指定项目构建目录。
  • -j <jobs>, --parallel <jobs>:指定构建使用的最大并发进程数。
  • -t <name>, --target <name>:仅构建指定的目标及其上游依赖。可以指定多个目标,用空格分隔。如果未指定则构建所有目标。目标clean用于清除构建输出。
  • --config <cfg>:对于多配置的构建工具指定使用的配置,例如Debug或Release。
  • --clean-first:首先清理构建输出(即构建目标clean)。如果要只清理构建输出,使用--target clean

4.3 安装

构建完成后,可以将指定的构建目录中的库文件和可执行文件安装到指定位置,使用install()命令指定安装规则。

安装命令的用法如下:

1
cmake --install <dir> [<options>]

常用选项:

  • --install <dir>:指定项目构建目录。
  • --prefix <dir>:覆盖默认的安装目录前缀CMAKE_INSTALL_PREFIX

4.4 运行脚本

CMake脚本文件由CMake命令组成,后缀通常为.cmake(本质上和CMakeLists.txt没有区别)。用法:

1
cmake [-D <var>=<value>]... -P <cmake-script-file> [<args>...]

后面的参数将被传递给脚本,可通过变量CMAKE_ARGCCMAKE_ARGV<n>访问。

注:CMAKE_ARGV0是cmake可执行程序,CMAKE_ARGV1-PCMAKE_ARGV2是脚本文件名,CMAKE_ARGV3及之后才是传递给脚本的参数。

4.5 运行命令行工具

CMake提供了一些命令行工具,包括文件和目录操作、访问环境变量等,可以避免针对不同的操作系统编写不同的脚本命令。用法:

1
cmake -E <command> [<options>]

直接运行cmake -E将列出所有命令。

常用命令:

  • cat <files>...:拼接文件并打印到标准输出。
  • chdir <dir> <cmd> [<args>...]:切换当前工作目录并运行命令。
  • compare_files [--ignore-eol] <file1> <file2>:比较两个文件,如果文件相同则返回0,否则返回1。--ignore-eol表示忽略换行符的差异(LF/CRLF)。
  • copy <file>... <dst>, copy -t <dst> <file>...:将文件拷贝到指定目录下,不支持通配符。
  • copy_directory <dir>... <dst>:将目录拷贝到指定目录下。
  • echo [<args>...]:打印参数。
  • make_directory <dir>...:创建目录,如果需要则创建父目录。
  • rename <oldname> <newname>:重命名文件或目录。
  • rm [-r] <file|dir>...:删除文件或目录。'r选项用于递归删除目录及其子目录。

5.CMake命令

CMakeLists.txt文件本身也是一种语言,叫做“CMake语言”,语法说明见cmake-language(7)

CMake语言的核心是命令(command),这些命令用于声明构建目标和依赖关系、指定编译和链接选项、设置CMake变量的值等。完整列表见cmake-commands(7)

CMake命令的语法格式如下:

1
2
command_name(arg1 arg2 ...)
command_name(KEYWORD1 arg1 KEYWORD2 arg2 ...)

下面是一些常用的CMake命令(有些命令并未列出全部参数)。

5.1 脚本命令

cmake_minimum_required

指定项目要求的最低CMake版本。

1
cmake_minimum_required(VERSION <min>)

set

设置CMake变量的值。

1
set(<variable> <value>...)

可以通过${var}的形式引用变量。如果指定了多个值,则变量的实际值是分号分隔的列表。例如:

1
set(srcs a.c b.c c.c)  # sets "srcs" to "a.c;b.c;c.c"

option

定义一个布尔型选项,可以在配置命令中使用-D选项指定值为ONOFF

1
option(<variable> "<help_text>" [value])

如果未指定初始值则默认为OFF

message

输出日志消息。

1
message("message text" ...)

include

从指定的文件或模块加载并运行CMake命令。

1
include(<file|module>)

if

条件语句,当条件为真时执行一组命令。

1
2
3
4
5
6
7
if(<condition>)
  <commands>
elseif(<condition>) # optional block, can be repeated
  <commands>
else()              # optional block
  <commands>
endif()

其中,if()支持的表达式语法和逻辑运算符见if - Condition Syntax。例如:

1
2
3
4
5
if(WIN32)
  set(output_file NUL)
else()
  set(output_file /dev/null)
endif()

while

循环语句,当条件为真时重复执行一组命令。

1
2
3
while(<condition>)
  <commands>
endwhile()

foreach

循环语句,对列表中的每个值执行一组命令。

1
2
3
foreach(<loop_var> <items>)
  <commands>
endforeach()

该命令有几种变体:

(1)遍历整数

1
2
foreach(<loop_var> RANGE <stop>)
foreach(<loop_var> RANGE <start> <stop> [<step>])

遍历0 ~ stopstart ~ stop之间的整数,包含上界。其中,startstopstep必须是非负整数,且stop大于等于startstep默认为1。

例如:

1
2
3
foreach(i RANGE 1 3)
  add_executable(prog${i} prog${i}.cpp)
endforeach()

等价于

1
2
3
add_executable(prog1 prog1.cpp)
add_executable(prog2 prog2.cpp)
add_executable(prog3 prog3.cpp)

(2)遍历列表

1
foreach(<loop_var> IN ITEMS <items>)

其中items是分号分隔的列表。

例如:

1
2
3
foreach(i IN ITEMS foo;bar;baz)
  add_executable(${i} ${i}.cpp)
endforeach()

等价于

1
2
3
add_executable(foo foo.cpp)
add_executable(bar bar.cpp)
add_executable(baz baz.cpp)

break

跳出foreach()while()循环。

1
break()

continue

继续下一次foreach()while()循环。

1
continue()

function

定义函数。

1
2
3
function(<name> [<arg1> ...])
  <commands>
endfunction()

该命令定义了一个名为name的函数,可以接受参数,在函数体中可以用${arg1}引用参数arg1。函数体中的命令只有在函数被调用时才会执行。

例如:

1
2
3
4
5
6
7
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)

等价于

1
2
3
4
add_executable(foo foo.cpp)
target_link_libraries(foo GUI)
add_executable(bar bar.cpp)
target_link_libraries(bar GUI)

在函数体中,除了${arg1}等形式参数,还可以使用以下变量:

  • ARGC:实际参数的个数
  • ARGV0ARGV1ARGV2等:各实际参数的值
  • ARGV:所有参数的列表
  • ARGN:最后一个期望的参数之后所有参数的列表

这些变量可用于创建带有可选参数的函数。例如,上面定义的函数add_gui_executable()只能接受单个源文件参数source。要接受多个源文件可修改为:

1
2
3
4
5
6
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

解析函数参数。

1
cmake_parse_arguments(<prefix> <option_keywords> <one_value_keywords> <multi_value_keywords> <args>...)

其中,option_keywords指定所有的选项关键字(即没有值的关键字参数),one_value_keywords指定所有的单值关键字,multi_value_keywords指定所有的多值关键字,<args>...是要被处理的参数。解析结果将被保存到各关键字对应的变量中,命名格式为<prefix>_<keyword>

例如:

1
2
3
4
5
6
7
function(my_install)
  set(option_keywords OPTIONAL FAST)
  set(one_value_keywords DESTINATION RENAME)
  set(multi_value_keywords TARGETS CONFIGURATIONS)
  cmake_parse_arguments(MY_INSTALL "${option_keywords}" "${one_value_keywords}" "${multi_value_keywords}" ${ARGN})
  # ...
endfunction()

如果像这样调用my_install()

1
my_install(TARGETS foo bar DESTINATION bin OPTIONAL blub CONFIGURATIONS)

则在函数体中调用cmake_parse_arguments()后将定义以下变量:

1
2
3
4
5
6
7
8
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"

list

列表操作,详见list

string

字符串操作,详见string

file

文件操作,详见file

execute_process

执行一个或多个子进程。

1
2
3
4
5
6
7
8
9
10
11
execute_process(
  COMMAND <cmd1> [<args>]
  [COMMAND <cmd2> [<aegs>]]...
  [WORKING_DIRECTORY <directory>]
  [RESULT_VARIABLE <variable>]
  [OUTPUT_VARIABLE <variable>]
  [ERROR_VARIABLE <variable>]
  [INPUT_FILE <file>]
  [OUTPUT_FILE <file>]
  [ERROR_FILE <file>]
  [COMMAND_ERROR_IS_FATAL <ANY|LAST>])

命令是以管道的方式并发执行的,每个命令的标准输出通过管道连接到下一个命令的标准输入。

execute_process()是在CMake 配置时运行指定的命令,使用add_custom_command()创建在构建时运行的自定义命令。

选项:

  • COMMAND:子进程命令行。重定向运算符(例如>)被当作普通参数,使用INPUT*OUTPUT*ERROR*选项重定向stdin、stdout和stderr。
  • WORKING_DIRECTORY:执行命令的工作目录。
  • RESULT_VARIABLE:指定的变量将被设置为最后一个子进程的结果(返回码或错误信息)。
  • OUTPUT_VARIABLE, ERROR_VARIABLE:指定的变量将被分别设置为标准输出和标准错误的内容。
  • INPUT_FILE:将第一个子进程的标准输入重定向到指定的文件。
  • OUTPUT_FILE:将最后一个子进程的标准输出重定向到指定的文件。
  • ERROR_FILE:将所有子进程的标准错误重定向到指定的文件。
  • COMMAND_ERROR_IS_FATALANY表示任何一个命令失败则算失败,LAST表示只有最后一个命令失败才算失败。

5.2 项目命令

project

设置项目名称。

1
project(<name>)

该命令将设置以下变量:

  • PROJECT_NAME:项目名称
  • PROJECT_SOURCE_DIR:项目源代码目录
  • PROJECT_BINARY_DIR:项目构建目录

add_subdirectory

将指定的子目录加入构建。

1
add_subdirectory(<dir>)

如果dir是相对路径,则相对于当前目录。CMake执行该命令时将立即处理子目录中的CMakeLists.txt文件。

add_executable

添加可执行程序构建目标。

1
add_executable(<name> <source>...)

可执行文件名为<name> (Linux)或<name>.exe (Windows)。

add_library

添加库构建目标。

(1)普通库

1
add_library(<name> [STATIC|SHARED] <source>...)

构建生成库文件,可以指定库文件的类型:

  • STATIC:静态链接库(默认),库文件名为lib<name>.a (Linux)或<name>.lib (Windows)
  • SHARED:动态链接库,库文件名为lib<name>.so (Linux)或<name>.dll (Windows)

(2)对象库

1
add_library(<name> OBJECT <source>...)

构建生成对象文件<name>.o (Linux)或<name>.obj (Windows),只编译不链接(类似于GCC编译器的-c选项)。其他add_library()add_executable()创建的目标可以通过$<TARGET_OBJECTS:name>的形式在输入源引用对象库。

(3)接口库

1
add_library(<name> INTERFACE)

接口库不编译任何源文件,也不生成库文件。然而可以通过target_link_libraries(INTERFACE)target_include_directories(INTERFACE)等命令设置接口属性。

接口库的一个主要通途是创建只有头文件的库(header-only library)。CMake 3.23之后可以使用target_sources()命令关联头文件。例如:

1
2
3
4
5
6
7
add_library(Eigen INTERFACE)

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

add_test

添加测试,详见第7节。

target_include_directories

为给定的目标添加包含目录(相当于GCC编译器的-I选项)。

1
target_include_directories(<target> <PUBLIC|INTERFACE|PRIVATE> <dir>...)

其中,PUBLICINTERFACEPRIVATE关键字用于指定该选项的作用域:

  • PUBLIC:对该目标及其下游依赖均生效
  • INTERFACE:仅对该目标的下游依赖生效
  • PRIVATE:仅对该目标生效

相对路径将被解释为相对于当前源代码目录。

例如:

1
2
add_library(foo foo.cpp)
target_include_directories(foo INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

定义了一个函数库foo,并指定foo的下游依赖要将当前源代码目录添加到包含目录,从而可以直接包含当前目录下的头文件。

为给定的目标添加库目录(相当于GCC编译器的-L选项)。

1
target_link_directories(<target> <PUBLIC|INTERFACE|PRIVATE> <dir>...)

注:一般不需要使用该命令,直接使用target_link_libraries()即可。

为给定的目标添加依赖库,即上游依赖(相当于GCC编译器的-l选项)。构建可执行文件时,依赖库将参与链接。

1
2
target_link_libraries(<target> <PUBLIC|INTERFACE|PRIVATE> <item>...)
target_link_libraries(<target> <item>...)

其中,第二种形式等价于PUBLIC,即为给定的目标及其下游依赖均添加依赖库,从而依赖关系具有传递性。

每个<item>可以是:

  • 库目标名称,由add_library()创建
  • 库文件完整路径
  • 库文件名称(例如foo变成-lfoofoo.lib
  • 链接选项,以-开头,但-l-framework除外。

例如:

1
2
3
4
5
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_compile_options

为给定的目标添加编译选项。

1
target_compile_options(<target> <PUBLIC|INTERFACE|PRIVATE> <item>...)

target_compile_definitions

为给定的目标添加宏定义(相当于GCC编译器的-D选项)。

1
target_compile_definitions(<target> <PUBLIC|INTERFACE|PRIVATE> <item>...)

其中<item>的格式为name=definitionname。例如:

1
2
target_compile_definitions(foo PUBLIC FOO)
target_compile_definitions(foo PUBLIC FOO=bar)

为给定的目标添加链接选项。

1
target_link_options(<target> <PUBLIC|INTERFACE|PRIVATE> <item>...)

include_directories

为当前目录及子目录下的所有目标添加包含目录。

1
include_directories(<dir>...)

注:优先使用target_include_directories()

为当前目录及子目录下的所有目标添加库目录。

1
link_directories(<dir>...)

注:优先使用target_link_directories()

为当前目录及子目录下的所有目标添加依赖库。

1
link_libraries(<item>...)

注:优先使用target_link_libraries()

add_compile_options

为当前目录及子目录下的所有目标添加编译选项。

1
add_compile_options(<option>...)

注:优先使用target_compile_options()

add_compile_definitions

为当前目录及子目录下的所有目标添加宏定义。

1
add_compile_definitions(<definition>...)

注:优先使用target_compile_definitions()

为当前目录及子目录下的所有目标添加链接选项。

1
add_link_options(<option>...)

注:优先使用target_link_options()

add_custom_command

添加自定义构建规则。

1
2
3
4
5
6
add_custom_command(
  OUTPUT output...
  COMMAND command [ARGS] [args...]
  DEPENDS depends...
  [WORKING_DIRECTORY dir]
  [VERBATIM])

其中,depends可以是构建目标或文件名,VERBATIM选项保证命令的参数被正确转义。如果command是一个可执行文件目标,将会被自动替换为构建生成的可执行文件路径。

例如:

1
2
3
4
5
6
7
add_custom_command(
  OUTPUT out.c
  COMMAND someTool -i ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
                   -o out.c
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
  VERBATIM)
add_library(myLib out.c)

命令参数可以包含重定向运算符(例如>)。

add_custom_target

添加一个没有输出的自定义目标。

1
2
3
4
5
6
7
add_custom_target(
  name
  [COMMAND command [args...] ...]
  [DEPENDS depends... ]
  [WORKING_DIRECTORY dir]
  [VERBATIM]
  [SOURCES src1 [src2...]])

install

指定安装规则。

1
2
install(TARGETS targets... DESTINATION <dir>)
install(FILES files... DESTINATION <dir>)

第一种形式用于安装构建目标(库文件或可执行文件),dir可以是绝对路径或相对路径,相对路径将被解释为相对于CMAKE_INSTALL_PREFIX变量的值。

第二种形式用于安装文件,如果文件名是相对路径,则相对于当前源代码目录。

例如:

1
2
3
4
5
6
add_executable(foo foo.cpp)
add_library(bar bar.cpp)

install(TARGETS foo DESTINATION bin)
install(TARGETS bar DESTINATION lib)
install(FILES bar.h DESTINATION include)

在执行安装命令cmake --install时分别将可执行文件foo、库文件libbar.a和头文件bar.h安装到CMAKE_INSTALL_PREFIX下的bin、lib和include目录。

5.3 生成器表达式

cmake-generator-expressions(7)

6.CMake内置变量

CMake提供了很多内置变量,可以通过set()命令或-D选项指定。下面是一些常用的变量,完整列表见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 系统相关变量

  • UNIX:如果目标系统是UNIX或类UNIX则设置为True
  • LINUX:如果目标系统是Linux则设置为True
  • WIN32:如果目标系统是Windows则设置为True
  • APPLE:如果目标系统是macOS则设置为True
  • CYGWIN:如果使用Cygwin则设置为True
  • MINGW:如果编译器是MinGW则设置为True
  • MSVC:如果编译器是Microsoft Visual C++则设置为True

6.3 语言相关变量

  • CMAKE_C_STANDARD:默认C标准版本,可选的值为90、99、11、17、23等
  • CMAKE_CXX_STANDARD:默认C++标准版本,可选的值为98、11、14、17、20、23、26等
  • CMAKE_<LANG>_COMPILER:指定语言的编译器完整路径,<LANG>可以是C、CXX等
  • CMAKE_<LANG>_COMPILER_ID:指定语言的编译器名称,可能的值包括GNU、Clang、MSVC等,详见文档
  • CMAKE_<LANG>_FLAGS:指定语言的编译选项

6.4 构建/安装相关变量

  • CMAKE_BUILD_TYPE:指定单配置生成器(例如Makefile、Ninja等)的构建类型,例如DebugRelease等,详见Build Configurations
  • CMAKE_INSTALL_PREFIXinstall()使用的安装目录。在UNIX上默认为 “/usr/local”,在Windows上默认为 “C:\Program Files\ ${PROJECT_NAME}”。

7.测试

CMake通过CTest模块提供了测试支持。

首先在项目根目录下的CMakeLists.txt中调用enable_testing()命令,之后可以在任意的CMakeLists.txt中通过add_test()命令添加测试。

7.1 添加测试

add_test()命令的用法如下:

1
2
3
4
add_test(
  NAME <name>
  COMMAND <command> [<arg>...]
  [WORKING_DIRECTORY <dir>])

其中,command指定测试命令,如果是一个可执行文件目标,将会被自动替换为构建生成的可执行文件路径。如果命令的返回码为0则认为测试通过,否则测试失败。

注:由于CTest并不是在shell中执行测试命令,因此无法使用标准输入/输出重定向,command中的<>将被当作普通参数。如果需要重定向测试命令的标准输入/输出,有两种方法:

  • 使用bash -c,例如add_test(NAME my_test COMMAND sh -c "foo < in.txt > out.txt"),但这种方法不是平台独立的,在Windows上需要使用cmd /c
  • 在一个cmake脚本中调用execute_process()执行真正的测试命令,并在add_test()中通过cmake -P调用该脚本。

参考:

7.2 运行测试

用于运行测试的命令行工具是ctest,文档见ctest(1)

添加测试并配置、构建完成后,在构建目录下直接执行ctest命令即可。

注:CTest本身不提供任何断言或比较功能,如何执行测试完全由测试命令决定。

7.3 GoogleTest

GoogleTest是一个常用的C++测试框架,CMake通过GoogleTest模块提供了对GoogleTest的支持。

示例见GoogleTest使用教程

8.模块

CMake自带一些提供额外功能的模块(module),例如前面提到的CTestGoogleTest,可以通过include()命令加载。完整列表见cmake-modules(7)

8.1 FetchContent

FetchContent模块提供了在配置时自动加载外部项目的功能,主要命令是FetchContent_Declare()FetchContent_MakeAvailable()

示例:

This post is licensed under CC BY 4.0 by the author.