Skip to content

fix(desktop): restore Linux node-pty packaging under npm 12 - #61798

Closed
KeroZelvin wants to merge 4 commits into
NousResearch:mainfrom
KeroZelvin:fix/desktop-node-pty-blocked-scripts
Closed

KeroZelvin wants to merge 4 commits into
NousResearch:mainfrom
KeroZelvin:fix/desktop-node-pty-blocked-scripts

Conversation

@KeroZelvin

@KeroZelvin KeroZelvin commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • restore stock Linux Desktop builds under npm 12's stable default install-script policy
  • rebuild node-pty during the desktop build when npm did not produce a usable host-native payload
  • make @electron/rebuild traverse the npm workspace root so it finds the root-hoisted node-pty
  • fail packaging instead of shipping an app without a non-empty pty.node
  • detect stale/incomplete packaged runtimes even when the desktop content stamp matches
  • make --skip-build reject an incomplete runtime with an actionable --force-build repair command
  • prevent host-compiled binaries from being copied into cross-target packages
  • stage exactly the payload node-pty will load at runtime instead of copying competing build and prebuild payloads
  • normalize electron-builder's ARMv7 armv7l label to Node's runtime arm label
  • preserve the NixOS PYTHON override for the fallback node-gyp build
  • add native-payload and launcher regression tests and run native tests in the desktop CI job

Fixes #62171.

Severity

P1 — High: latest stable npm breaks a major feature on the normal update path. Affected Linux users running stock Hermes with npm 12 can complete hermes update and then be unable to start Hermes Desktop. Recovery requires developer-level native rebuild/repackaging steps. The current P3 cosmetic label is incorrect and requires a maintainer with label permission to replace it with P1.

npm 12 stable compatibility

  • npm 12.0.0 was published as stable on July 8, 2026 and was the registry's latest version when this stock-Hermes failure was reproduced.
  • npm 12.0.1 was published on July 10 and is now tagged latest.
  • npm's documented v12 breaking default blocks dependency preinstall, install, and postinstall scripts—including implicit native node-gyp builds—unless explicitly covered by project allowScripts policy.
  • An isolated npm 12.0.1 probe against this repository still reports six blocked dependencies, including node-pty@1.1.0, electron@40.10.2, and esbuild@0.28.1. The 12.0.1 release contains unrelated npm view and sigstore fixes and does not revert this policy.
  • The trigger used unmodified Hermes v0.18.2 and only the supported hermes updatehermes desktop path. Manual rebuild commands were used only after reproduction to recover and validate the fix.

References:

Problem

On Linux x64, node-pty@1.1.0 does not include a prebuilds/linux-x64/pty.node payload. A native build therefore has to produce node_modules/node-pty/build/Release/pty.node.

With npm 12's install-script approval policy, a fresh root install can leave node-pty's lifecycle script blocked. The install completes with a warning, but no Linux native payload exists.

Hermes already had apps/desktop/scripts/rebuild-native.mjs, but it passed only the desktop workspace as buildPath. Because the dependency is hoisted to the repository root, @electron/rebuild stopped at apps/desktop/package.json, found no module to rebuild, and exited successfully. The subsequent staging step only warned when the target prebuild was absent, so electron-builder could package an app that later failed at startup with:

Failed to load native module: pty.node

A real hermes update also exposed a second boundary problem: npm completed with blocked-script warnings, but the packaged Desktop content stamp still matched. The update and the next hermes desktop launch therefore skipped rebuilding the already-incomplete package and attempted to start it. The Electron main process then crashed before the UI became usable.

This is a stock-Hermes compatibility regression with npm 12's stable defaults for Linux users installing or updating the Desktop app from a source checkout; it is not specific to one machine. The impact is a complete Desktop startup failure, not a cosmetic degradation.

Fix

  1. Run the native rebuild helper immediately before native dependency staging.
  2. Detect an existing host payload first, making the common macOS/Windows/pre-approved-install path a no-op.
  3. Give @electron/rebuild both the desktop buildPath and repository projectRootPath, force a source build for node-pty, and verify that the expected payload was actually created.
  4. Match node-pty's runtime precedence (build/Release before prebuilds/<platform>-<arch>) and stage exactly one selected payload, so validation cannot approve a different binary than the app loads.
  5. Copy local build artifacts only for host platform/architecture builds. Cross-target packages must provide a matching target prebuild.
  6. Normalize ARMv7 aliases when comparing host targets and naming runtime prebuild paths (armv7l from electron-builder, arm from Node).
  7. Make staging fail closed when neither a valid target prebuild nor valid host build exists. A payload must be a non-empty regular pty.node; Darwin also requires a non-empty regular spawn-helper.
  8. Carry Hermes' existing NixOS PYTHON discovery into the desktop build subprocess because the fallback compilation now occurs during npm run build/pack.
  9. Validate packaged-runtime completeness in the Python Desktop launcher before trusting the content stamp. Missing or empty native payloads force a rebuild.
  10. Reject incomplete runtimes in --skip-build with an actionable hermes desktop --force-build repair message.
  11. Revalidate a successful package command before writing the build stamp, so a nominally successful but incomplete package fails closed.

