CLI
作为一个跨平台的构建工具, cmake 的执行分成了两个阶段:
这也就导致,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.o 和 math.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 本身。