博客园 首页 私信博主 显示目录 隐藏目录 管理 动画

CMake 笔记

cmake 笔记


windows和C++配环境还能不能再恶心点😡
win+cmake+clangd+MSVC+vcpkg 组合一下就成了,还是得 gcc


(我都用C++20了还告诉我要兼容98,你怎么不说vector requires 也不兼容)


官网:https://cmake.org/cmake/help/latest/
https://github.com/ttroy50/cmake-examples
CMake-CookBook


CMake 命令行选项

https://cmake.org/cmake/help/latest/manual/cmake.1.html

运行cmake ..后得到 makefile,即可make生成目标。
但 win 默认使用 MSVC 的生成器,没有 makefile 不能make,需要通过 vscode 的一键运行。
win 下生成的目录文件与 Linux 下的不同,因为使用不同的生成器,需要在不同目录生成再 make。


CMake 语法

https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html

注意:

  • 一个头文件要被库或可执行文件使用,才会被算到项目里,clangd 才能识别。
  • 一个目标编译需要两部分:它使用到的头文件(通过 target_include_directory 添加寻找目录。注意是目录不是文件)、它使用到的源文件(.cc、.cpp,通过在创建时添加,或之后使用 target_sources)。
    头文件和源文件不要混。路径加不加引号随意。
    如果使用当前目录或相对于当前目录的头文件或路径,则不用添加,用#include "..."即可(只要编译器能找到目标使用的头文件,就不需要为目标设置 include_dir)。
  • CMakeLists.txt 的命令不区分大小写,但变量区分大小写(包括内置变量)。
  • 对于局部变量,在声明时可在名称前加下划线区分。

特殊变量

https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html

项目

  • CMAKE_BINARY_DIR:构建树的顶层路径。
  • CMAKE_SOURCE_DIR:源码树的顶层路径。
    当进行源内构建时,与 CMAKE_BINARY_DIR 相同。
  • CMAKE_CURRENT_BINARY_DIR:正在处理的构建目录。
    命令 add_subdirectory() 会添加一层构建目录到构建树。
  • CMAKE_CURRENT_SOURCE_DIR:正在处理的源码目录。

当运行 cmake -p 时,上述变量会被设为当前工作目录。

  • PROJECT_SOURCE_DIR:当前目录或父目录中最后一次执行 project() 的目录,即工程的根目录。子目录中执行不算。
  • CMAKE_RUNTIME_OUTPUT_DIRECTORY:可执行目标的默认输出目录。
  • RUNTIME_OUTPUT_DIRECTORY:某个可执行目标的输出目录属性。如果目标没有指定,且 CMAKE_RUNTIME_OUTPUT_DIRECTORY 在它定义时已指定,则使用 CMAKE...。
  • EXECUTABLE_OUTPUT_PATH:可执行目标的输出目录,会被目标属性覆盖。不推荐使用。
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY:库目标的默认输出目录。
  • LIBRARY_OUTPUT_PATH、ARCHIVE_OUTPUT_DIRECTORY、RUNTIME_OUTPUT_DIRECTORY:某个库目标的输出目录属性,同可执行目标。
  • LIBRARY_OUTPUT_PATH:库目标的输出目录,会被目标属性覆盖。不推荐使用。

其它:

  • PROJECT_NAME:项目名。
  • Demo_VERSION_MAJOR、Demo_VERSION_MINOR:版本号。
  • CMAKE_EXPORT_COMPILE_COMMANDS:生成 compile_command.json(但 MSVC 环境不行,仅限于 makefile 与 ninja)。

C++ 标准

# 标准
set(CMAKE_CXX_STANDARD 20)

# 设置为 on 时,如果编译器不支持指定的 C++ 标准,将报错并终止构建
# 默认为 off,CMake 会使用尽量接近的标准
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 禁用编译器的语言扩展,保证项目符合标准、可移植性强
set(CMAKE_CXX_EXTENSIONS OFF)

这些变量影响所有目标。可以通过 set_target_properties 为目标单独设置这些属性(去掉 CMAKE_ 前缀,比如 CXX_STANDARD)。

编译器

默认情况下 CMake 会自动检测和选择编译器,在 Unix 系统优先选择 gcc,在 Windows 上优先选择 MSVC。

某种语言使用的编译器会保存在 CMAKE_<LANG>_COMPLIER 中,LANG 可以是 C、CXX、Fortran 等。
因此可通过 CMAKE_CXX_COMPILER 指定 C++ 编译器:set(CMAKE_CXX_COMPILER "/path/to/compiler")
或使用命令行选项:cmake -D CMAKE_CXX_COMPILER=clang++。这样设置、不改变量还可以避免影响其它环境?
也更推荐为目标单独指定编译选项,而非全局。

