Skip to content

Fix Windows ARM64 installer payload corruption and modernize build - #2

Merged
FinnaXxx merged 4 commits into
mainfrom
claude/electron-bcj2-nsis-compat-mf6nap
Aug 26, 2026
Merged

Fix Windows ARM64 installer payload corruption and modernize build#2
FinnaXxx merged 4 commits into
mainfrom
claude/electron-bcj2-nsis-compat-mf6nap

Conversation

@FinnaXxx

Copy link
Copy Markdown
Owner

Summary

This PR fixes a critical issue where Windows ARM64 installers silently ship without native binaries, modernizes the build toolchain, and adds verification to catch similar issues in the future.

The Problem: electron-builder 26 compresses app payloads with 7-Zip 24.09, which automatically applies the ARM64 filter to ARM64 binaries. However, the bundled NSIS installer still uses the Nsis7z plugin from 2019, whose decoder predates the ARM64 filter (introduced in 7-Zip 23.01). When the installer runs, it silently fails to extract nine native binaries (the app exe, d3dcompiler_47, dxcompiler, dxil, ffmpeg, libEGL, libGLESv2, vk_swiftshader, and vulkan-1), leaving a broken app that appears to install successfully.

The Solution:

  1. New scripts/electron-builder.cjs: Wraps electron-builder and sets ELECTRON_BUILDER_7Z_FILTER=BCJ for Windows ARM64 builds. BCJ is an x86 filter that every 7z decoder understands, including the 2019 NSIS plugin. It costs ~15% on ARM64 binaries but ensures they extract correctly.

  2. New scripts/verify-windows-installer.cjs: Runs after all artifacts are built and re-tests each Windows installer with 7zip-bin (which ships 7-Zip 16.02, older than the NSIS plugin). It verifies:

    • The embedded 7z archive can be extracted
    • Only decodable compression methods are used (rejects ARM64, RISCV, etc.)
    • The product exe and at least 5 DLLs are present
    • All CRCs pass
  3. Removed scripts/rebuild-native.cjs: No longer needed. better-sqlite3 is a Node-API addon that ships prebuilt binaries for all platforms, so rebuilding against Electron headers produces only a host-only copy that nothing loads.

  4. Updated build configuration:

    • Set npmRebuild: false in electron-builder.yml (better-sqlite3 doesn't need rebuilding)
    • Added platform-specific file filters to exclude irrelevant prebuilt binaries from each package (~17MB savings per build)
    • Updated CI workflows to use the new wrapper script and build real Windows installers for verification
  5. Modernized dependencies:

    • Node.js 22 → 24
    • Electron 33 → 43 (Node 24.18 + Chromium 150)
    • electron-builder 25 → 26
    • electron-vite 3 → 5
    • vite 6 → 7
    • better-sqlite3 11 → 13
    • Added 7zip-bin 5.2.0 for verification
  6. Build script simplification: Removed manual native rebuilds from build:win-arm64 and all other build commands. The wrapper script handles filter selection automatically.

Verification

  • pnpm check passes
  • New verification script catches the ARM64 filter issue (would fail on old code)
  • CI updated to build and verify real Windows installers on every commit
  • No changes to cleanup logic or targets

Cleanup safety

  • This change does not expand what the app can delete
  • No cleanup-related code was modified

Privacy

  • No credentials, session contents, or private paths in changes

https://claude.ai/code/session_016qb5ER3Mq6Bq2atVoKDo6f

claude added 4 commits August 26, 2026 12:57
…the Windows arm64 installer

electron 33 -> 43.4.1 (Chromium 150, Node 24.18.1), electron-builder 25 -> 26.15.3,
better-sqlite3 11 -> 13.0.3, vite 6 -> 7 with electron-vite 5, @types/node and the
documented toolchain to Node 24.

Windows on ARM installed without its binaries. Not BCJ2, which is what the payload of
the working 0.1.6 x64 installer already uses: electron-builder 26 swapped the bundled
7za 16.02 for a downloaded 7-Zip 24.09, which auto-applies the ARM64 filter (7-Zip
23.01+) to ARM64 PE files, while the NSIS side still ships the Nsis7z plugin built in
2019. The plugin answers "Unsupported Method" for exactly the nine native binaries and
installs everything else. Reproduced on the real payload: without the override nine
files carry method 0A and a decoder of the plugin's vintage refuses them; with it every file is BCJ
LZMA2 and all 135 extract.

scripts/electron-builder.cjs sets ELECTRON_BUILDER_7Z_FILTER=BCJ for Windows arm64 only,
so x64 keeps the BCJ2 it already ships with and macOS, whose dmg and zip targets never
reach the 7z path, is untouched. scripts/verify-windows-installer.cjs runs from
afterAllArtifactBuild: it carves the embedded archive out of every .exe and re-tests it
with a decoder older than the plugin, so a payload the installer cannot unpack fails the
build instead of reaching a release page. CI now builds both Windows installers (arm64
packaged on the x64 runner - the payload is inspected, never executed) so the check has
something to inspect on every push.

better-sqlite3 13 is a Node-API addon shipping prebuilt binaries for every platform, so
the cross-rebuild machinery is gone: scripts/rebuild-native.cjs, the postinstall and
rebuild:native scripts, and npmRebuild. Each artifact now carries only the one prebuild
it can load. Verified by loading the package's own binary under Electron 43.

plist is pinned to 3.1.1: app-builder-lib pins 3.1.0, which calls parseFromString
without a mime type, and the existing @xmldom/xmldom 0.9.11 override turns that into an
error that fails mac packaging outright.

Build targets are now written out in electron.vite.config.ts. electron-vite infers them
from a table that ends at Electron 39 and falls back to its oldest entry, so Electron 43
would have compiled to a chrome94/node16 target without a word.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qb5ER3Mq6Bq2atVoKDo6f
…p macOS crash reports

tests/thread-index.test.ts dies on the macOS runner with SIGABRT and nothing else:
the assertion on result.signal runs before the one that carries result.stderr, so the
child's stdout and stderr are discarded and the crash has no explanation. Both Electron
tests now build the transcript first and pass it to every assertion.

The runner writes a stage marker straight to fd 1 before each phase. console.log to a
pipe is buffered and lost when a process aborts; a synchronous write is not, so the last
marker on stdout names the phase the crash happened in - fixtures, scanSessions, the
thread index, or the exit itself. Diagnostic, to be removed once that is known.

CI dumps the newest macOS crash reports when a job fails, decoding the .ips to the
exception, the termination reason and the faulting thread's frames with the image each
frame belongs to. That answers the one question worth answering: whether the process
aborts inside Electron or inside better_sqlite3.node.

The matrix no longer fail-fasts. A red macOS leg was cancelling the Windows leg, which
is the one that builds the installers and runs the payload check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qb5ER3Mq6Bq2atVoKDo6f
Electron 33 shipped "postinstall": "node install.js", so its binary was always on disk
before tests started. Electron 43 has no install script at all - require('electron')
notices the missing binary and downloads it right there, on first use. vitest runs test
files in parallel, so several of them hit that at once: the CI log shows two "Downloading
Electron binary..." lines, each starting its own download into the same dist/ directory,
while another file was already spawning what had been written so far.

macOS aborted at launch on the truncated Mach-O - dyld "segment '__TEXT' load command
content extends beyond end of file", terminated at launch, which is where the SIGABRT
came from. Windows failed to spawn at all. Whichever file happened to lose the race
failed while its twin, arriving two seconds later, passed against a complete binary.

A vitest globalSetup resolves the binary once, before the first test file, so the
download can no longer overlap a spawn. Nothing in the app changed, and this is not
CI-specific: a fresh clone with a cold Electron cache raced the same way locally.

The stage markers added while diagnosing this are gone - the process died before any
of the runner's JavaScript ran, so they never had anything to report. The two things
that did the work stay: the assertion transcript, which carried the dyld message that
named the cause, and the macOS crash-report decoder in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qb5ER3Mq6Bq2atVoKDo6f
Only this branch's work stands between v0.1.6 and this release: the Electron 43 /
Node 24 / better-sqlite3 13 upgrade, the Windows arm64 installer fix, and the test
and CI changes that came out of it. Nothing under electron/, src/ or shared/ was
touched, so the release notes lead with the one thing users can actually feel -
Electron 43 requires macOS 12, where 0.1.6 ran on macOS 11. Both numbers are read
from the two Electron builds' own Info.plist.

The notes say the published Windows installer is x64 only, because it is: the
release workflow builds --win nsis --x64 and nothing else. The arm64 fix is
described as what it is, something that matters when building your own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qb5ER3Mq6Bq2atVoKDo6f
@FinnaXxx
FinnaXxx merged commit 73f4104 into main Aug 26, 2026
4 checks passed
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.

2 participants