关于

CMake 是非常流行的跨平台 C/C++ 构建系统生成工具. 它不直接调用编译器编译代码, 而是生成更底层的编译过程, 比如 makefile, ninja, VS Project File等.

它生成的编译目录可以独立于源码目录之外, 编译完之后将编译目录移除, 而不影响源码目录.

可以直接访问在线版本 https://cmake.biofan.org.

全文基于 CC BY-NC-SA 4.0 协议发布.

基础

组织结构

cmake 有三种结构: 目录, 使用 add_subdirectory() 来导入子目录, 子目录中要有一个 CMakeLists.txt 文件, 执行子目录中的 CMakeLists.txt 时, 会创建一个新的子作用域, 继续父作用域中的属性.

脚本, cmake 本身是一个脚本语言, 可以用 cmake -P script.cmake 来执行一个脚本文件.

模块, 比如常用的 include()find_package() 命令, 就是导入相应的 cmake 模块, 跟 C++ 中的 #include 类似, 导入的模块就是在当前作用域运行的. 可以通过修改 CMAKE_MODULE_PATH 来修改模块的查找路径. 模块通常放在项目的 cmake 目录里, 比如 FindFooLibrary.cmake.

版本号

请使用 cmake 3.2 以上的版本. 另外, 条件允许的话优先考虑使用更高的版本, 比如 3.10, 3.13 甚至最新版. 这几版中改动比较大, 修复了不少问题, 具体可参考 cmake 的 changelog. 像 Ubuntu 14.04 这样的旧系统里, 默认还是 2.8 这种旧版本, 建议直接手动安装最新版.

命令

cmake 代码的基本的形式是:

command_name(string1 string2 ...)

参考

可以阅读 cmake-language 手册, 对 cmake 语法有个基本的认识.

编码格式

cmake 本身是代码, 就像 bash 脚本一样, 而不是一般的文本. 整个项目需要遵守一致的 编码风格.

比如, 常见的如下规则:

  • 使用 UTF-8 编码
  • 函数及宏都要有注释, 说明其功能及选项
  • 代码对齐方式, tab 或者 4 个空格
  • 一行代码只能最多包含80或120个字符
  • 函数名使用小写, 用下划线作分隔
  • 宏定义名使用全大写, 用下划线分隔
  • 选项 (option) 使用全大写, 用下划线作分隔

Options

定义一个新的选项

option(<variable> <help_text> [default_value])

比如, :

option(BUILD_EXAMPLES "构建示例代码" ON)

可选的值是 ON 或者 OFF, 默认值是 OFF.

编译时, 可以控制这个选项:

cmake -DBUILD_EXAMPLES=ON ...

高级选项

  • -DCMAKE_BUILD_TYPE, 构建类项, Debug, Release 等, 默认是 Debug
  • -DCMAKE_INSTALL_PREFIX, 类似于 configure 命令的 --prefix=xxx 选项, 用于设置安装目录前缀, 默认是 /usr/local
  • -DBUILD_SHARED_LIBS, 设置生成动态库还是静态库, 默认是静态库; 如果add_library() 里显式地指定了目标库的类型, 则忽略本选项
  • -DBUILD_TESTING, 设置是否编译测试代码, 默认为 OFF

前端工具

终端界面

ccmake 工具是交互式终端界面, 可以控制所有可用选项. 默认显示的是用户自定义的选项. 可以按下 t 以显示高级选项, 比如选定编译器, CMAKE_BUILD_TYPE.

修改完之后, 按下 c 生成配置文件.

安装:

sudo apt install cmake-curses-gui

图形化界面

在 linux 系统中, 提供了 Qt 界面, 用法跟 ccmake 类似.

安装:

sudo apt install cmake-qt-gui

非交互式命令行工具

生成缓存

生成 cmake 缓存:

cmake -B build 

等同于:

mkdir -v build
cd build
cmake ..

使用 nginx 生成器, cmake 在 linux 平台默认使用 makefile 生成器:

cmake -G Ninja -B build

