2
2
mirror of https://github.com/Llewellynvdm/nativefier.git synced 2024-12-23 10:38:55 +00:00

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 node_modules
.DS_Store .DS_Store
test/dist

9
cli.js
View File

@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
var fs = require('fs') 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 packager = require('./')
var usage = fs.readFileSync(__dirname + '/usage.txt').toString() 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) console.error(usage)
process.exit(1) process.exit(1)
} }
packager(args, function done (err, appPath) { packager(args, function done (err, appPaths) {
if (err) { if (err) {
if (err.message) console.error(err.message) if (err.message) console.error(err.message)
else console.error(err, err.stack) else console.error(err, err.stack)
process.exit(1) 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 child = require('child_process')
var fs = require('fs')
var os = require('os')
var path = require('path') var path = require('path')
var asar = require('asar')
var mkdirp = require('mkdirp')
var ncp = require('ncp').ncp
var rimraf = require('rimraf') 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 = { module.exports = {
asarApp: function asarApp (finalDir, cb) { generateFinalPath: generateFinalPath,
var src = path.join(finalDir, 'app')
var dest = path.join(finalDir, 'app.asar') initializeApp: function initializeApp (opts, templatePath, appRelativePath, callback) {
asar.createPackage(src, dest, function (err) { // Performs the following initial operations for an app:
if (err) return cb(err) // * Creates temporary directory
rimraf(src, function (err) { // * Copies template into temporary directory
if (err) return cb(err) // * Copies user's app into temporary directory
cb(null, dest) // * 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) { moveApp: function finalizeApp (opts, tempPath, callback) {
if (opts.prune) { var finalPath = generateFinalPath(opts)
child.exec('npm prune --production', { cwd: cwd }, function pruned (err) { // Prefer ncp over mv (which seems to cause issues on Win8)
if (err) return cb(err) series([
nextStep() function (cb) {
}) mkdirp(finalPath, cb)
} else { },
nextStep() function (cb) {
} ncp(tempPath, finalPath, cb)
}
], function (err) {
callback(err, finalPath)
})
}, },
userIgnoreFilter: function userIgnoreFilter (opts, finalDir) { normalizeExt: function normalizeExt (filename, targetExt, cb) {
return function filter (file) { // Forces a filename to a given extension and fires the given callback with the normalized filename,
if (path.sep === '\\') { // if it exists. Otherwise reports the error from the fs.stat call.
// convert slashes so unix-format ignores work // (Used for resolving icon filenames, particularly during --all runs.)
file = file.replace(/\\/g, '/')
}
var ignore = opts.ignore || [] // This error path is used by win32.js if no icon is specified
if (!Array.isArray(ignore)) ignore = [ignore] if (!filename) return cb(new Error('No filename specified to normalizeExt'))
if (typeof finalDir !== 'undefined') {
ignore = ignore.concat([finalDir]) var ext = path.extname(filename)
} if (ext !== targetExt) {
for (var i = 0; i < ignore.length; i++) { filename = filename.slice(0, filename.length - ext.length) + targetExt
if (file.match(ignore[i])) {
return false
}
}
return true
} }
fs.stat(filename, function (err) {
cb(err, err ? null : filename)
})
} }
} }

190
index.js
View File

@ -1,59 +1,169 @@
var path = require('path') var path = require('path')
var fs = require('fs')
var os = require('os') var os = require('os')
var download = require('electron-download') var download = require('electron-download')
var extract = require('extract-zip') var extract = require('extract-zip')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var series = require('run-series')
var common = require('./common')
var mac = require('./mac.js') var supportedArchs = {
var linux = require('./linux.js') ia32: 1,
var win32 = require('./win32.js') 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) { module.exports = function packager (opts, cb) {
var platformPackager var archs = validateList(opts.all ? 'all' : opts.arch, supportedArchs, 'arch')
var platform = opts.platform var platforms = validateList(opts.all ? 'all' : opts.platform, supportedPlatforms, 'platform')
var arch = opts.arch if (!opts.version) return cb(new Error('Must specify version'))
var version = opts.version if (!Array.isArray(archs)) return cb(new Error(archs))
if (!Array.isArray(platforms)) return cb(new Error(platforms))
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'))
}
// Ignore this and related modules by default // Ignore this and related modules by default
var defaultIgnores = ['/node_modules/electron-prebuilt($|/)', '/node_modules/electron-packager($|/)', '/\.git($|/)'] var defaultIgnores = ['/node_modules/electron-prebuilt($|/)', '/node_modules/electron-packager($|/)', '/\.git($|/)']
if (opts.ignore && !Array.isArray(opts.ignore)) opts.ignore = [opts.ignore] if (opts.ignore && !Array.isArray(opts.ignore)) opts.ignore = [opts.ignore]
opts.ignore = (opts.ignore) ? opts.ignore.concat(defaultIgnores) : defaultIgnores opts.ignore = (opts.ignore) ? opts.ignore.concat(defaultIgnores) : defaultIgnores
download({ series(createSeries(opts, archs, platforms), function (err, appPaths) {
platform: platform,
arch: arch,
version: version
}, function (err, zipPath) {
if (err) return cb(err) 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 cb(null, appPaths.filter(function (appPath) {
var tmpDir = path.join(os.tmpdir(), 'electron-packager-' + platform + '-template') // Remove falsy entries (e.g. skipped platforms)
rimraf(tmpDir, function (err) { return appPath
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)
})
})
})
}) })
} }

View File

@ -1,70 +1,15 @@
var path = require('path') var path = require('path')
var fs = require('fs') var mv = require('mv')
var mkdirp = require('mkdirp')
var ncp = require('ncp').ncp
var rimraf = require('rimraf')
var common = require('./common') var common = require('./common')
module.exports = { module.exports = {
createApp: function createApp (opts, templateApp, cb) { createApp: function createApp (opts, templatePath, callback) {
var finalDir = path.join(opts.out || process.cwd(), opts.name + '-linux') common.initializeApp(opts, templatePath, path.join('resources', 'app'), function buildLinuxApp (err, tempPath) {
var userAppDir = path.join(finalDir, 'resources', 'default_app') if (err) return callback(err)
var originalBinary = path.join(finalDir, 'electron') mv(path.join(tempPath, 'electron'), path.join(tempPath, opts.name), function (err) {
var finalBinary = path.join(finalDir, opts.name) if (err) return callback(err)
common.moveApp(opts, tempPath, callback)
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()
}) })
} })
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 path = require('path')
var fs = require('fs') var fs = require('fs')
var child = require('child_process') var child = require('child_process')
var plist = require('plist') var plist = require('plist')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var mv = require('mv') var mv = require('mv')
var ncp = require('ncp').ncp var ncp = require('ncp').ncp
var series = require('run-series')
var common = require('./common') var common = require('./common')
module.exports = { module.exports = {
createApp: function createApp (opts, electronPath, cb) { createApp: function createApp (opts, templatePath, callback) {
var electronApp = path.join(electronPath, 'Electron.app') var appRelativePath = path.join('Electron.app', 'Contents', 'Resources', 'app')
var tmpDir = path.join(os.tmpdir(), 'electron-packager-mac') 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 // Update plist files
rimraf(tmpDir, function rmrfd () { var defaultBundleName = 'com.electron.' + opts.name.toLowerCase().replace(/ /g, '_')
// ignore errors var appVersion = opts['app-version']
mkdirp(newApp, function mkdirpd () {
// ignore errors appPlist.CFBundleDisplayName = opts.name
// copy .app folder and use as template (this is exactly what Atom editor does) appPlist.CFBundleIdentifier = opts['app-bundle-id'] || defaultBundleName
ncp(electronApp, newApp, function copied (err) { appPlist.CFBundleName = opts.name
if (err) return cb(err) helperPlist.CFBundleIdentifier = opts['helper-bundle-id'] || defaultBundleName + '.helper'
buildMacApp(opts, cb, newApp) 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", "ncp": "^2.0.0",
"plist": "^1.1.0", "plist": "^1.1.0",
"rcedit": "^0.3.0", "rcedit": "^0.3.0",
"rimraf": "^2.3.2" "rimraf": "^2.3.2",
"run-series": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"run-waterfall": "^1.1.1",
"standard": "^3.3.2", "standard": "^3.3.2",
"tape": "^4.0.0" "tape": "^4.0.0"
}, },
"scripts": { "scripts": {
"test": "standard && node test/test.js" "test": "standard && tape test"
}, },
"standard": { "standard": {
"ignore": [ "ignore": [
"test/dist" "test/fixtures/**/node_modules"
] ]
} }
} }

View File

@ -25,14 +25,15 @@ Usage: electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arc
Required options Required options
platform linux, win32, darwin platform all, or one or more of: linux, win32, darwin (comma-delimited if multiple)
arch ia32, x64 arch all, ia32, x64
version see https://github.com/atom/electron/releases 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.25.1
Optional options 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 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 icon the icon file to use as the icon for the app
app-bundle-id bundle identifier to use in the app plist 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 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 ignore do not copy files into App whose filenames regex .match this string
prune runs `npm prune --production` on the app 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 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) 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 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: This will:
- Find or download the correct release of Electron - 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. 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. The application name.
`platform` - *String* `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* `arch` - *String*
Allowed values: *ia32, x64* Allowed values: *ia32, x64, all*
Not required if `all` is used.
`version` - *String* `version` - *String*
Electron version (without the 'v'). See https://github.com/atom/electron/releases Electron version (without the 'v'). See https://github.com/atom/electron/releases
**Optional** **Optional**
`all` - *Boolean*
Shortcut for `--arch=all --platform=all`
`out` - *String* `out` - *String*
`icon` - *String* `icon` - *String*
@ -103,10 +111,23 @@ Electron version (without the 'v'). See https://github.com/atom/electron/release
`prune` - *Boolean* `prune` - *Boolean*
`overwrite` - *Boolean*
`asar` - *Boolean* `asar` - *Boolean*
`sign` - *String* `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 ##### callback
`err` - *Error* `err` - *Error*

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

@ -2,14 +2,15 @@ Usage: electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arc
Required options Required options
platform linux, win32, darwin platform all, or one or more of: linux, win32, darwin (comma-delimited if multiple)
arch ia32, x64 arch all, ia32, x64
version see https://github.com/atom/electron/releases 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 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 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 icon the icon file to use as the icon for the app
app-bundle-id bundle identifier to use in the app plist 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 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 ignore do not copy files into App whose filenames regex .match this string
prune runs `npm prune --production` on the app 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 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 path = require('path')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var ncp = require('ncp').ncp
var mv = require('mv') var mv = require('mv')
var rcedit = require('rcedit') var series = require('run-series')
var common = require('./common') var common = require('./common')
module.exports = { module.exports = {
createApp: function createApp (opts, electronApp, cb) { createApp: function createApp (opts, templatePath, callback) {
var tmpDir = path.join(os.tmpdir(), 'electron-packager-windows') 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') var newExePath = path.join(tempPath, opts.name + '.exe')
// reset build folders + copy template app var operations = [
rimraf(tmpDir, function rmrfd () { function (cb) {
// ignore errors mv(path.join(tempPath, 'electron.exe'), newExePath, cb)
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)
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)
})
}