Dumb Ways for an Open Source Project to DieDumb Ways for an Open Source Project to Die
Dumb Ways for an Open Source Project to Die
Andrew Nesbitt
《周末在伯尼家》揭示了这样一个事实:大量被频繁依赖的开源软件包已经死亡,而项目走向衰亡的方式也多种多样。
维护者离开了。最常见的情况是“幽灵维护者”——最后的人类提交记录已是数年前,问题堆积如山却无人回应,仓库未被归档,因此不会出现在任何标记为废弃的过滤器中。通常,维护者只是转向了其他事务,而项目对他们来说并不重要到需要正式交接或关闭,尽管这种沉默可能涵盖从维护者离职到去世的所有情况,而注册表和仓库都无法表示这一点。从外部看,这就像一场漫长的假期,直到未解决的问题堆积到足以让沉默变得明确为止,而伯尼名单上大多数“死亡”的项目都属于这种情况。
企业孤儿项目。公司曾组建团队开发和开源该项目,随后因业务转型或裁员导致团队解散,却无人更新 README 文件。GitHub 组织账户依然保留着公司标志,而拥有管理员权限的最后一批人也已离开,因此公司内部几乎没人知道该项目仍属于他们。谷歌的各种“墓地”就是典型案例,但每家公司达到一定规模后都会出现几个类似项目,尤其是那些作为基础设施而非产品存在的项目,往往连弃用通知都不会收到。
论文孤儿项目。由研究生为其硕士课题或博士章节开发,毕业后便不再维护。实验室名义上拥有该仓库,但无人具备继续开发的背景知识,而学术界也不会鼓励他们尝试:维护他人软件无法获得引用,在评审中也毫无价值,远不如发表新成果来得重要。科研软件中充斥着这类项目,许多项目所对应的论文在被引用多年后,其代码早已无法运行。
资金悬崖。项目曾依靠资助或短期赞助运作,通常由基金会或公共软件基金提供,资金按计划到期终止。维护者回归本职工作,而原本可全职投入的项目如今只能在下班后和周末进行,对于该规模而言基本等同于零投入。资助方标志通常会长期保留在 README 中,即使资助早已结束,这使得此类项目容易被误认为是仍在健康运营的赞助项目。
雇佣带走。维护者被公司雇佣,或因雇佣合同或新工作量的原因导致项目停止。有时这是竞争对手有意为之,但更常见的是非恶意情况:苹果是典型例子,其雇主普遍不允许员工从事外部开源工作,因此维护者入职即意味着其原有项目默认进入静默状态。在开始前就完成交接是最合理的做法,但几乎没人能及时做到。
继承僵局。原始维护者失联,有人愿意接手,但注册表的发布权限绑定在一个无人能访问的账户上,且 GitHub 仓库没有其他管理员。同时,注册表的废弃流程需要原始维护者的同意,或需经历数月争议,而无人有明确立场发起此类争端。PEP 541 流程和 npm 的争议政策正是为此类情况设计,但通常耗时比分叉并重命名更长。
维护者仍在
The maintainer is still there
倦怠停滞期。从任何指标来看都仍保持活跃。拼写错误修复和依赖项更新会被合并,偶尔在 issue 上还会收到“谢谢,稍后会查看”的回复,但任何需要真正设计决策或调试会话的内容都会被无限期搁置,因为维护者已经很久没有为项目投入精力了。回应虽时有发生,足以让任何人建议 fork 时都能看到近期活动,却始终不足以推动实际发布,这种状态可能持续数年,既未彻底沉寂到让人有理由接手,也未真正消亡。
仁慈僵尸。贡献图一片翠绿,每一条提交都是机器人所为:Dependabot 自动升级、自动合并规则、可能还有由这些升级触发的自动化发布,如今甚至出现了定时编码代理——它们能无限期维持项目运转,而无需人类阅读任何内容。任何基于最近活跃度的健康评分都会将其评为良好,而这正是基于最近活跃度评分体系的根本问题所在。
监护权争夺战。两三位共同维护者之间出现分歧,每人都有足够权限阻止对方,却又不足以独自推进项目,于是项目在他们之间陷入僵局。最终可能演变为分叉,也可能有一方退出,但更多情况是僵持不下,issue 追踪器不断涌入用户询问进展的消息,却只得到相互矛盾的答复。
知识断层。代码仍能运行,测试也能通过,但当初理解其设计缘由的人已离开,剩下的人谁也不敢碰核心部分。项目实际上已进入只读模式:边缘的小补丁尚可接受,结构性改动则因风险过高而无人敢试。这种情况在数值计算和解析类代码中尤为常见——那些十年前根据某篇论文实现的关键算法从未被文档化,仅靠一人掌握。
有毒的门控机制。维护者就在那儿,态度却充满敌意。新贡献者经历一次令人沮丧的评审后便不再回来,公交因子(bus factor)仍为一人,因为没人受得了共享这个仓库。它在所有以提交数和关闭 issue 数为指标的“健康度”评估中都显得正常;而当最后那位维护者终于停止维护时,项目已成幽灵项目,无人可接棒——因为多年前就把所有潜在继任者赶跑了的缘故。
破坏与劫持
被劫持的维护者。原本应属合法维护者的提交或发布权限落入了敌对之手。xz 事件是复杂版本:历时两年的社会工程攻击,针对一位长期超负荷工作的单人维护者,成功安插 co-maintainer 并最终植入后门发布恶意版本。2018 年的 event-stream 则是简化版:原作者将包交给一位礼貌请求的志愿者,后者随后在其下游依赖中加入了窃取钱包的脚本。两种情况下,项目在被劫持期间看起来都比之前更“健康”——因为正是这位新维护者在投入工作。
抗议软件(Protestware)。正当维护者故意破坏自己的包。2022 年,colors 和 faker 被作者蓄意 sabotage;同年,node-ipc 向俄罗斯和白俄罗斯 IP 范围发送针对性载荷;2016 年,left-pad 则在 npm 纠纷中被完全下架。动机各异,但对下游的影响一致:注册表中代码不再是你以为会运行的样子,且通常毫无预警。
发布流水线崩溃
维护中但未发布。开发正在进行,修复已提交到 git,但无人能发布新版本。唯一拥有发布权限的账户丢失了2FA设备,或所属公司已不存在。下游项目卡在最后发布的版本,而他们需要的修复就在仓库里可见的提交中,却无法从注册表中安装——这正是原始 Bernie 帖子重点讨论的情况。
无法发布的默认分支。默认分支与最后一个标签的距离已足够远,以至于基于它发布将造成所有人的破坏性变更,没人愿意承担这个责任,因此无人打标签。新贡献者向主分支提交补丁,而用户运行的是多年前发布的版本,差距逐渐扩大,最终发布新版本竟成了一个无人负责的项目。
构建考古。已发布的构件可以工作,但没人能复现其构建过程。构建依赖的CI服务已消失,或基础镜像已被删除,或某个工具版本仅存在于某位维护者的旧笔记本上。要发布新版本,必须先重建整个构建环境,而关于其中内容的知识也随当初搭建它的人一同消失了。
影子维护。真正的开发在公司内部的私有单体仓库中进行,公共仓库定期接收一个“同步”之类的压缩代码快照。针对公共仓库提交的问题和拉取请求毫无回应,因为没人真正在那里工作。开源项目已成为闭源项目的发布渠道,从外部看几乎无法区分,除了偶尔有同步提交的日子。
被困的重大版本。该项目当前为v4且仍在积极维护,但生态中大部分仍在使用v1,因为v2是一次他们从未完成迁移的重写,而v1多年未获维护关注。“项目是否死亡”完全取决于你问的是哪个重大版本,而安装量最大的版本往往不是正在被维护的版本。
注册表孤儿包。该包可从注册表解析,但其元数据中的源码仓库URL已失效(被删除、设为私有、未更新注册表信息,或托管服务已关闭)。无法提交问题或分叉,也无法验证tarball是否与任何历史源码匹配。约1.7%的npm包和4%的Packagist包指向不存在的仓库,其中相当一部分仍在被持续安装。
不可抗力
制裁困局。维护者有能力、有意愿,却因注册商所在司法管辖区被封锁,或因出口管制导致账户冻结而无法推送。过去几年已有少数npm和GitHub账户因此被封禁,对下游而言这与幽灵维护者无异,只是维护者常会在其他平台大声说明情况。
下架受害者。因DMCA投诉或商标纠纷被从注册表或主机移除。youtube-dl在2020年下架后曾短暂回归;许多小型项目则再未恢复,无论投诉是否成立,只要包仍能解析,就说明它依然存在。
世界已向前发展
平台束缚。被绑定在一个已停止维护的运行时上:仅支持 Python 2,需要某个已从 CI 镜像中移除的 Node 版本,依赖一个已被删除的编译器扩展。将其向前移植所需的工作量超过了任何剩余维护者愿意承担的范围,因此它停留在原地,而其所需的平台正从你希望运行它的各个地方逐渐消失。
传递性死亡。项目本身没问题,维护者也存在且有意愿,但在其依赖树两到三层之下的某个地方,某个东西已经通过本列表中的某条路径死亡,且无法在不重写的情况下替换。该项目继承了这种死亡,而它自己的仓库中没有任何变化,这是递归情况:这里的每个条目同时也是杀死依赖于你的东西的方式。
API 骗局。该项目封装了某个外部内容,其所有者已撤回该内容。在服务层,这是一个用于已关闭或重新定价至无法企及的 API 的客户端库,Twitter 2023 年的变更以及 Reddit 的举措一举摧毁了一代此类项目。在平台层,这是浏览器废弃某个接口或操作系统封锁某种能力,涵盖了所有基于 NPAPI、Flash 或 Chrome 应用构建的内容。无论哪种情况,维护者都无法从他们这边采取任何措施。
已被取代。该项目所做的内容已不再需要,要么是因为它所实现的规范已被替代,要么是因为语言本身现在原生支持相同功能。例如 Object.assign 之后的 object-assign,ES2015 之后的 lodash 单函数包,以及各种 promise 和 fetch polyfill,再比如在协议层面为无人再输出的格式所写的各类库。维护者合理地停止了工作,而几十万个 lockfile 仍会安装它,因为移除一个仍能解析的依赖并不是任何人的优先事项。
项目分裂
分叉悬停。由于分歧或维护者离开,项目分裂为两个或多个分叉,其中没有一个明显胜出。下游项目冻结在分裂前的最后一个版本,而不是押注于可能失败的分叉,因此原始项目保持其安装计数,而所有开发工作都在其他地方以其他名称进行。io.js 和 Node 最终合并,libav 最终并入 FFmpeg,许多较小的分裂则从未解决。
许可证骗局余波。该项目重新授权为非开源许可,而遵循旧许可证的社区分叉存在,但采纳并未集中在其上。Terraform/OpenTofu 和 Redis/Valkey 都处于这条路径上的某个位置,Elasticsearch 则更深入一些。大多数 lockfile 仍指向原始项目的最后一个开源许可版本,现在这个版本已成为一个无人维护的固定点。
开源核心空心化。有趣的发展转移到了商业版本,而开源仓库则被保留作为免费层级。它仍在发布,主要是版本号提升以及所有不区分付费产品的内容,最初被采纳的项目实际上已变成一个更小、不同的项目,只是从未更名。
本文标题所纪念的墨尔本地铁安全宣传活动以“注意火车周围安全”结束,这比我能想到的任何建议都更具可操作性。无论上述哪种情况适用,软件包仍会解析为相同内容,只要没人仔细检查,你的 lockfile 就会继续带着太阳镜把它带到派对上转来转去。
需要完整排版与评论请前往来源站点阅读。