desktop: swap node-pty fork for upstream microsoft/node-pty 1.1.0

The previous dependency, @homebridge/node-pty-prebuilt-multiarch@0.13.1,
publishes no win32-arm64 prebuilds on its v0.13.x line, and its v0.14.x
betas (which do add an arm64 Windows build) ship no electron-vXXX-win32-
arm64 prebuilds at all -- so packaged Electron 40 builds (NMV 143) would
fail at runtime even on a successful npm install. Net effect: the
desktop's integrated terminal was unbuildable on Windows-on-ARM, in
both dev (npm install fails: 404 fetching the node-vXXX-win32-arm64
prebuilt) and packaged builds (no Electron-ABI prebuilt exists).

The homebridge fork was originally created because upstream node-pty
shipped no prebuilds at all. That hasn't been true since node-pty@1.0
(April 2024), which:

- bundles prebuilts for mac (arm64+x64) and Windows (arm64+x64) directly
  inside the npm tarball -- no GitHub-Releases fetch, no missing-binary
  failure mode
- uses N-API (node-addon-api) for ABI stability across Node and Electron
  major versions, so the same pty.node binary loads under Node 22 (dev)
  and Electron 40+ (packaged) without per-ABI rebuilds
- is what VS Code, Hyper, and Theia actually ship

API surface is identical (spawn / onData / onExit / write / resize /
kill) -- no call-site changes needed.

Specifically:

- apps/desktop/package.json: replace the @homebridge fork with
  node-pty@1.1.0 (exact pin). Widen `asarUnpack` from `["**/*.node"]`
  to also unpack `**/prebuilds/**`, because node-pty ships runtime-
  execed helpers alongside its .node files (darwin spawn-helper has no
  extension and would not be matched by `**/*.node`; conpty.dll,
  OpenConsole.exe, winpty.dll, winpty-agent.exe on Windows are also
  exec'd at runtime and cannot live inside asar).

- apps/desktop/electron/main.cjs: update both require() strings to
  match the new package name and the new staged path under
  resources/native-deps/node-pty/.

- apps/desktop/scripts/stage-native-deps.cjs: point at node_modules/
  node-pty. node-pty's prebuilts live under prebuilds/<plat>-<arch>/
  (not build/Release/), so update the include glob to copy that dir.
  Per-arch staging keeps the resource bundle small (target arch comes
  from npm_config_arch when electron-builder cross-builds, else
  process.arch). Explicitly enumerate file types in the prebuilds glob
  so the ~25 MB of .pdb debug symbols that prebuild-install bundles
  for Windows crash analysis don't bloat the installer (29 MB -> 2.6 MB
  staged on win32-arm64). Re-assert +x on the darwin spawn-helper
  defensively, since a stripped mode bit would manifest as a silent
  ENOENT at first pty.spawn().

- apps/desktop/scripts/test-desktop.mjs: update expectedNativeDepPaths()
  and its assertion site to look at prebuilds/<plat>-<arch>/ instead of
  build/Release/. Add an explicit spawn-helper-exists check on darwin
  so a regression in the asarUnpack glob would fail loudly in CI rather
  than at first PTY spawn.

Trade-off: Linux end-users lose prebuilts and fall back to building
node-pty from source on `npm install`. Acceptable because Hermes
ships no Linux desktop builds (desktop-release.yml matrix is mac + win
only, package.json declares no `linux` target), and Linux developers
hacking on the desktop already need a C++ toolchain for the rest of
the stack.

Verified on Windows 11 ARM64 (Snapdragon):
  npm install                                          -> exit 0
  node -e "require('node-pty').spawn(...)" round-trip  -> OK
  stage-native-deps                                    -> 27 files, 2.6 MB
  load from staged tree (simulates packaged fallback)  -> ConPTY
                                                           round-trip OK
This commit is contained in:
emozilla
2026-05-18 21:50:53 -07:00
parent 5dcfb0b82e
commit c858484b45
5 changed files with 82 additions and 43 deletions
+19 -11
View File
@@ -84,16 +84,16 @@ function exists(target) {
}
// Match nodepty native binding location to what main.cjs's resolver fallback
// expects (apps/desktop/electron/main.cjs, packaged-build branch).
// expects (apps/desktop/electron/main.cjs, packaged-build branch). Upstream
// node-pty 1.x is N-API based and ships per-arch prebuilts under
// prebuilds/<platform>-<arch>/ instead of build/Release/. We check the
// per-arch dir since that's what stage-native-deps actually copies.
function expectedNativeDepPaths() {
const root = path.join(APP.resourcesPath, 'native-deps', '@homebridge', 'node-pty-prebuilt-multiarch')
const releaseDir = path.join(root, 'build', 'Release')
// Just check the package.json exists; the actual .node binary names vary
// (pty.node + conpty.node on Windows; pty.node on Unix), so we let the
// existence-of-the-directory + a non-empty list be enough.
const root = path.join(APP.resourcesPath, 'native-deps', 'node-pty')
const prebuildsDir = path.join(root, 'prebuilds', `${PLATFORM}-${ARCH}`)
return {
packageJson: path.join(root, 'package.json'),
releaseDir,
prebuildsDir,
libIndex: path.join(root, 'lib', 'index.js')
}
}
@@ -325,12 +325,20 @@ function validateBundle() {
if (!exists(native.libIndex)) {
die(`Missing node-pty lib/index.js in resources/native-deps: ${native.libIndex}`)
}
if (!exists(native.releaseDir)) {
die(`Missing node-pty build/Release directory: ${native.releaseDir}`)
if (!exists(native.prebuildsDir)) {
die(`Missing node-pty prebuilds dir for ${PLATFORM}-${ARCH}: ${native.prebuildsDir}`)
}
const nodeBinaries = fs.readdirSync(native.releaseDir).filter(name => name.endsWith('.node'))
const nodeBinaries = fs.readdirSync(native.prebuildsDir).filter(name => name.endsWith('.node'))
if (nodeBinaries.length === 0) {
die(`No .node native binaries found in: ${native.releaseDir}`)
die(`No .node native binaries found in: ${native.prebuildsDir}`)
}
// Darwin requires a runtime-execed spawn-helper alongside pty.node; missing
// it manifests as "ENOENT: spawn-helper" on first pty.spawn() call.
if (PLATFORM === 'darwin') {
const spawnHelper = path.join(native.prebuildsDir, 'spawn-helper')
if (!exists(spawnHelper)) {
die(`Missing node-pty spawn-helper (required on darwin): ${spawnHelper}`)
}
}
// Renderer payload check (either unpacked or in the asar)