第13章 安装与打包:install()命令、CPack配置、生成deb/rpm/zip包
嵌入式项目做到一定规模,你一定会遇到这个问题:写好的固件库、工具链、配置文件,怎么交给别人用?总不能每次都让人家从头编译一遍吧。我早年带团队时,就吃过这个亏——每次给客户交付SDK,都是手动拷贝头文件、库文件、脚本,结果版本对不上、路径放错,折腾了好几个通宵。
后来我彻底搞明白了CMake的安装与打包机制。说白了,install()命令就是告诉CMake:哪些文件该放到哪个目录。而CPack则是把这些文件自动打包成deb、rpm、zip等格式。今天咱们就把这块彻底讲透。
13.1 install()命令:把东西放到该放的位置
install()命令是CMake安装系统的核心。它的作用不是拷贝文件,而是描述安装规则。CMake会根据这些规则,在make install时执行真正的文件复制。
13.1.1 安装目标文件
最常用的场景是安装编译好的库或可执行文件。语法很简单:
install(TARGETS mylib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/static
RUNTIME DESTINATION bin
)
这里要注意:LIBRARY对应动态库(.so/.dll),ARCHIVE对应静态库(.a/.lib),RUNTIME对应可执行文件。CMake会自动根据目标类型选择正确的安装位置。
13.1.2 安装头文件和配置文件
嵌入式项目里,头文件往往比库文件还重要。没有正确的头文件,别人根本没法调用你的API。
install(DIRECTORY include/
DESTINATION include
FILES_MATCHING
PATTERN "*.h"
PATTERN "*.hpp"
PATTERN "internal" EXCLUDE
)
这段代码会把include目录下所有.h和.hpp文件安装到目标系统的include目录。注意我排除了internal子目录——那些是内部实现细节,不该暴露给用户。
13.1.3 安装配置文件与脚本
嵌入式项目经常需要配套的配置文件、启动脚本、udev规则等。这些也可以用install()搞定:
install(FILES config/myapp.conf
DESTINATION /etc/myapp
PERMISSIONS OWNER_READ OWNER_WRITE
GROUP_READ WORLD_READ
)
install(PROGRAMS scripts/start.sh
DESTINATION /usr/local/bin
)
FILES和PROGRAMS的区别在于权限:前者默认644,后者默认755。嗯,这个细节很容易被忽略。
13.2 安装路径的约定与自定义
CMake提供了一套GNU标准的安装路径变量。我建议你尽量遵循这些约定,除非你有特殊理由:
| 变量名 | 默认值 | 用途 |
|---|---|---|
| CMAKE_INSTALL_PREFIX | /usr/local | 安装根目录 |
| CMAKE_INSTALL_BINDIR | bin | 可执行文件 |
| CMAKE_INSTALL_LIBDIR | lib | 库文件 |
| CMAKE_INSTALL_INCLUDEDIR | include | 头文件 |
| CMAKE_INSTALL_SYSCONFDIR | /etc | 配置文件 |
你可以在CMakeLists.txt中覆盖这些变量:
set(CMAKE_INSTALL_LIBDIR "lib/${PROJECT_NAME}")
set(CMAKE_INSTALL_INCLUDEDIR "include/${PROJECT_NAME}-${PROJECT_VERSION}")
我个人习惯把库和头文件都加上项目名和版本号,这样多版本共存时不会冲突。你想想看,如果客户同时用了你两个版本,头文件互相覆盖,那画面太美不敢看。
13.3 CPack配置:一键生成安装包
install()只是第一步。真正交付时,你需要一个安装包。CPack就是CMake自带的打包工具,它读取install()定义的规则,然后生成各种格式的安装包。
13.3.1 基础配置
在CMakeLists.txt末尾加上这几行:
include(CPack)
set(CPACK_PACKAGE_NAME "myapp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_VENDOR "My Embedded Company")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Embedded SDK for MyApp")
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")
然后运行:
cmake -B build
cmake --build build
cpack --config build/CPackConfig.cmake
默认会生成一个tar.gz压缩包。但嵌入式项目里,我们更常用deb或rpm。
13.3.2 生成deb包
Debian系的嵌入式系统(比如树莓派、Ubuntu Core)用deb包最方便。配置如下:
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@mycompany.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.28), libusb-1.0-0 (>= 1.0.22)")
这里CPACK_DEBIAN_PACKAGE_DEPENDS特别重要——它声明了运行时依赖。我曾经漏掉libusb的依赖,结果客户装完包后程序跑不起来,排查了半天才发现是缺少USB库。从那以后,我每次打包前都会用ldd检查一遍所有依赖。
13.3.3 生成rpm包
如果是Yocto或Fedora系的嵌入式系统,rpm包更合适:
set(CPACK_GENERATOR "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_PACKAGE_REQUIRES "libc >= 2.28, libusb >= 1.0.22")
rpm和deb的依赖声明语法略有不同,但逻辑一样。我建议你在CI脚本里同时生成两种格式,省得客户抱怨。
13.3.4 生成zip包
有些客户就是想要一个zip包,解压就能用。尤其是Windows上的嵌入式开发工具链:
set(CPACK_GENERATOR "ZIP")
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY ON)
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}")
这里CPACK_INCLUDE_TOPLEVEL_DIRECTORY控制是否在zip内创建一个顶层目录。我习惯设为ON,这样解压后所有文件都在一个文件夹里,不会散得到处都是。
13.4 知识体系与核心逻辑
下面这张图展示了安装与打包的整体流程。你可以看到,从CMakeLists.txt到最终安装包,中间经过了两个关键步骤:install()定义规则,CPack读取规则并生成包。
13.5 实战技巧与避坑指南
13.5.1 组件化安装
大型嵌入式SDK往往包含多个组件:运行时库、开发头文件、调试符号、示例代码。你可以用COMPONENT参数把它们分开:
install(TARGETS mylib
LIBRARY DESTINATION lib
COMPONENT runtime
)
install(FILES include/mylib.h
DESTINATION include
COMPONENT dev
)
install(DIRECTORY examples/
DESTINATION share/myapp/examples
COMPONENT examples
)
然后用户可以选择只安装运行时:
cmake -DCOMPONENT=runtime -P cmake_install.cmake
CPack也支持组件打包:
set(CPACK_COMPONENTS_ALL runtime dev examples)
set(CPACK_DEB_COMPONENT_INSTALL ON)
这样会生成多个deb包:myapp-runtime.deb、myapp-dev.deb、myapp-examples.deb。客户只需要装runtime就行,开发人员才需要装dev包。
13.5.2 跨平台路径处理
嵌入式项目经常要交叉编译。不同平台的安装路径可能不一样。比如ARM Linux的库路径可能是/usr/lib/arm-linux-gnueabihf,而x86 Linux是/usr/lib/x86_64-linux-gnu。
我建议用GNUInstallDirs模块自动处理:
include(GNUInstallDirs)
install(TARGETS mylib
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
这个模块会根据当前平台自动选择正确的路径。嗯,省心不少。
13.5.3 版本管理与符号链接
动态库的版本管理是个容易翻车的地方。CMake提供了VERSION和SOVERSION属性:
set_target_properties(mylib PROPERTIES
VERSION 1.2.3
SOVERSION 1
)
安装时会自动生成符号链接:
libmylib.so -> libmylib.so.1
libmylib.so.1 -> libmylib.so.1.2.3
libmylib.so.1.2.3 (实际文件)
这样应用程序链接时用-lmylib,运行时加载libmylib.so.1。如果以后升级到2.0,SOVERSION变成2,旧程序不会受影响。
13.6 总结
install()命令定义了文件该去哪,CPack把这些规则变成安装包。两者配合,就能实现一键交付。我见过太多项目手动打包、手动拷贝,结果版本混乱、路径错误。用CMake的安装与打包系统,这些烦恼都能省掉。
记住几个关键点:公开头文件和内部头文件分开、声明运行时依赖、用GNUInstallDirs处理跨平台路径、用SOVERSION管理库版本。做到这些,你的嵌入式SDK就能专业地交付给任何客户。
公众号:蓝海资料掘金营,微信deep3321