修改选项的值:

cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/path/to/ros/cmake ..

编译项目

编译:

cd build
cmake --build .

等同于:

cd build
make -j$(nproc)

安装

安装文件到系统里:

cd build
sudo cmake --build . --target install

等同于:

cd build
sudo make install

在新版本中, 引入了 --install 选项:

cd build
sudo cmake --install .

列举选项

  • -L, 列出可用选项
  • -LH, 列出可用选项, 包括注释
  • -N, 只读模式, 不会再重头运行一次 cmake 脚本

常用的选项组合是:

$ cmake -N -LH .

运行脚本

cmake 本身是脚本语言, 类似于 bash, 它们是可以直接运行的:

hello.cmake 的内容:

message("Hello, world!")
cmake -P hello.cmake

也可以使用 shebang line, 修改后的 hello.cmake 如下:

#!/usr/bin/env -S cmake -P

message("Hello, world")

然后直接运行它:

chmod +x hello.cmake
./hello.cmake

命令行工具

cmake 命令本身有提供命令行工具, 尤其是对 windows 平台比较有用, 很多命令在 *nix 环境默认已提供.

cmake -E <command> [<options>]

比如:

cmake -E sleep 5

常用选项有:

  • md5sum, sha1sum, sha256sum, sha512sum
  • copy, mv, sleep, time, touch, echo
  • environment 列出当前的环境变量, 类似于 env 命令
  • capabilities 以 JSON 格式显示当前 cmake 命令支持的功能, 比如有哪些 Generators
  • server, 运行服务器模式, 启动一个 cmake 守护进程, 可通过网络访问它

参考

Variable

变量 Variable

cmake 变量使用的是动态作用域(dynamic scope). 所有的变量类型都是字符串 string. 变量名是大小写敏感的.

使用 set() 命令来定义及修改一个变量:

set(PROJECT_VERSION 3.1)

如果一个变量未定义, 它的值就是 undefined 的, 是一个空字符串.

取消一个变量的定义, 可以用 unset() 命令.

使用如 ${ ... } 的形式展开变量.

缓存变量 Cache Entry

缓存变量, 只需要在第一次运行 cmake 时生成的, 之后再运行 cmake 时, 可以从 CMakeCache.txt 中读取. 缓存变量, 可以在 ccmakecmake-qt 交互式界面中出现. 全局可见, 变量查找优先级低.

定义一个缓存变量:

option(USE_SYSTEM_ZLIB "Use system zlib or compiled from source" OFF)

上面定义的缓存变量, 等同于使用 set 命令:

set(USE_SYSTEM_ZLIB OFF CACHE BOOL "User system zlib or compiled from source")

使用 set() 命令时, 需要指定变量类型:

  • BOOL
  • STRING
  • FILEPATH
  • PATH
  • INTERNAL, 内部使用, 不会出现在 cmake 交互式界面

支持枚举类型:

set(COMPRESS_LIB "zlib" CACHE STRING "Select a file compression library")
set_property(CACHE COMPRESS_LIB PROPERTY STRINGS "zlib" "zstd" "snappy" "lzma")

可以使用 $CACHE{varialbe} 的形式来读取, 比如 $CACHE{COMPRESS_LIB}

环境变量 Env

全局可见. 读取环境变量, $ENV{variable}, 比如: message(STATUS "Current user: $ENV{USER}")

修改环境变量:

set(ENV{OLDPWD} "/tmp")

所有的可用环境变量可以在 CMake Environment 手册 中查看.

字符串

cmake 中所有变量都是字符串.

查找与替换

查找:

set(NAME "Tasha Wang")
string(FIND ${NAME} " " LAST_NAME)
message("Last name: ${LAST_NAME}")

替换:

set(KERNEL_VERSION 5.4.2)
string(REPLACE "5." "4." OLD_VERSION ${KERNEL_VERSION})
message("OLD version ${OLD_VERSION}")

字符串操作方法

合并:

set(MAJOR_VERSION 5)
set(MINOR_VERSION 4)
set(PATCH_VERSION 2)
string(CONCAT VERSION_NUMBER ${MAJOR_VERSION} ${MINOR_VERSION} ${PATCH_VERSION})
message("KERNEL_VERSION: ${VERSION_NUMBER}")

拼接字符串:

set(APT_PARTS "/etc" "apt" "sources.list")
string(JOIN "/" APT_PATH ${APT_PARTS})
message("APT config path: ${APT_PATH}")

移除两端的空白字符:

set(USERNAME "Tasha Wang ")
string(STRIP ${USERNAME} USERNAME)
message("Username::${USERNAME}::")

比较字符串:

string(COMPARE EQUAL ${CMAKE_BUILD_TYPE} "Release" IS_RELEASE)
message("is release mode: ${IS_RELEASE}")

正则表达式

替换:

set(KERNEL_VERSION "5.4.2")
string(REGEX REPLACE "[0-9]$" "0" NEW_VERSION ${KERNEL_VERSION})
message("NEW version: ${NEW_VERSION}")

Hash 计算

set(NAME "Tasha Wang")
string(SHA256 NAME_HASH NAME)
message("hash of name: ${NAME_HASH}")

生成字符串

支持生成时间标签, UUID, 随机字符串等等.

string(RANDOM LENGTH 8 RANDOM_PASSWORD)
message("RANDOM_USER: ${RANDOM_PASSWORD}")

参考

CMake String 手册

列表

尽管所有的变量都是字符串 string, 但可以使用分号;分隔的字符串表示一个字符串数组.

set(SRC_FILES "match.c")
list(APPEND SRC_FILES "math.h")
message("src files: ${SRC_FILES}")

list(LENGTH SRC_FILES FILE_NUM)
message("There are ${FILE_NUM} files in source file list")

数学计算

支持的操作符:

  • + - * / %
  • << >> & | ^ ~
  • ( ... ) 优先级
  • 二进制/八进制/十进制/十六进制格式化输出
  • 只支持 int64_t 类型的整数, 如果是符点数的话就会报错.
math(EXPR ONE_KB "1 << 10")
message("ONE_KB: ${ONE_KB}")

控制语句

if else

if(<condition>)
  ...
endif()

比如:

if (NOT IS_DIRECTORY "/usr/local/lib")
    message(FATAL_ERROR "lib folder not found")
endif()
if(CMAKE_BUILD_TYPE MATCHES Debug)
  message("Is in debug mode")
endif()
if (WIN32)
  message("In windows env")
elif (LINUX)
  message("In linux env")
endif()
cmake_policy(SET CMP0057 NEW)

set(SRC_FILES list.h list.c math.h math.c)
if ("math.h" IN_LIST SRC_FILES)
    message("math.h already in src list")
endif()

这里的条件判断方式比较多, 可以参考 CMake if 手册

foreach && while

遍历一个列表:

foreach(FILE ${SRC_FILES})
    message("SRC FILE: ${FILE}")
endforeach()

以下示例打印出 110 之间所有的奇数, 类似于 python 中的 range() 函数:

foreach(ODD RANGE 1 10 2)
    message("Odd num: ${ODD}")
endforeach()

break && continue

在循环中使用.

return

用于从函数, 目录或者文件中返回, 注意它不支持返回数据.

作用域

函数作用域 function scope

即在函数内部定义和使用的变量, 只在当前函数及函数内部调用的子函数可见. 变量查找优先级最高. 函数作用域中会有一份复制的函数所在目录的目录作用域变量.

目录作用域 directory scope

即在目录的 CMakeLists.txt 中定义的变量, 在当前的目录作用域, 里面的函数, 及子目录作用域可见. 在子目录作中域中会有一份复制的父目录作用域变量. 变量查找优先级较高.

set() 修改上一级作用域中的变量

set() 命令默认只修改当前作用域变量, 可以传入 PARENT_SCOPE 参数, 让它修改父作用域中的变量:

set(WIDGETS_FILES 
  file1,
  file2,
  file3,
  PARENT_SCOPE)

Functions

函数体内部, 是一个独立的作用域, 里面定义的变量, 只在函数体内有效.

function(factorial n)
    set(product 1)
    foreach(i RANGE 1 ${n})
        math(EXPR product "${product} * ${i}")
    endforeach()
    message("${n}! = ${product}")
endfunction()

factorial(10)

函数返回值

函数本身不支持返回值, 但可以使用 set() 命令来强制修改调用处的变量名:

function(accumulate x y sum)
  math(EXPR s "${x} + ${y}")
  set(${sum} ${s}  PARENT_SCOPE)
endfunction()

set(result 0)
accumulate(3 4 result)

这里在调用时, 不要传入 ${result}, 这样会将 result 变量展开, 传入的值是 "0". 我们要传入变量名本身.

Macros

类似于 C 语言中的宏, 就是用于代码展开. 不会创建新的作用域, 是在当前作用域展示并执行的.

可以用宏来定义有返回值的操作, 其它情况下, 都应该定义成函数.

macro(FACTORIAL n)
    set(FACTORIAL_PRODUCT 1)
    foreach(i RANGE 1 ${n})
        math(EXPR FACTORIAL_PRODUCT "${FACTORIAL_PRODUCT} * ${i}")
    endforeach()
endmacro()
FACTORIAL(10)
message("10! = ${FACTORIAL_PRODUCT}")

引入模块

配置文件

用于动态生成配置文件, 根据当前 cmake 脚本中的变量的值.

比如, 生成当前程序的版本号到一个头文件中. 首先定义一个输入文件, version.h.in:

#ifndef FOO_VERSION_H
#define FOO_VERSION_H

#cmakedefine FOO_VERSION @FOO_VERSION@

#endif  // FOO_VERSION_H

之后在 CMakeLists.txt 中引入它:

set(FOO_MAJOR 1)
set(FOO_MINOR 2)
set(FOO_PATCH 4)
set(FOO_VERSION "${FOO_MAJOR}.${FOO_MINOR}.${FOO_PATCH}")
configure_file(version.h.in version.h)
include_directories(${CMAKE_CURRENT_BINARY_DIR})

这里先定义了版本号 FOO_VERSION 变量的值是 1.2.4, 之后调用 configure() 命令, 在当前的二进制目录下会生成 version.h 文件, 其内容是:

#ifndef FOO_VERSION_H
#define FOO_VERSION_H

#define FOO_VERSION 1.2.4

#endif  // FOO_VERSION_H

比如, 根据当前的最新的 git 提供记录生成包含提供哈稀的版本号时, 可以很方便地 使用这种方法.

file 命令

该命令用于处理文件操作, 比如读写文件, 列举目录, 移动文件, 查看文件大小等等, 甚至 还支持下载/上传文件.

file(READ "/etc/passwd" PASSWD_FILE)
message("${PASSWD_FILE}")

file(INSTALL /usr/bin/vim
    DESTINATION /tmp
    FILE_PERMISSIONS 
    OWNER_READ OWNER_EXECUTE
    GROUP_READ GROUP_EXECUTE
    WORLD_READ WORLD_EXECUTE
    )

file(WRITE "${CMAKE_BINARY_DIR}/sources.ist" ${SRC_FILES})

提示

  • cmake2 与 cmake3 之间的差别跟 python2 与 python3 类似, 尽可能使用高版本的 cmake
  • cmake 是跟 bash 一样的脚本语言, 注意基本的代码规范; 注意文件格式; 注意作用域
  • 尽管 cmake 不区分函数名大小写, 但需要统一编码规范, 函数名小写, 宏定义名全大写
  • 只在项目根目录的 CMakeLists.txt 中设置编译器选项, 这样所有的 target 可使用统一的选项

常用操作

现代化的 cmake 中, 已经弱化了变量以及 include() 的位置, 开始以目标 target 为 中心, 强化模块化的概念.

参考

