ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CMake实战:从CMakeLists.txt到make install的完整构建指南

CMake实战:从CMakeLists.txt到make install的完整构建指南 简介这是一份面向C/C开发者、嵌入式工程师及跨平台项目维护者的CMake实战学习资料聚焦于用CMake管理多目录、多组件的软件构建流程。内容从初识CMake、安装配置讲起逐步深入到Hello World构建、静态库与动态库的创建与安装、外部共享库和头文件的引用并系统梳理了常用变量、环境变量、INSTALL与FIND系列指令及控制指令最后以模块化示例演示FindCURL与自定义FindHELLO模块的编写以及多目录带so生成的源码目录结构模板。资源包内含1个PDF文件约998KB以图文与代码片段结合的方式呈现便于对照实践。目前已有978人学习适合希望摆脱手写Makefile、提升跨平台构建效率的读者系统入门与查阅。1. CMake实战从手写 CMakeLists.txt 到 make install 的完整落地路径很多人第一次接触 CMake是因为接手了一个别人写好的 C/C 工程打开目录一看根目录躺着一个CMakeLists.txt里面写着ADD_EXECUTABLE、ADD_SUBDIRECTORY、INSTALL这些大写命令改一行就报错删一行就编译不过。CMake 实战的核心其实就是把这套「用文本描述构建过程」的机制吃透它不直接编译代码而是根据你写的CMakeLists.txt生成 Makefile 或 Visual Studio 工程文件再交给底层构建工具去执行。搞懂这条链路你才能解释为什么cmake ..之后还要make为什么make install会把文件装到/usr/local而不是当前目录。这篇内容面向需要自己搭构建系统的 C/C 开发者从最小可运行工程讲起一路走到多目录、安装规则和跨平台踩坑每一步都能照着复现。2. 最小可运行工程ADD_EXECUTABLE 到底做了什么2.1 从一条 g 命令到 CMakeLists.txt 的映射先看不用 CMake 时你怎么编译一个单文件程序。假设目录下只有main.cpp传统做法是一条命令g main.cpp -o hello -stdc17这条命令里包含三个信息源文件是main.cpp输出可执行文件叫hello用 C17 标准。CMake 要做的就是把这些信息用声明式语法写下来。在同一个目录新建CMakeLists.txt# 指定 CMake 最低版本低于此版本会直接报错 cmake_minimum_required(VERSION 3.10) # 定义工程名和版本工程名会作为变量 PROJECT_NAME 使用 project(hello VERSION 1.0) # 设置 C 标准INTERFACE 表示该属性传递给依赖此目标的其它目标 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 声明一个可执行目标名字叫 hello源文件是 main.cpp add_executable(hello main.cpp)add_executable的第一个参数是目标名也是最终生成的可执行文件名后面跟的是源文件列表。这里有个新手常犯的错把目标名写成和源文件同名的main结果生成的可执行文件叫main和源文件main.cpp在大小写不敏感的平台上容易混淆。我一般习惯目标名用工程语义命名比如hello、server、image_tool源文件名保持独立。2.2 构建目录分离为什么不要在源码目录直接 cmake写完CMakeLists.txt最省事的做法是直接在源码目录执行cmake .但这会把CMakeCache.txt、CMakeFiles/、Makefile全部生成在源码旁边提交代码时得一个个加进.gitignore清理也麻烦。正确做法是建一个独立的构建目录# 在工程根目录下创建 build 目录并进入 mkdir -p build cd build # .. 指向源码目录CMake 会读取上级目录的 CMakeLists.txt cmake .. # 生成构建文件后执行编译-j 后面跟并行任务数 make -j4执行cmake ..时CMake 会做几件事读取CMakeLists.txt检测编译器生成CMakeCache.txt缓存变量最后在build目录里生成 Makefile。之后每次改代码只需要在build里执行make不用重新跑cmake只有改了CMakeLists.txt或增删了源文件才需要重新cmake ..。这个「配置」和「构建」分离的模型是 CMake 实战里最基础也最重要的一步很多人后面遇到的路径错乱、缓存不更新问题根源都在这里。提示如果cmake ..报找不到编译器先确认gcc/g在 PATH 里或者用-DCMAKE_CXX_COMPILER/usr/bin/g显式指定。2.3 用 cmake --build 统一构建入口make是 Unix 下的构建命令到了 Windows 的 Visual Studio 生成器下就变成msbuild或devenv。为了让命令跨平台一致CMake 提供了--build参数# 在 build 目录下等价于 make但跨生成器通用 cmake --build . --parallel 4 # 指定构建类型Debug 带调试符号Release 开优化 cmake --build . --config Release--parallel对应make -j--config在多配置生成器Visual Studio、Xcode下才生效单配置生成器Unix Makefiles、Ninja下构建类型在cmake配置阶段用-DCMAKE_BUILD_TYPERelease指定。这个区别是跨平台项目翻车的高发区在 Linux 上写cmake --build . --config Release不会报错但也不会真的切到 Release因为构建类型早就定死了。我一般会在配置阶段就写清楚cmake -DCMAKE_BUILD_TYPERelease .. cmake --build . --parallel3. 多目录工程ADD_SUBDIRECTORY 与目标依赖的组织方式3.1 什么时候该拆子目录单文件工程用不上ADD_SUBDIRECTORY但只要项目超过三四个源文件或者开始出现「库 可执行文件」的结构就该拆了。典型布局是这样project/ ├── CMakeLists.txt ├── include/ │ └── math_utils.h ├── src/ │ ├── CMakeLists.txt │ ├── main.cpp │ └── math_utils.cpp └── build/根目录的CMakeLists.txt负责全局设置和引入子目录src/CMakeLists.txt负责具体目标。根目录写法cmake_minimum_required(VERSION 3.10) project(math_demo VERSION 1.0) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 把 include 目录暴露给所有子目录PUBLIC 表示自己和依赖者都能用 include_directories(${PROJECT_SOURCE_DIR}/include) # 进入 src 子目录继续处理 add_subdirectory(src)add_subdirectory的参数是子目录路径CMake 会立即进入该目录读取它的CMakeLists.txt。这里的关键是执行顺序add_subdirectory之前的变量设置对子目录可见之后的不可见。所以include_directories、CMAKE_CXX_STANDARD这类全局设置必须写在add_subdirectory前面否则子目录里的目标拿不到。这个顺序问题我踩过不止一次表现是子目录编译报「找不到头文件」但头文件明明就在include/里。3.2 用 add_library 把公共代码抽成库src/CMakeLists.txt里先把math_utils.cpp编成静态库再让可执行文件链接它# 把 math_utils.cpp 编成静态库目标名 math_utils add_library(math_utils STATIC math_utils.cpp) # 声明可执行目标链接上面的库 add_executable(math_demo main.cpp) target_link_libraries(math_demo PRIVATE math_utils)add_library的第二个参数STATIC表示静态库生成.aLinux或.libWindows换成SHARED就是动态库生成.so或.dll。target_link_libraries的PRIVATE表示这个链接关系只对math_demo自己生效不传递给依赖math_demo的其它目标。对应的还有PUBLIC自己和依赖者都生效和INTERFACE只对依赖者生效。这三个关键字是 CMake 现代用法的核心搞混了会导致依赖传递失控出现「明明没链接某个库却报符号找不到」的玄学问题。3.3 头文件路径的两种写法与选择上面用了include_directories这是目录级命令影响当前目录及所有子目录的所有目标。更推荐的是目标级命令target_include_directoriesadd_library(math_utils STATIC math_utils.cpp) # 只对 math_utils 目标生效PUBLIC 让链接它的目标也能找到头文件 target_include_directories(math_utils PUBLIC ${PROJECT_SOURCE_DIR}/include)两种写法的差别在大型项目里非常明显include_directories是「全局污染」任何目标都能看到这些路径容易掩盖真实的依赖关系target_include_directories是「精确制导」谁需要谁声明。我现在的习惯是新工程一律用目标级命令只有在维护老工程时才保留include_directories。参数上PUBLIC适合头文件里#include了该路径下头文件的库PRIVATE适合只在.cpp里用、头文件不暴露的场景。注意target_include_directories必须在add_library或add_executable之后调用因为它的第一个参数是已经存在的目标名。4. INSTALL 规则make install 把文件装到哪里去了4.1 INSTALL 的基本语法与默认前缀make install的行为完全由INSTALL命令决定。如果CMakeLists.txt里一条INSTALL都没写make install什么也不做不会报错也不会拷贝文件。最小安装规则# 安装可执行文件到 bin 目录 install(TARGETS math_demo RUNTIME DESTINATION bin) # 安装头文件到 include 目录 install(FILES ${PROJECT_SOURCE_DIR}/include/math_utils.h DESTINATION include)DESTINATION写的是相对路径实际安装位置 CMAKE_INSTALL_PREFIXDESTINATION。CMAKE_INSTALL_PREFIX默认在 Linux 下是/usr/localWindows 下是C:/Program Files/项目名。所以上面的规则会把math_demo装到/usr/local/bin/math_demo头文件装到/usr/local/include/math_utils.h。想改安装位置在配置阶段指定cmake -DCMAKE_INSTALL_PREFIX/home/user/myapp .. cmake --build . --parallel cmake --install .cmake --install .是 CMake 3.15 之后推荐的写法等价于make install但跨生成器通用。老项目里常见make install新项目我一般用cmake --install。4.2 TARGETS 与 FILES 的区别以及 RUNTIME/LIBRARY/ARCHIVEinstall(TARGETS ...)用于安装构建产物install(FILES ...)用于安装源文件或配置。TARGETS后面可以跟三种目标类型关键字关键字对应产物典型平台RUNTIME可执行文件、动态库Windows所有平台LIBRARY动态库非 WindowsLinux/macOSARCHIVE静态库、导入库所有平台一个同时有可执行文件和静态库的工程安装规则通常写成install(TARGETS math_demo math_utils RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)如果只写RUNTIME DESTINATION bin静态库math_utils不会被安装因为静态库属于ARCHIVE类别。这个坑很隐蔽make install不报错但装完之后发现libmath_utils.a不见了。我第一次遇到时排查了半天最后才意识到是漏了ARCHIVE。4.3 安装路径变量与 CPack 的衔接CMake 提供了一组CMAKE_INSTALL_*变量可以在INSTALL里直接引用避免硬编码include(GNUInstallDirs) install(TARGETS math_demo RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES include/math_utils.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})GNUInstallDirs模块会根据平台自动设置CMAKE_INSTALL_BINDIR通常是bin、CMAKE_INSTALL_LIBDIRlib或lib64、CMAKE_INSTALL_INCLUDEDIRinclude。用这些变量而不是写死bin/lib能让安装规则在 64 位 Linux 上自动落到lib64在 macOS 上落到lib。这套变量也是后面接 CPack 打 deb/rpm/zip 包的基础CPack 会读取这些路径来组织包内目录结构。5. 避坑与排查CMake 实战里最容易翻车的五个点5.1 改了 CMakeLists.txt 但 make 没反应现象修改了CMakeLists.txt里的源文件列表执行make后编译结果没变化新加的源文件没被编译。原因make只检查CMakeLists.txt的时间戳如果修改时间没被正确识别或者构建目录里的缓存没刷新CMake 不会重新生成 Makefile。解决在构建目录执行cmake .强制重新配置或者直接删掉build目录重新来一遍。更稳妥的做法是每次改完CMakeLists.txt都跑一次cmake ..不要依赖make的自动检测。5.2 找不到头文件但路径明明是对的现象fatal error: math_utils.h: No such file or directory但include/math_utils.h确实存在。原因target_include_directories写在了add_library之前或者include_directories写在了add_subdirectory之后导致目标创建时路径还没生效。解决确认target_include_directories在目标创建之后调用include_directories在add_subdirectory之前调用。用cmake --build . --verbose查看实际编译命令里的-I参数能直接看到 CMake 传了哪些头文件路径。5.3 make install 装到了 /usr/local 却没权限现象cmake --install .报Permission denied因为默认前缀/usr/local需要 root 权限。原因CMAKE_INSTALL_PREFIX默认值指向系统目录。解决配置阶段用-DCMAKE_INSTALL_PREFIX$HOME/.local改到用户目录或者用sudo cmake --install .。我一般推荐前者避免污染系统目录卸载时直接删~/.local下的对应文件即可。5.4 静态库链接顺序导致符号找不到现象undefined reference to math_add但libmath_utils.a确实存在且已链接。原因target_link_libraries里库的顺序不对或者库之间的依赖关系没写清楚。静态库链接时被依赖的库要放在依赖者的后面。解决用target_link_libraries(math_demo PRIVATE math_utils)让 CMake 自动处理顺序不要手动拼-l参数。如果库之间有依赖用target_link_libraries(math_utils PUBLIC other_lib)声明传递关系。5.5 Windows 下生成器选错导致路径混乱现象在 Windows 上用cmake ..默认生成了 Visual Studio 工程但你想用 MinGW 编译结果报编译器不匹配。原因CMake 在 Windows 上默认选最新版 Visual Studio 作为生成器不会自动检测 MinGW。解决配置时显式指定生成器cmake -G MinGW Makefiles ..并确保mingw32-make在 PATH 里。如果同时装了多个 VS 版本用-G Visual Studio 17 2022指定具体版本避免 CMake 选到你不想要的那个。6. 进阶技巧用 CMAKE_BUILD_TYPE 和编译选项做条件构建走到这里基本工程已经能跑通了。最后一个实战技巧是把构建类型和编译选项做成条件逻辑让同一份CMakeLists.txt在 Debug 和 Release 下表现不同。常见做法是用CMAKE_BUILD_TYPE变量加if判断if(NOT CMAKE_BUILD_TYPE) # 没指定时默认用 Release避免空构建类型导致优化全关 set(CMAKE_BUILD_TYPE Release CACHE STRING Build type FORCE) endif() # 根据构建类型追加编译选项 if(CMAKE_BUILD_TYPE STREQUAL Debug) add_compile_options(-g -O0 -Wall -Wextra) elseif(CMAKE_BUILD_TYPE STREQUAL Release) add_compile_options(-O2 -DNDEBUG) endif()add_compile_options影响当前目录及子目录的所有目标适合放全局警告和优化级别。如果只想对某个目标生效用target_compile_options(math_demo PRIVATE -Wall)。-DNDEBUG会关掉assertRelease 下必须加否则断言在正式版里还在跑既影响性能又可能暴露内部状态。另一个实用技巧是用option命令做功能开关option(ENABLE_TESTS Build unit tests OFF) if(ENABLE_TESTS) enable_testing() add_subdirectory(tests) endif()配置时用cmake -DENABLE_TESTSON ..打开。这样默认构建不编译测试CI 里显式打开本地开发按需开启。option的默认值会写进CMakeCache.txt第二次配置时不加-D也会保持上次的值想改回来得手动编辑缓存或删掉build目录重来。验证构建类型是否生效最直接的方法是看编译命令cmake --build . --verbose 21 | grep -E O[0-2]|NDEBUGDebug 下应该看到-O0 -gRelease 下应该看到-O2 -DNDEBUG。如果什么都没看到说明CMAKE_BUILD_TYPE是空的检查配置阶段有没有传-DCMAKE_BUILD_TYPE或者if判断里的字符串有没有拼错。我自己就写过if(CMAKE_BUILD_TYPE STREQUAL release)这种小写比较结果 Release 分支永远不生效编译出来的二进制没有优化性能测试数据一直不对查了两天才发现是大小写问题。这种错误 CMake 不会报任何警告只能靠--verbose看实际命令来抓。希望帮到你。本文还有配套的精品资源点击获取
返回列表