
个人主页爱和冰阔乐专栏传送门《数据结构与算法》 、C学习方向C方向学习爱好者⭐人生格言得知坦然 失之淡然博主简介文章目录前言一、先弄清楚到底要复现什么二、准备一个最小项目三、CMakeLists.txt 只描述项目关系四、用 vcpkg manifest 声明依赖和版本基线4.1 vcpkg.json4.2 builtin-baseline 固定了什么4.3 feature 依赖五、用 CMake Presets 固定构建入口5.1 为什么要把 Configure、Build、Test 分开5.2 Preset 继承解决参数复制六、toolchain 路径放在哪里6.1 不要提交个人绝对路径6.2 用 CMakeUserPresets.json 保存本机路径七、Ninja 和 Visual Studio 别用同一套配置思路7.1 Ninja 是单配置生成器7.2 Visual Studio 是多配置生成器八、二进制缓存解决重复编译九、本地、IDE 与 CI 使用同一套命令十、换台机器失败时怎么查10.1 改了 Preset却只执行 build10.2 一个构建目录混多个工具链10.3 IDE 和终端的环境变量不一致10.4 find_package 找不到已经声明的依赖10.5 把所有选项继续留在命令行十一、发布前的验收清单仓库内容构建入口依赖干净环境验证总结参考资料前言C 项目里经常会遇到一种很难受的问题开发者 A 的 Windows 能编译 开发者 B 的 Linux 找不到依赖 IDE 里能运行命令行却不行 本地 Debug 正常CI Release 失败 重新拉取仓库后装到了另一套依赖 换一台机器CMake 参数全靠聊天记录恢复很多时候问题并不在某一条编译命令而是构建输入没有被完整保存。一套构建至少要回答下面这些问题使用哪个生成器 使用哪个编译器和工具链 构建类型是什么 打开了哪些项目选项 依赖从哪里解析版本基线是什么 源码目录和构建目录在哪里 测试命令怎样执行 本地、IDE 和 CI 是否使用同一套参数如果答案散落在 IDE 设置、环境变量、个人脚本和口头约定里CMakeLists.txt写得再整齐换台机器依然可能失败。这篇文章用一个小型 C 项目把CMakePresets.json、CMakeUserPresets.json和 vcpkg manifest 串起来。重点不是“怎么安装 CMake”而是怎样把容易漂移的参数和依赖从个人机器搬进一套可检查、可版本管理的构建入口。一、先弄清楚到底要复现什么严格的位级可复现会继续涉及时间戳、归档顺序、绝对路径映射和链接器行为。普通项目先把目标放在“构建过程可复现”上同一份源码 同一套声明的工具链与依赖 同一套构建选项 在支持的平台上得到功能一致的结果构建输入大致可以分成四层层级典型内容建议放置位置项目结构target、源码、链接关系CMakeLists.txt项目依赖fmt、Catch2、nlohmann-jsonvcpkg.json共享构建方式生成器、构建目录、选项、测试入口CMakePresets.json个人机器信息本机 vcpkg 路径、私有 SDKCMakeUserPresets.json或环境变量核心CMakeLists.txt描述“项目是什么”Preset 描述“这次准备怎样构建”User Preset 只补充“当前机器有什么不同”。把个人绝对路径写进共享 Preset会让仓库只在作者机器上可用把所有参数都塞进个人文件又会让团队无法复现。分层并不是为了多维护几个文件而是把“项目事实”和“本机事实”分开。二、准备一个最小项目目录如下reproducible-cpp/ ├── CMakeLists.txt ├── CMakePresets.json ├── CMakeUserPresets.example.json ├── vcpkg.json ├── src/ │ ├── main.cpp │ ├── checksum.cpp │ └── checksum.hpp └── tests/ └── checksum_test.cppsrc/checksum.hpp#pragmaonce#includestring_viewnamespacedemo{unsignedlonglongchecksum(std::string_view text);}src/checksum.cpp#includechecksum.hppnamespacedemo{unsignedlonglongchecksum(std::string_view text){unsignedlonglongvalue1469598103934665603ULL;for(unsignedcharcharacter:text){value^character;value*1099511628211ULL;}returnvalue;}}src/main.cpp#includeiostream#includestring#includefmt/format.h#includenlohmann/json.hpp#includechecksum.hppintmain(intargc,char**argv){conststd::string inputargc1?argv[1]:hello;nlohmann::json result{{input,input},{checksum,demo::checksum(input)}};std::coutfmt::format({}\n,result.dump(2));}这里故意引入fmt和nlohmann-json。后面不再依赖每个开发者手动安装而是通过 manifest 统一声明。三、CMakeLists.txt只描述项目关系项目的 target、源码、头文件目录和链接关系都应该留在CMakeLists.txt中cmake_minimum_required(VERSION 3.25) project( reproducible_cpp VERSION 1.0.0 LANGUAGES CXX ) option(REPRO_ENABLE_TESTS Build project tests ON) option(REPRO_WARNINGS_AS_ERRORS Treat warnings as errors OFF) add_library(repro_core src/checksum.cpp ) target_include_directories(repro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ) target_compile_features(repro_core PUBLIC cxx_std_20 ) find_package(fmt CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) add_executable(repro_app src/main.cpp ) target_link_libraries(repro_app PRIVATE repro_core fmt::fmt nlohmann_json::nlohmann_json )编译警告按编译器区分function(repro_set_warnings target_name) if(MSVC) target_compile_options(${target_name} PRIVATE /W4 $$BOOL:${REPRO_WARNINGS_AS_ERRORS}:/WX ) else() target_compile_options(${target_name} PRIVATE -Wall -Wextra -Wpedantic $$BOOL:${REPRO_WARNINGS_AS_ERRORS}:-Werror ) endif() endfunction() repro_set_warnings(repro_core) repro_set_warnings(repro_app)测试部分if(REPRO_ENABLE_TESTS) include(CTest) enable_testing() find_package(Catch2 3 CONFIG REQUIRED) add_executable(repro_tests tests/checksum_test.cpp ) target_link_libraries(repro_tests PRIVATE repro_core Catch2::Catch2WithMain ) repro_set_warnings(repro_tests) include(Catch) catch_discover_tests(repro_tests) endif()下面这些内容不适合硬编码在共享CMakeLists.txt里set(CMAKE_BUILD_TYPE Debug) set(VCPKG_ROOT D:/tools/vcpkg) set(CMAKE_CXX_COMPILER C:/...)构建类型、工具链路径和本机编译器属于某次配置或某台机器不是 target 之间的固定关系。推荐C 标准要求放在 target 上例如target_compile_features(... cxx_std_20)不要再在 Preset 中重复维护一份CMAKE_CXX_STANDARD。四、用 vcpkg manifest 声明依赖和版本基线4.1vcpkg.json{name:reproducible-cpp,version-string:1.0.0,dependencies:[fmt,nlohmann-json,catch2],builtin-baseline:替换为团队确认的完整vcpkg提交哈希}这里有一个容易写错的地方Catch2是测试程序需要链接的目标依赖不应该为了“它只在测试时使用”就随手写成{name:catch2,host:true}host: true表示依赖要为宿主平台构建适合代码生成器、脚本工具等宿主工具。在本机原生编译时host 和 target 往往相同问题不明显一旦交叉编译就可能暴露出来。4.2builtin-baseline固定了什么builtin-baseline不是某个库的版本号也不是完整锁文件。它指定的是使用 vcpkg 内置 registry 时依赖版本选择以哪个 vcpkg 提交对应的版本集合为基线。团队可以在确认过的 vcpkg 仓库中执行gitrev-parse HEAD把完整提交哈希填入builtin-baseline。如果还需要明确的最低版本或强制覆盖可以继续使用版本约束和overrides。只写依赖名但没有稳定基线新机器重新解析时就可能得到不同版本集合。别混淆baseline 解决“应该解析什么依赖输入”二进制缓存解决“相同输入能不能直接复用已经构建好的包”。4.3 feature 依赖依赖需要特性时可以明确声明{name:curl,default-features:false,features:[ssl]}不要默认把所有 feature 都打开。依赖树越大构建时间、缓存体积和平台差异越明显。五、用 CMake Presets 固定构建入口5.1 为什么要把 Configure、Build、Test 分开Preset 不是一条命令的缩写而是对构建阶段的命名configurePreset怎样生成构建系统 buildPreset构建哪个配置、使用多少并行任务 testPreset怎样运行测试、失败时输出什么一个可以直接使用的CMakePresets.json{version:6,cmakeMinimumRequired:{major:3,minor:25,patch:0},configurePresets:[{name:base,hidden:true,binaryDir:${sourceDir}/out/build/${presetName},cacheVariables:{REPRO_ENABLE_TESTS:ON,CMAKE_TOOLCHAIN_FILE:{type:FILEPATH,value:$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake}}},{name:ninja-debug,inherits:base,displayName:Ninja Debug,generator:Ninja,cacheVariables:{CMAKE_BUILD_TYPE:Debug,CMAKE_EXPORT_COMPILE_COMMANDS:ON,REPRO_WARNINGS_AS_ERRORS:OFF}},{name:ninja-release,inherits:base,displayName:Ninja Release,generator:Ninja,cacheVariables:{CMAKE_BUILD_TYPE:Release,CMAKE_EXPORT_COMPILE_COMMANDS:ON,REPRO_WARNINGS_AS_ERRORS:ON}},{name:vs2022,inherits:base,displayName:Visual Studio 2022,generator:Visual Studio 17 2022,architecture:{value:x64,strategy:set},condition:{type:equals,lhs:${hostSystemName},rhs:Windows}}],buildPresets:[{name:ninja-debug,configurePreset:ninja-debug,jobs:0},{name:ninja-release,configurePreset:ninja-release,jobs:0},{name:vs2022-debug,configurePreset:vs2022,configuration:Debug,jobs:0},{name:vs2022-release,configurePreset:vs2022,configuration:Release,jobs:0}],testPresets:[{name:ninja-debug,configurePreset:ninja-debug,output:{outputOnFailure:true},execution:{noTestsAction:error}},{name:ninja-release,configurePreset:ninja-release,output:{outputOnFailure:true},execution:{noTestsAction:error}},{name:vs2022-debug,configurePreset:vs2022,configuration:Debug,output:{outputOnFailure:true},execution:{noTestsAction:error}},{name:vs2022-release,configurePreset:vs2022,configuration:Release,output:{outputOnFailure:true},execution:{noTestsAction:error}}]}执行 Ninja Debugcmake--presetninja-debug cmake--build--presetninja-debug ctest--presetninja-debugjobs: 0对 build preset 来说相当于传入没有显式数量的--parallel由构建工具选择并行度。5.2 Preset 继承解决参数复制base是隐藏 Preset不能直接执行只保存公共内容。ninja-debug和ninja-release继承它以后只需要写各自不同的部分DebugCMAKE_BUILD_TYPEDebug ReleaseCMAKE_BUILD_TYPERelease以后修改公共构建目录或测试开关只改一个地方。六、toolchain 路径放在哪里vcpkg 的 CMake 集成依赖vcpkg-root/scripts/buildsystems/vcpkg.cmake该 toolchain 会在project()调用期间被读取。影响 vcpkg 的变量应当在第一次project()前进入 CMake 配置所以放在 Preset 的cacheVariables中很合适。6.1 不要提交个人绝对路径下面的写法只适合某一台机器{cacheVariables:{CMAKE_TOOLCHAIN_FILE:D:/tools/vcpkg/scripts/buildsystems/vcpkg.cmake}}共享 Preset 中使用环境变量{cacheVariables:{CMAKE_TOOLCHAIN_FILE:{type:FILEPATH,value:$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake}}}每台机器只需要设置自己的VCPKG_ROOT。PowerShell$env:VCPKG_ROOT D:\tools\vcpkgBashexportVCPKG_ROOT$HOME/tools/vcpkg6.2 用CMakeUserPresets.json保存本机路径仓库中可以提交一个示例文件CMakeUserPresets.example.json开发者复制为CMakeUserPresets.json内容{version:6,configurePresets:[{name:local-debug,inherits:ninja-debug,displayName:Local Ninja Debug,environment:{VCPKG_ROOT:D:/tools/vcpkg}}],buildPresets:[{name:local-debug,configurePreset:local-debug,jobs:0}],testPresets:[{name:local-debug,configurePreset:local-debug,output:{outputOnFailure:true},execution:{noTestsAction:error}}]}然后加入.gitignoreCMakeUserPresets.json /out/这里不需要手动写include:[CMakePresets.json]当两个标准文件都位于项目根目录时CMakeUserPresets.json会隐式包含CMakePresets.json因此可以直接继承共享 Preset。更稳的团队方式可以把 vcpkg 作为 Git submodule 固定到仓库约定目录或者通过统一环境脚本准备固定提交。路径固定但 vcpkg 内容不断变化依赖仍然会漂移。七、Ninja 和 Visual Studio 别用同一套配置思路7.1 Ninja 是单配置生成器Ninja 通常在 configure 阶段确定构建类型{generator:Ninja,cacheVariables:{CMAKE_BUILD_TYPE:Debug}}一个构建目录对应一种配置out/build/ninja-debug out/build/ninja-release7.2 Visual Studio 是多配置生成器Visual Studio 生成器一般不靠CMAKE_BUILD_TYPE选择 Debug 或 Release而是在 build/test 阶段指定{name:vs2022-release,configurePreset:vs2022,configuration:Release}常见误区给 Visual Studio configure preset 写了CMAKE_BUILD_TYPERelease并不代表后续构建命令一定选择了 Release。八、二进制缓存解决重复编译vcpkg 的二进制缓存保存已经构建好的依赖包。命中后另一台机器或下一次 CI 可以直接恢复而不是重新编译同一版本、同一 triplet、同一 ABI 条件下的包。本地文件缓存PowerShell$env:VCPKG_BINARY_SOURCES clear;files,D:\vcpkg-cache,readwriteBashexportVCPKG_BINARY_SOURCES\clear;files,/opt/vcpkg-cache,readwrite缓存是否命中会受到 port 内容、feature、triplet、编译器、工具链和依赖 ABI 等信息影响。缓存不是依赖锁baseline / 版本约束决定输入 binary cache复用相同输入的构建结果只有缓存没有稳定基线旧机器可能命中旧依赖新机器却重新解析新依赖最后两边都显示“缓存正常”构建结果仍然不同。九、本地、IDE 与 CI 使用同一套命令本地cmake--presetninja-release cmake--build--presetninja-release ctest--presetninja-releaseCI 也调用相同命令-name:Configurerun:cmake--preset ninja-release-name:Buildrun:cmake--build--preset ninja-release-name:Testrun:ctest--preset ninja-releaseCI 只负责准备CMake 编译器 Ninja 固定提交的 vcpkg VCPKG_ROOT 二进制缓存位置项目参数由仓库中的 Preset 提供。Preset 真正减少的不是键盘输入而是下面这种重复维护README 里一套 -D 参数 CI YAML 里一套 -D 参数 IDE 设置里一套 -D 参数 发布脚本里又一套 -D 参数参数只有一份修改和排障才有明确依据。十、换台机器失败时怎么查10.1 改了 Preset却只执行 build修改REPRO_ENABLE_TESTS:OFF然后只执行cmake--build--presetninja-debug构建目录可能仍保留旧配置。应该重新 configurecmake--presetninja-debug cmake--build--presetninja-debug查看可用 Presetcmake --list-presets cmake --list-presetsall10.2 一个构建目录混多个工具链同一个目录先用 GCC后改 Clangcmake-S.-Bbuild-GNinja\-DCMAKE_CXX_COMPILERgcmake-S.-Bbuild-GNinja\-DCMAKE_CXX_COMPILERclang旧缓存和编译器探测结果会让问题变得很难判断。更稳的做法out/build/linux-gcc-debug out/build/linux-clang-debug out/build/windows-msvc-debug10.3 IDE 和终端的环境变量不一致终端中可以看到echo$VCPKG_ROOT不代表 IDE 启动的 CMake 进程也继承到了同样的变量。排查时看IDE 实际选择的 Preset CMake configure 日志中的 toolchain 路径 CMakeCache.txt 中的 CMAKE_TOOLCHAIN_FILE IDE 是从哪个环境启动的 CMakeUserPresets.json 是否被读取最终以配置日志和缓存值为准不要只看系统环境变量设置界面。10.4find_package找不到已经声明的依赖先确认配置阶段真的加载了 vcpkg toolchaincmake-N-LAout/build/ninja-debug\|grepCMAKE_TOOLCHAIN_FILEWindowscmake-N-LA out/build/ninja-debug|Select-StringCMAKE_TOOLCHAIN_FILE需要观察搜索过程时cmake--presetninja-debug --debug-find在 manifest 模式下还要检查当前构建目录中的out/build/ninja-debug/vcpkg_installed/不要只运行一个与当前构建目录无关的全局vcpkg list然后就认定项目依赖已经安装正确。常见原因包括第一次 configure 没有加载 toolchain 使用了错误 triplet 修改 vcpkg 变量后没有重新配置 旧 CMakeCache 指向另一份包 包导出的 target 名与想象不同 CMAKE_PREFIX_PATH 改变了搜索顺序 manifest 没有声明直接依赖10.5 把所有选项继续留在命令行下面的命令本身没有错cmake-S.-Bbuild\-GNinja\-DCMAKE_BUILD_TYPERelease\-DREPRO_ENABLE_TESTSON\-DREPRO_WARNINGS_AS_ERRORSON\-DCMAKE_TOOLCHAIN_FILE...问题是它很快会被复制到 README、CI、个人脚本和 IDE 配置中。以后改一个参数需要同时改好几处。Preset 把入口变成cmake--presetninja-release十一、发布前的验收清单仓库内容[ ] CMakeLists.txt 只描述 target 和项目关系 [ ] vcpkg.json 声明全部直接依赖 [ ] builtin-baseline 使用确认过的完整提交哈希 [ ] CMakePresets.json 已提交 [ ] CMakeUserPresets.json 不提交 [ ] CMakeUserPresets.example.json 给出格式 [ ] 构建目录已加入 .gitignore构建入口[ ] configure、build、test 都有 Preset [ ] 单配置生成器设置 CMAKE_BUILD_TYPE [ ] 多配置生成器在 build/test preset 选择 configuration [ ] 不同工具链使用不同 binaryDir [ ] 本地、IDE 和 CI 使用同一套 Preset依赖[ ] vcpkg 提交或 registry baseline 固定 [ ] feature 明确声明 [ ] target triplet 与 host triplet 没有混淆 [ ] 二进制缓存没有被当成版本锁 [ ] 缓存目录权限和容量可观察干净环境验证[ ] 删除 out/build 后可以重新配置 [ ] 新终端中可以构建 [ ] 新机器按文档即可准备环境 [ ] Windows 和 Linux 的目标 Preset 可用 [ ] Debug 和 Release 都运行测试 [ ] 缺少依赖时错误信息明确总结“本机能编译”只能说明当前机器里的源码、缓存、环境变量、依赖和 IDE 设置碰巧能配合工作。要让另一台机器复现需要把构建输入逐层固定CMakeLists.txt 描述项目结构和 target 关系 vcpkg.json 声明直接依赖和版本基线 CMakePresets.json 描述共享的 configure / build / test 入口 CMakeUserPresets.json 补充个人机器路径 binary cache 减少相同依赖的重复构建 本地、IDE、CI 调用同一套 PresetPreset 不是命令缩写manifest 也不是依赖清单装饰。它们真正解决的是当参数和依赖从个人机器搬进版本控制以后换台机器失败时我们终于知道应该比较哪些输入而不是继续在 IDE 里反复点击“重新生成”。参考资料CMake Presetshttps://cmake.org/cmake/help/latest/manual/cmake-presets.7.htmlCMake Toolchainshttps://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.htmlCMakefind_packagehttps://cmake.org/cmake/help/latest/command/find_package.htmlvcpkg Manifest Modehttps://learn.microsoft.com/vcpkg/concepts/manifest-modevcpkg Versioninghttps://learn.microsoft.com/vcpkg/users/versioningvcpkg Binary Cachinghttps://learn.microsoft.com/vcpkg/reference/binarycachingvcpkg CMake Integrationhttps://learn.microsoft.com/vcpkg/users/buildsystems/cmake-integration