Electron for the Mac App Store
Before you go any further it is important to know that this blog post is out of date. The technique used here has been integrated into the offical Electron project. Look here for more info.
Electron and the Mac App Store are 2 things that are difficult to pair, like fine wine and McDonalds. Chromium, the rendering engine for Electron, uses a whole host of private APIs. One might be tempted to use NW.js instead as it already has a build for the MAS, but that is impossible for many people. NW.js differentiates between files that can run Node.js code and files that can touch the DOM, which happens to break many libraries. If you don't want to patch the code yourself you can simply download my precompiled binary from here and then skip straight to the relevant section.
My patch for Electron is heavily based on the work done by Trevor Linton, Alexey Stoletny, and Johan Satgé who created an up to date patch for NW.js. I simply ported their code to Electron, removed a few extra private APIs, and put the patch into a branch of libchromiumcontent.
How
To get your app on to the MAS you need to do 4 big things:
1. Patching libchromiumcontent
I have created a patch that removes all private APIs from Chromium. You can find it on Github [here][1]. The build is a long but worthwhile process. Make sure to have 40-50 gigs of data available for the easiest possible build. On my 2012 MBP it takes about 4 hours, but your speed will vary.
git clone https://github.com/wisesascha/libchromiumcontent.git
cd libchromiumcontent
git checkout -b atom-MAS
script/bootstrap
script/update -t x64
script/build -t x64
2. Building Electron
This steps gets slightly more annoying (from a disk space perspective). First you must bootstrap Electron, which will download the (massive) libchromiumcontent static libs, you may be tempted to just delete these but they are important for a couple of reasons.
git clone https://github.com/atom/electron.git
cd electron
./script/bootstrap.py -v
Now comes the semi-difficult part. First you have to build your own version of libchromiumcontent and then move it into the correct directory. Make sure to keep a backup of the original file because you will need libui-zoom
which is suspiciously left out of the libchromiumcontent build.
script/create-dist
mv ../electron/vendor/brightray/vendor/download/libchromiumcontent ../electron/vendor/brightray/vendor/download/libchromiumcontent.b
mv dist/main ../electron/vendor/brightray/vendor/download/libchromiumcontent
cd ../electron/
mv vendor/brightray/vendor/download/libchromiumcontent.b/static_library/libui_zoom.a vendor/brightray/vendor/download/libchromiumcontent/static_library/
Now comes the annoying manual section of this tutorial. The Electron build system, unlike libchromiumcontent, does not have an easy patching system so you will be forced to manually apply a small number of patches to Electron. Apply this patch automatically or manually.
diff -ruN vendor/crashpad/util/mac/mac_util.cc vendor/crashpad/util/mac/mac_util.cc
--- third_party/crashpad/crashpad/util/mac/mac_util.cc
+++ third_party/crashpad/crashpad/util/mac/mac_util.cc
@@ -39,16 +39,16 @@
// Don’t call these functions directly, call them through the
// TryCFCopy*VersionDictionary() helpers to account for the possibility that
// they may not be present at runtime.
-CFDictionaryRef _CFCopySystemVersionDictionary() WEAK_IMPORT;
-CFDictionaryRef _CFCopyServerVersionDictionary() WEAK_IMPORT;
+// CFDictionaryRef _CFCopySystemVersionDictionary() WEAK_IMPORT;
+// CFDictionaryRef _CFCopyServerVersionDictionary() WEAK_IMPORT;
// Don’t use these constants with CFDictionaryGetValue() directly, use them with
// the TryCFDictionaryGetValue() wrapper to account for the possibility that
// they may not be present at runtime.
-extern const CFStringRef _kCFSystemVersionProductNameKey WEAK_IMPORT;
-extern const CFStringRef _kCFSystemVersionProductVersionKey WEAK_IMPORT;
-extern const CFStringRef _kCFSystemVersionProductVersionExtraKey WEAK_IMPORT;
-extern const CFStringRef _kCFSystemVersionBuildVersionKey WEAK_IMPORT;
+// extern const CFStringRef _kCFSystemVersionProductNameKey WEAK_IMPORT;
+// extern const CFStringRef _kCFSystemVersionProductVersionKey WEAK_IMPORT;
+// extern const CFStringRef _kCFSystemVersionProductVersionExtraKey WEAK_IMPORT;
+// extern const CFStringRef _kCFSystemVersionBuildVersionKey WEAK_IMPORT;
#undef WEAK_IMPORT
@@ -96,16 +96,10 @@
// Helpers for the weak-imported private CoreFoundation internals.
CFDictionaryRef TryCFCopySystemVersionDictionary() {
- if (_CFCopySystemVersionDictionary) {
- return _CFCopySystemVersionDictionary();
- }
return nullptr;
}
CFDictionaryRef TryCFCopyServerVersionDictionary() {
- if (_CFCopyServerVersionDictionary) {
- return _CFCopyServerVersionDictionary();
- }
return nullptr;
}
@@ -210,8 +204,8 @@
bool success = true;
- CFStringRef version_cf = base::mac::CFCast<CFStringRef>(
- TryCFDictionaryGetValue(dictionary, _kCFSystemVersionProductVersionKey));
+ // CFStringRef version_cf = base::mac::CFCast<CFStringRef>(
+ // TryCFDictionaryGetValue(dictionary, _kCFSystemVersionProductVersionKey));
std::string version;
if (!version_cf) {
LOG(ERROR) << "version_cf not found";
@@ -221,8 +215,8 @@
success &= StringToVersionNumbers(version, major, minor, bugfix);
}
- CFStringRef build_cf = base::mac::CFCast<CFStringRef>(
- TryCFDictionaryGetValue(dictionary, _kCFSystemVersionBuildVersionKey));
+ // CFStringRef build_cf = base::mac::CFCast<CFStringRef>(
+ // TryCFDictionaryGetValue(dictionary, _kCFSystemVersionBuildVersionKey));
if (!build_cf) {
LOG(ERROR) << "build_cf not found";
success = false;
@@ -230,8 +224,8 @@
build->assign(base::SysCFStringRefToUTF8(build_cf));
}
- CFStringRef product_cf = base::mac::CFCast<CFStringRef>(
- TryCFDictionaryGetValue(dictionary, _kCFSystemVersionProductNameKey));
+ // CFStringRef product_cf = base::mac::CFCast<CFStringRef>(
+ // TryCFDictionaryGetValue(dictionary, _kCFSystemVersionProductNameKey));
std::string product;
if (!product_cf) {
LOG(ERROR) << "product_cf not found";
Next you have to remove QTKit from vendor/brightray/brightray.gyp at line 133. Now you are finally ready to build!.
./script/build.py -c R
Now you have a binary in the out/R directory. You'll have to go in and make some specific changes to make this work with the Mac App Store.
rm -rf "out/R/Electron.app/Contents/Frameworks/Electron Framework.framework/Libraries/ffmpegsumo.so"
rm -rf "out/R/Electron.app/Contents/Frameworks/Electron Framework.framework/Libraries/Libraries"
You now have a fresh build of Electron that is almost ready to be distributed to the MAS. This guide supplies more information on how to customize and brand the resulting binary.
3. Customize the info.plist & delete some files
Now its time to make sure the info.plist file is all correct. Open it up in Xcode and make sure it has these fields:
- Bundle display name
- Bundle identifier
- Bundle version
- Bundle versions string, short
- Copyright (human-readable)
- Application Category
Then just remove a couple files.
rm -rf "Electron.app/Contents/Frameworks/Electron Framework.framework/Resources/crashpad_handler"
rm -rf "Electron.app/Contents/Frameworks/Electron Framework.framework/Libraries/ffmpegsumo.so"
rm -rf "Electron.app/Contents/Frameworks/Squirrel.framework/Resources/ShipIt"
4. Sign everything!
Create 2 files, child.plist
and parent.plist
, these will hold the entitlements for signing.
Paste this code into child.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.inherit</key>
<true/>
</dict>
</plist>
Paste this into the parent.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/>
</dict>
</plist>
Be sure to add any other entitlements you need from this list and add them to your parent file. Before signing, replace Electron.app with the name of your application and add your MAS application certificate.
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Electron Framework.framework/Libraries/libnode.dylib"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Electron Framework.framework/Electron Framework"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Electron Framework.framework/"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Electron Helper.app/"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Electron Helper EH.app/"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Electron Helper NP.app/"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Mantle.framework"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Mantle.framework/Mantle"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/ReactiveCocoa.framework/ReactiveCocoa"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/ReactiveCocoa.framework"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Squirrel.framework/Squirrel"
codesign --deep -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements child.plist "Electron.app/Contents/Frameworks/Squirrel.framework"
codesign -fs "!!PLACE YOUR APPLICATION SIGNING CERT HERE!!" --entitlements parent.plist "Electron.app"
It's uploadin' time
You can just upload the signed app you first have to package it into a pkg file. For this step you need an installer certificate.
productbuild --component "Electron.app" /Applications --sign "PLACE YOUR INSTALLER SIGNING CERT HERE" "Electron.pkg"
Make sure that you have setup your application correctly on iTunes Connect then pop open Application Loader and upload it.
Now you should be complete. You can contact me at me@saschawise.com if you find any issues with this. I'll try my best to either help you and/or ammend the article. If you'd like to support me you can download my app which is now in the MAS (big suprise). [1]: https://github.com/wisesascha/libchromiumcontent/tree/atom-MAS