YC Yc.W home Systems · C++ · AI Infrastructure
Developer Tools · Deep-dive

现代 CMake 工程化:Targets、依赖发现、安装与 CPack

把零散的 CMake 记录整理成以 target 为中心、可安装、可测试、可打包的现代 C/C++ 构建流程。

Curated guide deep dive Featured Review required Version-sensitive Use caution
Explore all Developer Tools notes →
Related topics C & C++ DevOps & Infrastructure

CMake 记录通常从“怎样编译一个可执行文件”开始,但真正可维护的工程还需要回答:依赖如何传递、安装后的库如何被找到、测试如何运行、交叉编译如何隔离、发布包如何生成。现代 CMake 的核心是 target:把目标的属性、依赖和用法写成显式关系。

本文是基于历史 CMake 笔记的重组稿。示例使用 CMake 3.21 语法来说明 target 关系;实际项目应以所用工具链、生成器和支持矩阵为准。

技术交叉核对(2026-09-25):target 依赖、Config package、IMPORTED target 和安装导出关系已对照 CMake 官方 Tutorial 与 cmake-packages。示例仍是教学片段,不是某个项目的完整构建配方。

1. 先写 target,再写全局变量

一个最小的库和可执行文件可以这样表达:

cmake_minimum_required(VERSION 3.21)
project(demo LANGUAGES CXX)

add_library(core
    src/parser.cpp
    src/parser.hpp
)
target_include_directories(core PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
target_compile_features(core PUBLIC cxx_std_17)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)

这里有三个重要关系:

  • core 的公共头文件目录属于使用者和构建者;
  • app 只在本地使用 core,不应把依赖无意义地扩散出去;
  • 安装接口与构建接口分开,避免把源码树路径泄漏到安装包。

不要从旧式全局 include_directories()、全局编译选项和目录级变量开始新项目;历史项目可以先保持兼容,再逐步迁移到 target 语义。

2. 依赖发现的失败要显式处理

find_package 和 find_library 的结果必须检查,尤其要处理 *-NOTFOUND:

find_package(Threads REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)

add_library(json_adapter INTERFACE)
target_link_libraries(json_adapter INTERFACE nlohmann_json::nlohmann_json)

查找结果应支持用户传入 CMAKE_PREFIX_PATH、toolchain 或 package registry,而不是把开发机路径硬编码进项目。相关历史记录见 find_library、CMake 总览 和 交叉编译工具链。

3. 自定义命令要声明输入输出

构建规则最常见的问题是隐式依赖:文件变了,命令却没有重新运行;命令失败了,构建系统却以为成功。add_custom_command 应尽量同时声明 OUTPUT、DEPENDS 和 COMMENT:

add_custom_command(
    OUTPUT generated/version.hpp
    COMMAND ${CMAKE_COMMAND}
            -DVERSION=${PROJECT_VERSION}
            -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/write_version.cmake
    DEPENDS cmake/write_version.cmake
    COMMENT "Generating version header"
    VERBATIM
)
add_custom_target(generate_version DEPENDS generated/version.hpp)
add_library(core_dependencies INTERFACE)
add_dependencies(core_dependencies generate_version)
add_dependencies(core generate_version)
target_sources(core PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated/version.hpp")

生成文件、复制资源、编译代码生成器和打包前检查都可以采用这个模式,但不要把所有逻辑都塞进一个巨大的 add_custom_target。

4. 安装、导出和测试

一个库要真正被其他项目消费,至少需要:

  • install(TARGETS ...) 并明确库、运行时和归档目标;
  • install(DIRECTORY ...) 安装公共头文件;
  • install(EXPORT ...) 导出 target 配置;
  • 提供 CMakePackageConfigHelpers 生成的配置文件;
  • 用 find_package(OwnProject CONFIG REQUIRED) 做消费端测试。

测试应通过 enable_testing()、add_test() 或 CTest 接入,测试命令不能依赖 IDE 当前打开的工作目录。CMAKE_CXX_STANDARD 等全局设置能工作,但 target 级的 target_compile_features 更容易表达真实依赖。

5. 交叉编译要和本机构建隔离

交叉编译时,工具链文件负责编译器、sysroot 和目标平台;项目逻辑不应通过 if(CMAKE_HOST_WIN32) 猜测目标平台。至少记录:

目标架构 / ABI:
编译器与工具链版本:
sysroot:
依赖库来源:
CMake generator:
测试运行方式(原生或 emulator):

ARM/Linaro 工具链记录 可以作为离线环境的起点,但应在 CI 或干净容器中验证完整 configure、build、install 和 package 流程。

6. CPack 是发布流程的一部分

CPack 适合从已经安装的 staging 目录生成压缩包或安装包。推荐流程是:

configure → build → test → install 到 staging → package → 在干净环境安装并运行 smoke test

版本信息、依赖清单、许可证和校验值应进入发布元数据,而不是只写在聊天记录里。历史 CPack 记录 和 版本文件生成记录 可以作为起点,但不要把开发机绝对路径写进包。

7. 交付前检查

  • 新项目是否以 target 为中心传递依赖?
  • 依赖找不到时是否明确失败?
  • 自定义规则是否声明输入、输出和失败传播?
  • 安装后的头文件、库和 CMake 配置是否完整?
  • 交叉编译是否没有误用宿主机库?
  • CTest 是否能在干净环境运行?
  • 安装包能否在另一台机器安装并通过 smoke test?

当这些问题都有答案时,CMake 才从“能在本机编译”变成“能被团队和发布流程依赖”。

Source notes / 资料来源

本文是对站内历史资料的重新编排。原始文章保持独立发布,下面列出本文重组所依据的来源;正文中的引用会直接指向对应的编号来源。标签和署名信息描述仓库中的来源状态,不等同于外部事实验证。