CMake 3.25 Windows C++构建指南:生成器、Presets与避坑 简介这份资源面向在 Windows 平台上从事 C 开发、需要统一构建流程的工程师与学习者提供 CMake 3.25 的官方 Windows 版本用于替代手写 Makefile、在 Visual Studio、MinGW、Ninja 等不同编译环境间快速生成工程文件并完成配置与构建。资源包共 7090 个文件压缩后约 36.31MB内容以 1991 个 txt、1937 个 html、1917 个 rst 说明文档和 975 个 cmake 模块脚本为主同时包含 in 模板、json 配置、png 图示及少量 c、cxx、cpp 示例源码与可执行程序文档与模块相对完整便于离线查阅命令、属性、变量与策略等手册内容。目前已有 1337 人学习下载。借助包内的命令行与图形界面工具读者可直观设置源码路径、构建目录与编译器选项并结合 cmake、ctest、cpack 等组件理解配置、测试与打包的完整链路适合希望系统掌握跨平台构建方法的中级开发者查漏补缺。1. CMake 3.25 Windows 版一个装完就能编 C 的构建底座手上这个包是 CMake 3.25 的 Windows 二进制发行版装完或解压完本机就多出 cmake、ctest、cpack、cmake-gui 四个可执行程序。它的价值不在版本号新而在于把一条 cmake c 链路一次性补齐从 CMakeLists.txt 到 .exe生成器选 Visual Studio 还是 Ninja、编译器用 MSVC 还是 MinGW、Debug 跟 Release 怎么切、第三方依赖怎么拉进来全归它管。适合三类人接手别人工程、clone 下来 configure 就报错的新人要在 Windows 上给 C 项目搭一套能复现构建环境的老手以及需要把工具版本钉死、让本地和 CI 输出一致的维护者。后面几章我按“包是什么形态 → 怎么跑通第一个工程 → 3.25 的新东西怎么用 → 哪些地方会翻车”的顺序拆代码都能直接抄。2. 包形态与版本边界msi、zip 怎么选3.25 到底多了什么2.1 msi 和 zip 两种形态差别比想象中大Windows 发行版一般给两种壳.msi安装包和.zip免安装包里面装的是同一套东西——bin下四个 exeshare/cmake-3.25/Modules下几百个Find*.cmake和平台判定脚本。差别全在“怎么落到系统里”。msi 走的是标准安装流程默认装进C:\Program Files\CMake安装向导里有两个勾选项值得看一眼一个是“Add CMake to the system PATH for all users”一个是“for current user”。勾了前者任何账户打开终端都能直接敲cmake勾后者只对当前账户生效。它还会建开始菜单项和桌面图标卸载走系统设置里的应用列表。zip 就是解压。你把目录丢到D:\tools\cmake-3.25也好D:\tools\cmake\3.25.3也好目录结构原封不动不写注册表、不碰 PATH。想换版本改 PATH 或者直接敲绝对路径两台机器上的构建结果不会因为你装的是 msi 还是 zip 而有任何区别。维度msi 安装包zip 免安装包落地方式向导安装到 Program Files解压到任意目录PATH 处理安装时可选自动写入需自己加或写脚本临时设多版本共存升级即覆盖需手动保留旧版天然支持按目录区分卸载系统设置里卸载删目录管理员权限装到 Program Files 需要不需要CI / 打包机无人值守安装要参数首选解压即用我自己的习惯是个人开发机装 msi图省事项目里锁版本的那一份用 zip放在仓库外的tools/目录用脚本把它的bin前置到 PATH。这样换机器、换人接手版本永远是 3.25而不是“我这台机器上恰好是几就几”。这条血泪经验是从一次“本地编得过、CI 上策略报错”的事故里换来的。2.2 3.25 这条版本线上真正值得用的改动很多人装完 CMake 就当它是个翻译器把 CMakeLists 翻译成 vcxproj版本无所谓。实际上小版本之间的命令集和策略默认值是会变的3.25 这一版有四处改动在真实工程里能直接用上。第一处是block()/endblock()两个新命令。以前在 CMake 脚本里想临时改个变量又不污染外层只能塞进function()里造一个作用域但 function 传值很别扭。block()直接给一段脚本划出独立作用域还能指定哪些变量向外传播。第二处是SYSTEM关键字。add_subdirectory()和FetchContent_Declare()都支持了它被标记的目录里的头文件会按“系统头”处理第三方库里的警告不会再刷满你的编译输出。配套还加了EXPORT_NO_SYSTEM目标属性用来控制这个标记要不要跟着 install/export 传出去。第三处是CMAKE_MSVC_DEBUG_INFORMATION_FORMAT变量和MSVC_DEBUG_INFORMATION_FORMAT目标属性配合新策略 CMP0141 使用把 MSVC 的调试信息格式/Z7、/Zi从“每个 target 各写各的”变成全局可控。第四处是交叉配置 preset。preset 文件 schema 升到 version 6新增CMAKE_CROSS_CONFIGS、CMAKE_DEFAULT_BUILD_TYPE、CMAKE_DEFAULT_CONFIGS三个变量让一次 configure 就能覆盖多个构建配置不必为了出 Debug 和 Release 各跑一遍 configure。提示以上四处都跟cmake_minimum_required(VERSION 3.25)绑在一起。写了 3.25CMP0140、CMP0141 这些新策略默认就是 NEW如果团队里还有人用 3.20 的 cmakeconfigure 阶段会直接报版本不足这是好事问题提前暴露在构建机上而不是等到链接期。2.3 安装位置与 PATH我不让安装器碰系统 PATH不勾“自动加 PATH”的理由很实际Windows 上不止一个地方藏着cmake.exe——某些 IDE 的安装目录里有、某些 SDK 里有、某些人手动解压过一个到C:\cmake。当 PATH 里存在多个时敲cmake命中的是排在最前面的那个而不是你以为的那个。这个毛病极难自查因为cmake --version输出的版本号不会告诉你它从哪来。先确认当前 shell 到底在用什么# 列出 PATH 上所有能找到的 cmake第一条就是实际生效的 Get-Command cmake -All | Select-Object -ExpandProperty Source # 确认版本行首必须是 3.25.x cmake --version # 顺带看一眼 ctest / cpack 是不是同一个目录里的 Get-Command ctest -All | Select-Object -ExpandProperty Source-All是关键参数不加只返回第一个命中项等于什么都没查。如果发现生效的是旧版本改 PATH 顺序或者在项目脚本里前置目录# 只在当前会话生效不动系统环境变量 $env:PATH D:\tools\cmake\3.25\bin; $env:PATH cmake --version这种“会话级 PATH”是我最推荐的做法写进一个env.ps1进项目先执行一次退出终端就恢复原样不会把机器的全局环境越改越乱。多个项目各自依赖不同 CMake 版本时也不会互相打架。3. 从零跑通一个 C 工程最小 CMakeLists 到两条生成器路线3.1 目录结构与一个不掺水的最小 CMakeLists.txt先建一个能跑的最小工程别一上来就往真实项目里套。结构如下demo/ CMakeLists.txt include/math_utils.h src/math_utils.cpp src/main.cppCMakeLists.txt全文不到二十行# 3.25 决定 CMP0140/CMP0141 等策略的默认值 cmake_minimum_required(VERSION 3.25) # 只声明 CXX避免探测 Fortran 之类不存在的编译器 project(demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # OFF 才会生成 /std:c17而不是 gnu17 这类扩展方言 set(CMAKE_CXX_EXTENSIONS OFF) add_library(math_utils STATIC src/math_utils.cpp) # PUBLIC这个 include 目录对链接本库的使用者同样可见 target_include_directories(math_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) add_executable(demo src/main.cpp) target_link_libraries(demo PRIVATE math_utils)逐行说清楚LANGUAGES CXX省掉一段无谓的编译器探测工程里没有 C 文件时尤其明显CMAKE_CXX_STANDARD_REQUIRED ON表示宁可编译失败也不降级到 C14CMAKE_CXX_EXTENSIONS OFF在 MSVC 上影响不大在 MinGW 下决定用-stdc17还是-stdgnu17跨编译器时这个差别会造成“MSVC 编得过、gcc 编不过”的玄学。PUBLIC和PRIVATE的区别是传递性头文件目录对外可见用PUBLIC只自己用的链接项用PRIVATE写反了会在链接期报找不到符号。注意工程目录放在纯 ASCII、无空格、路径别太长的位置比如D:\work\demo。放在桌面或同步盘目录里MSVC 和 Ninja 在长路径下偶发找不到 obj 文件排查起来非常浪费时间。3.2 生成器路线一Visual Studio 多配置Windows 上最省事的是直接生成 VS 工程打开就能调试# -S 源码目录-B 构建目录-G 指定生成器-A 指定目标架构 cmake -S . -B build-vs -G Visual Studio 17 2022 -A x64 # 多配置生成器必须带 --config否则默认走 Debug cmake --build build-vs --config Release --parallel # 产物落在 构建目录/配置名/ 下 .\build-vs\Release\demo.exe三个参数值得单独说。-G后面的名字必须和cmake --help里列出的完全一致多一个空格都会报 “could not create named generator”。-A x64不能省VS 生成器在某些环境里默认走 Win32也就是 32 位目标编出来的 exe 在 64 位系统上照样能跑只是你调了半天才发现指针宽度不对。--parallel不写就单线程编译大工程能差出十几分钟。这条路线的好处是多配置。一个构建目录里同时能出 Debug、Release、RelWithDebInfo切换只靠--config。坏处是生成的一堆.vcxproj你基本不会去看增量构建的判定交给 MSBuild偶尔会出现“明明改了头文件但没重编”的情况删构建目录是最快的解法。3.3 生成器路线二Ninja 单配置想要编译速度、又不想被 IDE 工程文件干扰就用 Ninja。前提是Ninja 本身要单独装CMake 安装包不自带它把ninja.exe所在目录加进 PATH 即可。# 必须在已加载 MSVC 环境的终端里执行 # 例如 “x64 Native Tools Command Prompt”或先跑一次 vcvars64.bat cmake -S . -B build-ninja -G Ninja -DCMAKE_BUILD_TYPERelease cmake --build build-ninja .\build-ninja\demo.exe这里有个极易踩的概念混淆CMAKE_BUILD_TYPE是单配置生成器才认的缓存变量Ninja、Makefiles 属于这一类VS 属于多配置生成器它压根不看这个变量传了也是被忽略。反过来--config对单配置生成器基本无意义。两种生成器混着记参数就会出现“我明明设了 Release为什么产物是 Debug”的情况。如果既要 Ninja 的速度、又要多配置可以选Ninja Multi-Config这时CMAKE_BUILD_TYPE又不管用了回到--config那一套。判断标准很简单构建目录里出现CMakeFiles/并且只有一个配置的产物就是单配置出现Debug/、Release/并列就是多配置。3.4 用 CMakePresets.json 把两条路线固化命令行参数记在脑子里换个终端就忘写进 README 也没人看。3.25 支持 preset schema version 6把两条路线钉进仓库最实在{ version: 6, configurePresets: [ { name: vs-x64, generator: Visual Studio 17 2022, architecture: x64, binaryDir: ${sourceDir}/build/vs }, { name: ninja-release, generator: Ninja, binaryDir: ${sourceDir}/build/ninja, cacheVariables: { CMAKE_BUILD_TYPE: Release } } ], buildPresets: [ { name: vs-release, configurePreset: vs-x64, configuration: Release }, { name: ninja-release, configurePreset: ninja-release } ] }用起来是这样# 配置按 preset 走不用再记 -G / -A cmake --preset vs-x64 # 构建多配置的 preset 里已经写死 configuration cmake --build --preset vs-release # 跑测试同样支持 preset ctest --preset vs-releaseversion: 6对应 3.25写高了老版本 cmake 会直接拒绝读取。architecture只在 VS 这类生成器下有效等价于-A。binaryDir用${sourceDir}变量拼绝对路径避免在哪个目录下执行就生成到哪的随机性。这套文件进版本库之后新同事 clone 下来只有一条命令要记cmake --preset 名字剩下的靠 shell 补全。4. 把 3.25 的新特性用起来作用域、依赖、调试信息三处改动4.1 block()/endblock()给脚本块划作用域老写法里临时改一个全局变量再去add_subdirectory改完得手动改回来忘了就污染后面的所有目标。3.25 的block()直接从语法层面解决set(BUILD_SHARED_LIBS OFF) # 只在这段范围内改 BUILD_SHARED_LIBS 和策略出了 endblock 自动还原 block(SCOPE_FOR VARIABLES POLICIES) set(BUILD_SHARED_LIBS ON) add_subdirectory(third_party/mathlib) endblock() # 这里 BUILD_SHARED_LIBS 仍然是 OFFSCOPE_FOR后面跟VARIABLES、POLICIES或两者表示要隔离哪类状态不写默认两项都隔离。需要把内部算出来的结果带出去用传播列表block(SCOPE_FOR VARIABLES PROPAGATE mathlib_target) add_subdirectory(third_party/mathlib) set(mathlib_target mathlib::core) endblock() target_link_libraries(demo PRIVATE ${mathlib_target})PROPAGATE只列变量名不列的值一律留在块内。我在处理“同一份源码既要按静态库引入、又要按动态库引入”这类需求时用得最多——两次add_subdirectory分别包在各自的 block 里互不打扰比复制一份目录干净得多。4.2 FetchContent 的 SYSTEM 与 EXPORT_NO_SYSTEM第三方代码拉进来编最烦的是它们的头文件把警告刷满-Werror直接变成不可用。3.25 给FetchContent_Declare加了SYSTEM关键字include(FetchContent) # 实际项目里通常换成 GIT_REPOSITORY GIT_TAG # 这里用本地 vendored 目录避免离线环境拉不下来 FetchContent_Declare( mathlib SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/mathlib SYSTEM ) FetchContent_MakeAvailable(mathlib) target_link_libraries(demo PRIVATE mathlib::core)标了SYSTEM之后该目录导出的 include 目录会按系统头处理。编译器支持时较新的 MSVC、gcc、clang 都行CMake 会把包含目录换成/external:I一类的外部头开关并把外部头的警告等级压到最低编译器不支持就退回普通包含目录不报错只是效果打折。这一点决定了你要不要为第三方库单独关警告——以前的做法是target_compile_options里给每个第三方 target 打补丁现在一行SYSTEM完事。配套的EXPORT_NO_SYSTEM是给“我自己的库”用的。如果一个 target 被 install/export 出去而它恰好在 SYSTEM 目录里被创建导出后的使用方会继承“系统头”这个标记。某些情况下你并不希望自己的公开头被下游当成系统头会丢失警告检查这时给该 target 设set_target_properties(tgt PROPERTIES EXPORT_NO_SYSTEM ON)即可。4.3 CMAKE_MSVC_DEBUG_INFORMATION_FORMATPDB 与 /Z7、/ZiMSVC 的调试信息有两种基本放法/Z7塞进.obj/Zi生成独立.pdb。混着用就会出现链接期LNK4099找不到 PDB或者调试器里断点飘。3.25 之前只能在每个 target 上手动加编译选项现在有全局变量cmake_minimum_required(VERSION 3.25) # 使 CMP0141 默认为 NEW # RelWithDebInfo / Debug 生成 PDB其他配置不产生调试信息 set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT $$CONFIG:Debug,RelWithDebInfo:ProgramDatabase)取值上Embedded对应把调试信息嵌进对象文件/Z7ProgramDatabase对应生成独立 PDB/Zi另有一个与 PDB 路径控制相关的取值日常基本用不到。选哪个取决于场景静态库被多个可执行文件复用时/Z7每份 obj 自带调试信息不会出现“链接时找不到那个库的 pdb”全量构建、追求链接速度时/Zi生成共享 PDB 更省空间。这个变量对 target 级也有对应属性MSVC_DEBUG_INFORMATION_FORMAT个别 target 需要特例时覆盖它。提示老项目里cmake_minimum_required写的是 3.20 之类CMP0141 会是 OLD设置这个变量不生效。要么把最低版本抬到 3.25要么单独cmake_policy(SET CMP0141 NEW)。4.4 交叉配置 preset一次 configure 出多套构建传统流程里出一份 Debug、一份 Release要么 configure 两次单配置生成器要么用多配置生成器但每次都得记得敲--config。3.25 的交叉配置 preset 把这个过程写进文件{ version: 6, configurePresets: [ { name: multi, generator: Ninja Multi-Config, binaryDir: ${sourceDir}/build/multi, cacheVariables: { CMAKE_CONFIGURATION_TYPES: Debug;Release;RelWithDebInfo, CMAKE_CROSS_CONFIGS: all, CMAKE_DEFAULT_BUILD_TYPE: Release, CMAKE_DEFAULT_CONFIGS: all } } ], buildPresets: [ { name: debug, configurePreset: multi, configuration: Debug }, { name: release, configurePreset: multi, configuration: Release } ] }CMAKE_CROSS_CONFIGS设成all表示这个 configure preset 允许构建任意配置CMAKE_DEFAULT_BUILD_TYPE在缓存里没写CMAKE_BUILD_TYPE时兜底CMAKE_DEFAULT_CONFIGS决定不带configuration的 build preset 默认编哪些配置。三个变量配合一台机器上cmake --preset multi跑一次后面cmake --build --preset debug和--preset release各来一次就行。代价是产物目录变成build/multi/Debug、build/multi/Release并列脚本里写死路径的地方要跟着改。我的做法是把产物路径统一交给$TARGET_FILE_DIR:...生成器表达式不手写。5. 避坑与排查Windows 上最容易翻车的五个点5.1 工具链与环境现象cmake --version显示 3.20configure 时报 “CMake 3.25 or higher is required”。原因PATH 里有多个cmake.exe排在前面的不是这次装的 3.25。常见来源是某个 IDE 自带的版本它安装时会把自己的目录插到 PATH 靠前的位置。解决Get-Command cmake -All | Select-Object -ExpandProperty Source列出全部命中项看第一条是谁。改 PATH 顺序或者在项目里用会话级 PATH 把 3.25 的bin前置。别靠“重装一遍”解决重装通常不会改顺序。现象用 Ninja 生成器时报No CMAKE_CXX_COMPILER could be found同一个工程换 VS 生成器就正常。原因Ninja 自己不会找编译器它依赖环境变量里已经有cl.exe的路径。普通 PowerShell 或 cmd 里没有 MSVC 环境自然找不到。解决从“x64 Native Tools Command Prompt”启动或者先执行对应版本的vcvars64.bat再 configure。也可以显式指定-DCMAKE_CXX_COMPILERcl但前提还是环境里能找到 cl。这个坑在 CI 脚本里尤其常见因为 CI 的 shell 是干净的。5.2 构建目录与配置现象昨天用 VS 生成器配过今天改成 Ninja重新 configure 直接报一串参数错误或编译器路径无效。原因CMakeCache.txt里记着上次的生成器和编译器绝对路径CMake 不允许在同一个构建目录里换生成器或换编译器。解决删掉整个构建目录重来。3.24 之后有更快的做法cmake --preset 名字 --fresh它会重建缓存但保留构建目录本身。我一般直接在.gitignore里把build*全排掉出问题就整个删。现象本地跑得好好的打出来的包一运行就崩或者性能比预期差一大截。原因用了 VS 这类多配置生成器但cmake --build和cmake --install都没带--config产物落到了 Debug 目录install 出去的也是 Debug 版。解决多配置生成器下凡是--build、--install、ctest都显式带上--config Release或者干脆用 preset 把configuration写死。这条没有捷径只能靠流程约束。5.3 链接与产物现象链接报LNK2038: mismatch detected for RuntimeLibrary。原因静态库用/MT静态 CRT主程序用/MD动态 CRT或者第三方预编译库本身就是另一种。同一次链接里 CRT 模式必须统一。解决全局统一set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$$CONFIG:Debug:DebugDLL)这一句对应/MD和/MDd。然后检查第三方.lib是哪一种——这一步经常被跳过然后花半天怀疑自己代码。实在统一不了就只能把第三方库换成对应 CRT 的版本或者把它源码拉进来自己编。6. 校验与固化把这份工具包变成可复现的环境拿到安装包先做一次完整性校验这一步很多人省出事时又最容易扯皮。.msi或.zip到手用系统自带工具算摘要和发布页给出的值逐位比对# 计算 SHA256输出里那一长串就是要比对的目标 certutil -hashfile .\cmake-3.25.x-windows-x86_64.msi SHA256 # 解压版同样算一遍 certutil -hashfile .\cmake-3.25.x-windows-x86_64.zip SHA256certutil是 Windows 自带的不用装任何东西。比对不一致就别装重新获取一致再往下走至少排除了传输损坏这一层。装完之后跑一遍功能自检别等编译报错才发现工具不全检查项命令通过标准版本一致性cmake --version输出 3.25.x且路径指向本次安装目录生成器可用cmake --helpGenerator 列表里能看到目标生成器测试工具ctest --version版本号与 cmake 完全一致打包工具cpack --version版本号与 cmake 完全一致实际构建cmake --preset 名字configure 无报错缓存里生成器正确第三、四行容易被忽略。cmake、ctest、cpack版本不一致说明 PATH 上混了不同来源的安装后面执行ctest时行为和预期不符排查方向会被带偏。固化这一步我做的是三件事把带版本号的 zip 放进项目外的tools/目录写一个env.ps1只改会话级 PATH把CMakePresets.json提交进仓库里面写死version: 6和生成器CI 脚本里不用cmake这个裸命令一律用绝对路径的cmake.exe。三件事做完同一份代码在任何一台机器上 configure 出来的缓存是一致的出问题只可能是代码问题不会是环境问题。刚入行那会儿我不信这套觉得装个软件而已版本差一点能怎样。后来在一次跨机器联调里本地 3.24、对方 3.25一个SYSTEM关键字的差别让第三方库的警告策略完全不同我的-Werror构建在对方机器上直接红了一片查了一整晚才定位到版本号。从那以后我每换一台机器都先把版本、路径、生成器这三条命令跑一遍再动手写代码。希望帮到你。本文还有配套的精品资源点击获取