Relationship to install-script policy PRs

This is complementary to #60201, #51471, and #48816, which propose repository-level allowScripts declarations. Those PRs are the right place to declare trusted lifecycle scripts globally. This PR adds defense in depth at the artifact boundary:

  • if install scripts ran, the payload check succeeds and rebuild is skipped;
  • if they were blocked, the desktop build repairs the missing payload;
  • if repair is impossible, packaging fails instead of producing a broken application;
  • if an incomplete package already exists, the launcher does not trust a matching content stamp or --skip-build request.

#61395 also touches native staging, but addresses executable mode/ASAR behavior rather than a missing Linux pty.node payload.

This PR intentionally does not add an npm allowScripts policy or change TypeScript emission settings; those remain separate concerns and review surfaces.

Validation

Environment: Ubuntu Linux x86-64, host Node 22.23.1, npm 12.0.0, Electron 40.10.2 with embedded Node 24.13.0. Current stable npm 12.0.1 was also probed with an isolated empty user config and retains the blocked-script state.

  • Fresh npm installation reproduced npm 12's blocked install-script state.
  • npm run --prefix apps/desktop pack compiled/staged node-pty and completed successfully.
  • Packaged binding verified at:
    release/linux-unpacked/resources/app.asar.unpacked/dist/node_modules/node-pty/build/Release/pty.node
  • Packaged binding was non-empty (75,856 bytes) and identified as an x86-64 ELF shared object.
  • Python launcher completeness probe returned runtime_complete=True for the packaged app.
  • Electron-runtime PTY smoke loaded the packaged module, spawned /bin/sh, returned HERMES_PTY_OK, and exited 0.
  • Packaged application fake-boot smoke exited successfully without a native-module exception.
  • npm run --prefix apps/desktop test:native-deps: 14 passed, including a real Linux integration test that temporarily removes node-pty/build, invokes @electron/rebuild through the npm workspace, verifies a real pty.node, and restores the dependency tree.
  • Native tests also cover zero-byte binding/helper rejection, ARMv7 aliasing, build/prebuild runtime precedence, incomplete Darwin builds, cross-target isolation, and both staging-selection branches.
  • pytest tests/hermes_cli/test_gui_command.py -o 'addopts=' -q: 66 passed, including matching-stamp recovery, missing/empty payloads, --skip-build rejection, and post-build fail-closed behavior.
  • npm run --prefix apps/desktop typecheck: passed.
  • npm run --prefix apps/desktop lint: passed (warnings only, no errors).
  • ruff check hermes_cli/main.py tests/hermes_cli/test_gui_command.py: passed.
  • Prettier, Node syntax, and git diff --check: passed.

Platform behavior

Build/launch case Behavior
Host payload/prebuild exists and is non-empty Rebuild is skipped
Linux host payload missing Compile node-pty for the current Electron version
Payload exists but is empty/truncated to zero bytes Reject it and rebuild/fail closed
ARMv7 host (arm / armv7l) Treat aliases as the same host and stage for Node's runtime arm path
Both build and prebuild exist Stage only the payload selected by node-pty runtime precedence
Cross-target prebuild exists Stage only the matching target prebuild
Cross-target payload missing Fail with an actionable packaging error
Packaged executable exists but native payload is incomplete Ignore a matching content stamp and rebuild
--skip-build with an incomplete package Refuse to launch and print the --force-build repair command

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) area/docker Docker image, Compose, packaging P3 Low — cosmetic, nice to have labels Jul 10, 2026
@KeroZelvin
KeroZelvin marked this pull request as ready for review July 10, 2026 02:57
@KeroZelvin
KeroZelvin requested a review from a team July 10, 2026 02:57
@KeroZelvin
KeroZelvin force-pushed the fix/desktop-node-pty-blocked-scripts branch from 9988f8e to da4db56 Compare July 10, 2026 15:48
@KeroZelvin

Copy link
Copy Markdown
Contributor Author

Maintainer metadata action needed: please replace the current P3 label with P1. This is a hard Linux Desktop startup failure after an otherwise successful install/update, not a cosmetic issue.

