包管理发布:CPack 打包与Conan生态
说到C++项目的交付,我见过太多团队栽在“最后一公里”上。代码写好了,编译通过了,结果交付给运维或客户时,要么缺动态库,要么版本号对不上。说白了,构建只是前半程,打包和发布才是真正考验工程能力的地方。
今天我们来聊聊两个核心工具:CPack 和 Conan。一个负责把项目打包成DEB、RPM、NSIS等安装包,另一个负责把库发布到私有或公共仓库。再加上语义化版本控制,这三者构成了C++项目交付的完整闭环。
核心逻辑图:从源码到交付的完整链路
CPack:一键生成多平台安装包
CPack是CMake自带的打包工具。你不需要额外安装什么,只要在CMakeLists.txt里配好规则,它就能帮你生成DEB、RPM、NSIS等格式的安装包。我个人习惯把CPack配置单独放在一个CPackConfig.cmake文件里,这样主CMakeLists.txt不会太臃肿。
基础配置示例
# CMakeLists.txt 底部添加
include(CPack)
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "一个高性能计算库")
set(CPACK_PACKAGE_VENDOR "蓝海科技")
set(CPACK_PACKAGE_CONTACT "dev@deep3321.com")
# 指定生成器
set(CPACK_GENERATOR "DEB;RPM;NSIS")
# DEB 专属配置
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "dev@deep3321.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl-dev (>= 1.1), libboost-system-dev")
# RPM 专属配置
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
配置好之后,在构建目录执行:
cmake --build . --target package
# 或者
cpack -G DEB # 只生成DEB包
cpack -G RPM # 只生成RPM包
嗯,这里要注意:CPack的版本号必须和CMake项目版本号保持一致。我曾经在一个项目里忘了同步,结果DEB包的版本是1.2.0,但库文件里写的版本是1.3.0,运维那边排查了半天。
NSIS:Windows安装包的特殊处理
如果你需要给Windows用户生成安装包,NSIS是首选。CPack对NSIS的支持很成熟,但有几个坑我得提醒你:
- 图标路径必须用反斜杠,或者用正斜杠但加上
file:///前缀 - 安装路径默认是Program Files,建议显式设置
CPACK_NSIS_INSTALL_ROOT - 卸载程序需要额外配置,否则用户只能手动删除
# NSIS 专属配置
set(CPACK_NSIS_DISPLAY_NAME "MyApp ${CPACK_PACKAGE_VERSION}")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
"CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyApp.lnk' '$INSTDIR\\\\bin\\\\myapp.exe'")
set(CPACK_NSIS_DELETE_ICONS_EXTRA
"Delete '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyApp.lnk'")
小技巧: 如果你同时生成DEB和RPM,建议在CI/CD流水线中分别构建。因为DEB和RPM的依赖系统不同,混在一起容易出问题。我一般用GitLab CI的并行job来处理。
Conan:C++的包管理利器
Conan解决了C++生态里一个老大难问题——库的依赖和分发。你想想看,以前我们要用第三方库,要么手动编译,要么找预编译包,版本还经常对不上。Conan把这一切标准化了。
上传包到Conan仓库
假设你写了一个日志库叫fastlog,想发布到团队私有的Conan仓库。首先你得写一个conanfile.py:
from conans import ConanFile, CMake
class FastlogConan(ConanFile):
name = "fastlog"
version = "1.2.3"
license = "MIT"
author = "deep3321"
url = "https://gitlab.internal/fastlog"
description = "高性能异步日志库"
settings = "os", "compiler", "build_type", "arch"
options = {"shared": [True, False], "fPIC": [True, False]}
default_options = {"shared": False, "fPIC": True}
generators = "cmake"
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
self.copy("*.h", dst="include", src="include")
self.copy("*.lib", dst="lib", keep_path=False)
self.copy("*.dll", dst="bin", keep_path=False)
self.copy("*.so", dst="lib", keep_path=False)
self.copy("*.a", dst="lib", keep_path=False)
def package_info(self):
self.cpp_info.libs = ["fastlog"]
然后执行上传命令:
conan create . fastlog/1.2.3@myteam/stable
conan upload fastlog/1.2.3@myteam/stable --remote=my_private_repo
这里myteam/stable是channel,我建议团队内部统一用stable和testing两个channel。稳定版走stable,开发中的版本走testing。
版本管理的痛点
Conan的版本管理其实挺灵活的,但用不好就会乱。我见过一个团队,每个人上传的版本号格式都不一样——有人用1.2.3,有人用1.2.3-beta,还有人用1.2.3.20231001。结果下游项目根本不知道该依赖哪个版本。
警告: Conan的版本号是字符串比较,不是语义化比较。1.2.3-beta会被认为大于1.2.3,因为-的ASCII码小于.?不对,实际上1.2.3-beta和1.2.3比较时,Conan会按字符串逐字符比较,结果可能出乎你的意料。所以一定要统一版本号格式。
语义化版本控制:团队的共同语言
语义化版本控制(SemVer)不是C++特有的,但在C++大型项目中尤其重要。为什么?因为C++的ABI兼容性问题太敏感了。一个库的次版本号升级,可能就导致所有依赖它的项目需要重新编译。
SemVer 核心规则
| 版本号 | 含义 | C++ 场景举例 |
|---|---|---|
| 主版本号 | 不兼容的API修改 | 修改了类继承结构、删除了虚函数 |
| 次版本号 | 向下兼容的功能新增 | 新增了非虚函数、新增了类 |
| 修订号 | 向下兼容的问题修正 | 修复了内存泄漏、修正了算法bug |
我个人习惯在CMakeLists.txt里这样定义版本:
set(PROJECT_VERSION_MAJOR 2)
set(PROJECT_VERSION_MINOR 1)
set(PROJECT_VERSION_PATCH 0)
set(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}")
project(MyLib VERSION ${PROJECT_VERSION})
# 生成版本头文件
configure_file(
"${CMAKE_SOURCE_DIR}/include/version.h.in"
"${CMAKE_BINARY_DIR}/include/version.h"
)
对应的version.h.in模板:
#define MYLIB_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MYLIB_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MYLIB_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MYLIB_VERSION "@PROJECT_VERSION@"
这样,每次发布新版本时,只需要改CMakeLists.txt顶部的三个数字,版本头文件会自动更新。CPack和Conan的版本号也从这个变量读取,保证一致性。
避坑指南: 我曾经在一个项目里,把次版本号从1.2.0升到1.3.0,但忘了检查ABI兼容性。结果有个模块用了新加的API,另一个模块没更新,链接时直接报错。从那以后,我定了个规矩:次版本号升级必须跑一遍ABI兼容性检查,用abi-compliance-checker这个工具。
整合:从构建到发布的一键流水线
把CPack、Conan和SemVer串起来,才是真正的工程实践。我推荐在CI/CD中这样设计:
- 开发阶段:版本号带
-dev后缀,如1.2.3-dev,Conan上传到testingchannel - 预发布阶段:版本号带
-rc1后缀,生成DEB/RPM包,部署到测试环境 - 正式发布:去掉后缀,CPack生成正式安装包,Conan上传到
stablechannel
这样做的好处是:版本号本身就携带了发布阶段的信息,任何人看到1.2.3-rc1就知道这是候选版本,不能用于生产环境。
最后说一句,工具只是手段,规范才是核心。CPack和Conan再强大,如果团队不遵守版本号规则,不维护好conanfile.py,那一切都是白搭。我见过太多团队,工具链搭得漂漂亮亮,结果因为版本号乱写,最后还是要靠人工沟通来确认依赖关系。
嗯,今天就聊到这里。记住:好的包管理习惯,能让你的C++项目活得更久。
公众号:蓝海资料掘金营,微信deep3321