>> >> >> Reference << << << <<<<<<Ref>>>>>>
CMake
Modified: 2025-12-31 | Author:ljf12825

假设有一个C++项目,里面有几十个源文件,依赖好几个外部库。需要在不同的平台上编译它:

CMake的解决方案是: 不需要直接写Makefile或.vcxproj文件,而是用一个独立于编译器和平台的、更高级的语言(CMake语言)编写一个配置文件,叫做CMakeLists.txt

然后,CMake会根据你的平台和你选择的编译器,生成对应的原生构建文件

CMake是现代C++/C项目中最常见的构建系统生成工具,可以把它理解为跨平台的项目构建脚本语言 + 构建系统生成器

所以CMake的好处是:写一份CMakeLists.txt,到处都能用

CMake在开发流程中的位置

流程图

CMake在工具链中的位置

工具链中的位置

跨平台和交叉编译

跨平台中的位置

CMake基本工作流程

  1. 配置(Configure)
  1. 生成(Generate)
  1. 构建(Build)

CMake语法

CMake虽不像C++那样复杂,但它其实是一个声明式 + 脚本式DSL,有自己的一套规则和陷阱

基本特点

  1. 大小写不敏感,推荐小写
PROJECT(MyProj)
project(MyProj) # 等价
  1. 逐行解释,顺序敏感
  2. 注释:#后面的内容是注释
  3. 变量引用:用${var}
set(MYVAR Hello)
message(${MYVAR}) # 输出 Hello

变量

set(NAME value) # 普通变量
set(NAME "a;b;c") # 列表(用;分隔)
set(ENV{PATH} /usr/local/bin) # 设置环境变量
message($ENV{PATH}) # 读取环境变量

控制语句

  1. if
if (VAR STREQUAL "Hello")
    message("Matched")
elseif(VAR MATCHES "He.*")
    message("Regex matched")
else()
    message("No match")
endif()
  1. foreach
set(NAMES Alice Bob Charlie)
foreach(name ${NAMES})
    message("Name: ${name}")
endforeach()

也支持范围

foreach(i RANGE 3) # 0 1 2 3
foreach(i RANGE 1 5 2) # 1 3 5 [1, 5]间隔为2
endforeach()
  1. while
set(i 0)
while(i LESS 3)
    message(${i})
    math(EXPR i "${i}+1")
endwhile()

函数与宏

函数(作用域内变量)

funciton(print_message arg1)
    message("Function: ${arg1}")
endfunction()

print_message("Hello")

宏(全局变量修改)

macro(print_message arg1)
    message("Macro: ${arg1}")
endmacro()

区别:function内的set()默认是局部的,而macro会直接修改外部变量

列表操作

set(LST a b c)
list(APPEND LST d) # a;b;c;d
list(LENGTH LST len) # len=4
list(GET LST 1 second) # second=b
list(REMOVE_ITEM LST b) # LST=a;b;c

字符串操作

string(TOUPPER "abc" OUT) # OUT=ABC
string(REPLACE "a" "X" OUT "abc") # OUT=Xbc
string(REGEX MATCH "[0-9]+" NUM "abc123def) # NUM=123

文件与路径

file(READ my.txt CONTENTS) # 读文件
file(WRITE out.txt "Hello") # 写文件
file(MAKE_DIRECTORY build/include) # 创建目录
file(GLOB SOURCES "*.cpp") # 通配符获取文件

数学与逻辑

math(EXPR result "3 + 7") # result=10

逻辑运算在if

if(A AND B)
if(NOT C)

构建命令

add_executable(app main.cpp) # 可执行文件
add_library(mylib STATIC a.cpp b.cpp) # 静态库
add_library(mylib SHARED a.cpp b.cpp) # 动态库

target_link_libraries(app PRIVATE mylib) # 链接库
target_include_directories(app PRIVATE inc/) # 添加头文件路径

target属性(现代CMake推荐)

# 作用范围
# PRIVATE 仅自己使用
# PUBLIC 自己和依赖者都用
# INTERFACE 仅依赖者使用

target_compile_definitions(app PRIVATE DEBUG_MODE) # 宏定义
target_compile_options(app PRIVATE -Wall -Wextra) # 编译选项

模块与包

find_package(OpenGL REQUIRED) # 查找外部库
target_link_libraries(app PRIVATE OpenGL::GL)

子目录与安装

add_subdirectory(src) # 引入子目录

install(TARGETS app DESTINATION bin) # 安装可执行文件
install(FILES config.h DESTINATION include)

工具

cmake .. # 生成构建文件
cmake --build . # 构建(跨平台)
cmake --install . --prefix /usr # 安装

CMake特有命令

CMake功能

Debug/Release

CMake的Debug和Release模式是构建系统的核心概念,几乎每个实际项目都会用到。它们本质上是构建配置(Build Configuration),控制编译器优化级别、调试符号、宏定义等

CMake通过一个变量CMAKE_BUILD_TYPE来控制构建类型(单配置生成器),或者通过生成器本身来切换(多配置生成器)
常见的配置有:

设置方式

  1. 单配置生成器(Makefile、Ninja) 需要手动指定:
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..
make
  1. 多配置生成器(Visual Studio、XCode) 它们支持多配置,不用再CMake阶段指定,而是在IDE里切换:

命令行构建方式

cmake --build . --config Debug
cmake --build . --config Release

CMake内部变量

当选择不同配置时,CMake会自动设置一些编译器/链接器参数 例如(GCC/Clang下):

可以覆盖或追加

set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall")

在CMakeLists.txt中使用

可以根据构建类型做条件控制

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    message("Building Debug version")
    add_definitions(-DDEBUG_MODE)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
    message("Building Release version")
endif()

更推荐现代写法:

target_compline_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
    $<$<CONFIG:Release>:NDEBUG_MODE>
)

