CMake

CMake 是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个函数库。
CMake 通过使用简单的配置文件 CMakeLists.txt,自动生成不同平台的构建文件(如 Makefile、Ninja 构建文件、Visual Studio 工程文件等),简化了项目的编译和构建过程。
CMake 本身不是构建工具,而是生成构建系统的工具,它生成的构建系统可以使用不同的编译器和工具链。
image-20250710190457.webp

  • 跨平台支持: CMake 支持多种操作系统和编译器,使得同一份构建配置可以在不同的环境中使用。
  • 简化配置: 通过 CMakeLists.txt 文件,用户可以定义项目结构、依赖项、编译选项等,无需手动编写复杂的构建脚本。
  • 自动化构建: CMake 能够自动检测系统上的库和工具,减少手动配置的工作量。
  • 灵活性: 支持多种构建类型和配置(如 Debug、Release),并允许用户自定义构建选项和模块。
    基本工作流程:
  1. 编写 CMakeLists.txt 文件: 定义项目的构建规则和依赖关系。
  2. 生成构建文件: 使用 CMake 生成适合当前平台的构建系统文件(例如 Makefile、Visual Studio 工程文件)。
  3. 执行构建: 使用生成的构建系统文件(如 makeninjamsbuild)来编译项目。

安装

各架构安装方法如下:

sudo apt-get install cmake
sudo dnf install cmake
sudo pacman -S cmake
brew install cmake

从源码安装的方法这里略,可以看参考文献 1

基础使用

CMakeLists.txt 是 CMake 的配置文件,用于定义项目的构建规则、依赖关系、编译选项等。

一个工程里可以有多个 CMakeLists. txt 文件,例如 LLVM,可能每个 Cpp 文件夹都有一个。

最简单的实例:

cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)
# 添加源文件(多个)
add_executable(MyExecutable main.cpp other.cpp)
# 链接目标文件与其他库:
target_link_libraries(MyExecutable MyLibrary)
# 添加其他路径(头文件路径)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 设置目标属性
target_include_directories(MyExecutable PRIVATE ${PROJECT_SOURCE_DIR}/include)
# 安装规则
install(TARGETS MyExecutable RUNTIME DESTINATION bin)
#条件语句 elseif, else(),
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  message("Debug build")
endif()
# 自定义命令
add_custom_command(
   TARGET MyExecutable POST_BUILD
   COMMAND ${CMAKE_COMMAND} -E echo "Build completed."
)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)

CMake 使用变量来存储和传递信息,这些变量可以在 CMakeLists.txt 文件中定义和使用。变量可以分为普通变量和缓存变量。
缓存变量存储在 CMake 的缓存文件中,用户可以在 CMake 配置时修改这些值。缓存变量通常用于用户输入的设置,==例如编译选项和路径==。

  • 普通变量:定义:set(MY_VAR "Hello World") 使用 :message(STATUS "Variable MY_VAR is ${MY_VAR}")
  • 缓存变量:定义:set(MY_CACHE_VAR "DefaultValue" CACHE STRING "A cache variable")

CMake 可以通过 find_package() 指令自动检测和配置外部库和包。常用于查找系统安装的库或第三方库。

  • find_package(Boost 1.70 REQUIRED):指定库版本
  • find_package(OpenCV REQUIRED PATHS /path/to/opencv): 查找库并制定路径
  • 使用查找到的库:target_link_libraries(MyExecutable Boost::Boost)
  • 设置包含目录和链接目录:include_directories(${Boost_INCLUDE_DIRS}) link_directories(${Boost_LIBRARY_DIRS})
    image-20250710190514.webp

    构建方法

  • make 或 ninja 均可,例如先cmake -G “Ninja” .. 然后 ninja
  • 可以使用 ninja XXX 指定编译目标
  • make clean/ninja clean

    特殊语法

    容器 List

  • 列表在 Cmake 的存储形式:set(MYLIST foo bar baz) 等价于 set(MYLIST "foo;bar;baz") 例如:cmake ../llvm -DLLVM_ENABLE_PROJECTS="clang;lld;bolt"
  • List 的具体语法:
