From 58c14535ba2f1cc95756f3b97fd0c1d7cbea9990 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 24 Oct 2018 14:01:12 +1100 Subject: [PATCH 1/4] feat: add support for mojave app notarization --- common.js | 24 ++++++++++++++++++++++++ docs/api.md | 8 ++++++++ mac.js | 40 +++++++++++++++++++++++++++++++++++++++- package.json | 3 ++- usage.txt | 6 ++++++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/common.js b/common.js index 7c9bdb5c..3a8aab7c 100644 --- a/common.js +++ b/common.js @@ -58,6 +58,30 @@ function parseCLIArgs (argv) { args.osxSign = true } + let noNotarize = false + args.osxNotarize = args['osx-notarize'] + if (Array.isArray(args.osxNotarize) || (args.osxNotarize && typeof args.osxNotarize !== 'object')) { + warning('do not use --osx-notarize, use the sub-properties (see --help)') + noNotarize = true + } else if (args.osxNotarize) { + if (!args.osxNotarize.appleId) { + warning('--osx-notarize.appleId is required when using notarization, notarize will not run (see --help)') + noNotarize = true + } + if (!args.osxNotarize.appleIdPassword) { + warning('--osx-notarize.appleIdPassword is required when using notarization, notarize will not run (see --help)') + noNotarize = true + } + } + if (!args.osxSign) { + warning('notarization was enabled but osx code signing was not, code signing is a requirement for notarization, notarize will not run') + noNotarize = true + } + + if (noNotarize) { + args.osxNotarize = null + } + // tmpdir: `String` or `false` if (args.tmpdir === 'false') { warning('--tmpdir=false is deprecated, use --no-tmpdir instead') diff --git a/docs/api.md b/docs/api.md index 734aa340..3fcf5996 100644 --- a/docs/api.md +++ b/docs/api.md @@ -412,6 +412,14 @@ Entries from `extend-info` override entries in the base plist file supplied by ` The bundle identifier to use in the application helper's plist. +##### `osxNotarize` + +*Object* + +If present, notarizes OS X target apps when the host platform is OS X and XCode is installed. The configuration values listed below can be customized. See [electron-notarize](https://github.com/electron-userland/electron-notarize#method-notarizeopts-promisevoid) for more detailed option descriptions and how to use `appleIdPassword` safely. +- `appleId` (*String*): Your apple ID username / email +- `appleIdPassword` (*String*): The password for your apple ID, can be a keychain reference + ##### `osxSign` *Object* or *`true`* diff --git a/mac.js b/mac.js index e8333a68..15d59d43 100644 --- a/mac.js +++ b/mac.js @@ -6,6 +6,7 @@ const debug = require('debug')('electron-packager') const fs = require('fs-extra') const path = require('path') const plist = require('plist') +const { notarize } = require('electron-notarize') const { signAsync } = require('electron-osx-sign') class MacApp extends App { @@ -52,6 +53,10 @@ class MacApp extends App { return `com.electron.${common.sanitizeAppName(this.appName).toLowerCase()}` } + get bundleName () { + return filterCFBundleIdentifier(this.opts.appBundleId || this.defaultBundleName) + } + get originalResourcesDir () { return path.join(this.contentsPath, 'Resources') } @@ -172,7 +177,7 @@ class MacApp extends App { updatePlistFiles () { let plists - const appBundleIdentifier = filterCFBundleIdentifier(this.opts.appBundleId || this.defaultBundleName) + const appBundleIdentifier = this.bundleName this.helperBundleIdentifier = filterCFBundleIdentifier(this.opts.helperBundleId || `${appBundleIdentifier}.helper`) return this.determinePlistFilesToUpdate() @@ -292,6 +297,12 @@ class MacApp extends App { if (osxSignOpt) { const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.quiet) + if (this.opts.osxNotarize && !signOpts.hardenedRuntime) { + common.warning('notarization is enabled but hardenedRuntime was not enabled in ' + + 'signing options. It has been enabled for you but you should ' + + 'enable it in your config.') + signOpts.hardenedRuntime = true + } debug(`Running electron-osx-sign with the options ${JSON.stringify(signOpts)}`) return signAsync(signOpts) // Although not signed successfully, the application is packed. @@ -301,6 +312,20 @@ class MacApp extends App { } } + notarizeAppIfSpecified () { + let osxNotarizeOpt = this.opts.osxNotarize + + if (osxNotarizeOpt) { + const notarizeOpts = createNotarizeOpts( + osxNotarizeOpt, + this.bundleName, + this.renamedAppPath, + this.opts.quiet + ) + return notarize(notarizeOpts) + } + } + create () { return this.initialize() .then(() => this.updatePlistFiles()) @@ -309,6 +334,7 @@ class MacApp extends App { .then(() => this.renameAppAndHelpers()) .then(() => this.copyExtraResources()) .then(() => this.signAppIfSpecified()) + .then(() => this.notarizeAppIfSpecified()) .then(() => this.move()) } } @@ -348,6 +374,18 @@ function createSignOpts (properties, platform, app, version, quiet) { return signOpts } +function createNotarizeOpts (properties, appBundleId, appPath, quiet) { + const notarizeOpts = properties + + // osx-notarize options are handed off to notarize module, but + // with a few additions from the main options + // user may think they can pass bundle ID or appPath but they will be ignored + common.subOptionWarning(notarizeOpts, 'osx-notarize', 'appBundleId', appBundleId, quiet) + common.subOptionWarning(notarizeOpts, 'osx-notarize', 'appPath', appPath, quiet) + + return notarizeOpts +} + module.exports = { App: MacApp, createSignOpts: createSignOpts, diff --git a/package.json b/package.json index 9fe0860b..0a146e74 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "asar": "^0.14.0", "debug": "^4.0.1", "electron-download": "^4.1.1", - "electron-osx-sign": "^0.4.1", + "electron-notarize": "^0.0.5", + "electron-osx-sign": "^0.4.11", "extract-zip": "^1.0.3", "fs-extra": "^7.0.0", "galactus": "^0.2.1", diff --git a/usage.txt b/usage.txt index 58c753c2..7ad3dd28 100644 --- a/usage.txt +++ b/usage.txt @@ -82,6 +82,12 @@ osx-sign (OSX host platform only) Whether to sign the OSX app packages - identity: should contain the identity to be used when running `codesign` - entitlements: the path to entitlements used in signing - entitlements-inherit: the path to the 'child' entitlements +osx-notarize (OSX host platform only) Whether to notarize the OSX app packages. You must + use dot notation to configure a list of sub-properties, e.g. --osx-notarize.appleId="foo@example.com" + For info on supported values see https://github.com/electron-userland/electron-notarize#method-notarizeopts-promisevoid + Properties supported: + - appleId: should contain your apple ID username / email + - appleIdPassword: should contain the password for the provided apple ID protocol URL protocol scheme to register the app as an opener of. For example, `--protocol=myapp` would register the app to open URLs such as `myapp://path`. This argument requires a `--protocol-name` From 5aa3d78ab7280ed96797ffacc99f134ea372a60f Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sun, 4 Nov 2018 22:53:37 -0800 Subject: [PATCH 2/4] Reorganize some code, clarify docs, add tests --- common.js | 31 +++++++++++-------------------- docs/api.md | 2 ++ mac.js | 49 +++++++++++++++++++++++++++++++++---------------- test/_util.js | 9 +++------ test/asar.js | 2 -- test/cli.js | 15 +++++++++++++++ test/darwin.js | 29 +++++++++++++++++++++++++++++ usage.txt | 5 +++-- 8 files changed, 96 insertions(+), 46 deletions(-) diff --git a/common.js b/common.js index 3a8aab7c..046704fa 100644 --- a/common.js +++ b/common.js @@ -58,28 +58,19 @@ function parseCLIArgs (argv) { args.osxSign = true } - let noNotarize = false - args.osxNotarize = args['osx-notarize'] - if (Array.isArray(args.osxNotarize) || (args.osxNotarize && typeof args.osxNotarize !== 'object')) { - warning('do not use --osx-notarize, use the sub-properties (see --help)') - noNotarize = true - } else if (args.osxNotarize) { - if (!args.osxNotarize.appleId) { - warning('--osx-notarize.appleId is required when using notarization, notarize will not run (see --help)') - noNotarize = true + if (args.osxNotarize) { + let notarize = true + if (typeof args.osxNotarize !== 'object' || Array.isArray(args.osxNotarize)) { + warning('--osx-notarize does not take any arguments, it only has sub-properties (see --help)') + notarize = false + } else if (!args.osxSign) { + warning('Notarization was enabled but OSX code signing was not, code signing is a requirement for notarization, notarize will not run') + notarize = false } - if (!args.osxNotarize.appleIdPassword) { - warning('--osx-notarize.appleIdPassword is required when using notarization, notarize will not run (see --help)') - noNotarize = true - } - } - if (!args.osxSign) { - warning('notarization was enabled but osx code signing was not, code signing is a requirement for notarization, notarize will not run') - noNotarize = true - } - if (noNotarize) { - args.osxNotarize = null + if (!notarize) { + args.osxNotarize = null + } } // tmpdir: `String` or `false` diff --git a/docs/api.md b/docs/api.md index 3fcf5996..7d4b727d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -416,6 +416,8 @@ The bundle identifier to use in the application helper's plist. *Object* +**Requires [`osxSign`](#osxsign) to be set.** + If present, notarizes OS X target apps when the host platform is OS X and XCode is installed. The configuration values listed below can be customized. See [electron-notarize](https://github.com/electron-userland/electron-notarize#method-notarizeopts-promisevoid) for more detailed option descriptions and how to use `appleIdPassword` safely. - `appleId` (*String*): Your apple ID username / email - `appleIdPassword` (*String*): The password for your apple ID, can be a keychain reference diff --git a/mac.js b/mac.js index 15d59d43..5fc4b459 100644 --- a/mac.js +++ b/mac.js @@ -296,13 +296,7 @@ class MacApp extends App { } if (osxSignOpt) { - const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.quiet) - if (this.opts.osxNotarize && !signOpts.hardenedRuntime) { - common.warning('notarization is enabled but hardenedRuntime was not enabled in ' + - 'signing options. It has been enabled for you but you should ' + - 'enable it in your config.') - signOpts.hardenedRuntime = true - } + const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.osxNotarize, this.opts.quiet) debug(`Running electron-osx-sign with the options ${JSON.stringify(signOpts)}`) return signAsync(signOpts) // Although not signed successfully, the application is packed. @@ -313,8 +307,9 @@ class MacApp extends App { } notarizeAppIfSpecified () { - let osxNotarizeOpt = this.opts.osxNotarize + const osxNotarizeOpt = this.opts.osxNotarize + /* istanbul ignore if */ if (osxNotarizeOpt) { const notarizeOpts = createNotarizeOpts( osxNotarizeOpt, @@ -322,7 +317,9 @@ class MacApp extends App { this.renamedAppPath, this.opts.quiet ) - return notarize(notarizeOpts) + if (notarizeOpts) { + return notarize(notarizeOpts) + } } } @@ -348,7 +345,7 @@ function filterCFBundleIdentifier (identifier) { return identifier.replace(/ /g, '-').replace(/[^a-zA-Z0-9.-]/g, '') } -function createSignOpts (properties, platform, app, version, quiet) { +function createSignOpts (properties, platform, app, version, notarize, quiet) { // use default sign opts if osx-sign is true, otherwise clone osx-sign object let signOpts = properties === true ? { identity: null } : Object.assign({}, properties) @@ -371,23 +368,43 @@ function createSignOpts (properties, platform, app, version, quiet) { signOpts.identity = null } + if (notarize && !signOpts.hardenedRuntime) { + common.warning('notarization is enabled but hardenedRuntime was not enabled in the signing ' + + 'options. It has been enabled for you but you should enable it in your config.') + signOpts.hardenedRuntime = true + } + return signOpts } function createNotarizeOpts (properties, appBundleId, appPath, quiet) { const notarizeOpts = properties + let notarize = true - // osx-notarize options are handed off to notarize module, but - // with a few additions from the main options - // user may think they can pass bundle ID or appPath but they will be ignored - common.subOptionWarning(notarizeOpts, 'osx-notarize', 'appBundleId', appBundleId, quiet) - common.subOptionWarning(notarizeOpts, 'osx-notarize', 'appPath', appPath, quiet) + if (!notarizeOpts.appleId) { + common.warning('The appleId sub-property is required when using notarization, notarize will not run') + notarize = false + } + + if (!notarizeOpts.appleIdPassword) { + common.warning('The appleIdPassword sub-property is required when using notarization, notarize will not run') + notarize = false + } - return notarizeOpts + if (notarize) { + // osxNotarize options are handed off to the electron-notarize module, but with a few + // additions from the main options. The user may think they can pass bundle ID or appPath, + // but they will be ignored. + common.subOptionWarning(notarizeOpts, 'osxNotarize', 'appBundleId', appBundleId, quiet) + common.subOptionWarning(notarizeOpts, 'osxNotarize', 'appPath', appPath, quiet) + + return notarizeOpts + } } module.exports = { App: MacApp, + createNotarizeOpts: createNotarizeOpts, createSignOpts: createSignOpts, filterCFBundleIdentifier: filterCFBundleIdentifier } diff --git a/test/_util.js b/test/_util.js index fc57478f..c9a1c1af 100644 --- a/test/_util.js +++ b/test/_util.js @@ -9,6 +9,7 @@ const packager = require('../index') const path = require('path') const plist = require('plist') const setup = require('./_setup') +const sinon = require('sinon') const tempy = require('tempy') const test = require('ava') @@ -30,19 +31,15 @@ test.after.always(t => { test.beforeEach(t => { t.context.workDir = tempy.directory() t.context.tempDir = tempy.directory() + sinon.spy(console, 'warn') }) test.afterEach.always(t => { + console.warn.restore() return fs.remove(t.context.workDir) .then(() => fs.remove(t.context.tempDir)) }) -test.serial.afterEach.always(() => { - if (console.warn.restore) { - console.warn.restore() - } -}) - function testSinglePlatform (name, testFunction, testFunctionArgs, parallel) { module.exports.packagerTest(name, (t, opts) => { Object.assign(opts, module.exports.singlePlatformOptions()) diff --git a/test/asar.js b/test/asar.js index a8e94d5e..709b9443 100644 --- a/test/asar.js +++ b/test/asar.js @@ -3,7 +3,6 @@ const common = require('../common') const path = require('path') const test = require('ava') -const sinon = require('sinon') const util = require('./_util') test('asar argument test: asar is not set', t => { @@ -62,7 +61,6 @@ util.testSinglePlatform('prebuilt asar test', (t, opts) => { opts.ignore = ['foo'] opts.prune = false opts.derefSymlinks = false - sinon.spy(console, 'warn') let resourcesPath return util.packageAndEnsureResourcesPath(t, opts) diff --git a/test/cli.js b/test/cli.js index e7c03200..07cd540e 100644 --- a/test/cli.js +++ b/test/cli.js @@ -32,6 +32,21 @@ test('CLI argument test: --osx-sign=true', t => { t.true(args.osxSign) }) +test('CLI argument test: --osx-notarize=true', t => { + const args = common.parseCLIArgs(['--osx-notarize=true']) + t.falsy(args.osxNotarize, null) +}) + +test('CLI argument test: --osx-notarize is array', t => { + const args = common.parseCLIArgs(['--osx-notarize=1', '--osx-notarize=2']) + t.falsy(args.osxNotarize, null) +}) + +test('CLI argument test: --osx-notarize without --osx-sign', t => { + const args = common.parseCLIArgs(['--osx-notarize.appleId=myid']) + t.falsy(args.osxNotarize, null) +}) + test('CLI argument test: --tmpdir=false', t => { const args = common.parseCLIArgs(['--tmpdir=false']) t.false(args.tmpdir) diff --git a/test/darwin.js b/test/darwin.js index 3d36d6f4..c6ac5d7d 100644 --- a/test/darwin.js +++ b/test/darwin.js @@ -264,6 +264,30 @@ if (!(process.env.CI && process.platform === 'win32')) { ) }) + test('osxNotarize argument test: missing appleId', t => { + const notarizeOpts = mac.createNotarizeOpts({ appleIdPassword: '' }) + t.falsy(notarizeOpts, 'does not generate options') + util.assertWarning(t, 'WARNING: The appleId sub-property is required when using notarization, notarize will not run') + }) + + test('osxNotarize argument test: missing appleIdPassword', t => { + const notarizeOpts = mac.createNotarizeOpts({ appleId: '' }) + t.falsy(notarizeOpts, 'does not generate options') + util.assertWarning(t, 'WARNING: The appleIdPassword sub-property is required when using notarization, notarize will not run') + }) + + test('osxNotarize argument test: appBundleId not overwritten', t => { + const args = { appleId: '1', appleIdPassword: '2', appBundleId: 'no' } + const notarizeOpts = mac.createNotarizeOpts(args, 'yes', 'appPath', true) + t.is(notarizeOpts.appBundleId, 'yes', 'appBundleId is taken from arguments') + }) + + test('osxNotarize argument test: appPath not overwritten', t => { + const args = { appleId: '1', appleIdPassword: '2', appPath: 'no' } + const notarizeOpts = mac.createNotarizeOpts(args, 'appBundleId', 'yes', true) + t.is(notarizeOpts.appPath, 'yes', 'appPath is taken from arguments') + }) + test('osxSign argument test: default args', t => { const args = true const signOpts = mac.createSignOpts(args, 'darwin', 'out', 'version') @@ -300,6 +324,11 @@ if (!(process.env.CI && process.platform === 'win32')) { t.deepEqual(signOpts, { app: 'out', platform: 'darwin', version: 'version' }) }) + test('force osxSign.hardenedRuntime when osxNotarize is set', t => { + const signOpts = mac.createSignOpts({}, 'darwin', 'out', 'version', true) + t.true(signOpts.hardenedRuntime, 'hardenedRuntime forced to true') + }) + darwinTest('codesign test', (t, opts) => { opts.osxSign = { identity: 'Developer CodeCert' } diff --git a/usage.txt b/usage.txt index 7ad3dd28..dd7d3cee 100644 --- a/usage.txt +++ b/usage.txt @@ -82,8 +82,9 @@ osx-sign (OSX host platform only) Whether to sign the OSX app packages - identity: should contain the identity to be used when running `codesign` - entitlements: the path to entitlements used in signing - entitlements-inherit: the path to the 'child' entitlements -osx-notarize (OSX host platform only) Whether to notarize the OSX app packages. You must - use dot notation to configure a list of sub-properties, e.g. --osx-notarize.appleId="foo@example.com" +osx-notarize (OSX host platform only, requires --osx-sign) Whether to notarize the OSX app + packages. You must use dot notation to configure a list of sub-properties, e.g. + --osx-notarize.appleId="foo@example.com" For info on supported values see https://github.com/electron-userland/electron-notarize#method-notarizeopts-promisevoid Properties supported: - appleId: should contain your apple ID username / email From 8b9c16f7b7919f500d0195ea7eb5030a68f8ab62 Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sun, 4 Nov 2018 23:00:57 -0800 Subject: [PATCH 3/4] Note which notarize sub-properties are required --- docs/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 7d4b727d..1ba2b88b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -419,8 +419,8 @@ The bundle identifier to use in the application helper's plist. **Requires [`osxSign`](#osxsign) to be set.** If present, notarizes OS X target apps when the host platform is OS X and XCode is installed. The configuration values listed below can be customized. See [electron-notarize](https://github.com/electron-userland/electron-notarize#method-notarizeopts-promisevoid) for more detailed option descriptions and how to use `appleIdPassword` safely. -- `appleId` (*String*): Your apple ID username / email -- `appleIdPassword` (*String*): The password for your apple ID, can be a keychain reference +- `appleId` (*String*, **required**): Your apple ID username / email +- `appleIdPassword` (*String*, **required**): The password for your apple ID, can be a keychain reference ##### `osxSign` From 229c69ff387cdc11e0a59a6d9b1d406f6dc587de Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sun, 4 Nov 2018 23:09:51 -0800 Subject: [PATCH 4/4] Attempt to fix possible race condition with sinon.spy --- test/_util.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/_util.js b/test/_util.js index c9a1c1af..5ef08617 100644 --- a/test/_util.js +++ b/test/_util.js @@ -31,11 +31,15 @@ test.after.always(t => { test.beforeEach(t => { t.context.workDir = tempy.directory() t.context.tempDir = tempy.directory() - sinon.spy(console, 'warn') + if (!console.warn.restore) { + sinon.spy(console, 'warn') + } }) test.afterEach.always(t => { - console.warn.restore() + if (console.warn.restore) { + console.warn.restore() + } return fs.remove(t.context.workDir) .then(() => fs.remove(t.context.tempDir)) })