Skip to content

Commit

Permalink
Merge pull request #899 from electron-userland/notarize-support
Browse files Browse the repository at this point in the history
feat: add support for Mojave app notarization
  • Loading branch information
malept committed Nov 6, 2018
2 parents 3366253 + 229c69f commit c0c8014
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 11 deletions.
15 changes: 15 additions & 0 deletions common.js
Expand Up @@ -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')
Expand Down
10 changes: 10 additions & 0 deletions docs/api.md
Expand Up @@ -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`*
Expand Down
61 changes: 58 additions & 3 deletions mac.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand All @@ -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())
Expand All @@ -309,6 +331,7 @@ class MacApp extends App {
.then(() => this.renameAppAndHelpers())
.then(() => this.copyExtraResources())
.then(() => this.signAppIfSpecified())
.then(() => this.notarizeAppIfSpecified())
.then(() => this.move())
}
}
Expand All @@ -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)

Expand All @@ -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
}
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down
11 changes: 6 additions & 5 deletions test/_util.js
Expand Up @@ -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')

Expand All @@ -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) {
Expand Down
2 changes: 0 additions & 2 deletions test/asar.js
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions test/cli.js
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions test/darwin.js
Expand Up @@ -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')
Expand Down Expand Up @@ -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' }

Expand Down
7 changes: 7 additions & 0 deletions usage.txt
Expand Up @@ -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`
Expand Down

0 comments on commit c0c8014

Please sign in to comment.