安装与打包:从代码到可交付物
说实话,很多开发者写完 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.gz、MyApp-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-runtime 和 myapp-dev 两个包,用户按需安装。我在做 SDK 项目时特别喜欢用这个功能,因为用户只需要 runtime 就能跑,不需要装开发头文件。
4. 知识体系总览
下面这张图总结了安装与打包的核心流程和关键命令,你可以把它当作一个快速参考:
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,再打包,减少返工。
好了,安装与打包这部分就聊到这儿。你只要把 install 和 CPack 配合好,你的项目就能从“能编译”进化到“能交付”。下次别人拿到你的代码,直接 cmake --build . --target package 就能得到一个漂亮的安装包,那种感觉,嗯,挺爽的。