简介:《Cmake开发手册详解.pdf》是一份面向开发者的CMake入门与进阶参考,适合需要跨平台构建、多语言或多配置项目的开发者。手册系统梳理了CMake 2.8.3的常用选项与生成器,并逐条讲解add_custom_command、add_executable、add_library等命令的语法和用途,帮助读者快速掌握CMakeLists.txt的编写方法、构建流程及常见参数配置。内容还覆盖find_package、target_link_libraries、install等高级特性,以及模块化构建、目录结构组织等实践思想,并结合示例说明实际应用场景,适合零基础入门和日常查阅。全资源为1个PDF文件,共1.14MB,单文件便于本地离线阅读;已有1034人学习下载。整体结构按选项、命令与高级主题分类,目录清晰,可帮助读者按需定位到具体配置项或命令,无论是初学语法还是排查构建问题,这份手册都能提供实在的参考,读者无需额外搜索即可获得清晰的CMake 2.8.3使用指引。
1. 为什么 C/C++ 项目的现代化要从看懂这本文档开始
你大概率遇过这种情况:拿到一个开源 C/C++ 工程,README 第一行写着mkdir build && cd build && cmake .. && make,你照做了,编译通过,但完全不清楚 CMakeLists.txt 里那些add_library、target_include_directories、INTERFACE到底在干什么。等你想往工程里加一个第三方库、切换编译器或者把构建产物挪个位置时,每一步都在试错。
Cmake开发手册详解这本参考文档存在的意义,就是把这些分散在官方文档、博客碎片和 Stack Overflow 里的知识点压成一条完整的学习路径。它不只是一份命令清单,而是帮助你建立「描述构建逻辑 → 生成平台原生构建文件 → 编译链接 → 安装打包」这条完整心智模型。对于从 Keil、Visual Studio 工程转过来的开发者,这篇手册尤其有价值,因为它解释了如何在 Windows(包括 Win10 64 位)和 Linux 间统一构建行为,而不是靠两套维护成本极高的工程文件硬撑。
需要明确的是,CMake 本身不是构建器,它是一个构建系统生成器。它读取 CMakeLists.txt 中的指令,生成 Ninja、Visual Studio 解决方案或 Unix Makefiles 等你本机已经安装的构建工具能直接识别的文件。理解这层抽象,后面所有的命令、变量和模块才有一个正确的坐标。这篇文章就沿着这本手册的核心主线,从安装、语法、目标配置到测试打包和迁移实战,把它讲透。
2. 从安装到第一个最小构建:跨 Windows 与 Linux 的环境准备
2.1 为什么不要用apt install cmake一把梭
网上检索 CMake 下载、安装、版本相关的问题时,最常见的一幕是:Ubuntu 用户直接执行sudo apt install cmake,执行cmake --version发现是 3.16 或 3.18,然后用try_compile或FetchContent特性时报错,去查官方文档才发现这个特性要 3.24 以上才支持。Ubuntu 默认源里的 CMake 版本往往滞后两年以上,对于只做简单编译或许够用,但一旦用到较新的模块和命令,就要被迫改代码逻辑。
我一般建议在 Ubuntu 上通过 Kitware 官方 APT 仓库安装,或者更直接的方式——下载官方预编译的二进制包安装到 /opt 或 /usr/local 下:
wget https://github.com/Kitware/CMake/releases/download/v3.29.3/cmake-3.29.3-linux-x86_64.tar.gz tar -zxvf cmake-3.29.3-linux-x86_64.tar.gz sudo mv cmake-3.29.3-linux-x86_64 /opt/cmake sudo ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake sudo ln -s /opt/cmake/bin/ctest /usr/local/bin/ctest sudo ln -s /opt/cmake/bin/cpack /usr/local/bin/cpack这段操作的核心逻辑是:将 CMake 和它的配套工具链(ctest 用于测试、cpack 用于打包)做软链接到 PATH 已有路径。如果不做软链接,用户每次要输入 /opt/cmake/bin/cmake 这个完整路径,很绕。装完后在任意目录执行cmake --version验证,看到cmake version 3.29.3即成功。需要注意的是,CMake 的预编译包对 glibc 版本有最低要求,如果系统过老(比如 Ubuntu 18.04 以下),不建议用太新的 CMake 版本,否则会报 GLIBCXX 相关错误。
Windows 用户直接下载向导式安装包(cmake-3.29.3-windows-x86_64.msi),安装时勾选「Add CMake to the system PATH for all users」。这里最常见的坑是安装时没勾选,然后在 PowerShell 里执行cmake时报错:cmake : 无法将“cmake”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这个报错不是 CMake 坏了,而是 PATH 没生效或没配好。解决办法:手动把C:\Program Files\CMake\bin加到系统环境变量 Path 里,然后重新打开一个终端窗口让它重新读取环境变量。安装完成后建议用cmake --version确认,并同时检查 Ninja——在 Windows 上 CMake 配 Ninja 的组合比配 Visual Studio 生成器在命令行构建时更轻快。
2.2 最小范例:三行 CMakeLists.txt 里的隐藏逻辑
在任何目录下建立一个hello文件夹,里面放一个main.cpp:
#include <iostream> int main() { std::cout << "Hello CMake" << std::endl; return 0; }再建立一个CMakeLists.txt,内容如下:
cmake_minimum_required(VERSION 3.16) project(hello_world CXX) add_executable(hello_world main.cpp)然后执行:
cmake -S . -B build cmake --build build -j 4 ./build/hello_world-S指定源码根目录,-B指定构建目录。用这两个参数的好处是永远不要在源码目录里执行cmake .产生一堆构建中间文件污染仓库。project那两行声明了项目名和语言。add_executable(hello_world main.cpp)是一个目标(target)的定义,目标名是 hello_world,它会把 main.cpp 编译并链接成可执行文件。
构建目录下会生成一个build.ninja(如果用 Ninja 生成器)或 Makefile(默认 Unix Makefiles)以及大量的 CMake 内部文件。不要手动去翻这些文件来理解构建过程,这是初学者最常见的误区——很少有人能靠读 Makefile 反推 CMakeLists.txt 的意图,正确的做法是修改 CMakeLists.txt 并重新执行cmake --build build,CMake 会自动感知源码目录里 CMakeLists.txt 的变更并重新生成构建系统。
3. 核心命令与构建目标:看懂 add_library 与 target_* 系列
3.1 add_library 的三种形态:静态、动态、对象库
大多数工程的构建输出不只是可执行文件,还会有库。add_library是管理库目标的核心命令,它有三种主要形态:
add_library(my_static STATIC src/a.cpp src/b.cpp) add_library(my_shared SHARED src/c.cpp src/d.cpp) add_library(my_obj OBJECT src/e.cpp)STATIC 是静态库,链接时库代码被复制进可执行文件,Windows 下生成.lib,Linux 下生成.a。SHARED 是动态库,Windows 下生成 DLL,Linux 下生成.so,运行时需要把 DLL 或 .so 放进可执行文件能搜到的目录。OBJECT 库比较特殊,它不生成归档或动态库文件,只把src/e.cpp编译出的.o目标文件收集起来,供后续目标通过$<TARGET_OBJECTS:my_obj>引用。
OBJECT 库在大型工程里非常有价值,典型场景是同一批源码要按不同编译选项编译两遍——例如一份源码既要打进正常模块,又要用不同宏定义打进另一个模块,用 OBJECT 库可以避免源码被编译两次。使用方式如下:
add_library(common OBJECT src/common.cpp) target_compile_definitions(common PRIVATE -DVERSION=2) add_executable(app_a main_a.cpp $<TARGET_OBJECTS:common>) add_executable(app_b main_b.cpp $<TARGET_OBJECTS:common>)$<TARGET_OBJECTS:common>是一个生成器表达式,CMake 会在构建系统生成阶段把所有 object 文件路径展开替换进去。使用生成器表达式是 CMake 老手和新手的一个重要分水岭。还有一个区别值得注意:STATIC 库在 CMake 里还有一个默认行为——它不会自动传递链接依赖给最终可执行文件,除非用target_link_libraries明确传递。
3.2 PUBLIC / PRIVATE / INTERFACE:三者的语义边界与传递规则
target_include_directories、target_compile_definitions、target_link_libraries这三条命令都带有PUBLIC / PRIVATE / INTERFACE访问限定符。这可能是 Cmake开发手册里容易让人困惑的地方,很多人写的 CMakeLists.txt 能跑但完全乱用,所有东西都标 PUBLIC。这三者的语义其实非常容易理解:
- PRIVATE:只对本目标自身生效,对外不可见。比如一个库内部用的私有头文件目录,不对外暴露。
- INTERFACE:本目标自身不使用,只传递给链接它的目标。比如一个纯头文件库(header-only library),没有 .cpp 文件,所有内容都在头文件里,那 include 路径就应该是 INTERFACE。
- PUBLIC = PRIVATE + INTERFACE:本目标自身用,且传递给下游目标。
用一个具体例子讲清楚。假设我们有一个 util 静态库,它内部依赖了 fmt(一个格式化库),但 util 的公共头文件里引用了 fmt 的头文件——例如util.h里写了#include <fmt/format.h>。
add_library(util STATIC src/util.cpp include/util.h) target_include_directories(util PUBLIC include PRIVATE src) target_link_libraries(util PUBLIC fmt::fmt)由于 util.h 是公共头文件,它对引用者暴露了 fmt 的类型,所以fmt::fmt必须为 PUBLIC。当可执行文件app链上util时,CMake 会自动把 fmt 的 include 路径和链接库也传给 app。若这里误写成 PRIVATE,app 在编译时会报找不到<fmt/format.h>。这种依赖传播机制是target_link_libraries最核心的价值,也是它取代旧式include_directories和link_directories命令的原因——后两者是目录级全局设置,不具备目标级别的边界和传递能力。
3.3 interface 库与target_sources(INTERFACE)的最小实现
纯头文件库在现代 C++ 生态非常常见(spdlog、glm、catch2 早期版本),它的 CMake 写法应使用 INTERFACE 库:
add_library(header_only_lib INTERFACE) target_include_directories(header_only_lib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)这里只定义了 INTERFACE 库目标,不产生任何编译产物,但其他目标链接header_only_lib时,会自动获得 include 目录。$<BUILD_INTERFACE:...>是一个很有用的生成器表达式:它只在当前工程直接构建时生效。如果这个库要安装到系统并供别的工程使用,通常还应该搭配$<INSTALL_INTERFACE:include>,这样别人用find_package找到这个库时拿到的是安装路径下的 include 目录,而不是本机某个源码目录。
target_sources也可以与 INTERFACE 库一起使用。把源文件列表集中在某个 INTERFACE 目标上,有经验的团队会在大型工程里用这种方法做一个「源文件集合」的概念,比如:
add_library(core_sources INTERFACE) target_sources(core_sources INTERFACE src/module_a.cpp src/module_b.cpp ) add_executable(app main.cpp) target_link_libraries(app PRIVATE core_sources)这样 app 的源文件列表被拆成 main.cpp 与核心实现两部分,多人协作时改动target_sources不需要触碰主 CMakeLists.txt。
4. 依赖管理与环境适配:find_package、FetchContent 与平台分支
4.1 find_package 的两种模式与 CONFIG 路径查找顺序
工程一旦开始依赖第三方库,find_package就成了最需要看懂的命令。它有两种模式:模块模式(Module Mode)和配置模式(Config Mode)。模块模式是 CMake 在/usr/share/cmake-3.29/Modules/下查找FindXXX.cmake脚本,由脚本负责定位库。CMake 官方对不少常用库直接提供了FindXXX.cmake(如 FindZLIB、FindThreads),这种模式下的变量命名规则常是XXX_INCLUDE_DIRS与XXX_LIBRARIES。
配置模式则是库的安装包自己提供了一个XXXConfig.cmake文件,里面定义了XXX::XXX这样的导入目标。现代 C++ 库主推的就是配置模式——比如安装 OpenCV 或者 Qt 后,你应该直接看到opencvConfig.cmake或Qt6Config.cmake。配置模式下find_package的查找路径顺序是:<PackageName>_DIR缓存变量指定的路径、CMAKE_PREFIX_PATH、系统默认前缀。这就是网上常说的 PREFIX_PATH 问题——装了库但 find_package 找不到时,最常见的排查方式就是手动指定前缀路径:
cmake -S . -B build -DCMAKE_PREFIX_PATH=/opt/opencv/lib/cmake或直接在 CMakeLists.txt 中设置:
set(OpenCV_DIR "/opt/opencv/lib/cmake/opencv4") find_package(OpenCV REQUIRED)REQUIRED 关键字表示如果找不到就直接报错,这比默认的静默失败好得多——静默失败会继续运行,到后面链接阶段报一些令人摸不着头脑的错误。
4.2 FetchContent 在 3.24 之后的推荐写法
网络检索 CMake 相关内容,涉及依赖下载时大概率会提到 FetchContent。它的作用是直接在配置阶段把第三方源码拉进构建系统,省掉 git submodule 的同步问题和 find_package 的版本不一致问题。CMake 3.24 之后推荐的写法是:
include(FetchContent) FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) FetchContent_MakeAvailable(googletest)FetchContent_Declare声明了源码地址和版本,FetchContent_MakeAvailable内部会执行三步操作:下载源码、添加子目录、把变量(如googletest_SOURCE_DIR)暴露给当前作用域。之后在当前 CMakeLists.txt 中直接使用gtest_main这个 target 即可,因为它已经被 add_subdirectory 引入了构建图。
值得注意的是,FetchContent_MakeAvailable会默认在首次配置时访问网络。如果构建环境是离线状态,可以先用FetchContent_Populate把源码下载到本地,然后设置FETCHCONTENT_SOURCE_DIR_GOOGLETEST指向本地目录来跳过下载。
set(FETCHCONTENT_SOURCE_DIR_GOOGLETEST "/path/to/cached/googletest") include(FetchContent) FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0) FetchContent_MakeAvailable(googletest)这里的关键变量命名规则是FETCHCONTENT_SOURCE_DIR_<UPPER_NAME>,名称全部转大写。代码中体现的实践是:把网络获取和本地缓存两种模式做成统一的option,方便在 CI 和开发环境之间切换。
4.3 用 CMAKE_SYSTEM_NAME 与 WIN32 / UNIX 处理平台差异
涉及跨平台构建时,比较克制的做法是:平台差异用生成器表达式在命令中表达,而不是到处写一大堆if(WIN32)/if(UNIX)分支。比如编译一个需要链接 ws2_32(Windows 网络库)的功能模块:
target_link_libraries(network_lib PUBLIC $<$<PLATFORM_ID:Windows>:ws2_32> )$<$<PLATFORM_ID:Windows>:ws2_32>是两层嵌套的生成器表达式:PLATFORM_ID:Windows返回值是 1 或 0,随后条件表达式决定是否把 ws2_32 加入链接列表。这样表达的好处是:一段代码同时覆盖两个平台,不需要两个分支。
当分支逻辑确实很繁琐时,再使用if(WIN32)也不迟。需要注意WIN32、UNIX是 CMake 的预设布尔变量,不需要CMAKE_SYSTEM_NAME,除非需要精确判断是 Linux、Darwin 还是其系统。涉及路径分隔符时,CMake 内部统一用/,它会自动转换,不要在 CMakeLists.txt 里写\。
5. 编译选项、多配置生成器与构建产物的精细化控制
5.1 CMAKE_BUILD_TYPE 和 CMAKE_CXX_FLAGS 的分层覆盖关系
几乎没有工程能绕开编译选项。CMAKE_BUILD_TYPE是单配置生成器(Makefiles 和 Ninja)所选用的构建类型,常见取值:
| 构建类型 | 对应 C++ 编译选项 | 适用场景 |
|---|---|---|
| Debug | -g,无优化或 -O0 | 开发调试,保留完整符号信息 |
| Release | -O3 或 -O2,NODEBUG | 发布版本,性能最大 |
| RelWithDebInfo | -O2 -g | 需要调试的发布版本,线上问题排查 |
| MinSizeRel | -Os | 嵌入式等空间受限场景 |
在命令行上用下面的方式选择构建类型:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j 8CMAKE_BUILD_TYPE本质上只是设置了一个变量,CMake 根据它的值把对应的CMAKE_CXX_FLAGS_RELEASE内容追加到全局的CMAKE_CXX_FLAGS之后。CMAKE_CXX_FLAGS是全局基础标志,CMAKE_CXX_FLAGS_<CONFIG>是配置级标志,两者是叠加关系而不是互相覆盖。
多配置生成器(Visual Studio 和 Xcode)没有 CMAKE_BUILD_TYPE 这个概念,它们在构建阶段通过--config参数选择配置:
cmake -S . -B build_msvc -G "Visual Studio 17 2022" cmake --build build_msvc --config Release这意味着同一个build_msvc目录里可能同时存在 Debug 和 Release 版本的产物,不会互相覆盖。如果你在团队协作中维护构建脚本,建议把 CMake 生成器的选择逻辑封装成脚本,避免团队成员手动在 Windows 上用了 Unix 的生成器参数。
5.2 目标级编译选项的写法与后向兼容
对特定目标单独附加编译选项,用的仍然是target_compile_options。一个常见需求是让某个模块在 Debug 下关闭优化或在 Release 下打开特定警告:
target_compile_options(my_lib PRIVATE $<$<CONFIG:Debug>:-O0 -g3> $<$<CONFIG:Release>:-O3> )$<$<CONFIG:Debug>:...>是一个相对更常用的生成器表达式形式:当 CONFIG 变量的值是 Debug 时,展开为后面的选项。这里不能写成老的if(CMAKE_BUILD_TYPE STREQUAL "Debug"),因为后者在多配置生成器下无法正确工作——它只在配置阶段求值一次,而多配置生成器在同一构建目录中可能存在多个配置。
5.3 CMAKE_RUNTIME_OUTPUT_DIRECTORY 与 DLL 的运行时目录策略
Windows 下写 CMake,最折磨人的问题不是编译错误,而是 DLL 找不到。默认情况下,Visual Studio 生成器会把 exe 和 dll 分别放在build/Release/与build/bin/等不同目录里,运行时经常报「找不到 xxx.dll」。
解决方法是显式指定输出目录:
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)CMAKE_RUNTIME_OUTPUT_DIRECTORY管可执行文件和 DLL(在 Windows 上 DLL 被视为运行时输出),CMAKE_LIBRARY_OUTPUT_DIRECTORY管 Linux 下的 .so,CMAKE_ARCHIVE_OUTPUT_DIRECTORY管 .a 和 .lib。把它们统一指向同一个 bin 目录,exe 启动时在当前目录就能找到 DLL,省掉手动拷贝。如果某些库希望保持不同组织方式,也可以在目标级别使用set_target_properties的RUNTIME_OUTPUT_DIRECTORY属性单独覆盖全局设置。
6. 将 Keil / Visual Studio 工程迁移为 CMake 时的目录组织与技巧
6.1 迁移时的顶层 CMakeLists.txt 骨架设计
热词检索里频繁出现「如何将 keil 工程变成 cmake」的问题。这不只是嵌入式开发者的需求,也是很多维护过 Windows 桌面项目的人会遇到的场景——工程里积攒了几百个源文件和一堆乱七八糟的 include 路径。迁移的第一步不是写源文件列表,而是设计目录骨架。我的常用做法是顶层 CMakeLists.txt 只做三件事:
cmake_minimum_required(VERSION 3.16) project(firmware C CXX ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_subdirectory(application) add_subdirectory(drivers) add_subdirectory(bsp)顶层不直接列出源文件,只声明项目语言和标准,然后通过add_subdirectory把各模块引入。在 Keil 工程里,源文件按 Functional Group 分组——启动文件、驱动、中间件、应用层,迁移到 CMake 时正好对应不同的子目录或库目标。
set(CMAKE_C_STANDARD 11)和set(CMAKE_CXX_STANDARD 17)和直接传-std=c++17的区别在于:CMake 会优先选择编译器真正支持的标准版本,并且在编译器不支持时给出更清晰的诊断信息,而不是简单报一个无法识别的参数。对于嵌入式交叉编译,标准统一尤其重要,因为不同的 arm-none-eabi-gcc 版本对 C++ 标准的支持度参差不齐。
6.2 嵌入式场景的链接脚本与二进制产物拷贝
Keil 工程中有分散加载文件(.sct),GCC 工具链对应的是链接脚本(.ld)。在 CMake 里指定链接脚本,需要向编译器传递-T参数:
add_executable(firmware.elf application/main.c startup/startup_stm32f407xx.s ) target_compile_definitions(firmware.elf PRIVATE STM32F407xx) target_link_options(firmware.elf PRIVATE -T${CMAKE_SOURCE_DIR}/linker/stm32f407_flash.ld -Wl,-Map=${CMAKE_BINARY_DIR}/firmware.map )target_link_options是 CMake 3.13 引入的命令,之前必须用set_target_properties设置LINK_FLAGS。它的 PRIVATE 关键字的含义和target_compile_options一样。-Wl,-Map=...告诉链接器额外生成一份 map 文件,用于分析代码体积与内存布局,这在嵌入式 flash 空间紧张的工程里很有价值。
生成 .hex 或 .bin 文件通常用add_custom_command:
add_custom_command(TARGET firmware.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex firmware.elf firmware.hex COMMENT "Generate Intel HEX file" )POST_BUILD表示在 firmware.elf 构建完成之后执行objcopy,将 ELF 转为烧录用的 HEX 文件。CMAKE_OBJCOPY是工具链对应的 objcopy 路径,交叉编译时它会自动指向 arm-none-eabi-objcopy,不需要你手动写死。
6.3 迁移后验证构建的检查清单
迁移完成后,建议按下面的顺序跑一遍验证,而不是直接拿给团队用:先确认cmake --build build在全量构建下没有警告之外的输出;再在空目录重新构建一次,确保没有依赖上一次残留的中间文件;然后用cmake --build build --target clean后重跑。能用ctest跑的单元测试尽早接入,对后续工程演进帮助巨大。最后把cmake -S . -B build -DCMAKE_BUILD_TYPE=Release的分支作为持续集成的主入口。这套验证流程比单纯「编译通过」多了一层保障——至少能发现隐藏的路径硬编码、错误的依赖声明和跨平台路径问题。
本文还有配套的精品资源,点击获取