list(APPEND \ …) 在列表末尾追加元素
list(PREPEND \ …) 在列表开头插入元素
list(INSERT \ i …) 在索引 i 处插入
list(LENGTH \ \) 计算元素个数,结果放
list(GET \ i out) 取第 i 个元素,放入 out
==list(FIND \ val idx)== 在列表中查找 val,返回索引 idx
list(REMOVE_ITEM \ …) 删除所有值等于给定项的元素
list(REMOVE_AT \ …) 删除指定索引的元素
list(REMOVE_DUPLICATES \) 去重
list(REVERSE \) 反转列表
list(JOIN \ sep out) 用 sep 把列表拼回字符串,结果放 out
  • 遍历方式:
    foreach(proj IN LISTS LLVM_ENABLE_PROJECTS)
      message(STATUS "Will build subproject: ${proj}")
    endforeach()
  • 查找元素是否存在:
    list(FIND LLVM_ENABLE_PROJECTS "clang" clang_idx)
    if(clang_idx GREATER -1)
      message(STATUS "Include Clang frontend")
    endif()
    实例:LLVM 项目里如何解析 LLVM_ENABLE_PROJECTS
    # 顶层 CMakeLists.txt 中
    set(LLVM_ENABLE_PROJECTS "clang;lld;bolt" CACHE STRING
        "List of LLVM sub-projects to build")
    
    # 然后在合适的位置做:
    foreach(subproj IN LISTS LLVM_ENABLE_PROJECTS)
      # 确保目录存在
      if(EXISTS "${CMAKE_SOURCE_DIR}/${subproj}/CMakeLists.txt")
        add_subdirectory(${subproj})
      else()
        message(FATAL_ERROR "Requested project '${subproj}' not found")
      endif()
    endforeach()

    字符串 STRING

  • 在 CMake 里,所有变量都是字符串,即便你觉得它是数值或列表,==底层依然以字符串形式存储。==
  • 访问与展开:
    set(MSG "Build type: ${CMAKE_BUILD_TYPE}")
    message(STATUS "${MSG}")
  • STRING 命令全解:
    string(LENGTH <input> <out_var>)           # 计算长度
    string(SUBSTRING <input> <start> <len> <out_var>)  # 截取子串
    string(FIND <input> <substr> <out_var> [<start_index>])  # 查找位置
    string(REPLACE <match> <replace> <input> <out_var>)     # 直接替换
    string(REGEX REPLACE <regex> <replace> <input> <out_var>) # 正则替换
    string(REGEX MATCH <regex> <input> <out_var>)            # 匹配并提取
    string(TOUPPER <input> <out_var>)         # 转大写
    string(TOLOWER <input> <out_var>)         # 转小写
    string(STRIP <input> <out_var>)           # 去除首尾空白
    string(JOIN <sep> <out_var> <list>...)    # 拼接列表(注意参数差异)
    string(RANDOM <length> <out_var>)         # 随机字符串
    string(TIMESTAMP <out_var> [format] [UTC])# 当前时间戳,支持格式化
    使用方法:
  • 截取与查找:
    set(FULL "clang-format-12.0.1")
    string(FIND "${FULL}" "-" dash_pos)                # dash_pos = 11
    math(EXPR version_start "${dash_pos} + 1")
    string(SUBSTRING "${FULL}" ${version_start} -1 ver) # ver = "12.0.1"
  • 正则与替换
    set(TEXT "foo123bar456")
    # 将所有数字替换成 “#”
    string(REGEX REPLACE "[0-9]+" "#" result "${TEXT}")
    # result => foo#bar#

    if else

    基本语法:
    if(<condition>)
      # 条件为真时执行
    elseif(<condition2>)
      # 前面的条件都不满足且 condition2 为真时执行
    else()
      # 上述条件都不满足时执行
    endif()
    判断条件:
