11、安装与打包:install命令详解、CPack配置、生成安装包、安装路径规范

说实话,很多开发者写CMakeLists.txt时,只关心编译能不能过。安装和打包?那是最后才想的事。我以前也这样,直到有一次要给客户交付一个SDK,手动复制头文件和库文件,折腾了大半天,还漏了一个依赖库……嗯,从那以后,我再也不敢轻视install命令和CPack了。

这一章,我们就来聊聊怎么把你的构建产物,优雅地安装到系统里,再打包成漂亮的安装包。

11.1 install() 命令——安装的核心

install() 是CMake里最常用的安装指令。它告诉构建系统:哪些文件要装到哪里去。我个人习惯把install命令写在每个子目录的CMakeLists.txt末尾,这样模块清晰,维护起来也方便。

11.1.1 安装目标(TARGETS)

最常用的场景,就是把编译出来的库或可执行文件安装到指定目录。

install(TARGETS mylib myapp
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib/static
)

这里要注意:RUNTIME 对应可执行文件和Windows上的DLL,LIBRARY 对应Linux上的.so或macOS上的.dylib,ARCHIVE 对应静态库.a或.lib。我曾经见过有人把静态库装到 lib 目录,结果链接器找不到,折腾了半天才发现是路径写错了。

小技巧: 如果你在Windows上开发,记得把DLL的安装路径和可执行文件放在一起(都放bin),否则运行时找不到DLL会报错。

11.1.2 安装文件(FILES / DIRECTORY)

除了目标文件,头文件、配置文件、文档等也需要安装。

install(FILES include/mylib.h DESTINATION include/mylib)
install(DIRECTORY config/ DESTINATION etc/mylib
    FILES_MATCHING PATTERN "*.conf"
)

我习惯把头文件单独放在一个 include/ 子目录下,这样用户引用时路径清晰:#include <mylib/mylib.h>。你想想看,如果所有头文件都平铺在 include/ 下,大型项目很容易重名冲突。

11.1.3 安装脚本与权限

有时候需要安装一些脚本,或者设置文件权限。

install(PROGRAMS scripts/start.sh DESTINATION bin)
install(FILES README.md DESTINATION share/doc/mylib)

PROGRAMS 会自动给文件添加可执行权限,省得你手动 chmod +x

11.2 安装路径规范——别乱放

安装路径是有行业惯例的。GNU标准定义了 prefixbindirlibdirincludedir 等变量。CMake内置了 GNUInstallDirs 模块,帮你自动适配。

include(GNUInstallDirs)
install(TARGETS mylib
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib
)

这样做的好处是:在Linux上默认装到 /usr/local/lib,在macOS上装到 /usr/local/lib,在Windows上装到 C:/Program Files/.../lib。跨平台不用改代码。

注意: 不要硬编码路径!比如 DESTINATION /usr/local/lib,这样用户想安装到其他前缀时就会失败。用 ${CMAKE_INSTALL_PREFIX}GNUInstallDirs 才是正道。

11.3 CPack——一键生成安装包

CMake自带的打包工具叫CPack。它读取CMakeLists.txt里的安装规则,然后生成各种格式的安装包:Linux上的deb/rpm、macOS上的dmg、Windows上的NSIS/InnoSetup,甚至压缩包tar.gz/zip。

11.3.1 基础配置

在顶层CMakeLists.txt里加上这几行:

include(CPack)
set(CPACK_PACKAGE_NAME "MyLib")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A modern C++ library")
set(CPACK_GENERATOR "TGZ;DEB;RPM")

然后运行:

cmake --build . --target package
# 或者
cpack

你就会在当前目录下看到 MyLib-1.0.0-Linux.tar.gzMyLib-1.0.0-Linux.deb 等文件。是不是很简单?

11.3.2 针对不同平台的定制

不同平台的安装包有各自的元数据要求。比如deb包需要维护者信息,rpm需要分组。

set(CPACK_DEBIAN_PACKAGE_MAINTAINER "me@example.com")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_NSIS_DISPLAY_NAME "MyLib Installer")

我曾经给一个客户打包Windows安装程序,忘了设置 CPACK_NSIS_CREATE_ICONS,结果安装完桌面上没有快捷方式,客户以为安装失败了……嗯,从那以后我每次都会检查NSIS的配置项。

11.3.3 组件安装

大型项目通常有多个组件:运行时库、开发头文件、文档、示例等。CPack支持按组件打包。

install(TARGETS mylib
    COMPONENT runtime
    LIBRARY DESTINATION lib)
install(FILES include/mylib.h
    COMPONENT dev
    DESTINATION include)
install(DIRECTORY examples/
    COMPONENT examples
    DESTINATION share/mylib/examples)

set(CPACK_COMPONENTS_ALL runtime dev examples)
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "Runtime libraries")
set(CPACK_COMPONENT_DEV_DESCRIPTION "Development headers")

这样用户可以选择只安装运行时,或者全部安装。我一般会把 dev 组件单独打包,因为很多用户只需要运行程序,不需要头文件。

11.4 知识体系结构图

下面这张图,帮你理清安装与打包的整体脉络。

安装与打包知识体系 安装与打包 install() 命令 安装路径规范 CPack 打包 TARGETS 安装目标 FILES 安装文件 DIRECTORY 目录 PROGRAMS 脚本 GNUInstallDirs CMAKE_INSTALL_PREFIX 跨平台适配 基础配置 平台定制(deb/rpm/nsis) 组件安装 生成安装包 核心:install() 定义规则 → CPack 读取规则 → 生成安装包

11.5 实战:一个完整的安装打包示例

我们来看一个实际项目的CMakeLists.txt片段。假设我有一个库叫 fastjson,包含头文件、动态库和示例程序。

cmake_minimum_required(VERSION 3.16)
project(fastjson VERSION 2.1.0 LANGUAGES CXX)

include(GNUInstallDirs)

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

# 添加可执行文件
add_executable(json_parser examples/parser.cpp)
target_link_libraries(json_parser PRIVATE fastjson)

# 安装规则
install(TARGETS fastjson
    EXPORT fastjsonTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/fastjson
)
install(TARGETS json_parser
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES include/fastjson/json.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/fastjson
)
install(EXPORT fastjsonTargets
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/fastjson
)

# CPack配置
set(CPACK_PACKAGE_NAME "fastjson")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_GENERATOR "TGZ;DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "dev@mycompany.com")
include(CPack)

运行 cmake --build . --target package 后,你会得到 fastjson-2.1.0-Linux.tar.gzfastjson-2.1.0-Linux.deb。安装到系统里试试:

sudo dpkg -i fastjson-2.1.0-Linux.deb
# 或者
tar -xzf fastjson-2.1.0-Linux.tar.gz -C /usr/local
核心要点: 安装和打包不是事后才想的事。从项目一开始,就应该规划好头文件、库文件、可执行文件的安装路径。用 GNUInstallDirs 保证跨平台兼容,用 CPack 一键生成安装包。这样交付给用户时,他们只需要一条命令就能安装使用。

好了,这一章的内容就到这里。安装和打包看似简单,但细节很多。我建议你动手试一下,哪怕是一个只有两个文件的小项目,走一遍完整的安装打包流程,你会对CMake的理解更深一层。


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