Skip to content

fix(install): install.sh 的 PATH 守卫问错问题,装完新终端仍找不到 botmux - #1129

Merged
deepcoldy merged 1 commit into
masterfrom
fix/install-sh-path-guard
Sep 1, 2026
Merged

fix(install): install.sh 的 PATH 守卫问错问题,装完新终端仍找不到 botmux#1129
deepcoldy merged 1 commit into
masterfrom
fix/install-sh-path-guard

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

问题

install.sh 在写启动文件前有一道闸:

case ":$PATH:" in
  *":$INSTALL_DIR:"*) : ;;  # already on PATH

它问的是「执行安装那个进程的 PATH 里有没有 INSTALL_DIR」,而决定 botmux 能不能用的是「用户以后开的终端有没有」——那是启动文件的属性,不是当前进程的属性。

这与 #1117 在 npm 侧(scripts/postinstall-bin.mjs)修掉的是同一个错,curl 侧当时没跟上。那边留了整段 ⚠️ DO NOT GATE THIS ON process.env.PATH 的注释讲这件事。

两者何时会分叉:botmux 自己就制造这个场景。 daemon 会把 ~/.botmux/bin 前置进它 spawn 的每个 CLI 会话的 PATH(5 处 prependBotmuxBin)。所以在 botmux 会话里跑这个安装器 ⟹ 命中 skip 分支 ⟹ 零文件、零提示、exit 0,而新终端里 botmux 依然 command not found。而自 #1047 起 npm 包没有 bin 字段,那个 launcher 是唯一入口,PATH 没配就等于没装上。

改法

删掉这层外壳,让写启动文件无条件执行。不需要这道闸,因为下面本来就有两层各自独立的幂等,且都不依赖当前 PATH:

作用
path_line_present() 文件里已有的行不再追加
写出去的那行自带 case 守卫 重复 source 也不会把 PATH 撑大

原注释整段保留并改写成「为什么不能加这道闸」,与 postinstall-bin.mjs 里同款说明保持一致——下一个人想「顺手优化一下」时能看到理由。

实测:四格 A/B(隔离 HOME)

dir 不在 PATH dir 已在 PATH
修前 写 2 个文件 写 0 个(exit 0,无任何提示)← bug
修后 写 2 个文件 写 2 个文件

修后两列一致 = PATH 不再影响结果,这正是要的性质。

新增回归测试 test/install-sh-path-entry.test.ts(8 用例)

抽出 install.sh 里真实的 PATH 段执行,而不是断言脚本文本——原代码文本上完全合理,错的只是「预置 PATH 下的行为」,纯文本断言抓不到。做法沿用已有的 install-sh-musl-asset.test.ts

⚠️ 第一版 harness 对本 bug 完全失明(实测,已修)

harness 起初从 posix_line= 开始抽。把闸重新加回去(就加在那行上面,正是原 bug 的位置)时 —— 8 个用例全绿。因为那道闸根本没被抽进来。现改为从最后一个 helper 之后一路取到 EOF,任何位置的闸都在范围内。

另加 assertHarnessIsWellFormed():本文件的症状就是「零文件」,而抽取失败也会产出零文件,两者无法区分(甚至会把结论反过来:buggy 分支看着像 PASS)。所以先证明 harness 语法合法、且真的含有 writer / 幂等检查 / per-shell 分发,再下结论。

反变异 6/6 全红

