【CMake】第3期 CMake 安装、导出与打包体系完整详解 开头OK了直接马不停蹄的开始我们的这一期话题——关于CMake的安装、导出与打包。直接开始。CMake 的安装与导出体系核心是解决「如何把自研库从「本地自用」变成「可被外部项目规范引用」的问题。整个体系是一条逐层递进的交付链路文件安装 → 目标导出 → 包配置生成 → 打包分发每一步只解决一个问题最终实现外部项目通过find_package一行代码即可接入使用一.文件的安装从构建树到安装树编译完成后所有产物都散落在build/构建目录中路径混杂、不适合直接交付。install系列命令的作用就是按照规范的目录结构把需要交付的文件复制到「安装树」中1. 安装树CMake 的安装树是项目执行安装cmake --install/make install后所有产物在安装前缀目录下形成的标准化目录结构它是项目的「最终分发形态」—— 只保留运行、开发、部署所需的文件去掉编译中间产物供最终用户或下游项目使用1根目录安装树的根目录——CMAKE_INSTALL_PREFIX默认值Linux / macOS/usr/localWindowsC:/Program Files/项目名修改方式配置阶段指定cmake -DCMAKE_INSTALL_PREFIX/opt/myproject ..2标准目录布局CNUInstallDirs模板规范的 CMake 项目不会硬编码bin/lib/include而是通过GNUInstallDirs模块获取跨平台兼容的标准目录变量。include(GNUInstallDirs)该模块定义的核心变量均为相对路径相对于CMAKE_INSTALL_PREFIX变量含义默认值CMAKE_INSTALL_BINDIR用户可执行文件目录binCMAKE_INSTALL_LIBDIR库文件目录lib64 位部分系统为lib64CMAKE_INSTALL_INCLUDEDIR头文件目录includeCMAKE_INSTALL_DATADIR架构无关数据文件share/项目名CMAKE_INSTALL_MANDIRman 手册目录share/manCMAKE_INSTALL_SYSCONFDIR配置文件目录etc2.install() 命令install()命令的唯一作用指定「什么文件」放到「安装树的哪个位置」并附带权限、组件、导出等属性1install (TARGETS)安装编译目标用于安装通过add_executable、add_library生成的目标是最核心的安装命令。基础语法install(TARGETS 目标1 目标2 ... [EXPORT 导出集名称] RUNTIME DESTINATION 可执行文件安装目录 LIBRARY DESTINATION 动态库安装目录 ARCHIVE DESTINATION 静态库安装目录 [INCLUDES DESTINATION 头文件安装目录] [COMPONENT 组件名]核心参数详解参数含义设计原因EXPORT 名称将目标归入指定「导出集」后续用于生成导出脚本导出目标的前置标记配合install(EXPORT)使用RUNTIME DESTINATION可执行文件、Windows 平台 DLL 动态库的安装路径不同平台产物类型定义不同Windows 下 DLL 属于运行时文件归入 RUNTIMELIBRARY DESTINATIONLinux/macOS 下的动态库.so/.dylib安装路径类 Unix 系统动态库归为 LIBRARY 类别ARCHIVE DESTINATION静态库.a/.lib、Windows DLL 导入库的安装路径静态库始终属于归档文件INCLUDES DESTINATION声明该目标对应的头文件安装目录导出后自动写入目标的INTERFACE_INCLUDE_DIRECTORIES属性使用者无需手动配置头文件路径COMPONENT对安装内容分组如 Runtime/Development支持按需安装组件打包时也可拆分示例2install (FILES)安装单个文件用于安装头文件、配置文件、文档等零散文件。install(FILES 路径文件名 DESTINATION 安装到哪 [PERMISSIONS OWNER_READ OWNER_WRITE] [RENAME 重命名] [OPTIONAL])RENAME安装时重命名文件OPTIONAL文件不存在也不报错适合可选文件示例3install (DIRECTORY)安装整个目录用于批量安装一整个目录的文件比如整个头文件目录。install(DIRECTORY 目录 DESTINATION 安装到哪 FILES_MATCHING PATTERN *.h PATTERN *.hpp )⚠️最容易踩的坑末尾斜杠install(DIRECTORY include/ ...)复制目录内的内容结果为install/include/math.h✅install(DIRECTORY include ...)复制目录本身结果为install/include/include/math.h❌示例二.目标导出将目标元信息序列化在没有目标导出的年代交付一个 C/C 库非常繁琐你需要给对方提供库文件.a/.so/.lib/.dll 头文件对方拿到后要手动配置头文件搜索路径、库文件搜索路径、链接依赖、编译宏如果你的库还依赖第三方库比如 pthread、fmt对方还要自己手动补上所有依赖非常容易配错。目标导出的本质把「这个库该怎么用」的所有规则头文件目录、编译选项、依赖关系全部打包成标准的 CMake 脚本。外部项目只需要一行target_link_libraries就能自动获得完整的编译链接配置零手动配置1.目标导出的核心原理导出最终会生成一个xxxTargets.cmake文件这个文件就是纯 CMake 代码核心做两件事创建导入目标通过add_library创建一个IMPORTED导入型目标代表这是一个 “已经编译好、从外部导入的库”add_library(demo::math STATIC IMPORTED)写入所有使用属性通过set_target_properties把使用该库需要的所有信息全部设置到这个导入目标上属性作用IMPORTED_LOCATION库文件本身的绝对路径告诉 CMake 库文件在哪INTERFACE_INCLUDE_DIRECTORIES头文件目录。使用者链接这个目标时自动把头文件路径加入自己的搜索路径INTERFACE_COMPILE_DEFINITIONS编译宏。使用者自动带上这些宏定义INTERFACE_LINK_LIBRARIES依赖的其他库。使用者链接时自动把依赖库也一起链接上最终效果使用者只需要写target_link_libraries(app PRIVATE demo::math)CMake 会自动把头文件路径、编译宏、依赖库全部配置好不用手动写任何路径2.install (EXPORT)安装导出在上面install(TARGETS)的时候里面有一个字段EXPORT 导出集你肯定有疑惑这个导出集是啥东西我们通过add_executable()和add_library()创建出来的CMake目标对象它有一个关键局限目标只存在于当前项目的构建过程中是内存里的临时对象项目编译完、安装出去之后这个目标对象就消失了层级存在阶段作用范围路径基准普通 Target目标CMake 配置 构建期仅当前项目内部可见基于本地源码树、构建树的绝对路径EXPORT 导出集安装 分发阶段外部下游项目find_package可见基于安装前缀的可重定位路径EXPORT做的第一件事就是把这些属性序列化写到一个.cmake文件里比如mylibTargets.cmake随着安装树一起分发出去。下游项目执行find_package(mylib)时会自动 include 这个文件在下游的 CMake 环境里重新创建出一个IMPORTED导入目标比如mylib::mylib效果和你本地add_library出来的目标几乎一模一样下游直接target_link_libraries就能用头文件、依赖、编译选项自动带上现在我们需要根据导出集生成.cmake文件并安装到指定路径下install(EXPORT)语法install(EXPORT 导出集 FILE .cmake文件名 NAMESPACE 命名空间 DESTINATION 安装到哪参数详解参数作用注意事项FILE 文件名生成的导出脚本文件名建议命名为项目名Targets.cmakeNAMESPACE 前缀::给所有导出目标加上命名空间前缀形成demo::math格式避免和其他库重名是 CMake 官方推荐规范DESTINATION导出文件的安装目录标准路径为lib/cmake/包名/find_package会自动在此查找示例3.export(EXPORT)上面我们学习的install(EXPORT)是把导出集XXXTargets.cmake安装到安装树目录下系统默认路径发生在执行make install / cmake --install之后这就会有一个很麻烦的点在本地的工程想要使用到这个库传统流程里要等到执行完make install后在系统安装目录下生成了XXXTargets.cmake文件这个本地工程才能使用但如果我需要对库进行修改每修改一次就得重新执行install安装操作反复往系统目录写文件开发效率极低这就引出了export(EXPORT它就干一件事把本来要等 install 步骤才生成的 Targets.cmake提前在 cmake 配置阶段就是你第一次执行cmake -S . -B build的时候直接生成在你的 build 目录里而且这个文件里写的库路径直接指向 build 目录里刚编译出来的库文件根本不用复制到系统目录。在这个 build 目录里的 Targets.cmake它存在的唯一价值就是把开发时的 “编译→install→重新配置下游工程” 三步循环砍成 “编译” 一步例如你写 MyMath 的时候cmake 配置一次这时候export(EXPORT)就已经把 Targets.cmake 生成在 build 目录了改完代码执行编译旁边那个独立的测试工程配置的时候只要把CMAKE_PREFIX_PATH指向 MyMath 的 build 目录就能读到这个 Targets.cmake直接链接 build 目录里刚编译出来的最新库全程不需要执行一次 install也不会往系统目录写任何文件基本语法export(EXPORT 导出集 FILE ${PROJECT_BINARY_DIR}/cmake/XXXTargets.cmake NAMESPACE 命名空间4.生成器1why如上图如果我们现在构建会出现上面的报错这是咋回事在构建库目标的CMakeLists.txt文件中我们添加了库其中头文件路径这个属性使用的是CMAKE_CURRENT_SOURCE_DIR这个变量也就是说通过这样的方式设置的头文件路径是在当前这台机器的绝对路径如果把/home/yht/cmake/blog/my_lib/include这个本地绝对路径一起导出到安装后的TextTargets.cmake里下游用户find_package导入时他的电脑上根本不存在这个路径头文件必然找不到问题的核心矛盾同一个头文件路径在两个场景下必须是不同的值本项目构建时必须用本地源码的绝对路径否则自己编译找不到头文件导出安装后必须用相对于安装前缀的相对路径否则下游用户用不了生成器表达式就是专门解决这个问题的2基本语法先回忆CMake的三个执行过程阶段执行时机做什么哪些语法在这个阶段执行配置阶段Configure执行cmake -S . -B build时解析 CMakeLists、展开变量、执行if/set/function逻辑、缓存配置普通变量${}、if()、message()、大部分命令生成阶段Generate配置完成后生成构建文件前根据配置生成最终的 Makefile/VS 工程计算所有延迟求值的内容生成器表达式构建阶段Build执行make/cmake --build时编译器实际编译链接代码编译器、链接器执行生成器表达式的名字就来自这里它只在「生成阶段」才会被计算求值配置阶段它就是一串普通文本不会被解析生成器表达式是 CMake 的「延迟求值语法」$关键字参数现在我们要解决这个问题就可以使用下面两个关键字表达式什么时候生效$BUILD_INTERFACE:路径在本工程构建树里使用 target 时$INSTALL_INTERFACE:路径安装的时候路径相对安装前缀a.条件布尔类用来实现「满足条件时才生效某个值」格式$条件:生效内容条件为真时返回内容为假时返回空。表达式含义$0:...永远为假内容始终丢弃$1:...永远为真内容始终保留$CONFIG:Debug当前构建类型是 Debug 时为真$CONFIG:Release当前构建类型是 Release 时为真$CXX_COMPILER_ID:GNUC 编译器是 GCC 时为真$PLATFORM_ID:Linux目标平台是 Linux 时为真最经典的用法Debug 加调试选项target_compile_options(MyMath PRIVATE $$CONFIG:Debug:-g -O0 $$CONFIG:Release:-O3 )注意这里是嵌套写法外层$条件:内容内层条件也是一个表达式。Debug 配置下最终展开为-g -O0Release 下展开为-O3b.逻辑运算类用于组合多个条件表达式含义$AND:a,b逻辑与a 和 b 都为真时结果为 1$OR:a,b逻辑或任一为真时结果为 1$NOT:a逻辑非示例Linux Debug 才生效$$AND:$PLATFORM_ID:Linux,$CONFIG:Debug:-fsanitizeaddressc. 目标信息查询类在生成阶段获取目标的真实路径、属性常用于复制文件、自定义命令表格表达式含义$TARGET_FILE:目标名目标输出文件的完整绝对路径如libMyMath.a的完整路径$TARGET_FILE_DIR:目标名目标输出文件所在的目录$TARGET_NAME当前目标的名称三.包配置文件支持 find_package现在我们作为库的制作方如下图是我们的目录结构其中的CMakeLists.txt的实现执行以后是这样的结果现在身份切换——变成库的使用方现在来执行一下这是怎么回事没关系我们来一步步学习1.find_package()1基本语法find_package(包名 [最低版本] [REQUIRED] [mod])包名你要找的包名CMake 会拿这个名字按固定规则拼搜索的文件名最低版本你需要的最低版本号。写了之后CMake 找包的同时会做版本兼容性检查低的包直接跳过REQUIRED找不到包就直接报错终止配置不写的话找不到就静默继续往下走mode指定执行模式MODULE和CONFIG2两种执行模式find_package(Pkg)本质上是让 CMake 去磁盘上找一个 “描述这个包在哪、怎么用” 的.cmake文件并执行它。根据找的是谁写的.cmake文件分成两种模式表格维度Module 模式Config 模式找的文件FindPkg.cmakePkgConfig.cmake或pkg-config.cmake谁写的CMake 官方 / 用户 / 第三方贡献者包作者自己在安装包时生成典型命名FindBoost.cmakeBoostConfig.cmake/boost-config.cmake输出变量Pkg_FOUND、Pkg_INCLUDE_DIRS、Pkg_LIBRARIES同样有变量但主推 imported target如Boost::boost可靠性一般靠外部模块猜测包结构高作者最懂自己的包是否处理传递依赖通常不处理通常会Config 文件里写好了找到后做什么include()那个模块文件include()那个 Config 文件a.Module 模式查找逻辑CMake 按顺序找一个叫FindPkg.cmake的文件用户自己写的CMAKE_MODULE_PATH里列出的目录CMake 自带的cmake-install/share/cmake-ver/Modules/FindPkg.cmake它长什么样以FindOpenSSL.cmake为例内部就是干三件事find_path(OpenSSL_INCLUDE_DIR openssl/ssl.h)— 找头文件find_library(OpenSSL_LIBRARY ssl)— 找库文件include(FindPackageHandleStandardArgs)→ 统一设置变量OpenSSL_FOUND、OpenSSL_INCLUDE_DIRS、OpenSSL_LIBRARIES特点包作者不参与。这个模块是 CMake 社区写的作者只负责把包装到标准位置模块去 “猜” 它在哪。只给你一堆变量OpenSSL_LIBRARIES你得自己写target_link_libraries(mytarget ${OpenSSL_LIBRARIES})。包升级了路径变了模块可能就失效需要 CMake 官方更新b.Config 模式查找逻辑找PkgConfig.cmake或小写pkg-config.cmake。搜索路径比 Module 模式复杂得多Pkg_DIR变量直接指定的目录CMAKE_PREFIX_PATH里每个路径下的/lib/cmake/Pkg/、/share/Pkg/等包安装时自己的前缀CMAKE_INSTALL_PREFIX系统默认路径/usr、/usr/local、Windows 注册表、macOS 的/Library/...环境变量Pkg_ROOT要做什么它至少要做三件事includePkgTargets.cmake检查依赖如果依赖别的包调用check_required_components。特点包作者亲自写的或安装脚本生成的最清楚自己包的头文件、库文件、依赖关系直接提供imported targetfind_package(Boost)之后你写target_link_libraries(mytarget Boost::filesystem)就行include 目录、编译选项、传递依赖全自动带过来会处理传递依赖比如你 find_package (OpenCV)Config 文件里会顺手把它依赖的 Eigen、FFMPEG 也找齐CMake先按 Module 模式找FindPkg.cmake如果没找到再按 Config 模式找PkgConfig.cmake2.configure_package_config_file()configure_package_config_file()这个函数就是专门来创建Config.cmake文件的并且自动更换为相对路径configure_package_config_file( .cmake.in输入模板 # 参数1输入模板文件的路径 Config.cmake文件在哪输出 # 参数2生成出来的最终Config文件放哪 INSTALL_DESTINATION 最终安装到哪 # 参数3这个Config文件最终install到哪个目录 )这个.cmake.in就是一个普通文本文件它里面其实只写三件事PACKAGE_INIT # ① 到对方机器上先算出安装前缀 include(${CMAKE_CURRENT_LIST_DIR}/DemoTargets.cmake) # ② 把我的 target 清单拿进来 find_dependency(Threads) # ③ 我依赖谁替对方也找一下 check_required_components(Demo) # ④ 检查组件一个类比.cmake.in是带空格的快递单模板 → configure_package_config_file 是打印机 → DemoConfig.cmake 是打印出来的单子 → find_package 是收件人拿着单子去取货。模板本身不会被寄出去注意使用configure_package_config_file()要include(CMakePackageConfigHelpers)3.代码展示1库的发布方2库的使用方四.打包分发CPack现在有个新问题安装树是一堆零散的文件我怎么发布给别人呢这就需要打包这个功能——CPack简单来说CPack工作逻辑就两步读取你 CMakeLists.txt 里写的所有install()规则在一个系统临时空目录里把你写的 install 规则完整执行一遍最后把这个临时目录里的所有文件打成你要的包格式基本用法在CMakeLists.txt最后所有install之后加set(参数 值) include(CPack)include(CPack)本身没有参数它的行为由CPACK_* 变量控制变量含义常用值CPACK_GENERATOR包格式ZIP、TGZ、DEB、RPM、NSISCPACK_PACKAGE_NAME包名demoCPACK_PACKAGE_VERSION版本${PROJECT_VERSION}CPACK_PACKAGE_VENDOR厂商—CPACK_PACKAGE_DESCRIPTION_SUMMARY描述—CPACK_PACKAGE_INSTALL_DIRECTORY解压后的顶层目录名默认包名CPACK_SOURCE_GENERATOR源码包格式TGZCPACK_COMPONENTS_ALL组件列表配合install(... COMPONENT ...)现在我想把我刚刚构建好的库打包我可以这样结尾OK了这一期的CMake学习就到这里了我们下期继续。如果对你的学习有所帮助感谢你的点赞、收藏、关注我主页里有更好康的呦往期回顾1.【CMake】第2期 详解 CMake 多目录工程、变量作用域与核心语法2.【CMake】第1期 从 CMake 初识到库的构建3. 【MySQL】第4期 用户管理与 C 语言连接编程详解