CLI

作为一个跨平台的构建工具, cmake 的执行分成了两个阶段:

  • 根据 CMakeLists.txt 生成平台特定的构建脚本,比如在 Linux 上生成 Make 或者 Ninja
  • 运行生成的构建脚本完成构建

这也就导致,cmake 有两种使用方式:

生成构建脚本

# 指定 CMakeLists.txt 的位置和构建脚本的生成位置
cmake [options] -S <source_directory> -B <build_directory>

此外还附加一些 options:

  • -D<var>:<type>=<value>: 设置 CMake 变量
  • -G <generator-name>:设置具体的构建器,比如 Ninja

执行构建脚本

cmake --build <build_directory> [options]

常用的 options 有:

  • --target <name>:构建特定目标,类似于 make target
  • -j [<jobs>]:构建的并行度,类似于 make -j8
  • --clean-first: 在构建之前先执行清理操作。

此外构建完之后,可能还有安装流程,命令如下:

cmake --install <build_directory> [options]

Grammer

Command

所有 CMake 代码都由命令构成。一个命令的基本结构是:

command_name(argument1 argument2 ...)
  • command_name 是大小写不敏感的:project() 和 PROJECT() 效果完全一样。但约定俗成使用小写
  • argument 是大小写敏感的:MyProject 和 myproject 是不同的。这尤其在处理文件名和变量名时很重要。

Variable

设置变量:

# 设置一个普通变量
set(PROJECT_VERSION "1.0.0")
 
# 设置一个包含多个值的变量(本质上是一个分号分隔的字符串,称为 "list")
set(SOURCES main.cpp utils.cpp logger.cpp)

使用变量:

# 使用上面设置的变量
project(MyApp VERSION ${PROJECT_VERSION})
add_executable(my_app ${SOURCES})
 
# 环境变量
message("System Path is: $ENV{PATH}")

Control Flow

条件:

if(CONDITION)
  # do something
elseif(OTHER_CONDITION)
  # do something else
else()
  # do something as a fallback
endif()

循环:

set(MY_SOURCES a.cpp b.cpp c.cpp)
 
foreach(SOURCE_FILE IN LISTS MY_SOURCES)
  # message() 命令会打印信息到终端
  message("Found source file: ${SOURCE_FILE}")
endforeach()

Function

# 定义一个函数,用于为目标设置通用的编译器警告
function(set_common_warnings TARGET_NAME)
  target_compile_options(${TARGET_NAME} PRIVATE
      -Wall
      -Wextra
      -Wpedantic
  )
endfunction()
 
# 调用函数
add_executable(app1 main1.cpp)
set_common_warnings(app1) # 为 app1 添加警告
 
add_library(lib1 lib1.cpp)
set_common_warnings(lib1) # 为 lib1 添加警告

Target

Overview

为了让项目的构建变得井然有序,cmake 抽象出了 target 这个概念来描述构建。

每个 target 都是一个构建单元,比如说可执行文件, object 文件,库文件,都可以视为一个 target 。当然,一些抽象的东西,比如一个 echo 命令,或者头文件,也可以视为一个 target

一些 target,是需要其他一些 target 的。比如说一个可执行文件,依赖于一个库文件。可执行文件是一个 target,库文件同样是一个 target。cmake 就是靠着这种 target 之间的联系,组织起了一个复杂的构建系统。

其实这个概念在 GNU Make 中也有体现,只不过没有 cmake 演化得这么全面。BTW,这个东西在 Scons 中被叫作 Node

Types

target 根据创建的形式不同,有不同的类型。

我们自己写的可执行文件:

add_executable(my_app main.cpp math.cpp)

我们自己写的库文件:

# 静态库
add_library(math_lib STATIC math.cpp)
# 动态库
add_library(math_lib SHARED math.cpp)
 
# 公开 API 的头文件,方便库的调用者使用
target_include_directories(math_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

还有一种特殊的库文件,它只有头文件,并没有对应的库实现,这种存在主要是在开发上,传递“接口”:

add_library(my_header_lib INTERFACE)
# 告诉 CMake,任何链接了 my_header_lib 的 Target, 
# 都需要自动添加这个头文件搜索路径。 
 
target_include_directories(my_header_lib
	INTERFACE
	${CMAKE_CURRENT_SOURCE_DIR}/include
)

自定义的命令:

# 创建一个名为 "docs" 的 Target,当构建它时,会执行 Doxygen 命令
add_custom_target(docs
    COMMAND doxygen ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMENT "Generating API documentation with Doxygen"
)

第三方的库文件:

如果这个第三方库文件已经在系统中安装了:

# 查找 Boost 库,要求版本至少为 1.71,并且需要 filesystem 和 system 这两个组件
# REQUIRED 关键字表示如果找不到,CMake 将会报错并停止 
find_package(Boost 1.71 REQUIRED COMPONENTS filesystem system)

这同样会创建一个名为 Boost 的 target,我们可以这样使用:

target_link_libraries(my_app PRIVATE Boost::filesystem Boost::system)

如果第三方库需要从网络上下载,或者指定特定的版本:

# include 是一个 cmake 命令,表示启动 cmake 的 FetchContent 功能
include(FetchContent) 
FetchContent_Declare( 
	fmt # 给这个依赖起个名字 
	GIT_REPOSITORY https://github.com/fmtlib/fmt.git 
	GIT_TAG 10.2.1 # 指定一个确切的版本 
)
# 下载
FetchContent_MakeAvailable(fmt)

看到最后我们会疑惑,我们的 .o 文件呢?为什么没有一个专门的 target 用于生成 Object 文件。仔细思考一下就会发现,Object 文件只是从“编译-链接”角度才会产生的概念,而从“构建”的角度去看,只会包括最终可以运行的“可执行文件”和辅助可执行文件的“库文件”。实际上,在我们执行如下命令的时候:

add_executable(my_app main.cpp math.cpp)

就包含了生成 main.omath.o 并最终被链接成 my_app 的逻辑了。

Properties

target 具有一系列属性,比如说:

  • 它由哪些源文件编译而来?add_executable(target ...), add_library(target ...)
  • 它需要包含哪些头文件目录target_include_directories(target ...)
  • 它需要链接哪些其他的库target_link_libraries(target ...)
  • 它需要哪些编译选项target_compile_options(target ...)
  • 它需要哪些预处理宏定义target_compile_definitions(target ...)

target 的属性有 3 种类型,这些类型决定了它们自己的属性,是否要“传播给”依赖于他们的 target:

  • PRIVATE:属性仅应用于当前 Target,不会传递给任何依赖它的消费者
  • PUBLIC:属性既应用于当前 Target,也会传递给依赖它的消费者。
  • INTERFACE:属性仅传递给消费者,不应用于当前 Target 本身。