The PR branch and body are updated at da4db56fb52c; the author account cannot manage upstream labels (GitHub returned HTTP 403). The updated branch now adds packaged-runtime completeness checks, non-empty native-payload validation, and regressions.

@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the focused native-runtime hardening. The premise holds on current main: apps/desktop/scripts/stage-native-deps.mjs:97 copies a local build only if present, while :120-126 merely warns when a target prebuild is absent; apps/desktop/package.json:21 invokes staging without a rebuild step. The launcher also treats an existing executable as sufficient in _desktop_build_needed() (hermes_cli/main.py:5075-5076), so a matching stamp can preserve an incomplete package.

The proposed rebuild, selected-payload staging, and launcher completeness checks address those verified gaps without adding a new configuration surface. @electron/rebuild supports the proposed workspace-root option (node_modules/@electron/rebuild/lib/rebuild.d.ts:90) and the PR’s native tests cover the selection and failure paths.

Automated hermes-sweeper review.

@KeroZelvin KeroZelvin changed the title fix(desktop): rebuild missing node-pty payload fix(desktop): restore Linux node-pty packaging under npm 12 Jul 10, 2026
@KeroZelvin

Copy link
Copy Markdown
Contributor Author

Reframed with verified npm compatibility evidence:

  • npm 12.0.0 was published as stable on July 8 and was latest when the stock-Hermes failure was reproduced.
  • npm 12.0.1 is now latest; an isolated 12.0.1 probe still reports Hermes' node-pty@1.1.0, Electron, esbuild, and three other dependencies blocked by the default allowScripts policy.
  • The trigger was unmodified Hermes v0.18.2 using the normal hermes updatehermes desktop path. No source patches or unsupported launch flags were used; manual rebuild steps came only afterward to recover.
  • npm's official v12 migration notice confirms dependency lifecycle/native node-gyp scripts are blocked by default unless explicitly allowed.

So the compatibility impact is: latest stable npm 12 can leave stock Hermes Desktop unlaunchable on Linux after the normal update path.

@falkoro

falkoro commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Verified this end-to-end and the severity claim holds up. Evidence:

  • node-pty@1.1.0's npm tarball ships prebuilds for darwin-arm64/x64 and win32-arm64/x64 only — no Linux payloads at all, so Linux always depends on the lifecycle-script compile that npm 12's install-script policy can block.
  • Reproduced the silent no-op on current main: with the host payload absent, node apps/desktop/scripts/rebuild-native.mjs exits 0 without producing build/Release/pty.node@electron/rebuild stops at apps/desktop/package.json while node-pty is hoisted to the workspace root (confirmed in package-lock.json). This branch's version, run in the same tree, actually compiles the payload through projectRootPath.
  • The staging selection matches node-pty's real load order in lib/utils.js (build/Releasebuild/Debugprebuilds/<platform>-<arch>), so what gets validated is what the app will load — including the darwin spawn-helper requirement and the armv7larm label fix.
  • Ran both suites locally (Linux x64, Python 3.12 / Node 22): node --test scripts/native-deps.test.mjs 14/14 (the workspace-root rebuild E2E genuinely compiles against Electron), pytest tests/hermes_cli/test_gui_command.py 66/66.

On priority: this is a deterministic, complete Desktop startup failure on Linux source installs, and the content-stamp match means hermes update can never self-repair it. That's P1 territory ("major feature broken, no workaround"), not P3.

Coordination note for maintainers: #61832 (Windows recursive cpSync crash), #61395 (spawn-helper asar path), and #60201 (allowScripts root cause) all touch this area for distinct bugs — this PR still uses cpSync(..., { recursive: true }) for the conpty dir, so if #61832's Windows finding is confirmed, whichever lands second should pick up the per-file walk there.

@alt-glitch alt-glitch added comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades and removed area/docker Docker image, Compose, packaging P3 Low — cosmetic, nice to have labels Jul 10, 2026
@KeroZelvin
KeroZelvin force-pushed the fix/desktop-node-pty-blocked-scripts branch from da4db56 to b4c306c Compare July 10, 2026 19:49
@KeroZelvin

Copy link
Copy Markdown
Contributor Author

Rebased onto current protected main at b8880f124 and resolved the apps/desktop/package.json conflict semantically:

  • preserved upstream removal of tsc -b from the production build (no source-side JS emission);
  • preserved upstream Electron TypeScript coverage in typecheck;
  • retained this PR's rebuild-native.mjs immediately before stage-native-deps.mjs;
  • retained test:native-deps.

New head: b4c306cc8. GitHub now reports the PR as mergeable.

