胡萝卜烤地瓜
胡萝卜烤地瓜
发布于 2026-08-02 / 1 阅读
0
0

NO.B3 包的身份证怎么办

⚠️ 法律与用途声明:本文讨论企业自有 Conan 包的命名、版本与归档规范,基于开源工具(Conan 2 / CMake),属于经验分享。所有企业名、项目名、内部仓库与账号均已脱敏,代码示例中的标识符均为占位。读者须遵守所在地区法律法规。


一、企业自有的包,到底该怎么叫

B0 和 B2 讲了 metadata.json 怎么驱动 recipe、怎么管字段。但有个更基础的问题一直没正面回答:企业自有的 Conan 包,到底该怎么命名、版本怎么标、源码怎么归档、二进制兼容性怎么保证

这事看着是格式问题,实际上决定了整个包生态能不能跑顺。包名乱起、版本乱标、源码不归档,后面所有能力(双 remote 消费、构建矩阵展开、人工晋升门禁、上板验证)都会被拖累。

这篇正面讲清楚五件事:包叫什么名、源码怎么随包走、一个仓为什么出两个库、自家包怎么防撞、recipe revision 和 package_id 怎么让同名包精确区分。

二、门牌号还是暗号:name/version vs @user/channel

Conan 里包有两种引用风格,A5 讲双 remote 时提过,这里展开讲透。

  • name/version:门牌号风格。清晰、好记、和 ConanCenter 完全一致。
  • name/version@user/channel:暗号风格。后面挂着一串用户名和频道,是 Conan 1 时代个人往公共仓传东西留下的。

团队里统一用门牌号,不用暗号。理由不是"暗号不合法",而是暗号在团队规范里只增加心智负担。想象一下:开发者要用一个包,得记住它叫 my-algo/1.0.0@team-a/experimental,换个频道又是 my-algo/1.0.0@team-a/stable——谁记得住?门牌号风格只有 my-algo/1.0.0,公共仓和私有仓长得一模一样,不用切换脑子。

来看两种写法的 recipe 长什么样(对比):

# ❌ 暗号风格:写死在 recipe 里
class MyPackage(ConanFile):
    name = "my-algo"
    version = "1.0.0"
    # Conan 2 其实已经不推荐 @user/channel 了
    # 但有些老 recipe 还在用

# ✅ 门牌号风格:从 metadata 继承
class PackageRecipe(ConanFile):
    def init(self):
        meta = load_json("metadata.json")
        self.name = meta["name"]       # "acme-my-algo"
        self.version = meta["version"] # "1.0.0"
        # 不写 user/channel,Conan 2 默认就是 name/version

在模板仓里这件事靠 metadata.json 保证:recipe 的 init() 从 metadata 读 name/version,不写死 @user/channel。所以只要从模板创建的仓库,天生就是门牌号风格。

还有一个实际影响:双 remote 消费时,暗号风格的包在公共仓和私有仓之间容易混乱。公共仓的包都是 name/version,私有仓如果用 name/version@user/channel,Conan 在查找时匹配逻辑不同,偶尔会出现"明明仓库里有,但 Conan 找不到"的灵异问题。统一门牌号,查找逻辑也统一。

三、源码随包归档:不归档等于不可追溯

一个包进了真源(Artifactory),它归档的应该不只是二进制,还有 recipe 和源码。A8 讲过"制品真源要完整",这里讲怎么在 recipe 层面实现。

Conan 2 有两种声明源码归档的方式:

# 方式一:列表式(简单直接)
class PackageRecipe(ConanFile):
    exports_sources = "src/*", "include/*", "CMakeLists.txt"

# 方式二:方法式(需要动态计算时用)
class PackageRecipe(ConanFile):
    def export_sources(self):
        # 比如根据 metadata 决定导出哪些文件
        self.export_sources = ["src/*", "include/*"]

声明之后,conan upload 在传二进制的同时,会把列出的源码文件打包成 conan_sources.tgz 一起归档到仓库。效果是:任何一份正式发布的包,都能追到产生它的源码

如果没归档源码会怎样?我在实际部署里见过这种情况:一个包出了线上 bug,查回去发现 recipe 里有版本号、有依赖声明,但没有源码——它是从哪份代码编出来的、当时改了什么,全靠人脑记忆和聊天记录。有了 conan_sources.tgz,任何时候从仓库拉一个包,连源码一起拉出来,打开就能看:

