第二十三讲:CMake进阶——安装与打包
说实话,很多嵌入式工程师写CMakeLists.txt,写到编译链接能过就收工了。但项目要交付、要部署、要给别人用的时候,你总不能让人家去你电脑上拷二进制吧?
这一讲,我们就来聊聊CMake的安装与打包。说白了,就是怎么把你的编译产物——库、头文件、可执行文件、配置文件——规规矩矩地放到指定位置,再一键打包成deb、rpm或者tar.gz。
核心知识点速览
install()命令:定义安装规则- CMake的安装目标与DESTDIR
- CPack配置:打包成deb/rpm/tar.gz
- 组件安装与多包生成
- 避坑指南:路径、权限、依赖
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路径写成相对路径,比如lib、bin、include。安装时通过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生成器:
| 生成器 | 说明 | 适用场景 |
|---|---|---|
| DEB | Debian包 | Ubuntu/Debian系统 |
| RPM | Red Hat包 | CentOS/Fedora/RHEL |
| TGZ | tar.gz压缩包 | 通用、嵌入式裸机部署 |
| STGZ | 自解压安装包 | Windows/Linux通用 |
| NSIS | Windows安装程序 | 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-libraries、mylib-headers、mylib-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里的架构后缀,比如armhf、aarch64。 - 依赖自动检测:
CPACK_DEBIAN_PACKAGE_SHLIBDEPS设为ON时,CPack会调用dpkg-shlibdeps分析二进制文件的动态库依赖。但交叉编译环境下这个工具可能找不到库路径,需要额外配置CMAKE_INSTALL_RPATH。
嗯,安装与打包这部分内容,说白了就是「把东西收拾好,装进箱子,贴上标签」。规则不难,但细节很多。你只要把install()和CPack的配置写清楚,后续的交付、部署、CI/CD都会顺畅很多。
公众号:蓝海资料掘金营,微信deep3321