条件类型 语法 / 示例 说明
布尔常量 if(TRUE)if(ON)if(FALSE) 等价于布尔值,区分大小写
字符串非空 if(MY_VAR) 只要变量非空或非 “FALSE”/“OFF” 就为真
变量已定义 if(DEFINED VAR) 检查变量是否存在(即是否用过 set())
文件存在 if(EXISTS “path/to/file”) 可是绝对路径或相对路径
目录存在 if(IS_DIRECTORY “dir”) 是否为目录
文件是否常规文件 if(IS_SYMLINK “file”)if(IS_ABSOLUTE “path”) 软链接、是否绝对路径等判断
目标是否存在 if(TARGET target_name) 判断某个 CMake target 是否已定义
命令是否存在 if(COMMAND my_macro) 判断函数/宏/命令是否存在
测试是否定义 if(TEST my_test) 是否存在某个 add_test() 测试名
策略是否定义 if(POLICY CMP0077) CMake 策略编号是否存在
环境变量是否定义 if(ENV{HOME}) 读取环境变量;空字符串为假
版本比较 if(VERSION_LESS “3.20”)if(A VERSION_GREATER B) 支持 VERSION_EQUAL, VERSION_NOT_EQUAL, VERSION_LESS, 等
字符串比较 if(A STREQUAL B)if(A STRLESS B) 字符串比较,大小写敏感
数值比较 if(A EQUAL B)if(A LESS_EQUAL B) 比较整数值;不能用于浮点
正则匹配 if(A MATCHES “regex”) A 匹配某个正则表达式
逻辑运算符 if(A AND B)if(NOT A)if(A OR B)if((A OR B) AND C) 支持嵌套括号与组合运算
列表展开 if(A;B;C) 相当于 if(A AND B AND C)
表达式常量比较 if(“1” LESS “2”)if(“abc” STRGREATER “aaa”) 字符串/数值混合判断也支持

FUNCTION

最基础的定义和调用:

# 定义函数
function(<name> [ARG1 [ARG2 ...]])
  # 函数体:可以访问 ${ARG1}, ${ARG2}, ${ARGN}, ${ARGV}, ${ARGC} 等
  message(STATUS "In function ${name}: ARGC=${ARGC}, ARGV=${ARGV}")
endfunction()

# 调用函数
<name>(value1 value2)

内置参数变量:

变量名 含义
ARGV 列表,==函数所有实参(一个字符串,分号分隔)==
ARGN 列表,所有未被形参捕获的尾部实参
ARGC 整数,实际传入参数的个数
ARGV0 字符串,第 1 个实参
ARGV1… 字符串,第 2、3…各个实参
CMAKE_CURRENT_FUNCTION 字符串,当前调用的函数名

举例:

function(print_all)
  message("ARGS(${ARGC}): ${ARGV}")
  message("REST: ${ARGN}")
endfunction()

# 调用
print_all(a b c d)
# ARGS(4): a;b;c;d
# REST:   (空,因为无溢出参数)

支持return() 语法。

macro

定义与调用:

# 语法
macro(<name> [ARG1 [ARG2 ...]])
  # 函数体:可以使用 ${ARG1}, ${ARG2}, ${ARGV}, ${ARGC}, ${ARGN}
  message(STATUS "In macro ${name}: ARGV=${ARGV}, ARGC=${ARGC}")
endmacro()

# 调用
<name>(foo bar baz)

和函数的对比:

特性 macro() function()
作用域 共享调用处作用域 私有局部作用域
变量修改 改动的变量直接影响外层 需 PARENT_SCOPE 才影响外层
参数捕获 不捕获形参,所有实参都在 ARGV 捕获到形参,剩余在 ARGN
推荐场景 需要在调用点展开或注入作用域时用 一般推荐使用函数以避免污染作用域

例如如下就是一个共享作用域的场景:

macro(set_default var default)
  if(NOT DEFINED ${var})
    set(${var} ${default})  # 直接在调用处定义
  endif()
endmacro()

# 使用
set_default(FOO "bar")
message(STATUS "FOO=${FOO}")  # FOO=bar

foreach

foreach() 是 CMake 中的核心迭代命令,支持多种迭代模式。
基本语法:

foreach(loop_var IN ITEMS item1 item2 ...)
  # body: 使用 ${loop_var}
endforeach()

遍历:
set(MY_LIST a b c d)
foreach(item IN LISTS MY_LIST)
  message(STATUS "Item=${item}")
endforeach()

foreach 支持 break () 语法!

Generator Expressions

下面深入讲解 CMake 的 Generator Expressions(生成器表达式),它们是在生成阶段(Generate)而非配置阶段(Configure)被解析,用来根据上下文动态生成属性值。

