⚠️ 法律与用途声明:本文讨论基于 GitHub Actions、Conan 2、Docker 的多平台构建矩阵设计,属于经验分享与架构讨论。所有企业名、项目名、脚本名、具体参数与数值均已脱敏。读者须遵守所在地区法律法规。

一、多目标构建的"if/else 地狱"
嵌入式 CI 和后端 CI 最大的不同,是目标平台的爆炸。同一个算法库,往往要同时构建到:
- Linux x86(开发机验证)
- Linux ARM(Cortex-A 类开发板)
- 裸机 MCU(Cortex-M 类微控制器)
而每个目标又有自己的维度:操作系统、CPU 架构、工具链版本、要不要开 benchmark、recipe 在哪个子目录……组合起来,构建需求是一张多维矩阵。
最直觉的做法,是在 CI workflow 里写一堆 if:如果目标是 ARM 就用 ARM 工具链,如果是裸机就关掉测试,如果要 benchmark 就加这些参数。一开始还能忍,目标一多就成灾难——workflow 文件膨胀成几百行 if/else,每加一个目标或换一个工具链版本,都要去改这个脆弱的条件分支。更糟的是,业务仓库要"知道"太多平台内部细节,平台一改脚本,所有业务仓库的 workflow 都得跟着改。
根因在于:构建需求和构建实现搅在了同一个 workflow 里。业务不该关心平台用 Docker 还是 Podman,平台也不该硬编码某个业务仓库的目标列表。
解法是把这两件事拆开:业务用一份声明式配置说清构建意图,平台负责把它变成具体的执行动作。

二、声明式构建矩阵:把"构建需求"写成一份配置
核心是每个算法仓库里放一份声明式构建矩阵配置(一个 YAML 文件)。它只回答一个问题:这个包,要在哪些目标上、用什么工具链、做哪些事。它完全不回答"怎么构建"——那是平台的事。
一份脱敏后的配置示意(只看结构,不看具体值):
# 声明式构建矩阵(结构示意,值已脱敏)
package_ref: <包名>/<版本>
defaults:
mode: default # 默认展开模式
toolchain_version: <版本> # 默认工具链
create_extra_args: --build=missing
targets:
- id: linux-arm # 目标唯一标识
default: true # 是否默认目标
enabled: true # 是否启用
build_kind: linux # 构建大类:linux / baremetal
os: Linux
arch: <ARM 架构>
toolchain_versions:
- <版本>
benchmark: # 该目标是否开 benchmark
enabled: true
- id: baremetal-mcu
default: false
build_kind: baremetal
arch: <MCU 核心>
注意它的设计哲学:这份文件只描述意图,不含任何平台脚本路径或 CI 内部细节。一个算法工程师能读懂它、能改它,而不需要懂 CI 是怎么跑的。这是"声明式优于命令式"在构建配置层的落地。
三、矩阵的 schema 设计:defaults + targets 两层
这份配置的 schema 只有两层,但这两层解决了一个很实际的工程问题:默认值与个体覆盖。
defaults 层:所有目标共享的默认值(默认展开模式、默认工具链、默认构建参数)。绝大多数目标用同一套工具链和参数,写在 defaults 里,避免每个目标重复写。
targets 层:每个目标自己的属性(id、是否默认、构建大类、架构、工具链版本、benchmark 子配置)。某个目标如果和默认不一样,就在自己这里覆盖。

这个"默认 + 覆盖"的结构,本质是配置的继承与多态——defaults 是基类,每个 target 是一个继承基类、可覆盖个别字段的子类。它的工程价值是消除重复:加一个新目标,如果它和默认一致,只写 id 就行;只有特殊需求才展开写。这让一份十几个目标的矩阵配置,依然能保持清爽可读。
每个 target 还有一个 benchmark 子配置块——它声明这个目标要不要上板测性能、测的时候 CPU/FPU 是什么。把 benchmark 声明嵌进构建矩阵,意味着构建和性能验证共用同一份目标清单,不会出现构建了某个目标、却忘了给它跑 benchmark 的遗漏。
四、矩阵展开器:把声明变成 CI job
配置写好了,谁来把它变成 GitHub Actions 能理解的 matrix?答案是平台侧的一个矩阵展开器——一个小工具,读入这份 YAML,按指定模式输出 CI 要跑的 job 列表。
展开器支持三种模式,对应三种使用场景:

- default 模式:只跑标记为默认的目标。日常开发 push 用这个——快速验证主线目标,不浪费时间在全部目标上。
- all 模式:跑所有启用的目标。发版前全量回归用这个——确保每个支持的目标都能构建。
- selected 模式:只跑手动指定的几个目标。调试某个目标用这个——精准、省时间。
这三种模式的精髓在于:同一份配置,按场景投影出不同的执行范围。开发者日常 push 只验证默认目标(快),发版前全量验证(全),调试时只跑关心的目标(准)。一份声明,三种用法,避免日常 CI 跑全套太慢、跑太少又不放心的两难。
展开器输出的是结构化数据(比如 JSON),CI workflow 读它来生成 matrix。业务仓库永远不写 workflow 里的 matrix,matrix 由声明加展开器自动生成。这是单一数据源——构建需求的唯一真源是那份 YAML 配置,workflow 只是它的展开结果。
五、触发契约:什么信号触发什么动作
声明式矩阵解决了"构建什么",但还有个相邻问题:"什么时候构建"。这里我们也用一份触发契约来约束,而不是让开发者随手触发。

