From 33b1d144590a211100f42aa911fd7f91ba031507 Mon Sep 17 00:00:00 2001 From: xxxigm <54813621+xxxigm@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:42:30 +0700 Subject: [PATCH] fix(desktop): pin Electron below the broken native extract-zip install (#47792) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(desktop): pin Electron below the broken native extract-zip install The Windows desktop install fails at "Building desktop app": Electron's postinstall aborts with `ERR_DLOPEN_FAILED loading index.win32-x64-msvc.node` / "Cannot find native binding" from `@electron-internal/extract-zip`. Root cause is a dependency drift, not the user's machine. Electron changed its install mechanism mid-patch-series: electron 40.9.3 .. 40.10.2 -> @electron/get@^2 + extract-zip@^2 (pure JS) electron 40.10.3 / 40.10.4 -> @electron/get@^5 + @electron-internal/extract-zip@^1 (native napi) apps/desktop declares `electronVersion: 40.9.3` (the tested, JS-extract build) but pinned the dependency as `electron: ^40.9.3`, so `npm ci`/`npm install` silently resolved 40.10.3/40.10.4 — onto the brand-new native extract-zip whose win32-x64 binding fails to dlopen on some Windows hosts. The committed lockfile already carried 40.10.3, and the installer's mirror fallback can't help (it re-runs Electron's own `install.js`, which uses the same broken native module). Fix: - Pin `electron` to an exact `40.10.2` — the newest build before the native extract-zip switch — and align `build.electronVersion` to match (Electron Builder needs electronVersion/electronDist to match the installed binary). - Add a root `yauzl: ^3.3.1` override so the (re-introduced) JS extract-zip path also works on Node >= 24.16 / >= 26.1, where the old yauzl hangs. This is the same workaround the wider Electron ecosystem adopted. - Regenerate package-lock.json: drops @electron-internal/extract-zip and @electron/get@5, restores @electron/get@2 + extract-zip@2 + yauzl@3.4.0. * test(desktop): lock the Electron pin/version/lockfile consistency contract Guards against the dependency drift that broke the Windows desktop install: the Electron dependency must be an exact version, must equal build.electronVersion, and the lockfile must resolve to that same version so `npm ci` installs exactly what electron-builder packages. Asserts the relationships, not a specific version number. --- apps/desktop/package.json | 4 +- package-lock.json | 224 +++++++++++++++++++---------- package.json | 3 +- tests/test_desktop_electron_pin.py | 96 +++++++++++++ 4 files changed, 249 insertions(+), 78 deletions(-) create mode 100644 tests/test_desktop_electron_pin.py diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 08080188a53..5a3df77e91d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -117,7 +117,7 @@ "@vitejs/plugin-react": "^6.0.1", "concurrently": "^10.0.3", "cross-env": "^10.1.0", - "electron": "^40.9.3", + "electron": "40.10.2", "electron-builder": "^26.8.1", "eslint": "^9.39.4", "eslint-plugin-perfectionist": "^5.9.0", @@ -134,7 +134,7 @@ "wait-on": "^9.0.5" }, "build": { - "electronVersion": "40.9.3", + "electronVersion": "40.10.2", "electronDist": "../../node_modules/electron/dist", "appId": "com.nousresearch.hermes", "productName": "Hermes", diff --git a/package-lock.json b/package-lock.json index 5658a679592..8f95ffeee80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,7 @@ "@vitejs/plugin-react": "^6.0.1", "concurrently": "^10.0.3", "cross-env": "^10.1.0", - "electron": "^40.9.3", + "electron": "40.10.2", "electron-builder": "^26.8.1", "eslint": "^9.39.4", "eslint-plugin-perfectionist": "^5.9.0", @@ -151,6 +151,28 @@ "node": "^20.19.0 || >=22.12.0" } }, + "apps/desktop/node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, "apps/desktop/node_modules/@nous-research/ui": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/@nous-research/ui/-/ui-0.13.2.tgz", @@ -194,6 +216,80 @@ } } }, + "apps/desktop/node_modules/electron": { + "version": "40.10.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-40.10.2.tgz", + "integrity": "sha512-Xj3Hy0Imbu4g0gDIW55w/jJYz94nMO2JRSGYA3LyAn5SwaERCelgZrA21vfH+Bi//SWAWQXddHsMwCqauyMT8g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "apps/desktop/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "apps/desktop/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "apps/desktop/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "apps/desktop/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "apps/desktop/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "apps/shared": { "name": "@hermes/shared", "version": "0.0.0", @@ -935,16 +1031,6 @@ "react": ">=16.8.0" } }, - "node_modules/@electron-internal/extract-zip": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.3.tgz", - "integrity": "sha512-OjKpjB7gohtEjZiq6nDx1egqjZJhGPN1iFOIED+NFhB/MMkXw/XRcHjh1DGXKT5z2W9eW7Jy2UKU3gpjvusFTQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=22.12.0" - } - }, "node_modules/@electron/asar": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", @@ -1081,38 +1167,6 @@ "node": ">=8" } }, - "node_modules/@electron/get": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", - "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^3.0.0", - "graceful-fs": "^4.2.11", - "progress": "^2.0.3", - "semver": "^7.6.3", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=22.12.0" - }, - "optionalDependencies": { - "undici": "^7.24.4" - } - }, - "node_modules/@electron/get/node_modules/undici": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", - "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/@electron/notarize": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", @@ -5778,6 +5832,17 @@ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", @@ -8795,25 +8860,6 @@ "node": ">=0.10.0" } }, - "node_modules/electron": { - "version": "40.10.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-40.10.3.tgz", - "integrity": "sha512-DdWRsHm4j5wH9TMcfnB2Dqx44G/6BgLKSG/oeRe9kS60pfqCUwzUkHk0ClwvZzBVXtJ1kcdkHVRrJsl1ooKp+g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron-internal/extract-zip": "^1.0.1", - "@electron/get": "^5.0.0", - "@types/node": "^24.9.0" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 22.12.0" - } - }, "node_modules/electron-builder": { "version": "26.15.3", "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", @@ -9178,19 +9224,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -9968,6 +10001,27 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -14504,6 +14558,13 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -18437,6 +18498,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index eebd955e417..9e9448497f6 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ }, "overrides": { "lodash": "4.18.1", - "@assistant-ui/store": "0.2.13" + "@assistant-ui/store": "0.2.13", + "yauzl": "^3.3.1" }, "engines": { "node": ">=20.0.0" diff --git a/tests/test_desktop_electron_pin.py b/tests/test_desktop_electron_pin.py new file mode 100644 index 00000000000..e1a0d2e88e7 --- /dev/null +++ b/tests/test_desktop_electron_pin.py @@ -0,0 +1,96 @@ +"""Regression: the desktop Electron dependency must be an exact, consistent pin. + +The Windows desktop install failed at "Building desktop app" because Electron +changed its install mechanism mid patch-series: + + electron 40.9.3 .. 40.10.2 -> @electron/get@^2 + extract-zip@^2 (pure JS) + electron 40.10.3 / 40.10.4 -> @electron/get@^5 + + @electron-internal/extract-zip@^1 (native napi) + +``apps/desktop/package.json`` declared ``electronVersion: 40.9.3`` (the tested, +JS-extract build) but pinned the dependency loosely as ``electron: ^40.9.3``. +``npm ci`` then resolved 40.10.3/40.10.4 — the new *native* extract-zip whose +win32-x64 binding fails to ``dlopen`` on some Windows hosts +(``ERR_DLOPEN_FAILED loading index.win32-x64-msvc.node``). + +These tests lock the contract that prevents that drift, without hard-coding the +specific version (which is allowed to move): + +1. the Electron dependency is an *exact* version (Electron Builder needs the + installed binary to match ``electronVersion`` / ``electronDist``), and +2. the dependency, ``build.electronVersion``, and the resolved lockfile entry + all agree — so ``npm ci`` installs exactly what the build packages. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parent.parent +DESKTOP_PKG = REPO_ROOT / "apps" / "desktop" / "package.json" +ROOT_LOCK = REPO_ROOT / "package-lock.json" + +# An exact semver: digits.digits.digits with an optional prerelease/build tag, +# but NO range operators (^ ~ > < = * x || spaces || -range). +_EXACT_SEMVER = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$") + + +def _desktop_pkg() -> dict: + assert DESKTOP_PKG.is_file(), f"missing {DESKTOP_PKG}" + return json.loads(DESKTOP_PKG.read_text(encoding="utf-8")) + + +def _electron_spec(pkg: dict) -> str: + for section in ("dependencies", "devDependencies"): + spec = pkg.get(section, {}).get("electron") + if spec: + return spec + pytest.fail("electron is not listed in apps/desktop dependencies") + + +def test_electron_dependency_is_exactly_pinned(): + """A loose range lets npm drift onto an Electron with a different installer.""" + spec = _electron_spec(_desktop_pkg()) + assert _EXACT_SEMVER.match(spec), ( + f"electron must be pinned to an exact version, got {spec!r}. " + "A range (^/~) lets npm ci resolve a newer Electron whose postinstall " + "may differ from the one the build was validated against." + ) + + +def test_electron_dependency_matches_electron_version(): + """electron-builder packages build.electronVersion against the installed binary.""" + pkg = _desktop_pkg() + spec = _electron_spec(pkg) + builder_version = pkg.get("build", {}).get("electronVersion") + assert builder_version, "build.electronVersion is missing" + assert spec == builder_version, ( + f"electron dependency ({spec!r}) must equal build.electronVersion " + f"({builder_version!r}); otherwise electron-builder packages a different " + "version than npm installs into electronDist." + ) + + +def test_lockfile_resolves_the_pinned_electron(): + """npm ci installs from the lockfile, so it must agree with the pin.""" + if not ROOT_LOCK.is_file(): + pytest.skip("root package-lock.json not present") + spec = _electron_spec(_desktop_pkg()) + lock = json.loads(ROOT_LOCK.read_text(encoding="utf-8")) + packages = lock.get("packages", {}) + resolved = [ + meta.get("version") + for path, meta in packages.items() + if path.endswith("node_modules/electron") and meta.get("version") + ] + assert resolved, "no electron entry found in package-lock.json" + assert all(v == spec for v in resolved), ( + f"package-lock.json resolves electron to {sorted(set(resolved))}, " + f"but the pin is {spec!r}; run `npm install --package-lock-only` so " + "`npm ci` stays consistent." + )