1. 概念与使用场景
  • 定义:Generator Expression(简称 GE)以 $<…> 形式书写,内部可以包含条件、查询或属性引用。
  • 解析时机:GE 在 CMake 生成 Ninja/Makefile 等构建脚本时解析,而不会在第一次 cmake 配置时展开。
  • 用途:按配置(Debug/Release)、目标类型、平台、编译特性等按需生成编译器/链接器选项、安装路径、依赖列表等。

    基本语法:$<GENEXPR:arg1,arg2,…>

    2. 常用 Generator Expressions 一览
表达式 含义与参数 示例
配置相关
\$<CONFIG:cfg> 如果当前配置是 cfg(大小写不敏感)返回 1,否则空。 "$<CONFIG:Debug>" 在 Debug 下为 1,Release 下空。
\$<CONFIG:Debug,Release> 多值匹配,只要在列表里就返回 1。 "$<CONFIG:Debug,RelWithDebInfo>"
条件判断
\$<AND:\$<...>,\$<...>> 逻辑且。两个子表达式都非空时返回 1。 $<AND:$<CONFIG:Debug>,$<BOOL:USE_FOO>>
\$<OR:…> 逻辑或。任一非空即 1。 $<OR:$<CONFIG:Release>,$<BOOL:USE_BAR>>
\$<NOT:…> 逻辑非。内部为空或 “0” 则 1,否则空。 $<NOT:$<CONFIG:Debug>>
\$<IF:cond,true,false> 三元表达式:cond 非空时取 true,否则取 false。 $<IF:$<CONFIG:Debug>,-DDEBUG=1,>
布尔与字符串
\$<BOOL:var> 将 var 当布尔值解析,真返回 1,否则空。 $<BOOL:${BUILD_TESTS}>
\$<STREQUAL:a,b> 字符串相等返回 1,否则空。 $<STREQUAL:${CMAKE_SYSTEM_NAME},Linux>
目标与属性
\$<TARGET_FILE:tgt> 返回目标 tgt 的最终可执行/库文件路径。 $<TARGET_FILE:MyLib>
\$<TARGET_LINKER_FILE:tgt> 返回链接器可见的文件(DLL/SO)路径。 $<TARGET_LINKER_FILE:MyLib>
\$<TARGET_PROPERTY:tgt,prop> 读取已定义目标的属性值(通常是列表拼成字符串)。 $<TARGET_PROPERTY:MyLib,INTERFACE_INCLUDE_DIRECTORIES>
列表与字符串处理
\$<JOIN:list;sep> 把 list(分号分隔)用 sep 拼接成字符串。 $<JOIN:WARN;ERROR,;>WARN;ERROR
\$<SEMICOLON> 字面输出分号,用于在属性字符串中插入 ; "-DVAL1=1$<SEMICOLON>-DVAL2=1"
编译器/平台信息
\$<C_COMPILER_ID> C 编译器标识(GNU/Clang/MSVC 等)。 "$<C_COMPILER_ID>"
\$<CXX_COMPILER_ID> C++ 编译器标识。
\$<PLATFORM_ID> 平台标识(Windows, Linux, Darwin, …)。
\$<TARGET_PROPERTY>… 上表所示
安装/导出
\$<INSTALL_INTERFACE:…> 安装时展开;构建时为空。
\$<BUILD_INTERFACE:…> 构建时展开;安装时为空。