可以阅读 cmake-buildsystem 手册, 了解构建系统的基本用法及 target 相关内容.

目标 Targets

构造目标 Constructor

创建库, add_library() 接受一个参数, STATIC/SHARED/INTERFACE 等用于指定该库的类型:

  • STATIC, 生成静态库
  • SHARED, 生成动态库
  • INTERFACE, 用于处理只包含头文件的库, 这种库不需要编译操作.

创建可执行文件, add_executable().

创建自定义目标, add_custom_target().

add_library(Foo foo.cpp)
target_link_libraries(Foo PRIVATE Bar::Bar)

if (WIN32) 
  target_source(Foo PRIVATE foo_win32.cpp)
  target_link_libraries(Foo PRIVATE Bar::Win32Support)
endif()

属性

读写属性的一些命令

  • set_property()
  • get_property()
  • set_target_properties()
  • get_target_property()
  • set_directory_properties()
  • get_directory_properties()

get_target_property(), get_directory_properties()get_property() 命令的简化版本.

全局作用域属性

  • ENABLED_LANGUAGES, 当前支持编译哪些语言
  • JOB_POOLS, 用于管理并行编译的任务
  • CMAKE_CXX_KNOWN_FEATURES, 当前 cmake 版本支持的 C++ 语言特性
get_property(ENABLED_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES)
message("languages: ${ENABLED_LANGUAGES}")

目录属性

  • VARIABLES, 当前目录定义了哪些变量, 方便调试
  • BINARY_DIR, 当前目录对应的构建目录, 绝对路径
  • SOURCE_DIR, 当前源码目录的绝对路径
  • PARENT_DIRECTORY, 由哪个目录引入的当前目录, 绝对路径
  • MACROS, 当前目录定义了哪些宏
  • SUBDIRECTORIES, 当前目录引入了哪些子目录
  • LINK_DIRECTORIES, 当前目录的库查找路径, 是一个字符串列表; 里面包含了从 父作用域中继承过来的值. 通常是通过 link_libraries() 命令引入的.
  • INCLUDE_DIRECTORIES, 当前目录的头文件查找路径, 是一个字符串列表; 里面包含了 从父作用域中继承过来的值, 通常是通过 include_directories() 命令引入的.

比如:

get_property(PARENT_DIR DIRECTORY . PROPERTY PARENT_DIRECTORY)
message("PARENT DIR: ${PARENT_DIR}")

Target 属性

  • BINARY_DIR
  • BUILD_RPATH,
  • INSTALL_RPATH,
  • LINK_DIRECTORIES, 该 target 的库查找路径, 是一个字符串列表; 里面包含了从 当前的目录作用域中继承过来的值. 通常是通过 target_link_libraries() 命令引入的.
  • INCLUDE_DIRECTORIES, 该 target 的头文件查找路径, 是一个字符串列表; 里面包含了 从当前的目录作用域中继承过来的值, 通常是通过 target_include_directories() 命令引入的.
  • COMPILE_OPTIONS, 传给编译器的选项, 通常是用 target_compile_options() 命令引入的.
  • CXX_STANDARD, 使用哪个标准编译当前目标.
set_property(TARGET demo PROPERTY CXX_STANDARD 14)

引入头文件

target_include_directories() 用于给一个目标指定可查找的头文件, 它接受一个权限参数:

  • PULIBC, 对于库有意. 对于本库的使用者来说, 库本身的头文件及其依赖的头文件, 都是必需的. 会同时将指定的库加入到 LINK_LIBRARIESINTERFACE_LINK_LIBRARIES 属性里.
  • PRIVATE, 对于本库的使用者来说, 只有库本身的头文件是导出的, 不会导出本库的依赖库, 只会将指定的库加入到 LINK_LIBRARIES 属性里.
  • INTERFACE, 对于
target_link_libraries(Foo
    PUBLIC Bar::Bar
    PRIVATE Cow::Cow
)

引入新的源代码文件

target_sources() 用于给一个已定义的 target 引入新的源代码文件. 比如:

if(WIN32)
    target_sources(Foo utils_win32.cpp utils_win32.h)
endif()

链接到新的库

target_link_libraries() 将一个目标链接到指定的库或者别的目标.

find_package(Bar 2.0 REQUIRED)
...
target_link_libraries(Foo PRIVATE Bar::Bar)

要注意的是, 需要给 target_link_libraries() 指定可见性, 它有三种:

  • PUBLIC, 本 target 导出的API, 依赖了这个库时, 要用 PUBLIC
  • PRIVATE, 对于只用于内部的库, 使用这个
  • INTERFACE, 对于 header-only 的库, 使用这个选项

参考

CMake Properties 手册

查找库

查找路径

CMAKE_PREFIX_PATH 定义了查找 cmake 库的路径, 可以在编译时指定这个选项, 比如, 在使接非系统目录中安装的 Qt5.12 时, 可以写成:

cmake -DCMAKE_PREFIX_PATH=/path/to/qt5.12/cmake ..

find_package()

最常用的引入第三方库的方式就是使用 find_package() 命令:

find_package(Foo 2.0 REQUIRED)
...
target_link_libraries(Bar Foo::Foo ...)

cmake 项目本身提供了很多常用第三方库的查找脚本, 我们也可以自定义这类脚本. 将这些 脚本放在项目的 cmake 目录, 然后引入整个目录:

set(CMAKE_MODULE_PATH
    ${CMAKE_SOURCE_DIR}/cmake
    ${CMAKE_MODULE_PATH})

自定义查找模块

以查找 Foo 库为例, 提供一个基本的模板:

find_path(Foo_INCLUDE_DIR foo.h)
find_library(Foo_LIBRARY foo)
# 将这两个选项标记为高级选项, 让该库默认不在 cmake gui 工具中显示.
mark_as_advanced(Foo_INCLUDE_DIR Foo_LIBRARY)

# 用于提供 Foo_FOUND 变量, 也处理 QUITE/REQUIRED 等选项, 以及库的版本号.
# 比如: find_package(Foo 2.0 REQUIRED)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Foo
    REQUIRED_VARS Foo_LIBRARY FOO_INCLUDE_DIR
    )

# 添加一个自定义的 target
if(Foo_FOUND AND NOT TARGET Foo::Foo)
    add_library(Foo::Foo UNKNOWN IMPORTED)
    set_target_properties(Foo::Foo PROPERTIES
        IMPORTED_LINK_INTERFACE_LANGUAGES "CXX"
        IMPORTED_LOCATION "${Foo_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${Foo_INCLUDE_DIR}"
        )
endif()

依赖关系

如果引入的模块比较多的话,相互间的依赖关系就容易复杂,可以使用 cmake 自带的 命令来生成依赖关系图:

cmake --graphviz=foo.dot

生成的 dot 文件可以再转为一般的图片格式。

我们可以将这个步骤固化下来,形成一个 cmake 模块,比如就叫 GeneriteDeps.cmake

add_custom_target(generate_deps
    COMMAND cmake --graphviz=deps.dot .
    COMMAND dot -Tsvg -o deps.svg deps.dot
    COMMAND setsid xdg-open deps.svg
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    )

这样可以在命令行生成:

cmake --build . --target generate_deps

但方便的是,可以直接在 IDE 里触发它。

比如,libuv 库的依赖图是这样的: libuv-deps

编译与链接

编译器

通过环境变量指定编译器:

$ CC=clang CXX=clang++ cmake ..

通过参数指定编译器:

$ cmake -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang++ ..

也可以在 cmake 脚本中指定:

set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)

环境检查

  • CheckCCompilerFlags
  • CheckCSourceCompiles
  • CheckCSourceRuns
  • CheckCXXCopmilerFlags
  • CheckCXXSourceCompiles
  • CheckCXXSourceRuns

以下示例用于检查当前 linux 系统中是否有 sendfile() 系统调用.