这里用了生成表达式$<CONFIG:...>,自动根据配置切换

实际开发中的使用场景

  1. 调试时 -> 用Debug,有完整符号,方便调试器定位问题
  2. 发布给用户时 -> 用Release,优化过的二进制,运行更快更小
  3. 线上诊断 -> 有时会用RelWithDebInfo,既有优化,又保留符号文件,便于分析crash
  4. 嵌入式/移动平台 -> 有时用MinSizeRel,为了减小体积

跨平台工具链

CMake默认使用本机编译器(Linux上用GCC/Clang,Windows上用MSVC,macOS上用AppleClang) 但如果要:

这时就需要工具链文件(Toolchain File)告诉CMake:要用什么编译器、链接器、sysroot、库路径、平台信息

指定工具链文件

运行cmake时指定

cmake -DCMAKE_TOOLCHAIN_FILE=path/to/toolchain.cmake ..

例如给Android构建:

cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake ..

工具链文件的内容

一个toolchain.cmake文件本质上就是一份CMakeLists.txt片段,里面写编译器和平台信息

示例:交叉编译到ARM Linux

# toolchain-arm.cmake
set(CMAKE_SYSTEM_NAME Linux) # 目标平台系统
set(CMAKE_SYSTEM_PROCESSOR arm) # 目标架构

# 指定交叉编译工具链
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)

# 搜索规则
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # 不在目标环境找可执行程序
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # 只在目标环境找库
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # 只在目标环境找头文件

构建:

cmake -DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake ..
make

现代CMake更推荐的做法

在CMakePresets.json里定义工具链,更方便管理
例如

{
    "version": 3,
    "cmakeMinimumRequired": { "major": 3, "minor": 20},
    "configurePresets": [
        {
            "name": "linux-arm",
            "generator": "Ninja",
            "toolchainFile": "cmake/toolchains/toolchain-arm.cmake",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        }
    ]
}

然后就可以

cmake --preset linux-arm

安装与导出

安装(install)

目的:把编译好的二进制文件、头文件、配置文件复制到一个标准位置,供其他项目使用
常见用法

  1. 安装可执行文件
add_execetable(myapp main.cpp)
install(TARGETS myapp DESTINATION bin)

安装后myapp会被放到<prefix>/bin

  1. 安装库
add_library(mylib STATIC foo.cpp)
install(TARGETS mylib DESTINATION lib)

安装后libmylib.a<prefix>/lib

  1. 安装头文件
install(FILES foo.h DESTINATION include)
install(DIRECTORY include/ DESTINATION include) # 整个目录
  1. 安装配置文件、资源文件
install(FILES config.json DESTINATION share/myproj)
  1. 设置安装前缀
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make install

最终文件会安装到/usr/local/bin,/usr/local/lib,/usr/local/include

导出(export)

目的:让别人通过find_package()使用你的库

  1. 导出目标
install(TATGETS mylib
        EXPOrT mylibTargets
        DESTINATION lib)
install(EXPORT mylibTargets
        NAMESPACE MyLib::
        DESTINATION lib/cmake/mylib)

这样会生成一个mylibTargets.cmake,里面描述了目标 + 依赖 别人find_package(mylib)时就能拿到MyLib::mylib目标

  1. 包配置文件 需要写一个mylibConfig.cmake,让CMake知道如何找到mylibTargets.cmake
# mylibConfig.cmake
include("${CMAKE_CURRENT_LIST_DIR}/mylibTargets.cmake")

然后安装它

install(FILES mylibConfig.cmake DESTINATION lib/cmake/mylib)

完整工作流

