资讯详情

CMake入门指南:从零学会跨平台构建与CMakeLists.txt配置

📅 2026/10/20 4:06:38 | 华诺云谱 👁 阅读
CMake入门指南:从零学会跨平台构建与CMakeLists.txt配置
如果你跟我一样写 C 代码的历史里有一半时间都花在“手动敲编译命令、然后被各种链接错误支配”这件事上那 CMake 绝对是你绕不过去的一个工具。简单说CMake 是一个跨平台的构建系统生成器它不直接编译代码而是根据你写好的 CMakeLists.txt 规则生成当前平台需要的构建文件——在 Linux 上是 Makefile在 Windows 上可能是 Visual Studio 工程在 macOS 上可以是 Xcode 工程你不换代码就能在不同平台间复用同一套构建逻辑。这篇文章我会从安装开始把 CMake 的基本使用给你完整捋一遍包括 CMakeLists.txt 怎么写、构建流程怎么走、常见报错怎么排查全都是我实际用下来摸索出来的经验适合刚接触 CMake 或者一直被各种编译问题劝退的同学。CMake 的学习门槛其实不高真正难的是很多人一开始就被“生成器”“目标”“构建目录”这些概念绕晕。我会用尽量直白的方式讲先把 CMake 到底干了什么事说清楚再带你上手写第一个项目。你不需要一次记住所有命令只要跟着我搭好一个能跑的工程后面的细节慢慢就会融会贯通。1. 项目结构设计先搞清楚 CMake 站在哪一层1.1 为什么需要 CMake 这个中间层直接写 Makefile 不行吗对于单个平台、单个编译器、不依赖第三方库的小项目Makefile 完全够用。但一旦你需要同时支持 Linux、Windows、macOS或者要动态切换编译选项、引入一堆第三方依赖手写 Makefile 就会变得非常痛苦。CMake 的出现就是为了解决“同一份构建逻辑跨平台复用”的问题。CMake 的工作模式可以这样理解你写的是“构建剧本”CMakeLists.txt描述项目里有哪些源文件、要生成什么目标、需要链哪些库。CMake 读取这份剧本结合当前系统和编译器信息生成对应的构建系统文件Makefile、Ninja 文件、Visual Studio 工程等。然后用生成的构建工具去执行真正的编译和链接。也就是说你不需要关心各个平台的编译命令差异CMake 帮你翻译成了对应平台能理解的语言。这也是它叫“构建系统生成器”而不是“编译器”的原因。1.2 CMake 的两个阶段配置和生成CMake 在使用时分两个阶段。第一阶段是配置阶段执行cmake命令读取 CMakeLists.txt检查编译器、头文件、库文件然后生成构建文件第二阶段是构建阶段执行cmake --build或者直接调用生成的构建系统比如make此时才会真正编译你的源码。很多人第一次跑cmake ..后看到一大堆输出就慌其实大部分信息是配置阶段的检查结果。比如它会找编译器、检查语言特性、查找依赖库等。如果这一步顺利你就会看到Generating done的字样如果这里报错通常是你 CMakeLists.txt 写的条件不满足或者环境里缺了什么工具。所以当你遇到问题时先分清是配置阶段还是构建阶段的问题。配置阶段的问题多半是找不到编译器、找不到依赖、语法错误构建阶段的问题才跟你的源码有关比如编译错误、链接错误。1.3 源码目录与构建目录分离CMake 有个重要的设计原则编译产生的临时文件和最终产物不要和源码混在一起。你可以在源码目录旁边单独建一个 build 目录在 build 目录里执行cmake 源码路径所有生成的 Makefile、中间文件、可执行文件都会放进这个目录源码目录保持干净。这种“外部构建”的方式是我强烈建议你从一开始就养成的习惯。我见过太多人直接在源码目录里执行cmake .结果生成一堆 CMakeCache.txt、CMakeFiles 文件夹代码仓库里总是出现各种不明文件维护起来非常折磨。外部构建的好处是你随时可以删掉 build 目录重新来不会污染源码也不会误把临时文件提交到消息管理里而且同一个源码可以同时生成多个配置比如一个 Debug 一个 Release放进不同目录。2. 安装 CMake三个平台的坑我都替你踩过了2.1 Linux 下安装版本号比想象中重要Linux 下安装 CMake 最省事的方式是用系统包管理器。Ubuntu 和 Debian 系执行sudo apt update sudo apt install cmake如果你是 CentOS、RHEL、Fedora用sudo yum install cmake # 或者 sudo dnf install cmake不过这里有个很容易被忽略的问题包管理器自带的版本通常比较旧。比如某些老版 Ubuntu 仓库里的 CMake 可能停留在 3.16 甚至更早而很多现代项目的 CMakeLists.txt 会要求最低版本 3.20 以上。如果你只需要编译一些基础项目系统自带的版本够用但如果你准备参与开源项目或者想体验新特性我建议直接用 CMake 官方发布的安装脚本。官方会发布类似cmake-3.29.3-linux-x86_64.tar.gz的二进制压缩包把压缩包下载到/opt或~/tools目录解压然后把bin目录加到 PATH 里就行。这样你拿到的是一个非常新的版本而且不影响系统自带的包。2.2 Windows 下安装安装包和配置环境变量Windows 上安装最简单的方法是去 CMake 官方网站下载 Windows 安装程序.msi。安装过程中它会问你是否把 CMake 添加到 PATH我建议你勾选上尤其是“Add CMake to the system PATH for all users”这个选项。如果没有勾选后面你去命令行敲cmake就会提示“不是内部或外部命令”还得自己手动补环境变量麻烦得很。装完以后你可以在“开始菜单”里找到“CMake GUI”它提供一个图形化界面让你填源码目录、构建目录、选生成器点 Configure 按钮完成配置再点 Generate 生成构建文件。图形界面适合不想敲命令的初学者但实际开发中命令行更灵活也更好脚本化所以我推荐你还是尽早习惯命令行用法。如果你的 Windows 上装了 Visual StudioCMake 会自动检测到 VS 的各个版本并可以使用-G Visual Studio 17 2022这种参数指定生成器。用 VS 作为生成器时CMake 会生成一个.sln解决方案文件双击就能在 Visual Studio 里开发调试体验很顺。2.3 macOS 下安装Homebrew 一把梭macOS 用户最舒服的安装方式是 Homebrewbrew install cmake这个命令会给你装好最新版 CMake同时把依赖都处理好。如果你还没装 Homebrew那先去装 Homebrew它是 macOS 上必不可少的包管理器。装完 CMake 后终端里直接敲cmake --version就能看到输出。除了 Homebrew你也可以从官网下载 macOS 安装包或者用我要安装在/Applications/CMake.app/Contents/bin/cmake的方式但那样还得改 PATH不如 Homebrew 省心。2.4 验证安装cmake --version无论哪个平台安装完成后先执行一条命令验证cmake --version输出类似cmake version 3.29.3 CMake suite maintained and supported by Kitware (kitware.com/cmake).如果能看到版本号说明安装成功。如果系统提示找不到命令请先检查你的 PATH 环境变量是否配置正确。这时候不要急着重装先确认是不是环境变量没生效——我见过太多人卡在这一步明明装了但终端找不到。另外如果你想确认 CMake 支持哪些生成器可以执行cmake --help其中有一段会列出所有可用的生成器比如 Unix Makefiles、Ninja、Visual Studio 17 2022 等。这个信息在排查生成器相关错误的时候很有用。3. 第一个项目从零写出可运行的 CMakeLists.txt3.1 最基础的项目目录结构假设我们要做一个超级简单的 C 项目只有一个主文件输出 “Hello CMake”。项目目录长这样hello_cmake/ ├── CMakeLists.txt └── main.cpp这里的CMakeLists.txt就是整个项目的构建配置文件所有 CMake 指令都写在这个文件里。main.cpp是源码文件。这里先说明一下CMakeLists.txt 的文件名是固定的不能写错也不能改叫别的。CMake 执行时会自动在当前或指定目录里寻找这个文件如果找不到直接报错让你怀疑人生。3.2 最简 CMakeLists.txt 的写法打开 CMakeLists.txt写入cmake_minimum_required(VERSION 3.16) project(HelloCmake LANGUAGES CXX) add_executable(hello_cmake main.cpp)这三行就是 CMake 工程的骨架cmake_minimum_required(VERSION 3.16)声明最低 CMake 版本。这是为了防止你用新版命令去跑旧版 CMake 时产生莫名其妙的错误这是项目管理上的一个保险丝。project(HelloCmake LANGUAGES CXX)定义项目名称并告诉 CMake 这是一个 C 项目。如果你不声明 LANGUAGESCMake 默认会同时探测 C 和 CXX没有实际坏处但显式声明是更清晰的习惯。add_executable(hello_cmake main.cpp)是最核心的命令意思是用main.cpp编译并链接出一个名为hello_cmake的可执行文件。如果后续你还想加其他源文件就往后面的列表里继续加。写好以后进入项目根目录建立 build 目录并进入执行 CMake 配置和构建。3.3 构建三步走配置、编译、运行在项目根目录下执行mkdir build cd build cmake ..这里的..指的是源码目录CMake 会去hello_cmake目录下找 CMakeLists.txt。这一步执行完build 目录里会生成一堆文件包括 Makefile、CMakeCache.txt 等。如果输出结尾出现-- Build files have been written to: /path/to/hello_cmake/build就说明配置阶段成功。接着执行cmake --build .这个命令会调用构建系统我们这里用的是 Makefile所以实际上等于执行make把 main.cpp 编译成可执行文件。编译成功后你会看到 build 目录里出现一个hello_cmake可执行文件Windows 下是hello_cmake.exe。最后运行./hello_cmake如果输出Hello CMake恭喜你第一个 CMake 项目已经跑通了。我建议你把这三步刻在脑子里cmake ..配置、cmake --build .构建、直接运行产物。以后无论项目多复杂最核心的流程不会变。4. CMake 核心概念与常用命令逐个拆解4.1 版本声明和项目声明cmake_minimum_required这句很多人写项目时会忽略觉得无关紧要。实际上如果你用了一个高版本才有的命令而别人用低版本 CMake 运行你的项目就会收到“CMake 3.10 does not support ... ”这类错误。我一般会查看自己当前 CMake 的版本然后声明一个比它略低的版本比如当前是 3.29我就写 3.16既支持了绝大多数功能又不至于过高地限制别人。project命令除了定义名字还会定义一些变量比如PROJECT_NAME、PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR。这两个变量在大型项目中非常常用分别指向源码根目录和构建目录。比如你有一个配置文件需要拷贝就可以用${PROJECT_SOURCE_DIR}/config/xxx.ini来定位源文件位置而不是写死绝对路径。4.2 生成可执行文件add_executableadd_executable是 CMake 中最常用的命令。它的第一个参数是生成的目标名第二个及以后是参与编译的源文件列表。目标名不需要和可执行文件名完全一致但为了方便我通常保持一致。如果源文件只有一个直接把文件名写在后面如果有很多可以用空格分隔罗列也可以使用变量来收集。比如set(SOURCES src/main.cpp src/utils.cpp src/logger.cpp ) add_executable(my_app ${SOURCES})这个写法在项目变大的时候尤其好用。你把所有源文件放在一个变量里后面想加几个文件只需要修改set(SOURCES ...)那一块不需要改动其他命令。4.3 生成库文件add_library当你的项目不是简单的主程序而是包含几个模块时把公共代码拆成库是一个好习惯。CMake 里用add_library来生成静态库或动态库add_library(calc STATIC src/calc.cpp) add_library(calc_shared SHARED src/calc.cpp)STATIC生成静态库Linux 下是.aWindows 下是.libSHARED生成动态库Linux 下是.soWindows 下是.dll。一般小项目我更喜欢静态库因为部署一个可执行文件就够了不用额外考虑动态库的搜索路径。库目标生成以后其他目标可以通过target_link_libraries来链接它。这里注意一个概念在 CMake 里“目标”包括可执行文件、库文件它们之间可以做依赖关联。CMake 会自动推导出编译顺序不需要你手动去管“先编译库还是先编译主程序”。4.4 头文件路径与链接库目标属性是关键假设项目里有独立的头文件目录include源文件在src下那你需要让编译器能找到头文件。最直观的方式是target_include_directories(my_app PRIVATE include)target_include_directories的作用是告诉编译器在编译my_app时要从include目录里搜索头文件。这里的PRIVATE关键字是传递范围限制指这个目录只对当前目标生效不会影响别人。稍微复杂一点的场景是你有一个库目标calc它自己的源文件需要include目录同时可执行目标my_app链接了calc也需要拿到calc的头文件路径。如果你只在calc上做target_include_directories(calc PUBLIC include)并且之后target_link_libraries(my_app PRIVATE calc)那么my_app也会自动获得include目录。这就是PUBLIC和PRIVATE的意义。如果你用 PRIVATE 把头文件路径限定在calc内部my_app去包含calc.h时就找不到了。这个传递机制是 CMake 里最容易让人迷糊的地方也是很多“找不到头文件”问题的根源。target_link_libraries的语法是target_link_libraries(target_name PRIVATE calc )如果你链接的是系统库比如m数学库、pthread直接写上库名即可target_link_libraries(my_app PRIVATE m pthread)4.5 引入子目录add_subdirectory实战项目通常会划分成多个层级比如最外层 CMakeLists.txt 通过add_subdirectory(calc)引入 calc 子目录而calc/CMakeLists.txt里定义自己的目标。这样做的好处是每个模块的构建规则就近管理不需要在一个巨大的 CMakeLists.txt 里塞下所有东西。比如顶层 CMakeLists.txt 可以写成cmake_minimum_required(VERSION 3.16) project(MiniCalc) add_subdirectory(calc) add_executable(my_app main.cpp) target_link_libraries(my_app PRIVATE calc)然后calc/CMakeLists.txt写add_library(calc STATIC calc.cpp) target_include_directories(calc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)这里${CMAKE_CURRENT_SOURCE_DIR}表示当前 CMakeLists.txt 所在目录用它拼出 include 绝对路径可以避免各种相对路径出错。CMake 本身对相对路径有一套规则但新手很容易绕晕所以我建议在子目录里总是用这个变量来定位当前模块路径。5. 一个完整的小型实战项目多文件 子目录 库前面把命令都过了一遍现在我们来把这些东西串起来做一个 MiniCalc 项目。这个项目会包含一个calc静态库、一个main.cpp主程序结构大概是这样MiniCalc/ ├── CMakeLists.txt ├── main.cpp └── calc/ ├── CMakeLists.txt ├── include/ │ └── calc.hpp └── src/ └── calc.cpp5.1 calc 模块的 CMakeLists.txtcalc/include/calc.hpp里声明一个简单的加法函数#ifndef CALC_HPP #define CALC_HPP int add(int a, int b); #endifcalc/src/calc.cpp里实现#include calc.hpp int add(int a, int b) { return a b; }calc/CMakeLists.txt写add_library(calc STATIC src/calc.cpp) target_include_directories(calc PUBLIC include)注意这里我没有写cmake_minimum_required和project因为在子目录的 CMakeLists.txt 里不需要重复声明顶层已经声明过了。如果写上也不算错但会显得冗余。target_include_directories(calc PUBLIC include)里的include是相对路径CMake 会相对于当前 CMakeLists.txt 所在目录去解释所以它会指向calc/include。这里我用PUBLIC目的是让链接calc的my_app也能自动找到calc.hpp。5.2 主程序的 CMakeLists.txt顶层CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(MiniCalc LANGUAGES CXX) add_subdirectory(calc) add_executable(my_app main.cpp) target_link_libraries(my_app PRIVATE calc)main.cpp#include iostream #include calc.hpp int main() { std::cout 3 4 add(3, 4) std::endl; return 0; }这里直接#include calc.hpp因为my_app链接了calc目标calc的 PUBLIC include 目录会被 CMake 自动传递给my_app编译器就能找到头文件。5.3 构建与运行流程在项目根目录执行mkdir build cd build cmake .. cmake --build . ./my_app运行结果3 4 7这个流程里CMake 会自动识别calc库的依赖关系先编译calc再编译my_app最后链接。你不用手动指定也根本不需要关心add函数的实现细节。这就是 CMake 的“目标系统”帮我们解决的最大问题。这个小项目如果再往大了扩展比如再加一个tests目录、一个utils目录逻辑都类似add_subdirectory引入模块target_link_libraries建立依赖target_include_directories传递头文件路径。一旦这个思路被你看透后面看别人的大型 CMake 项目就不会再一头雾水。6. 常见报错与排查经验我遇到过的坑基本都在这了6.1 找不到编译器No CMAKE_CXX_COMPILER found这种错误通常出现在 Linux 上原因是你系统里只装了 CMake却没装 C 编译器。Ubuntu 上执行sudo apt install build-essential如果错误信息里提示找不到 C 编译器则还要确认gcc是否存在。Windows 上出现这个错误通常是 Visual Studio 未安装或者没有安装 C 开发组件。CMake 默认会去系统 PATH 里找编译器如果你装有 VS 但不在 PATH你需要用-G Visual Studio 17 2022 -A x64这样的参数显式指定生成器CMake 会通过注册表信息定位 VS 并自动配置编译器。这个报错还有一个隐藏坑当你在 build 目录里换了一个编译器路径时CMakeCache.txt 里会残留旧配置。比如你先用 gcc 配置了一次后来把 gcc 卸载了重新执行 cmake 可能还会报错说找不到之前的编译器。这时候直接删掉 build 目录重新来比疯狂找原因更高效。6.2 找不到头文件fatal error: xxx.h: No such file or directory这个问题我在复习时见过无数次。原因基本是两个一是你根本没有写target_include_directories或者写错了路径二是用了PRIVATE导致头文件路径没有传递给使用者。排查步骤我一般这样走先确认头文件是否真的存在于你写的目录里排除拼写错误。再检查target_include_directories的路径是否正确如果用的是相对路径结合当前的 CMakeLists.txt 所在目录来判断。最后看目标之间的依赖关系如果你需要main.cpp能包含库提供的头文件那库的 include 属性必须是PUBLIC且你的主目标必须链接了库。如果你是在 IDE 里比如 VS遇到这个错误还有一个可能是 CMake 重新生成之后 IDE 没刷新缓存。VS 对 CMake 工程有时会缓存旧的 include 路径右键点击项目根目录选择“重新加载”或者清一下 CMake 缓存就能解决。6.3 链接报错undefined reference to xxx编译能通过但链接时报错说明你的声明写在了头文件里但实现没有被链接进来。常见情况你声明了一个函数但是忘记把它所在的源文件加入add_executable或者你用了库目标但忘了target_link_libraries。我遇到过的典型场景是在 Windows 上使用 VS 生成器源码文件在 VS 里看着已经包含了但链接器就是找不到实现。最后发现是 CMakeLists.txt 里的源文件列表是硬编码好的后来新增了一个 .cpp 文件却忘了加进去。解决方案很简单把新文件加入源文件列表然后重新生成工程。另外如果你链接动态库还要注意库文件的搜索路径。Linux 下动态库找不到常见的报错是cannot open shared object file此时你需要设置LD_LIBRARY_PATH或者在 CMake 里用install规则把库安装到系统路径。个人小项目更推荐直接用静态库省掉这堆麻烦。6.4 修改了 CMakeLists.txt 但似乎没生效CMake 的配置是累积的如果你在源码目录外层配置过后来增删了源文件、改了编译选项理论上重新执行cmake --build .时会自动感知 CMakeLists.txt 的变化并重新生成构建文件。但有时因为修改了cmake_minimum_required或者改变了生成器、缓存等原因会发生“改了半天没反应”的问题。最粗暴也最有效的方法清掉 build 目录重新走一遍cmake ..和cmake --build .。虽然会让增量编译失效但几乎所有诡异问题都能通过“删除 build 目录重来”解决。这个习惯我建议你彻底接受它和 “重启大法”一样万能。6.5 如何确定使用了哪个生成器如果系统里有多个生成器CMake 默认选择会根据平台决定Linux 上默认Unix MakefilesWindows 上默认 VS 工程macOS 上默认 Xcode。很多时候你想用 Ninja或者明确要用 VS 的新版本可以通过-G参数指定cmake -G Ninja .. cmake --build .注意如果你第一次配置时用了-G之后重新配置也必须用相同的-G否则 CMake 会报错说构建目录已经配置过。我建议在项目里写好一个build.sh或build.bat保存配置命令避免每次都要回忆。7. 提升开发体验的几个实用技巧7.1 用 CMakePresets.json 统一团队配置CMake 3.19 以后支持CMakePresets.json它可以把常用的配置参数固化下来比如生成器、构建类型、编译选项等。以后团队成员执行cmake --preset release就可以得到一套统一的配置方式不用每个人都在命令行里敲一长串参数。同时配套的还有CMakeUserPresets.json用于覆盖本机个性化设置这个文件一般不需要提交到版本管理。这个功能对团队协作非常香。我之前参与过一个项目每个人配置命令都不同有人用 Ninja有人用 Makefile还有人用 VS经常出现“我这边编译好好的别人那报错”的情况。后来统一用 presets 后再也没因为生成器不一致争吵过。7.2 Debug 和 Release 要分开目录如果你想在同一份源码上分别生成 Debug 和 Release 版本不要用同一个 build 目录来回切换配置。因为 CMakeCache.txt 会记住上一次的构建类型切来切去很容易出现缓存混乱。推荐的做法是mkdir build-debug cd build-debug cmake .. -DCMAKE_BUILD_TYPEDebug mkdir build-release cd build-release cmake .. -DCMAKE_BUILD_TYPERelease这样两份构建产物互不干扰。Debug 版本默认带上调试信息不优化Release 版本开优化开关跑起来快很多。如果你不开CMAKE_BUILD_TYPE默认是空字符串不会加优化参数但也不影响功能。我一般主程序都用 Release只有需要断点调试时才切到 Debug。7.3 怎么快速判断一个项目能不能用 CMake 构建很多开源项目下载下来后你只需要看根目录有没有CMakeLists.txt。如果有那构建路径基本就是老三样mkdir build cd build cmake .. cmake --build .如果项目用到外部依赖你可能还需要先cmake ..之后看一下输出的错误提示按提示装依赖。有些项目提供了INSTALL步骤安装到系统路径或者指定前缀cmake --install .在 Windows 上cmake --install .会把产物安装到你指定的目录配合-DCMAKE_INSTALL_PREFIX可以控制安装位置。这个功能用于部署到本地环境很方便不需要手动复制一堆文件。7.4 CTest 和 CPackCMake 全家桶里的另外两兄弟CTest 是 CMake 自带的测试工具你只要在 CMakeLists.txt 里用enable_testing()和add_test注册测试再执行ctest就能跑全部测试。CPack 则是打包工具可以生成 tar.gz、zip、deb、rpm、NSIS 安装包等。新手阶段不一定需要掌握但至少知道它们存在当你的项目需要发布给更多人用的时候CPack 能省掉非常多重复劳动。我自己是从一年后才开始用 CTest 的。之前一直觉得 “测试随便跑跑就行”但后来项目大了手动回归测试根本覆盖不过来。用add_test注册几个关键用例之后每次改动跑一遍ctest心里踏实很多。最后分享一点我的真实体会如果让我说学习 CMake 最重要的几个节点我会总结成先跑通一个最小工程再理解目标target的概念最后熟悉怎么排查缓存和生成器问题。这三个节点只要过了CMake 对你来说就不再是拦路虎了。很多人的误区是一上来就找一堆高级语法比如 module、macro、自定义命令结果被吓得不敢用。其实日常开发用到的东西不到 CMake 能力的十分之一先把这十分之一练熟后面遇到新需求时再去查那个具体功能的文档效率才是最高的。我个人到现在写新项目时还是会习惯先丢一个最简单的cmake_minimum_requiredadd_executable进去让项目先动起来再逐步补充目录、库、测试。每次手动敲完cmake ..和cmake --build .看到 “Build files have been written” 这个输出还是会有一种“今天也是顺利构建的一天”的感觉。希望这篇文章能帮你少走一点弯路也让 CMake 成为你顺手的东西而不是学习路上的绊脚石。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