The macOS digest-order test emitted the strict PM failure from its expected
negative case into the shared Vitest log. That traceback looked like a second
packaging failure even though the test passed.
Run corrupt and missing fact checks in a captured child process. Keep the PM
reader strict, verify both errors, and retain the real builder ordering proof
for valid payload facts.
Windows VERSIONINFO and the MSIX package version were two derivations of
one fact, and the sideload one counted minutes since the last stable, which
overflows a 16-bit field after 45 days. Both now come from
storePackageVersionAt's year.hourOfYear.secondOfHour.0, so a later build
always sorts above an earlier one and the cap has nothing left to cap.
before-build.mjs builds the msix-extensions.xml fragment and the hidden
CLI <Application> entries from string templates, interpolating the
display name and the payload-declared launcher stems raw. A value with
`&`, `<` or `"` would corrupt the manifest makeappx reads (an opaque
0x80080204 at best, a differently named alias at worst). Every
interpolated attribute now goes through xmlAttribute().
Merge origin/main at 8e806ae1b2. Keep native helper compilation in
prepareDesktopNativeDependencies and keep bundling/beforePack consume-only.
Bind helper sources and headers into preparation identities and cache keys;
copy admitted executable resources beside node_modules and preserve signing
semantics in product freshness checks.
Verified desktop typecheck, focused native/packaging/UI and gateway/cache
tests, and the real Linux preparation/copy/Xvfb execution path. Incoming
upstream anti-slop findings remain unchanged; no baseline was raised.
- check_no_tmp_literals: resolve scratch via tempfile/os.tmpdir; the termux
container mount point is one marked variable per script
- ruff TID251: desktop E2E fixtures may reach PM internals like tests do;
the pm.runtime_stage ban message no longer names a module that never existed
- auth_codex: build the capped httpx stream subclass on first use so importing
hermes_cli.auth_codex no longer forces httpx (the lazy proxy in auth_constants
was defeated by a module-scope base class; broke lanes without httpx)
- desktop-smoke: launchApp is a parameter; the bundle-env test substitutes a
refusing launcher instead of letting Playwright spawn a dying binary
(3 unhandled rejections failed the tests-js lane)
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim
salvage + its test and workflow steps dropped), the shim re-exec stays retired,
package.json carries no electron-builder block (afterExtract identity stamp wired
into electron-builder.config.cjs instead; after-pack.mjs keeps signing only),
Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap
(export_scratch_tmp_env) is taken and re-run after profile resolution.
Throwaway HERMES_HOME, perf JSON/cpuprofile outputs and typing-lag profiles now land in the Hermes scratch dir; the CONTRIBUTING anti-pattern mention keeps its literal with a no-tmp marker.
Eval probes wrote fixtures, receipts and evidence to hard-coded /tmp paths and
two live A/B tasks literally instructed the model to work in /tmp. They now
derive locations from tempfile.gettempdir() / os.tmpdir() (env overrides kept),
usage examples use relative output names, and the desktop e2e screenshot dirs,
perf scripts and the short-session repro fixture stop naming /tmp. Also adds
the explicit encoding= the windows-footgun check wants in the touched files.
An interrupted extract leaves node_modules/get-windows without package.json
and npm never revisits an existing directory, so the tree stayed broken on
every later update until a manual `npm install get-windows`. Delete such a
dir (workspace hoist or app-local copy) in _install_desktop_workspace_deps
before npm runs so the same update re-extracts it; the staging repair hint
now points at `hermes desktop --force-build` instead of the manual install.
Also drops the unused `exists` injection on findHalfInstalledGetWindowsDir
and the stale "exported for tests" note on missingGetWindowsWarning.
The unresolvable-package case already degraded, but a half-extracted install
(#90829) that keeps package.json while losing lib/binding (win32) or the
macOS helper (darwin) still threw from stageGetWindowsInto and before-pack
rethrew, so the Desktop rebuild died on the optional dep anyway. Stage the
fail-soft JS surface without a binding on win32 (the win32-arm64 precedent),
skip staging on darwin without the helper, and treat a failing native
installer the same way; the runtime reports read_window_below as unavailable
in every one of those states. The classify gate still refuses a present
binary compiled for the wrong platform.
Follow-up to the cherry-picked #109251 (which stops a missing optional
get-windows from killing `npm run build` on win32-x64/darwin):
- The reporter's install (#90829) was not "npm skipped an optional dep" but a
Windows in-place update interrupted by a running Desktop/gateway
(TAR_ENTRY_ERROR): node_modules/get-windows exists with its binding but no
package.json, so require.resolve fails while npm never revisits the
directory. Degrading silently would leave read_window_below dead forever.
`findHalfInstalledGetWindowsDir` walks the same node_modules ancestors
require.resolve does; when the dir is there the warning names it and the
repair (`npm install get-windows --save-exact` in apps/desktop, then
`hermes desktop --force-build`).
- Warning text lives in `missingGetWindowsWarning` (pure, exported) and the
locator is injectable, so tests assert behaviour instead of source text.
- Tests: the old per-platform "fails when absent" cases are replaced by one
every-platform degrade invariant and one half-install → repair-hint case
(both red on origin/main). Docs: desktop.md build-troubleshooting note.
Follow-up to the salvaged #106846 commit (@JoaoMarcos44):
- after-extract.mjs: spell out WHY the stamp moved (electron-builder's
beforeCopyExtraFiles rebuilds the PE with resedit for the ELECTRONASAR
resource; rcedit then cannot commit to that exe, deterministically —
#105629), and why disableAsarIntegrity was not taken.
- after-extract.test.mjs: two invariants — the hook wiring (afterExtract set,
afterPack unset, ASAR integrity still on) and the stamp target
(electron.exe on win32, nothing on other platforms). Red on origin/main.
- set-exe-identity.mjs / scripts/install.ps1: comments still named the
afterPack hook / after-pack.mjs.
The builder takes an optional `platform` (default process.platform) so the
argv test can drive the darwin and non-darwin branches explicitly. The test
no longer redefines process.platform via Object.defineProperty.
The salvaged test returned early off macOS, so Linux CI never executed the
assertion. Stub process.platform to 'darwin' (execFileSync is already mocked)
so the `--sdk macosx clang` argv contract is checked wherever vitest runs, and
add the off-macOS no-op as the control case.
A bare xcrun clang inherits the host default SDK, which may be newer than
the active linker (unknown-arch .tbd stubs at link time). Naming --sdk
macosx explicitly forces the driver and linker to agree on one SDK.
Fixes#113708.
(cherry picked from commit d5fcb6c6ab3de96602528dade2d7f75b7e47ddd5)
prepareDesktopNativeDependencies called a bare `rmSync` that the file never
imported (every sibling here uses `fs.rmSync`), so `npm run build` in
apps/desktop died with "ReferenceError: rmSync is not defined" at
stage-native-deps.mjs:696. That fails EVERY desktop build, which is why the
install e2e's products stage reported "[hermes] app products or command
publication failed" and install.sh exited 1 at HEAD.
Proof: the covering native case in tests-js/desktop-builder.test.mjs is red on
base with exactly that ReferenceError and green with the import.
* refactor(desktop): extract titleBarOverlayOptions into a tested helper
getTitleBarOverlayOptions in main.ts branched inline on mac/windows/wsl.
Move the decision into titlebar-overlay-width.ts so the WSLg → false rule
(renderer paints its own controls there) is unit-tested next to the width
reservation it pairs with. main.ts keeps a thin caller.
Co-authored-by: null-runner <nicholas.mariani@hotmail.it>
* fix(desktop): renderer-drawn window controls on WSLg
Under WSLg the frameless window (titleBarStyle: 'hidden') had no
minimize/maximize/close. getTitleBarOverlayOptions returned false on the
premise that the RDP host paints replacement controls; it does not for a
frameless window. Electron's native overlay is not the fix either: its
cluster's hit-region drifts from the rendered buttons under the RAIL
compositor.
The renderer now paints Windows-style min/max/close (wslg-window-controls)
routed over a hermes:window-control IPC channel (window-controls.ts →
preload → main). getWindowState reports customWindowControls/isMaximized so
the renderer knows when to mount them and which glyph to show. The cluster
is pinned to TITLEBAR_HEIGHT px (the contrib shell zeroes --titlebar-height
for content subtrees) with 46px native-width caption buttons.
Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
* fix(desktop): wire WSLg window-control IPC and snap maximize onto the work area
Register hermes:window-control in main, report windowControlState from
getWindowState, and re-send window state on maximize/unmaximize so the
renderer's maximize/restore glyph tracks the window.
maximizedBoundsCorrection snaps a WSLg-maximized frameless window back onto
the display work area (RAIL can settle it offset — reported on WSLg 1.0.65).
It is a no-op wherever native maximize already fills the work area, so
healthy compositors are never fought and setBounds cannot loop.
Co-authored-by: null-runner <nicholas.mariani@hotmail.it>
* fix(desktop): call WSLg window-control bridge without the click event
contextBridge structured-clones every argument, and a React SyntheticEvent
is not cloneable, so onClick={controls.minimize} threw "An object could
not be cloned" before ipcRenderer.send ran. The buttons rendered but did
nothing. Wrap the handlers so the bridge is called with no arguments, and
pin that in the test.
* fix(desktop): remove recursive WSLg maximize bounds correction
* fix(desktop): select native Wayland before Electron initialization on WSLg
* fix(desktop): preserve ready pipe and debugger across WSLg launch
* fix(desktop): keep WSLg caption controls scoped to every window
* style(desktop): order window chrome event import
---------
Co-authored-by: null-runner <nicholas.mariani@hotmail.it>
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
apps/desktop/scripts/set-exe-identity.mjs::stampExeIdentity retried every rcedit
rejection with 0.5/1/2 s backoff. A failure to SPAWN rcedit (bin/rcedit-x64.exe
missing or not executable) is permanent, so the only effect was a 3.5 s delay
before after-pack.mjs's warning.
The npm rcedit wrapper (@malept/cross-spawn-promise) reports a spawn failure as
CrossSpawnError with the errno on `originalError.code`; a non-zero rcedit exit
("Unable to commit changes") is an ExitCodeError with a numeric `code`. Skip the
retry loop when `originalError.code ?? code` is ENOENT/EACCES and rethrow as-is.
Probe: stub rcedit rejecting with that shape -> before 4 attempts, sleeps
[500,1000,2000]; after 1 attempt, no sleeps. Transient-lock retry unchanged.
Part of #112544 (optional review atom).
Preview-pane guest pages (Streamlit's traceback "Ask Google" / "Ask …"
buttons, plain `<a target="_blank">` anchors) could not open anything: the
`<webview>` has no `allowpopups`, so Chromium drops the popup before any
handler runs. Opening from `setWindowOpenHandler` is banned
(GHSA-9f4c-93c8-jc8g, window-open-policy.ts), so this adds an explicit
click bridge instead:
- main.ts installs a guest preload through `will-attach-webview`, keyed on
the `persist:hermes-preview` partition only.
- The preload forwards a clicked `_blank` anchor's resolved href to the
host renderer via `ipcRenderer.sendToHost`; it opens nothing itself.
- PreviewPane admits the URL and routes it through the existing audited
`hermes:openExternal` IPC.
Salvaged from PR #112959 (squash of its two commits).
Fixes#112941
The salvaged commits drop the cpSync/rmSync imports, but stageGetWindowsInto
landed on main after the PR was opened and still called both, so the module
threw ReferenceError on every platform (7 vitest failures on the cherry-picked
head). Route its six sites through copyFileSync/removeDirSync so get-windows
staging survives the same non-ASCII profile paths as node-pty.
preserveRollbackBackup had the same rmSync no-op as cleanStaleAppOutDir: a
surviving .bak makes renameSync fail, so the previous working build gets wiped
instead of kept as rollback material. Same existsSync -> removeDirSync fallback.
Earlier fixes for the same crash: #60480 (@liuhao1024), #61832 (@danilofalcao),
#76211, #103458, #109273, #111590.
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: danilofalcao <danilofalcao@users.noreply.github.com>
Review follow-ups to the previous commit:
- before-pack.mjs: cleanStaleAppOutDir kept the retrying rmSync (its
EBUSY resilience is worth keeping) but now verifies the tree is gone
and falls back to the libuv-backed removeDirSync when the native
rmSync silently no-ops on a non-ASCII path — previously it logged
'removed stale unpacked dir' while the stale tree survived.
- removeDirSync: export it for reuse; handle a plain file or symlink at
the path (rm -rf semantics) instead of throwing ENOTDIR, and tolerate
broken symlinks via lstat.
- Add a source-level guard test: repo CI runs on Linux where the native
implementations happen to work, so the accented staging test cannot
catch a cpSync/rmSync reintroduction there.
- Link the upstream Node issues in the banner (nodejs/node#61878, fixed
in v24.15.0; nodejs/node#56049, fixed in v24.13.1) and stop
attributing the native rewrite to Node 24 — only the observation was
on v24.11.1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Node 24's native rewrite of fs.cpSync/fs.rmSync mishandles non-ASCII
Windows paths (observed on v24.11.1 with an accented Windows user name,
i.e. the default %LOCALAPPDATA%\hermes install home):
- recursive cpSync fails with EIO "Access is denied" (errno 5) or
hard-crashes the process (0xC0000409),
- cpSync over an existing file fails with a bogus errno-0
'unlink' / "The operation completed successfully" error,
- rmSync({recursive, force}) silently deletes nothing, so the
half-staged dist/node_modules/node-pty tree poisons every retry.
In practice this bricked the desktop build (and thus install/update)
for every Windows user whose profile path contains accented characters:
stage-native-deps.mjs died copying the conpty prebuild, and each rerun
failed earlier on the stale tree the silent rmSync left behind.
Replace all cpSync/rmSync uses with libuv-backed primitives that handle
these paths correctly on the same node.exe: copyFileSync for single
files, a manual mkdirSync+readdirSync+copyFileSync walk for directory
copies, and a manual unlinkSync/rmdirSync walk (verified with existsSync
afterwards, failing loudly instead of silently) for the dest cleanup.
Add a regression test that stages a fake node-pty tree - including a
nested conpty/ payload - into an accented src/dest path twice, so the
restage exercises the delete-then-recopy path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first cut derived the package from `binding-${platform}-${arch}` with an
exact-suffix match, which never matches Windows (`-msvc`) or Linux
(`-gnu`/`-musl`) names, so the repair only ever worked on macOS and
install.sh had to gate it there. Rolldown's own loader already resolves
platform, arch and libc and prints the exact `@rolldown/binding-*` it wanted
in its error chain; parse that instead and drop the gate. Also spawn npm
through a shell on Windows (Node refuses to spawn npm.cmd directly) and trim
the tests to the two invariants (no-op when it loads; installs exactly what
the loader asked for, then re-probes).
The field report saw the commit still failing 5 s after a first attempt,
so 100/300 ms could exhaust before the scanner released the exe. Use
500 ms / 1 s / 2 s (four attempts, 3.5 s worst case) and retry every
rcedit failure instead of one error-message shape: a held handle can also
surface as a load/open error, and a permanent failure only costs 3.5 s
before after-pack.mjs swallows it.
Windows desktop rebuilds intermittently hit rcedit's `Fatal error: Unable
to commit changes` while a real-time file scanner holds the freshly packed
Hermes.exe; `stampExeIdentity` called rcedit exactly once, so the stamp
failed on the first lock. Retry the commit with bounded backoff before
surfacing the original error.
Trimmed from #112552: the after-pack.mjs injection seam and its test —
the hook's swallow-and-warn is pre-existing behaviour on main and needs no
change (the reporter's "Could not install the rebuilt desktop app" comes
from hermes_cli/main_desktop.py::_swap_staged_desktop_app's directory
rename, not from the stamp).
Commit desktop bundles can bake environment defaults/clears (--bundle-unset
HERMES_DESKTOP_USER_DATA_DIR, HERMES_HOME=null) that stomp the smoke driver's
--home/--user-data pin, so every native smoke threw "Desktop did not honor the
isolated home and userData directories".
Instead of pinning, the driver replays the bundle env over its launch env
through the same resolver the app runs, seeds the predicted home (bailing
rather than wiping when it is not empty), and verifies the app landed there
via a new hermesHome report on the version bridge. The --user-data equality
check now applies only when the artifact bakes no env.
- Extract the pure path resolver into electron/data-paths.mjs (data-paths.ts
is now a typed re-export) so Node's type-stripped driver can import it.
- Add applyBundleEnvironment/validateBundleEnvironment as the pure twin of the
bundle banner, pinned by a lockstep test.
- Record bundleEnv in the install stamp and add readBundledBundleEnv.
- Report hermesHome from hermes:version and assert it equals the predicted home.
Apple's notary extracts every zip in the submitted archive and validates
each Mach-O; the notarization failure (err 4000) traced to uv-cache wheel
zips whose native members were never signed — electron-osx-sign reaches
only the extracted archive buckets, not zip members.
signWheelZipMembers (afterPack, darwin) walks the payload's uv-cache
wheel zips, signs .so/.dylib members with the same Developer ID identity
and keychain the chromium signer resolves, repacks, then re-extracts and
runs codesign --verify --strict on every signed member — the same
predicate the notary applies — failing the build loudly if a host's zip
semantics ever drop the embedded signature.
Proven on a real macOS 26 host: codesign may write signatures to extended
attributes for ad-hoc/generic binaries, and plain zip repack silently
drops xattrs; the verify gate converts that host behavior from a silent
notary rejection into a same-run build failure.
Channel identities splice a 16-hex token into artifactNamePascal
(HermesChannel<token>), so Id="${artifactNamePascal}CopilotKeyProvider"
reached 47 chars and makeappx rejects the manifest with an opaque
maxLength 0x80080204. The Id only discriminates within the package;
DisplayName already carries the variant name, so pin the literal.