include(CheckCSourceRuns)
check_c_source_runs(
    "
#include <sys/sendfile.h>
#include <errno.h>

int main(void) {
  int s = 0;
  int fd = 1;
  ssize_t n;
  off_t off = 0;
  n = sendfile(s, fd, &off, 1);
  if (n == -1 && errno == ENOSYS) {
    return 1;
  } else {
    return 0;
  }
}
    "
    HAS_SENDFILE
)

if (HAS_SENDFILE)
  message("Has sendfile")
endif ()

# 从缓存变量中移除 HAS_SENDFILE.
unset(HAS_SENDFILE CACHE)

编译选项

对当前目录作用域及子目录作用域中的所有目标(targets) 都有效:

add_compile_options(-Wall -Wextra -Werror -Wpedantic --pedantic-errors)
target_add_compile_options(Foo -Wall -Wextra -Werror -Wpedantic --pedantic-errors)

编译器特性

比如, 我们要求使用 c++17 的标准来编译目标 Foo.

target_compile_features(Foo PRIVATE cxx_std_17)

CMake 是如何处理这些编译选项的呢? 首先 cmake 命令在生成配置文件时, 会打印这样的信息:

-- Detecting CXX compile features
-- Detecting CXX compile features - done

这里, 运持的是 cmake 自己提供的一些编译器特性检查模块. 并会生成一个基于当前 编译器版本号对应的所有 C++ 版本. 如果对某个版本是完全支持的, 就会有类似 CMAKE_CXX17_STANDARD__HAS_FULL_SUPPORT 这样的临时属性. 如果不是完整支持的, 就会进行一些临时的编译任务, 编译一些 c++ 代码片段, 以检测当前编译器对某个特性 是否支持.

静态编译可执行文件

只需要修改一下链接时的选项即可:

target_link_libraries(Foo -static)

生成静态库

默认情况下, 如果不指定库类型时, 默认编译出的是动态库.

add_library(Foo ${FOO_SRC_FILES})

此时, 可以修改选项 BUILD_SHARE_LIBS:

cmake -DBUILD_SHARE_LIBS=OFF ....

安装

安装自定义的目标对象

install(TARGETS Foo
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include
    )

安装文件和目录

install(FILES data/foo.png
    DESTINATION /usr/share/icons/hicolor/256x256/apps/
    )
install(DIRECTORY docs
    DESTINATION /usr/share/doc/${PROJECT_NAME}
    FILES_MATCHING PATTERN "*.html"
    )

调试

Make

cmake 脚本运行时指定:

$ cmake -D CMAKE_VERBOSE_MAKEFILE=ON ..

编译时通过环境变量指定:

VERBOSE=1 make

Ninja

$ ninja -v -C .

CMake

如果要调试 cmake 脚本, 可以用:

  • --trace, 追踪所有脚本运行情况
  • --trace-source="file", 只追踪特定的脚本运行情况

提示

不要再使用命令

  • include_directories()
  • link_libraries()
  • link_directories()
  • add_compile_options()
  • file(GLOB ...), 如果使用了它来方便地引入整个目录, 当新加文件后, 只有重新运行 cmake 时, 才能将新文件加入到生成的 makefile 文件里.

不要再使用的全局变量

  • 不要通过修改 CMAKE_CXX_FLAGS 变量引入新的第三方库, 或者修改头文件查找路径.

Advanced

这里介绍一些关于 cmake 的高级主题.

Policy

交叉编译

cmake 有一个选项, CMAKE_TOOLCHAIN, 用于指定预定义好的交叉编译工具链, 下面是一个简单的配置,用于编译树莓派32位应用:

set(CMAKE_SYSTEM_NAME Linux)

# C/C++ compilers
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

set(CMAKE_FIND_ROOT_PATH /opt/pi4)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

使用方法也很简单,只需要在编译时指定这个工具链即可:

mdkir build
cd build
cmake -DCMAKE_TOOLCHAIN=/path/to/toolchain ..

generate expression

创建安装包 CPack

包管理

CMake 项目结构

GN 和 Ninja

参考