From a157f716a572515a2d117a8bb51e9da3e34022d3 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Fri, 19 Jun 2015 22:36:15 -0400 Subject: [PATCH] Implement multi-target options and refactor code This adds support for --all, --platform=all, and --arch=all. In order to accommodate outputting multiple directories for multiple platforms and architectures, this also implements a new directory structure under the output folder (distinguished by both platform and arch). This structure is applied even to OS X distributions, which formerly were output directly to an .app folder. This could be considered a backwards-incompatible change. One other backwards-incompatible change is the value that the packager function passes to the callback, which is now always an array of paths, rather than just a single path. The behavior of the icon option has also been modified to use its basename and apply .ico or .icns depending on platform, to make it usable with --all and --platform=all. This attempts to maximize backwards compatibility, by allowing a full filename to be specified, but replacing the filename's extension with what is appropriate for each target platform. Alternatively, the extension can now be omitted. In the process of implementing this, it became evident that some things were being done in 3 different places, and weren't always being done consistently, so I've deduplicated everything I could. This also includes a few other changes to improve stability for multi-target runs, and other fixes: * Avoid targeting darwin if the environment doesn't support symlinks, to avoid the process bailing out on Windows * Implement --overwrite centrally in index.js such that it explicitly skips if an output directory already exists, for consistency with all target platforms and to avoid any possible errors that would halt operation during one target of a multi-target run * Use ncp instead of mv to move to finalPath, which avoids flakiness I noticed when testing on Windows 8 especially with multi-target runs * Simplify temp directory logic by using a nested structure, so there is only one top-level directory to clean up * Reinstate fix from #55 which seems to have been clobbered by a later merge * linux.createApp now resolves to the final output directory; it was formerly resolving to the executable path * mac.createApp now replaces space with underscore in bundle IDs * Only the platform modules that are needed are loaded * The win32 module only loads rcedit if needed This also fixes a couple of missing updates to docs (readme/usage). This commit addresses the following issues: * Resolves #40 * Resolves #38 * Resolves #70 * Works around #71 * Resolves #84 by reinstating #55 --- .gitignore | 1 - cli.js | 9 ++- common.js | 159 +++++++++++++++++++++++++++++--------- index.js | 190 +++++++++++++++++++++++++++++++++++---------- linux.js | 71 ++--------------- mac.js | 213 +++++++++++++++++---------------------------------- package.json | 3 +- readme.md | 33 ++++++-- usage.txt | 15 ++-- win32.js | 104 +++++++------------------ 10 files changed, 421 insertions(+), 377 deletions(-) diff --git a/.gitignore b/.gitignore index 7e95763..53d98e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -downloaded/*.zip node_modules .DS_Store test/dist \ No newline at end of file diff --git a/cli.js b/cli.js index a01e653..9bb0c4f 100755 --- a/cli.js +++ b/cli.js @@ -1,6 +1,6 @@ #!/usr/bin/env node var fs = require('fs') -var args = require('minimist')(process.argv.slice(2), {boolean: ['prune', 'asar', 'overwrite']}) +var args = require('minimist')(process.argv.slice(2), {boolean: ['prune', 'asar', 'all', 'overwrite']}) var packager = require('./') var usage = fs.readFileSync(__dirname + '/usage.txt').toString() @@ -16,17 +16,18 @@ if (protocolSchemes && protocolNames && protocolNames.length === protocolSchemes }) } -if (!args.dir || !args.name || !args.platform || !args.arch || !args.version) { +if (!args.dir || !args.name || !args.version || (!args.all && (!args.platform || !args.arch))) { console.error(usage) process.exit(1) } -packager(args, function done (err, appPath) { +packager(args, function done (err, appPaths) { if (err) { if (err.message) console.error(err.message) else console.error(err, err.stack) process.exit(1) } - console.error('Wrote new app to', appPath) + if (appPaths.length > 1) console.error('Wrote new apps to:\n' + appPaths.join('\n')) + else if (appPaths.length === 1) console.error('Wrote new app to', appPaths[0]) }) diff --git a/common.js b/common.js index 2d194a0..84ccd7f 100644 --- a/common.js +++ b/common.js @@ -1,50 +1,139 @@ -var asar = require('asar') var child = require('child_process') +var fs = require('fs') +var os = require('os') var path = require('path') + +var asar = require('asar') +var mkdirp = require('mkdirp') +var ncp = require('ncp').ncp var rimraf = require('rimraf') +var series = require('run-series') + +function asarApp (appPath, cb) { + var src = path.join(appPath) + var dest = path.join(appPath, '..', 'app.asar') + asar.createPackage(src, dest, function (err) { + if (err) return cb(err) + rimraf(src, function (err) { + if (err) return cb(err) + cb(null, dest) + }) + }) +} + +function generateFinalBasename (opts) { + return opts.name + '-' + opts.platform + '-' + opts.arch +} + +function generateFinalPath (opts) { + return path.join(opts.out || process.cwd(), generateFinalBasename(opts)) +} + +function userIgnoreFilter (opts) { + return function filter (file) { + file = file.split(path.resolve(opts.dir))[1] + + if (path.sep === '\\') { + // convert slashes so unix-format ignores work + file = file.replace(/\\/g, '/') + } + + var ignore = opts.ignore || [] + if (!Array.isArray(ignore)) ignore = [ignore] + for (var i = 0; i < ignore.length; i++) { + if (file.match(ignore[i])) { + return false + } + } + return true + } +} module.exports = { - asarApp: function asarApp (finalDir, cb) { - var src = path.join(finalDir, 'app') - var dest = path.join(finalDir, 'app.asar') - asar.createPackage(src, dest, function (err) { - if (err) return cb(err) - rimraf(src, function (err) { - if (err) return cb(err) - cb(null, dest) + generateFinalPath: generateFinalPath, + + initializeApp: function initializeApp (opts, templatePath, appRelativePath, callback) { + // Performs the following initial operations for an app: + // * Creates temporary directory + // * Copies template into temporary directory + // * Copies user's app into temporary directory + // * Prunes non-production node_modules (if opts.prune is set) + // * Creates an asar (if opts.asar is set) + + var tempParent = path.join(os.tmpdir(), 'electron-packager', opts.platform + '-' + opts.arch) + var tempPath = path.join(tempParent, generateFinalBasename(opts)) + // Path to `app` directory + var appPath = path.join(tempPath, appRelativePath) + + var operations = [ + function (cb) { + rimraf(tempParent, function () { + // Ignore errors (e.g. directory didn't exist anyway) + cb() + }) + }, + function (cb) { + mkdirp(tempPath, cb) + }, + function (cb) { + ncp(templatePath, tempPath, cb) + }, + function (cb) { + ncp(opts.dir, appPath, {filter: userIgnoreFilter(opts), dereference: true}, cb) + } + ] + + // Prune and asar are now performed before platform-specific logic, primarily so that + // appPath is predictable (e.g. before .app is renamed for mac) + if (opts.prune) { + operations.push(function (cb) { + child.exec('npm prune --production', {cwd: appPath}, cb) }) + } + + if (opts.asar) { + operations.push(function (cb) { + asarApp(path.join(appPath), cb) + }) + } + + series(operations, function (err) { + if (err) return callback(err) + // Resolve to path to temporary app folder for platform-specific processes to use + callback(null, tempPath) }) }, - prune: function prune (opts, cwd, cb, nextStep) { - if (opts.prune) { - child.exec('npm prune --production', { cwd: cwd }, function pruned (err) { - if (err) return cb(err) - nextStep() - }) - } else { - nextStep() - } + moveApp: function finalizeApp (opts, tempPath, callback) { + var finalPath = generateFinalPath(opts) + // Prefer ncp over mv (which seems to cause issues on Win8) + series([ + function (cb) { + mkdirp(finalPath, cb) + }, + function (cb) { + ncp(tempPath, finalPath, cb) + } + ], function (err) { + callback(err, finalPath) + }) }, - userIgnoreFilter: function userIgnoreFilter (opts, finalDir) { - return function filter (file) { - if (path.sep === '\\') { - // convert slashes so unix-format ignores work - file = file.replace(/\\/g, '/') - } + normalizeExt: function normalizeExt (filename, targetExt, cb) { + // Forces a filename to a given extension and fires the given callback with the normalized filename, + // if it exists. Otherwise reports the error from the fs.stat call. + // (Used for resolving icon filenames, particularly during --all runs.) - var ignore = opts.ignore || [] - if (!Array.isArray(ignore)) ignore = [ignore] - if (typeof finalDir !== 'undefined') { - ignore = ignore.concat([finalDir]) - } - for (var i = 0; i < ignore.length; i++) { - if (file.match(ignore[i])) { - return false - } - } - return true + // This error path is used by win32.js if no icon is specified + if (!filename) return cb(new Error('No filename specified to normalizeExt')) + + var ext = path.extname(filename) + if (ext !== targetExt) { + filename = filename.slice(0, filename.length - ext.length) + targetExt } + + fs.stat(filename, function (err) { + cb(err, err ? null : filename) + }) } } diff --git a/index.js b/index.js index 6d5ed16..b506245 100644 --- a/index.js +++ b/index.js @@ -1,59 +1,169 @@ var path = require('path') +var fs = require('fs') var os = require('os') var download = require('electron-download') var extract = require('extract-zip') var mkdirp = require('mkdirp') var rimraf = require('rimraf') +var series = require('run-series') +var common = require('./common') -var mac = require('./mac.js') -var linux = require('./linux.js') -var win32 = require('./win32.js') +var supportedArchs = { + ia32: 1, + x64: 1 +} + +var supportedPlatforms = { + // Maps to module ID for each platform (lazy-required if used) + darwin: './mac', + linux: './linux', + win32: './win32' +} + +var tempBase = path.join(os.tmpdir(), 'electron-packager') + +function testSymlink (cb) { + var testPath = path.join(tempBase, 'symlink-test') + var testFile = path.join(testPath, 'test') + var testLink = path.join(testPath, 'testlink') + series([ + function (cb) { + mkdirp(testPath, cb) + }, + function (cb) { + fs.writeFile(testFile, '', cb) + }, + function (cb) { + fs.symlink(testFile, testLink, cb) + } + ], function (err) { + var result = !err + rimraf(testPath, function () { + cb(result) // ignore errors on cleanup + }) + }) +} + +function validateList (list, supported, name) { + // Validates list of architectures or platforms. + // Returns a normalized array if successful, or an error message string otherwise. + + if (!list) return 'Must specify ' + name + if (list === 'all') return Object.keys(supported) + + if (!Array.isArray(list)) list = list.split(',') + for (var i = list.length; i--;) { + if (!supported[list[i]]) { + return 'Unsupported ' + name + ' ' + list[i] + '; must be one of: ' + Object.keys(supported).join(', ') + } + } + + return list +} + +function createSeries (opts, archs, platforms) { + 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 + if (platform === 'darwin' && arch === 'ia32') return + combinations.push({ + platform: platform, + arch: arch, + version: opts.version + }) + }) + }) + + return [ + function (cb) { + rimraf(tempBase, cb) + } + ].concat(combinations.map(function (combination) { + var arch = combination.arch + var platform = combination.platform + var version = combination.version + + return function (callback) { + download(combination, function (err, zipPath) { + if (err) return callback(err) + + var tmpDir = path.join(tempBase, platform + '-' + arch + '-template') + + var operations = [ + function (cb) { + mkdirp(tmpDir, cb) + }, + function (cb) { + extract(zipPath, {dir: tmpDir}, cb) + } + ] + + function createApp (comboOpts) { + console.error('Packaging app for platform', platform + ' ' + arch, 'using electron v' + version) + series(operations, function () { + require(supportedPlatforms[platform]).createApp(comboOpts, tmpDir, callback) + }) + } + + function checkOverwrite () { + // Create delegated options object with specific platform and arch, for output directory naming + var comboOpts = Object.create(opts) + comboOpts.arch = arch + comboOpts.platform = platform + + var finalPath = common.generateFinalPath(comboOpts) + fs.exists(finalPath, function (exists) { + if (exists) { + if (opts.overwrite) { + rimraf(finalPath, function () { + createApp(comboOpts) + }) + } else { + console.error('Skipping ' + platform + ' ' + arch + + ' (output dir already exists, use --overwrite to force)') + callback() + } + } else { + createApp(comboOpts) + } + }) + } + + if (combination.platform === 'darwin') { + testSymlink(function (result) { + if (result) return checkOverwrite() + + console.error('Cannot create symlinks; skipping darwin platform') + callback() + }) + } else { + checkOverwrite() + } + }) + } + })) +} module.exports = function packager (opts, cb) { - var platformPackager - var platform = opts.platform - var arch = opts.arch - var version = opts.version - - if (!platform || !arch || !version) cb(new Error('Must specify platform, arch and version')) - - switch (arch) { - case 'ia32': break - case 'x64': break - default: return cb(new Error('Unsupported arch. Must be either ia32 or x64')) - } - - switch (platform) { - case 'darwin': platformPackager = mac; break - case 'linux': platformPackager = linux; break - case 'win32': platformPackager = win32; break - default: return cb(new Error('Unsupported platform. Must be either darwin, linux, or win32')) - } + var archs = validateList(opts.all ? 'all' : opts.arch, supportedArchs, 'arch') + var platforms = validateList(opts.all ? 'all' : opts.platform, supportedPlatforms, 'platform') + if (!opts.version) return cb(new Error('Must specify version')) + if (!Array.isArray(archs)) return cb(new Error(archs)) + if (!Array.isArray(platforms)) return cb(new Error(platforms)) // Ignore this and related modules by default var defaultIgnores = ['/node_modules/electron-prebuilt($|/)', '/node_modules/electron-packager($|/)', '/\.git($|/)'] if (opts.ignore && !Array.isArray(opts.ignore)) opts.ignore = [opts.ignore] opts.ignore = (opts.ignore) ? opts.ignore.concat(defaultIgnores) : defaultIgnores - download({ - platform: platform, - arch: arch, - version: version - }, function (err, zipPath) { + series(createSeries(opts, archs, platforms), function (err, appPaths) { if (err) return cb(err) - console.error('Packaging app for platform', platform + ' ' + arch, 'using electron v' + version) - // extract zip into tmp so that packager can use it as a template - var tmpDir = path.join(os.tmpdir(), 'electron-packager-' + platform + '-template') - rimraf(tmpDir, function (err) { - if (err) {} // ignore err - mkdirp(tmpDir, function (err) { - if (err) return cb(err) - extract(zipPath, {dir: tmpDir}, function (err) { - if (err) return cb(err) - platformPackager.createApp(opts, tmpDir, cb) - }) - }) - }) + + cb(null, appPaths.filter(function (appPath) { + // Remove falsy entries (e.g. skipped platforms) + return appPath + })) }) } diff --git a/linux.js b/linux.js index 29c6f24..5e05ae1 100644 --- a/linux.js +++ b/linux.js @@ -1,70 +1,15 @@ var path = require('path') -var fs = require('fs') -var mkdirp = require('mkdirp') -var ncp = require('ncp').ncp -var rimraf = require('rimraf') +var mv = require('mv') var common = require('./common') module.exports = { - createApp: function createApp (opts, templateApp, cb) { - var finalDir = path.join(opts.out || process.cwd(), opts.name + '-linux') - var userAppDir = path.join(finalDir, 'resources', 'default_app') - var originalBinary = path.join(finalDir, 'electron') - var finalBinary = path.join(finalDir, opts.name) - - function copyApp () { - var createApp = function (err) { - if (err) return cb(err) - mkdirp(finalDir, function AppFolderCreated (err) { - if (err) return cb(err) - copyAppTemplate() - }) - } - if (opts.overwrite) { - fs.exists(finalDir, function (exists) { - if (exists) { - console.log('Overwriting existing ' + finalDir + ' ...') - rimraf(finalDir, createApp) - } else { - createApp() - } - }) - } else { - createApp() - } - } - - function copyAppTemplate () { - ncp(templateApp, finalDir, {filter: appFilter}, function AppCreated (err) { - if (err) return cb(err) - copyUserApp() + createApp: function createApp (opts, templatePath, callback) { + common.initializeApp(opts, templatePath, path.join('resources', 'app'), function buildLinuxApp (err, tempPath) { + if (err) return callback(err) + mv(path.join(tempPath, 'electron'), path.join(tempPath, opts.name), function (err) { + if (err) return callback(err) + common.moveApp(opts, tempPath, callback) }) - } - - function copyUserApp () { - ncp(opts.dir, userAppDir, {filter: common.userIgnoreFilter(opts, finalDir), dereference: true}, function copied (err) { - if (err) return cb(err) - common.prune(opts, userAppDir, cb, renameElectronBinary) - }) - } - - function renameElectronBinary () { - fs.rename(originalBinary, finalBinary, function electronRenamed (err) { - var asarDir - if (err) return cb(err) - if (opts.asar) { - asarDir = path.join(finalDir, 'resources') - common.asarApp(asarDir, cb) - } else { - cb(null, finalBinary) - } - }) - } - - function appFilter (file) { - return file.match(/default_app/) === null - } - - copyApp() + }) } } diff --git a/mac.js b/mac.js index e541750..6045488 100644 --- a/mac.js +++ b/mac.js @@ -1,160 +1,87 @@ -var os = require('os') var path = require('path') var fs = require('fs') var child = require('child_process') var plist = require('plist') -var mkdirp = require('mkdirp') -var rimraf = require('rimraf') var mv = require('mv') var ncp = require('ncp').ncp +var series = require('run-series') var common = require('./common') module.exports = { - createApp: function createApp (opts, electronPath, cb) { - var electronApp = path.join(electronPath, 'Electron.app') - var tmpDir = path.join(os.tmpdir(), 'electron-packager-mac') + 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 newApp = path.join(tmpDir, opts.name + '.app') + var contentsPath = path.join(tempPath, 'Electron.app', 'Contents') + var helperPath = path.join(contentsPath, 'Frameworks', 'Electron Helper.app') + var appPlistFilename = path.join(contentsPath, 'Info.plist') + var helperPlistFilename = path.join(helperPath, 'Contents', 'Info.plist') + var appPlist = plist.parse(fs.readFileSync(appPlistFilename).toString()) + var helperPlist = plist.parse(fs.readFileSync(helperPlistFilename).toString()) - // reset build folders + copy template app - rimraf(tmpDir, function rmrfd () { - // ignore errors - mkdirp(newApp, function mkdirpd () { - // ignore errors - // copy .app folder and use as template (this is exactly what Atom editor does) - ncp(electronApp, newApp, function copied (err) { - if (err) return cb(err) - buildMacApp(opts, cb, newApp) + // Update plist files + var defaultBundleName = 'com.electron.' + opts.name.toLowerCase().replace(/ /g, '_') + var appVersion = opts['app-version'] + + appPlist.CFBundleDisplayName = opts.name + appPlist.CFBundleIdentifier = opts['app-bundle-id'] || defaultBundleName + appPlist.CFBundleName = opts.name + helperPlist.CFBundleIdentifier = opts['helper-bundle-id'] || defaultBundleName + '.helper' + helperPlist.CFBundleName = opts.name + + if (appVersion) { + appPlist.CFBundleVersion = appVersion + } + + if (opts.protocols) { + helperPlist.CFBundleURLTypes = appPlist.CFBundleURLTypes = opts.protocols.map(function (protocol) { + return { + CFBundleURLName: protocol.name, + CFBundleURLSchemes: [].concat(protocol.schemes) + } }) + } + + fs.writeFileSync(appPlistFilename, plist.build(appPlist)) + fs.writeFileSync(helperPlistFilename, plist.build(helperPlist)) + + var operations = [] + + 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 { + ncp(icon, path.join(contentsPath, 'Resources', 'atom.icns'), cb) + } + }) + }) + } + + // Move Helper binary, then Helper.app, then top-level .app + var finalAppPath = path.join(tempPath, opts.name + '.app') + operations.push(function (cb) { + var helperBinaryPath = path.join(helperPath, 'Contents', 'MacOS') + mv(path.join(helperBinaryPath, 'Electron Helper'), path.join(helperBinaryPath, opts.name + ' Helper'), cb) + }, function (cb) { + mv(helperPath, path.join(path.dirname(helperPath), opts.name + ' Helper.app'), cb) + }, function (cb) { + mv(path.dirname(contentsPath), finalAppPath, cb) + }) + + if (opts.sign) { + operations.push(function (cb) { + child.exec('codesign --deep --force --sign "' + opts.sign + '" ' + finalAppPath, cb) + }) + } + + series(operations, function () { + common.moveApp(opts, tempPath, callback) }) }) } } - -function buildMacApp (opts, cb, newApp) { - var paths = { - info1: path.join(newApp, 'Contents', 'Info.plist'), - info2: path.join(newApp, 'Contents', 'Frameworks', 'Electron Helper.app', 'Contents', 'Info.plist'), - app: path.join(newApp, 'Contents', 'Resources', 'app'), - helper: path.join(newApp, 'Contents', 'Frameworks', 'Electron Helper.app') - } - - // update plist files - var pl1 = plist.parse(fs.readFileSync(paths.info1).toString()) - var pl2 = plist.parse(fs.readFileSync(paths.info2).toString()) - - var bundleId = opts['app-bundle-id'] || 'com.electron.' + opts.name.toLowerCase() - var bundleHelperId = opts['helper-bundle-id'] || 'com.electron.' + opts.name.toLowerCase() + '.helper' - var appVersion = opts['app-version'] - - pl1.CFBundleDisplayName = opts.name - pl1.CFBundleIdentifier = bundleId - pl1.CFBundleName = opts.name - pl2.CFBundleIdentifier = bundleHelperId - pl2.CFBundleName = opts.name - - if (appVersion) { - pl1.CFBundleVersion = appVersion - } - - if (opts.protocols) { - pl2.CFBundleURLTypes = pl1.CFBundleURLTypes = opts.protocols.map(function (protocol) { - return { - CFBundleURLName: protocol.name, - CFBundleURLSchemes: [].concat(protocol.schemes) - } - }) - } - - fs.writeFileSync(paths.info1, plist.build(pl1)) - fs.writeFileSync(paths.info2, plist.build(pl2)) - - // copy users app into .app - ncp(opts.dir, paths.app, {filter: common.userIgnoreFilter(opts), dereference: true}, function copied (err) { - if (err) return cb(err) - - function moveHelper () { - // Move helper binary before moving the parent helper app directory itself - var helperDestination = path.join(path.dirname(paths.helper), opts.name + ' Helper.app') - var helperBinary = path.join(paths.helper, 'Contents', 'MacOS', 'Electron Helper') - var helperBinaryDestination = path.join(path.dirname(helperBinary), opts.name + ' Helper') - - fs.rename(helperBinary, helperBinaryDestination, function (err) { - if (err) return cb(err) - fs.rename(paths.helper, helperDestination, function (err) { - if (err) return cb(err) - moveApp() - }) - }) - } - - function moveApp () { - // finally, move app into cwd - var outdir = opts.out || process.cwd() - var finalPath = path.join(outdir, opts.name + '.app') - - mkdirp(outdir, function mkoutdirp () { - if (err) return cb(err) - if (opts.overwrite) { - fs.exists(finalPath, function (exists) { - if (exists) { - console.log('Overwriting existing ' + finalPath + ' ...') - rimraf(finalPath, deploy) - } else { - deploy() - } - }) - } else { - deploy() - } - - function deploy (err) { - if (err) return cb(err) - mv(newApp, finalPath, function moved (err) { - if (err) return cb(err) - if (opts.asar) { - var finalPath = path.join(opts.out || process.cwd(), opts.name + '.app', 'Contents', 'Resources') - common.asarApp(finalPath, function (err) { - if (err) return cb(err) - updateMacIcon(function (err) { - if (err) return cb(err) - codesign() - }) - }) - } else { - updateMacIcon(function (err) { - if (err) return cb(err) - codesign() - }) - } - }) - } - }) - } - - function updateMacIcon (cb) { - var finalPath = path.join(opts.out || process.cwd(), opts.name + '.app') - - if (!opts.icon) { - return cb(null) - } - - ncp(opts.icon, path.join(finalPath, 'Contents', 'Resources', 'atom.icns'), function copied (err) { - cb(err) - }) - } - - function codesign () { - var appPath = path.join(opts.out || process.cwd(), opts.name + '.app') - - if (!opts.sign) return cb(null, appPath) - - child.exec('codesign --deep --force --sign "' + opts.sign + '" ' + appPath, function (err, stdout, stderr) { - cb(err, appPath) - }) - } - - common.prune(opts, paths.app, cb, moveHelper) - }) -} diff --git a/package.json b/package.json index 0a28a60..1628657 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "ncp": "^2.0.0", "plist": "^1.1.0", "rcedit": "^0.3.0", - "rimraf": "^2.3.2" + "rimraf": "^2.3.2", + "run-series": "^1.1.1" }, "devDependencies": { "standard": "^3.3.2", diff --git a/readme.md b/readme.md index fe74fc4..cbe362b 100644 --- a/readme.md +++ b/readme.md @@ -25,14 +25,15 @@ Usage: electron-packager --platform= --arch=/-` +- Use that version of electron to create a app in `/--` You should be able to launch the app on the platform you built for. If not, check your settings and try again. @@ -80,15 +82,21 @@ The source directory. The application name. `platform` - *String* -Allowed values: *linux, win32, darwin* +Allowed values: *linux, win32, darwin, all* +Not required if `all` is used. +Arbitrary combinations of individual platforms are also supported via a comma-delimited string or array of strings. `arch` - *String* -Allowed values: *ia32, x64* +Allowed values: *ia32, x64, all* +Not required if `all` is used. `version` - *String* Electron version (without the 'v'). See https://github.com/atom/electron/releases **Optional** +`all` - *Boolean* +Shortcut for `--arch=all --platform=all` + `out` - *String* `icon` - *String* @@ -103,10 +111,23 @@ Electron version (without the 'v'). See https://github.com/atom/electron/release `prune` - *Boolean* +`overwrite` - *Boolean* + `asar` - *Boolean* `sign` - *String* +`version-string` - *Object* +Object hash of application metadata to embed into the executable (Windows only): +* `CompanyName` +* `LegalCopyright` +* `FileDescription` +* `OriginalFilename` +* `FileVersion` +* `ProductVersion` +* `ProductName` +* `InternalName` + ##### callback `err` - *Error* @@ -121,4 +142,4 @@ If you run this on windows and you want to set the icon for your app using the ` ### related -- [grunt-electron](https://github.com/sindresorhus/grunt-electron) - grunt plugin for electron-packager +- [grunt-electron](https://github.com/sindresorhus/grunt-electron) - grunt plugin for electron-packager \ No newline at end of file diff --git a/usage.txt b/usage.txt index fdc3c85..ef068c2 100644 --- a/usage.txt +++ b/usage.txt @@ -1,15 +1,16 @@ Usage: electron-packager --platform= --arch= --version= - + Required options -platform linux, win32, darwin -arch ia32, x64 +platform all, or one or more of: linux, win32, darwin (comma-delimited if multiple) +arch all, ia32, x64 version see https://github.com/atom/electron/releases - -Example electron-packager ./ FooBar --platform=darwin --arch=x64 --version=0.25.1 + +Example electron-packager ./ FooBar --platform=darwin --arch=x64 --version=0.28.2 Optional options +all equivalent to --platform=all --arch=all out the dir to put the app into at the end. defaults to current working dir icon the icon file to use as the icon for the app app-bundle-id bundle identifier to use in the app plist @@ -17,4 +18,6 @@ app-version version to set for the app helper-bundle-id bundle identifier to use in the app helper plist ignore do not copy files into App whose filenames regex .match this string prune runs `npm prune --production` on the app -asar packages the source code within your app into an archive \ No newline at end of file +overwrite if output directory for a platform already exists, replaces it rather than skipping it +asar packages the source code within your app into an archive +sign should contain the identity to be used when running `codesign` (OS X only) \ No newline at end of file diff --git a/win32.js b/win32.js index 68cfe4d..0f7abaa 100644 --- a/win32.js +++ b/win32.js @@ -1,92 +1,40 @@ -var os = require('os') var path = require('path') -var mkdirp = require('mkdirp') -var rimraf = require('rimraf') -var ncp = require('ncp').ncp var mv = require('mv') -var rcedit = require('rcedit') +var series = require('run-series') var common = require('./common') module.exports = { - createApp: function createApp (opts, electronApp, cb) { - var tmpDir = path.join(os.tmpdir(), 'electron-packager-windows') + createApp: function createApp (opts, templatePath, callback) { + common.initializeApp(opts, templatePath, path.join('resources', 'app'), function buildWinApp (err, tempPath) { + if (err) return callback(err) - var newApp = path.join(tmpDir, opts.name + '-win32') - // reset build folders + copy template app - rimraf(tmpDir, function rmrfd () { - // ignore errors - mkdirp(newApp, function mkdirpd () { - // ignore errors - // copy app folder and use as template (this is exactly what Atom editor does) - ncp(electronApp, newApp, function copied (err) { - if (err) return cb(err) - // rename electron.exe - mv(path.join(newApp, 'electron.exe'), path.join(newApp, opts.name + '.exe'), function (err) { - if (err) return cb(err) + var newExePath = path.join(tempPath, opts.name + '.exe') + var operations = [ + function (cb) { + mv(path.join(tempPath, 'electron.exe'), newExePath, cb) + } + ] - buildWinApp(opts, cb, newApp) + if (opts.icon || opts['version-string']) { + operations.push(function (cb) { + common.normalizeExt(opts.icon, '.ico', function (err, icon) { + var rcOpts = {} + if (opts['version-string']) rcOpts['version-string'] = opts['version-string'] + + // Icon might be omitted or only exist in one OS's format, so skip it if normalizeExt reports an error + if (!err) { + rcOpts.icon = icon + } + + require('rcedit')(newExePath, rcOpts, cb) }) }) + } + + series(operations, function () { + common.moveApp(opts, tempPath, callback) }) }) } } - -function copy (from, to, cb) { - rimraf(to, function () { - mkdirp(to, function () { - ncp(from, to, function (err) { - if (err) { return cb(err) } - cb() - }) - }) - }) -} - -function buildWinApp (opts, cb, newApp) { - var paths = { - app: path.join(newApp, 'resources', 'app') - } - - // copy users app into destination path - ncp(opts.dir, paths.app, {filter: common.userIgnoreFilter(opts), dereference: true}, function copied (err) { - if (err) return cb(err) - - function moveApp () { - // finally, move app into cwd - var finalPath = path.join(opts.out || process.cwd(), opts.name + '-win32') - copy(newApp, finalPath, function moved (err) { - if (err) return cb(err) - if (opts.asar) { - var finalPath = path.join(opts.out || process.cwd(), opts.name + '-win32', 'resources') - common.asarApp(finalPath, function (err) { - if (err) return cb(err) - updateResourceData() - }) - } else { - updateResourceData() - } - }) - } - - function updateResourceData () { - var finalPath = path.join(opts.out || process.cwd(), opts.name + '-win32') - - if (!opts.icon && !opts['version-string']) { - return cb(null, finalPath) - } - - var exePath = path.join(opts.out || process.cwd(), opts.name + '-win32', opts.name + '.exe') - var rcOptions = { - icon: opts.icon, - 'version-string': opts['version-string'] - } - rcedit(exePath, rcOptions, function (err) { - cb(err, finalPath) - }) - } - - common.prune(opts, paths.app, cb, moveApp) - }) -}