简介:这份《CMake实战》PDF面向需要掌握跨平台自动化构建的C/C++开发者、嵌入式工程师及在校学生,尤其适合正在从手写Makefile或Autotools转向CMake的中级读者。内容围绕CMake的安装配置、CMakeLists.txt语法、静态库与动态库构建、外部库查找与链接、常用变量与环境变量、INSTALL与FIND系列指令,以及自定义Find模块和多目录工程模板展开,并配有Hello World、共享库、模块化工程等递进式实例。资源包内共1个PDF文件,约998KB,篇幅紧凑,便于按章节查阅与随身携带。目前已有978人学习,说明其在构建工具学习群体中具备一定参考价值。读者可借此系统梳理CMake的构建逻辑,理解cmake与make的协作方式,掌握库依赖处理与模块扩展思路,为大型项目或多平台构建打下基础。
1. 从一份 CMake 实战 PDF 说起:为什么手写 Makefile 的团队最后都换了
如果你维护过一个超过 20 个源文件的 C/C++ 工程,大概率经历过这种场景:新增一个模块,Makefile 里要改三处依赖;换台机器编译,路径全挂;想把静态库换成动态库,链接顺序调半天。这份《CMake 实战.pdf》就是冲着这类问题来的——它不是官方文档的翻译,而是一份从 helloworld 一路推到多目录、带 so 生成的实战笔记,覆盖安装、外部构建、静态库/动态库、INSTALL 指令、自定义 Find 模块这些真正会在工程里用到的环节。适合谁?正在从手写 Makefile 或 autotools 往 CMake 迁移的 C/C++ 开发者,以及需要给团队搭一套可复用构建模板的人。它不教你 C++ 语法,只解决一件事:让构建这件事从“编译专家的黑匣子”变成普通工程师能改、能扩、能交接的东西。
2. 安装与第一个可执行目标:把 cmake 和 make 的工具链跑通
2.1 为什么优先用官方二进制包而不是包管理器
很多人第一反应是apt install cmake或yum install cmake,但发行版仓库里的版本往往偏旧,而 CMake 的语法和模块在不同版本间有实际差异——比如target_link_libraries的PUBLIC/PRIVATE关键字、FetchContent的行为,老版本上跑不通的写法在新版本里可能是标准做法。这份 PDF 选择的是官方预编译二进制包,解压即用,不污染系统包管理,也方便在同一台机器上并存多个版本。常见做法是把解压目录放到/opt下,再用软链接把bin里的可执行文件挂到/usr/bin,这样终端里直接敲cmake就能命中。
# 卸载旧版本(非必需,仅在包管理器装过时执行) apt-get autoremove cmake # 下载官方预编译包(版本按需替换) wget https://cmake.org/files/v3.9/cmake-3.9.1-Linux-x86_64.tar.gz # 解压 tar zxvf cmake-3.9.1-Linux-x86_64.tar.gz # 移动到 /opt 并建立软链接 mv cmake-3.9.1-Linux-x86_64 /opt/cmake-3.9.1 ln -sf /opt/cmake-3.9.1/bin/* /usr/bin/这里几个参数值得说清楚:tar zxvf里的z是 gzip 解压、x是提取、v是显示过程、f指定文件名,顺序不能乱,f必须紧跟文件名。ln -sf的-s是符号链接(软链接),-f是强制覆盖已存在的同名链接——如果你之前装过别的版本,不加-f会报“文件已存在”然后静默失败,这是新手最常踩的坑之一。解压后的目录结构里,bin下有cmake、ccmake、cmake-gui、cpack、ctest五个程序,日常构建只用cmake,ccmake是终端里的交互式配置界面,cmake-gui是图形版,调试复杂配置时比反复改命令行参数舒服。
2.2 helloworld 的三个指令:PROJECT、SET、ADD_EXECUTABLE
先建一个练习目录,把最小工程跑起来。这一步的目的不是学会写 helloworld,而是理解 CMake 的“两段式”构建:先由cmake读取CMakeLists.txt生成 Makefile,再由make执行真正的编译链接。
mkdir -p cmake/t1 && cd cmake/t1main.c内容:
#include <stdio.h> int main() { printf("Hello World from t1 Main!\n"); return 0; }CMakeLists.txt内容:
PROJECT(HELLO) SET(SRC_LIST main.c) MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR}) MESSAGE(STATUS "This is SOURCE dir " ${HELLO_SOURCE_DIR}) ADD_EXECUTABLE(hello ${SRC_LIST})逐条拆:PROJECT(HELLO)定义工程名,同时隐式创建HELLO_BINARY_DIR和HELLO_SOURCE_DIR两个变量,前者指向构建目录,后者指向源码目录。内部构建时两者相同,外部构建时才会分开——这也是后面要讲外部构建的伏笔。SET(SRC_LIST main.c)是显式定义变量,多个源文件写成SET(SRC_LIST main.c t1.c t2.c)。MESSAGE(STATUS ...)向终端输出信息,STATUS前缀是--,另有SEND_ERROR(产生错误但继续)和FATAL_ERROR(立即终止)两种级别。ADD_EXECUTABLE(hello ${SRC_LIST})定义生成名为hello的可执行文件,源文件列表来自变量。
注意${}是变量引用语法,但在IF控制语句里是直接用变量名、不加${}——这是 CMake 语法里最容易记混的一条。指令本身大小写无关,PROJECT和project等价,但参数和变量名大小写敏感,SRC_LIST和src_list是两个不同的变量。工程名HELLO和生成的可执行文件名hello没有任何绑定关系,ADD_EXECUTABLE(t1 main.c)照样能生成t1。
构建过程:
cmake . make ./hellocmake .后面那个点代表当前目录,执行后会生成CMakeFiles、CMakeCache.txt、cmake_install.cmake和Makefile。make的输出里能看到Scanning dependencies、Building C object、Linking C executable三个阶段。想看完整编译命令加make VERBOSE=1,排查链接错误时这个参数几乎是必用的。
2.3 内部构建的代价与外部构建的切换
上面用的是内部构建(in-source build),中间文件和源码混在一起,CMakeCache.txt一旦生成,改CMakeLists.txt后有时不会重新配置,得手动删缓存。更麻烦的是make distclean在 CMake 工程里是无效的——官方明确不提供这个目标,因为CMakeLists.txt可以执行任意脚本生成临时文件,CMake 无法追踪到底生成了哪些,提供一个“看起来能清理干净”的目标反而会误导人。
外部构建的做法是单独建build目录:
# 先清掉内部构建产生的中间文件,关键是 CMakeCache.txt rm -rf CMakeFiles CMakeCache.txt cmake_install.cmake Makefile mkdir build && cd build cmake .. makecmake ..里的..指向父目录,因为CMakeLists.txt在那里。构建产物全部落在build目录内,源码目录保持干净。此时HELLO_SOURCE_DIR仍指向源码路径,而HELLO_BINARY_DIR指向build路径,两者正式分家。这也是为什么建议统一用PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR这两个预定义变量——它们不随工程名变化,改工程名时不用同步改一堆引用。
3. 让工程像个工程:ADD_SUBDIRECTORY、输出路径与 INSTALL 规则
3.1 用 ADD_SUBDIRECTORY 拆分源码目录
helloworld 跑通后,下一步是把源码挪进src子目录,让工程结构清晰。每个需要被管理的目录都要有自己的CMakeLists.txt,这是 CMake 的硬性约定。
mkdir src mv main.c src根目录CMakeLists.txt:
PROJECT(HELLO) ADD_SUBDIRECTORY(src bin)src/CMakeLists.txt:
ADD_EXECUTABLE(hello main.c)ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])三个参数:第一个是源目录,第二个是编译输出目录(中间结果和目标文件都放这里),第三个EXCLUDE_FROM_ALL表示该目录默认不参与构建,适合example这类需要单独编译的子工程。上面写成ADD_SUBDIRECTORY(src bin),编译后hello会出现在build/bin下;如果省略bin,则落在build/src下。老写法SUBDIRS(src)已不推荐,它不支持指定输出目录,且会保留完整的目录层级。
3.2 用 EXECUTABLE_OUTPUT_PATH 控制产物落点
ADD_SUBDIRECTORY控制的是整个子目录的输出位置,如果只想改最终可执行文件或库的落点、不动中间文件,用SET重定义两个变量:
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)EXECUTABLE_OUTPUT_PATH管可执行文件,LIBRARY_OUTPUT_PATH管库文件,都不影响.o这类中间产物。写在哪?原则很简单:哪里出现ADD_EXECUTABLE或ADD_LIBRARY,就写在哪。本例中就是src/CMakeLists.txt。${PROJECT_BINARY_DIR}在外部构建时指向build目录,所以最终路径是build/bin和build/lib。
3.3 INSTALL 指令的四种安装类型
安装规则用INSTALL指令定义,配合CMAKE_INSTALL_PREFIX变量控制安装根路径,作用类似 autotools 的--prefix。命令行指定方式:
cmake -DCMAKE_INSTALL_PREFIX=/usr .. make make installINSTALL按安装对象分四类,参数各有侧重:
| 安装类型 | 指令形式 | 默认权限 | 典型用途 |
|---|---|---|---|
| 目标文件 | INSTALL(TARGETS ...) | 按类型 | 可执行文件、动态库、静态库 |
| 普通文件 | INSTALL(FILES ...) | 644 | 配置文件、文档 |
| 可执行脚本 | INSTALL(PROGRAMS ...) | 755 | shell 脚本、辅助工具 |
| 目录 | INSTALL(DIRECTORY ...) | 继承源 | 资源目录、头文件目录 |
目标文件安装要区分类型:
INSTALL(TARGETS myrun mylib mystaticlib RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION libstatic)RUNTIME对应可执行二进制,LIBRARY对应动态库,ARCHIVE对应静态库。DESTINATION如果以/开头就是绝对路径,CMAKE_INSTALL_PREFIX失效;写相对路径才会拼接到前缀后面。目录安装有个容易翻车的细节:INSTALL(DIRECTORY abc DESTINATION share)会把abc目录本身装过去,而INSTALL(DIRECTORY abc/ DESTINATION share)只装abc里的内容、不含目录本身,末尾那个斜杠的区别必须记住。
4. 静态库、动态库与外部依赖:从 ADD_LIBRARY 到自定义 Find 模块
4.1 ADD_LIBRARY 构建共享库与静态库
库的构建用ADD_LIBRARY,语法是ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 ...)。SHARED生成动态库(.so),STATIC生成静态库(.a),不指定则根据BUILD_SHARED_LIBS变量决定,默认是静态。
# 构建共享库 ADD_LIBRARY(hello SHARED hello.c) # 构建静态库 ADD_LIBRARY(hello_static STATIC hello.c)同一个源文件不能同时用于两个同名目标,所以静态库和动态库要用不同目标名。如果想让两者输出同名文件(比如都叫libhello),需要设置OUTPUT_NAME属性:
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")动态库版本号通过VERSION和SOVERSION两个属性控制:
SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)VERSION是完整版本号,SOVERSION是 API 兼容版本号。生成的libhello.so.1.2是实际文件,libhello.so.1和libhello.so是指向它的软链接,运行时加载器按SOVERSION找库。
4.2 头文件搜索路径与 target 链接库
使用外部库时,编译器需要找到头文件,链接器需要找到库文件。CMake 里对应两个指令:
INCLUDE_DIRECTORIES(/usr/include/hello) TARGET_LINK_LIBRARIES(main hello)INCLUDE_DIRECTORIES把路径加到编译器的-I参数里,TARGET_LINK_LIBRARIES把库加到链接器的-l参数里。顺序上,TARGET_LINK_LIBRARIES要写在ADD_EXECUTABLE之后,因为它的第一个参数是已经定义好的 target 名。
如果不想在CMakeLists.txt里硬编码路径,可以用环境变量CMAKE_INCLUDE_PATH和CMAKE_LIBRARY_PATH来补充搜索路径:
export CMAKE_INCLUDE_PATH=/usr/local/include/hello export CMAKE_LIBRARY_PATH=/usr/local/lib这两个变量影响的是FIND_PATH和FIND_LIBRARY的搜索行为,不是直接加到编译参数里,所以配合FIND_系列指令用才有效。
4.3 自定义 FindHELLO 模块查找非标准库
当依赖的库没有提供 CMake 配置文件时,需要自己写Find<Package>.cmake模块。模块放在CMAKE_MODULE_PATH指向的目录下,用FIND_PACKAGE调用。
# FindHELLO.cmake FIND_PATH(HELLO_INCLUDE_DIR hello.h /usr/include/hello /usr/local/include/hello) FIND_LIBRARY(HELLO_LIBRARY NAMES hello PATH /usr/lib /usr/local/lib) IF (HELLO_INCLUDE_DIR AND HELLO_LIBRARY) SET(HELLO_FOUND TRUE) ENDIF () IF (HELLO_FOUND) IF (NOT HELLO_FIND_QUIETLY) MESSAGE(STATUS "Found Hello: ${HELLO_LIBRARY}") ENDIF () ELSE () IF (HELLO_FIND_REQUIRED) MESSAGE(FATAL_ERROR "Could not find hello library") ENDIF () ENDIF ()FIND_PATH找头文件所在目录,FIND_LIBRARY找库文件,NAMES后面跟库名(不带lib前缀和.so/.a后缀)。HELLO_FIND_QUIETLY和HELLO_FIND_REQUIRED是FIND_PACKAGE自动传入的变量,分别对应QUIET和REQUIRED参数。工程里这样调用:
SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) FIND_PACKAGE(HELLO) IF (HELLO_FOUND) INCLUDE_DIRECTORIES(${HELLO_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(main ${HELLO_LIBRARY}) ENDIF ()CMAKE_MODULE_PATH告诉 CMake 去哪里找自定义模块,不设置的话FIND_PACKAGE(HELLO)会去系统模块目录找,找不到就报错。
5. 避坑与排查:那些让构建失败但报错看不懂的地方
5.1 现象:改了 CMakeLists.txt 但 make 没反应
原因:CMake 把配置结果缓存在CMakeCache.txt里,某些改动(尤其是变量定义和FIND_指令)不会触发自动重新配置。解决:删掉CMakeCache.txt和CMakeFiles目录重新cmake ..,或者直接删整个build目录重建。外部构建的好处在这里体现得最明显——删build不影响源码。
5.2 现象:链接时报 undefined reference,但库明明装了
原因:TARGET_LINK_LIBRARIES里库的顺序不对。链接器从左到右解析符号,被依赖的库要放在依赖它的目标后面。如果libA依赖libB,写成TARGET_LINK_LIBRARIES(main A B)是对的,写成B A就可能报错。解决:调整顺序,或者用LINK_INTERFACE_LIBRARIES声明库之间的依赖关系让 CMake 自动排序。
5.3 现象:INSTALL 目录时多了一层或少了内容
原因:INSTALL(DIRECTORY abc DESTINATION share)和INSTALL(DIRECTORY abc/ DESTINATION share)行为不同,前者装abc目录本身,后者只装内容。解决:根据需求决定末尾加不加斜杠,装头文件目录时通常希望保留目录名,装资源文件时通常只装内容。
5.4 现象:动态库运行时找不到
原因:libhello.so装到了/usr/local/lib,但系统加载器不搜这个路径。解决:把路径加到/etc/ld.so.conf后执行ldconfig,或者设置LD_LIBRARY_PATH环境变量。更规范的做法是在CMakeLists.txt里用SET_TARGET_PROPERTIES设置INSTALL_RPATH,把运行时搜索路径编进二进制。
5.5 现象:IF 语句里变量判断永远为假
原因:在IF里写了${VAR}而不是VAR。CMake 的IF直接使用变量名,写成${VAR}会被展开成变量的值,然后IF去判断一个叫那个值的变量,自然找不到。解决:IF里去掉${},写成IF (HELLO_FOUND)而不是IF (${HELLO_FOUND})。
6. 多目录模板与 so 生成的完整骨架
把前面所有片段拼成一个可直接复用的多目录模板,这是这份 PDF 最后给出的实战结构,也是我日常起新工程时直接抄的骨架。目录结构如下:
project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.c │ └── hello.c ├── include/ │ └── hello.h └── build/根目录CMakeLists.txt:
CMAKE_MINIMUM_REQUIRED(VERSION 3.5) PROJECT(HELLO C) # 自定义模块搜索路径 SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) # 全局输出路径 SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) # 头文件目录 INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/include) # 添加子目录,指定输出到 bin ADD_SUBDIRECTORY(src bin) # 安装规则 INSTALL(TARGETS hello RUNTIME DESTINATION bin) INSTALL(FILES ${PROJECT_SOURCE_DIR}/include/hello.h DESTINATION include)src/CMakeLists.txt:
# 生成共享库 ADD_LIBRARY(hello SHARED hello.c) SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.0 SOVERSION 1) # 生成可执行文件并链接共享库 ADD_EXECUTABLE(main main.c) TARGET_LINK_LIBRARIES(main hello)构建与验证:
mkdir -p build && cd build cmake -DCMAKE_INSTALL_PREFIX=/usr/local .. make # 检查产物 ls ../lib/ # 应看到 libhello.so.1.0、libhello.so.1、libhello.so ls ../bin/ # 应看到 main # 安装 make install # 验证安装结果 ls /usr/local/lib/libhello* ls /usr/local/bin/main这个模板里几个关键点:CMAKE_MINIMUM_REQUIRED声明最低版本,避免老版本 CMake 解析新语法时报奇怪的错;PROJECT(HELLO C)显式声明只支持 C 语言,不写的话默认启用所有语言,配置阶段会多花时间检测 C++ 编译器;INCLUDE_DIRECTORIES放在根目录,子目录自动继承;ADD_SUBDIRECTORY(src bin)把src的产物输出到build/bin,配合全局的EXECUTABLE_OUTPUT_PATH和LIBRARY_OUTPUT_PATH,最终可执行文件和库分别落在build/bin和build/lib。
验证动态库版本号是否正确,用readelf -d libhello.so.1.0 | grep SONAME,应该看到SONAME是libhello.so.1。验证可执行文件链接的是哪个库,用ldd bin/main,输出里libhello.so.1应该指向你构建的那个路径。这两个命令是我每次改完库配置后必跑的,比看编译日志快得多。
从那以后我每次新建工程,都强制先跑一遍cmake .. && make && ldd三连,确认产物路径和链接关系没问题再往里加业务代码。构建系统这东西,前期多花十分钟验证,后期少花十小时排查。希望帮到你。
本文还有配套的精品资源,点击获取