Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Mojave app notarization #899

Merged
merged 4 commits into from Nov 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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