Windows 上的"半更新":hermes.exe 被删掉、版本戳还停在旧版本

你在 Windows 上敲 hermes update,滚了一屏日志,最后一行是:
⚠ Update follow-up 'launchers' did not finish: launchers: source launcher publication failed — retry the source update (the next launch or `hermes update` retries it)
看着像“下次会自己修好”,于是你关掉窗口。第二天 hermes --version 还是旧版本,bin\hermes.exe 不见了,只剩一个 hermes.cmd。再跑一次 hermes update,还是这行警告。这个死循环在 2026 年 10 月 22-23 日合入的两个 PR 里被拆开了。
两个症状,同一个根因
把日志拆开看,其实是两件独立的事:
- 运行中的 exe 删不掉。更新过程中 Python 版本被重新钉了一版(
pm/lock.json从 Python 3.14.7 换到 3.14.9),所以启动器(launcher,就是bin\hermes.exe这类入口)要重新发布。发布逻辑会先删掉旧的hermes.exe,免得它挡住新命令——而 Windows 不允许删除正在运行的镜像文件。正在运行的那个 exe,恰好就是你在跑hermes update的这个。 - 发布失败连带跳过了版本戳。启动器没发布成功,
write_source_stamp就不会执行,于是install-stamp.json和.hermes-bootstrap-complete还停在更新前的v0.22.4。
最要命的是第三层:提示里的“下次启动或再跑一次 hermes update 会重试”,重试时用的还是同一个 exe,所以它永远清不掉。你以为在等它自愈,它在等你换一个 exe。
为什么只有 Windows 会这样
在 POSIX 系统上,一个正在运行的文件可以直接被 unlink(删掉目录项),进程继续用原来的 inode;Windows 不一样,映射中的镜像不允许删除。
触发条件也窄:只在 Windows + 跨 Python 换版本 的更新里出现。从 v0.22.3 升到 v0.22.4 时 Python 一直是 3.14.7,旧 exe 对得上就留着了,所以你不会每次都遇到——这恰恰是最难排查的那种 bug。
修法一:删不掉就改名挪开
#143291 在 hermes_cli/_launchers.py 里加了一个 _retire_exe:先试删除;Windows 因为“文件正在使用”拒绝时,把它改名挪到一边(hermes.exe.<pid>.deleteme)。Windows 允许重命名一个被映射的镜像——distlib 自己也是这么干的。挪开的副本会在之后某次发布时被回收,那时它的进程早就退出了。
改完之后,发布不再失败,版本戳也能照常写入。顺便 _collect_retired 也会把 distlib 自己留下的 hermes.exe.deleteme 一并打扫掉。
修法二:别退化成 .cmd,重新铸一个真 exe
只修“删不掉”还不够。#143339 记录了下半段:更新过程只激活依赖环境,而那个环境里没有 distlib,store Python 的 site-packages(里面有 pip)也被摘掉了。于是 mint_launcher 找不到 ScriptMaker,只能发布一个 .cmd;再加上前一个修复已经能把运行中的 exe 改名挪开,这个 exe 就被“合法”地退役了——最终安装里只剩下 hermes.cmd。
安装器当初铸第一个 exe 用的是 python-build-standalone 自带的 pip,而更新从来不去问它。修法是把这条路接上:当运行中的进程 import 不到 distlib 时,mint_launcher 从它正在绑定的那个解释器旁边加载 pip._vendor.distlib(路径是 <python>\Lib\site-packages\pip\_vendor\distlib,每个 PBS 构建都带),并挂在一个私有模块名下,这样 store 的 pip 不会在进程里抢答 import pip。
完全找不到 distlib 的解释器仍然回退到 .cmd——只是现在这种情况真的很少。
你现在该做什么
如果你在 Windows 上,并且最近更新过 Python 版本,花一分钟核对三件事:
hermes --version # 语义版本 + 绑定的 Python 版本
- 看
bin\hermes.exe是否还在(在你的 Hermes home 下的bin目录里)。只剩hermes.cmd说明踩到了这个问题; - 打开
install-stamp.json和.hermes-bootstrap-complete,里面的 commit 应该和你实际跑的一致,不是上一次的版本号; - 两者对不上时,先更新到包含这两个修复的构建,然后再跑一次
hermes update——新的发布逻辑才能在旧 exe 还在运行时把它挪开。
如果你怀疑安装本身已经乱了(git 工作区被改过、更新后命令时好时坏),损坏 git 修复指南里有更完整的排查顺序;hermes update 的通道与回滚语义看稳定通道说明,命令参数在 hermes update 参考。
目前的状态
这两个修复都在 2026 年 10 月 22-23 日合入 main,第三个 PR(#143261)顺手修了 Windows E2E 的磁盘占用问题——测试在拆除时没删掉安装目录,把 runner 塞满了。问题是在 v0.22.5 的候选构建里被 E2E 抓出来的(v0.22.4 到 HEAD 的升级路径),所以它影响的是“从 v0.22.4 升级”的 Windows 用户。最近一次正式发布仍是 10 月 21 日的 v0.22.4,这些修复还没进稳定通道。
顺带说一句:这不是那种“只有 CI 关心”的修复。Windows 用户的启动器一旦退化成 .cmd,指向它的快捷方式、计划任务、以及 PATH 里记着 .exe 的脚本都会静默失效——而失败信息只有那一行 ⚠。