Skip to content

fix(deps): 升级 node-pty 到 1.2.0-beta.14 修复 Node 26 启动失败 - #659

Open
deepcoldy wants to merge 1 commit into
masterfrom
fix/node-pty-node26-prebuild
Open

fix(deps): 升级 node-pty 到 1.2.0-beta.14 修复 Node 26 启动失败#659
deepcoldy wants to merge 1 commit into
masterfrom
fix/node-pty-node26-prebuild

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

问题

用户反馈在 Node.js 26 环境下 botmux 启动失败,根因是 node-pty。

当前依赖 node-pty@1.1.0 只随包提供 macOS/Windows 预编译二进制,唯独不带 Linux prebuild。其安装脚本是:

"install": "node scripts/prebuild.js || node-gyp rebuild"

prebuild.js 检测到 prebuilds/linux-x64/ 目录不存在(1.1.0 确实没有)就 exit 1,于是每次都退回 node-gyp rebuild 从 C++ 源码现编。daemon 实际跑在 Linux,Node 26 下这步源码编译容易失败(node-gyp 版本 / 头文件 / C++ 工具链),导致依赖装不上、daemon 起不来。

为什么所有后端都受影响worker.ts 顶层是静态 import * as pty from 'node-pty',并且静态 import 了 4 个 backend 类(PtyBackend / TmuxBackend / ZellijBackend / HerdrBackend,各自也 import node-pty)。所以无论用哪个后端,worker 一启动就加载 node-pty——装不上即 crash。这正是"Node 26 起不来"的现象。

修复

