10、CMake安装与打包:install()命令、CPack打包、导出目标与Config文件生成
说实话,CMake 的安装和打包这块,我当年刚接触的时候也绕了不少弯路。你想想看,代码写好了,编译通过了,但怎么把它装到系统里?怎么给别人用?怎么生成一个漂亮的安装包?这些才是真正考验构建系统功底的地方。
今天我们就来彻底搞懂 CMake 的安装与打包机制。我个人习惯把这部分分成四个核心模块:install() 命令、CPack 打包、导出目标、以及Config 文件生成。它们环环相扣,缺一不可。
核心观点:CMake 的安装系统不是简单的文件拷贝,而是一套完整的「构建-安装-分发-复用」生态链。理解了这个链条,你就能做出专业级的软件包。
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 只需要两步:
- 在 CMakeLists.txt 中设置打包参数
- 运行
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 管理一个完整的软件项目」。这两者之间的差距,说白了就是专业和业余的区别。