# 从私有仓拉包,连源码一起拉
conan download "<包名>/<版本>" -r=<私有仓>

# 本地缓存的包目录里就有解压后的源码
ls ~/.conan2/p/<包名>_<版本>_<hash>/source/
# src/  include/  CMakeLists.txt

在模板仓里,exports_sources 不在 recipe 里写死,而是由 metadata 和模板逻辑控制。如果某个项目的 recipe 忘了声明,A9 讲的 promote 门禁还会兜底补上——绝不让一个查不到源码的包溜进真源。

四、双接口产物:一个仓库出两个库

这个模板仓有个特别的设计:一个仓库的产物不是一个库,是两个——C 接口库和 C++ 接口库。

先看 package_info 里怎么声明这两个组件:

def package_info(self):
    # 两个组件,各自有独立的库名和依赖
    self.cpp_info.components[f"{self.name}_c"].libs = [f"{self.name}_c"]
    self.cpp_info.components[f"{self.name}_c"].requires = [
        # C 接口只依赖 C 兼容的库(比如 pcre2)
    ]

    self.cpp_info.components[f"{self.name}_cpp"].libs = [f"{self.name}_cpp"]
    self.cpp_info.components[f"{self.name}_cpp"].requires = [
        # C++ 接口可以依赖 C++ 生态库(比如 Eigen、fmt)
    ]

为什么要分两个?

C 接口供跨语言调用和裸机环境用。它的头文件用 extern "C" 包裹,保证没有 C++ name mangling,任何语言(Python ctypes、Java JNI、Rust FFI)都能调。更重要的是,裸机 MCU 上只有 C 编译器(arm-none-eabi-gcc),没有 C++ 运行时——C 接口是唯一能在裸机上跑的。

// C 接口头文件:extern "C" 保证 ABI 兼容
#ifdef __cplusplus
extern "C" {
#endif

int my_algo_process(const float* input, int len, float* output);

#ifdef __cplusplus
}
#endif

C++ 接口供现代 C++ 开发用。完整的类、模板、RAII、异常安全,依赖 C++ 生态的库(比如 Eigen 做矩阵运算、fmt 做格式化)。这些在 Linux 开发机上跑得好好的,但搬到裸机 MCU 上就跑不了——没 C++ 运行时、没 STL、没异常。

两个接口从同一份源码编译出来(CMake 编译两次,一次出 .a 给 C、一次出 .a 给 C++),但产物分开、依赖分开、链接目标分开。下游使用者按需链接:

# CMakeLists.txt(消费者侧,按需链接)
find_package(<包名> REQUIRED)

# 只要 C 接口 → 只链 C 库和它的 C 依赖
target_link_libraries(my-app PRIVATE <包名>::<包名>_c)

# 要 C++ 接口 → 链 C++ 库和它的 C++ 依赖
target_link_libraries(my-app PRIVATE <包名>::<包名>_cpp)

这个设计的工程价值是:一份代码服务两类消费者,不互相拖累。C 侧不需要 C++ 的重依赖,C++ 侧不受 C 的 ABI 限制。同一个包,在 Linux 开发机上可以用 C++ 接口享受现代开发体验,在裸机 MCU 上切到 C 接口照样能跑。

五、给自家包加前缀:防撞条码

A5 讲双 remote 时提过"给自家包加前缀防撞",这里从 recipe 角度再讲一次。

双 remote 的消费顺序是"公共仓优先、私有仓兜底"。查找逻辑伪代码:

def resolve_package(name, version):
    # 先查公共仓
    pkg = conancenter.lookup(name, version)
    if pkg:
        return pkg       # 命中公共仓,直接用

    # 公共仓没有,再查私有仓
    pkg = private_repo.lookup(name, version)
    if pkg:
        return pkg       # 命中私有仓

    raise NotFound(f"{name}/{version} 两个仓都没有")

如果自家包和公共包同名同版本,按顺序会先命中公共仓——你可能拿到的是公共原版,不是你自家 patched 版。这种 bug 极其难查:包名一样、版本一样,内容不一样,conan install 不报错,但行为和你预期不同。

解法:企业所有内部包统一加一个独有前缀(比如 acme-xxx),让条码撞不上。公共仓不可能有你们内部前缀的包,于是同名冲突从根上不存在。

这条规矩在模板仓里通过 metadata.json 的 name 字段保证——模板初始化时就填好前缀:

{
  "name": "acme-vision-algo",
  "version": "1.0.0"
}

后续创建的包天生带前缀,开发者不用自己去想"该加什么前缀"。

六、recipe revision:同名同版本的包怎么区分内容

Conan 2 有一个机制叫 recipe revision(RREV)——它相当于包的指纹。

同一个包名加版本(比如 acme-algo/1.0.0),如果 recipe 被改过(比如修了个依赖声明、调了个默认选项),Conan 会生成一个新的 recipe revision。两个 recipe revision 对应同一份 name/version,但内容不同。

# 查看私有仓里某包的所有 recipe revision
conan list "<包名>/*:*" -r=<私有仓>
# 输出示意:
# acme-algo/1.0.0#759a98dfeaefc942a899043e97fb3465
# acme-algo/1.0.0#b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7

两串 hash 就是两个 recipe revision。同一个 name/version,因为 recipe 改了,产生了两个 revision。Conan 在查找时,默认拿最新的 revision,但你也可以指定用哪个。

为什么这条很重要?因为没有 recipe revision,同名同版本的包就是一锅粥。你改了 recipe 推上去,别人本地缓存的还是老 recipe,出了行为不一致的 bug,谁也说不清"到底是哪一版"。有了 revision,每次 recipe 变更都有唯一的指纹可查,版本管理的精度从 name/version 细到了 name/version#revision。

七、package_id:什么决定二进制兼容性

recipe revision 管的是 recipe 本身(配方),那二进制产物呢?Conan 2 里,同一个 recipe 在不同环境下编出来的二进制是不同的 package_id

# package_id 由 settings + options 组合决定
# 不同的 os / arch / compiler / build_type → 不同 package_id

# 同一份 recipe,不同环境下:
# Linux x86_64 gcc 13 Release → package_id#aaa111
# Linux armv7 gcc 11 Release  → package_id#bbb222
# baremetal cortex-m4 Release  → package_id#ccc333

这意味着同一个 recipe revision 下面,可能挂着好几个 package_id——每个对应一种目标环境的二进制。CI 矩阵每跑一个 target,产出的就是一个新的 package_id。

这条机制的工程含义是:二进制兼容性由 settings 组合决定,不由人嘴说。你不能口头说"这个包和上个版本兼容"——Conan 用 package_id 检查,如果 settings 变了(比如编译器从 gcc 11 升到 gcc 13),package_id 就不同,Conan 会认为这是两个不兼容的二进制,要求重新编译。

八、规范决定生态

回头看,包规范不是"命名好看不好看"的问题,它决定了整个生态的可消费性:

  • 门牌号格式让公共仓和私有仓引用方式统一,消费者不用切换。
  • 源码归档让每个包可追溯到源码。
  • 双接口让一份代码服务两类消费者(C 裸机 + C++ 现代)。
  • 前缀防撞让自家包和公共包天然隔离。
  • recipe revision 让同名同版本的包内容变更可精确区分。
  • package_id 让二进制兼容性由 settings 组合客观判定,不靠人嘴。

这些规范一旦定下来、写进模板仓,后面所有从模板创建的仓库都自动遵守——不需要每个开发者去记"该用什么格式"。规范不是靠人记的,是靠模板和门禁强制的

下一篇,我们讲版本怎么自动管:semantic-release + Conventional Commits——提交消息怎么自动推出版本号、生成 CHANGELOG,不用人拍版本。


参考链接:

[1] Conan 2:exports_sources 与源码归档 https://docs.conan.io/2/reference/conanfile/attributes.html

[2] Conan 2:cpp_info 组件与多接口 https://docs.conan.io/2/reference/conanfile/methods.html

[3] Conan 2:recipe revision 与包可追溯性 https://docs.conan.io/2/reference/commands/list.html

[4] Conan 2:package_id 与 settings https://docs.conan.io/2/reference/config_files/settings.yml.html

[5] ConanCenter 包命名规范(name/version 参考) https://github.com/conan-io/conan-center-index

[6] CMake:target_link_libraries 与组件链接 https://cmake.org/cmake/help/latest/command/target_link_libraries.html

[7] C/C++ 双接口设计(extern "C" 与 ABI 兼容) https://en.cppreference.com/w/cpp/language/language_linkage

[8] Conan 2:export_sources 方法 https://docs.conan.io/2/reference/conanfile/methods.html


评论