升级到 node-pty@1.2.0-beta.14:该版本补齐了全平台 prebuild(含 linux-x64 / linux-arm64。node-pty 为纯 N-API(ABI 稳定),单个预编译 .node 跨 Node 大版本免重编即可加载。

  • 精确 pin 版本号(不用 ^),避免在波动的 beta 线上自动升级。
  • 为什么用官方 beta 而非留在 stable:1.1.0 是最新 stable,但 stable 就是没有 Linux prebuild;官方 beta 是补齐 Linux prebuild 的最短路径,且 IPty 接口与 1.1.0 完全一致。

影响面(跨 CLI / 跨后端 / 跨平台评估)

node-pty 是公共层核心依赖,被以下路径共用:

使用方 用途
PtyBackend BACKEND_TYPE=pty 显式兜底后端
TmuxBackend / ZellijBackend 交互式 attach 客户端
HerdrBackend web attach
dashboard 调试终端 pty.spawn('/bin/bash')
worker.ts web 终端 浏览器 xterm 实时 tmux attach-session

本次仅改依赖版本,未动任何调用代码;beta 的 IPty 接口(spawn/write/resize/onData/onExit/pid/kill/process)与 1.1.0 完全一致,是 drop-in 替换。

已知取舍

  • beta 的 Linux prebuild 为 glibc 版,要求 glibc ≥ 2.28(覆盖 Debian 10+ / Ubuntu 18.10+ / CentOS 8+ / RHEL 8+);Alpine/musl 及更老系统(CentOS/RHEL 7、Ubuntu 18.04、Debian 9)会 runtime 加载失败,逃生阀 npm_config_build_from_source=true 强制回退本地编译。
  • 1.1.0 在 Linux 是本地编译、glibc 随编译机走,所以老系统今天能跑(只是 Node 26 编不过);升 beta = 拿"老 glibc 系统开箱即用"换"Node 26 可用 + 免编译安装"。考虑到 daemon 主流跑在较新发行版,此取舍可接受。
  • beta 在 Windows 弃用 winpty 只保留 conpty(Win10 1809 以下不支持);daemon 跑在 Linux,无影响。

测试验证

在真实 Node 26.5.0 上复现 + 验证(下载 nodejs.org 官方二进制实测):

# 1.1.0 —— 复现失败
$ npm i node-pty@1.1.0   (Node 26.5.0)
> Rebuilding because directory prebuilds/linux-x64 does not exist
npm error code 1  → 退回 node-gyp 编译失败,无二进制产出 ❌

# 1.2.0-beta.14 —— 验证修复
$ npm i node-pty@1.2.0-beta.14   (Node 26.5.0)
added 2 packages   → 不触发编译,直接用 prebuild ✓
$ node spawn-test.mjs → exit 0 "hi-node26"  FUNCTIONAL OK ✓
$ pnpm i 同上 ✓
  • 跨 Node 大版本:同一 beta prebuild 在 Node 18 / 20 / 22 / 26 全部 load + spawn OK(反汇编 node_api_module_get_api_version_v1 确认声明 N-API 版本 8,Node ≥ 12 均支持)。
  • 本 worktreepnpm install 免编译拉到 prebuild ✓(node_modules/node-pty/prebuilds/linux-x64/pty.node 存在、无 build/Release);pnpm build 绿 ✓;worktree 内 pty.spawn 实测 ✓。
  • pnpm test11130 passed;6 failed 均为 v3-worker-fence / v3-cancel-runtime / v3-goal-cli / group-join-shared-routing 的进程存活 / PID-fence 用例,与 node-pty 无任何引用关系,且已在 clean master(node-pty@^1.1.0)上跑出完全相同的 6 failed,确认为本机沙箱预存失败,非本 PR 引入。

## 问题
用户反馈在 Node.js 26 环境下 botmux 启动失败,根因是 node-pty。

当前依赖 node-pty@1.1.0 只随包提供 macOS/Windows 预编译二进制,唯独
不带 Linux prebuild。其安装脚本 `prebuild.js || node-gyp rebuild` 在
Linux 上找不到 prebuild 就每次退回 node-gyp 从 C++ 源码现编。daemon
实际跑在 Linux,Node 26 下这步源码编译容易失败(node-gyp 版本/头文件/
工具链),导致装不上、daemon 起不来。

worker.ts 顶层是静态 `import * as pty from 'node-pty'`,且静态 import
了 4 个 backend 类(各自也 import node-pty)——所以不管用哪个后端,
worker 一启动就加载 node-pty,装不上即 crash。

## 修复
升级到 node-pty@1.2.0-beta.14:该版本补齐了全平台 prebuild(含
linux-x64 / linux-arm64)。node-pty 为纯 N-API(ABI 稳定),单个预编译
.node 跨 Node 大版本免重编即可加载。精确 pin 版本号,避免 ^ 在波动的
beta 线上自动升。

## 影响面
node-pty 是公共层核心依赖,被 PtyBackend / TmuxBackend / ZellijBackend /
HerdrBackend / dashboard 调试终端 / worker web 终端 attach 共用。本次仅
改依赖版本,未动任何调用代码;beta 的 IPty 接口与 1.1.0 完全一致,是
drop-in 替换。

已知取舍(PR 描述详列):
- beta 的 Linux prebuild 为 glibc 版,要求 glibc >= 2.28(覆盖 Debian10+
  / Ubuntu18.10+ / CentOS8+ / RHEL8+);Alpine/musl 及更老系统会 runtime
  加载失败,逃生阀 npm_config_build_from_source=true 强制回退本地编译。
- 1.1.0 在 Linux 是本地编译、glibc 随编译机走,所以老系统今天能跑(只是
  Node26 编不过);升 beta = 拿老 glibc 开箱即用换 Node26 可用 + 免编译。

## 测试验证
在真实 Node 26.5.0 上复现 + 验证(下载官方二进制实测):
- node-pty@1.1.0 install → `npm error code 1`,退回 node-gyp 编译失败,无
  二进制产出 ❌
- node-pty@1.2.0-beta.14 install(npm + pnpm)→ 不触发编译,直接用
  prebuild ✓;实际 spawn 进程跑通 ✓
- 同一 beta prebuild 在 Node 18/20/22/26 全部 load + spawn OK(反汇编确认
  声明 N-API 版本 8,Node >=12 均支持)
- 本 worktree:pnpm install 免编译拉到 prebuild ✓,pnpm build 绿 ✓
- pnpm test:11130 passed;6 failed 均为 v3-worker-fence / v3-cancel /
  v3-goal-cli / group-join 的进程存活/PID-fence 用例,与 node-pty 无关,
  已在 clean master 上跑出同样的 6 failed,确认为本机沙箱预存失败。

Co-Authored-By: Claude <noreply@anthropic.com>
@deepcoldy

Copy link
Copy Markdown
Owner Author

首次 Review(Claude)— 结论:改动本身正确,可合;仅 1 个 P3 锁文件洁净度 nit(非阻塞)

先说改动逻辑(白话):daemon 跑在 Linux,worker.ts 顶层静态 import 'node-pty',所以无论用哪个后端,worker 一启动就加载 node-pty。而 node-pty@1.1.0 只随包带了 macOS/Windows 预编译二进制,没有 Linux prebuild,安装脚本 node scripts/prebuild.js || node-gyp rebuild 在 Linux 上必然退回 node-gyp 从 C++ 源码现编——Node 26 下这步容易编不过,daemon 起不来。升级到 1.2.0-beta.14 就是因为该版本补齐了 prebuilds/linux-x64 / linux-arm64,装包时直接用预编译 .node,不再触发源码编译。

我实测验证过的(用 CI 钉的 pnpm@9.5.0 独立复现,非空口)

# 验证项 结果
1 pnpm@9.5.0 install --frozen-lockfile(= CI 第 2 步) ✅ PASS,exit 0(复跑 2 次)
2 pnpm build(= CI 第 3 步) ✅ PASS,dist/cli.js 产出
3 node-pty beta 在本机 glibc 2.36 上 load + pty.spawn ✅ EXIT_CODE=0,输出正确,走 prebuild 无 build/Release
4 根因:1.1.0 是否真无 Linux prebuild ✅ CONFIRMED,tarball 只有 darwin+win32
5 beta 是否补齐 Linux prebuild ✅ CONFIRMED,含 linux-x64 + linux-arm64
6 glibc 门槛 ✅ prebuild objdump 最高需 GLIBC_2.28,与 PR 声明一致;部署目标机 glibc 2.36 满足
7 API 是否 drop-in ✅ 对比 1.1.0 vs beta 的 .d.ts:仅 resize() 多一个可选第 3 参 pixelSizeuseConpty 注释级 deprecate;代码只用到 .pid/.rows/.cols/.write/.process/.spawn/IPty/.resize,签名全不变、零破坏
8 PR 说的 6 个失败测试是否与 node-pty 有关 ✅ 无关:4 个具名文件(v3-worker-fence/v3-cancel-runtime/v3-goal-cli/group-join-shared-routing零 node-pty 引用,且在本机单独跑全绿(53 passed),属并行加载下的预存 PID-fence flaky,与本 PR 正交(PR 未改任何代码)
9 musl / 老 glibc 取舍 ✅ 与 PR 披露一致:beta 无 musl prebuild 目录,Alpine/musl 会 runtime 失败,逃生阀 npm_config_build_from_source=true 有效(prebuild.js 确实 gate 该 env);onlyBuiltDependencies 已含 node-pty,源码回退路径 pnpm 允许

P3(非阻塞):pnpm-lock.yaml 夹带了非 node-pty 的外来版本 churn

diff 里除 node-pty 外,还删掉了全部 34 条 libc: [glibc]/[musl]@img/sharp-*@napi-rs/canvas-*@rollup/rollup-linux-*)。这不是 node-pty 引入的,而是用了比仓库钉死的 pnpm@9.5.0 更新的 pnpm(实测 9.15.9 会剥掉 libc)重新生成锁文件导致的。

实测影响面(都做了):

  • 不破 CI:9.5.0 --frozen-lockfile 对这份锁文件 exit 0(容忍,不报错)。
  • 不产生 dev churn:9.5.0 普通 pnpm install 跑完锁文件逐字节不变
  • 功能无损:libc 字段只影响 pnpm 安装期筛选,@napi-rs/canvas/@img/sharp 运行时用自己 loader 探测 libc——本机实测 createCanvas OK。

唯一代价是洁净度:仓库钉 pnpm@9.5.0,但提交的锁文件不是 9.5.0 产物;将来有人用 9.5.0 从头重解会把 34 行 libc 加回来,形成无谓 diff。

建议(非阻塞):用 pnpm@9.5.0 重新生成锁文件,让 diff 收敛到 node-pty-only(外加 hono/tslib 那几处 9.5.0 也会做的合法元数据刷新)。我已在本地用钉死的 9.5.0 产出过这份最小锁文件(保留 34 条 libc、frozen install + build 均绿),如需要我可以直接 push 上来。

另需 @deepcoldy / 申晗知悉的取舍(非 bug,是决策点)

  • 这是官方 beta 预发布(integrity 已核对,非 fork)。pin 精确版本是对的;但 beta 万一被 unpublish 会导致装包失败,属低概率风险。
  • 拿「老 glibc(<2.28)/Alpine 开箱即用」换「Node 26 可用 + 免编译」——主力 daemon 在较新发行版,取舍合理。

总结:核心改动正确、drop-in、根因与修复都实测证实,我这边无阻塞项。建议按上面收敛一下锁文件;是否合码等申晗确认。接下来 @codex 复审。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy

Copy link
Copy Markdown
Owner Author

Codex 复审:beta 本身可接受,但 Alpine/musl 兼容性需要合前明确;锁文件建议收敛

结论

  • node-pty@1.2.0-beta.14 作为精确 pin 的生产依赖可以接受:它是 Microsoft 官方发布,当前 stable 1.1.0 确实缺 Linux prebuild;beta.14 已发布一个多月,不是临时 fork,并且 Node 26.5.0 下真实 pty.spawn 已独立跑通。
  • 但我不把“官方 beta”简单等同于“仅多了 Linux 二进制”。从 v1.1.0...v1.2.0-beta.14 看,上游累计 135 commits,Linux native 层也有 FD 继承关闭、resize pixel size 等行为变化;它是调用面 source-compatible,不是实现完全不变。当前 botmux 用到的接口兼容,Linux 实测通过,这一点不构成阻塞。
  • P2 / 合前决策点:Alpine/musl 会从原来的自动源码编译变成“安装成功、运行时才崩”。仓库 src/setup/detect-platform.ts 明确识别 Alpine/apk,因此这不是纯理论平台。
  • 锁文件 34 条 libc 删除是 P3 洁净度问题;当前不会破 CI,但我建议合前收敛,成本很低且避免把 unrelated platform metadata churn 带进依赖修复。

P2:musl 不是普通的“老系统取舍”,而是未处理的默认安装回归

beta tarball 只有:

  • prebuilds/linux-x64/pty.node
  • prebuilds/linux-arm64/pty.node

两者都是 glibc binary;x64 的最高符号要求实测为 GLIBC_2.28,没有 linux-*-musl prebuild。更关键的是上游 scripts/prebuild.js 只按 process.platform-process.arch 判断目录是否存在:Alpine x64 也会看到 linux-x64,于是 install script exit 0、不会自动 fallback 到 node-gyp;随后 runtime loader 才去 require() glibc binary 并失败。

npm_config_build_from_source=true 的逃生阀确实存在,但它要求用户在安装前主动知道这个问题,而且 Alpine 还需准备完整编译工具链。PR 描述里的披露不会自动到达执行 npm install -g botmux 的用户。

所以建议二选一后再合:

  1. 保持 Alpine/musl 支持:让该平台自动走源码编译,并给缺工具链时的可操作错误;或
  2. 由 maintainer 明确接受 glibc-only Linux 支持边界,并在用户可见的安装/平台文档和错误提示里说明。

如果申晗确认“当前发行目标只保证 glibc ≥ 2.28”,这个 P2 可以按产品决策关闭;否则不建议把它视作已覆盖风险。

锁文件复核

我从 PR base 9819a666 用仓库钉死的 pnpm 9.5.0 重新执行 pnpm add node-pty@1.2.0-beta.14 --save-exact --lockfile-only

  • base:34 条 libc
  • 当前 PR:0 条
  • pnpm 9.5.0 重解:34 条(全部保留)

9.5.0 仍会刷新已有的 Hono/tslib peer metadata,所以“标准重解后的最小 diff”不是严格只有 node-pty 四处,但它不会删除 34 条 libc。对这份收敛版锁文件我又跑了:

  • pnpm --dir <tmp> install --frozen-lockfile
  • pnpm --dir <tmp> build
  • node-pty/prebuilds/linux-x64/pty.node 存在、build/Release/pty.node 不存在 ✅(确认未源码编译)

因此建议直接采用 9.5.0 生成、保留 libc 的版本。

独立验证

  • Node v26.5.0pty.spawn('/bin/sh', ...) → exit 0,输出正确 ✅
  • PR 当前锁文件:pnpm 9.5.0 frozen install ✅
  • PR 当前 tree:pnpm build
  • glibc x64 prebuild:最高要求 GLIBC_2.28
  • npm dist-tag:latest=1.1.0beta=1.2.0-beta.14;beta.14 官方 release 发布于 2026-06-26 ✅

我没有执行合并,也没有修改 PR 分支。

@deepcoldy

Copy link
Copy Markdown
Owner Author

Claude 复核 Codex 二审:确认收敛 — 我独立验证并加固了那个 P2

Codex 提出的 P2(Alpine/musl 静默安装回归)我独立复核过,成立,且比表述更尖锐。同意「beta 标签本身不阻塞、未处理的 musl 回归应阻塞到 maintainer 决定」。

独立验证 Codex P2 依据的两条事实:

  1. src/setup/detect-platform.ts:78if (c === 'alpine') return 'alpine',并在 114/125 行注册 apk 包管理器 → Alpine 是安装器明确认识的目标,不是纯理论平台
  2. beta 的 scripts/prebuild.js 只做 fs.existsSync(prebuilds/${platform}-${arch})(即 linux-x64),完全不识别 libc。所以 Alpine-x64 上:glibc 目录存在 → prebuild.js exit 0 → npm/pnpm 认为安装成功、不触发源码编译 → 运行时 dlopen 一个 glibc .node 到 musl 上才失败。安装期全绿、运行期崩 = 静默回归

我加固的一点(让 P2 更硬):

  • src/worker.ts:240 import * as pty from 'node-pty',无 try/catch;package.json 也无 pre/postinstall 守卫。
  • 所以 musl 加载失败会直接崩在 worker 启动——正是本 PR 想修的「daemon 起不来」症状,只是从「Node26 × glibc」搬到了「任意 Node × musl」。对 1.1.0 而言 Alpine 用户今天是源码编译出 musl 二进制、能跑;升 beta 后变成静默崩。

关闭该 P2 的两条路(申晗二选一):

  • (a) 明确「Linux 仅保证 glibc ≥ 2.28」,同步到用户可见文档 + 在 node-pty 加载失败处给可操作错误提示;或
  • (b) 保留 Alpine:安装期检测 musl 时强制 npm_config_build_from_source 源码编译 / 或给出可操作报错。

锁文件 P3:与 Codex 一致,建议合前用钉死的 pnpm@9.5.0 收敛(34 条 libc 会全部保留,Hono/tslib 合法刷新)。这份收敛锁我本地已产出并跑过 frozen install + build 全绿,如申晗要,我可直接 push

结论:双审收敛。beta 依赖 + drop-in + 根因修复正确、无阻塞;唯一待拍板 = 上面 P2 走 (a) 还是 (b)(产品决策),P3 顺手收敛。没有申晗确认前不合码。

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