Post-rebase verification:

  • Python Desktop launcher tests: 66 passed; Ruff passed
  • native dependency tests: 14 passed, including real missing-payload rebuild
  • renderer/shared + Electron typecheck: passed
  • Desktop lint: 0 errors (116 existing warnings)
  • Prettier, Node syntax, package-script assertions, and git diff --check: passed
  • full Electron package build: passed
  • packaged pty.node: non-empty 75,856-byte x86-64 ELF
  • launcher completeness probe: RUNTIME_COMPLETE=True
  • packaged Electron PTY smoke: HERMES_PTY_OK, exit 0

The isolated worktree is clean and local, fork, and live PR head all match b4c306cc8.

@teknium1 teknium1 added sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@KeroZelvin
KeroZelvin force-pushed the fix/desktop-node-pty-blocked-scripts branch from b4c306c to 3963a9e Compare July 11, 2026 13:48
@KeroZelvin

Copy link
Copy Markdown
Contributor Author

Fresh current-main usefulness audit + rebase completed.

I audited current protected main at c55298453 (53 commits beyond the previous PR base) before retaining the patch. The fix is still needed in full:

Current main reproduction (disposable detached worktree, empty npm user config):

  • npm 12 npm ci exits 0 while blocking six lifecycle-script packages, including node-pty@1.1.0;
  • neither node_modules/node-pty/build/Release/pty.node nor prebuilds/linux-x64/pty.node exists;
  • untouched npm run --prefix apps/desktop build exits 0 after warning the target will fail at runtime;
  • the staged Desktop runtime still contains no usable Linux native payload.

Current main also still lacks the PR's workspace-root rebuild, runtime-precedence selection, non-empty validation, fail-closed staging, cross-target/ARM protections, packaged-runtime completeness/freshness guard, --skip-build refusal, and focused native regression/CI coverage. No PR behavior has been superseded.

The only overlapping upstream change was fc977f62b, which added prebuild: tsc -b . --clean to remove stale generated JS. The rebase preserves that cleanup and adds rebuild-native.mjs immediately before stage-native-deps.mjs.

New head: 3963a9e20 on current main c55298453. GitHub now reports the PR mergeable.

Post-rebase proof from the same npm 12 blocked-script state:

  • fresh npm ci: blocked state confirmed, no Linux payload before build
  • Python launcher tests: 66 passed; Ruff passed
  • native dependency tests: 14 passed, including real missing-payload rebuild
  • Desktop renderer/shared + Electron typecheck: passed
  • lint: 0 errors (119 current-main warnings)
  • Prettier, Node syntax, exact package-script union, and git diff --check: passed
  • full Electron package build: passed
  • packaged pty.node: non-empty 75,856-byte x86-64 ELF
  • launcher completeness: RUNTIME_COMPLETE=True
  • packaged Electron PTY: HERMES_PTY_OK, exit 0
  • worktree clean; local, fork, raw remote, and live PR head all match 3963a9e20.

@alt-glitch alt-glitch added area/docker Docker image, Compose, packaging P2 Medium — degraded but workaround exists and removed P1 High — major feature broken, no workaround sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 11, 2026
@teknium1 teknium1 added the sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows label Jul 11, 2026
@alt-glitch alt-glitch added area/docker Docker image, Compose, packaging and removed sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows area/docker Docker image, Compose, packaging labels Jul 11, 2026
@teknium1 teknium1 added the sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows label Jul 11, 2026
@alt-glitch alt-glitch added area/docker Docker image, Compose, packaging and removed area/docker Docker image, Compose, packaging labels Jul 11, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Canonical fix PR for the npm 12 / Linux node-pty Desktop-launch family. Related: #62171 (bug spec), #62462 (dup of #62171), #62564 (closed build-validation fix, different mechanism). Human pick needed between this PR's workspace-root rebuild-traversal and #62564's build-time payload validation.

@vawaver

vawaver commented Jul 13, 2026

Copy link
Copy Markdown

Hi everyone,

I'm relatively new to the Hermes GitHub repository, so I’m not entirely familiar with the usual release cycle. I wanted to ask how long it typically takes for bug fixes to be merged into main and made available in the official updates.

My report for the same issue can be found here: #62462.

The bug has been affecting my workflow for the past 3 days, as hermes desktop is currently unusable for me, and working through the Hermes Console as a temporary workaround is quite inconvenient.

I see that this PR addresses the issue, so I would appreciate any insight into the timeline for the merge and release.

Glad for your time and effort on this project!

@alt-glitch alt-glitch added area/docker Docker image, Compose, packaging and removed area/docker Docker image, Compose, packaging sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 13, 2026
@KeroZelvin

Copy link
Copy Markdown
Contributor Author