其它变量:

  • CMAKE_<LANG>_COMPILER_VERSION:该语言使用的编译器版本。
  • CMAKE_<LANG>_COMPILER_LOADED:返回项目是否启用了该语言。
  • CMAKE_<LANG>_COMPILER_ID:该语言使用的编译器标识字符串。
  • CMAKE_COMPILER_IS_GNU<LANG>:判断使用的编译期是否属于 GNU 编译器集合(LANG 可以是 CC、CXX、G77)。
  • CMAKE_<LANG>_FLAGS:始终会添加的(?)编译选项。
    CMAKE_CXX_FLAGS_DEBUG、CMAKE_CXX_FLAGS_RELEASE 等为对应编译类型时的编译选项。

编译选项

CMAKE_<LANG>_FLAGS_<CONFIG> 影响项目中的所有目标的编译选项。
通过 CMAKE_<LANG>_FLAGS 查看所有?

更推荐通过 target_compile_options 为每个目标单独设置。

  • CMAKE_EXPORT_COMPILE_COMMANDS:是否在生成时输出编译命令。

构建

CMAKE_BUILD_TYPE 配置构建类型,默认为空。
可用的值有:

  • Debug:用于在没有优化的情况下,使用带有调试符号构建库或可执行文件。添加-g
  • Release:用于构建的优化的库或可执行文件,不包含调试符号。添加-O3 -DNDEBUG
  • RelWithDebInfo:用于构建较少的优化库或可执行文件,包含调试符号。添加-O2 -g -DNDEBUG
  • MinSizeRel:用于不增加目标代码大小的优化方式,来构建库或可执行文件。添加-Os -DNDEBUG

set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)

函数

  • CMAKE_CURRENT_FUNCTION:当前函数名。
  • CMAKE_CURRENT_FUNCTION_LIST_DIR:当前函数所在文件的目录?
  • CMAKE_CURRENT_FUNCTION_LIST_FILE:当前函数所在文件的路径。
  • CMAKE_CURRENT_FUNCTION_LIST_LINE

ARG 等见 函数

变量

变量

使用 ${name} 取值。在 if 等条件判断语句内时不加 $。

变量名可以含括号,但需要使用括号和引号(如:set([[var 1]] ...)set("var 1" ...)),且后续使用时空格需要转义(如:$(var\ 1))。
可以含减号、下划线。
当变量名不存在时,会将其替换为空字符串,不会产生错误。

使用变量时可以嵌套,内部变量会被替换为值用于确定外部变量。
比如:变量a的值是1,则${b${a}}等同于${b1}

变量有三种:普通变量、环境变量、缓存变量。
${} 引用普通变量或缓存变量;$ENV{} 引用环境变量;$CACHE{} 引用缓存变量。

环境变量

在变量前加ENV前缀,比如:set(ENV{CXX} "clang++")message($ENV{CXX})

当未设置时,环境变量会取系统变量。
设置环境变量后,其作用域为当前进程(还是当前会话?类似终端内的 export),不会影响系统和其它进程(或者会话?)。

在构建阶段前(即配置和生成阶段中),CMake 就会取得环境变量的值并保存(直接替换到其出现的位置?)。在这之后更改环境变量的值,都不会影响构建中使用到的值。

缓存变量

在变量前加CACHE前缀,比如:message($CACHE{CXX})

set 语法与其它变量不同:set(<name> <value> CACHE <type> <docstr> [FORCE])
set 中必须指定 CACHE。如果不指定 FORCE,则对缓存变量的修改无效。

缓存变量会被保存在 CMakeCache.txt 中?它相当于全局变量,可在同一个 CMake 工程中的任何位置使用(任意目录)。

逻辑变量

bool 变量可以用多种值表示真:1、ON、YES、Y、true、非零数。
用多种值表示假:0、OFF、NO、N、false、IGNORE、空字符串、NOTFOUND、或以 -NOTFOUND 为后缀?

if 中用if (xxx)if (NOT xxx)

set

set(<name> <value>... [PARENT_SCOPE])

定义变量。环境变量缓存变量的设置见上。
如果设置了 PARENT_SCOPE,则设置的变量会影响上层作用域。

set(<name> ${<name>}...)可以在原变量的后面添加其它元素(类似 list(APPEND)?)。

unset

unset(<name>)
set(<name>)等价。

常用

set_properties

通用的设置属性方式。
属性列表:https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html

set_target_properties

set_target_properties(target1 target2 ... PROPERTIES prop1 value1 prop2 value2 ...)

