CMake实战指南:从安装配置到IDE集成与报错排查

CMake实战指南:从安装配置到IDE集成与报错排查
先说说我自己的经历。前段时间从网上拉了一个开源项目敲下cmake ..屏幕上直接甩出一行红字CMake 3.13 or higher is required. You are running version 3.10.2。那台机器是Ubuntu 18.04自带的CMake就是3.10.2。当时项目里还有一堆代码等着编译我整个人是懵的。后来升级完CMake又遇到VS里打开CMake工程报project configuration failed再后来帮朋友调一个STM32的CMake工程又碰见main函数链接不到的问题。这一串跟CMake相爱相杀的经历让我觉得很有必要把这些常见场景从头到尾捋一遍。这篇内容就是围绕CMake展开的覆盖了安装、入门、IDE集成、常见报错和工具链配置。不管你是第一次接触CMake还是在VS、VSCode、Qt6里被它折磨过这篇文章都值得花十分钟读完。我不讲虚的直接上命令、上代码、上排查思路保证你按着操作就能把问题解决掉。1. 从链接不到main说起CMake到底解决了什么问题1.1 没有CMake之前我们是怎么构建C/C项目的我第一次做C项目的时候用的是纯手写Makefile。一个简单的工程还好无非写几个变量、规则和依赖关系。但一旦项目变大涉及多个目录、多个第三方库、不同平台的差异Makefile就会变得非常痛苦。举个例子Windows上用MSVC编译需要/EHscLinux上用GCC编译要-fPICmacOS上clang的链接参数又是另一套。如果每个平台都写一份Makefile维护成本立刻爆炸。更麻烦的是如果同事用的IDE和你不一样每个人都要自己去折腾编译环境项目根本没法快速跑起来。CMake的出现本质上是把构建规则和平台差异这两件事解耦了。你用一套CMakeLists.txt描述工程里有什么目标、需要哪些源文件、链接哪些库然后CMake根据当前平台的编译器、系统环境自动生成对应的构建文件Windows下可以生成Visual Studio的slnLinux下可以生成Makefile也可以生成Ninja的配置文件。这个思路很像你在餐厅点菜菜单CMakeLists.txt描述的是想吃什么厨房CMake生成器负责根据手头食材平台工具链把菜做出来。1.2 CMake的核心工作流程与心智模型很多初学者拿到工程直接敲cmake .结果各种报错。其实CMake的工作分成两个阶段理解这两个阶段你就成功了一半。第一阶段叫配置阶段Configure。CMake读取CMakeLists.txt检查编译器是否存在、库是否找到、参数是否合法最后生成构建系统文件。如果这个阶段报错通常会在终端里输出很明确的错误信息比如版本太低、找不到Qt、找不到某某库。第二阶段叫构建阶段Build。CMake调用底层工具make或ninja或MSVC来真正执行编译、链接。这个阶段报错那就是编译器、头文件、链接这几层的问题了。我常用的命令组合是这样mkdir build cd build cmake .. cmake --build .很多新手会直接在源码根目录执行cmake .然后发现目录里到处都是CMake生成的临时文件特别乱以后想重新配置还得手动清理。我推荐永远使用独立构建目录也就是上面这三行命令。这样源码目录保持干净想换编译器或者换Release/Debug配置再建一个build目录就行互不干扰。2. 安装与版本管理会装才算入门2.1 各平台安装CMake的正确姿势先看Windows。最简单的方式是去CMake官网下载安装包直接运行msi安装。这里有一个特别容易踩的坑安装时一定勾选Add CMake to the system PATH for all users否则装完以后在cmd里敲cmake会提示找不到命令。如果你已经装完了但没勾选可以重新运行安装包选择Modify或者手动把安装目录默认是C:\Program Files\CMake\bin加进用户环境变量PATH。macOS用户直接用Homebrewbrew install cmakeLinux用户分两种情况。Debian/Ubuntu用apt装起来最快但有一个大坑系统源里的CMake版本往往非常老。Ubuntu 18.04自带3.10.2Ubuntu 20.04自带3.16.3而现在的开源项目普遍要求CMake 3.16甚至3.20以上。如果你只是练习写小项目apt装的老版本暂时够用但一旦从GitHub拉了稍微新一点的项目马上就会遇到版本太低的报错。装完以后一定要验证一下cmake --version顺便确认make或ninja也装好了因为CMake只是生成构建文件的人真正干活的是make或ninja。2.2 版本太旧升级CMake的三种实用办法我遇到过很多次cmake 3.13 or higher is required. You are running version 3.10.2这种报错基本上都是老系统源里版本太旧导致的。这里分享三种升级办法按推荐程度排序。第一种用Kitware提供的官方APT源。这也是我目前在Ubuntu上最推荐的方式一次配置永久生效wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2/dev/null | sudo apt-key add - sudo apt-add-repository deb https://apt.kitware.com/ubuntu/ bionic main sudo apt update sudo apt install cmake注意bionic对应Ubuntu 18.04如果是20.04就换成focal22.04换成jammy别搞错了。第二种用pip安装最新版CMake。这种方式在conda环境里特别实用不会影响系统全局环境pip install --user cmake装完以后可能需要重新登录终端或者在当前shell里刷新一下PATH。第三种源码编译安装。这种方法最万能适用于源里没有新版本、又不想折腾源的场景。CMake编译自己需要的依赖很少过程比较顺利wget https://github.com/Kitware/CMake/releases/download/v3.27.6/cmake-3.27.6.tar.gz tar -xzf cmake-3.27.6.tar.gz cd cmake-3.27.6 ./bootstrap make -j$(nproc) sudo make install编译过程大约需要几分钟到十几分钟取决于机器性能。装完以后用cmake --version确认版本号。说到这个我想起在树莓派上给一个视觉项目装依赖时的经历。树莓派官方系统的源里默认是CMake 3.13而那个项目要求至少3.16。我当时不想折腾系统源直接pip install --user cmake几分钟搞定。对于树莓派这种性能有限的板子源码编译也不是不行就是等着费电。3. 手写第一份CMakeLists.txt从一个可运行的程序开始3.1 最小工程的结构与传统三件套我见过很多入门教程直接甩一大段CMakeLists.txt变量满天飞新手看了直接劝退。其实一个最小工程只需要三个核心命令我先说清楚这三个命令是干什么的你以后再去看任何复杂工程都能抓住主线。假设你有一个最简单的项目目录结构如下HelloCMake/ CMakeLists.txt main.cppmain.cpp内容#include iostream int main() { std::cout Hello, CMake! std::endl; return 0; }那么CMakeLists.txt最小写法是这样cmake_minimum_required(VERSION 3.16) project(HelloCMake) add_executable(hello main.cpp)第一行cmake_minimum_required指定最低CMake版本。很多新手不理解为什么要写这一行其实它是在告诉CMake我这套配置需要至少多少版本才能正确解析。如果你用了某些高版本才引入的语法却不写版本号老版本解析时会猜错行为很容易出现莫名其妙的错误。我建议统一写cmake_minimum_required(VERSION 3.16)兼顾新特性与兼容性。第二行project(HelloCMake)定义工程名。这个工程名会用于生成各种变量比如PROJECT_NAME、PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR。它不会直接生成可执行文件只是一个命名空间。第三行add_executable(hello main.cpp)是真正的核心。它的意思是用main.cpp生成一个名为hello的可执行程序。如果有多个源文件直接在后面列出来就行add_executable(hello main.cpp utils.cpp logger.cpp)然后构建mkdir build cd build cmake .. cmake --build . ./hello终端输出Hello, CMake!3.2 main函数链接不到的5种典型原因很多朋友在VS或VSCode里用CMake工程时会碰到奇奇怪怪的链接错误比如出个LNK2019、未定义的外部符号之类。这里跟大家聊聊main函数链接不到最常见的几种原因以及怎么排查。第一种也是最常见的add_executable里根本没写main函数所在的源文件。比如目录结构是src/main.cpp但你写的是add_executable(hello main.cpp)而CMake处理源文件路径是相对于当前CMakeLists.txt所在的目录所以它会去根目录找main.cpp找不到文件编译的时候自然没有main符号。解决办法就是把路径写对用相对路径add_executable(hello src/main.cpp)第二种源文件路径写成了绝对路径。虽然CMake支持绝对路径但一旦整个项目拷贝到别的电脑路径就全断了。强烈建议只用相对路径让CMake自动基于CMAKE_CURRENT_SOURCE_DIR去解析。第三种检查编译器是否选对。如果你的工程原本是给MinGW用的你却在Visual Studio的CMake集成了里面打开那GCC编译出的目标文件和MSVC的链接器完全不兼容最终报的还是链接错误。解决办法是明确指定生成器和工具链不要指望CMake自动猜。第四种变量被覆盖了。比如有人喜欢这样写set(SOURCE_FILES main.cpp) add_executable(hello ${SOURCE_FILES})然后后面又改了SOURCE_FILES的值或者不小心在某个子目录里把它替换成空结果就是编译时根本没编译main.cpp。这种问题排查起来特别头疼我自己的经验是给变量起名字时别太长太随意而且别在多个地方二次赋值。第五种不是main函数的问题而是链接第三方库时缺少库文件。比如你使用了某个第三方SDK头文件找得到但lib文件路径没配好报错会指向你在代码里调用的那个函数看起来像函数链接不到。这类问题需要检查target_link_libraries和link_directories配置是否正确。3.3 拆源文件时我踩过的坑别再盲目用GLOB刚开始写CMakeLists时我喜欢用file(GLOB SOURCES src/*.cpp)这种写法理由是省事以后新增文件不用改CMakeLists。但后来被坑过好几次。问题在于GLOB是在配置阶段展开的。如果你在src目录下新增了一个 .cpp 文件然后直接按F5构建CMake不会重新执行配置阶段它仍然使用旧的文件列表导致你新加的cpp根本没有参与编译。你必须在构建之前手动重新运行cmake ..或者修改CMakeLists.txt触发重新配置。Visual Studio的CMake集成有时候会自动检测并重新配置但这并不总是可靠。更麻烦的是GLOB会把目录下所有cpp都拉进来如果有一些单元测试文件、示例文件混在一起经常会把不该编译的东西也编了。所以我现在的主力写法是在源文件数量不太多的时候全部显式列出来add_executable(hello src/main.cpp src/utils.cpp src/logger.cpp )如果源文件真的很多可以考虑用一个专门的列表文件或者用target_sources来分层组织但显式列出的可维护性和可读性比GLOB强太多了。4. 把CMake项目跑进IDE里VS、VSCode与Qt64.1 Visual Studio打开CMake项目的两种方式VSVisual Studio对CMake的支持算是比较成熟的了。网上搜vs上如何打开cmake项目出来的结果五花八门其实归结起来就两种方式。第一种方式用CMake先生成sln工程再用VS打开sln。命令行这样操作cmake -G Visual Studio 17 2022 -A x64 ..这样会在当前目录下生成一个工程名.sln和一堆.vcxproj。然后用VS打开sln就跟传统的VS工程一样操作。这种方式适合你已经习惯了VS工程的界面想在IDE里跑、断点调试。第二种方式直接让VS打开CMake文件夹。方法是VS里File - Open - Folder选中CMake项目的根目录。VS会自动识别CMakeLists.txt并在打开时进行配置。你在解决方案资源管理器顶部会看到CMake图标点击可以切换配置、选择目标。这种方式不需要生成slnVS会使用它内置的CMake支持来解析你的工程。我建议能用第二种就用第二种因为它的配置更贴近CMake原生的逻辑。你在CMakeSettings.jsonVS生成的配置文件里可以指定生成器、编译器、配置类型。比如一个简单的CMakeSettings.json可以这么写{ configurations: [ { name: x64-Debug, generator: Ninja, configurationType: Debug, buildRoot: ${projectDir}\\out\\build\\${name}, installRoot: ${projectDir}\\out\\install\\${name}, cmakeCommandArgs: , buildCommandArgs: } ] }VS打开CMake工程的报错绝大多数都发生在配置阶段。比如报CMake project configuration failed. No CMake configuration for binary...通常是因为你的CMakeLists.txt里写死了某个路径或者依赖了某个不存在的库而VS不像命令行那样把堆栈打印得特别清楚。这个时候我建议切到输出窗口里的CMake分类看里面具体的报错信息比盯着错误列表有用得多。4.2 VSCode CMake配置STM32工程用VSCode做STM32开发配合CMake现在是很主流的工作流。核心思路是用arm-none-eabi-gcc作为交叉编译器用CMake的toolchain文件来告诉CMake我们不是在编译宿主机上跑的程序而是在为一个单片机目标编译。我在真实项目中用的最小toolchain文件是这样的set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g) set(CMAKE_ASM_COMPILER arm-none-eabi-as) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)其中CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY这一行特别关键它告诉CMake在检测编译器时不要尝试链接成可执行文件因为ARM交叉编译器默认没有宿主机的启动文件和系统库链接可执行文件会失败而静态库则不会。如果不加这一行CMake在配置早期的编译器检测阶段就会说编译器不工作实际上编译器好好的。在VSCode里配合CMake Tools插件使用你需要在.vscode/settings.json里指定工具链{ cmake.configureArgs: [ -DCMAKE_TOOLCHAIN_FILE${workspaceFolder}/cmake/gcc-arm-none-eabi.cmake, -DCMAKE_BUILD_TYPEDebug ] }然后CtrlShiftP调出命令面板选择CMake: Select a Kit选择GCC for ARM。之后用CMake Tools的构建按钮就能一键编译STM32工程了。我个人用了很长一段时间这个方式比Keil轻量比Makefile直观VSCode的插件生态也比CLion便宜虽然CLion也支持ARM但要花钱。4.3 Qt6项目CMake从可选变成了默认如果你用过Qt5可能还记得qmake是Qt的老牌构建工具。但Qt6发布以后CMake已经成了官方钦点的构建系统新的Qt项目模板默认就是CMake。很多老人从Qt5迁移到Qt6时第一件事就是要弄懂Qt6的CMake语法跟传统CMake有什么区别。Qt6的CMakeLists比普通CMake多出几个专属函数。以最经典的Widgets程序为例cmake_minimum_required(VERSION 3.16) project(MyQtApp) find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_standard_project_setup() qt_add_executable(MyQtApp main.cpp mainwindow.cpp mainwindow.h ) target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)注意qt_standard_project_setup()这一行千万别漏。它会自动设置一些Qt开发中的常见属性比如自动处理MOC、UIC、RCC等工具还可以指定源码所在目录。qt_add_executable和add_executable的区别在于它会自动扫描头文件中的Q_OBJECT宏把需要经过MOC处理的头文件加到MOC流程里。如果你直接用add_executable然后手动管理MOC那工作量会大很多。所以我的建议是在Qt6项目里直接拥抱官方这一套不要再尝试用qmake或者手写MOC命令自己去折腾了。官方既然把CMake这条路铺好了顺着走是最省力的。5. 进阶配置工具链、编码方式与构建目录管理5.1 工具链文件toolchain是交叉编译的灵魂网上搜cmake toolchain搜索量很大说明很多人对工具链文件这一块是模糊的。其实工具链文件就是提前告诉CMake一系列编译器相关的变量常见的有CMAKE_SYSTEM_NAME目标系统名称CMAKE_C_COMPILERC编译器路径CMAKE_CXX_COMPILERC编译器路径CMAKE_FIND_ROOT_PATH搜索库和头文件的根目录为什么要单独写一个工具链文件而不是直接在命令行里传-DCMAKE_C_COMPILER...因为工具链文件里通常还有一系列配套设置比如CMAKE_SYSTEM_NAME、根文件系统路径、搜索路径限制等这些配置很多且相互关联写在文件里方便复用和版本管理。拿树莓派的交叉编译举例。如果你在一台x86的电脑上想编译出ARM架构的树莓派程序就需要set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g) set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)然后在CMake命令行中指定cmake -DCMAKE_TOOLCHAIN_FILE/path/to/toolchain.cmake ..这样CMake会跳过宿主机环境的检测完全按照工具链文件里的定义来配置。需要注意的是交叉编译时第三方库的查找路径往往不是系统默认路径所以经常还要配合CMAKE_PREFIX_PATH指定依赖库的位置。5.2 编码方式也能指定中文注释乱码的根治方法很多人第一次看到cmake如何指定编码方式这个热搜词的时候心里第一反应是CMake还能管这个其实这背后的问题非常典型尤其是在Windows MSVC环境下。MSVC编译器默认按本地代码页中文系统里通常是GBK解释源文件而你的代码如果使用了UTF-8编码现代编辑器的默认选择那么代码里的中文注释、中文字符串就会变成乱码严重的时候还会触发编译错误。解决办法是在CMake里给MSVC加一个/utf-8编译选项if(MSVC) add_compile_options(/utf-8) endif()更精确一点用target_compile_options只作用于特定目标target_compile_options(hello PRIVATE /utf-8)GCC和Clang在大多数Linux环境下默认使用UTF-8所以一般不需要额外设置。但如果你在Windows上使用MinGW的GCC它通常也能正确处理UTF-8源文件不需要额外选项。除了编译选项CMake本身处理文件路径和字符串时也需要注意编码。CMake在Windows下对中文路径的支持一直不太好如果你把工程放在带中文的目录下有些旧版本CMake会崩溃。这个没有特别完美的绕过方案最好的做法是工程路径全用英文别跟自己过不去。5.3 忽略文件怎么写把构建产物挡在git仓库外面很多新手用上CMake之后第一件事就是往git里提交了build目录然后发现每次构建完git status都显示几百个文件变更。你需要在项目提交时排除掉那么cmake的忽略文件怎么写就很关键。这里的忽略文件指的是.gitignore不是CMakeLists.txt。CMake构建产生的文件一般包括build/目录我们构建用的目录CMakeCache.txt缓存了上次配置的参数CMakeFiles/CMake内部生成的临时文件cmake_install.cmake安装脚本Makefile或build.ninja生成出来的构建文件*.user、*.suoVS相关的用户设置我的个人习惯是对于一个CMake项目根目录的.gitignore至少写成这样build/ build-*/ out/ *.user *.suo .cache/ compile_commands.json .vscode/ .idea/compile_commands.json是CMake生成的一个文件很多编辑器比如VSCode的clangd会用到它但它不应该被提交到仓库因为每个机器生成出来的路径不同。.vscode/每个人的插件配置也不同除非你把推荐配置和共享配置严格分离否则不建议提交。我之所以这么强调忽略文件是因为一个干净的仓库能帮你省下大量不必要的git冲突。CMake生成的文件经常包含绝对路径两个人改了同一个构建目录然后同时提交每次合并都头痛欲裂。6. 高频报错与实战排查速查表6.1 版本报错CMake 3.13 or higher is required这个报错我已经遇到好几次了。网上搜cmake 3.13 or higher is required. you are running version 3.10.2基本都是Ubuntu 18.04的老系统。项目作者在cmake_minimum_required里写了3.13以上而系统自带的CMake是3.10.2。排查思路很简单先确认当前CMake版本cmake --version再确认项目要求版本打开CMakeLists.txt看第一行对比之后决定升级CMake还是降项目要求如果项目是你自己维护的可以适当放低要求。但如果是第三方开源项目老老实实升级CMake。这里要特别提醒一点如果你用pip或者源码装了新版CMake有时候旧版CMake还被留在/usr/bin/里。你敲cmake --version看到的可能还是旧版本因为PATH顺序的原因。此时用which cmake看路径如果指向/usr/bin/cmake而不是/usr/local/bin/cmake就需要调整PATH或者把/usr/local/bin放前面。6.2 CMake project configuration failed 的常见触发点VS用户可能见过这种报错:-1: error: CMake project configuration failed. No CMake configuration for binary...。这种情况通常出现在VS的CMake集成里导致配置失败的原因五花八门。我排错一般按这个顺序来先看输出窗口里CMake这个分类的完整日志找到第一行error检查CMakeLists.txt语法是否有明显错误比如括号没闭合或者某个变量名拼错清理CMake缓存重新配置。VS缓存目录一般在out/build下面直接把整个out目录删了重新打开文件夹触发配置确认CMake路径和编译器路径。VS有时候会优先使用系统PATH里的CMake如果你装了太多版本的CMake很容易混乱如果工程里用了find_package检查依赖库是否已经安装、CMAKE_PREFIX_PATH是否指向正确位置我最常犯的错是第2条有一次在一个函数里漏了一个右括号CMake报的错根本不在发生错误的那一行而是在下一个文件末尾。所以当你看到CMake跟你说有语法错误但是位置很奇怪的时候试着把CMakeLists.txt里每一层的括号都检查一遍。6.3 Windows下Conda Python CMake的环境混乱问题最后聊一个比较综合的场景也是我在Windows下做视觉项目时反复踩的坑conda环境、Python、CMake三方纠缠。问题是这样的你在conda里创建了一个虚拟环境python --version明明显示3.10但CMake在配置时find_package(Python3)找到的却是系统自带的Python 3.8或者干脆找不到。这是因为CMake在查找Python时不完全依赖环境变量PATH它的查找策略有自己的一套顺序。解决方法之一是在调用CMake时显式指定Python的路径cmake -DPython3_ROOT_DIRC:/Users/xxx/miniconda3/envs/myenv ..或者在CMakeLists.txt里写set(Python3_ROOT_DIR C:/Users/xxx/miniconda3/envs/myenv) find_package(Python3 REQUIRED COMPONENTS Interpreter Development)类似的还有CMAKE_PREFIX_PATH。当CMake要找各种各样的库时这个变量可以指定搜索根目录多个路径用分号隔开cmake -DCMAKE_PREFIX_PATHC:/Users/xxx/miniconda3/envs/myenv;D:/third_party/libs ..如果你是Windows Orbbec相机的SDK这类外设SDK原理也是一样的SDK的include和lib目录要写进CMake的搜索路径。在Windows下还有一个很隐蔽的坑如果你同时装了Visual Studio的CMake插件、命令行里的CMake、conda里的CMake不同CMake版本很可能共存。配置时往往不是报版本冲突就是报找不到库。我的解决办法是在conda激活对应的环境后在scripts目录里放一个与你环境匹配的cmake然后在PATH里保证conda环境目录排在System32前面。这样每次激活环境cmake就是那个环境对应的cmake不会互相干扰。7. 写在最后我的CMake使用小习惯最后分享一个我个人的经验。CMake排查问题最忌讳的就是不看日志瞎猜。很多人遇到报错直接删build目录然后重新configure发现还是不行又删掉再试一次来回折腾。我现在的习惯是先把完整的报错信息复制下来看第一行是Could NOT find还是CMake Error前者是找不到依赖后者通常是语法或参数问题然后才有针对性地去查。另外CMake的官方文档虽然写得枯燥但它真的是最权威的参考。碰到新函数时不妨先搜一下它的官方页面看例子往往比看别人二手教程更清晰。这个工具学习曲线不算陡只要把configure和build两个阶段分清楚基本就够用了。

最新新闻

日新闻

周新闻

月新闻