我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

【CMake】第1期 从 CMake 初识到库的构建

【CMake】第1期 从 CMake 初识到库的构建 开头OK了前面我们花了4期把MySQL的基础知识学完了今天我们来学习一个全新的板块——CMake。这是啥你可能都没听过废话不多说我们一起来学习。一.初识CMake1.基本概念如上图一个最简单的代码现在我们想要运行预处理-编译-汇编-链接-可执行文件我们只需要能跑但一旦项目变大四个问题立刻出现平台差异Linux 用g、Windows 用cl.exe、macOS 用clang参数不一样增量构建改一个文件哪些.o要重编手写命令做不到依赖管理用到第三方库头文件在哪、库在哪、要链哪些IDE/工具VS Code、CLion、clangd 怎么知道你的源文件列表和编译参数CMake 是目前 C/C 领域最主流的跨平台构建系统生成工具也是现代 C 项目的事实标准。它不直接编译代码而是根据一份统一的CMakeLists.txt配置文件生成对应平台的原生构建脚本Linux 的 Makefile、Windows 的 VS 工程、macOS 的 Xcode 工程、跨平台的 Ninja 文件最终由原生工具完成编译链接2.三大阶段 ——三棵树在写第一行 CMake 之前你必须知道这三个阶段① 配置 Configure 读 CMakeLists.txt探测编译器写缓存 ↓ ② 生成 Generate 把 target 属性翻译成 build.ninja / Makefile ↓ ③ 构建 Build 调用 make编译、链接1. 源树Source Tree定义存放项目全部源代码、手写的CMakeLists.txt构建脚本、资源文件、配置文档的目录是开发者全程维护的 “原材料目录”。核心特点所有内容均为人工编写维护构建过程不会向其中写入任何文件。示例~/demo/目录内包含.cpp/.h源码、顶层CMakeLists.txt、README 等项目原生文件。2. 构建树Build Tree定义CMake 配置 编译过程中所有自动生成的中间产物的存放目录包括缓存文件、生成的构建脚本Makefile/VS 工程文件、目标文件.o/.obj、临时库、未安装的可执行程序等核心特点全部为自动生成的文件无人工维护内容可随时删除重建示例~/demo/build/目录内会生成CMakeCache.txt、CMakeFiles目录、Makefile、编译产出的可执行文件等3. 安装树Install Tree定义执行cmake --install后最终交付的成品文件可执行程序、动态 / 静态库、公开头文件、配置文件、文档等按照规范结构安装到的目录是项目的 “最终交付目录”核心特点仅保留运行 / 使用项目必需的文件剔除了所有编译中间产物示例Linux 系统默认安装路径/usr/local/也可自定义本地安装路径如~/demo/install/3.源内 / 源外构建1. 源内构建定义直接在源树的根目录执行 CMake 配置让所有构建生成的文件与源代码混在同一个目录中是一种过时的构建方式操作方法cd ~/demo # 直接进入源码根目录 cmake . # 在源码根目录执行配置生成文件直接散落在源码目录中 make # 编译产物也存放在源码目录中核心缺陷污染源树源码目录会混入大量自动生成的文件无法直观区分 “手写代码” 和 “生成文件”目录结构混乱。清理成本高想要清空编译产物重新构建需要手动删除大量零散的生成文件极易误删源码文件。无法多配置并行不能同时存在 Debug、Release 两种构建版本切换编译类型必须清空全部内容、重新配置编译。2. 源外构建定义将构建树与源树完全分离所有自动生成的构建、编译产物都放在独立的目录通常命名为build操作方法两种写法效果完全一致# 写法1现代 CMake 标准写法截图中的方式 cmake -S . -B build # -S 指定源目录-B 指定构建目录 cmake --build build # 执行编译 # 写法2传统手动进入目录的写法 mkdir build cd build cmake .. make核心优势源树保持纯净源码目录永远只有人工维护的文件结构清晰Git 仅需忽略build/一个目录即可。支持多配置并行可以同时创建build_debug、build_release等多个构建目录分别配置不同的编译选项、编译类型互不干扰、无需重复配置。cmake -S . -B build_debug -DCMAKE_BUILD_TYPEDebug cmake -S . -B build_release -DCMAKE_BUILD_TYPERelease重置成本极低想要彻底清空构建、解决缓存异常问题只需rm -rf build删除整个构建目录完全不会影响源码相当于 “一键重来”。安全性更高不会因为构建脚本的问题意外修改、覆盖源码文件更适合大型多人协作项目二.构建一个可执行文件1.示例前面我们写了一个简单的main.cpp我们现在用 CMake 的语法来构建首先就是创建一个CMakeLists.txt文件注意大小写编写CMakeLists.txt构建make并执行2.拆解三个函数1cmake_minimum_required()cmake_minimum_required(VERSION 3.18)语法cmake_minimum_required(VERSION min[...max])参数含义VERSION 3.20这个工程至少需要 CMake 3.18...max可选版本范围上界极少用它不只是版本检查它还会把 CMake 的 **策略policy**锁定到 3.18 那一套。CMake 每几个版本会改默认行为比如if()的比较、install的默认路径policy 机制保证老工程不会突然坏掉。你写VERSION 3.18等于告诉 CMake“请用 3.18 的规矩理解我。”为什么必须第一行后面所有命令的行为都受它影响。常见坑写VERSION 2.8CMake 4.x 已经不支持 3.5 以下直接报错不写用最新策略老写法可能报CMP00xx警告甚至错误写太高3.30而队友是 3.22配置直接失败。团队取交集。2project()project(hello LANGUAGES CXX)语法project(name [VERSION major[.minor[.patch[.tweak]]]] [DESCRIPTION text] [HOMEPAGE_URL url] [LANGUAGES language...])参数含义不写会怎样name项目名必填—VERSION版本号1.1.1没有PROJECT_VERSION后面install/CPack用不了DESCRIPTION描述只影响打包元数据HOMEPAGE_URL主页同上LANGUAGES启用哪些语言默认 C 和 CXX只写CXX可以少探测一个 C 编译器配置更快它偷偷生成了一堆变量后面会反复用${PROJECT_NAME} # hello ${PROJECT_VERSION} # 1.1.1 ${PROJECT_VERSION_MAJOR} # 1 ${PROJECT_SOURCE_DIR} # 本 project() 所在目录 ${PROJECT_BINARY_DIR} # 对应的构建目录容易混的两个CMAKE_SOURCE_DIR最顶层的源目录整个工程的老大PROJECT_SOURCE_DIR最近一次project()所在目录。3add_excutable()add_executable(main main.cpp)语法add_executable(name [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source...])参数含义说明name目标名全局唯一也是cmake --build build --target hello的名字source...源文件列表相对路径相对于当前 CMakeLists 所在目录不是相对 build/WIN32Windows GUI 程序入口是WinMain而不是main控制台程序不要写MACOSX_BUNDLEmacOS.app包跨平台项目才用EXCLUDE_FROM_ALL默认不构建只有--target hello才编适合示例/工具它创建的不只是一个文件而是一个 target——这是 CMake 的核心概念3.构建树跑完cmake -S . -B build后build/构建目录里会多出makefile本质CMake 生成的原生构建脚本。作用Linux下 CMake 默认生成 Makefile 构建系统后续执行make或cmake --build build时就是读取这个文件按照里面定义的编译、链接、清理规则来执行编译补充如果是 Windows Visual Studio 环境这里对应的会是.sln解决方案和.vcxproj工程文件CMakeCache.txt本质CMake 的变量缓存文件是构建目录里最核心的文件之一作用第一次配置时CMake 会把检测到的编译器路径、编译选项、用户自定义变量比如CMAKE_BUILD_TYPE、CMAKE_INSTALL_PREFIX、第三方库的查找结果全部缓存到这个文件里。后续重新执行cmake配置时会直接读取这份缓存不需要重新做编译器检测、变量计算大幅加快配置速度。你修改CMakeLists.txt后重新配置CMake 会基于这份缓存做增量更新。常见用法想彻底重置配置、清空所有缓存时删除这个文件或直接删整个 build 目录即可cmake_install.cmake本质安装阶段的执行脚本。作用里面写好了所有安装规则 —— 哪些可执行文件 / 库要复制到安装目录、公开头文件放哪、权限如何设置等。当你执行cmake --install build时CMake 就是运行这个脚本完成最终的文件部署CMakeFiles 目录本质CMake 的内部工作目录存放所有中间产物、临时文件、日志和编译中间件。里面常见的内容每个编译目标比如你的main对应的子目录存放.o目标文件、依赖关系记录、编译参数快照。编译器检测日志、配置过程日志排错时会用到。CMake 内部用的临时脚本、进度标记文件。这个目录全程由 CMake 自动管理一般不需要手动修改或查看三.构建一个库1.示例如上图现在我们有两个 .c 文件我们如何生成可执行呢最简单的方式就是可以运行但是别的项目想复用math.cpp只能复制源码改main.cpp会连math.cpp一起重编大项目里很致命没法区分哪些头文件是公开的、哪些是实现细节解决方法就是把math.cpp变成库让main来进行链接2.相关函数1add_library() 创建库完整语法add_library(name [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [source...])参数含义细节name库的逻辑名全局唯一产物名自动加前缀/后缀math→libmath.aLinux/math.libWindowsSTATIC静态库链接时把代码复制进可执行文件产物.a/.libSHARED动态库运行时加载产物.so/.dllMODULE插件不被链接只给dlopen用产物也是.soEXCLUDE_FROM_ALL默认不构建只有显式指定 target 才编source...源文件相对当前 CMakeLists 所在目录不写类型会怎样默认是STATIC但如果全局set(BUILD_SHARED_LIBS ON)默认变成SHARED。建议永远显式写类型别依赖默认值2target_include_directories()设置头文件路径完整语法**target_include_directories(target [SYSTEM] [AFTER|BEFORE] INTERFACE|PUBLIC|PRIVATE dir... [INTERFACE|PUBLIC|PRIVATE dir...] ...)参数作用大白话target给哪个目标加必须是add_library/add_executable创建过的PUBLIC自己编译要用使用者也要用“我的头文件在 include/用我的人也得加这个路径”PRIVATE只有自己用“实现细节别人不需要知道”INTERFACE自己不用只给使用者header-only 库自己不编译但使用者要加路径dir...路径相对路径相对于当前 CMakeLists 所在目录这一行其实往 target 身上写了两个属性target_include_directories(math PUBLIC include) │ ├─► INCLUDE_DIRECTORIES include 自己编译用 └─► INTERFACE_INCLUDE_DIRECTORIES include 别人用我时用PRIVATE只写第一个INTERFACE只写第二个PUBLIC两个都写。3target_link_directories()设置库的路径完整语法target_link_directories(target [BEFORE] INTERFACE|PUBLIC|PRIVATE dir... [INTERFACE|PUBLIC|PRIVATE dir...] ...)参数作用大白话target给哪个目标加必须是add_library/add_executable创建过的PUBLIC自己链接要用使用者也要用“我依赖的库文件在 lib/用我的人也得加这个搜索路径”PRIVATE只有自己用“实现细节我自己找库用的路径别人不需要知道”INTERFACE自己不用只给使用者header-only 库自己不编译链接但使用者要加这个路径dir...库文件搜索路径相对路径相对于当前 CMakeLists 所在目录等价于 GCC 的-L参数 注意现代 CMake 实践中99% 的场景都不需要、也不推荐使用target_link_directories以及更老旧的全局link_directories因为库文件的搜索路径这个属性库本身就携带了4target_link_libraries()链接库完整语法target_link_libraries(target PRIVATE|PUBLIC|INTERFACE item...)参数作用写进哪个属性target给谁链接—PRIVATE只有我自己链接LINK_LIBRARIESPUBLIC我链接用我的人也要链接LINK_LIBRARIESINTERFACE_LINK_LIBRARIESINTERFACE我不链接用我的人要链接INTERFACE_LINK_LIBRARIESitem...库名 / target 名 / 全路径推荐写 target 名3. 代码展示为了代码更加规范我们进行了分层管理所以CMakeLists.txt可以这样写四.CMake目标、属性、属性传递机制现代 CMake 的设计核心是**「以目标为中心用属性描述构建规则通过链接关系自动传递使用要求」**PUBLIC / PRIVATE / INTERFACE不是特殊魔法本质是控制你设置的构建规则写入目标的「自用属性表」还是「对外接口属性表」进而决定这条规则会不会传递给链接它的下游目标1.Target 目标 / 属性目标是现代 CMake 最基本的构建单元也是所有构建规则的容器。创建方式通过add_executable()创建可执行文件目标通过add_library()创建库目标。本质一个独立的「属性集合」。头文件路径、编译选项、依赖库、链接路径等所有构建信息全部作为属性挂在目标上互不干扰每个目标的属性逻辑上都拆成两半对应两种使用场景。这就是你教程里说的「属性分两半」的核心。属性分类作用给谁用自用属性编译 / 链接这个目标自身时生效的规则给自己构建用接口属性使用要求别人链接我这个目标时必须带上的规则给所有依赖我的下游目标用几乎所有构建规则都有对应的「自用版」和「接口版」属性成对出现规则类型自用属性名接口属性名INTERFACE_前缀对应设置命令头文件搜索路径INCLUDE_DIRECTORIESINTERFACE_INCLUDE_DIRECTORIEStarget_include_directories编译选项COMPILE_OPTIONSINTERFACE_COMPILE_OPTIONStarget_compile_options宏定义COMPILE_DEFINITIONSINTERFACE_COMPILE_DEFINITIONStarget_compile_definitions依赖库LINK_LIBRARIESINTERFACE_LINK_LIBRARIEStarget_link_libraries库搜索路径LINK_DIRECTORIESINTERFACE_LINK_DIRECTORIEStarget_link_directories所有target_xxx系列命令底层都是在给这两类属性赋值。三个关键字PUBLIC / PRIVATE / INTERFACE就是用来控制你设置的内容写到哪张表里2.相关函数1set_target_properties()语法格式set_target_properties(target1 target2 ... PROPERTIES prop1 value1 prop2 value2 ...)核心特性支持多目标 多属性批量设置PROPERTIES是固定关键字其后紧跟「属性名 - 属性值」键值对。覆盖式赋值若属性已存在新值会直接覆盖旧值如果需要追加属性内容应使用set_property(TARGET ... APPEND ...)不能用此命令。仅能设置可写属性CMake 自动维护的只读属性如LOCATION无法通过此命令修改高频常用目标属性属性名功能说明适用目标OUTPUT_NAME替换目标默认输出的文件名可执行文件、库CXX_STANDARD指定 C 标准版本11/14/17/20/23所有编译目标CXX_STANDARD_REQUIRED强制要求编译器支持指定 C 标准ON/OFF所有编译目标POSITION_INDEPENDENT_CODE生成位置无关代码即-fPIC动态库通常设为 ON库目标VERSION库完整版本号如1.2.3动态库SOVERSION库 API 兼容主版本号如1动态库RUNTIME_OUTPUT_DIRECTORY可执行文件输出目录可执行文件LIBRARY_OUTPUT_DIRECTORY动态库输出目录动态库ARCHIVE_OUTPUT_DIRECTORY静态库输出目录静态库COMPILE_DEFINITIONS编译宏定义对应-D参数所有编译目标INCLUDE_DIRECTORIES头文件搜索路径对应-I参数所有编译目标LINK_LIBRARIES链接依赖库列表所有编译目标2get_target_property()语法格式get_target_property(输出变量名 目标名 属性名)核心行为只能读取单个目标的单个属性结果存入指定变量。返回规则目标不存在CMake 直接报错终止。目标存在且属性已设置变量值为属性的实际内容。目标存在但属性未设置变量会被赋值为输出变量名-NOTFOUND注意不是空值是变量名拼接-NOTFOUND后缀3set_property()语法格式set_property(TARGET 目标名 [APPEND] prop1 value1)核心行为向目标追加单条属性值或者修改某一条属性3.属性传递机制这是现代 CMake 最核心的机制也是为什么「只写target_link_libraries就能自动带头文件路径」的根本原因传递的核心规则当目标 A 链接目标 B 时target_link_libraries(A PRIVATE/PUBLIC B)CMake 会自动把 B 的所有接口属性INTERFACE_ 开头的追加到 A 的自用属性中。大白话库的作者把「用我这个库必须带什么规则」打包在自己的接口属性里谁链接我我就自动把这些规则塞给谁的自用规则里使用者不用手动写完整示例第一步给 math 库设置公开头文件路径add_library(math src/math.cpp) target_include_directories(math PUBLIC include)执行后 math 的两张表自用表INCLUDE_DIRECTORIESincludemath 自己编译时用接口表INTERFACE_INCLUDE_DIRECTORIESinclude给所有链接 math 的目标用第二步hello 可执行文件链接 mathadd_executable(hello main.cpp) target_link_libraries(hello PRIVATE math)此时 CMake 自动完成传递把 math 的INTERFACE_INCLUDE_DIRECTORIES也就是include追加到 hello 的INCLUDE_DIRECTORIES里。最终结果main 从来没写过头文件路径但编译时自动就能找到 math 的头文件。这就是属性传递链式传递如果依赖有多层传递会沿着链接链自动传播如果 B 对 C 的依赖是PUBLICC 的接口属性 → B 的接口属性 → A 的自用属性A 能完整继承 C 的所有使用要求。如果 B 对 C 的依赖是PRIVATEC 的属性只进到 B 的自用表不会进入 B 的接口表A 完全感知不到 C 的存在。这也是「内部实现依赖要写 PRIVATE」的根本原因不会把内部依赖泄漏给下游4.PUBLIC / PRIVATE / INTERFACE 的本质三个关键字唯一的作用就是控制你设置的规则写入目标的哪张属性表。规则完全统一对所有target_xxx命令都生效。我们以target_link_libraries(math XXX fmt)为例给 math 库添加 fmt 依赖关键字写入自用表LINK_LIBRARIESmath 自己链接用写入接口表INTERFACE_LINK_LIBRARIES下游用大白话解释PRIVATE✅ 是❌ 否fmt 只是我 math 内部实现用的我自己链接就行用我的人不需要知道PUBLIC✅ 是✅ 是fmt 不仅我内部用我公开的头文件里也用到了 fmt用我的人必须也链接 fmtINTERFACE❌ 否✅ 是我自己不用编译比如纯头文件库但所有用我的人都必须链接 fmt只在 .cpp 实现里用公开头文件完全没出现→ 选PRIVATE比如 math 库内部用 fmt 打日志但 math 的头文件里没有任何 fmt 的类型、函数用户用 math 不需要知道 fmt 的存在。在公开 .h 头文件里用到了返回值、函数参数、继承、宏定义等 → 选PUBLIC比如 math 的头文件里函数参数用了 fmt 的类型那么用户包含 math 头文件时必须能找到 fmt 的头文件必须公开传递。纯头文件库没有 .cpp自身不编译→ 所有属性都用INTERFACEPUBLIC 泄漏# math 内部用了 fmt但只在 .cpp 里用 target_link_libraries(math PUBLIC fmt) # ❌ 错 target_link_libraries(math PRIVATE fmt) # ✅ 对写PUBLIC会让所有链接math的人都被迫去找fmt增加编译时间多余的头文件、宏也可能和下游代码产生命名冲突结尾OK了这一期的CMake初识就到这里了我们下期继续。如果对你的学习有所帮助感谢你的点赞、收藏、关注我主页里有更好康的呦往期回顾【MySQL】第4期 用户管理与 C 语言连接编程详解【MySQL】第3期 索引、事务与视图系统详解【MySQL】 第2期 查询与函数系统详解从基础 CRUD 到复合多表查询
返回列表