FEATURED · 精选文章

CMake学习笔记:从跨平台构建到CUDA/MPI接入的实战指南

发布时间 / 2026/9/9 3:06:03
来源 / 创域科博编辑部
栏目 / 资讯中心
CMake学习笔记:从跨平台构建到CUDA/MPI接入的实战指南 我在第一次被要求用CMake接手一个跨平台项目时内心是拒绝的。当时我习惯了手写MakefileWindows侧则直接在Visual Studio里点下一步配工程总觉得再学一个工具是浪费时间。结果接手的第一个月就被现实反复敲打同一个源码要在Linux和Windows上维护两套构建、同事升级VS版本后工程文件冲突到几乎没法合并、新同学看VS的依赖配置界面一脸茫然。这三件事让我下定决心把CMake从头到尾学一遍于是有了这份学习笔记。它不官方、不完整但覆盖了我实际踩过坑的几个点CMake安装升级、第一个工程怎么写、生成的VS工程里怎么用相对路径、预编译头文件、输出路径管理、在CMake里执行bash命令、接入MPI和CUDA以及那些让人头大的CMake报错到底怎么解。如果你也在学CMake这份笔记应该能帮你省下不少试错时间。1. 为什么要学CMake手写构建配置踩过的坑1.1 手写Makefile和VS工程的真实痛苦先说结论CMake是一个跨平台构建系统生成器它根据你写的CMakeLists.txt配置脚本自动生成Makefile、Visual Studio工程、Ninja文件等。很多新手和我一开始一样把CMake误当成编译器其实它不编译也不链接它负责的是把构建方案翻译成各平台能直接执行的构建脚本。我之前维护Linux项目时Makefile写多了之后最烦的是新增源文件。每加一个.cpp就要去更新OBJ列表头文件路径、依赖关系全靠手工维护。项目一旦超过二十个文件Makefile的可维护性直线下降。Windows那边同样难受。同事用VS2019创建的工程我用VS2022打开后提示升级升级完产生的工程文件diff能把人逼疯。更别提CI环境想命令行编译还得临时去研究devenv命令行的各种参数。这些痛点的根源在于构建配置和具体构建系统、IDE强绑定。换一个平台、换一个IDE就要把构建规则重写一遍。两个人各写一半风格迥异合并时更是灾难。1.2 CMake的设计思路目标与生成器CMake的做法和手写构建文件是相反的。你要做的不是告诉它每个文件怎么编译、怎么链接而是声明这个项目里有几个target每个target是什么类型target之间怎么依赖。剩下的跨平台翻译工作CMake在configure阶段自己完成。我习惯用一个类比去理解CMake它像一份菜品配方Makefile或VS工程是不同厨房灶具的操作说明书。配方不需要为每个灶具单独写一份CMake负责按你选择的generator把配方翻译成对应灶具的说明书。CMake里的target是最核心的概念。一个target可以是一个可执行文件、一个静态库、一个动态库。你可以用add_executable、add_library声明target然后用target_link_libraries声明target之间的依赖关系。CMake会根据这些关系自动推导编译顺序、传递头文件路径。1.3 什么项目最值得用CMake我个人经验是遇到下面这些情况CMake能带来体感极强的改进项目需要在多个平台编译源码同一份构建脚本却要各写各的。需要接入第三方库依赖关系复杂手工维护头文件和库路径非常痛苦。有命令行构建或CI需求希望构建流程是几条命令能跑完的。多人协作希望工程级配置可以通过CMake热更新而不是让别人手动改IDE配置。CMake的学习曲线确实有点陡因为它的CMakeLists.txt算是一套小语言但真正高频用到的语法词其实很少。先把target相关的二三十个命令吃透日常工作就够用了。这也是这份笔记的目标帮你过滤掉那些用不上的冷门内容。2. 安装与版本从下载到升级的实操记录CMake的安装本身不复杂真正坑人的是版本差异。老版本跑新语法会直接报错这时候你往往分不清是语法写错还是CMake版本不支持。我建议先确认版本再决定写法。2.1 Windows下的下载安装细节去cmake.org/download页面下载对应系统的.msi安装包。安装向导里有一个很容易忽略的选项Add CMake to the system PATH for all users这个一定要勾选。不勾选的话你装完后在CMD里敲cmake会显示找不到命令只能从开始菜单打开CMake GUI命令行构建完全用不起来。如果当时忘了勾选补的路径是右键我的电脑 → 属性 → 高级系统设置 → 环境变量 → 在Path中新增CMake的安装目录比如安装路径\bin。设置完要重新打开CMD才会生效。验证安装是否成功只需执行cmake --version能输出版本号就说明PATH没问题。2.2 Win7 32位和旧机器的安装选择现在开发机大多是64位但如果还在用Win7 32位的老机器下载最新版CMake安装包时就要小心了。新版本对老系统支持有限安装包可能在安装阶段就失败提示缺少系统组件。我实际遇到的情况是新版本怎么都装不上后来去官网下载页翻历史版本找到带win32-x86字样的版本比如3.16.x装完就能正常使用。如果你的项目对CMake最低版本没有硬性要求这类老版本的稳定版本完全够用。比如目标编译C17、写简单target之间的链接3.16没有任何问题。另外要提醒Win7 32位老机器上CMake版本往往不是唯一的瓶颈。如果用的编译器不支持C17那CMake配置再新也编不出来。先确认工具链的底线在哪。2.3 Linux、macOS安装与快速升级Linux下大多数人会走系统包管理sudo apt install cmake但Ubuntu自带的源里CMake版本经常偏老比如18.04源里默认还是3.10很多新语法根本用不了。如果不想自己从源码编译推荐直接用pip安装官方打包好的二进制pip install cmake这个包是CMake官方发布在PyPI上的装完在命令行里执行python -m cmake --version若想直接使用cmake命令需要把Python的Scripts目录加入PATH。这种方式特别适合公司内网机器没有sudo权限、又想快速升级CMake的场景实测很稳。macOS就用Homebrewbrew install cmake2.4 升级后别忘了清理build缓存这里有一个非常容易踩的坑。CMake第一次configure时会把编译器路径、生成器类型、各种检测结果写进build目录下的CMakeCache.txt。跨大版本升级后这份缓存里记录的东西可能已经失效。我遇到过升级CMake后反复报找不到编译器的问题折腾半天最后把build目录整个删掉重新configure一切正常。所以升级后第一个习惯动作应该是rm -rf build cmake -S . -B build不要偷懒旧缓存里的脏东西真的会浪费一下午。版本和语法之间的兼容关系我做了一个常用对照表功能特性最低CMake版本备注target_precompile_headers3.16预编译头文件--log-level3.15控制日志级别CUDA语言支持正式化3.93.18后更完善CUDA_ARCHITECTURES属性3.18控制CUDA算力架构个人建议新项目直接把cmake_minimum_required写成3.16或更高省得老版本各种兼容性问题。3. 第一个CMakeLists最小工程和语法逐行拆解看文档十遍不如亲手跑通一个最小工程。这一节我带你把最小的CMake工程从零跑起来。3.1 准备目录和源文件新建一个demo目录里面放两个文件demo/ ├── CMakeLists.txt └── main.cppmain.cpp先写一个最简单的程序#include iostream int main() { std::cout Hello, CMake! std::endl; return 0; }3.2 CMakeLists.txt逐行拆解CMakeLists.txt内容如下cmake_minimum_required(VERSION 3.16) project(Demo VERSION 1.0.0 LANGUAGES CXX) add_executable(demo main.cpp)第一行cmake_minimum_required(VERSION 3.16)是告诉使用CMake的人这个项目至少需要3.16版本的CMake太低就立刻报错。这个版本号要和你的项目实际用到的语法匹配。比如你用了target_precompile_headers就至少写3.16。第二行project(Demo VERSION 1.0.0 LANGUAGES CXX)声明了工程名、版本号和需要用到的编程语言这里写CXX表示只需要C编译器不要求C编译器。如果项目既有C又有C可以写成C CXX。第三行add_executable(demo main.cpp)声明了一个可执行文件目标demo源文件列表是main.cpp。CMake在生成构建文件时会自动处理main.cpp的编译和链接。3.3 加入一个库头文件路径和链接关系真实项目的文件不会这么单一。假设我们还有一个utils库add_library(utils STATIC utils.cpp) target_include_directories(utils PUBLIC include) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE utils)add_library(utils STATIC utils.cpp)创建了一个静态库目标。target_include_directories(utils PUBLIC include)是告诉CMake头文件搜索路径中要包含include目录。这里的PUBLIC关键字非常关键它的意义是utils编译时能找到这个头文件目录而且任何链接了utils的目标也会自动继承这个头文件搜索路径。这样demo只要链接utils就能直接include utils暴露的头文件不需要再单独给demo设置一遍include路径。target_link_libraries(demo PRIVATE utils)用于把utils链接到demo。PRIVATE表示这个依赖只对demo自身的构建有效不会被demo的下游目标继续传递。初学者容易搞混PUBLIC、PRIVATE、INTERFACE三个关键字这里用一句话总结PUBLIC是自己用也让别人用PRIVATE是只有自己用INTERFACE是自己不用只让别人用。搞明白了target的关系CMake的骨架就算搭起来了。3.4 构建流程与build目录的讲究推荐使用新式命令在项目根目录执行cmake -S . -B build cmake --build build --config Release第一条命令交代源码根目录和构建目录第二条命令执行实际编译。在Windows上使用Visual Studio生成器时需要加--config Release来指定构建Release配置否则默认构建Debug。还有很多人习惯的老式写法mkdir build cd build cmake .. cmake --build .两种方式本质相同但新式命令更省事因为不用切换当前目录。构建产物全部生成在build目录内部源码目录保持干净。这里强烈建议永远在build目录里构建不要在源码目录下直接执行cmake .。不然源码目录会被CMake自动生成的CMakeCache.txt、CMakeFiles这些目录污染。我见过有项目把build输出误提交到Git仓库后续维护全是痛苦。4. VS工程生成与相对路径Windows下的实测笔记Windows用户使用CMake的一大场景是生成Visual Studio工程。这一节专门说清楚VS工程怎么生成以及热词里常问的相对路径写法到底怎么处理。4.1 生成VS工程和选择生成器在Windows开发者命令行里CMake默认会优先选择Visual Studio生成器。也可以显式指定cmake -S . -B build-vs -G Visual Studio 17 2022 -A x64-G指定生成器-A指定目标平台架构。生成完成后打开build-vs目录里面会出现.sln和.vcxproj文件直接用Visual Studio打开.sln就能编译调试。VS生成器是多配置生成器一次会生成Debug、Release、RelWithDebInfo、MinSizeRel四种配置。这也是VS生成器和Makefile生成器最大的区别。用Makefile生成器时构建类型是通过CMAKE_BUILD_TYPE在configure阶段固定下来的VS则是同一个工程文件里同时包含多个配置。有一个细节值得注意当你修改CMakeLists.txt后Visual Studio会在下一次生成或构建时自动重新运行CMake configure不需要每次手工用命令行重新生成工程。4.2 CMake里相对路径的正确打开方式生成的VS工程使用相对路径这个需求我理解的核心不是让.vcxproj里所有路径都是相对路径而是保证你的CMakeLists.txt不依赖绝对路径从而让整个工程可以任意搬迁目录、适应不同开发者本机环境。常见的错误写法是add_executable(demo C:/Users/me/projects/demo/main.cpp)这种写法一旦项目换目录就全部失效。正确做法是利用CMake内置变量CMAKE_SOURCE_DIR配置时的源码根目录。CMAKE_CURRENT_SOURCE_DIR当前CMakeLists.txt所在目录。CMAKE_BINARY_DIR当前构建目录。在add_executable里写源文件时直接写相对当前CMakeLists目录的路径就行add_executable(demo main.cpp)如果需要拼接更复杂的头文件路径使用${CMAKE_CURRENT_SOURCE_DIR}target_include_directories(demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)链接外部库时同样使用变量target_link_libraries(demo PRIVATE ${CMAKE_SOURCE_DIR}/third_party/xxx/lib)这样整个CMakeLists里不会出现一个本机绝对路径。项目被拷贝到任何一台新机器执行cmake -S . -B build重新生成一次工程所有路径都会被重新计算。4.3 把资源路径传给代码以及VS调试工作目录很多程序运行时要读取配置文件、资源文件如果代码里写死了一个绝对路径项目一移动就崩。我在CMake里通常是配合target_compile_definitions把路径定义成宏target_compile_definitions(demo PRIVATE ASSETS_PATH${CMAKE_SOURCE_DIR}/assets )这样代码里可以直接使用ASSETS_PATH字符串。因为CMake在configure阶段已经把它展开成了当前项目目录下的assets路径VS工程怎么变都能正确匹配。另一个很实用的是调试工作目录。在VS里按F5调试时默认工作目录是.vcxproj所在目录有时和程序预期的运行目录不一致导致相对路径文件读不到。解决办法set_target_properties(demo PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} )设置后在VS里F5程序的工作目录就是源码根目录读取相对路径文件就不再容易踩坑了。再强调一句生成的.vcxproj文件是产物不要手工去改。需要修改工程属性一定要回CMakeLists里想办法。5. 输出路径管理统一目录和去掉Debug后缀玩CMake一段时间后你一定会被输出产物在哪这个问题烦到。VS生成器下默认行为是每个配置一个子目录比如build/Debug/demo.exebuild/Release/demo.exe。想要统一输出目录以及解决Debug版本exe自带的d后缀问题这一节是实操答案。5.1 默认输出路径到底有多乱不设置任何输出目录变量时Visual Studio生成器会把可执行文件放在build/Debug或build/Release目录下动态库放另一个目录静态库可能还会放到特定子目录。如果你的项目同时有多个可执行文件、多个库打包脚本去固定位置找文件就非常痛苦。Makefile生成器则是单配置直接放在build目录下相对清爽。但跨平台项目不可能只用Makefile所以统一输出目录是值得做的一件事。5.2 用输出目录变量统一产物位置CMake提供了三个最常用的输出路径变量变量作用CMAKE_RUNTIME_OUTPUT_DIRECTORY可执行文件以及Windows下的DLLCMAKE_LIBRARY_OUTPUT_DIRECTORYLinux/macOS下的动态库、模块库CMAKE_ARCHIVE_OUTPUT_DIRECTORY静态库、导入库在CMakeLists.txt里可以这样设置set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)这样构建完成后exe和DLL统一进bin目录静态库进lib目录。但需要注意VS多配置生成器下CMake会自动在目标目录后面追加配置名实际产物会出现在bin/Debug和bin/Release。如果你希望Debug和Release的产物直接在同一个bin目录里可以针对每个配置单独覆盖set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_SOURCE_DIR}/bin) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_SOURCE_DIR}/bin)这种方式确实能去掉配置子目录但后果是Debug和Release的可执行文件同名时后构建的会覆盖先构建的。我个人的建议是开发调试阶段临时用一用没问题正式打包还是保留配置子目录避免版本混乱。5.3 去掉Debug后缀的正确姿势热词里cmake输出路径去掉debug还有一种常见表现那就是MSVC编译Debug配置时CMake默认会给你生成带d后缀的可执行文件或动态库比如demo_d.exe、demo_d.dll。这是Windows平台约定俗成的习惯目的是让Debug版和Release版可以同时存在于同一目录而不互相覆盖。如果你希望Debug版也叫demo.exe有两种写法。全局关闭set(CMAKE_DEBUG_POSTFIX )只针对某个目标关闭set_target_properties(demo PROPERTIES DEBUG_POSTFIX )这两种写法我实测都能生效。但还是那个建议去掉d后缀会让Debug和Release二进制在同一个目录时发生覆盖。如果只是因为脚本调用不匹配而想去掉后缀不如让脚本根据配置选择文件名如果是个人开发图输出干净去掉也无妨。6. 预编译头文件PCH给大型项目提速的正确姿势C工程编译慢很大一部分时间花在反复处理重复的公共头文件上。每个源文件都要去读一遍 、 、 编了上一百个文件就白读一百遍。预编译头文件的思路是把这些公共头文件先灌进一个PCH后续源文件编译直接复用结果从而显著提速。6.1 什么时候才值得上PCH如果你的项目只有几个源文件别折腾PCH收益可以忽略。但如果是几十上百个源文件、每个文件都include一大串STL和第三方头文件预编译头带来的提速体感会非常明显。我实测过一个几万行的C项目启用PCH后全量编译时间从约6分钟降到约4分钟增量编译从30秒降到10秒以内。代价是首次configure之后第一次全量编译会额外生成PCH文件有时候反而更慢。所以PCH适合持续开发的中大型项目不适合一次性构建任务。6.2 target_precompile_headers与指定PCH文件CMake自3.16开始提供了标准的target_precompile_headers命令跨MSVC、GCC、Clang统一处理。这是最推荐的预编译写法target_precompile_headers(demo PRIVATE vector string iostream )尖括号形式表示让CMake自动把这些系统头文件放进预编译列表。另一种是自定义PCH文件比如创建pch.htarget_precompile_headers(demo PRIVATE [[pch.h]] )这里的[[pch.h]]是CMake的特殊写法用于表示一个头文件路径让脚本不把它误解析成表达式命令。使用这种自定义PCH文件的方式就是我们常说的指定precompiledheaderfile。它的效果是CMake在生成VS工程时会自动把工程的预编译头属性指向pch.h。你在Visual Studio里打开项目能看到C/C → 预编译头里已经设置了使用/Yu和头文件名。如果你同时使用自动列表模式和自定义PCH文件我的建议是选择其中一种即可。自动列表的好处是你不必维护一个pch.h文件自定义PCH的好处是PCH内容对你来说完全可见不容易出现到底预编译了啥的黑盒感。6.3 PCH使用中的坑PCH不是银弹用不好反而添乱。第一PCH里的头文件一旦改动所有依赖这个目标的所有源文件都会重新编译。所以PCH应该放稳定不变的STL头文件和第三方库头文件千万别把自己正在频繁修改的业务头文件塞进去否则每次改动都会触发一次全量重编。第二PCH文件名在不同编译器下行为不同MSVC生成的是.pch文件GCC/Clang生成的是.gch文件而且位数、编译选项变了都要重新生成。不要试图手动指定这些中间产物路径交给CMake处理就好。第三调试时偶尔会遇到无法进入PCH内部函数定义的怪异现象这是预编译带来的副作用。日常业务代码调试基本不受影响但如果你重度依赖调试器去查看STL内部细节需要有个心理预期。7. 在CMake里执行命令execute_process与add_custom_command在CMake里执行bash命令这个需求本质上要分清是在哪个阶段执行。很多新手踩坑就是因为把配置阶段和执行阶段搞混了。7.1 两个阶段必须分清configure和buildCMake整个流程分成两个大阶段第一个是configure阶段也就是你执行cmake -S . -B build的时候。CMake会读取CMakeLists.txt探测编译器、查找依赖然后生成构建文件。这个阶段用execute_process执行命令。第二个是build阶段也就是你执行cmake --build build或者make/ninja的时候。这个阶段用add_custom_command和add_custom_target执行命令。我见过有同事在CMakeLists里写execute_process(COMMAND rm -rf ${CMAKE_BINARY_DIR})结果每次配置构建目录就被自己删掉了CMake当场崩溃。虽然是个极端例子但足以说明阶段混淆的杀伤力。7.2 execute_process配置阶段执行一次最简单的用法是获取Git提交信息生成一个版本号execute_process( COMMAND git rev-parse --short HEAD OUTPUT_VARIABLE GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ) message(STATUS Git hash: ${GIT_HASH})OUTPUT_STRIP_TRAILING_WHITESPACE必须加否则变量末尾会带一个换行符影响后续使用。还要注意如果当前路径不在Git仓库里这条命令会失败。更健壮的写法是检查RESULT_VARIABLEexecute_process( COMMAND git rev-parse --short HEAD OUTPUT_VARIABLE GIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE GIT_RESULT ) if(GIT_RESULT EQUAL 0) message(STATUS Git hash: ${GIT_HASH}) else() message(WARNING Failed to get git hash) endif()如果只是想在configure阶段生成一个文本文件其实还有更简洁的办法不需要执行外部命令file(WRITE ${CMAKE_BINARY_DIR}/build_info.h #define GIT_HASH \${GIT_HASH}\\n )能用CMake内建命令解决的就不要调外部命令这是提升跨平台兼容性的基础原则。7.3 在CMake中执行bash命令以及add_custom_command的用法热词里的cmake执行bash命令在CMake里是通过COMMAND bash -c ...实现的。比如execute_process( COMMAND bash -c echo hello ${CMAKE_BINARY_DIR}/hello.txt )但Windows机器默认没有bash除非你装了Git Bash或者WSL。所以这里需要引出一个重要建议跨平台项目尽量使用cmake -E命令族比如cmake -E copy_directory、cmake -E remove_directory、cmake -E echo这些在Windows、Linux、macOS上都稳定可用。构建阶段需要生成文件时更可靠的是add_custom_command。下面是一个生成version.h的完整例子add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/generated/version.h COMMAND ${CMAKE_COMMAND} -E echo #define VERSION \1.0.0\ ${CMAKE_BINARY_DIR}/generated/version.h DEPENDS version.txt COMMENT Generating version.h )然后把这个生成文件作为源文件加入目标CMake会自动在编译前先执行这个命令add_executable(demo main.cpp ${CMAKE_BINARY_DIR}/generated/version.h) target_include_directories(demo PRIVATE ${CMAKE_BINARY_DIR}/generated)如果你希望每次构建都执行某个脚本而不是只在文件缺失时执行就用add_custom_target配合ALLadd_custom_target(run_script ALL COMMAND bash script.sh COMMENT Running script )说实话那种特别复杂的bash脚本我不建议在CMakeLists里用字符串拼。引号转义、换行、环境变量一堆问题写起来容易让人崩溃。更靠谱的做法是把bash脚本独立成一个.sh文件然后在CMake里只写COMMAND bash script.sh让脚本内部去处理复杂逻辑。8. MPI与CUDA接入科学计算场景两个典型报错复盘科学计算领域的C项目经常要接入MPI做并行或者用CUDA做GPU加速。这两个场景在CMake里都有标准做法但报错时的排查过程值得单独复盘。8.1 引入MPIfind_package(MPI)的标准流程MPI的引入比想象中简单前提是系统里已经装好了MPI实现。Windows上一般装Microsoft MPILinux上装OpenMPI或MPICH。以Ubuntu为例sudo apt install libopenmpi-devCMake侧只需要三步find_package(MPI REQUIRED) add_executable(mpi_demo mpi_demo.cpp) target_link_libraries(mpi_demo PRIVATE MPI::MPI_CXX)find_package(MPI REQUIRED)会去系统默认路径和PATH环境变量里查找MPI编译器包装器比如mpicxx和头文件。REQUIRED关键字表示找不到就直接报错避免配置出一个根本编译不了的工程。如果报Could NOT find MPI排查顺序是确认MPI开发包真的装了注意是开发包而不是运行库。Windows上要装MS-MPI SDK而不是只装Redistributable。检查PATH或环境变量里有没有MPI相关路径。手动指定cmake -S . -B build -DMPI_CXX_COMPILERmpicxx使用MPI时最容易踩的坑是位数不匹配。VS生成x64工程但MS-MPI安装的是32位SDK会导致链接阶段各种找不到符号。装MS-MPI时注意选择64位版本。8.2 CUDA接入与CMAKE_CUDA_COMPILER_NOT_SET报错CMake从很早就支持CUDA真正成熟是在3.9以后。现在的写法是在project里直接声明CUDA语言cmake_minimum_required(VERSION 3.18) project(CudaDemo LANGUAGES CXX CUDA) add_executable(cuda_demo main.cu) set_target_properties(cuda_demo PROPERTIES CUDA_STANDARD 17 CUDA_ARCHITECTURES 75 )这个配置本身不难真正折磨人的是CMake报错CMake Error: CMAKE_CUDA_COMPILER is not set. After EnableLanguage这个错误的出现说明CMake尝试启用CUDA语言却在系统里找不到nvcc编译器。可能原因有三个CUDA Toolkit没安装或者安装的是显卡驱动而不是完整的Toolkit。nvcc不在PATH环境变量里。之前configure失败过一次CMakeCache里留下了错误的空缓存。排查链路我建议按这个顺序走第一步先确认CUDA编译器存在。打开终端执行nvcc --version如果提示找不到命令去确认CUDA Toolkit的安装位置。Windows常见路径是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.x\bin\nvcc.exeLinux常见路径是/usr/local/cuda/bin/nvcc。第二步把nvcc所在目录加入PATH。Windows上加环境变量后要重开CMDLinux上加到~/.bashrc并source。第三步如果PATH已经正常但还是报错那基本就是缓存问题手动指定编译器并强制重新configurecmake -S . -B build -DCMAKE_CUDA_COMPILER/usr/local/cuda/bin/nvccWindows下路径带空格要写成cmake -S . -B build -DCMAKE_CUDA_COMPILERC:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.2/bin/nvcc.exe第四步如果还不行删掉build目录重来rm -rf build cmake -S . -B build -DCMAKE_CUDA_COMPILER/usr/local/cuda/bin/nvcc这套链路能解决绝大多数CMAKE_CUDA_COMPILER_NOT_SET的问题。还有一个容易忽略的坑CUDA_ARCHITECTURES最好显式指定CMake 3.18后如果不设置会输出警告某些编译器组合下还会默认成非常保守的算力导致生成代码没法利用GPU特性。8.3 通用排错三板斧不管是MPI还是CUDA包括其他第三方依赖的查找失败我的排查套路都差不多统称三板斧第一板斧删除build目录重新configure排除CMakeCache缓存问题。这一步能解决一半以上的疑难杂症。第二板斧在命令行里手动指定相关工具的绝对路径绕过PATH查找和缓存残留。比如-DCMAKE_CUDA_COMPILER...、-DMPI_CXX_COMPILER...、-DCMAKE_C_COMPILER...。第三板斧打开CMake的调试输出看它到底去哪些路径找过。比如cmake -S . -B build --debug-find这个方法在find_package失败时非常直观能告诉你CMake是否真的搜索了你以为它会搜索的路径。下面汇总一下这两个场景的常见问题报错现象可能原因解决手段Could NOT find MPI未安装MPI开发包安装libopenmpi-dev或MS-MPI SDK找不到mpicxxPATH未包含MPI编译器设置MPI_CXX_COMPILER为绝对路径CMAKE_CUDA_COMPILER not set未安装CUDA Toolkit或nvcc不在PATH安装Toolkit设置PATH或指定编译器CUDA架构相关的警告未设置CUDA_ARCHITECTURES在target上显式设置CUDA_ARCHITECTURES9. 日志分级与排查从message到--trace最后说一个很多人忽略但极其有用的主题CMake的日志和调试。你自己写CMakeLists也好排查现成项目也好掌握日志分级能让你少抓狂很多。热词里的cmake loglevel指的就是这个。9.1 message的级别到底怎么分多数人学习CMake时只知道message(xxx)和message(STATUS xxx)。但CMake的message其实分了多个级别不同级别默认是否显示还不一样。常用级别如下级别用途默认是否显示message(FATAL_ERROR ...)报错并终止configure显示message(WARNING ...)警告显示message(STATUS ...)状态信息显示带--前缀message(VERBOSE ...)详细信息不显示message(DEBUG ...)调试信息不显示message(TRACE ...)跟踪信息不显示理解了这点就会明白为什么你写了message(VERBOSE xxx)却看不见输出。不是它没执行而是默认日志级别把它屏蔽了。我在项目里的习惯是一次性状态信息用STATUS比如Enabled modules: xxx辅助排查信息用VERBOSE和DEBUG平时不污染终端需要排查时再用参数打开。9.2 用--log-level控制输出级别CMake从3.15开始支持--log-level参数cmake -S . -B build --log-levelVERBOSE这样message(VERBOSE)里的内容就会输出。如果输出量太大可以只开DEBUG或TRACE。对应关系是--log-levelERROR只报错。--log-levelWARNING报错和警告。--log-levelNOTICE默认级别。--log-levelSTATUS显示状态信息。--log-levelVERBOSE显示详细。--log-levelDEBUG显示调试信息。--log-levelTRACE显示全部跟踪。除了命令行参数也可以在CMakeLists里设置set(CMAKE_MESSAGE_LOG_LEVEL VERBOSE)这个变量可以写在脚本开头也可以作为-D参数传入。我的建议是不要随便把项目默认级别改成VERBOSE因为会刷屏用命令行参数按需打开就好。9.3 --trace和--debug-output定位诡异问题的终极武器有些CMake问题靠几个message是排查不完的比如变量在某处被意外覆盖、if分支走向不对。这时候就需要看CMake到底逐行执行了什么。第一个武器是cmake --trace -S . -B build--trace会把CMakeLists中每一行执行的代码原样打印出来。它的缺点是变量不会被展开看到的一堆变量名比较抽象。想看到展开后的真实值加一个参数cmake --trace-expand -S . -B build这个命令输出的信息量非常大通常会重定向到文件里看cmake --trace-expand -S . -B build trace.log 21另一个不够常用但很实用的参数是--debug-output它会让CMake输出更多内部诊断信息。在配合第三方模块查找问题时可以打开。如果你正在查的是find_package相关的问题那我最推荐的是上一节提过的cmake -S . -B build --debug-find它能输出CMake在查找包时的详细路径列表比自己瞎猜路径高效太多。写到这里我发现CMake学习真正的核心其实不在语法而在理解目标、生成器、阶段这三个概念。语法忘了查文档就行概念搞不清就会一直在报错和猜测之间打转。从最小工程开始练手逐步加入目标链接、输出路径管理和PCH再往MPI、CUDA这些方向扩展是一条走起来比较稳的路线。最后分享一个我把这套流程跑熟后养成的小习惯在每份CMakeLists里用VERBOSE级别写几个关键变量的输出需要排查时用--log-levelVERBOSE统一打开这样既不会平时刷屏出问题时又能马上看到上下文。比起出了问题再去临时加message重新跑配置这个习惯能省下大量来回尝试的时间也是我想留给后来人最实用的一条经验。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