你的 Fork 将超出你的耐心。系统思考事后分析。
Source: Dev.to
每个内部分叉都始于一句话
“我们只需要修补这个文件。”
六个月后,你要维护四个平行的仓库,害怕每一次上游发布,并且花更多时间维持补丁的存活,而不是构建它们原本应该启用的东西。
我知道,因为我正是这么做的。我分叉了四个上游工具,以将 973 ROS 包 移植到一个不受支持的操作系统。它成功了—— 61 % 的包编译通过,turtlesim 运行成功,我的演示也取得了成功。随后,这个分叉把我吞噬了。
这不是战争故事。这是对为何分叉上游工具会产生一种结构性陷阱的 系统‑动力学诊断,无论多么自律都无法逃脱。
设置
我正在将 ROS 2 Jazzy(机器人操作系统)移植到 openEuler 24.03 LTS——一个 ROS 官方并不支持的 Linux 发行版。ROS 构建工具链(bloom、rosdep、rospkg、rosdistro)硬编码了其支持的平台列表;openEuler 并不在其中。
选项
| 选项 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 向上游贡献 | 提交 PR 为官方工具添加 openEuler 支持。 | 可持续、社区所有。 | 进度慢,取决于维护者的意愿。 |
| 全部分叉 | 克隆这四个仓库,自己添加 openEuler 支持,从源码构建。 | 快速、独立。 | 维护负担全部落在我身上。 |
我选择了选项 2。 当然我会这么做。我还有演示要交付。
失败的修复 (R1)
下面是我的 fork 的简化系统示意图:
(Problem) (Relief)
TOOLCHAIN DOESN'T ----------> TOOLCHAIN WORKS
RECOGNIZE openEuler |
^ |
| (Short Term) |
| BALANCING |
| LOOP |
| v
+------ ---------+
| (Intervention) |
| |
| |
| (Long Term Side‑Effect) |
| REINFORCING LOOP (R1) |
| "Fixes that Fail" |
| |
| v
+-----------+ +-----------------+
| FORK GETS | (frozen in time)
^ |
| |
| REINFORCING v
| LOOP (R2) METADATA ROTS
| "Data Decay" (Wrong versions,
| missing packages)
| |
| v
| BUILD FAILURES
| INCREASE
| |
+------------------------------+
(Need more manual
patching of YAML)
每天官方的 rosdistro 都会收到更新,而我的 fork 越来越落后。每落后一天,就会有更多构建因与 openEuler 兼容性无关的原因而失败——它们失败是因为我的元数据已经陈旧。
我编写了一个脚本 (auto_generate_openeuler_yaml.py),它读取官方的 YAML 并尝试通过 dnf list 将每个依赖映射到 openEuler 包。遗憾的是:
- 只能在真实的
openEuler机器上运行。 - 不能在 CI 中运行。
- 不能离线运行。
因此这成为了一个需要手动记住执行的过程,每次我忘记时,数据就会进一步腐烂。
实际中 R1 + R2 的表现
以下是我系统中的真实数据(运行于 EulerMaker):
| 架构 | 成功 | 依赖缺口 | 失败 | 中断 | 总计 |
|---|---|---|---|---|---|
| aarch64 | 606 | 215 | 152 | — | 973 |
| x86_64 | 597 | 214 | 151 | 11 | 973 |
- 61 % 的成功率 –
turtlesim能运行。这是好消息。 - 坏消息:这 214 个依赖缺口和 151 次构建失败是 由两个相互强化的循环累积的 问题库存。每一个缺口都代表我的分叉元数据出错或我的分叉工具链做了真实工具链不会做的事。每当上游有变动时,一部分原本的 597 次成功会变成新的失败,因为我的分叉没有跟上。
系统并没有“崩溃”——它在 漂移。漂移是由分叉引入的结构性陷阱导致的。唯一可持续的解决办法是通过向上游贡献来闭环,而不是不断为不断分叉的代码打补丁。
我错过的杠杆点
在系统思考中,有一个概念叫做 杠杆点——结构上的微小变化能够产生行为上的巨大变化。Meadows 将 系统的规则 排列为最高杠杆点之一。
我的分支遵循了一个隐含的规则:
“我们维护自己的工具链版本。”
这条规则把所有与上游的交互都置于对抗关系之中。上游的更新不再是改进,而是威胁。
高杠杆的替代方案是将规则改为:
“我们让自己的补丁被上游接受。”
在这条规则下,每一次上游更新都会成为包含我们平台支持的改进。原本摧毁我系统的同一股力量(上游动能)将转而支撑它。
我知道自己为什么没有这么做。向上游贡献既慢又充满政治斗争,且结果不确定。分叉则快速、可控且确定。但“短期的快速确定”在长期会变成“昂贵且脆弱”。这正是 Fixes that Fail(失败的修复)原型的全部意义——症状式的解决方案在当下总是更具吸引力。
我真正学到的
-
A fork is a liability, not an asset.
一旦你进行分叉,就会产生一个维护义务,并且随着每一次上游提交而不断增长。如果你不能在有限的时间内将更改推送回上游,你就在累积会复利的结构性债务。 -
Data forks are worse than code forks.
分叉代码本身已经很糟糕。分叉数据(例如我的rosdistroYAML 文件)更糟,因为数据会悄然陈旧。代码会大声报错——函数签名的更改会导致编译错误。数据则静默腐烂——某个包的版本不对,几周后会出现神秘的运行时失败。 -
The brute‑force approach is valuable — as a probe.
v1 并不是一次失败。它是一次有意的暴力调查,生成了一张情报地图:- 识别出 973 个软件包
- 哪些能够工作
- 准确定位缺口所在
失败在于把探针当作了生产系统。探针是一次性的;生产系统需要结构完整性。
-
Know your band‑aids.
我的系统里有 virtualenv 绕过、RHEL‑clone 注册以及冻结的 YAML 快照。我清楚每一样都是临时补丁。大多数团队并不跟踪自己的补丁,它们会悄然累积,直到有人问:“为什么我们的构建要花 45 分钟,而且有 30 % 的时间会失败?”而没人能给出答案。
后续
v1 让我看到了当暴力‑强制管道触及结构极限时的样子。我在 v1 事后报告仓库 中记录了完整的系统动力学,包括陷阱架构。
v2 的设计目标是打破这个循环:在构建之前进行验证,而不是之后。它不再把 973 个软件包全部喂进管道并让 40 % 失败,而是先探测操作系统环境,先发现缺口再消耗构建资源,并在已验证的依赖图上工作。详情请参见 v2 验证引擎仓库。
这个结构性教训远超 ROS 移植的范围:
- 内部 fork 的开源库: 你正在使用 R1。把补丁上游,或者为维护成本做好规划。
- 上游会覆盖的配置文件补丁: 你正在使用 R2。自动化合并,或者接受数据腐烂。
- 在构建脚本中使用
--skip-broken、--force或|| true: 你在掩盖症状。每个标志都是创可贴——数一数它们。
每个 fork 都始于“就这一个补丁”。
每个成瘾都始于“就这一次”。
系统不在乎你的意图;它在乎它的结构。
带有系统动力学图的 v1 事后报告: the_brute_force_probe
v2 验证引擎: the_adaptive_verification_engine