变异 结果
M1 把 $PATH 闸加回去(即本 bug 2 failed
M2 per-shell 分发恒走 .profile 4 failed
M3 path_line_present 恒 false 2 failed
M4 写出的行丢掉自带 case 守卫 1 failed
M5 bash 只写 .bashrc 不写登录文件 3 failed
M6 awk 整元素比对改前缀比对(兄弟目录假阳性) 1 failed

每次变异后立即 cp 还原并 cmp 校验(restored ✓)。

⚠️ 按惯例声明边界:反变异全红只证明守卫对我想到的那 6 种退化有牙,不等于覆盖完备。欢迎 reviewer 补变异体。

影响面

只动 install.sh 的 PATH 段 + 新增一个测试文件,不含 src/ 变更。不影响 npm / pnpm / bun 安装路径(那半边由 #1117 / #1119 / #1122 负责),也不影响任何平台 / CLI / 会话类型的运行时行为。

测试

  • 新文件 8/8 绿sh -n install.sh 语法通过。
  • install-sh-path-entry + install-sh-musl-asset + install-path-entry + npm-binary-distribution 并集:99 passed
  • ⚠️ 其中 npm-binary-distribution2 条失败THE bun/pnpm BUG: a global LAYOUT… / SAFETY: a LOCAL dependency install…)。已在干净 origin/master 的独立 worktree 上同样跑出 2 failed | 33 passed,逐字相同 ⟹ 属既有失败,与本 PR 无关(本 PR 没碰 postinstall-bin.mjs)。

相关:#1117(npm 侧同款修复)· #1128(文档改推 curl 为默认安装方式,其中已把本问题写成「已知限制」,本 PR 合入后应把那段删掉)

🤖 Generated with Claude Code

install.sh 写启动文件前有一道

    case ":$PATH:" in *":$INSTALL_DIR:"*) : ;;  # already on PATH

它问的是「执行安装那个进程的 PATH 里有没有 INSTALL_DIR」,而决定
`botmux` 能不能用的是「用户以后开的终端有没有」——那是启动文件的属性,
不是当前进程的属性。这与 #1117 在 npm 侧(scripts/postinstall-bin.mjs)
修掉的是同一个错,curl 侧当时没跟上。

两者何时会分叉:botmux 自己就制造这个场景。daemon 会把 ~/.botmux/bin
前置进它 spawn 的每个 CLI 会话的 PATH(5 处 prependBotmuxBin),所以
在 botmux 会话里跑这个安装器,就会命中 skip 分支——零文件、零提示、
exit 0,而新终端里 `botmux` 依然 command not found。而自 #1047 起
npm 包没有 bin 字段,那个 launcher 是唯一入口,PATH 没配就等于没装上。

改法:删掉这层外壳,让写启动文件无条件执行。不需要这道闸,因为
下面本来就有两层各自独立的幂等,且都不依赖当前 PATH:
- path_line_present():文件里已有的行不再追加
- 写出去的那行自带 case 守卫:重复 source 也不会把 PATH 撑大
原注释整段保留并改写为「为什么不能加这道闸」,与 postinstall-bin.mjs
里同款说明保持一致。

## 实测(隔离 HOME,四格 A/B)

           |  dir 不在 PATH  |  dir 已在 PATH
  修前     |  写 2 个文件    |  写 0 个(exit 0,无任何提示)← bug
  修后     |  写 2 个文件    |  写 2 个文件

修后两列一致 = PATH 不再影响结果,这正是要的性质。

## 新增回归测试 test/install-sh-path-entry.test.ts(8 用例)

抽出 install.sh 里真实的 PATH 段执行,而不是断言脚本文本——原代码
文本上完全合理,错的只是「预置 PATH 下的行为」,纯文本断言抓不到。

⚠️ harness 必须整段取到 EOF:第一版从 posix_line= 起取,导致把闸
重新加回去(就加在那行上面)时 8 个用例全绿——即对本 bug 完全失明,
已实测。现改为从最后一个 helper 之后一路取到文件尾。
另加 assertHarnessIsWellFormed:本文件的症状就是「零文件」,若抽取
失败也会产出零文件,两者无法区分,故先证明 harness 语法合法且含
真实逻辑再下结论。

反变异 6/6 全红:
  M1 把 $PATH 闸加回去              → 2 failed(即本 bug)
  M2 per-shell 分发恒走 .profile    → 4 failed
  M3 path_line_present 恒 false     → 2 failed
  M4 写出的行丢掉自带 case 守卫     → 1 failed
  M5 bash 只写 .bashrc 不写登录文件 → 3 failed
  M6 awk 整元素比对改前缀比对       → 1 failed
