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
This commit is contained in:
Kenneth G. Franqueiro 2015-06-19 22:36:15 -04:00
parent cff6ab50ee
commit a157f716a5
10 changed files with 421 additions and 377 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
downloaded/*.zip
node_modules
.DS_Store
test/dist

9
cli.js
View File

@ -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])
})

159
common.js
View File

@ -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)
})
}
}

190
index.js
View File

@ -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
}))
})
}

View File

@ -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()
})
}
}

213
mac.js
View File

@ -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)
})
}

View File

@ -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",

View File

@ -25,14 +25,15 @@ Usage: electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arc
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
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
@ -40,6 +41,7 @@ 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
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)
version-string should contain a hash of the application metadata to be embedded into the executable (Windows only). Keys supported
@ -58,7 +60,7 @@ version-string should contain a hash of the application metadata to be embed
This will:
- Find or download the correct release of Electron
- Use that version of electron to create a app in `<out>/<appname>-<platform>`
- Use that version of electron to create a app in `<out>/<appname>-<platform>-<arch>`
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

View File

@ -1,15 +1,16 @@
Usage: electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> --version=<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
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)

104
win32.js
View File

@ -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)
})
}