设置目标的属性,比如:

  • OUTPUT_NAME:目标文件的输出名称。默认为目标名称。
  • CXX_STANDARD:C++ 标准(是否会被全局的 CMAKE_<LANG>_STANDARD 覆盖?)。
  • CXX_STANDARD_REQUIRED:是否要求必须使用指定版本。
  • CXX_EXTENSIONS:是否开启编译器扩展。
  • COMPILE_FLAGS:编译选项。
  • RUNTIME_OUTPUT_DIRECTORY:某个可执行目标的输出目录。如果目标没有指定,且 CMAKE_RUNTIME_OUTPUT_DIRECTORY 在它定义时已指定,则使用 CMAKE...。
  • LIBRARY_OUTPUT_PATH、ARCHIVE_OUTPUT_DIRECTORY、RUNTIME_OUTPUT_DIRECTORY:某个库目标的输出目录,同可执行目标。
  • POSITION_INDEPENDENT_CODE:
  • LINK_FLAGS:链接标志。

set_target_properties(target PROPERTIES LINK_FLAGS "/DELAYLOAD:libDynamic.dll")
指示链接器启用 DLL 延迟加载机制 (delay load),允许程序在运行时按需(使用到时)加载 DLL,而非在启动时立即加载。可以降低程序启动时间,在某些情况下还可以避免因缺少 DLL 导致的程序启动失败。

get_target_property

get_target_property(<VAR> target property)

获取某个目标的属性,保存在 var 中。具体行为见文档。

target_sources

target_sources(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...]...

为目标添加要依赖的源文件。
可在 add_executable 和 add_library 创建目标后,再通过该命令添加文件。

PRIVATE、PUBLIC (都?)会传递目标的 SOURCES 属性到与该目标链接的目标,用于构建其它目标。
INTERFACE 会传播目标的 INTERFACE_SOURCES 属性。

https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/
通常来说源文件应该都是 PRIVATE 的?除了只含header 的接口库,因为它只能用 INTERFACE。

add_executable

add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source1] [source2 ...])

创建一个可执行文件目标hello。它通过编译和链接与 CML 同目录的 hello.cpp 生成。
生成的可执行文件名取决于平台,可能是<name>.exe<name>

如果设置三个选项,则会给目标分别设置同名的三个属性。

还有 Imported、Alias 目标。

target_link_directories

target_link_directories(<target> [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

声明链接器在链接该目标时,要去哪些目录寻找库。会被添加到链接选项里。
item 可以是绝对路径或相对路径。推荐前者。
PUBLIC 等控制语义同上。
如果声明了 BEFORE,则内容会被添加到相应属性的前面,而非后面。

很少用,推荐用其它命令。

include_directories

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

添加目录用于编译器寻找头文件。
路径会被添加到当前目录的 INCLUDE_DIRECTORIES 属性,以及当前 CML 中所有目标的 INCLUDE_DIRECTORIES 属性。

目录可以是绝对路径或相对路径。后者是相对于当前源码路径(CMAKE_CURRENT_SOURCE_DIR)。
添加的目录默认会被放在当前目录列表的后面,可通过 BEFORE 更改。
如果声明 SYSTEM,则告诉编译器某些平台上该目录可能是系统目录,无需警告等。

更推荐使用 target_...。

target_include_directories

target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...]...])

为特定目标添加目录(注意不是源文件),用于编译器寻找头文件。

PRIVATE、PUBLIC 会传递目标的 INCLUDE_DIRECTORIES 属性。
INTERFACE 会传播目标的 INTERFACE_INCLUDE_DIRECTORIES 属性。

通常库文件要用 PUBLIC,可执行文件就用 PRIVATE?
如果库文件是 PRIVATE,则其它目标使用该库的头文件、源文件或链接的库时,必须自己 include、target_source 或 link。

使用src/base/base.h时,如果添加了src,则#include "base/base.h";如果添加了src/base,则#include "base.h"

add_library

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

通过指定的源文件,创建库文件目标。
生成的库文件名取决于平台,可能是lib<name>.a<name>.lib

STATIC 生成静态库,编译文件的打包,用于链接到其它目标。
SHARED 生成动态库,可在运行时动态链接和加载。
MODULE 生成对象库,不能链接到其它目标,但可在运行时通过 dlopen 等加载。
如果不给出,则根据 BUILD_SHARED_LIBS 为 OFF 还是 ON 选择 STATIC 或 SHARED。
如果没有导出任何符号,则库不能被设为 STATIC。

SHRAED 和 MODULE 会被自动设置 POSITION_INDEPENDENT_CODE。

还有 Object、Interface、Imported、Alias 库。

Interface Libraries

add_library(<name> INTERFACE)
add_library(<name> INTERFACE [<source>...] [EXCLUDE_FROM_ALL])

接口库不编译和依赖源文件,不产生一个实体库文件,但可用于安装和导出属性到链接它的目标。

INTERFACE 库似乎只是导出了自己的 include_dir 属性(以及 interface_link_lib、interface_link_options 等),从而允许链接它的库找到这些头文件。
因此接口库可用来定义 header-only。但如果库的 include_dir 本身就包含这些 header-only file,就不需要一个 INTERFACE 库。
当 header-only 依赖其它库时,还是需要定义它并链接,可能是给 include_dir 目录下的所有头文件链接?
其它 - 使用 header-only

add_custom_target

创建一个目标,该目标会被构建但不会输出。

find_library

find_library(lib_path libName.lib PATHS "${CMAKE_SOURCE_DIR}/lib/static")

在指定路径中查找库文件,将其路径保存到指定变量 lib_path。

link_libraries

更推荐使用 target_...。

target_link_libraries

target_link_libraries(<target> <item>...)

将指定库链接到指定目标。target 必须是 add_executable 和 add_library 创建的非 ALIAS 目标。

item 可以是:

  • 库文件目标名。
  • 到库文件的完整路径。
  • 一个库名,会要求链接器在链接时寻找并链接。比如:foo会变成-lfoo
  • 链接标记,以-开头,会用于链接器。更推荐 target_link_options?
  • 生成器表达式,其求值为上述各类,或以分号分割的上述各类。

库的依赖性是可传递的(即 PUBLIC?)。即链接 target 的 item,在 target 链接到其它库时,item 也会链接到它们。

target_link_libraries(<target> <PUBLIC|PRIVATE|INTERFACE> <item>... [<PUBLIC|PRIVATE|INTERFACE> <item>...])

PUBLIC、PRIVATE、INTERFACE 控制链接依赖。
PRIVATE 表示库仅对当前目标可见,不会影响链接到 target 的其它目标。
PUBLIC 与此相反,表示链接到 target 的其它目标也依赖这些库、也能使用它们的接口。且在构建时必须本地有这份库?
INTERFACE 表示库会被添加到链接接口,给其它依赖当前库的库使用,但不会用来链接当前 target。比如 header-only 库。

链接动态库时,要链接对应的导入库 .lib 而非直接链接 .dll?
导入库包含了调用动态库函数所需的信息,编译器和链接器需要这些信息来正确生成可执行文件。运行时,可执行文件会自动加载相应的 .dll。

target_link_options

add_link_options

target_compile_options

target_compile_options(<target> [PRIVATE] ...)

为特定的目标添加编译选项。
也可以修改 CMAKE_<LANG>_FLAGS_ 变量,将对项目中的所有目标生效,但更推荐为每个目标单独设置。
也可以修改目标的 COMPILE_FLAGS 属性?区别是?

PRIVATE 表示编译选项只对当前目标生效,不会影响依赖于 target 的其它目标。
PUBLIC 表示编译选项会被传给依赖 target 的目标。
INTERFACE 表示给定的编译选项只用来传递给与依赖 target 的目标。

# 可用列表保存选项
list(APPEND flags "-fPIC" "-Wall")
if(NOT WIN32)
	list(APPEND flags "-Wextra" "-Wpedantic")
endif()
target_compile_options(math flags)

# 或用宏
macro(add_msvc_options target)
  if(MSVC)
    target_compile_options(${target} PRIVATE
        /W4                # 设置警告级别为 4
        /WX                # 将警告视为错误
        /MP                # 启用多处理器编译
        /permissive-       # 禁用不严格的语言 conformance
        /Zc:__cplusplus    # 启用正确的 __cplusplus 宏值
        /Zc:inline         # 移除未使用的函数
        /EHsc              # 指定异常处理模型
    )
  endif()
endmacro()
add_msvc_options(math)

MSVC 编译选项:

  • /arch:...:编译器使用的指令集。
    比如:/arch:SSE2/arch:AVX2

add_compile_options

add_compile_options(<option>...)

添加编译选项到目录的 COMPILE_OPTIONS 属性,影响所有目标。
选项只用于编译,不用于链接(需要 add_link_options)。

add_definitions

add_definitions(-DMACRO_NAME)

添加宏定义。-D 后加宏名。
可在代码中使用,比如:add_definitions(-DDEBUG)#ifdef DEBUG

target_compile_definitions

target_compile_definitions(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...]...

为特定目标设置编译选项。

一个 item 可以是:FOO添加宏定义;-DFOO移除宏定义 FOO(注意空格是分隔符,不要多加);FOO=1为宏定义设置值。

add_subdirectory

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])

在指定的子目录中查找 CML 并执行,让 CMake 构建系统继续构建子项目。
可以将一个大型项目分解为多个子项目,使项目的组织结构更清晰。

binary_dir 为生成文件的路径。默认为 source_dir。
如果指定 EXCLUDE_FROM_ALL,则子目录里的目标不会被父目录的 ALL 目标所包含,且会从 IDE 项目文件中排除(用户只能在子目录中手动编译目标)。常用于子目录包含一些有用但非必须的东西,独立于源工程,比如一些 example。这种情况下通常子目录也有个单独的 project() 来构建一个新的系统。

子项目可以包含它自己的源文件、库、可执行文件等,并可以与主项目共享变量、目标和属性。

该命令会引入新的目录作用域(directory scope),其执行的 CML 中修改或定义的变量仅对当前作用域生效,不会影响父目录(除了缓存变量,是全局的)。在进入子目录前,会对父目录的变量做一份拷贝使用。

include

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>])

引用官方和 CMake 社区中已经配置好的 CMake 模板。
CMake 模板就是保存在 .cmake 中的写好 CMake 代码,与 #include 类似。
通过cmake --help-module <module_name>查看其手册。

若不填 OPTIONAL,则文件不存在时会给出错误。
若给出 RESULT_VARIABLE,则会在操作成功时将文件的路径写入到指定变量中,在操作失败时写入 NOTFOUND。

引入模板不会引入新的作用域,对文件中变量的修改会影响当前作用域。

option

option(<op_name> "desc" <OFF|ON>)

定义用户可配置的选项 opName,默认值为 OFF 或 ON,是一个 bool 变量。
用户可以通过 CMake 命令行参数中的 -D 或 GUI 来改变选项的值,比如:cmake -D op_name=OFF ..

desc 是对可配置选项的描述,会在使用 CMake GUI 时提示(但命令行界面不会显示)。

调试

需要指定 BUILD_TYPE 为 Debug,并添加 -g 选项:

set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

基本

aux_source_directory

aux_source_directory(<dir> <var>)

将指定目录的所有源文件路径(.c、.cc、.cpp)保存在列表 var 中(不会递归查找)。

常用于引入保存在 Templates 等目录下的显式实例化模板文件。
不推荐使用,因为用于一般文件时,当新的源文件被添加时,CMake 生成的构建系统没法知道,需要清除缓存再手动重新运行 CMake 生成。而如果是依次列出文件,则添加文件需要修改 CML,从而能自动触发重新生成。这与 file(GLOB) 一样。
构建树只有在 CML 发生改变时才会更新,单纯的增删文件 GLOB 无法识别。因此当不清空缓存重新构建时,新的文件不会被记录。只推荐依次列出文件。

如果要添加特定文件(筛选)或递归添加,需要 file。

file

以一种与系统无关的方式读取、写入和传输文件,并使用文件系统、文件锁、路径和存档。

file (filesystem)

file(GLOB <variable> [LIST_DIRECTORIES true|false] [RELATIVE <path>] [CONFIGURE_DEPENDS] [<globbing-expressions>...])
# 递归获取
file(GLOB_RECURSE <variable> [FOLLOW_SYMLINKS] [LIST_DIRECTORIES true|false] [RELATIVE <path>] [CONFIGURE_DEPENDS] [<globbing-expressions>...])

获取匹配 glob-expr 的文件名,保存到 variable。glob-expr 与正则类似,但更简单。
如果声明 RELATIVE,则返回相对指定路径的相对路径,而非绝对路径。
结果会按字典序排序。

比如:递归添加 src 目录下的所有 .cc:file(GLOB_RECURSE SOURCE "src/*.cc")

不推荐使用,原因与 aux_source_directory 一致。

message

message([<mode>] "msg"...)

打印文本。
可以选择 mode 控制输出后的行为(默认为 NOTICE)。常用 mode:

  • FATAL_ERROR:停止处理和生成。
  • SEND_ERROR:继续处理,但跳过生成。
  • WARNING:警告并继续处理。
  • NOTICE:重要信息,输出到 stderr。
  • STATUS:用于输出简洁的普通信息。
  • VERBOSE:用于输出详细的次要信息。
  • DEBUG:用于输出一般不重要的详细信息。

STATUS 会输出到 stdout,其它会输出到 stderr。

project

project(proName VERSION 1.0.0 LANGUAGES CXX C)

定义项目名称、版本(可选)、支持的语言(可选,默认是 CXX 即 C++)。

if

if(CONDITION)
  # ...
elseif(NOT OTHER_CONDITION)
  # ...
else()
  # ...
endif()

判断字符串相等:if (PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)

foreach

下标循环:
min 和 step 可选,默认分别为 0 和 1。min max 必须都是非负整数。

foreach(<loop_var> RANGE [<min>] <max> [<step>])
	# ...
endforeach()

列表循环:
依次从将 loop_var 设置为所有 list 中的元素和所有 item。可以只提供一种,也可都提供。