每次变异后立即 cp 还原并 cmp 校验。

## 影响面

只动 install.sh 的 PATH 段 + 新增一个测试文件,不含 src/ 变更,
不影响 npm/pnpm/bun 安装路径(那半边由 #1117/#1119/#1122 负责)、
也不影响任何平台 / CLI / 会话类型的运行时行为。

测试:新文件 8/8 绿;install-sh-musl-asset / install-path-entry /
npm-binary-distribution 并集 99 passed,其中 npm-binary-distribution
有 2 条失败——已在干净 origin/master 的独立 worktree 上同样跑出
`2 failed | 33 passed`,逐字相同,属既有失败,与本 PR 无关。
@deepcoldy

Copy link
Copy Markdown
Owner Author

自动评审的初步意见(最终以维护者审阅为准)

方向认同,改动逻辑站得住:这道闸问的是「跑安装器那个进程」的 PATH,而决定 botmux 可用性的是「用户以后开的终端」——确实问错了对象,且与 #1117 在 npm 侧修的是同一个错。

独立复核过的几点(不是读描述得出的):

  • 零净新增可执行行:把 diff 的 +/- 行剥注释、剥缩进后求差集为空 ⟹ 除那 5 行 wrapper 外逻辑未动,本 PR 是纯删除。
  • 四格 A/B 用真 install.sh 的 PATH 段跑(隔离 HOME,master vs 修后各两格):master「已在 PATH」写 0 文件,其余三格均 2 文件 ⟹ 复现如描述。
  • prependBotmuxBin 独立数为 5 个真调用点(worker.ts×4 + worker-pool.ts×1),与描述一致。
  • 两层幂等各自验过:写出的行 source 3 次、嵌套至 depth 3,目录在 PATH 中始终只出现 1 次。
  • 补跑了描述未覆盖的 fish(含 XDG_CONFIG_HOME)与 ZDOTDIR 分支:行为正确,只是无测试覆盖。
  • 另跑了一组独立变异(换写法的闸 / zsh 写 .zshrc / 丢 MARKER / wrote= 不置位 / 登录文件恒 .bash_profile均转红,且三个语义等价重构(${wrote} 改写、整段注释重写、basename${SHELL##*/}保持绿 ⟹ SOURCE PIN 不误红。
  • 9 个读 install.sh 的测试文件:163 passed

两条建议(都不阻断合入):

  1. 描述里对那 2 条失败的归因建议修正。 描述称 npm-binary-distribution 有「2 条既有失败」。实测根因是 ENOENT: dist/utils/global-install.js —— dist/.gitignore 里,新 worktree 中没有 build 产物;跑完 bun run build 后同一并集 101/101 全绿。「与本 PR 无关」的结论是对的,但原因是构建状态而非既有失败。这段会进 git 历史,照现在的写法会让后来者去找一个并不存在的 bug。

  2. 建议同 PR 删掉 docs-site/docs/zh/pitfalls.md:11 那段「已知限制」。 那正是本 PR 修掉的行为(「install.sh 会认为无需处理而不写启动文件」)。描述里也提到合入后应删除,但当前 diff 未包含该文件,合完文档即刻过期。

另外供参考(属既有缺口,非本 PR 引入,不必在本 PR 处理):两个变异体在 install.sh:143-188(本 PR 未改动的区域)存活 —— 把写出的行改成未引号目录(注入防护),以及 path_line_present 去掉注释过滤。若后续有意补强这段的覆盖,可作为线索。

以上为自动评审的初步意见,可能有误判,最终以维护者审阅为准

@deepcoldy
deepcoldy merged commit 788918e into master Sep 1, 2026
7 of 9 checks passed
@deepcoldy
deepcoldy deleted the fix/install-sh-path-guard branch September 1, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant