Skip to content

Research Electron build tools and packaging#2642

Open
bengotow wants to merge 5 commits intomasterfrom
claude/research-electron-build-FL21J
Open

Research Electron build tools and packaging#2642
bengotow wants to merge 5 commits intomasterfrom
claude/research-electron-build-FL21J

Conversation

@bengotow
Copy link
Collaborator

claude added 5 commits March 10, 2026 04:07
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
@indent-staging
Copy link
Contributor

indent-staging bot commented Mar 11, 2026

Issues

9 potential issues found:

  • Autofix scripts/verify-build.sh contains markdown code-fence artifacts (triple backticks on lines 21-22 and 1079-1080) and the shebang is on line 23 instead of line 1, making the script non-executable.
  • Autofix fileAssociations with ext: "*" is redundant with extra.plist's CFBundleDocumentTypes (which uses LSHandlerRank: Alternate). electron-builder generates entries with default Owner rank. If extendInfo is fixed but fileAssociations remains, the merge order could register Mailspring as the Owner handler for all file types on macOS.
  • Autofix compile-typescript.js destructively deletes all ~670 TypeScript source files from the working tree. The old pipeline ran transpilation inside @electron/packager's temporary build copy via afterCopy hooks, never touching the source. Running npm run build will require git checkout -- app/ to recover.
  • Autofix linux.desktopFile is not a valid electron-builder property. LinuxConfiguration has additionalProperties: false in the schema, so schema validation will reject this config and fail the build. The custom .desktop file with i18n translations will not be used.
  • Autofix inject-commit-hash.js modifies the source app/package.json in-place, dirtying the git working tree and breaking on repeated builds (second build on a different commit ships a stale hash because the placeholder is already consumed).
  • Autofix mac.extendInfo is set to the string "mac/extra.plist" but electron-builder v26 passes this value directly to Object.assign(appPlist, extendInfo). With a string, this enumerates character indices ({"0":"m","1":"a",...}), corrupting the macOS Info.plist instead of merging the plist entries.
  • Autofix mac.entitlements and mac.entitlementsInherit paths ("mac/entitlements.plist", "mac/entitlements.child.plist") are returned as-is by electron-builder and resolve relative to project root. The files exist at app/build/resources/mac/, not <project-root>/mac/.
  • Autofix Windows CI workflow (.github/workflows/build-windows.yaml:69) references the now-deleted app/build/create-signed-windows-installer.js. This step is now redundant (electron-builder creates the installer during npm run build) but needs to be removed along with updating the subsequent signing step paths.
  • Autofix deb.afterInstall and deb.afterRemove paths ("linux/debian/postinst", "linux/debian/postrm") resolve relative to projectDir via FpmTarget.getResource(), not buildResources. The files are at app/build/resources/linux/debian/, so Linux DEB builds will fail with file-not-found errors.

Summary

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.

  • Add electron-builder.json with full config for macOS (DMG+ZIP, code signing, notarization), Windows (Squirrel installer), and Linux (DEB+RPM)
  • Add scripts/compile-typescript.js to replace the afterCopy TypeScript transpiler
  • Add scripts/inject-commit-hash.js to replace the commit hash injection hook
  • Add scripts/electron-builder-hooks.js (afterPack) for Windows resource copying and Linux sandbox permissions
  • Add pre-rendered mailspring.desktop with full i18n Actions and mailspring.appdata.xml for Linux metadata
  • Add scripts/verify-build.sh (1100-line verification script) and verification-plan.md
  • Add research document (docs/electron-build-migration-research.md) comparing electron-builder, @electron/forge, and @electron/packager
  • Delete Gruntfile.js, all Grunt task files, and create-signed-windows-installer.js
  • Replace grunt/packager/winstaller dependencies with electron-builder and electron-builder-squirrel-windows
  • Add platform-specific build scripts (build:mac, build:win, build:linux, build:dir)

CI Checks

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.

Rule Checks 3 rules evaluated, 3 passed, 0 failed

Passing rules

Passing This is a longer title to see what happens when they are too long to fit
Passing B
Passing Ben Rule

Autofix All


const res = TypeScript.transpileModule(tsCode, { compilerOptions, fileName: tsPath });
fs.writeFileSync(outPath, res.outputText);
fs.unlinkSync(tsPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source file mutation: This modifies app/package.json in the source tree. After running npm run build:

  1. git status shows app/package.json as modified
  2. A second npm run build (on a different commit, without git checkout app/package.json first) will print a warning and ship the stale hash from the first build — the placeholder is already consumed
  3. 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-$$"
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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