Fix macOS x64 crash on launch: package sharp for both architectures - #89
Merged
Conversation
pnpm materialises only the host architecture's optional dependencies, but electron-builder packages both mac architectures from one node_modules. The arm64 runner therefore produced an x64 artifact with no @img/sharp-darwin-x64, which threw "Could not load the sharp module" on first launch. Separately, sharp was never unpacked from the asar. Its native binary finds libvips in a sibling package through an @rpath, and dlopen cannot reach a dylib inside an asar, so the arm64 build was broken by the same packaging gap. Add a release gate that inspects each packaged app and loads sharp from it, plus a verify workflow that runs the packaging on real macOS and Windows runners. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first line of a Node module error is just the loader frame, which says nothing about which module was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI invokes the gate with a relative path, and require() reads a bare relative specifier as a package name, so the runtime check failed on a correctly packaged app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loading sharp through ELECTRON_RUN_AS_NODE proves the binary resolves but never starts Electron, so it cannot catch a native module that only fails once the main process boots - which is the failure users report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #88
The bug
The x64 macOS build crashed on launch with
Could not load the "sharp" module using the darwin-x64 runtime. Two independent packaging defects:architecture's optional dependencies, but
electron-builder --macpackages both macarchitectures from one
node_modules. On the arm64 runner,@img/sharp-darwin-x64wasnever on disk.
sharpwas never unpacked from the asar. Its.nodefinds libvips in a siblingpackage via an
@rpath, anddlopencannot read a dylib inside an asar. This would havebroken arm64 too - x64 just failed earlier, at the
require.The fix
pnpm-workspace.yaml:supportedArchitecturesso every target platform's binary isinstalled, not just the runner's.
package.json:asarUnpackforsharpand@img, plus per-platformfilesnegations sothe Windows installer does not inherit ~34 MB of darwin libvips.
Each mac artifact now carries both architectures' libvips. That is unavoidable while one
node_modulesfeeds both targets, and it is the trade that makes the x64 build work.Verification
Nothing in CI launched or inspected the packaged app, which is why this shipped. Added two
checks that run on real runners (
verify-native-deps.yml), plus a gate on the release job:test/verify-packaged-sharp.mjs- derives each artifact's architecture from its Mach-O/PEheader, asserts the matching binary is unpacked, resolves libvips the way dyld will,
verifies the bundle signature covers the unpacked Mach-O files, then loads sharp from the
packaged asar and runs the real
grayscale().png()operation.test/smoke-packaged-launch.mjs- launches the packaged app for real. Loading sharp throughELECTRON_RUN_AS_NODEnever starts Electron proper, so it cannot catch a failure that onlyappears once the main process boots. The uncaught-exception dialog also keeps the process
alive, so the check requires clean output as well as survival.
Results on a real arm64 macOS runner:
.node, correct arch@rpathmac/is the exact darwin-x64 artifact that was crashing. Both gates were confirmed to failon a deliberately broken build before being trusted.
Also included
42832bb(@alpha5611331) fixes the release notes, which pointed every macOS user at the-arm64.dmg- so Intel users were handed a build their machine cannot run at all. Same bugclass, so it belongs with this change.
Note for a follow-up
--disable-content-protectiondoes not work on the packaged app: a packaged Electron binaryrejects the unrecognised option outright, so it exits before
src/main/index.ts:161reads it.It works in dev, where args follow the script path. Pre-existing and out of scope here, but
CLAUDE.mddocuments it without that caveat.🤖 Generated with Claude Code