foreach(<loop_var> IN [LISTS <list>...] [ITEMS <item>...])
	message(${loop_var})
	# ...
endforeach()

列表循环:
依次将 loop_var 设置为所有 list 中的元素和所有 item。可以只提供一种,也可都提供。

foreach(<loop_var> IN [LISTS <list>...] [ITEMS <item>...])
  # ...
endforeach()

# 例
set(list 1 2 3)
foreach(var IN LISTS list ITEMS e f)
	message(${var}) # 1 2 3 e f
endforeach()

压缩列表循环:
设提供的 list 为 list0, ..., listn,每次循环创建变量 loop_var_0 代表 list1[i]、...、变量 loop_var_n 代表 listn[i]。
允许每次迭代同时使用每个列表该下标的元素。当提供的列表不等长时,如果列表不含该下标则不创建该变量。
变量名 loop_var 可以声明多个,将依次对应到每个列表,不使用前缀+下标。

foreach(<loop_var>... IN ZIP_LISTS <list>...)
  # ...
endforeach()

# 例
set(L1 "one;two;three;four")
set(L2 "1;2;3;4;5")

foreach(num IN ZIP_LISTS L1 L2)
	message("num_0=${num_0}, num_1=${num_1}")
endforeach()
 
foreach(word num IN ZIP_LISTS L1 L2)
	message("word=${word}, num=${num}")
endforeach()

while

while(CONDITION)
	# ...
endwhile()

function

function(<func_name> [arg...])
  message(${arg1})
  # ...
endfunction()

函数参数和内部定义的变量,作用域只限于函数内(函数作用域,function scope),即使它与外部变量重名,也不会影响外部变量。

参数可以通过特殊的变量名来访问:

  • ${ARGC}:参数数量。
  • ${ARGV}:所有参数的列表。
  • ${ARG0}、${ARG1}...:每个参数。传入参数的数量可以多于函数定义中的形参数量。
  • ${ARGN}:当实参数量多于形参时,保存了超出形参的那些匿名参数的列表。

macro

macro(<macro_name> [arg...])
  message(${arg1})
  # ...
endmacro()

宏是替换,不会像函数一样新建一个作用域。在宏中调用 return() 会在当前作用域直接返回。

宏相当于将文本嵌入进使用位置,并用变量值替换使用变量的地方。
因此当宏参数与外部变量名同名时,在宏内使用的值是调用宏之前的值;在宏内修改它的值,会在宏之外影响外部变量的值。

list

用分号分割的对象是列表。list 提供了对 list 的各操作,也可以定义列表。
set(<var> <value>...)也能定义列表。

常用:

Reading
  list(LENGTH <list> <out-var>)
  list(GET <list> <element index> [<index> ...] <out-var>)
  list(SUBLIST <list> <begin> <length> <out-var>)

Search
  list(FIND <list> <value> <out-var>)

Modification
  list(APPEND <list> [<element>...])
  list(INSERT <list> <index> [<element>...])
  list(POP_BACK <list> [<out-var>...])
  list(POP_FRONT <list> [<out-var>...])
  list(PREPEND <list> [<element>...])
  list(REMOVE_ITEM <list> <value>...)
  list(REMOVE_AT <list> <index>...)

Ordering
  list(REVERSE <list>)
  list(SORT <list> [...])

如果插入等操作的 list 不存在,则定义一个并插入。

测试

产生 makefile 后,可通过make test执行。

需要在项目根目录通过enable_testing()启用测试。
启用后,不能有文件名叫 test,包括 test.cpp。

add_test

add_test(NAME <name> COMMAND <command> [<arg>...]
         [CONFIGURATIONS <config>...]
         [WORKING_DIRECTORY <dir>]
         [COMMAND_EXPAND_LISTS])

name 定义该测试的名字。
command 为要运行的命令。如果为可执行目标,则会调用它。通过紧跟 arg 传命令行参数。
WORKING_DIRECTORY 设置测试要在哪个目录执行。默认为 CMAKE_CURRENT_BINARY_DIR。也可通过 set_tests_properties() 设置。

add_test(<name>)也可创建,需要通过 set_tests_properties 设置其属性。
只能在测试被创建的那个目录设置。

add_test(<name> <command> [<arg>...])是更旧、不推荐的方式。

set_tests_properties

set_tests_properties(test1... PROPERTIES prop1 value1 prop2 value2)

为指定测试设置属性。如果测试不存在则报错。
测试相关的属性:https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#test-properties

常用属性:

  • PASS_REGULAR_EXPRESSION:要求测试必须符合列出的正则表达式列表中的一个,否则测试失败。
    值用字符串声明。

可以通过宏定义测试,比如:

# 定义一个宏,用来简化测试工作
macro (do_test arg1 arg2 result)
  add_test (test_${arg1}_${arg2} Demo ${arg1} ${arg2})
  set_tests_properties (test_${arg1}_${arg2}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)
 
# 使用该宏进行一系列的数据测试
do_test (5 2 "is 25")
do_test (10 5 "is 100000")
do_test (2 10 "is 1024")

安装

产生 makefile 后,可通过make install执行。

通过 cpack 还可以生成二进制或源码安装包。

install

指定安装规则,定义在构建完成后如何将目标文件(可执行文件、库等)以及相关文件(如动态库、配置文件等)安装到指定目录(其实就是复制过去?)。
对于将构建好的项目打包成安装包、或将项目部署到目标系统上非常有用。

常用的有:

  • install(TARGETS <target>... RUNTIME [<artifact-option>...] [<artifact-kind> <artifact-option>...]...)

其中<artifact-option>主要包括:

[DESTINATION <dir>]
[CONFIGURATIONS <config>...]
[COMPONENT <component>]
[OPTIONAL] [EXCLUDE_FROM_ALL]

DESTINATION path 指定安装到的目录,比如 bin。

第一组 option 应用到目标的 Output Artifacts,后面的 option 应用到指定的 artifact kind。

