安装与打包:从代码到可交付物

说实话,很多开发者写完 CMakeLists.txt 就以为万事大吉了。编译通过、能跑起来,嗯,任务完成。但作为一个经历过多次交付痛苦的工程师,我得说:安装和打包才是真正让项目“毕业”的环节。你想想看,代码写得再好,如果别人不知道怎么装、怎么用,那跟没写有什么区别?

这一章,我们就来聊聊 CMake 的 install 命令和 CPack 打包工具。我会结合我踩过的坑,帮你把这些看似琐碎但极其重要的技能点打通。

1. install 命令:告诉 CMake 什么东西该放哪儿

install() 是 CMake 里最核心的安装指令。它的作用说白了就是:定义哪些文件、目标、目录需要被复制到安装路径下。我个人习惯把 install 命令放在每个子目录的 CMakeLists.txt 末尾,这样模块清晰,维护起来也方便。

1.1 安装目标(Targets)

最常见的场景是安装库和可执行文件。比如你编译了一个动态库 libfoo.so,你想把它装到 /usr/local/lib,头文件装到 /usr/local/include。用 CMake 表达就是:

add_library(foo SHARED foo.cpp)
target_include_directories(foo PUBLIC include)

install(TARGETS foo
        LIBRARY DESTINATION lib
        PUBLIC_HEADER DESTINATION include)

这里有个细节:PUBLIC_HEADER 需要你在 set_target_properties 里先指定好头文件列表。我在项目中遇到过有人忘了这步,结果头文件没装上去,别人引用时报错找不到头文件,排查了半天。

我的习惯: 对于库项目,我通常会同时安装 ARCHIVE(静态库)、LIBRARY(动态库)、RUNTIME(可执行文件或 DLL)三个组件,覆盖所有平台。

1.2 安装文件(Files)

除了目标本身,你可能还需要安装一些辅助文件:配置文件、脚本、文档、甚至图片资源。这时候用 install(FILES ...)install(DIRECTORY ...)

install(FILES config.json DESTINATION etc/myapp)
install(DIRECTORY scripts/ DESTINATION bin
        FILES_MATCHING PATTERN "*.sh")

注意 DIRECTORY 的路径末尾带不带斜杠,行为是不一样的。带斜杠表示复制目录内容,不带斜杠表示复制目录本身。嗯,这个坑我踩过两次,后来干脆统一用 DIRECTORY dir/ 加斜杠,省心。

1.3 安装脚本与自定义逻辑

有时候你需要安装前或安装后执行一些操作,比如创建符号链接、修改权限、生成配置文件。CMake 提供了 install(CODE ...)install(SCRIPT ...) 来嵌入 CMake 代码或运行外部脚本。

install(CODE [[
    message(STATUS "Post-install: creating symlink")
    execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
                    libfoo.so.1 libfoo.so)
]])

我曾经用这个功能在安装后自动生成 .pc 文件(pkg-config 用),省去了手动维护的麻烦。

2. 安装路径与 DESTINATION 的讲究

DESTINATION 是相对路径,默认相对于 CMAKE_INSTALL_PREFIX。这个前缀默认是 /usr/local(Unix)或 C:/Program Files(Windows)。你可以通过 cmake -DCMAKE_INSTALL_PREFIX=/opt/myapp .. 来修改。

但更灵活的做法是使用 GNUInstallDirs 模块。它会帮你定义一套标准路径变量:

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

这样你的项目就能自动适配不同发行版的目录规范。比如 Fedora 用 lib64,Debian 用 lib/x86_64-linux-gnu,GNUInstallDirs 会帮你处理好。

注意: 不要在 DESTINATION 里写死绝对路径,比如 /usr/lib。这会让用户无法通过 CMAKE_INSTALL_PREFIX 改变安装位置,非常不友好。

3. CPack:一键生成安装包

CMake 自带的打包工具叫 CPack。它读取 CMake 项目中的 install() 信息,然后生成各种格式的安装包:tar.gz、deb、rpm、NSIS(Windows 安装程序)、DMG(macOS)等等。你只需要在顶层 CMakeLists.txt 里加上几行配置:

set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A fantastic application")
set(CPACK_GENERATOR "TGZ;DEB;RPM")
include(CPack)

然后运行:

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

就会在当前构建目录下生成 MyApp-1.2.3-Linux.tar.gzMyApp-1.2.3-Linux.deb 等文件。

3.1 配置 CPack 的常见变量

变量名 说明 示例值
CPACK_PACKAGE_NAME 包名 MyApp
CPACK_PACKAGE_VERSION 版本号 1.2.3
CPACK_GENERATOR 要生成的包格式列表 TGZ;DEB;RPM
CPACK_PACKAGE_CONTACT 维护者邮箱 dev@example.com
CPACK_DEBIAN_PACKAGE_MAINTAINER Deb 包维护者 John Doe <john@example.com>
CPACK_RPM_PACKAGE_LICENSE RPM 包许可证 MIT

每个生成器还有自己专属的变量,比如 CPACK_DEBIAN_PACKAGE_DEPENDS 用来声明依赖。我曾经因为忘了设置这个,生成的 deb 包安装时缺少 libssl,用户反馈说“装完打不开”。从那以后,我每次都会仔细检查依赖声明。

3.2 组件打包(Component Packaging)

如果你的项目包含多个组件(比如库、开发头文件、示例程序),你可以让用户选择安装哪些部分。这需要用到 install()COMPONENT 参数:

install(TARGETS foo
        LIBRARY DESTINATION lib
        COMPONENT runtime)
install(FILES include/foo.h DESTINATION include
        COMPONENT dev)

然后在 CPack 中启用组件安装:

set(CPACK_COMPONENTS_ALL runtime dev)
set(CPACK_DEB_COMPONENT_INSTALL ON)
include(CPack)

这样生成的 deb 包会拆成 myapp-runtimemyapp-dev 两个包,用户按需安装。我在做 SDK 项目时特别喜欢用这个功能,因为用户只需要 runtime 就能跑,不需要装开发头文件。

4. 知识体系总览

下面这张图总结了安装与打包的核心流程和关键命令,你可以把它当作一个快速参考:

CMake 安装与打包知识体系 install 命令 安装目标 TARGETS 安装文件 FILES / DIRECTORY 安装脚本 CODE / SCRIPT 组件安装 COMPONENT DESTINATION 路径 + GNUInstallDirs CMAKE_INSTALL_PREFIX 输入 CPack 打包 配置变量 CPACK_* 系列 生成器 TGZ / DEB / RPM / NSIS 组件打包 CPACK_COMPONENTS_ALL 平台专属 DEBIAN_* / RPM_* cmake --build . --target package 或直接运行 cpack 输出:安装包(.tar.gz / .deb / .rpm ...)

5. 实战建议与避坑指南

  • 先测试 install,再测试 CPack。 我习惯先跑 cmake --install . --prefix /tmp/test-install 看看文件结构对不对,确认无误后再打包。
  • 版本号一定要同步。 我曾经因为忘记更新 CPACK_PACKAGE_VERSION,导致打出来的包版本号还是旧的,用户升级时被覆盖了都不知道。
  • 跨平台打包时注意换行符。 Windows 上的脚本文件如果用了 Unix 换行符,安装后可能无法直接运行。可以用 install(FILES ...) 配合 PERMISSIONS 设置可执行权限。
  • 善用 cmake --install--component 选项。 调试组件安装时特别有用,可以只安装某个组件看看效果。

核心要点回顾:

  • install() 定义安装规则,支持目标、文件、目录、脚本、组件。
  • 使用 GNUInstallDirs 让路径更规范、更可移植。
  • CPack 读取 install 规则,生成多种格式的安装包。
  • 组件打包让用户按需安装,提升体验。
  • 先本地测试 install,再打包,减少返工。

好了,安装与打包这部分就聊到这儿。你只要把 installCPack 配合好,你的项目就能从“能编译”进化到“能交付”。下次别人拿到你的代码,直接 cmake --build . --target package 就能得到一个漂亮的安装包,那种感觉,嗯,挺爽的。


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