假设写了一个库mylib

  1. CMakeLists.txt
cmake_minimu_required(VERSION 3.15)
project(MyLib)

add_library(mylib foo.cpp)
target_include_directories(mylib PUBLIC include)

install(TARGETS mylib
        EXPORT mylibTargets
        DESTINATION lib)
install(EXPORT mylibTargets
        NAMESPACE MyLib::
        DESTINATION lib/cmake/mylib)
install(DIRECTORY include/ DESTINATION include)

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake"
    VERSION 1.0
    COMPATIBILITY AnyNewerVersion
)
install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake"
    DESTINATION lib/cmake/mylib)
  1. 安装
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make install

目录结构

/usr/local/
  ├── include/
  │     └── foo.h
  ├── lib/
  │     ├── libmylib.a
  │     └── cmake/mylib/
  │           ├── mylibTargets.cmake
  │           ├── mylibConfig.cmake
  │           └── mylibConfigVersion.cmake
  1. 在另一个项目中使用
find_package(mylib REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE MyLib::mylib)

现代CMake理念

CMake的我发展经历了一个从“命令式”到“声明式”的转变,这就是所谓的现代CMake理念

传统CMake(旧写法)

早期的CMake写法往往是命令式、全局变量驱动的

# 旧写法
include_directories(${CMAKE_SOURCE_DIR}/include)
link_directories(${CMAKE_SOURCE_DIR}/lib)

add_executable(app main.cpp)
target_link_libraries(app mylib)

缺点:

现代CMake(推荐写法)

现代CMake强调目标导向(Target-based)、声明式、可复用性
核心理念就是:所有的编译信息都绑定在target上,而不是全局设置

add_library(mylib src/mylib.cpp)

# 指定库的头文件(公开接口)
target_include_directories(mylib
    PUBLIC include # 使用该库的人也需要的头文件
    PRIVATE src/internal # 仅库内部需要
)

# 指定库依赖
target_link_libraries(mylib
    PUBLIC otherlib # 链接依赖传播出去
    PRIVATE pthread # 内部使用,外部不需要关心
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)

这样

现代CMake核心理念

  1. 目标导向(Target-base)
  1. 作用域清晰(PUBLIC/PRIVATE/INTERFACE)
  1. 导出与安装
  1. 声明式而非命令式
  1. 跨平台与工具链

现代CMake优点

target_*API

  1. target_link_libraries 声明某个目标依赖哪些库,并指定作用域
target_link_libraries(myApp
    PRIVATE myLibA
    PUBLIC myLibB
    INTERFACE myLibC
)
  1. target_include_directories 给目标添加头文件路径:
target_include_directories(myLib
    PUBLIC include/
    PRIVATE src/
    INTERFACE api/
)
  1. target_compile_definitions 给目标添加预处理宏
target_compile_definitions(myApp
    PRIVATE DEBUG_MODE
    PUBLIC USE_LIBX
)
  1. target_compile_options 给目标添加编译器选项
target_compile_options(myApp
    PRIVATE -Wall -Wextra
)
  1. target_sources 直接声明目标的源文件(比add_executable/add_library更灵活)
target_sources(myLib
    PRIVATE src/foo.cpp
    PUBLIC include/foo.h
)
  1. target_compile_features 声明目标需要的C++标准
target_compile_feature(myApp PUBLIC cxx_std_17)

比`set(CMAKE_CXX_STANDARD 17)更推荐

target API的传播机制

add_library(libA ...)
target_compile_definitions(libA PUBLIC USE_A)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE libA)

app会自动拥有USE_A

示例

  1. 假设有一个最简单的C++项目
project-root/
  ├── CMakeLists.txt
  └── main.cpp

main.cpp

# include <iostream>
int main()
{
    std::cout << "Hello, CMake!" << std::endl;

    return 0;
}

CMakeLists.txt

cmake_minium_required(VERSION 3.10) # 要求的最低CMake版本
project(HelloCMake) # 项目名称
set(CMAKE_CXX_STANDARD 17) # 设置C++标准

add_executable(hello main.cpp) # 生成可执行文件

构建:

mkdir build && cd build
cmake ..
make
./hello
  1. 稍微复杂的项目 目录结构:
project-root/
  ├── CMakeLists.txt
  ├── src/
  │     ├── CMakeLists.txt
  │     ├── main.cpp
  │     └── foo.cpp
  └── include/
        └── foo.h

project-root/CMakeLists.txt

cmake_minium_required(VERSION 3.10)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

add_subdirectory(src) # 进入scr/目录继续处理

scr/CMakeLists.txt

add_executable(myapp main.cpp foo.cpp)
target_include_directories(myapp PUBLIC ../include) # 指定头文件目录

这样目录更清晰,适合大型项目