包管理发布:CPack 打包与Conan生态

说到C++项目的交付,我见过太多团队栽在“最后一公里”上。代码写好了,编译通过了,结果交付给运维或客户时,要么缺动态库,要么版本号对不上。说白了,构建只是前半程,打包和发布才是真正考验工程能力的地方

今天我们来聊聊两个核心工具:CPackConan。一个负责把项目打包成DEB、RPM、NSIS等安装包,另一个负责把库发布到私有或公共仓库。再加上语义化版本控制,这三者构成了C++项目交付的完整闭环。

核心逻辑图:从源码到交付的完整链路

CMake 构建 CPack 打包 安装包 交付/部署 Conan 构建 Conan 上传 私有仓库 团队复用 语义化版本控制(SemVer)贯穿始终

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,我建议团队内部统一用stabletesting两个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-beta1.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中这样设计:

  1. 开发阶段:版本号带-dev后缀,如1.2.3-dev,Conan上传到testing channel
  2. 预发布阶段:版本号带-rc1后缀,生成DEB/RPM包,部署到测试环境
  3. 正式发布:去掉后缀,CPack生成正式安装包,Conan上传到stable channel

这样做的好处是:版本号本身就携带了发布阶段的信息,任何人看到1.2.3-rc1就知道这是候选版本,不能用于生产环境。

最后说一句,工具只是手段,规范才是核心。CPack和Conan再强大,如果团队不遵守版本号规则,不维护好conanfile.py,那一切都是白搭。我见过太多团队,工具链搭得漂漂亮亮,结果因为版本号乱写,最后还是要靠人工沟通来确认依赖关系。

嗯,今天就聊到这里。记住:好的包管理习惯,能让你的C++项目活得更久


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