第二十三讲:CMake进阶——安装与打包

说实话,很多嵌入式工程师写CMakeLists.txt,写到编译链接能过就收工了。但项目要交付、要部署、要给别人用的时候,你总不能让人家去你电脑上拷二进制吧?

这一讲,我们就来聊聊CMake的安装与打包。说白了,就是怎么把你的编译产物——库、头文件、可执行文件、配置文件——规规矩矩地放到指定位置,再一键打包成deb、rpm或者tar.gz。

核心知识点速览

  • install()命令:定义安装规则
  • CMake的安装目标与DESTDIR
  • CPack配置:打包成deb/rpm/tar.gz
  • 组件安装与多包生成
  • 避坑指南:路径、权限、依赖
CMake安装与打包知识体系 install() + CPack install(TARGETS ...) ARCHIVE / LIBRARY / RUNTIME install(FILES ...) / install(DIRECTORY ...) set(CPACK_GENERATOR ...) DEB / RPM / TGZ / STGZ CPACK_PACKAGE_VERSION / CONTACT make install → cpack → 交付

install()命令:安装规则的定义

先看最基础的东西。install()命令是CMake里定义安装行为的入口。我刚开始用的时候,以为它就是把文件拷到某个目录,后来才发现它背后有一套完整的规则体系。

最常见的用法是安装目标文件:

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

这里要注意:ARCHIVE对应静态库(.a),LIBRARY对应动态库(.so),RUNTIME对应可执行文件(以及Windows上的DLL)。CMake会自动根据目标类型把文件放到对应的分类里。

我的习惯:我会把DESTINATION路径写成相对路径,比如libbininclude。安装时通过CMAKE_INSTALL_PREFIX控制根目录。这样移植性最好。

除了安装目标,还经常需要安装头文件和配置文件:

install(FILES mylib.h mylib_config.h
    DESTINATION include/mylib
)

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

install(DIRECTORY ...)有个坑——它会保留目录结构。如果你只想拷贝文件内容,记得用FILES而不是DIRECTORY

安装路径与DESTDIR

安装路径由CMAKE_INSTALL_PREFIX控制。默认是/usr/local。你可以这样改:

cmake -DCMAKE_INSTALL_PREFIX=/opt/mylib ..
make install

但实际项目中,我们经常需要做「临时安装」——先装到一个临时目录,再打包。这时候就要用DESTDIR

make install DESTDIR=/tmp/staging

最终文件会被放到/tmp/staging/opt/mylib/...。这个技巧在交叉编译和打包时特别有用。

我曾经踩过的坑:在某个嵌入式项目中,我忘了设置DESTDIR,直接make install把文件装到了宿主机的/usr/local里。结果交叉编译的ARM库和宿主机的x86库混在一起,排查了半天。从那以后,我只要做交叉编译,一定先设DESTDIR。

CPack配置:从安装到打包

安装规则写好了,下一步就是打包。CMake的CPack模块可以帮你生成deb、rpm、tar.gz等多种格式。

配置CPack其实就是在CMakeLists.txt里设置一堆CPACK_开头的变量:

include(CPack)

set(CPACK_PACKAGE_NAME "mylib")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My Library for Embedded Systems")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_PROCESSOR}")

# 指定生成器
set(CPACK_GENERATOR "DEB;RPM;TGZ")

然后运行:

cmake ..
cpack

CPack会自动读取你之前定义的install()规则,把安装好的文件打包成指定格式。

常用CPack生成器:

生成器说明适用场景
DEBDebian包Ubuntu/Debian系统
RPMRed Hat包CentOS/Fedora/RHEL
TGZtar.gz压缩包通用、嵌入式裸机部署
STGZ自解压安装包Windows/Linux通用
NSISWindows安装程序Windows桌面部署

组件安装:按需打包

大项目里,你可能想把库、开发头文件、示例程序分开打包。CMake的组件安装(COMPONENT)就是干这个的:

install(TARGETS mylib
    ARCHIVE DESTINATION lib
    COMPONENT libraries
)

install(FILES mylib.h
    DESTINATION include
    COMPONENT headers
)

install(TARGETS myapp
    RUNTIME DESTINATION bin
    COMPONENT applications
)

然后在CPack里指定组件:

set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_COMPONENTS_ALL libraries headers applications)

这样就能生成多个deb包:mylib-librariesmylib-headersmylib-applications。用户只需要装自己需要的部分。

我的建议:嵌入式项目里,我通常把运行时库和开发头文件分开打包。客户只需要运行时库,不需要头文件。这样包体积能小不少。

CPack的进阶配置

针对不同打包格式,CPack提供了很多细粒度配置。比如deb包:

set(CPACK_DEBIAN_PACKAGE_MAINTAINER "dev@mycompany.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31), libstdc++6 (>= 10)")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)  # 自动检测共享库依赖

rpm包类似:

set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_PACKAGE_REQUIRES "glibc >= 2.31, libstdc++ >= 10")

tar.gz包最简单,基本不需要额外配置:

set(CPACK_GENERATOR "TGZ")
# 搞定,直接cpack

注意:deb和rpm的依赖声明格式不同。deb用逗号分隔,rpm用空格分隔。我一开始没注意,结果生成的rpm包依赖解析失败,安装时报错。后来老老实实查了文档才改对。

一个完整的例子

把上面所有知识点串起来,一个典型的CMakeLists.txt长这样:

cmake_minimum_required(VERSION 3.16)
project(mylib VERSION 1.2.3 LANGUAGES C CXX)

add_library(mylib STATIC src/mylib.c)
add_executable(mylib_test test/test_mylib.c)
target_link_libraries(mylib_test mylib)

# 安装规则
install(TARGETS mylib
    ARCHIVE DESTINATION lib
    COMPONENT libraries
)
install(FILES include/mylib.h
    DESTINATION include
    COMPONENT headers
)
install(TARGETS mylib_test
    RUNTIME DESTINATION bin
    COMPONENT tests
)

# CPack配置
set(CPACK_PACKAGE_NAME "mylib")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")
set(CPACK_GENERATOR "DEB;RPM;TGZ")
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_COMPONENTS_ALL libraries headers tests)

include(CPack)

构建和打包命令:

mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make
make install DESTDIR=/tmp/staging
cpack

执行完,build/目录下会生成mylib-1.2.3-x86_64.deb.rpm.tar.gz三个包。每个包里都按组件分好了。

避坑指南

最后分享几个我实际项目中遇到的坑:

  • 路径硬编码:不要在install()里写绝对路径。用CMAKE_INSTALL_PREFIX + 相对路径,否则换台机器就炸了。
  • 权限问题:默认安装的文件权限是644(文件)和755(目录)。如果你有可执行脚本,记得用PERMISSIONS参数显式设置。
  • 交叉编译打包:CPack默认使用宿主机的架构名。交叉编译时,要手动设置CPACK_PACKAGE_FILE_NAME里的架构后缀,比如armhfaarch64
  • 依赖自动检测:CPACK_DEBIAN_PACKAGE_SHLIBDEPS设为ON时,CPack会调用dpkg-shlibdeps分析二进制文件的动态库依赖。但交叉编译环境下这个工具可能找不到库路径,需要额外配置CMAKE_INSTALL_RPATH

嗯,安装与打包这部分内容,说白了就是「把东西收拾好,装进箱子,贴上标签」。规则不难,但细节很多。你只要把install()和CPack的配置写清楚,后续的交付、部署、CI/CD都会顺畅很多。


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