第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会自动根据目标类型选择正确的安装位置。

我的习惯:我会把静态库和动态库分开目录存放。静态库放到lib/static,动态库放到lib。这样客户链接时不会搞混。

13.1.2 安装头文件和配置文件

嵌入式项目里,头文件往往比库文件还重要。没有正确的头文件,别人根本没法调用你的API。

install(DIRECTORY include/
    DESTINATION include
    FILES_MATCHING
    PATTERN "*.h"
    PATTERN "*.hpp"
    PATTERN "internal" EXCLUDE
)

这段代码会把include目录下所有.h和.hpp文件安装到目标系统的include目录。注意我排除了internal子目录——那些是内部实现细节,不该暴露给用户。

踩坑提醒:我曾经把整个include目录一股脑全装进去,结果用户看到了内部宏定义,直接拿来用。后来版本迭代时那些宏被删了,用户代码就崩了。所以,只安装公开接口,内部头文件藏好。

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
)

FILESPROGRAMS的区别在于权限:前者默认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读取规则并生成包。

安装与打包核心流程 CMakeLists.txt install() 定义规则 cmake --build 编译生成目标文件 make install 安装到系统 CPack 配置 读取 install() 规则 DEB 包 .deb RPM 包 .rpm ZIP 包 .zip tar.gz .tar.gz cpack -G DEB / cpack -G RPM / cpack -G ZIP

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提供了VERSIONSOVERSION属性:

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,旧程序不会受影响。

核心要点:安装与打包不是最后才考虑的事。我建议你在项目一开始就规划好安装规则。哪怕现在只生成zip包,也要把install()写清楚。因为CPack直接复用install()的规则,你后面想生成deb包,只需要加几行配置就行。

13.6 总结

install()命令定义了文件该去哪,CPack把这些规则变成安装包。两者配合,就能实现一键交付。我见过太多项目手动打包、手动拷贝,结果版本混乱、路径错误。用CMake的安装与打包系统,这些烦恼都能省掉。

记住几个关键点:公开头文件和内部头文件分开、声明运行时依赖、用GNUInstallDirs处理跨平台路径、用SOVERSION管理库版本。做到这些,你的嵌入式SDK就能专业地交付给任何客户。

我的建议:在CI脚本里加上cpack步骤,每次打tag自动生成deb和zip包。这样客户随时能拿到最新版本,你也不用半夜爬起来给人发邮件传文件。

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