典型的三类触发映射:
- commit 标记 → 自动构建:只有 commit message 里带特定标记(比如一个 emoji 或关键词),push 才触发构建 workflow。日常提交不触发,避免每次 push 都跑一遍 CI。
- 手动触发 → 选模式:在 GitHub Actions 页面手动触发,可选 default / all / selected 模式。发版前全量、调试时选目标,都走这个入口。
- 晋升触发 → 单独动作:把一次成功构建正式发布是另一个 workflow,必须人工触发、指定具体哪次构建。它和构建 workflow 严格分开。
这份触发契约的价值是把信号与动作的映射显式化。它避免了两种常见混乱:一是任何 push 都触发全量构建(CI 资源浪费、反馈慢);二是构建和发布搅在一个 workflow 里(容易误发布)。声明式的精神在这里延伸到了触发逻辑——信号和动作的映射本身,也是一种需要被声明、被看见的契约。
六、声明式 vs 命令式:一个工程权衡
把构建矩阵做成声明式,不是免费午餐,它有代价。值得把权衡讲清楚:

声明式的好处:
- 业务仓库只写意图,不写实现,平台脚本升级时业务无感。
- 构建需求集中在一处(单一数据源),不会散落在 workflow 的 if/else 里。
- 同一份配置能被多种消费者使用(CI 展开、文档生成、目标清单展示)。
- 新目标接入成本低——加一段 target 声明即可。
声明式的代价:
- 要设计一套 schema,schema 的好坏决定一切(字段太少不够用、太多变复杂)。
- 展开器是额外的代码,要维护、要测试。
- 极端特殊的构建需求,声明式可能表达不了,需要逃生舱(比如允许 target 带自定义参数透传)。
工程判断是:当目标数量多、且会持续增长时,声明式的收益远大于代价。如果只有一两个固定目标,直接在 workflow 里写反而更简单。这套矩阵方案适合的是目标平台多、新目标会不断加入、需要团队统一规范的场景——而这正是嵌入式 AI 算法库的典型处境。
七、这份矩阵是平台与业务的关键契约
回到整个系列的架构主线。这份声明式构建矩阵,是平台和业务仓库之间最重要的契约接口之一(NO.A0 讲两层架构时提过)。
它的契约地位体现在:
- 业务侧:算法仓库只维护这份矩阵配置,声明自己要在哪些目标上构建。它不感知平台用 Docker 三层模型(NO.A2)、hardlink 快照(NO.A3)还是别的什么执行机制。
- 平台侧:CI 读这份矩阵,把每个 target 展开成一个 job,job 内部走执行层的三层模型加 hardlink 缓存。平台不关心这个算法是什么、依赖了什么。
矩阵配置稳定,则平台和业务可以各自演进。平台升级执行层、优化缓存,业务仓库无感;业务加新目标、换工具链,平台 CI 模板不用改。这是高内聚低耦合在构建配置上的精准落地——耦合点收敛到一份声明式文件,而不是散落在几十个仓库的 workflow 里。
八、架构哲学
这一篇落地的核心哲学:
声明式优于命令式。业务声明要构建什么,平台负责怎么构建。意图和实现分离,是配置可维护性的根基。
单一数据源。构建需求的唯一真源是那份矩阵 YAML。workflow 的 matrix 是它的展开结果,文档里的目标清单也可以从它生成。一处声明,多处消费,消除配置漂移。
默认加覆盖的配置继承。defaults 提供共性、targets 提供个性,消除重复。这是把面向对象的继承思想用到了配置设计上。
触发契约显式化。信号和动作的映射本身也是一份需要被声明、被看见的契约,避免触发逻辑的随意性。
下一篇,我们从调度层进入制品层:私有 Conan Center 落地——怎么用 Artifactory CE 搭一个企业私有的 C/C++ 包仓库,让团队自己的算法包有一个可追溯的家。

参考链接:
[1] GitHub Actions 矩阵策略(matrix) https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-your-workflow-does/run-variations-of-tasks-using-a-matrix
[2] GitHub Actions workflow_dispatch(手动触发) https://docs.github.com/en/actions/how-tos/manage-workflow-runs/dispatch-a-workflow
[3] Conan 2 profiles 与 settings(多目标构建基础) https://docs.conan.io/2/reference/config_files/profiles.html
[4] YAML 官方文档(声明式配置格式) https://yaml.org/
[5] GitHub Actions workflow_call(可复用 workflow) https://docs.github.com/en/actions/how-tos/sharing-workflows-reusing-workflows
[6] JFrog Artifactory CE for C/C++(私有包仓库) https://jfrog.com/open-source/