ARTICLE DETAIL

资讯详情

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

CMake实战指南:从Makefile迁移到多目录构建与动态库生成

CMake实战指南:从Makefile迁移到多目录构建与动态库生成 简介这份《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五个程序日常构建只用cmakeccmake是终端里的交互式配置界面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 VERBOSE1排查链接错误时这个参数几乎是必用的。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.txtPROJECT(HELLO) ADD_SUBDIRECTORY(src bin)src/CMakeLists.txtADD_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 ...)755shell 脚本、辅助工具目录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生成动态库.soSTATIC生成静态库.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 配置文件时需要自己写FindPackage.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.txtCMAKE_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三连确认产物路径和链接关系没问题再往里加业务代码。构建系统这东西前期多花十分钟验证后期少花十小时排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表