From 0f3aa5556cd5fee0628610f99517098bc49e0540 Mon Sep 17 00:00:00 2001 From: sethlu Date: Tue, 16 Feb 2016 20:45:49 +0800 Subject: [PATCH 1/6] Mac App Store platform support - Added isPlatformMac to check whether platform build for OS X - Changed from `codesign` to `electron-osx-sign` for code-signing - Changed `test/mac` to be a shared script for testing both `darwin` and `mas` platforms - Updates on usage/readme --- index.js | 11 ++++++++--- mac.js | 20 ++++++++++++++++++-- package.json | 1 + readme.md | 4 ++++ test/basic.js | 6 +++--- test/darwin.js | 13 +++++++++++++ test/index.js | 3 ++- test/mac.js | 35 +++++++++++++++-------------------- test/mas.js | 13 +++++++++++++ test/multitarget.js | 4 ++-- test/util.js | 16 ++++++++++++---- usage.txt | 13 +++++++------ 12 files changed, 98 insertions(+), 41 deletions(-) create mode 100644 test/darwin.js create mode 100644 test/mas.js diff --git a/index.js b/index.js index 21db0cb9..b8cc3a99 100644 --- a/index.js +++ b/index.js @@ -20,9 +20,14 @@ var supportedPlatforms = { // Maps to module ID for each platform (lazy-required if used) darwin: './mac', linux: './linux', + mas: './mac', // map to darwin win32: './win32' } +function isPlatformMac (platform) { + return platform === 'darwin' || platform === 'mas' +} + function validateList (list, supported, name) { // Validates list of architectures or platforms. // Returns a normalized array if successful, or an error message string otherwise. @@ -95,7 +100,7 @@ function createSeries (opts, archs, platforms) { archs.forEach(function (arch) { platforms.forEach(function (platform) { // Electron does not have 32-bit releases for Mac OS X, so skip that combination - if (platform === 'darwin' && arch === 'ia32') return + if (isPlatformMac(platform) && arch === 'ia32') return combinations.push({ platform: platform, arch: arch, @@ -161,11 +166,11 @@ function createSeries (opts, archs, platforms) { }) } - if (combination.platform === 'darwin') { + if (isPlatformMac(combination.platform)) { testSymlink(function (result) { if (result) return checkOverwrite() - console.error('Cannot create symlinks; skipping darwin platform') + console.error('Cannot create symlinks; skipping ' + combination.platform + ' platform') callback() }) } else { diff --git a/mac.js b/mac.js index f697f908..eee49036 100644 --- a/mac.js +++ b/mac.js @@ -1,12 +1,12 @@ var path = require('path') var fs = require('fs') -var child = require('child_process') var plist = require('plist') var mv = require('mv') var ncp = require('ncp').ncp var series = require('run-series') var common = require('./common') +var sign = require('electron-osx-sign') function moveHelpers (frameworksPath, appName, callback) { function rename (basePath, oldName, newName, cb) { @@ -101,7 +101,23 @@ module.exports = { if (opts.sign) { operations.push(function (cb) { - child.exec('codesign --deep --force --sign "' + opts.sign + '" "' + finalAppPath + '"', cb) + sign({ + app: finalAppPath, + platform: opts.platform, + // Take argument sign as signing identity: + // Provided in command line --sign, opts.sign will be recognized + // as boolean value true. Then fallback to null for auto discovery, + // otherwise provided signing certificate. + identity: opts.sign === true ? null : opts.sign, + entitlements: opts['sign-entitlements'] + }, function (err) { + if (err) { + console.warn('Code sign failed; please retry manually.') + // Though not signed successfully, the application is packed. + // It might have to be signed for another time manually. + } + cb() + }) }) } diff --git a/package.json b/package.json index 5a054c0f..0df094d2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "asar": "^0.8.2", "electron-download": "^1.0.0", + "electron-osx-sign": "^0.1.6", "extract-zip": "^1.0.3", "get-package-info": "0.0.2", "minimist": "^1.1.1", diff --git a/readme.md b/readme.md index 7d36adfe..cd654b5e 100644 --- a/readme.md +++ b/readme.md @@ -201,6 +201,10 @@ If the file extension is omitted, it is auto-completed to the correct extension The identity used when signing the package via `codesign`. (Only for the OS X target platform, when XCode is present on the build platform.) +`sign-entitlements` - *String* + + The path to entitlements used in signing. (Currently limited to Mac App Store distribution.) + `strict-ssl` - *Boolean* Whether SSL certificates are required to be valid when downloading Electron. **Defaults to `true`**. diff --git a/test/basic.js b/test/basic.js index e34322cb..d575509f 100644 --- a/test/basic.js +++ b/test/basic.js @@ -15,7 +15,7 @@ function generateNamePath (opts) { // Generates path to verify reflects the name given in the options. // Returns the Helper.app location on darwin since the top-level .app is already tested for the resources path; // returns the executable for other OSes - if (opts.platform === 'darwin') { + if (util.isPlatformMac(opts.platform)) { return path.join(opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper.app') } @@ -49,7 +49,7 @@ function createDefaultsTest (combination) { resourcesPath = path.join(finalPath, util.generateResourcesPath(opts)) fs.stat(path.join(finalPath, generateNamePath(opts)), cb) }, function (stats, cb) { - if (opts.platform === 'darwin') { + if (util.isPlatformMac(opts.platform)) { t.true(stats.isDirectory(), 'The Helper.app should reflect opts.name') } else { t.true(stats.isFile(), 'The executable should reflect opts.name') @@ -296,7 +296,7 @@ function createInferTest (combination) { opts.name = packageJSON.productName fs.stat(path.join(finalPath, generateNamePath(opts)), cb) }, function (stats, cb) { - if (opts.platform === 'darwin') { + if (util.isPlatformMac(opts.platform)) { t.true(stats.isDirectory(), 'The Helper.app should reflect productName') } else { t.true(stats.isFile(), 'The executable should reflect productName') diff --git a/test/darwin.js b/test/darwin.js new file mode 100644 index 00000000..2f0c9a00 --- /dev/null +++ b/test/darwin.js @@ -0,0 +1,13 @@ +var path = require('path') + +var config = require('./config.json') + +var baseOpts = { + name: 'basicTest', + dir: path.join(__dirname, 'fixtures', 'basic'), + version: config.version, + arch: 'x64', + platform: 'darwin' +} + +require('./mac')(baseOpts) diff --git a/test/index.js b/test/index.js index 68a2a15c..340f6531 100644 --- a/test/index.js +++ b/test/index.js @@ -23,6 +23,7 @@ series([ if (process.platform !== 'win32') { // Perform additional tests specific to building for OS X - require('./mac') + require('./darwin') + require('./mas') } }) diff --git a/test/mac.js b/test/mac.js index cde0ff36..a21ab36a 100644 --- a/test/mac.js +++ b/test/mac.js @@ -10,15 +10,7 @@ var config = require('./config.json') var util = require('./util') var plist = require('plist') -var baseOpts = { - name: 'basicTest', - dir: path.join(__dirname, 'fixtures', 'basic'), - version: config.version, - arch: 'x64', - platform: 'darwin' -} - -function createIconTest (icon, iconPath) { +function createIconTest (baseOpts, icon, iconPath) { return function (t) { t.timeoutAfter(config.timeout) @@ -46,7 +38,7 @@ function createIconTest (icon, iconPath) { } } -function createAppVersionTest (appVersion, buildVersion) { +function createAppVersionTest (baseOpts, appVersion, buildVersion) { return function (t) { t.timeoutAfter(config.timeout) @@ -81,7 +73,7 @@ function createAppVersionTest (appVersion, buildVersion) { } } -function createAppCategoryTypeTest (appCategoryType) { +function createAppCategoryTypeTest (baseOpts, appCategoryType) { return function (t) { t.timeoutAfter(config.timeout) @@ -109,6 +101,8 @@ function createAppCategoryTypeTest (appCategoryType) { } } +// Share testing script with platform darwin and mas +module.exports = function (baseOpts) { util.setup() test('helper app paths test', function (t) { t.timeoutAfter(config.timeout) @@ -152,15 +146,15 @@ util.teardown() var iconBase = path.join(__dirname, 'fixtures', 'monochrome') var icnsPath = iconBase + '.icns' util.setup() -test('icon test: .icns specified', createIconTest(icnsPath, icnsPath)) +test('icon test: .icns specified', createIconTest(baseOpts, icnsPath, icnsPath)) util.teardown() util.setup() -test('icon test: .ico specified (should replace with .icns)', createIconTest(iconBase + '.ico', icnsPath)) +test('icon test: .ico specified (should replace with .icns)', createIconTest(baseOpts, iconBase + '.ico', icnsPath)) util.teardown() util.setup() -test('icon test: basename only (should add .icns)', createIconTest(iconBase, icnsPath)) +test('icon test: basename only (should add .icns)', createIconTest(baseOpts, iconBase, icnsPath)) util.teardown() util.setup() @@ -168,7 +162,7 @@ test('codesign test', function (t) { t.timeoutAfter(config.timeout) var opts = Object.create(baseOpts) - opts.sign = '-' // Ad-hoc + opts.sign = true // Ad-hoc var appPath @@ -180,7 +174,7 @@ test('codesign test', function (t) { fs.stat(appPath, cb) }, function (stats, cb) { t.true(stats.isDirectory(), 'The expected .app directory should exist') - exec('codesign --verify --deep ' + appPath, cb) + exec('codesign -v ' + appPath, cb) }, function (stdout, stderr, cb) { t.pass('codesign should verify successfully') cb() @@ -194,17 +188,18 @@ test('codesign test', function (t) { util.teardown() util.setup() -test('app and build version test', createAppVersionTest('1.1.0', '1.1.0.1234')) +test('app and build version test', createAppVersionTest(baseOpts, '1.1.0', '1.1.0.1234')) util.teardown() util.setup() -test('app version test', createAppVersionTest('1.1.0')) +test('app version test', createAppVersionTest(baseOpts, '1.1.0')) util.teardown() util.setup() -test('app and build version integer test', createAppVersionTest(12, 1234)) +test('app and build version integer test', createAppVersionTest(baseOpts, 12, 1234)) util.teardown() util.setup() -test('app categoryType test', createAppCategoryTypeTest('public.app-category.developer-tools')) +test('app categoryType test', createAppCategoryTypeTest(baseOpts, 'public.app-category.developer-tools')) util.teardown() +} diff --git a/test/mas.js b/test/mas.js new file mode 100644 index 00000000..e31b4ffc --- /dev/null +++ b/test/mas.js @@ -0,0 +1,13 @@ +var path = require('path') + +var config = require('./config.json') + +var baseOpts = { + name: 'basicTest', + dir: path.join(__dirname, 'fixtures', 'basic'), + version: config.version, + arch: 'x64', + platform: 'mas' +} + +require('./mac')(baseOpts) diff --git a/test/multitarget.js b/test/multitarget.js index 1c9240de..14566b0b 100644 --- a/test/multitarget.js +++ b/test/multitarget.js @@ -38,8 +38,8 @@ test('all test', function (t) { function (cb) { packager(opts, cb) }, function (finalPaths, cb) { - // Windows skips packaging for OS X, and OS X only has 64-bit releases - t.equal(finalPaths.length, process.platform === 'win32' ? 4 : 5, + // Windows skips packaging for OS X (darwin + mas), and OS X only has 64-bit releases + t.equal(finalPaths.length, process.platform === 'win32' ? 4 : 6, 'packager call should resolve with expected number of paths') verifyPackageExistence(finalPaths, cb) }, function (exists, cb) { diff --git a/test/util.js b/test/util.js index f3b23ccc..b2998171 100644 --- a/test/util.js +++ b/test/util.js @@ -11,17 +11,21 @@ var ORIGINAL_CWD = process.cwd() var WORK_CWD = path.join(__dirname, 'work') var archs = ['ia32', 'x64'] -var platforms = ['darwin', 'linux', 'win32'] +var platforms = ['darwin', 'linux', 'mas', 'win32'] var slice = Array.prototype.slice var version = require('./config.json').version +function isPlatformMac (platform) { + return platform === 'darwin' || platform === 'mas' +} + var combinations = [] archs.forEach(function (arch) { platforms.forEach(function (platform) { // Electron does not have 32-bit releases for Mac OS X, so skip that combination - // Also skip testing darwin target on Windows since electron-packager itself skips it + // Also skip testing darwin/mas target on Windows since electron-packager itself skips it // (see https://github.com/maxogden/electron-packager/issues/71) - if (platform === 'darwin' && (arch === 'ia32' || require('os').platform() === 'win32')) return + if (isPlatformMac(platform) && (arch === 'ia32' || require('os').platform() === 'win32')) return combinations.push({ arch: arch, @@ -59,13 +63,17 @@ exports.forEachCombination = function forEachCombination (cb) { } exports.generateResourcesPath = function generateResourcesPath (opts) { - return opts.platform === 'darwin' ? path.join(opts.name + '.app', 'Contents', 'Resources') : 'resources' + return isPlatformMac(opts.platform) + ? path.join(opts.name + '.app', 'Contents', 'Resources') + : 'resources' } exports.getWorkCwd = function getWorkCwd () { return WORK_CWD } +exports.isPlatformMac = isPlatformMac + // tape doesn't seem to have a provision for before/beforeEach/afterEach/after, // so run setup/teardown and cleanup tasks as additional "tests" to put them in sequence // and run them irrespective of test failures diff --git a/usage.txt b/usage.txt index 20e9d858..753f388f 100644 --- a/usage.txt +++ b/usage.txt @@ -2,7 +2,7 @@ Usage: electron-packager --platform= --arch=. For example, `--asar-unpack-dir=sub_dir` will unpack the directory `//sub_dir`. -build-version build version to set for the app +build-version build version to set for the app (darwin/mas platform only) cache directory of cached Electron downloads. Defaults to '$HOME/.electron' -helper-bundle-id bundle identifier to use in the app helper plist (darwin platform only) +helper-bundle-id bundle identifier to use in the app helper plist (darwin/mas platform only) icon the icon file to use as the icon for the app. Note: Format depends on platform. ignore do not copy files into app whose filenames regex .match this string out the dir to put the app into at the end. defaults to current working dir overwrite if output directory for a platform already exists, replaces it rather than skipping it prune runs `npm prune --production` on the app -sign should contain the identity to be used when running `codesign` (only for building for the darwin platform, on OS X) +sign should contain the identity to be used when running `codesign` (only for building for the darwin/mas platform, on OS X) +sign-entitlements the path to entitlements used in signing (mas platform only) strict-ssl whether SSL certificates are required to be valid when downloading Electron. It defaults to true, use --strict-ssl=false to disable checks. tmpdir temp directory. Defaults to system temp directory. From cf1810e940336918e2730a2443458e43b19b5e8b Mon Sep 17 00:00:00 2001 From: sethlu Date: Tue, 16 Feb 2016 20:46:55 +0800 Subject: [PATCH 2/6] Indent test/mac Indented for clarity in commits --- test/mac.js | 182 ++++++++++++++++++++++++++-------------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/test/mac.js b/test/mac.js index a21ab36a..c50bc3b0 100644 --- a/test/mac.js +++ b/test/mac.js @@ -103,103 +103,103 @@ function createAppCategoryTypeTest (baseOpts, appCategoryType) { // Share testing script with platform darwin and mas module.exports = function (baseOpts) { -util.setup() -test('helper app paths test', function (t) { - t.timeoutAfter(config.timeout) - - function getHelperExecutablePath (helperName) { - return path.join(helperName + '.app', 'Contents', 'MacOS', helperName) - } + util.setup() + test('helper app paths test', function (t) { + t.timeoutAfter(config.timeout) - var opts = Object.create(baseOpts) - var frameworksPath - - waterfall([ - function (cb) { - packager(opts, cb) - }, function (paths, cb) { - frameworksPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Frameworks') - // main Helper.app is already tested in basic test suite; test its executable and the other helpers - fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper')), cb) - }, function (stats, cb) { - t.true(stats.isFile(), 'The Helper.app executable should reflect opts.name') - fs.stat(path.join(frameworksPath, opts.name + ' Helper EH.app'), cb) - }, function (stats, cb) { - t.true(stats.isDirectory(), 'The Helper EH.app should reflect opts.name') - fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper EH')), cb) - }, function (stats, cb) { - t.true(stats.isFile(), 'The Helper EH.app executable should reflect opts.name') - fs.stat(path.join(frameworksPath, opts.name + ' Helper NP.app'), cb) - }, function (stats, cb) { - t.true(stats.isDirectory(), 'The Helper NP.app should reflect opts.name') - fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper NP')), cb) - }, function (stats, cb) { - t.true(stats.isFile(), 'The Helper NP.app executable should reflect opts.name') - cb() + function getHelperExecutablePath (helperName) { + return path.join(helperName + '.app', 'Contents', 'MacOS', helperName) } - ], function (err) { - t.end(err) + + var opts = Object.create(baseOpts) + var frameworksPath + + waterfall([ + function (cb) { + packager(opts, cb) + }, function (paths, cb) { + frameworksPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Frameworks') + // main Helper.app is already tested in basic test suite; test its executable and the other helpers + fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper')), cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The Helper.app executable should reflect opts.name') + fs.stat(path.join(frameworksPath, opts.name + ' Helper EH.app'), cb) + }, function (stats, cb) { + t.true(stats.isDirectory(), 'The Helper EH.app should reflect opts.name') + fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper EH')), cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The Helper EH.app executable should reflect opts.name') + fs.stat(path.join(frameworksPath, opts.name + ' Helper NP.app'), cb) + }, function (stats, cb) { + t.true(stats.isDirectory(), 'The Helper NP.app should reflect opts.name') + fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper NP')), cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The Helper NP.app executable should reflect opts.name') + cb() + } + ], function (err) { + t.end(err) + }) }) -}) -util.teardown() - -var iconBase = path.join(__dirname, 'fixtures', 'monochrome') -var icnsPath = iconBase + '.icns' -util.setup() -test('icon test: .icns specified', createIconTest(baseOpts, icnsPath, icnsPath)) -util.teardown() - -util.setup() -test('icon test: .ico specified (should replace with .icns)', createIconTest(baseOpts, iconBase + '.ico', icnsPath)) -util.teardown() - -util.setup() -test('icon test: basename only (should add .icns)', createIconTest(baseOpts, iconBase, icnsPath)) -util.teardown() - -util.setup() -test('codesign test', function (t) { - t.timeoutAfter(config.timeout) - - var opts = Object.create(baseOpts) - opts.sign = true // Ad-hoc - - var appPath - - waterfall([ - function (cb) { - packager(opts, cb) - }, function (paths, cb) { - appPath = path.join(paths[0], opts.name + '.app') - fs.stat(appPath, cb) - }, function (stats, cb) { - t.true(stats.isDirectory(), 'The expected .app directory should exist') - exec('codesign -v ' + appPath, cb) - }, function (stdout, stderr, cb) { - t.pass('codesign should verify successfully') - cb() - } - ], function (err) { - var notFound = err && err.code === 127 - if (notFound) console.log('codesign not installed; skipped') - t.end(notFound ? null : err) + util.teardown() + + var iconBase = path.join(__dirname, 'fixtures', 'monochrome') + var icnsPath = iconBase + '.icns' + util.setup() + test('icon test: .icns specified', createIconTest(baseOpts, icnsPath, icnsPath)) + util.teardown() + + util.setup() + test('icon test: .ico specified (should replace with .icns)', createIconTest(baseOpts, iconBase + '.ico', icnsPath)) + util.teardown() + + util.setup() + test('icon test: basename only (should add .icns)', createIconTest(baseOpts, iconBase, icnsPath)) + util.teardown() + + util.setup() + test('codesign test', function (t) { + t.timeoutAfter(config.timeout) + + var opts = Object.create(baseOpts) + opts.sign = true // Ad-hoc + + var appPath + + waterfall([ + function (cb) { + packager(opts, cb) + }, function (paths, cb) { + appPath = path.join(paths[0], opts.name + '.app') + fs.stat(appPath, cb) + }, function (stats, cb) { + t.true(stats.isDirectory(), 'The expected .app directory should exist') + exec('codesign -v ' + appPath, cb) + }, function (stdout, stderr, cb) { + t.pass('codesign should verify successfully') + cb() + } + ], function (err) { + var notFound = err && err.code === 127 + if (notFound) console.log('codesign not installed; skipped') + t.end(notFound ? null : err) + }) }) -}) -util.teardown() + util.teardown() -util.setup() -test('app and build version test', createAppVersionTest(baseOpts, '1.1.0', '1.1.0.1234')) -util.teardown() + util.setup() + test('app and build version test', createAppVersionTest(baseOpts, '1.1.0', '1.1.0.1234')) + util.teardown() -util.setup() -test('app version test', createAppVersionTest(baseOpts, '1.1.0')) -util.teardown() + util.setup() + test('app version test', createAppVersionTest(baseOpts, '1.1.0')) + util.teardown() -util.setup() -test('app and build version integer test', createAppVersionTest(baseOpts, 12, 1234)) -util.teardown() + util.setup() + test('app and build version integer test', createAppVersionTest(baseOpts, 12, 1234)) + util.teardown() -util.setup() -test('app categoryType test', createAppCategoryTypeTest(baseOpts, 'public.app-category.developer-tools')) -util.teardown() + util.setup() + test('app categoryType test', createAppCategoryTypeTest(baseOpts, 'public.app-category.developer-tools')) + util.teardown() } From 92a63a9653af357f96bff4fb37cb042ed936739f Mon Sep 17 00:00:00 2001 From: sethlu Date: Tue, 16 Feb 2016 21:04:53 +0800 Subject: [PATCH 3/6] Filter CFBundleIdentifier + set CFBundle properties Fix: ix https://github.com/maxogden/electron-packager/issues/261 Fix: https://github.com/maxogden/electron-packager/issues/163 Added filterCFBundleIdentifier function Properties to set in app plist - CFBundleDisplayName - CFBundleIdentifier - CFBundleName Properties to set in helper plist - CFBundleDisplayName - CFBundleIdentifier - CFBundleName - CFBundleExecutable In testing script: - Added testing with special characters --- mac.js | 34 ++++++++++-- test/mac.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 4 deletions(-) diff --git a/mac.js b/mac.js index eee49036..a857f356 100644 --- a/mac.js +++ b/mac.js @@ -27,6 +27,12 @@ function moveHelpers (frameworksPath, appName, callback) { }) } +function filterCFBundleIdentifier (identifier) { + // Remove special characters and allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) + // Apple documentation: https://developer.apple.com/library/mac/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102070 + return identifier.replace(/ /g, '-').replace(/[^a-zA-Z0-9.-]/g, '') +} + module.exports = { createApp: function createApp (opts, templatePath, callback) { var appRelativePath = path.join('Electron.app', 'Contents', 'Resources', 'app') @@ -37,20 +43,37 @@ module.exports = { var frameworksPath = path.join(contentsPath, 'Frameworks') var appPlistFilename = path.join(contentsPath, 'Info.plist') var helperPlistFilename = path.join(frameworksPath, 'Electron Helper.app', 'Contents', 'Info.plist') + var helperEHPlistFilename = path.join(frameworksPath, 'Electron Helper EH.app', 'Contents', 'Info.plist') + var helperNPPlistFilename = path.join(frameworksPath, 'Electron Helper NP.app', 'Contents', 'Info.plist') var appPlist = plist.parse(fs.readFileSync(appPlistFilename).toString()) var helperPlist = plist.parse(fs.readFileSync(helperPlistFilename).toString()) + var helperEHPlist = plist.parse(fs.readFileSync(helperEHPlistFilename).toString()) + var helperNPPlist = plist.parse(fs.readFileSync(helperNPPlistFilename).toString()) // Update plist files - var defaultBundleName = 'com.electron.' + opts.name.toLowerCase().replace(/ /g, '_') + var defaultBundleName = 'com.electron.' + opts.name.toLowerCase() + var appBundleIdentifier = filterCFBundleIdentifier(opts['app-bundle-id'] || defaultBundleName) + var helperBundleIdentifier = filterCFBundleIdentifier(opts['helper-bundle-id'] || appBundleIdentifier + '.helper') + var appVersion = opts['app-version'] var buildVersion = opts['build-version'] var appCategoryType = opts['app-category-type'] appPlist.CFBundleDisplayName = opts.name - appPlist.CFBundleIdentifier = opts['app-bundle-id'] || defaultBundleName + appPlist.CFBundleIdentifier = appBundleIdentifier appPlist.CFBundleName = opts.name - helperPlist.CFBundleIdentifier = opts['helper-bundle-id'] || defaultBundleName + '.helper' + helperPlist.CFBundleDisplayName = opts.name + ' Helper' + helperPlist.CFBundleIdentifier = helperBundleIdentifier helperPlist.CFBundleName = opts.name + helperPlist.CFBundleExecutable = opts.name + ' Helper' + helperEHPlist.CFBundleDisplayName = opts.name + ' Helper EH' + helperEHPlist.CFBundleIdentifier = helperBundleIdentifier + '.EH' + helperEHPlist.CFBundleName = opts.name + ' Helper EH' + helperEHPlist.CFBundleExecutable = opts.name + ' Helper EH' + helperNPPlist.CFBundleDisplayName = opts.name + ' Helper NP' + helperNPPlist.CFBundleIdentifier = helperBundleIdentifier + '.NP' + helperNPPlist.CFBundleName = opts.name + ' Helper NP' + helperNPPlist.CFBundleExecutable = opts.name + ' Helper NP' if (appVersion) { appPlist.CFBundleShortVersionString = appPlist.CFBundleVersion = '' + appVersion @@ -75,6 +98,8 @@ module.exports = { fs.writeFileSync(appPlistFilename, plist.build(appPlist)) fs.writeFileSync(helperPlistFilename, plist.build(helperPlist)) + fs.writeFileSync(helperEHPlistFilename, plist.build(helperEHPlist)) + fs.writeFileSync(helperNPPlistFilename, plist.build(helperNPPlist)) var operations = [] @@ -126,5 +151,6 @@ module.exports = { common.moveApp(opts, tempPath, callback) }) }) - } + }, + filterCFBundleIdentifier: filterCFBundleIdentifier } diff --git a/test/mac.js b/test/mac.js index c50bc3b0..9c6b2720 100644 --- a/test/mac.js +++ b/test/mac.js @@ -9,6 +9,7 @@ var waterfall = require('run-waterfall') var config = require('./config.json') var util = require('./util') var plist = require('plist') +var filterCFBundleIdentifier = require('../mac').filterCFBundleIdentifier function createIconTest (baseOpts, icon, iconPath) { return function (t) { @@ -101,6 +102,119 @@ function createAppCategoryTypeTest (baseOpts, appCategoryType) { } } +function createAppBundleTest (baseOpts, appBundleId) { + return function (t) { + t.timeoutAfter(config.timeout) + + var plistPath + var opts = Object.create(baseOpts) + if (appBundleId) { + opts['app-bundle-id'] = appBundleId + } + var defaultBundleName = 'com.electron.' + opts.name.toLowerCase() + var appBundleIdentifier = filterCFBundleIdentifier(opts['app-bundle-id'] || defaultBundleName) + + waterfall([ + function (cb) { + packager(opts, cb) + }, function (paths, cb) { + plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist') + fs.stat(plistPath, cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The expected Info.plist file should exist') + fs.readFile(plistPath, 'utf8', cb) + }, function (file, cb) { + var obj = plist.parse(file) + t.equal(obj.CFBundleDisplayName, opts.name, 'CFBundleDisplayName should reflect opts.name') + t.equal(obj.CFBundleName, opts.name, 'CFBundleName should reflect opts.name') + t.equal(obj.CFBundleIdentifier, appBundleIdentifier, 'CFBundleName should reflect opts["app-bundle-id"] or fallback to default') + t.equal(typeof obj.CFBundleDisplayName, 'string', 'CFBundleDisplayName should be a string') + t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string') + t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string') + t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)') + cb() + } + ], function (err) { + t.end(err) + }) + } +} + +function createAppHelpersBundleTest (baseOpts, helperBundleId, appBundleId) { + return function (t) { + t.timeoutAfter(config.timeout) + + var tempPath, plistPath + var opts = Object.create(baseOpts) + if (helperBundleId) { + opts['helper-bundle-id'] = appBundleId + } + if (appBundleId) { + opts['app-bundle-id'] = appBundleId + } + var defaultBundleName = 'com.electron.' + opts.name.toLowerCase() + var appBundleIdentifier = filterCFBundleIdentifier(opts['app-bundle-id'] || defaultBundleName) + var helperBundleIdentifier = filterCFBundleIdentifier(opts['helper-bundle-id'] || appBundleIdentifier + '.helper') + + waterfall([ + function (cb) { + packager(opts, cb) + }, function (paths, cb) { + tempPath = paths[0] + plistPath = path.join(tempPath, opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper.app', 'Contents', 'Info.plist') + fs.stat(plistPath, cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The expected Info.plist file should exist in helper app') + fs.readFile(plistPath, 'utf8', cb) + }, function (file, cb) { + var obj = plist.parse(file) + t.equal(obj.CFBundleName, opts.name, 'CFBundleName should reflect opts.name in helper app') + t.equal(obj.CFBundleIdentifier, helperBundleIdentifier, 'CFBundleName should reflect opts["helper-bundle-id"], opts["app-bundle-id"] or fallback to default in helper app') + t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string in helper app') + t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string in helper app') + t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)') + // check helper EH + plistPath = path.join(tempPath, opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper EH.app', 'Contents', 'Info.plist') + fs.stat(plistPath, cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The expected Info.plist file should exist in helper EH app') + fs.readFile(plistPath, 'utf8', cb) + }, function (file, cb) { + var obj = plist.parse(file) + t.equal(obj.CFBundleName, opts.name + ' Helper EH', 'CFBundleName should reflect opts.name in helper EH app') + t.equal(obj.CFBundleDisplayName, opts.name + ' Helper EH', 'CFBundleDisplayName should reflect opts.name in helper EH app') + t.equal(obj.CFBundleExecutable, opts.name + ' Helper EH', 'CFBundleExecutable should reflect opts.name in helper EH app') + t.equal(obj.CFBundleIdentifier, helperBundleIdentifier + '.EH', 'CFBundleName should reflect opts["helper-bundle-id"], opts["app-bundle-id"] or fallback to default in helper EH app') + t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string in helper EH app') + t.equal(typeof obj.CFBundleDisplayName, 'string', 'CFBundleDisplayName should be a string in helper EH app') + t.equal(typeof obj.CFBundleExecutable, 'string', 'CFBundleExecutable should be a string in helper EH app') + t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string in helper EH app') + t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)') + // check helper NP + plistPath = path.join(tempPath, opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper NP.app', 'Contents', 'Info.plist') + fs.stat(plistPath, cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The expected Info.plist file should exist in helper NP app') + fs.readFile(plistPath, 'utf8', cb) + }, function (file, cb) { + var obj = plist.parse(file) + t.equal(obj.CFBundleName, opts.name + ' Helper NP', 'CFBundleName should reflect opts.name in helper NP app') + t.equal(obj.CFBundleDisplayName, opts.name + ' Helper NP', 'CFBundleDisplayName should reflect opts.name in helper NP app') + t.equal(obj.CFBundleExecutable, opts.name + ' Helper NP', 'CFBundleExecutable should reflect opts.name in helper NP app') + t.equal(obj.CFBundleIdentifier, helperBundleIdentifier + '.NP', 'CFBundleName should reflect opts["helper-bundle-id"], opts["app-bundle-id"] or fallback to default in helper NP app') + t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string in helper NP app') + t.equal(typeof obj.CFBundleDisplayName, 'string', 'CFBundleDisplayName should be a string in helper NP app') + t.equal(typeof obj.CFBundleExecutable, 'string', 'CFBundleExecutable should be a string in helper NP app') + t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string in helper NP app') + t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)') + cb() + } + ], function (err) { + t.end(err) + }) + } +} + // Share testing script with platform darwin and mas module.exports = function (baseOpts) { util.setup() @@ -202,4 +316,36 @@ module.exports = function (baseOpts) { util.setup() test('app categoryType test', createAppCategoryTypeTest(baseOpts, 'public.app-category.developer-tools')) util.teardown() + + util.setup() + test('app bundle test', createAppBundleTest(baseOpts, 'com.electron.basetest')) + util.teardown() + + util.setup() + test('app bundle (w/ special characters) test', createAppBundleTest(baseOpts, 'com.electron."bãśè tëßt!@#$%^&*()?\'')) + util.teardown() + + util.setup() + test('app bundle app-bundle-id fallback test', createAppBundleTest(baseOpts)) + util.teardown() + + util.setup() + test('app helpers bundle test', createAppHelpersBundleTest(baseOpts, 'com.electron.basetest.helper')) + util.teardown() + + util.setup() + test('app helpers bundle (w/ special characters) test', createAppHelpersBundleTest(baseOpts, 'com.electron."bãśè tëßt!@#$%^&*()?\'.hęłpėr')) + util.teardown() + + util.setup() + test('app helpers bundle helper-bundle-id fallback to app-bundle-id test', createAppHelpersBundleTest(baseOpts, null, 'com.electron.basetest')) + util.teardown() + + util.setup() + test('app helpers bundle helper-bundle-id fallback to app-bundle-id (w/ special characters) test', createAppHelpersBundleTest(baseOpts, null, 'com.electron."bãśè tëßt!!@#$%^&*()?\'')) + util.teardown() + + util.setup() + test('app helpers bundle helper-bundle-id & app-bundle-id fallback test', createAppHelpersBundleTest(baseOpts)) + util.teardown() } From 50eb49a1d3f2e90a4ee67c073deae05bce4e57c8 Mon Sep 17 00:00:00 2001 From: sethlu Date: Wed, 17 Feb 2016 14:49:22 +0800 Subject: [PATCH 4/6] Opts app-copyright (NSHumanReadableCopyright on OS X) Fix: maxogden#163 --- mac.js | 5 +++++ readme.md | 4 ++++ test/mac.js | 32 ++++++++++++++++++++++++++++++++ usage.txt | 1 + 4 files changed, 42 insertions(+) diff --git a/mac.js b/mac.js index a857f356..1d8898bd 100644 --- a/mac.js +++ b/mac.js @@ -58,6 +58,7 @@ module.exports = { var appVersion = opts['app-version'] var buildVersion = opts['build-version'] var appCategoryType = opts['app-category-type'] + var humanReadableCopyright = opts['app-copyright'] appPlist.CFBundleDisplayName = opts.name appPlist.CFBundleIdentifier = appBundleIdentifier @@ -96,6 +97,10 @@ module.exports = { appPlist.LSApplicationCategoryType = appCategoryType } + if (humanReadableCopyright) { + appPlist.NSHumanReadableCopyright = humanReadableCopyright + } + fs.writeFileSync(appPlistFilename, plist.build(appPlist)) fs.writeFileSync(helperPlistFilename, plist.build(helperPlist)) fs.writeFileSync(helperEHPlistFilename, plist.build(helperEHPlist)) diff --git a/readme.md b/readme.md index cd654b5e..a3cc7bb7 100644 --- a/readme.md +++ b/readme.md @@ -137,6 +137,10 @@ packager(opts, function done (err, appPath) { }) Valid values are listed in [Apple's documentation](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/uid/TP40009250-SW8). +`app-copyright` - *String* + + The copyrights string to use in the app plist, will be displayed in the application About box (OS X only). + `app-version` - *String* The release version of the application. Maps to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString` on OS X. diff --git a/test/mac.js b/test/mac.js index 9c6b2720..2bf68268 100644 --- a/test/mac.js +++ b/test/mac.js @@ -215,6 +215,34 @@ function createAppHelpersBundleTest (baseOpts, helperBundleId, appBundleId) { } } +function createAppHumanReadableCopyrightTest (baseOpts, humanReadableCopyright) { + return function (t) { + t.timeoutAfter(config.timeout) + + var plistPath + var opts = Object.create(baseOpts) + opts['app-copyright'] = humanReadableCopyright + + waterfall([ + function (cb) { + packager(opts, cb) + }, function (paths, cb) { + plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist') + fs.stat(plistPath, cb) + }, function (stats, cb) { + t.true(stats.isFile(), 'The expected Info.plist file should exist') + fs.readFile(plistPath, 'utf8', cb) + }, function (file, cb) { + var obj = plist.parse(file) + t.equal(obj.NSHumanReadableCopyright, opts['app-copyright'], 'NSHumanReadableCopyright should reflect opts["app-copyright"]') + cb() + } + ], function (err) { + t.end(err) + }) + } +} + // Share testing script with platform darwin and mas module.exports = function (baseOpts) { util.setup() @@ -348,4 +376,8 @@ module.exports = function (baseOpts) { util.setup() test('app helpers bundle helper-bundle-id & app-bundle-id fallback test', createAppHelpersBundleTest(baseOpts)) util.teardown() + + util.setup() + test('app humanReadableCopyright test', createAppHumanReadableCopyrightTest(baseOpts, 'Copyright © 2003–2015 Organization. All rights reserved.')) + util.teardown() } diff --git a/usage.txt b/usage.txt index 753f388f..2122630b 100644 --- a/usage.txt +++ b/usage.txt @@ -13,6 +13,7 @@ all equivalent to --platform=all --arch=all app-bundle-id bundle identifier to use in the app plist (darwin/mas platform only) app-category-type the application category type (darwin/mas platform only) For example, `app-category-type=public.app-category.developer-tools` will set the application category to 'Developer Tools'. +app-copyright copyrights string to use in the app plist (darwin/mas platform only) app-version release version to set for the app asar packages the source code within your app into an archive asar-unpack unpacks the files to app.asar.unpacked directory whose filenames regex .match this string From ad1e9f7b5f09a4d824cd28fd9dd2dd7ddcaff6cc Mon Sep 17 00:00:00 2001 From: sethlu Date: Wed, 17 Feb 2016 14:50:04 +0800 Subject: [PATCH 5/6] Use Electron v0.35.6 for testing The latest version of Electron to best support `mas` build --- test/config.json | 2 +- test/fixtures/basic/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/config.json b/test/config.json index 5c6c3707..7d6ada01 100644 --- a/test/config.json +++ b/test/config.json @@ -1,4 +1,4 @@ { "timeout": 30000, - "version": "0.28.3" + "version": "0.35.6" } diff --git a/test/fixtures/basic/package.json b/test/fixtures/basic/package.json index b4ad8f76..3fb8f849 100644 --- a/test/fixtures/basic/package.json +++ b/test/fixtures/basic/package.json @@ -7,6 +7,6 @@ "devDependencies": { "ncp": "^2.0.0", "run-waterfall": "^1.1.1", - "electron-prebuilt": "0.36.4" + "electron-prebuilt": "0.35.6" } } From 921291990e4bc673e81be73c716b52382220a939 Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sat, 13 Feb 2016 15:29:09 -0800 Subject: [PATCH 6/6] Add basic Mac App Store docs + differentiate between host & target platforms --- readme.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index a3cc7bb7..f3de3c1f 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # electron-packager -Package your [Electron](http://electron.atom.io) app into OS-specific bundles (`.app`, `.exe`, etc.) via JavaScript or the command line. Supports building Windows, Linux or Mac executables. +Package your [Electron](http://electron.atom.io) app into OS-specific bundles (`.app`, `.exe`, etc.) via JavaScript or the command line. [![Build Status](https://travis-ci.org/maxogden/electron-packager.svg?branch=master)](https://travis-ci.org/maxogden/electron-packager) [![Coverage Status](https://coveralls.io/repos/github/maxogden/electron-packager/badge.svg?branch=master)](https://coveralls.io/github/maxogden/electron-packager?branch=master) @@ -13,6 +13,22 @@ This module was developed as part of [Dat](http://dat-data.com/), a grant funded Note that packaged Electron applications can be relatively large. A zipped barebones OS X Electron application is around 40MB. +## Supported Platforms + +Electron Packager is known to run on the following **host** platforms: + +* Windows (32/64 bit) +* OS X +* Linux (x86/x86_64) + +It generates executables/bundles for the following **target** platforms: + +* Windows (also known as `win32`, for both 32/64 bit) +* OS X (also known as `darwin`) / [Mac App Store](http://electron.atom.io/docs/v0.36.0/tutorial/mac-app-store-submission-guide/) (also known as `mas`)* +* Linux (for both x86/x86_64) + +* *Note for OS X / MAS target bundles: the `.app` bundle can only be signed when building on a host OS X platform.* + ## Installation ```sh @@ -203,7 +219,7 @@ If the file extension is omitted, it is auto-completed to the correct extension `sign` - *String* - The identity used when signing the package via `codesign`. (Only for the OS X target platform, when XCode is present on the build platform.) + The identity used when signing the package via `codesign`. (Only for the OS X / Mac App Store target platforms, when XCode is present on the host platform.) `sign-entitlements` - *String* @@ -245,7 +261,7 @@ If the file extension is omitted, it is auto-completed to the correct extension ## Building Windows apps from non-Windows platforms -Building an Electron app for the Windows platform with a custom icon requires editing the `Electron.exe` file. Currently, electron-packager uses [node-rcedit](https://github.com/atom/node-rcedit) to accomplish this. A Windows executable is bundled in that node package and needs to be run in order for this functionality to work, so on non-Windows platforms, [Wine](https://www.winehq.org/) needs to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/). +Building an Electron app for the Windows platform with a custom icon requires editing the `Electron.exe` file. Currently, electron-packager uses [node-rcedit](https://github.com/atom/node-rcedit) to accomplish this. A Windows executable is bundled in that node package and needs to be run in order for this functionality to work, so on non-Windows host platforms, [Wine](https://www.winehq.org/) needs to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/). ## Related