57
loading...
This website collects cookies to deliver better user experience
.app
, then compressing it using:zip
utility,zipinfo
command line tool reveals that:zip
version 3.0 of the generic ZIP algorithm.__MACOSX
folders to embed macOS-specific attributes into the archive, especially for links to dynamic libraries (e.g. found in some Node modules).ditto
instead of zip
to create a compressed archive of an .app
package. Ditto is a command line tool shipped with macOS for copying directories and creating/extracting archives. It uses the same scheme as Finder (PKZIP) and preserves metadata, thus making the output compatible with Apple’s service. The relevant options for executing ditto
in this context, i.e. to mimic Finder’s behavior, are:-c
and -k
to create a PKZIP-compressed archive,—sequesterRsrc
to preserve metadata (__MACOSX
),—keepParent
to embed parent directory name source in the archive.ditto -c -k —sequesterRsrc —keepParent APP_NAME.app APP_NAME.app.zip
Create an app-specific password to use with Apple notarization service. Preferably using your organization’s developer Apple ID.
Create an Entitlements .plist
file specific to your Electron apps. In our case, the following did the trick (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>
<!-- https://github.com/electron/electron-notarize#prerequisites -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<!-- https://github.com/electron-userland/electron-builder/issues/3940 -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
Set entitlements
and entitlementInherit
options for macOS build in Electron Builder’s configuration file to the .plist
created in the previous step.
Create a notarize.js
script to execute after Electron Builder signs the .app
and its contents. Place the file in the build directory defined in Electron Builder’s configuration file.
const {notarize} = require("electron-notarize");
exports.default = async function notarizing(context) {
const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== "darwin") {
return;
}
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: process.env.APP_BUNDLE_ID,
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
});
};
Add "afterSign": "./PATH_TO_NOTARIZE_JS_IN_BUILD_DIRECTORY”
to Electron Builder’s configuration file.
Monkey patch Electron Notarize. The script should run before Electron Builder’s CLI command. In our case, since we’ve taken a very modular approach to general app architecture, the build scripts (TypeScript files) include a separate commons
module, which is imported by Electron Notarize patcher. The .ts
files can be executed using ts-node
via
ts-node -O {\"module\":\"CommonJS\"} scripts/patch-electron-notarize.ts
build/node_modules/electron-notarize/lib/index.js
:spawn('zip', ['-r', '-y', zipPath, path.basename(opts.appPath)]
spawn('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), zipPath]
commons
(patcher-commons.ts
):import {promises as fsp} from "fs";
export type FileContentsTransformer = (content: string) => string;
export async function replaceFileContents(path: string, transformer: FileContentsTransformer) {
let fh: fsp.FileHandle | null = null;
let content: string = "";
try {
fh = await fsp.open(path, "r");
if (fh) {
content = (await fh.readFile()).toString();
}
} finally {
if (fh) {
await fh.close();
}
}
try {
fh = await fsp.open(path, "w");
if (fh) {
await fh.writeFile(transformer(content));
}
} finally {
if (fh) {
await fh.close();
}
}
}
patch-electron-notarize.ts
):import {FileContentsTransformer, replaceFileContents} from "./common";
const ELECTRON_NOTARIZE_INDEX_PATH = "build/node_modules/electron-notarize/lib/index.js";
async function main() {
const transformer: FileContentsTransformer = (content: string) => {
return content.replace(
"spawn('zip', ['-r', '-y', zipPath, path.basename(opts.appPath)]",
"spawn('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), zipPath]"
);
};
await replaceFileContents(ELECTRON_NOTARIZE_INDEX_PATH, transformer);
}
// noinspection JSIgnoredPromiseFromCall
main();
APPLE_ID
and APPLE_ID_PASSWORD
environment variables (the ones defined in Step 1) before running Electron Builder on your developer machine or in your CI environment. You can use Keychain on your local machine instead.When stuck, look for the root cause in the least expected places. In the case of my project, the compression step was the unexpected culprit.
Be stubborn when a particular feature or bugfix is essential to a product’s success. Here, the notarization was important and it took some time to get it right, but the end result is customers feeling safe when installing the software.
Sometimes “working” is good enough. I could develop a better solution, but that would take some precious time. I opted to focus on more pressing issues instead.
57