Shipping Electron Apps to Mac App Store with Electron Builder

Shipping Electron apps have changed with time in plenty of ways. The latest that works pretty neatly is Electron Builder, it’s also worth watching the Electron Userland space for more developments.

One small warning for anyone dealing with this, is that history shows that the workflow around packaging Electron is happy to break. Building and packaging an Electron app for multiple distribution methods is still no small feat.

Here’s just a few things that can change with time with a real-world Electron app — each can make your build fail.

  • Electron version.
  • Electron builder version.
  • Webpack (you do want an independent bundle).
  • Babel (you do want a modern Javascript)
  • Native deps and how to package and sign those.
  • Xcode tools, macOS changes and electron tooling changes upstream (read: later) to that.
  • Certificate voodoo. Expirey, invalidation, management of certificates (which is a pain for iOS too, mitigated with Fastlane match)

With that in mind, if you follow the below instructions, you’ll have a rather smooth and uneventful packaging experience.

Folder structure

Here’s how it looks like without Javascript specific stuff. Only packaging related files and directories are here.

We mark each of the topics with a number and cover them later below.

src/
build/
all-certs.p12 <--- (1) all certs exported
entitlements.mac.plist <--- boilerplate, nothing important
entitlements.mas.plist <--- (3) team and dev ids
electronMac{.original}.js <--- (5) fixes bug with builder
electron-builder.yaml <--- (4) contains build information
embedded.provisionprofile <--- (2) provisioning profile

Build Process and Terminology

Electron builder documentation assumes (maybe rightfully?) some terminology, I’m guessing because Electron tooling has come a way now and this is the Nth incarnation of packaging tools and it relies on your having some experience with older tools.

Let’s explain a few of the relevant terminology now.

  • Build process. Electron-builder will look at targets and just the one (1) certificate bundle and build all variants at once, placing results in 'release'.
  • mas: “Mac App Store” build. Submitting needs a ‘pkg’ package.
  • mac: “Mac stand-alone” build, Apple doesn’t supervise and participate in distributing this, but macOS does verify product signatures so that people don’t run things that come from no-where (a verifying check which a power-user can disable through security settings on a macOS). Usually distributed with a ‘dmg’ package.
  • pkg, dmg: two packaging formats for the app; each is needed base on the variant packaging that we opt for (mac, mas).

1. Certificates

Electron builder’s code-signing section doesn’t tell you much about what you need to prepare before configuring it.

Use Xcode to generate your mac app certificates:

  1. Open Xcode
  2. Preferences (Cmd+,) -> Accounts Tab
  3. Manage certificates (log in if you never did)

Use the small ‘+’ dropdown at the button to generate the following:

  1. Mac App Distribution
  2. Mac Installer Distribution
  3. Developer ID Application
  4. Developer ID Installer
  5. Mac Development (not a must)

The most important step is to export these into a single p12 file. You might have been used to exporting individually when working with iOS apps.

Launch Keychain, and go to to ‘My Certificates’. Find these certificates and highlight all of them (Cmd+click), right click and export as p12.

Place this p12 file as build/all-certs.p12.

Super important to realize that if you have additional binaries, whether a node.js native dependency or just an executable that you include in your resources, that you want your app to use (let's say ffmpeg or such), they too need to be signed and will be signed by Electron Builder, which is a nice surprise.

If any of this kind of signing is broken or the application doesn’t run properly, be ready to look there.

2. Provisioning Profile

Log into your apple developer account (developer.apple.com) and go to “Certificates, IDs & Profiles”. Be sure to move to the “macOS” dropdown or else you’ll be manipulating iOS related assets.

Go to ‘Provisioning Profiles’ and create one for distribution named embedded. Download that into embedded.provisionprofile at the root of your project (from where ever you'll be running electron-builder). Place it only at the root and don't try to be smart about it in favor of a cleaner project structure (as I did), the story is that multiple tools are involved with the Electron Builder packaging workflow, one of them relies on this file existing at the root.

3. Entitlements

On an app that doesn’t require special external services from Apple, these files will be almost empty.

entitlements.mac.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

entitlements.mas.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<string>XXXXX.your.bundle.id</string>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

In the *.mas.* variant fill in the value denoted by XXX... with your bundle ID and prefix. You can take these values from your developer account, where you have allocated a bundle ID for your app.

4. Electron Builder Configuration

It is preferred to keep in a separate YAML file rather than in your package.json file as an embedded block of configuration because this will let your work with a single package.json file more easily both for build and development and skip the two-package project layout which is arguably less clean.

Here’s an electron-builder.yaml file that builds for all platforms and ways of distribution, including Mac App Store.

directories:
output: release
appId: your.bundle.id
asarUnpack: '**/resources/some-native-binary'
productName: MyAwesomeApp
files:
- dist/
- from: dist/
to: "."
filter: index.js
- resources/
- index.html
- embedded.provisionprofile
mac:
category: public.app-category.productivity
entitlements: build/entitlements.mac.plist
icon: resources/app.icns
target:
- pkg
- dmg
- zip
- mas
mas:
type: distribution
category: public.app-category.productivity
entitlements: build/entitlements.mas.plist
icon: resources/app.icns

5. Packaging

We need to specify the environment variable (as documented) CSC_LINK to be tied to our bundle of certificates.

In addition, due to this recent issue your app will build but won’t validate on the Mac App Store. To resolve it, you can use the patch provided there before building.

Something like this will work around the validation issue:

"package:standalone":
"cp build/electronMac.original.js node_modules/electron-builder-lib/out/electron/electronMac.js && CSC_LINK=./build/all-certs.p12 electron-builder",
"package:app-store":
"cp build/electronMac.js node_modules/electron-builder-lib/out/electron/electronMac.js && CSC_LINK=./build/all-certs.p12 electron-builder",

Each build process will build with errors for the opposing build variant (either “mac” or “mas”), because of this patch but it will still build the variant it was invoked for properly.

Finally use Mac Application Loader to upload the freshly baked mas/*.pkg build result.

Summary

If you follow these instructions, everything should build smoothly (sans the issue above, which I imagine will resolve with time).

Should you bump into an issue, remember the list of things that can make your build break and check those in order:

  • Electron version.
  • Electron builder version.
  • Webpack (you do want an independent bundle).
  • Babel (you do want a modern Javascript)
  • Native deps and how to package and sign those.
  • Xcode tools, macOS changes and electron tooling changes upstream (read: later) to that.
  • Certificate voodoo. Expirey, invalidation, management of certificates (which is a pain for iOS too, mitigated with Fastlane match)

Even though it feels like a lot of work, doing all this to ship a macOS app outside Xcode without tools like Fastlane is simply amazing.