Research Electron build tools and packaging#2642
Conversation
Surveys VS Code, Signal Desktop, Hyper, Bitwarden, and Mattermost build pipelines. Compares electron-builder vs @electron/forge vs @electron/packager. Recommends electron-builder as the pragmatic replacement for the current Grunt + @electron/packager + custom shell scripts setup. https://claude.ai/code/session_01VtVDZhA27pRziZQzXjdqmr
Migrate from the legacy Grunt-based build system to electron-builder, which is the most widely adopted packaging tool for production Electron apps (used by Signal, Hyper, Bitwarden, Mattermost). What changed: - Add electron-builder.json with full config for macOS (DMG+ZIP with code signing, notarization, provisioning profiles, entitlements), Windows (NSIS installer), and Linux (DEB+RPM with correct deps) - Add scripts/compile-typescript.js to replace the afterCopy transpiler - Add scripts/inject-commit-hash.js to replace the commit hash hook - Add scripts/electron-builder-hooks.js (afterPack) for platform-specific post-packaging steps (Windows resource copying, Linux sandbox perms) - Create pre-rendered mailspring.desktop with full i18n Actions section - Create pre-rendered mailspring.appdata.xml for Linux package metadata - Add NSIS include script for Windows registry/elevation helpers - Update package.json: replace grunt/packager/winstaller deps with electron-builder, add platform-specific build scripts What was removed: - Gruntfile.js and all Grunt task files (package-task, create-mac-zip, installer-linux-task, task-helpers, create-signed-windows-installer) - Dependencies: grunt, grunt-cli, load-grunt-parent-tasks, @electron/packager, electron-winstaller All existing resources preserved: icons, entitlements, plists, DMG background, Windows ICO/registry/elevation files, Linux debian control/postinst/postrm scripts, RPM spec template, lintian overrides. https://claude.ai/code/session_01VtVDZhA27pRziZQzXjdqmr
Defines 100+ checks across all platforms to verify that new electron-builder artifacts match old Grunt + @electron/packager artifacts, covering: - ASAR structure and unpack rules - macOS code signing, entitlements, provisioning profiles, notarization - Windows code signing, EXE metadata, resource files, installer behavior - Linux DEB/RPM metadata, dependencies, maintainer scripts, icons - Desktop files, AppData/metainfo, protocol handlers - Cross-platform version consistency and functional smoke tests https://claude.ai/code/session_01VtVDZhA27pRziZQzXjdqmr
Existing Windows users are on Squirrel.Windows (via electron-winstaller). Switching to NSIS would break their update path. Instead, use the electron-builder-squirrel-windows plugin which produces compatible Squirrel installers using the same underlying vendor binaries. Changes: - Replace nsis target with squirrel in electron-builder.json - Add squirrelWindows config (iconUrl, loadingGif, setupIcon) - Remove nsis config and nsis-include.nsh - Install electron-builder-squirrel-windows package - Update verification-plan.md with Squirrel-specific checks including update compatibility verification (section 4.6) https://claude.ai/code/session_01VtVDZhA27pRziZQzXjdqmr
Replace the table-based verification plan with a runnable bash script (scripts/verify-build.sh) that performs 100+ assertions against build artifacts. Run it against old and new builds, then diff the outputs. Usage: ./scripts/verify-build.sh ./app/dist-old mac > /tmp/old.txt ./scripts/verify-build.sh ./app/dist mac > /tmp/new.txt diff /tmp/old.txt /tmp/new.txt Covers: ASAR structure/contents, macOS codesigning/entitlements/ provisioning/notarization/Info.plist, Windows resource files/Squirrel installer/nupkg, Linux DEB metadata/deps/postinst/icons/desktop/ appdata, RPM metadata/scripts, and cross-platform consistency. https://claude.ai/code/session_01VtVDZhA27pRziZQzXjdqmr
|
9 potential issues found:
This PR replaces the legacy Grunt-based build pipeline with electron-builder for packaging Mailspring across macOS, Windows, and Linux. The migration removes ~500 lines of imperative Grunt task code and replaces it with a declarative JSON config, but introduces several critical configuration and architectural issues that would prevent the build from succeeding on any platform.
All CI checks passed on commit 2c61072. Note that CI only runs lint/typecheck/test — it does not execute the build pipeline, so the critical configuration issues identified in the review (wrong electron-builder paths, invalid properties, source tree destruction) would not be caught by CI.
|
|
|
||
| const res = TypeScript.transpileModule(tsCode, { compilerOptions, fileName: tsPath }); | ||
| fs.writeFileSync(outPath, res.outputText); | ||
| fs.unlinkSync(tsPath); |
There was a problem hiding this comment.
Source tree destruction: fs.unlinkSync(tsPath) deletes the original TypeScript source files from the working tree (app/src/ and app/internal_packages/). The old pipeline's runTranspilers function received a buildPath parameter from @electron/packager — a temporary copy of the app directory — so deletions only affected the copy, never the source.
With this script running as a pre-build step, every npm run build invocation permanently destroys ~670 source files. Recovery requires git checkout -- app/. This also means the git working tree will show massive changes after every build, and developers could accidentally commit the compiled .js output over the .ts source.
Consider either: (1) using electron-builder's beforeBuild/afterPack hooks to transpile inside the build output, (2) copying app/ to a temp dir before transpiling, or (3) adding a post-build restore step.
| const commit = execSync('git rev-parse HEAD').toString().trim().substr(0, 8); | ||
|
|
||
| let content = fs.readFileSync(packageJsonPath, 'utf8'); | ||
|
|
There was a problem hiding this comment.
Source file mutation: This modifies app/package.json in the source tree. After running npm run build:
git statusshowsapp/package.jsonas modified- A second
npm run build(on a different commit, withoutgit checkout app/package.jsonfirst) will print a warning and ship the stale hash from the first build — the placeholder is already consumed - A developer could accidentally commit the baked-in hash
The old pipeline's runWriteCommitHashIntoPackage received buildPath from @electron/packager and operated on the copy. Consider using electron-builder's extraMetadata option to inject the hash at build time without modifying source, e.g.:
"extraMetadata": { "commitHash": "${env.COMMIT_HASH}" }or use a beforeBuild hook that only modifies the build output.
| "category": "public.app-category.business", | ||
| "icon": "mac/mailspring.icns", | ||
| "extendInfo": "mac/extra.plist", | ||
| "hardenedRuntime": true, |
There was a problem hiding this comment.
Corrupts Info.plist: electron-builder v26 (app-builder-lib@26.8.1, macPackager.js:473) does Object.assign(appPlist, extendInfo) directly — it does NOT read this as a file path. Passing the string "mac/extra.plist" causes Object.assign to enumerate its character indices, producing {"0":"m","1":"a","2":"c",...} in the plist.
This means the app will lack critical entries from extra.plist: NSPrincipalClass (AtomApplication), NSFocusStatusUsageDescription, CFBundleDocumentTypes, and all URL scheme registrations defined there.
The fix is to inline the plist contents as a JSON object, e.g.:
"extendInfo": {
"NSPrincipalClass": "AtomApplication",
"NSFocusStatusUsageDescription": "Mailspring checks your Focus status to avoid sending notifications while you have Do Not Disturb enabled.",
...
}| "icon": "mac/mailspring.icns", | ||
| "extendInfo": "mac/extra.plist", | ||
| "hardenedRuntime": true, | ||
| "entitlements": "mac/entitlements.plist", |
There was a problem hiding this comment.
Wrong path resolution: electron-builder passes entitlements as-is to @electron/osx-sign (confirmed in macPackager.js:352-353). Unlike icon which goes through getResource(), entitlements paths are returned verbatim. "mac/entitlements.plist" resolves to <projectDir>/mac/entitlements.plist which doesn't exist.
The actual files are at app/build/resources/mac/entitlements.plist and app/build/resources/mac/entitlements.child.plist. Use project-root-relative paths:
"entitlements": "app/build/resources/mac/entitlements.plist",
"entitlementsInherit": "app/build/resources/mac/entitlements.child.plist"| "gir1.2-gnomekeyring-1.0" | ||
| ], | ||
| "afterInstall": "linux/debian/postinst", | ||
| "afterRemove": "linux/debian/postrm", |
There was a problem hiding this comment.
Wrong path resolution: FpmTarget.getResource() (at FpmTarget.js:41) resolves afterInstall/afterRemove relative to projectDir, not buildResources. "linux/debian/postinst" resolves to <projectDir>/linux/debian/postinst which doesn't exist.
The actual files are at app/build/resources/linux/debian/postinst and app/build/resources/linux/debian/postrm. Use:
"afterInstall": "app/build/resources/linux/debian/postinst",
"afterRemove": "app/build/resources/linux/debian/postrm"| "executableName": "mailspring", | ||
| "icon": "linux/icons", | ||
| "category": "Network;Email", | ||
| "desktopFile": "app/build/resources/linux/mailspring.desktop", |
There was a problem hiding this comment.
Invalid property — build will fail: desktopFile does not exist in electron-builder's LinuxConfiguration type (confirmed in app-builder-lib@26.8.1 type definitions). The schema has additionalProperties: false, so schema validation will throw an error.
The valid property is desktop (type LinuxDesktopFile) which accepts:
"desktop": {
"entry": { "Name": "Mailspring", "MimeType": "x-scheme-handler/mailto;x-scheme-handler/mailspring;" },
"desktopActions": { "NewMessage": { "Name": "New Message", "Exec": "mailspring mailto:" } }
}However, electron-builder has no option to specify a pre-existing .desktop file by path — you'll need to inline the desktop entry fields or use extraFiles to install the custom file.
|
|
||
| # Temp dirs for extraction | ||
| export WORK="/tmp/mailspring-verify-$$" | ||
| ``` |
There was a problem hiding this comment.
Script is malformed: Lines 1-20 contain an environment variable preamble, then lines 21-22 have markdown code-fence artifacts (triple backticks). The #!/usr/bin/env bash shebang is on line 23 instead of line 1, so the OS won't recognize this as a bash script.
Similarly, the script ends at line 1078 (exit $FAIL) but lines 1079-1102 contain another markdown code block with example usage.
Remove lines 1-22 (env var preamble + fences), move the shebang to line 1, and remove the trailing code block (lines 1079-1102).
| "!**/node_modules/**/coverage/**", | ||
| "!**/node_modules/**/benchmark/**" | ||
| ], | ||
| "fileAssociations": [ |
There was a problem hiding this comment.
Redundant and potentially dangerous: This fileAssociations block registers Mailspring as a handler for ALL file extensions (ext: "*"). electron-builder generates CFBundleDocumentTypes entries with default LSHandlerRank: Owner (primary handler).
The existing extra.plist already defines CFBundleDocumentTypes with LSHandlerRank: Alternate (secondary handler), which is the correct behavior — the app can receive files via drag-and-drop without becoming the default handler for every file type.
If extendInfo is fixed (currently broken per Issue #3) and both sources contribute document types, the merge result depends on ordering. If fileAssociations takes precedence, Mailspring becomes the Owner handler for all file types. Remove this block entirely.
https://claude.ai/code/session_01VtVDZhA27pRziZQzXjdqmr