常用例子:

  • install(TARGETS <target> DESTINATION <path>
    install(FILES <file> DESTINATION <path>

file 为文件路径,比如:"${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll"
当使用绝对路径时,CMake 会在前面附加 CMAKE_INSTALL_PREFIX 形成完整路径(默认为 /usr/local),比如:bin会被生成到/usr/local/bin

其它

find_package

为了让某个外部库能被找到,推荐使用命令行传递-DCMAKE_PREFIX_PATH="path/to/...",或设置环境变量fmt_DIR="path/to/...",以避免硬编码(set(fmt_DIR ...))、影响可移植性。最好还是用包管理器。

configure_file

configure_file(<input> <output> [@ONLY]

根据模板文件 input 生成文件 output,并将其中@VAR@${VAR}形式的字符串替换为变量值。可用于源码。
比如:configure_file(config.h.in config.h @ONLY)

@ONLY 表示只替换@VARIABLE_NAME@形式的占位符。如果不加也会替换 ${VARIABLE_NAME}形式的占位符。

CMake 会将模板文件中的变量替换为其实际值。如果不存在则替换为空字符串。
如果变量是 bool,则根据是 ON/OFF 替换为#define xxx 0/1
如果变量是字符串,则被替换为字符串值,不带引号。
注意大小写。因为宏一般大写,所以这部分参数命名最好也大写。

比如:

// config.h.in
#define ENABLE_FEATURE_X @ENABLE_FEATURE_X@

// CMakeList.txt
// 可以在命令行更改
option(ENABLE_FEATURE_X "Enable feature X" OFF)
configure_file(config.h.in config.h @ONLY)

// 生成的 config.h
#define ENABLE_FEATURE_X 0

模板中还可以使用#cmakedefine xxx [some...],如果定义了非零值则替换为#define xxx [some...],否则不定义它。
#cmakedefine01 xxx则要么是#define xxx 0,要么是#define xxx 1

execute_process

运行其它进程,并收集它们的输出。


其它

基本使用

配置

添加 cmake 运行但不调试的快捷键(比如 F9)。

vscode 使用

  • F1 - cmake: quick start 创建 cmake 项目,选择编译器。

  • 然后就可以在右侧 cmake 菜单 - 大纲 - 右击项目名,生成,就可以运行或调试了(下面状态栏也有运行/调试快捷键)。

    如果要用 Intelligence 的调试,则在生成的 launch.json 中配置 program 为${workspaceFolder}/build/projectName.exe,miDebuggerPath 为 gdb 所在路径(mingw/bin 下)。

一般更新保存 CMakeList 就会触发重新编译,如果没有,在 cmake 菜单点配置所有项目。

也可命令行直接运行,但是要自行添加configureSettings配置中的选项。
注意更换生成器时清理缓存!(CMakeFiles 和 CMakeCache.txt)。

linux:

  • 更改 CMakeList,cmake ..
  • 在 build 下make,然后找到可执行文件运行(可能在 build/Debug 下)。

windows:

  • cmake .. -G "Unix Makefiles"。默认不会生成 makefile,而是 .sln 用于 MSVC。
  • make

注意,想要 makefile,cmake 时必须指定 -G "MinGW Makefiles" 等(或是在 setting.json 里构建方式使用 ninja?),否则还是用 MSVC 生成器,指定编译器 CMAKE_CXX_COMPILER 也没用。不声明该值也会自动寻找编译器。
在处理 CML 前,cmake 就会指定好生成器,因此只能通过命令行修改。

生成 complie_commands.json:cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1
或设置变量:set(CMAKE_EXPORT_COMPILE_COMMANDS ON)(如果还不行,用set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL ""))。

注意仅限于 Makefile 和 Ninja 生成器。MSVC 默认的不行,除非用命令行指定使用-G "Unix Makefiles"生成器。。注意新建目录或清除缓存。

某些应用

使用 header-only

问题:仅包含 .h 或 .hpp、不含源文件的头文件,无法出现在 compile_commands 内,从而 clangd 无法识别它的依赖、不能引用非标准库(否则会 not found)。
linux/wsl 下可以用 compdb;windows 只能在 wsl 下生成,然后手动改。

想要使用 header-only file,直接 include 它所在的目录就行了。如果它依赖某些库,就创建接口库链接。

add_library(header_only INTERFACE)
# target_include_directories(header_only INTERFACE ${CMAKE_SOURCE_DIR}/src/header_only) # 非必需。。
target_link_libraries(header_only INTERFACE fmt::fmt)

主要问题是:由于 .h 头文件通常不会被当做一个编译单元、不能作为源文件,不会被直接编译,所以它们不会出现在 compile_commands 里,从而不会被 clangd 识别,不会正确进行提示,从而在引用非标准头文件时出现提示错误(即使 cmake 可正常运行)。
主要是 cmake 无法确定头文件要使用什么编译选项,因为这取决于包含它的那些编译单元。
虽然 .hpp 似乎可以作为源文件,但也不会出现在里面 hpp 也算头文件)

这里说能通过 VERIFY_INTERFACE_HEADER_SETS 编译头文件,使其出现在 compile_commands 里,但像这里所说好像并不行。。

这里说要用 compdb 等工具做一个后处理,添加头文件生成新的 compile_commands,但是如 issues 所说它不支持 Windows。。
可以对照它 linux 或 wsl 下生成的文件,然后修改使其能在 Windows 下可用(可行,主要是修改编译器路径、文件路径,把 -I -i 改为@CMakeFiles/xxx.dir/includes_CXX.rsp)。

FetchContent

自动下载并配置 github 上的第三方库。


vcpkg

windows

安装

安装:https://vcpkg.io/en/getting-started(默认不会添加到环境变量)
然后vcpkg install xxx安装库。

注意

注意,install 时可以--triplet x64-mingw-static指定三元组,默认是 x64-windows。不同的 triplet 是不一样的包,CML 中指定的 VCPKG_TARGET_TRIPLET 必须已安装。
使用cmake -G "MinGW Makefiles"时,必须使用 x64-mingw-static 而非 x64-windows。

在 vscode 中使用

使用新配置时,注意删除缓存!(项目状态 - 输出缓存并重新配置)

如果与 cmake 使用,则在 setting 里添加:
(或者像 vcpkg 官网所说运行 cmake 时加参数,也可在 CMakeList 里定义 CMAKE_TOOLCHAIN_FILE)

"cmake.configureSettings": { // 运行时附加的命令行参数
    "CMAKE_TOOLCHAIN_FILE": "vcpkg安装目录\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake",
    "VCPKG_TARGET_TRIPLET": "x64-windows" // 构建平台。注意 gcc(mingw) 要用 x64-mingw-static
}

然后将安装完成后提示的find_package... target_link_libraries...放到 CMakeLists 后面(里面的 main 要改成项目名)。

问题:对于 C++ 插件的 Intelligence,没有代码提示和补全。似乎只能在 c_cpp_properties 的 includePath 加"E:\\Runtime\\vcpkg\\packages\\fmt_x64-windows\\include"(如果没有,快速修复 - 编辑 includePath 生成)。
(好像可以设置使用 cmake 进行配置,不管了反正不用它,遇到模板就死了)

直接使用 / 在 CML 中添加

在 CML 中添加:

# 需要在 project() 前设置。如果用 vscode 一键运行,则在 setting 里添加即可
# 如果不行,注意删缓存重新配置!
set(CMAKE_TOOLCHAIN_FILE "E:\\Runtime\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake")
set(VCPKG_TARGET_TRIPLET "x64-windows")

linux 及 wsl

就不需要 vcpkg 了,直接安装即可。
如果 wsl 要用 win 通过 vcpkg 安装的库,注意路径是 /mnt/e 不是 E:/...。也可以直接设置 fmt_DIR。

# 设置 fmt_DIR 为包含 fmt-config.cmake 的路径
set(fmt_DIR 
"/mnt/e/Runtime/vcpkg/installed/x64-windows/share/fmt")
posted @ 2024-01-31 14:50  SovietPower  阅读(70)  评论(0编辑  收藏  举报