Merge pull request #88 from kfranqueiro/refactor

Implement --all, refactor code, add tests
This commit is contained in:
=^._.^= 2015-07-01 12:54:34 -07:00
commit 8d26a51213
26 changed files with 1105 additions and 442 deletions

2
.gitignore vendored
View File

@ -1,4 +1,2 @@
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,18 +26,20 @@
"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": {
"run-waterfall": "^1.1.1",
"standard": "^3.3.2",
"tape": "^4.0.0"
},
"scripts": {
"test": "standard && node test/test.js"
"test": "standard && tape test"
},
"standard": {
"ignore": [
"test/dist"
"test/fixtures/**/node_modules"
]
}
}

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

273
test/basic.js Normal file
View File

@ -0,0 +1,273 @@
var fs = require('fs')
var path = require('path')
var packager = require('..')
var waterfall = require('run-waterfall')
var config = require('./config.json')
var util = require('./util')
function generateBasename (opts) {
return opts.name + '-' + opts.platform + '-' + opts.arch
}
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') {
return path.join(opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper.app')
}
return opts.name + (opts.platform === 'win32' ? '.exe' : '')
}
function createDefaultsTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
var finalPath
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
t.true(Array.isArray(paths), 'packager call should resolve to an array')
t.equal(paths.length, 1, 'Single-target run should resolve to a 1-item array')
finalPath = paths[0]
t.equal(finalPath, path.join(util.getWorkCwd(), generateBasename(opts)),
'Path should follow the expected format and be in the cwd')
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(path.join(finalPath, generateNamePath(opts)), cb)
}, function (stats, cb) {
if (opts.platform === 'darwin') {
t.true(stats.isDirectory(), 'The Helper.app should reflect opts.name')
} else {
t.true(stats.isFile(), 'The executable should reflect opts.name')
}
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(path.join(resourcesPath, 'app', 'node_modules', 'run-waterfall'), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain devDependencies by default (no prune)')
util.areFilesEqual(path.join(opts.dir, 'main.js'), path.join(resourcesPath, 'app', 'main.js'), cb)
}, function (equal, cb) {
t.true(equal, 'File under packaged app directory should match source file')
util.areFilesEqual(path.join(opts.dir, 'ignore', 'this.txt'),
path.join(resourcesPath, 'app', 'ignore', 'this.txt'),
cb)
}, function (equal, cb) {
t.true(equal,
'File under subdirectory of packaged app directory should match source file and not be ignored by default')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createOutTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.out = 'dist'
var finalPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
t.equal(finalPath, path.join('dist', generateBasename(opts)),
'Path should follow the expected format and be under the folder specifed in `out`')
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
fs.stat(path.join(finalPath, util.generateResourcesPath(opts)), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAsarTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.asar = true
var finalPath
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(path.join(resourcesPath, 'app.asar'), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'app.asar should exist under the resources subdirectory when opts.asar is true')
fs.exists(path.join(resourcesPath, 'app'), function (exists) {
t.false(exists, 'app subdirectory should NOT exist when app.asar is built')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
function createPruneTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.prune = true
var finalPath
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(path.join(resourcesPath, 'app', 'node_modules', 'run-series'), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'npm dependency should exist under app/node_modules')
fs.exists(path.join(resourcesPath, 'app', 'node_modules', 'run-waterfall'), function (exists) {
t.false(exists, 'npm devDependency should NOT exist under app/node_modules')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
function createIgnoreTest (combination, ignorePattern, ignoredFile) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.ignore = ignorePattern
var appPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
appPath = path.join(paths[0], util.generateResourcesPath(opts), 'app')
fs.stat(path.join(appPath, 'package.json'), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected output directory should exist and contain files')
fs.exists(path.join(appPath, ignoredFile), function (exists) {
t.false(exists, 'Ignored file should not exist in output app directory')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
function createOverwriteTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout * 2) // Multiplied since this test packages the application twice
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
var finalPath
var testPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
// Create a dummy file to detect whether the output directory is replaced in subsequent runs
testPath = path.join(finalPath, 'test.txt')
fs.writeFile(testPath, 'test', cb)
}, function (cb) {
// Run again, defaulting to overwrite false
packager(opts, cb)
}, function (paths, cb) {
fs.stat(testPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The existing output directory should exist as before (skipped by default)')
// Run a third time, explicitly setting overwrite to true
opts.overwrite = true
packager(opts, cb)
}, function (paths, cb) {
fs.exists(testPath, function (exists) {
t.false(exists, 'The output directory should be regenerated when overwrite is true')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
util.testAllPlatforms('defaults test', createDefaultsTest)
util.testAllPlatforms('out test', createOutTest)
util.testAllPlatforms('asar test', createAsarTest)
util.testAllPlatforms('prune test', createPruneTest)
util.testAllPlatforms('ignore test: string in array', createIgnoreTest, ['ignorethis'], 'ignorethis.txt')
util.testAllPlatforms('ignore test: string', createIgnoreTest, 'ignorethis', 'ignorethis.txt')
util.testAllPlatforms('ignore test: RegExp', createIgnoreTest, /ignorethis/, 'ignorethis.txt')
util.testAllPlatforms('ignore test: string with slash', createIgnoreTest, 'ignore/this',
path.join('ignore', 'this.txt'))
util.testAllPlatforms('ignore test: only match subfolder of app', createIgnoreTest, 'electron-packager',
path.join('electron-packager', 'readme.txt'))
util.testAllPlatforms('overwrite test', createOverwriteTest)

4
test/config.json Normal file
View File

@ -0,0 +1,4 @@
{
"timeout": 15000,
"version": "0.28.3"
}

View File

@ -0,0 +1,2 @@
This file exists to test ability to ignore paths under app, without also
ignoring the entire app folder due to a match above it (#54 / #55).

0
test/fixtures/basic/ignore/this.txt vendored Normal file
View File

1
test/fixtures/basic/ignorethis.txt vendored Normal file
View File

@ -0,0 +1 @@

4
test/fixtures/basic/index.html vendored Normal file
View File

@ -0,0 +1,4 @@
<!DOCTYPE html>
<html>
<body>Hello, world!</body>
</html>

22
test/fixtures/basic/main.js vendored Normal file
View File

@ -0,0 +1,22 @@
var app = require('app')
var BrowserWindow = require('browser-window')
var mainWindow
app.on('window-all-closed', function () {
app.quit()
})
app.on('ready', function () {
mainWindow = new BrowserWindow({
center: true,
title: 'Basic Test',
width: 800,
height: 600
})
mainWindow.loadUrl('file://' + require('path').resolve(__dirname, 'index.html'))
mainWindow.on('closed', function () {
mainWindow = null
})
})

9
test/fixtures/basic/package.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"main": "main.js",
"dependencies": {
"run-series": "^1.1.1"
},
"devDependencies": {
"run-waterfall": "^1.1.1"
}
}

BIN
test/fixtures/monochrome.icns vendored Normal file

Binary file not shown.

27
test/index.js Normal file
View File

@ -0,0 +1,27 @@
var exec = require('child_process').exec
var path = require('path')
var series = require('run-series')
var config = require('./config.json')
var util = require('./util')
// Download all Electron distributions before running tests to avoid timing out due to network speed
series([
function (cb) {
console.log('Calling electron-download before running tests...')
util.downloadAll(config.version, cb)
}, function (cb) {
console.log('Running npm install in fixtures/basic...')
exec('npm install', {cwd: path.join(__dirname, 'fixtures', 'basic')}, cb)
}
], function () {
console.log('Running tests...')
require('./basic')
require('./multitarget')
if (process.platform !== 'win32') {
// Perform additional tests specific to building for OS X
require('./mac')
}
})

94
test/mac.js Normal file
View File

@ -0,0 +1,94 @@
var exec = require('child_process').exec
var fs = require('fs')
var path = require('path')
var packager = require('..')
var test = require('tape')
var waterfall = require('run-waterfall')
var config = require('./config.json')
var util = require('./util')
function createIconTest (icon, iconPath) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'x64',
platform: 'darwin',
icon: icon
}
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
resourcesPath = path.join(paths[0], util.generateResourcesPath(opts))
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
util.areFilesEqual(iconPath, path.join(resourcesPath, 'atom.icns'), cb)
}, function (equal, cb) {
t.true(equal, 'atom.icns should be identical to the specified icon file')
cb()
}
], function (err) {
t.end(err)
})
}
}
var iconBase = path.join(__dirname, 'fixtures', 'monochrome')
var icnsPath = iconBase + '.icns'
util.setup()
test('icon test: .icns specified', createIconTest(icnsPath, icnsPath))
util.teardown()
util.setup()
test('icon test: .ico specified (should replace with .icns)', createIconTest(iconBase + '.ico', icnsPath))
util.teardown()
util.setup()
test('icon test: basename only (should add .icns)', createIconTest(iconBase, icnsPath))
util.teardown()
util.setup()
test('codesign test', function (t) {
t.timeoutAfter(config.timeout)
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'x64',
platform: 'darwin',
sign: '-' // 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 --verify --deep ' + 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()

145
test/multitarget.js Normal file
View File

@ -0,0 +1,145 @@
var fs = require('fs')
var path = require('path')
var packager = require('..')
var series = require('run-series')
var test = require('tape')
var waterfall = require('run-waterfall')
var config = require('./config.json')
var util = require('./util')
function verifyPackageExistence (finalPaths, callback) {
series(finalPaths.map(function (finalPath) {
return function (cb) {
fs.stat(finalPath, cb)
}
}), function (err, statsResults) {
if (err) return callback(null, false)
callback(null, statsResults.every(function (stats) {
return stats.isDirectory()
}))
})
}
util.setup()
test('all test', function (t) {
t.timeoutAfter(config.timeout * 5) // 4-5 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
all: true
}
waterfall([
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,
'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for all possible platforms')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
util.setup()
test('platform=all test (one arch)', function (t) {
t.timeoutAfter(config.timeout * 2) // 2 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'ia32',
platform: 'all'
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
t.equal(finalPaths.length, 2, 'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for both 32-bit platforms')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
util.setup()
test('arch=all test (one platform)', function (t) {
t.timeoutAfter(config.timeout * 2) // 2 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'all',
platform: 'linux'
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
t.equal(finalPaths.length, 2, 'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for both architectures')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
function createMultiTest (arch, platform) {
return function (t) {
t.timeoutAfter(config.timeout * 4) // 4 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: arch,
platform: platform
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
t.equal(finalPaths.length, 4, 'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for all combinations of specified archs and platforms')
cb()
}
], function (err) {
t.end(err)
})
}
}
util.setup()
test('multi-platform / multi-arch test, from arrays', createMultiTest(['ia32', 'x64'], ['linux', 'win32']))
util.teardown()
util.setup()
test('multi-platform / multi-arch test, from strings', createMultiTest('ia32,x64', 'linux,win32'))
util.teardown()

View File

@ -1,45 +0,0 @@
var test = require('tape')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var packager = require('../index.js')
var distdir = __dirname + '/dist'
rimraf.sync(distdir)
mkdirp.sync(distdir)
var opts = {
dir: __dirname + '/testapp',
name: 'Test',
version: '0.28.2',
out: distdir
}
test('package for windows', function (t) {
opts.platform = 'win32'
opts.arch = 'ia32'
packager(opts, function done (err, appPath) {
t.notOk(err, 'no err')
t.end()
})
})
test('package for linux', function (t) {
opts.platform = 'linux'
opts.arch = 'x64'
packager(opts, function done (err, appPath) {
t.notOk(err, 'no err')
t.end()
})
})
test('package for darwin', function (t) {
opts.platform = 'darwin'
opts.arch = 'x64'
packager(opts, function done (err, appPath) {
t.notOk(err, 'no err')
t.end()
})
})

View File

@ -1,6 +0,0 @@
var app = require('app')
app.on('ready', function () {
console.log('pizza')
app.exit()
})

View File

@ -1,11 +0,0 @@
{
"name": "testapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "BSD-2-Clause"
}

100
test/util.js Normal file
View File

@ -0,0 +1,100 @@
var fs = require('fs')
var path = require('path')
var test = require('tape')
var download = require('electron-download')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var series = require('run-series')
var ORIGINAL_CWD = process.cwd()
var WORK_CWD = path.join(__dirname, 'work')
var archs = ['ia32', 'x64']
var platforms = ['darwin', 'linux', 'win32']
var slice = Array.prototype.slice
var version = require('./config.json').version
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
// (see https://github.com/maxogden/electron-packager/issues/71)
if (platform === 'darwin' && (arch === 'ia32' || require('os').platform() === 'win32')) return
combinations.push({
arch: arch,
platform: platform,
version: version
})
})
})
exports.areFilesEqual = function areFilesEqual (file1, file2, callback) {
series([
function (cb) {
fs.readFile(file1, cb)
},
function (cb) {
fs.readFile(file2, cb)
}
], function (err, buffers) {
callback(err, slice.call(buffers[0]).every(function (b, i) {
return b === buffers[1][i]
}))
})
}
exports.downloadAll = function downloadAll (version, callback) {
series(combinations.map(function (combination) {
return function (cb) {
download(combination, cb)
}
}), callback)
}
exports.forEachCombination = function forEachCombination (cb) {
combinations.forEach(cb)
}
exports.generateResourcesPath = function generateResourcesPath (opts) {
return opts.platform === 'darwin' ? path.join(opts.name + '.app', 'Contents', 'Resources') : 'resources'
}
exports.getWorkCwd = function getWorkCwd () {
return WORK_CWD
}
// 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
exports.setup = function setup () {
test('setup', function (t) {
mkdirp(WORK_CWD, function (err) {
if (err) t.end(err)
process.chdir(WORK_CWD)
t.end()
})
})
}
exports.teardown = function teardown () {
test('teardown', function (t) {
process.chdir(ORIGINAL_CWD)
rimraf(WORK_CWD, function (err) {
t.end(err)
})
})
}
exports.testAllPlatforms = function testAllPlatforms (name, createTest /*, ...createTestArgs */) {
var args = slice.call(arguments, 2)
exports.setup()
exports.forEachCombination(function (combination) {
test(name + ': ' + combination.platform + '-' + combination.arch,
createTest.apply(null, [combination].concat(args)))
})
exports.teardown()
}

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