UNPKG

electron-packager

Version:

Package and distribute your Electron app with OS-specific bundles (.app, .exe etc) via JS or CLI

208 lines (175 loc) 8.47 kB
var common = require('./common') var fs = require('fs-extra') var path = require('path') var plist = require('plist') var series = require('run-series') var sign = require('electron-osx-sign') function rename (basePath, oldName, newName, cb) { fs.move(path.join(basePath, oldName), path.join(basePath, newName), cb) } function moveHelpers (frameworksPath, appName, callback) { series([' Helper', ' Helper EH', ' Helper NP'].map(function (suffix) { return function (cb) { var executableBasePath = path.join(frameworksPath, `Electron${suffix}.app`, 'Contents', 'MacOS') rename(executableBasePath, `Electron${suffix}`, appName + suffix, function (err) { if (err) return cb(err) rename(frameworksPath, `Electron${suffix}.app`, appName + suffix + '.app', cb) }) } }), function (err) { callback(err) }) } 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, '') } function createSignOpts (properties, platform, app) { // use default sign opts if osx-sign is true, otherwise clone osx-sign object var signOpts = properties === true ? {identity: null} : Object.assign({}, properties) // osx-sign options are handed off to sign module, but // with a few additions from the main options // user may think they can pass platform or app, but they will be ignored common.subOptionWarning(signOpts, 'osx-sign', 'platform', platform) common.subOptionWarning(signOpts, 'osx-sign', 'app', app) if (signOpts.binaries) { console.warn('WARNING: osx-sign.binaries signing will fail. Sign manually, or with electron-osx-sign.') } // Take argument osx-sign as signing identity: // if opts['osx-sign'] is true (bool), fallback to identity=null for // autodiscovery. Otherwise, provide signing certificate info. if (signOpts.identity === true) { signOpts.identity = null } return signOpts } module.exports = { createApp: function createApp (opts, templatePath, callback) { var appRelativePath = path.join('Electron.app', 'Contents', 'Resources', 'app') common.initializeApp(opts, templatePath, appRelativePath, function buildMacApp (err, tempPath) { if (err) return callback(err) var contentsPath = path.join(tempPath, 'Electron.app', 'Contents') 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 // If an extend-info file was supplied, copy its contents in first if (opts['extend-info']) { var extendAppPlist = plist.parse(fs.readFileSync(opts['extend-info']).toString()) for (var key in extendAppPlist) { appPlist[key] = extendAppPlist[key] } } // Now set fields based on explicit options 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'] var humanReadableCopyright = opts['app-copyright'] appPlist.CFBundleDisplayName = opts.name appPlist.CFBundleIdentifier = appBundleIdentifier appPlist.CFBundleName = opts.name helperPlist.CFBundleDisplayName = opts.name + ' Helper' helperPlist.CFBundleIdentifier = helperBundleIdentifier appPlist.CFBundleExecutable = opts.name 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 } if (buildVersion) { appPlist.CFBundleVersion = '' + buildVersion } if (opts.protocols && opts.protocols.length) { appPlist.CFBundleURLTypes = opts.protocols.map(function (protocol) { return { CFBundleURLName: protocol.name, CFBundleURLSchemes: [].concat(protocol.schemes) } }) } if (appCategoryType) { 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)) fs.writeFileSync(helperNPPlistFilename, plist.build(helperNPPlist)) var operations = [] // Copy in the icon, if supplied if (opts.icon) { operations.push(function (cb) { common.normalizeExt(opts.icon, '.icns', function (err, icon) { if (err) { // Ignore error if icon doesn't exist, in case it's only available for other OS cb(null) } else { fs.copy(icon, path.join(contentsPath, 'Resources', appPlist.CFBundleIconFile), cb) } }) }) } // Copy in any other extras var extras = opts['extra-resource'] if (extras) { if (!Array.isArray(extras)) extras = [extras] extras.forEach(function (val) { operations.push(function (cb) { fs.copy(val, path.join(contentsPath, 'Resources', path.basename(val)), cb) }) }) } // Rename the Contents/MacOS/Electron binary operations.push(function (cb) { rename(path.join(contentsPath, 'MacOS'), 'Electron', appPlist.CFBundleExecutable, cb) }) // Move Helper apps/executables, then top-level .app var finalAppPath = path.join(tempPath, `${opts.name}.app`) operations.push(function (cb) { moveHelpers(frameworksPath, opts.name, cb) }, function (cb) { fs.move(path.dirname(contentsPath), finalAppPath, cb) }) if ((opts.platform === 'all' || opts.platform === 'mas') && opts['osx-sign'] === undefined) { console.warn('WARNING: signing is required for mas builds. Provide the osx-sign option, or manually sign the app later.') } if (opts['osx-sign']) { operations.push(function (cb) { sign(createSignOpts(opts['osx-sign'], opts.platform, finalAppPath), function (err) { if (err) { // Although not signed successfully, the application is packed. console.warn('Code sign failed; please retry manually.', err) } cb() }) }) } series(operations, function (err) { if (err) return callback(err) common.moveApp(opts, tempPath, callback) }) }) }, createSignOpts: createSignOpts, filterCFBundleIdentifier: filterCFBundleIdentifier }