10、CMake安装与打包:install()命令、CPack打包、导出目标与Config文件生成

说实话,CMake 的安装和打包这块,我当年刚接触的时候也绕了不少弯路。你想想看,代码写好了,编译通过了,但怎么把它装到系统里?怎么给别人用?怎么生成一个漂亮的安装包?这些才是真正考验构建系统功底的地方。

今天我们就来彻底搞懂 CMake 的安装与打包机制。我个人习惯把这部分分成四个核心模块:install() 命令CPack 打包导出目标、以及Config 文件生成。它们环环相扣,缺一不可。

核心观点:CMake 的安装系统不是简单的文件拷贝,而是一套完整的「构建-安装-分发-复用」生态链。理解了这个链条,你就能做出专业级的软件包。

CMake 安装与打包知识体系 CMake 安装与打包 install() 命令 安装目标/文件/目录 CPack 打包 DEB / RPM / NSIS 导出目标 export / install(EXPORT) Config 文件生成 find_package 支持 TARGETS FILES DIRECTORY SCRIPT DEB 生成器 RPM 生成器 NSIS 生成器 TGZ / STGZ export() install(EXPORT) 命名空间 兼容性 Config.cmake.in 版本文件 find_dependency 目标导入 四个模块协同工作:install() 定义安装规则 → CPack 打包分发 → 导出目标供内部复用 → Config 文件供外部 find_package

10.1 install() 命令:安装规则的基石

install() 命令是 CMake 安装系统的入口。说白了,它就是告诉 CMake:「编译好的东西,应该放到系统的哪个位置」。我见过不少新手直接用手动拷贝的方式部署,那真是自找麻烦。

install() 有几种常见用法,我一个个说:

10.1.1 安装目标(TARGETS)

这是最常用的形式。你编译了一个库或可执行文件,想把它装到系统里:

# 安装可执行文件到 bin 目录
install(TARGETS myapp
    RUNTIME DESTINATION bin
)

# 安装库文件
install(TARGETS mylib
    LIBRARY DESTINATION lib          # 动态库
    ARCHIVE DESTINATION lib/static   # 静态库
    RUNTIME DESTINATION bin          # Windows DLL
)

这里有个细节要注意:RUNTIME、LIBRARY、ARCHIVE 是三个不同的组件类型。我在项目中遇到过有人把动态库当 ARCHIVE 装,结果装到了奇怪的位置,运行时死活找不到库。嗯,这种坑踩过一次就记住了。

10.1.2 安装文件和目录

有时候你需要安装一些配置文件、文档或者头文件:

# 安装单个文件
install(FILES config.ini DESTINATION etc/myapp)

# 安装整个目录
install(DIRECTORY include/ DESTINATION include
    FILES_MATCHING PATTERN "*.h"
)

# 安装脚本
install(SCRIPT cmake/post_install.cmake)

我的习惯:头文件安装时一定要用 FILES_MATCHING 过滤,不然把 .cpp 文件也装进去就尴尬了。另外,DIRECTORY 后面的斜杠很有讲究——带斜杠表示目录内容,不带斜杠表示目录本身。

10.2 CPack 打包:一键生成安装包

CPack 是 CMake 自带的打包工具。你想想看,如果每次发布都要手动打 DEB 包、RPM 包,那得多痛苦。CPack 就是来解决这个问题的。

使用 CPack 只需要两步:

  1. 在 CMakeLists.txt 中设置打包参数
  2. 运行 cpack 命令

10.2.1 基本配置

# 设置包的基本信息
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_VENDOR "My Company")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A wonderful application")

# 指定生成器
set(CPACK_GENERATOR "DEB;RPM;NSIS")

# 包含 CPack 模块
include(CPack)

然后运行:

# 生成所有指定格式的包
cpack

# 或者只生成 DEB 包
cpack -G DEB

10.2.2 DEB 包的特殊配置

做 Linux 开发的朋友应该知道,DEB 包有严格的依赖关系要求。我曾经因为漏写依赖,导致用户安装后运行报错,被吐槽了好久:

set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl-dev (>= 1.1), libc6 (>= 2.28)")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@example.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")

10.2.3 RPM 和 NSIS 配置

RPM 和 NSIS 的配置思路类似,只是变量名不同:

# RPM
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")

# NSIS (Windows 安装包)
set(CPACK_NSIS_DISPLAY_NAME "MyApp")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
    "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyApp.lnk' '$INSTDIR\\\\bin\\\\myapp.exe'"
)

注意:CPack 的变量名体系比较庞大,建议先查阅官方文档。我个人的做法是写一个 CPackConfig.cmake 文件,把所有打包配置集中管理,这样 CMakeLists.txt 会清爽很多。

10.3 导出目标:让其他项目找到你

导出目标(export)是 CMake 中一个非常强大的特性。它允许你把当前项目编译好的目标「导出」给其他 CMake 项目使用,而不需要重新编译。

10.3.1 export() 命令

export() 用于在构建目录中生成目标导出文件,方便同一项目内的其他子项目使用:

# 导出目标到构建目录
export(TARGETS mylib
    FILE ${CMAKE_BINARY_DIR}/MyLibTargets.cmake
    NAMESPACE MyLib::
)

10.3.2 install(EXPORT) 命令

这个才是生产环境常用的。它会在安装目录中生成导出文件:

# 安装时导出目标
install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
)

# 安装导出文件本身
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)

为什么要加 NAMESPACE?说白了就是为了避免名字冲突。你想想看,如果两个库都导出一个叫 core 的目标,那不乱套了?加上命名空间后,使用者通过 MyLib::core 来引用,清晰明了。

关键点:install(EXPORT) 生成的 .cmake 文件包含了所有安装后的目标信息,包括它们的路径、依赖关系、编译选项等。其他项目通过 find_package 找到这个文件后,就能直接使用这些目标,就像使用自己编译的一样。

10.4 Config 文件生成:让 find_package 找到你

这是整个安装系统的「最后一公里」。你生成了安装包,导出了目标,但如果别人用 find_package(MyLib) 找不到你,那一切都白搭。

10.4.1 编写 Config 文件模板

CMake 使用 .cmake.in 模板文件来生成 Config 文件。我一般会这样写:

# MyLibConfig.cmake.in
@PACKAGE_INIT@

# 包含导出的目标文件
include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")

# 检查依赖
include(CMakeFindDependencyMacro)
find_dependency(OpenSSL REQUIRED)

# 设置版本信息
set(MyLib_VERSION @PROJECT_VERSION@)

# 检查编译选项兼容性
check_required_components(MyLib)

然后在 CMakeLists.txt 中配置:

# 配置 Config 文件
configure_package_config_file(
    ${CMAKE_SOURCE_DIR}/cmake/MyLibConfig.cmake.in
    ${CMAKE_BINARY_DIR}/MyLibConfig.cmake
    INSTALL_DESTINATION lib/cmake/MyLib
)

# 安装 Config 文件
install(
    FILES ${CMAKE_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION lib/cmake/MyLib
)

# 安装版本文件
write_basic_package_version_file(
    ${CMAKE_BINARY_DIR}/MyLibConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)
install(
    FILES ${CMAKE_BINARY_DIR}/MyLibConfigVersion.cmake
    DESTINATION lib/cmake/MyLib
)

10.4.2 使用者视角

当一切配置好后,其他项目只需要这样:

find_package(MyLib REQUIRED)

target_link_libraries(myapp PRIVATE MyLib::mylib)

就这么简单。CMake 会自动找到 MyLibConfig.cmake,加载导出的目标,设置好所有头文件路径和库路径。

避坑指南:我曾经在 Config 文件中忘记调用 check_required_components,结果用户 find_package 成功了,但链接时却找不到目标。这个函数会检查所有请求的组件是否都存在,少了它,CMake 不会报错,但后面会出问题。

10.5 完整示例:一个可安装的库项目

说了这么多,我们来个完整的例子。假设你有一个库叫 calculator,包含加减乘除功能:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(calculator VERSION 1.0.0 LANGUAGES CXX)

# 添加库
add_library(calculator SHARED src/calc.cpp)
target_include_directories(calculator
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

# 安装规则
install(TARGETS calculator
    EXPORT CalculatorTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
)

install(DIRECTORY include/ DESTINATION include
    FILES_MATCHING PATTERN "*.h"
)

# 导出目标
install(EXPORT CalculatorTargets
    FILE CalculatorTargets.cmake
    NAMESPACE calculator::
    DESTINATION lib/cmake/calculator
)

# Config 文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    cmake/CalculatorConfig.cmake.in
    ${CMAKE_BINARY_DIR}/CalculatorConfig.cmake
    INSTALL_DESTINATION lib/cmake/calculator
)
write_basic_package_version_file(
    ${CMAKE_BINARY_DIR}/CalculatorConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY AnyNewerVersion
)
install(FILES
    ${CMAKE_BINARY_DIR}/CalculatorConfig.cmake
    ${CMAKE_BINARY_DIR}/CalculatorConfigVersion.cmake
    DESTINATION lib/cmake/calculator
)

# CPack 配置
set(CPACK_PACKAGE_NAME "calculator")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_GENERATOR "DEB;RPM")
include(CPack)

对应的 Config 模板文件 cmake/CalculatorConfig.cmake.in

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/CalculatorTargets.cmake")
check_required_components(calculator)

构建和打包命令:

# 构建
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make

# 安装到系统
sudo make install

# 打包
cpack -G DEB    # 生成 calculator-1.0.0-Linux.deb
cpack -G RPM    # 生成 calculator-1.0.0-Linux.rpm

嗯,到这里,CMake 的安装与打包体系就完整了。从 install() 定义安装规则,到 CPack 生成安装包,再到 export 导出目标,最后用 Config 文件让 find_package 找到你——这一整套流程,就是现代 CMake 项目的标准做法。

我个人觉得,掌握这套体系后,你就不再是「会用 CMake 编译代码」了,而是真正「能用 CMake 管理一个完整的软件项目」。这两者之间的差距,说白了就是专业和业余的区别。

公众号:蓝海资料掘金营,微信 deep3321