CMake .cmake 模块与 Preset:可复现构建的拆分技巧
.cmake 模块与 Preset 使用技巧
根目录 CMakeLists.txt 一旦堆满 find_package、警告选项、生成器判定,项目会很快「能编但难讲」。更稳的做法是:
- 按职责拆
.cmake模块(工具链 / 生成器 / 警告 / 日志 / 安装) - 用
CMakePresets.json固定常用组合(本机 Ninja、CI Release、开 tidy 等)
本文是实操向清单,偏 CMake 3.20+ / 4.x。
1. 为什么要拆 .cmake
| 问题 | 拆模块后 |
|---|---|
| 新人不知道改哪 | README 写「改 cmake/ToolchainClang.cmake」 |
| AppleClang / Homebrew LLVM 混用 | 工具链探测集中一处 |
| 生成器搞错(modules 需要 Ninja) | Generator.cmake 统一 FATAL |
| 选项满天飞 | preset 命名表达意图 |
建议分层(名字可自定,职责别混):
cmake/
Generator.cmake # 强制/探测生成器
ToolchainClang.cmake # 编译器、sysroot、modules JSON
StdlibConfig.cmake # import std / stdlib 探测
CompilerWarnings.cmake # -Wall 与可选 tidy
BuildLog.cmake # 彩色日志 / tee
ProjectSetup.cmake # 打印关键变量摘要
Modules.cmake # 挂 CXX modules 相关根 CMakeLists.txt 只做:project()、add_subdirectory / add_executable、按顺序 include()。
2. .cmake 编写技巧
2.1 用 option() / CACHE 暴露开关,默认保守
option(CXX_ENABLE_CLANG_TIDY "Enable clang-tidy" OFF)
option(CXX_ENABLE_GTEST "Build tests" OFF)
set(CXX_PREFERRED_COMPILER "clang++" CACHE STRING "clang++|g++")默认 OFF,避免「拉下来编不过」。
2.2 致命条件用清晰单行字符串
CMake 的 list 与多参数 set(msg "a" "b") 容易把 FATAL_ERROR 显示成空白。拼错误信息时用 string(APPEND):
set(_err "")
string(APPEND _err "Ninja is required for C++23 modules.\n")
string(APPEND _err "Hint: export CMAKE_GENERATOR=Ninja or use -G Ninja")
message(FATAL_ERROR "${_err}")2.3 生成器:早失败
if(NOT CMAKE_GENERATOR STREQUAL "Ninja")
message(FATAL_ERROR "Use Ninja (C++ modules / import std). Got: ${CMAKE_GENERATOR}")
endif()配合根目录 .envrc:export CMAKE_GENERATOR=Ninja,减少「裸 cmake .. 掉进 Unix Makefiles」的缓存坑。
2.4 include 顺序要稳定
典型顺序:
include(cmake/Generator.cmake)
include(cmake/ToolchainClang.cmake)
include(cmake/StdlibConfig.cmake)
# … project() 之后再:
include(cmake/CompilerWarnings.cmake)
include(cmake/ProjectSetup.cmake)工具链相关尽量在 project() 前完成;依赖 PROJECT_* 的摘要放后面。
3. CMakePresets.json 技巧
3.1 最小结构
{
"version": 6,
"configurePresets": [
{
"name": "dev",
"displayName": "Dev (Ninja RelWithDebInfo)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"CXX_PREFERRED_COMPILER": "clang++",
"CXX_ENABLE_CLANG_TIDY": "OFF"
}
},
{
"name": "tidy",
"inherits": "dev",
"displayName": "Dev + clang-tidy",
"cacheVariables": {
"CXX_ENABLE_CLANG_TIDY": "ON"
}
}
],
"buildPresets": [
{ "name": "dev", "configurePreset": "dev" },
{ "name": "tidy", "configurePreset": "tidy" }
]
}3.2 实用习惯
| 技巧 | 说明 |
|---|---|
inherits | 共用 generator / binaryDir,只覆盖差异 |
binaryDir 固定 build/ | 与文档「进 build 再 cmake」一致 |
显式写 generator: Ninja | 不依赖环境变量也正确 |
分开 dev / ci / tidy | 意图可读,少口头约定 |
| 勿把本机绝对路径写进 preset | 用 ${sourceDir}、环境变量 |
3.3 命令
cmake --preset dev
cmake --build --preset dev
cmake --preset tidy
cmake --build --preset tidy无 preset 时等价习惯:
mkdir -p build && cd build
cmake -G Ninja ..
cmake --build .4. 和 clang-tidy 的组合
- 日常:
devpreset,tidy OFF - 合入前 / CI:
tidypreset 或-DCXX_ENABLE_CLANG_TIDY=ON - 规则文件:仓库根
.clang-tidy(见专栏上一篇)
这样「模块拆分」负责机制,「preset」负责场景,两边都不膨胀。
5. 排障速查
| 现象 | 处理 |
|---|---|
cmake .. 后生成器是 Makefiles | rm -rf build && mkdir build,再用 Ninja / preset |
| FATAL 信息空白 | 检查是否把错误字符串写成了 CMake list |
| tidy 找不到 | PATH 含 Homebrew LLVM;或 find_program 失败时明确 WARNING |
| preset 不生效 | 确认 CMakePresets.json 在源码根;CMake ≥ 3.19(建议 3.23+ / 4.x) |
6. 小结
把「怎么探测环境」放进 .cmake,把「这次我想怎样编」放进 preset。根 CMakeLists.txt 保持薄,专栏工程和脚手架模板就能共用同一套心智模型。