diff --git a/common.js b/common.js index 7c9bdb5c..046704fa 100644 --- a/common.js +++ b/common.js @@ -58,6 +58,21 @@ function parseCLIArgs (argv) { args.osxSign = 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 (!notarize) { + 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..1ba2b88b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -412,6 +412,16 @@ 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* + +**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*, **required**): Your apple ID username / email +- `appleIdPassword` (*String*, **required**): 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..5fc4b459 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() @@ -291,7 +296,7 @@ class MacApp extends App { } if (osxSignOpt) { - const signOpts = createSignOpts(osxSignOpt, platform, this.renamedAppPath, version, this.opts.quiet) + 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. @@ -301,6 +306,23 @@ class MacApp extends App { } } + notarizeAppIfSpecified () { + const osxNotarizeOpt = this.opts.osxNotarize + + /* istanbul ignore if */ + if (osxNotarizeOpt) { + const notarizeOpts = createNotarizeOpts( + osxNotarizeOpt, + this.bundleName, + this.renamedAppPath, + this.opts.quiet + ) + if (notarizeOpts) { + return notarize(notarizeOpts) + } + } + } + create () { return this.initialize() .then(() => this.updatePlistFiles()) @@ -309,6 +331,7 @@ class MacApp extends App { .then(() => this.renameAppAndHelpers()) .then(() => this.copyExtraResources()) .then(() => this.signAppIfSpecified()) + .then(() => this.notarizeAppIfSpecified()) .then(() => this.move()) } } @@ -322,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) @@ -345,11 +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 + + 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 + } + + 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/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/test/_util.js b/test/_util.js index fc57478f..5ef08617 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,17 +31,17 @@ test.after.always(t => { test.beforeEach(t => { t.context.workDir = tempy.directory() t.context.tempDir = tempy.directory() + if (!console.warn.restore) { + sinon.spy(console, 'warn') + } }) test.afterEach.always(t => { - return fs.remove(t.context.workDir) - .then(() => fs.remove(t.context.tempDir)) -}) - -test.serial.afterEach.always(() => { if (console.warn.restore) { console.warn.restore() } + return fs.remove(t.context.workDir) + .then(() => fs.remove(t.context.tempDir)) }) function testSinglePlatform (name, testFunction, testFunctionArgs, parallel) { 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 58c753c2..dd7d3cee 100644 --- a/usage.txt +++ b/usage.txt @@ -82,6 +82,13 @@ 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, 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 + - 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`