@vawaver, I'm resolving the current conflict right now. To get rolling in the short term you can point your agent to this PR and ask it to implement the fix now (before the PR is accepted in main) locally.

Preserve node-pty loader fallbacks, validate target binary format and architecture, rebuild host bindings for Electron, fail universal packages closed, and keep native helper/retry behavior safe across platforms.
@KeroZelvin
KeroZelvin force-pushed the fix/desktop-node-pty-blocked-scripts branch from ffd1b58 to e8561e5 Compare July 13, 2026 14:08
@KeroZelvin

Copy link
Copy Markdown
Contributor Author

Revisited and rebased again onto current protected main (bd740f203b44237dbc5c27a2de4d86ef32af4dde). New PR head: e8561e52665887458a9f6d2d39e14b003d161001.

Conflict resolution

  • Resolved the only conflict in apps/desktop/package.json behaviorally.
  • Preserved upstream's new electron/wsl-path-bridge.test.ts platform coverage.
  • Preserved prebuild: tsc -b . --clean, the expanded Electron typecheck, rebuild-native -> stage-native-deps, and both native/before-pack regression suites.
  • Range-diff shows commits 2/3 patch-identical; commits 1/4 differ only by retaining the upstream WSL test.

Current-main usefulness check

Post-rebase verification

  • native/packaging tests: 24 passed
  • focused launcher tests: 71 passed; Ruff passed
  • Desktop typecheck passed
  • Node syntax, Prettier, package-script assertions, and git diff --check passed
  • full Electron package build from the blocked-script state passed
  • packaged binding: 75,856-byte x86-64 ELF
  • packaged Electron PTY smoke: HERMES_PTY_OK, exit 0
  • launcher probe: RUNTIME_COMPLETE=True
  • local/tracking/raw-remote/live-PR heads match; branch 0 behind / 4 ahead; worktree clean

Desktop lint currently reports four errors inherited unchanged from current main (electron/main.ts, cron UI, clarify test, icons); none of those files are changed by this PR.

GitHub now reports MERGEABLE + BLOCKED; the conflict is resolved and the remaining block is review/policy.

@KeroZelvin

Copy link
Copy Markdown
Contributor Author

Closing this PR as superseded by current main, not because the original report or implementation was invalid.

When #61798 was opened and during its earlier rebases, stock npm 12 could complete npm ci and the Desktop build with node-pty@1.1.0's lifecycle scripts blocked, leaving no Linux native payload and an app that failed on first PTY use. The PR was necessary against those bases.

Since then, current main independently landed the core packaging behavior through:

  • c008f41bb — rebuild node-pty from staging when no native binary exists;
  • 7a44a8fde — reject wrong-platform cross-target payloads and pass target arch to electron-rebuild;
  • 7fdae5d22 — preserve executable spawn-helper mode;
  • 5d691374c — recognize real little-endian Mach-O payloads;
  • 47d56b802 / 955c5b73c / 7b3f3047a — migrate script tests to Vitest and replace the old Typecheck workflow with the current JS check architecture.

I re-audited protected main at 861d69c7bba8d2ea6a1cd170e989c901c74d32d1; current protected main is now c7e09f25716764b2e4dacf518f1c553a497c15d7, whose additional change is unrelated to native packaging. Fresh proof with an empty npm user config:

  1. npm ci exited 0 while npm 12 blocked six lifecycle-script packages, including node-pty@1.1.0.
  2. No Release, Debug, or linux-x64 prebuild payload existed after install.
  3. Untouched current main's npm run --prefix apps/desktop pack invoked the staging rebuild and completed successfully.
  4. The packaged binding was a 75,856-byte x86-64 ELF.
  5. A real PTY spawn through the packaged Electron runtime returned CURRENT_MAIN_PTY_OK.

Therefore, rebasing the old four-commit branch again would mostly reintroduce superseded staging code, a deleted CI workflow, and pre-Vitest test integration. That would add conflict/review burden without restoring missing core behavior.

One narrower defense-in-depth opportunity remains: current main considers an existing stamped package current even if its packaged pty.node is later missing (CURRENT_MAIN_BUILD_NEEDED_WITH_MISSING_PTY=False). I am not opening another PR for that now; it can be proposed separately as a compact launcher-completeness change if maintainers want it.

Issue #62171 is intentionally left untouched for maintainer triage against the now-landed current-main commits. Thank you to the maintainers whose overlapping fixes made this PR unnecessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/desktop Electron desktop app (apps/desktop/*) P1 High — major feature broken, no workaround sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Latest stable npm 12 breaks Hermes Desktop on Linux after hermes update

5 participants