开源软件支撑着现代互联网的运转,但这些项目并非总能健康存活。研究者 Andrew Nesbitt 日前发表文章,指出大量被高度依赖的开源软件包,其实已经“死了”,而项目走向死亡的方式有很多种。

维护者离开了
幽灵维护者(Ghost maintainer)
最简单、也最常见的一种情况:最后一次人工提交停留在几年前,issue 不断堆积却无人回应,仓库也没被归档,所以各种健康度筛选工具根本不会标记它。
通常只是维护者去忙别的事了,项目对他来说没重要到值得正式移交或关闭。但同样的沉默,也可能意味着更极端的情况——包括维护者已经去世,而无论 registry 还是 repo 都没有机制表达这一点。
从外部看,它和“人在休长假”毫无区别,直到未处理 issue 多到无法再自欺欺人。npm 上那批位列 Bernie’s dead list 顶部的工具,大多属于这种。
企业弃儿(Corporate orphan)
一个公司开发并开源了项目,也曾有团队维护它。但后来公司转型、裁员,团队没了,而 README 却没人更新。
GitHub 组织还挂着公司 logo,但当年有 admin 权限的人已经离职,于是公司内部甚至没人知道这个项目还是自己的。
Google 的各种“墓地”最出名,但任何稍微大点的公司都有类似遗迹。尤其是那些基础设施类项目,很多甚至连 deprecated 公告都没有。
论文遗孤(Thesis orphan)
项目是研究生或博士生为了论文做的,毕业后人走了。
实验室名义上拥有 repo,但没人理解上下文,也没人有动力继续维护——学术体系不会因为“维护别人软件”给你表彰,更不会在评审里加分。相比之下,发一篇新 paper 显然更重要。
科研软件里这种情况极其普遍:论文还在被引用,代码却早就无法编译。
资金断裂(Funding cliff)
项目依靠赠款或固定期限赞助运行,通常来自基金会或公共软件基金之一,一旦资金按计划耗尽,维护者就回到维持生计的其他工作上,而一个原本按全职规模成长起来的项目,如今只能靠“晚上和周末”维护。对于这种规模来说,约等于没人维护。
最迷惑的一点在于:赞助方 logo 在资金停止很久后往往还挂在 README 上,所以它看起来仍像一个“健康、有资助”的项目。
被大厂招安(Hired away)
维护者被一家公司聘用,而无论是劳动合同还是新的工作量,都会导致项目停止。有时是竞争对手故意“消灭项目”,但更常见的情况是完全没有恶意:苹果公司就是一个典型的例子,他们不允许大多数员工参与外部开源项目,因此维护者加入苹果意味着他们的项目自然而然地就停止了。理论上,在入职前完成项目交接是最合理的做法。但现实里,几乎没人来得及做。
继承僵局(Succession deadlock)
原维护者失联了,也有人愿意接手,但:
PEP 541 流程和 npm 的争议处理政策正是为了解决这种情况而设立的,但两者通常都比 fork 和重命名耗时更长。
维护者还在
倦怠平台期(Burnout plateau)
从各种指标看,项目仍然“活着”。
拼写修复、依赖升级还能 merge,issue 偶尔也会回复一句“谢谢,我会看看”。但真正需要设计决策或深度 debug 的问题,会无限期悬而未决——因为维护者早已没有精力。
项目会维持一种诡异状态:
-
活跃度足够让 fork 提议被反驳;
-
但又不足以真正发布新东西。
这种状态能持续很多年。
善意“僵尸”(Benevolent zombie)
贡献图一片绿色,但 commit 全是 bot。
Dependabot 自动升级依赖、auto-merge 自动合并、自动 release,再加上如今的 coding agent,项目甚至可以长期“无人值守运行”。
所有基于“最近活跃度”的健康评分都会认为它很健康。
而这恰恰是 recency-based 指标最大的问题。
所有权大战(Custody battle)
两位或多位共同维护者意见不合,各自拥有足够的权限来阻止对方,但又不足以独立推进项目,导致项目陷入僵局。
有些最后会 fork,有些会有人退出,但更多情况是:
issue 区越来越满,用户不断问“到底发生了什么”,而双方给出两套互相矛盾的答案。
经验知识消失(Tribal knowledge gone)
代码能跑,测试也通过。
但真正理解“为什么这样设计”的那个人已经离开了。剩下的人没有任何人敢动核心部分。
于是项目实际上进入“只读模式”:
-
边缘小 patch 可以接受;
-
结构性修改风险太大没人敢碰。
这种情况在数值计算和解析代码中尤为常见,因为其中最难的部分往往是某个人十年前根据一篇论文实现的算法,而他本人却从未写过相关文档。
有毒守门人(Toxic gatekeeping)
维护者人还在,但抱着极大的敌意。
新人第一次 PR 就会遭遇毁灭性 code review,然后就再也不来了,而 “巴士系数” 一直保持在 1,因为其他人都无法忍受和他共用 repo。
从所有统计 commit 次数和已关闭 issue 的指标来看,代码库看起来都很健康,但当维护者最终停止维护时,就会出现 “幽灵维护者” 的情况,而且没有接班人,因为所有可能接手的人几年前就被赶走了。
破坏与劫持
被劫持的维护者(Captured maintainer)
有人恶意获得 commit 或发布权限。
xz 就是一个精心策划的例子:针对一位疲惫的独立维护者发起了一场长达两年的社会工程攻击,最终让攻击者成为 co-maintainer,并植入后门。
2018 年的 event-stream 更简单:原作者把软件包交给一个“热心志愿者”,这位志愿者礼貌地请求后,在下游依赖项中添加了一个窃取钱包的程序。
有趣的是:项目在被“劫持”期间,往往比以前“更活跃、更健康”。
因为真正干活的人,正是攻击者。
利用软件进行抗议(Protestware)
合法维护者故意破坏自己的包。
-
colors 和 faker 在 2022 年因作者抗议而被自己破坏;
-
node-ipc 同年对俄罗斯和白俄罗斯 IP 发布恶意载荷;
-
left-pad 则在 2016 年 npm 争议中被直接下架。
动机各不相同,但结果一致:
registry 里的代码已经不再是用户以为自己在运行的东西,而且通常毫无预警。
发布流水线坏了
有维护,但发不了版(Maintained-not-shipping)
开发仍在继续,git 里也有修复。
但没人能发布新版本:
-
唯一拥有发布权限的账号丢了;
-
双因素认证设备丢失;
-
账号所属公司已经倒闭。
下游只能继续使用旧版本,而真正修复 bug 的 commit 明明就在 repo 里,却无法从 registry 安装。
无法发布的 main 分支(Unreleasable main)
默认分支与上一个 release 已经偏离太远。
现在发版会导致增加大量破坏性变更,而没人愿意背锅,于是永远没人打 tag。
新贡献者继续往 main 提 patch,而用户仍跑着几年前的版本。差距不断扩大,直到“发一个新版”本身变成了一个永远没人启动的大项目。
构建考古学(Build archaeology)
已发布的制品库(artifact)还能用,但没人能重新构建。
因为进行构建需要的依赖如下:
-
一个已经关闭的 CI 服务;
-
一个被删除的基础镜像;
-
某个维护者电脑上的特殊工具版本。
现在想发新版本,第一步是“重建古代构建环境”。而其中的所有内容都已遗失,只剩下搭建环境的人。
影子维护(Shadow-maintained)
真正的开发工作在公司内部的私有单体仓库中进行,公开仓库只是定期收到一次压缩后的代码 dump,提交信息类似于 “sync”。
提交到公开仓库的 issue 和 PR 都石沉大海,因为那里根本没人工作,开源项目已经沦为闭源项目的发布渠道。从外部来看,除了偶尔 sync 一次,它和幽灵维护者没什么区别。
停滞不前的大版本(Stranded major)
项目已经发展到 v4,而且维护活跃。
但整个生态还停留在 v1,因为 v2 曾是一次大重写,大家根本没迁移过去。
于是:
-
v4 在维护;
-
安装量最大的 v1 却多年没人管。
“这个项目死了吗”完全取决于你问的是哪个大版本,而安装量最高的版本通常并非最受关注的版本。
Registry 孤儿(Registry orphan)
包还能从 registry 下载,但 metadata 里的源码地址已经 404:
-
repo 被删了;
-
改私有了;
-
搬迁后没更新;
-
托管平台倒闭了。
你无法提 issue,也无法 fork,甚至无法验证 tarball 是否与任何曾经在源代码控制中存在的内容匹配。
约 1.7% 的 npm 包、4% 的 Packagist 包都指向不存在的 repo,而其中很多仍在被安装。
不可抗力
制裁困境(Sanctions-stranded)
维护者有能力、也愿意更新,但推不上去。
因为:
-
registry 封锁了其所在地区;
-
账号因出口管制被冻结。
过去几年,npm 和 GitHub 都出现过这种案例。对下游来说,它和幽灵维护者完全一样,只是维护者会在别的平台拼命解释发生了什么。
下架受害者(Takedown casualty)
项目因收到 DMCA 版权投诉或商标纠纷而被从 registry 或托管平台移除。
youtube-dl 在 2020 年下架后又恢复了,但大量小项目再也没回来。
而“指控是否合理”,与“包还能不能下载”没有任何关系。
世界已经变了
平台遗弃(Platform-stranded)
项目绑定在一个已经停止维护的运行时上:
-
只支持 Python 2;
-
依赖的 Node 版本已从 CI 镜像中移除
-
使用被移除的编译器扩展。
向前移植的工作量太大,而剩下的人没人愿意做。
于是它和它依赖的平台一起慢慢消失。
传递性死亡(Transitive death)
项目本身没问题,维护者也还在。
但它依赖树下游两三层的某个依赖死了,而且无法替换。
于是项目“继承了死亡”。
这是递归问题:
你死亡的每一种方式,也都会成为杀死依赖你的项目的方式。
API 突然终止(API rug-pull)
项目封装的外部服务被拥有者撤掉了。
比如:
Twitter 2023 年的 API 改动,以及 Reddit 后续动作,一次性杀死了一整代客户端库。
平台层面也类似:
Flash、NPAPI、Chrome Apps 全部属于这一类。
无论哪种情况,维护者对此毫无办法。
被时代替代(Superseded)
项目的功能已经不再需要:
例如:
-
Object.assign 出现后的 object-assign;
-
ES2015 后的 lodash 单函数包;
-
各种 Promise/fetch polyfill;
-
以及协议层面上大量用于处理不再有人使用的格式的库。
维护者理所当然地停止了维护,但数十万个 lockfile 文件仍然在安装它,因为“既然还能运行,就没人有动力删依赖”。
项目分裂
Fork 地狱(Fork limbo)
因为分歧或维护者离开,项目分裂成多个 fork。
没有哪个 fork 明显胜出,于是下游停留在“分裂前最后一个版本”,不愿押注未来可能失败的 fork。
原项目继续拥有最高安装量,而真正开发工作已经发生在别的名字下。
io.js 与 Node 最终重新合并,libav 最后回归 FFmpeg,但更多小型 fork 永远不会解决。
许可证变更的后续影响(Licence rug-pull aftermath)
项目改成非开源许可证。
社区基于旧许可证 fork 出新项目,但生态尚未统一。
Terraform/OpenTofu、Redis/Valkey 都处于这个阶段,而 Elasticsearch 已经更往前走了一段。
多数 lockfile 仍指向原项目最后一个开源版本——一个再也没人维护的固定版本。
开源核心被掏空了(Open-core hollowing)
真正有价值的开发已经转向商业版。
开源 repo 仍存在,也会继续发版,但主要只是:
用户最初采用的那个项目,实际上已经变成了一个更小、更弱的东西,只是从未改名。