完整请参考> 官方文档($\

目标属性(Target Properties)

1. 常见目标属性
  • ARCHIVE_OUTPUT_DIRECTORYLIBRARY_OUTPUT_DIRECTORYRUNTIME_OUTPUT_DIRECTORY
    set_target_properties(MyLib PROPERTIES
      ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
      LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
      RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
    )
  • INCLUDE_DIRECTORIES(已被 target_include_directories 取代)
  • COMPILE_OPTIONS / COMPILE_DEFINITIONS
  • LINK_OPTIONS
  • INTERFACE_INCLUDE_DIRECTORIES / INTERFACE_LINK_LIBRARIES / INTERFACE_COMPILE_FEATURES
    2. 查询与继承
  • get_target_property
    get_target_property(srcs MyLib SOURCES)
    message(STATUS "MyLib sources: ${srcs}")
  • 目标间自动继承(Keyword: INTERFACE_*
    add_library(Base INTERFACE)
    target_include_directories(Base INTERFACE ${CMAKE_SOURCE_DIR}/include)
    
    add_library(Impl STATIC impl.cpp)
    target_link_libraries(Impl PUBLIC Base)
    # Impl 会继承 Base 的 include 目录
    3. Generator Expressions 与属性
    target_compile_definitions(MyLib
      PUBLIC
        $<$<CONFIG:Debug>:DEBUG_MODE>
    )

    自定义构建步骤(Custom Build Steps)

    1. add_custom_command
  • 直接为目标添加命令(POST_BUILD)
    add_executable(App main.cpp)
    add_custom_command(TARGET App POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:App> ${CMAKE_BINARY_DIR}/deploy/
      COMMENT "Deploying App..."
    )
  • 生成文件(OUTPUT / COMMAND)
    add_custom_command(
      OUTPUT generated.cpp
      COMMAND python3 ${CMAKE_SOURCE_DIR}/tools/gen.py -o generated.cpp
      DEPENDS ${CMAKE_SOURCE_DIR}/tools/gen.py
      COMMENT "Generating C++ source"
    )
    add_executable(GenApp generated.cpp)
    2. add_custom_target
  • 虚拟目标,不产生文件
    add_custom_target(format
      COMMAND clang-format -i ${SOURCES}
      WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
      COMMENT "Formatting sources"
    )
  • 联合依赖
    add_custom_target(all_gen DEPENDS generated.cpp)
    add_custom_target(build_all ALL DEPENDS all_gen App)
    3. 强制依赖(add_dependencies)
    add_dependencies(App generate_code)
    # 确保 generate_code 先执行

    测试与打包(Testing & Packaging)

    1. 测试(CTest)
  • 启用测试
    enable_testing()
    include(CTest)
  • 添加测试

    add_executable(TestFoo test_foo.cpp)
    add_test(NAME FooTest COMMAND TestFoo --gtest_output=xml)
  • 使用 add_llvm_unittest / add_test

  • 并行测试ctest -j ${N}
    2. 安装规则(install)
  • 库与可执行文件
    install(TARGETS MyLib MyApp
      EXPORT MyProjectTargets
      ARCHIVE DESTINATION lib
      LIBRARY DESTINATION lib
      RUNTIME DESTINATION bin
    )
  • 头文件与目录
    install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/ DESTINATION include)
  • 导出 CMake 配置
    install(EXPORT MyProjectTargets
      FILE MyProjectConfig.cmake
      NAMESPACE MyProj::
      DESTINATION lib/cmake/MyProject
    )
    3. CPack 打包
    include(CPack)
    set(CPACK_PACKAGE_NAME "MyProject")
    set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
    set(CPACK_GENERATOR "TGZ;ZIP;DEB")
    # 可选设置:CPACK_DEBIAN_PACKAGE_MAINTAINER, CPACK_DEBIAN_PACKAGE_DEPENDS
    执行:
    make package   # 生成 .tar.gz/.zip
    cpack -G DEB  # 生成 .deb

    LLVM 实战

    cmake_minimum_required(VERSION 3.13.4)           # 1
    project(LLVM LANGUAGES C CXX)                    # 2
    
    # 3. 启用选项与外部项目列表
    option(LLVM_ENABLE_ASSERTIONS "Enable LLVM assertions." OFF)        # Bool选项
    set(LLVM_ENABLE_PROJECTS "clang;lld;compiler-rt" CACHE STRING "")
    
    # 4. 包含通用宏与功能函数
    list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
    include(LLVMConfig)                              # 定义 find_llvm_*, add_llvm_* 系列宏
    include(AddLLVM)                                 # 提供 add_llvm_library, llvm_output, etc.
    
    # 5. 全局设置——编译特性、警告、优化
    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    add_compile_options(-Wall -Wextra -Werror)       # LLVM 常用警告级别
    
    # 6. 分发子项目
    add_subdirectory(lib)                            # LLVM core libraries
    add_subdirectory(clang)                          # Clang 前端
    # … 其它 projects based on LLVM_ENABLE_PROJECTS 列表
    
    # 7. 测试与安装
    enable_testing()
    add_subdirectory(test)                           # LLVM 自身测试
    include(InstallLLVM)                             # 自动生成 install 规则、CPack 打包脚本

参考文献