mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-11-10 15:21:03 +00:00
Merge pull request #88 from kfranqueiro/refactor
Implement --all, refactor code, add tests
This commit is contained in:
commit
8d26a51213
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,2 @@
|
||||
downloaded/*.zip
|
||||
node_modules
|
||||
.DS_Store
|
||||
test/dist
|
9
cli.js
9
cli.js
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
var fs = require('fs')
|
||||
var args = require('minimist')(process.argv.slice(2), {boolean: ['prune', 'asar', 'overwrite']})
|
||||
var args = require('minimist')(process.argv.slice(2), {boolean: ['prune', 'asar', 'all', 'overwrite']})
|
||||
var packager = require('./')
|
||||
var usage = fs.readFileSync(__dirname + '/usage.txt').toString()
|
||||
|
||||
@ -16,17 +16,18 @@ if (protocolSchemes && protocolNames && protocolNames.length === protocolSchemes
|
||||
})
|
||||
}
|
||||
|
||||
if (!args.dir || !args.name || !args.platform || !args.arch || !args.version) {
|
||||
if (!args.dir || !args.name || !args.version || (!args.all && (!args.platform || !args.arch))) {
|
||||
console.error(usage)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
packager(args, function done (err, appPath) {
|
||||
packager(args, function done (err, appPaths) {
|
||||
if (err) {
|
||||
if (err.message) console.error(err.message)
|
||||
else console.error(err, err.stack)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.error('Wrote new app to', appPath)
|
||||
if (appPaths.length > 1) console.error('Wrote new apps to:\n' + appPaths.join('\n'))
|
||||
else if (appPaths.length === 1) console.error('Wrote new app to', appPaths[0])
|
||||
})
|
||||
|
159
common.js
159
common.js
@ -1,50 +1,139 @@
|
||||
var asar = require('asar')
|
||||
var child = require('child_process')
|
||||
var fs = require('fs')
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
|
||||
var asar = require('asar')
|
||||
var mkdirp = require('mkdirp')
|
||||
var ncp = require('ncp').ncp
|
||||
var rimraf = require('rimraf')
|
||||
var series = require('run-series')
|
||||
|
||||
function asarApp (appPath, cb) {
|
||||
var src = path.join(appPath)
|
||||
var dest = path.join(appPath, '..', 'app.asar')
|
||||
asar.createPackage(src, dest, function (err) {
|
||||
if (err) return cb(err)
|
||||
rimraf(src, function (err) {
|
||||
if (err) return cb(err)
|
||||
cb(null, dest)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function generateFinalBasename (opts) {
|
||||
return opts.name + '-' + opts.platform + '-' + opts.arch
|
||||
}
|
||||
|
||||
function generateFinalPath (opts) {
|
||||
return path.join(opts.out || process.cwd(), generateFinalBasename(opts))
|
||||
}
|
||||
|
||||
function userIgnoreFilter (opts) {
|
||||
return function filter (file) {
|
||||
file = file.split(path.resolve(opts.dir))[1]
|
||||
|
||||
if (path.sep === '\\') {
|
||||
// convert slashes so unix-format ignores work
|
||||
file = file.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
var ignore = opts.ignore || []
|
||||
if (!Array.isArray(ignore)) ignore = [ignore]
|
||||
for (var i = 0; i < ignore.length; i++) {
|
||||
if (file.match(ignore[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
asarApp: function asarApp (finalDir, cb) {
|
||||
var src = path.join(finalDir, 'app')
|
||||
var dest = path.join(finalDir, 'app.asar')
|
||||
asar.createPackage(src, dest, function (err) {
|
||||
if (err) return cb(err)
|
||||
rimraf(src, function (err) {
|
||||
if (err) return cb(err)
|
||||
cb(null, dest)
|
||||
generateFinalPath: generateFinalPath,
|
||||
|
||||
initializeApp: function initializeApp (opts, templatePath, appRelativePath, callback) {
|
||||
// Performs the following initial operations for an app:
|
||||
// * Creates temporary directory
|
||||
// * Copies template into temporary directory
|
||||
// * Copies user's app into temporary directory
|
||||
// * Prunes non-production node_modules (if opts.prune is set)
|
||||
// * Creates an asar (if opts.asar is set)
|
||||
|
||||
var tempParent = path.join(os.tmpdir(), 'electron-packager', opts.platform + '-' + opts.arch)
|
||||
var tempPath = path.join(tempParent, generateFinalBasename(opts))
|
||||
// Path to `app` directory
|
||||
var appPath = path.join(tempPath, appRelativePath)
|
||||
|
||||
var operations = [
|
||||
function (cb) {
|
||||
rimraf(tempParent, function () {
|
||||
// Ignore errors (e.g. directory didn't exist anyway)
|
||||
cb()
|
||||
})
|
||||
},
|
||||
function (cb) {
|
||||
mkdirp(tempPath, cb)
|
||||
},
|
||||
function (cb) {
|
||||
ncp(templatePath, tempPath, cb)
|
||||
},
|
||||
function (cb) {
|
||||
ncp(opts.dir, appPath, {filter: userIgnoreFilter(opts), dereference: true}, cb)
|
||||
}
|
||||
]
|
||||
|
||||
// Prune and asar are now performed before platform-specific logic, primarily so that
|
||||
// appPath is predictable (e.g. before .app is renamed for mac)
|
||||
if (opts.prune) {
|
||||
operations.push(function (cb) {
|
||||
child.exec('npm prune --production', {cwd: appPath}, cb)
|
||||
})
|
||||
}
|
||||
|
||||
if (opts.asar) {
|
||||
operations.push(function (cb) {
|
||||
asarApp(path.join(appPath), cb)
|
||||
})
|
||||
}
|
||||
|
||||
series(operations, function (err) {
|
||||
if (err) return callback(err)
|
||||
// Resolve to path to temporary app folder for platform-specific processes to use
|
||||
callback(null, tempPath)
|
||||
})
|
||||
},
|
||||
|
||||
prune: function prune (opts, cwd, cb, nextStep) {
|
||||
if (opts.prune) {
|
||||
child.exec('npm prune --production', { cwd: cwd }, function pruned (err) {
|
||||
if (err) return cb(err)
|
||||
nextStep()
|
||||
})
|
||||
} else {
|
||||
nextStep()
|
||||
}
|
||||
moveApp: function finalizeApp (opts, tempPath, callback) {
|
||||
var finalPath = generateFinalPath(opts)
|
||||
// Prefer ncp over mv (which seems to cause issues on Win8)
|
||||
series([
|
||||
function (cb) {
|
||||
mkdirp(finalPath, cb)
|
||||
},
|
||||
function (cb) {
|
||||
ncp(tempPath, finalPath, cb)
|
||||
}
|
||||
], function (err) {
|
||||
callback(err, finalPath)
|
||||
})
|
||||
},
|
||||
|
||||
userIgnoreFilter: function userIgnoreFilter (opts, finalDir) {
|
||||
return function filter (file) {
|
||||
if (path.sep === '\\') {
|
||||
// convert slashes so unix-format ignores work
|
||||
file = file.replace(/\\/g, '/')
|
||||
}
|
||||
normalizeExt: function normalizeExt (filename, targetExt, cb) {
|
||||
// Forces a filename to a given extension and fires the given callback with the normalized filename,
|
||||
// if it exists. Otherwise reports the error from the fs.stat call.
|
||||
// (Used for resolving icon filenames, particularly during --all runs.)
|
||||
|
||||
var ignore = opts.ignore || []
|
||||
if (!Array.isArray(ignore)) ignore = [ignore]
|
||||
if (typeof finalDir !== 'undefined') {
|
||||
ignore = ignore.concat([finalDir])
|
||||
}
|
||||
for (var i = 0; i < ignore.length; i++) {
|
||||
if (file.match(ignore[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
// This error path is used by win32.js if no icon is specified
|
||||
if (!filename) return cb(new Error('No filename specified to normalizeExt'))
|
||||
|
||||
var ext = path.extname(filename)
|
||||
if (ext !== targetExt) {
|
||||
filename = filename.slice(0, filename.length - ext.length) + targetExt
|
||||
}
|
||||
|
||||
fs.stat(filename, function (err) {
|
||||
cb(err, err ? null : filename)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
190
index.js
190
index.js
@ -1,59 +1,169 @@
|
||||
var path = require('path')
|
||||
var fs = require('fs')
|
||||
var os = require('os')
|
||||
|
||||
var download = require('electron-download')
|
||||
var extract = require('extract-zip')
|
||||
var mkdirp = require('mkdirp')
|
||||
var rimraf = require('rimraf')
|
||||
var series = require('run-series')
|
||||
var common = require('./common')
|
||||
|
||||
var mac = require('./mac.js')
|
||||
var linux = require('./linux.js')
|
||||
var win32 = require('./win32.js')
|
||||
var supportedArchs = {
|
||||
ia32: 1,
|
||||
x64: 1
|
||||
}
|
||||
|
||||
var supportedPlatforms = {
|
||||
// Maps to module ID for each platform (lazy-required if used)
|
||||
darwin: './mac',
|
||||
linux: './linux',
|
||||
win32: './win32'
|
||||
}
|
||||
|
||||
var tempBase = path.join(os.tmpdir(), 'electron-packager')
|
||||
|
||||
function testSymlink (cb) {
|
||||
var testPath = path.join(tempBase, 'symlink-test')
|
||||
var testFile = path.join(testPath, 'test')
|
||||
var testLink = path.join(testPath, 'testlink')
|
||||
series([
|
||||
function (cb) {
|
||||
mkdirp(testPath, cb)
|
||||
},
|
||||
function (cb) {
|
||||
fs.writeFile(testFile, '', cb)
|
||||
},
|
||||
function (cb) {
|
||||
fs.symlink(testFile, testLink, cb)
|
||||
}
|
||||
], function (err) {
|
||||
var result = !err
|
||||
rimraf(testPath, function () {
|
||||
cb(result) // ignore errors on cleanup
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function validateList (list, supported, name) {
|
||||
// Validates list of architectures or platforms.
|
||||
// Returns a normalized array if successful, or an error message string otherwise.
|
||||
|
||||
if (!list) return 'Must specify ' + name
|
||||
if (list === 'all') return Object.keys(supported)
|
||||
|
||||
if (!Array.isArray(list)) list = list.split(',')
|
||||
for (var i = list.length; i--;) {
|
||||
if (!supported[list[i]]) {
|
||||
return 'Unsupported ' + name + ' ' + list[i] + '; must be one of: ' + Object.keys(supported).join(', ')
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
function createSeries (opts, archs, platforms) {
|
||||
var combinations = []
|
||||
archs.forEach(function (arch) {
|
||||
platforms.forEach(function (platform) {
|
||||
// Electron does not have 32-bit releases for Mac OS X, so skip that combination
|
||||
if (platform === 'darwin' && arch === 'ia32') return
|
||||
combinations.push({
|
||||
platform: platform,
|
||||
arch: arch,
|
||||
version: opts.version
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return [
|
||||
function (cb) {
|
||||
rimraf(tempBase, cb)
|
||||
}
|
||||
].concat(combinations.map(function (combination) {
|
||||
var arch = combination.arch
|
||||
var platform = combination.platform
|
||||
var version = combination.version
|
||||
|
||||
return function (callback) {
|
||||
download(combination, function (err, zipPath) {
|
||||
if (err) return callback(err)
|
||||
|
||||
var tmpDir = path.join(tempBase, platform + '-' + arch + '-template')
|
||||
|
||||
var operations = [
|
||||
function (cb) {
|
||||
mkdirp(tmpDir, cb)
|
||||
},
|
||||
function (cb) {
|
||||
extract(zipPath, {dir: tmpDir}, cb)
|
||||
}
|
||||
]
|
||||
|
||||
function createApp (comboOpts) {
|
||||
console.error('Packaging app for platform', platform + ' ' + arch, 'using electron v' + version)
|
||||
series(operations, function () {
|
||||
require(supportedPlatforms[platform]).createApp(comboOpts, tmpDir, callback)
|
||||
})
|
||||
}
|
||||
|
||||
function checkOverwrite () {
|
||||
// Create delegated options object with specific platform and arch, for output directory naming
|
||||
var comboOpts = Object.create(opts)
|
||||
comboOpts.arch = arch
|
||||
comboOpts.platform = platform
|
||||
|
||||
var finalPath = common.generateFinalPath(comboOpts)
|
||||
fs.exists(finalPath, function (exists) {
|
||||
if (exists) {
|
||||
if (opts.overwrite) {
|
||||
rimraf(finalPath, function () {
|
||||
createApp(comboOpts)
|
||||
})
|
||||
} else {
|
||||
console.error('Skipping ' + platform + ' ' + arch +
|
||||
' (output dir already exists, use --overwrite to force)')
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
createApp(comboOpts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (combination.platform === 'darwin') {
|
||||
testSymlink(function (result) {
|
||||
if (result) return checkOverwrite()
|
||||
|
||||
console.error('Cannot create symlinks; skipping darwin platform')
|
||||
callback()
|
||||
})
|
||||
} else {
|
||||
checkOverwrite()
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = function packager (opts, cb) {
|
||||
var platformPackager
|
||||
var platform = opts.platform
|
||||
var arch = opts.arch
|
||||
var version = opts.version
|
||||
|
||||
if (!platform || !arch || !version) cb(new Error('Must specify platform, arch and version'))
|
||||
|
||||
switch (arch) {
|
||||
case 'ia32': break
|
||||
case 'x64': break
|
||||
default: return cb(new Error('Unsupported arch. Must be either ia32 or x64'))
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'darwin': platformPackager = mac; break
|
||||
case 'linux': platformPackager = linux; break
|
||||
case 'win32': platformPackager = win32; break
|
||||
default: return cb(new Error('Unsupported platform. Must be either darwin, linux, or win32'))
|
||||
}
|
||||
var archs = validateList(opts.all ? 'all' : opts.arch, supportedArchs, 'arch')
|
||||
var platforms = validateList(opts.all ? 'all' : opts.platform, supportedPlatforms, 'platform')
|
||||
if (!opts.version) return cb(new Error('Must specify version'))
|
||||
if (!Array.isArray(archs)) return cb(new Error(archs))
|
||||
if (!Array.isArray(platforms)) return cb(new Error(platforms))
|
||||
|
||||
// Ignore this and related modules by default
|
||||
var defaultIgnores = ['/node_modules/electron-prebuilt($|/)', '/node_modules/electron-packager($|/)', '/\.git($|/)']
|
||||
if (opts.ignore && !Array.isArray(opts.ignore)) opts.ignore = [opts.ignore]
|
||||
opts.ignore = (opts.ignore) ? opts.ignore.concat(defaultIgnores) : defaultIgnores
|
||||
|
||||
download({
|
||||
platform: platform,
|
||||
arch: arch,
|
||||
version: version
|
||||
}, function (err, zipPath) {
|
||||
series(createSeries(opts, archs, platforms), function (err, appPaths) {
|
||||
if (err) return cb(err)
|
||||
console.error('Packaging app for platform', platform + ' ' + arch, 'using electron v' + version)
|
||||
// extract zip into tmp so that packager can use it as a template
|
||||
var tmpDir = path.join(os.tmpdir(), 'electron-packager-' + platform + '-template')
|
||||
rimraf(tmpDir, function (err) {
|
||||
if (err) {} // ignore err
|
||||
mkdirp(tmpDir, function (err) {
|
||||
if (err) return cb(err)
|
||||
extract(zipPath, {dir: tmpDir}, function (err) {
|
||||
if (err) return cb(err)
|
||||
platformPackager.createApp(opts, tmpDir, cb)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
cb(null, appPaths.filter(function (appPath) {
|
||||
// Remove falsy entries (e.g. skipped platforms)
|
||||
return appPath
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
71
linux.js
71
linux.js
@ -1,70 +1,15 @@
|
||||
var path = require('path')
|
||||
var fs = require('fs')
|
||||
var mkdirp = require('mkdirp')
|
||||
var ncp = require('ncp').ncp
|
||||
var rimraf = require('rimraf')
|
||||
var mv = require('mv')
|
||||
var common = require('./common')
|
||||
|
||||
module.exports = {
|
||||
createApp: function createApp (opts, templateApp, cb) {
|
||||
var finalDir = path.join(opts.out || process.cwd(), opts.name + '-linux')
|
||||
var userAppDir = path.join(finalDir, 'resources', 'default_app')
|
||||
var originalBinary = path.join(finalDir, 'electron')
|
||||
var finalBinary = path.join(finalDir, opts.name)
|
||||
|
||||
function copyApp () {
|
||||
var createApp = function (err) {
|
||||
if (err) return cb(err)
|
||||
mkdirp(finalDir, function AppFolderCreated (err) {
|
||||
if (err) return cb(err)
|
||||
copyAppTemplate()
|
||||
})
|
||||
}
|
||||
if (opts.overwrite) {
|
||||
fs.exists(finalDir, function (exists) {
|
||||
if (exists) {
|
||||
console.log('Overwriting existing ' + finalDir + ' ...')
|
||||
rimraf(finalDir, createApp)
|
||||
} else {
|
||||
createApp()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
createApp()
|
||||
}
|
||||
}
|
||||
|
||||
function copyAppTemplate () {
|
||||
ncp(templateApp, finalDir, {filter: appFilter}, function AppCreated (err) {
|
||||
if (err) return cb(err)
|
||||
copyUserApp()
|
||||
createApp: function createApp (opts, templatePath, callback) {
|
||||
common.initializeApp(opts, templatePath, path.join('resources', 'app'), function buildLinuxApp (err, tempPath) {
|
||||
if (err) return callback(err)
|
||||
mv(path.join(tempPath, 'electron'), path.join(tempPath, opts.name), function (err) {
|
||||
if (err) return callback(err)
|
||||
common.moveApp(opts, tempPath, callback)
|
||||
})
|
||||
}
|
||||
|
||||
function copyUserApp () {
|
||||
ncp(opts.dir, userAppDir, {filter: common.userIgnoreFilter(opts, finalDir), dereference: true}, function copied (err) {
|
||||
if (err) return cb(err)
|
||||
common.prune(opts, userAppDir, cb, renameElectronBinary)
|
||||
})
|
||||
}
|
||||
|
||||
function renameElectronBinary () {
|
||||
fs.rename(originalBinary, finalBinary, function electronRenamed (err) {
|
||||
var asarDir
|
||||
if (err) return cb(err)
|
||||
if (opts.asar) {
|
||||
asarDir = path.join(finalDir, 'resources')
|
||||
common.asarApp(asarDir, cb)
|
||||
} else {
|
||||
cb(null, finalBinary)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function appFilter (file) {
|
||||
return file.match(/default_app/) === null
|
||||
}
|
||||
|
||||
copyApp()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
213
mac.js
213
mac.js
@ -1,160 +1,87 @@
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
var fs = require('fs')
|
||||
var child = require('child_process')
|
||||
|
||||
var plist = require('plist')
|
||||
var mkdirp = require('mkdirp')
|
||||
var rimraf = require('rimraf')
|
||||
var mv = require('mv')
|
||||
var ncp = require('ncp').ncp
|
||||
var series = require('run-series')
|
||||
var common = require('./common')
|
||||
|
||||
module.exports = {
|
||||
createApp: function createApp (opts, electronPath, cb) {
|
||||
var electronApp = path.join(electronPath, 'Electron.app')
|
||||
var tmpDir = path.join(os.tmpdir(), 'electron-packager-mac')
|
||||
createApp: function createApp (opts, templatePath, callback) {
|
||||
var appRelativePath = path.join('Electron.app', 'Contents', 'Resources', 'app')
|
||||
common.initializeApp(opts, templatePath, appRelativePath, function buildMacApp (err, tempPath) {
|
||||
if (err) return callback(err)
|
||||
|
||||
var newApp = path.join(tmpDir, opts.name + '.app')
|
||||
var contentsPath = path.join(tempPath, 'Electron.app', 'Contents')
|
||||
var helperPath = path.join(contentsPath, 'Frameworks', 'Electron Helper.app')
|
||||
var appPlistFilename = path.join(contentsPath, 'Info.plist')
|
||||
var helperPlistFilename = path.join(helperPath, 'Contents', 'Info.plist')
|
||||
var appPlist = plist.parse(fs.readFileSync(appPlistFilename).toString())
|
||||
var helperPlist = plist.parse(fs.readFileSync(helperPlistFilename).toString())
|
||||
|
||||
// reset build folders + copy template app
|
||||
rimraf(tmpDir, function rmrfd () {
|
||||
// ignore errors
|
||||
mkdirp(newApp, function mkdirpd () {
|
||||
// ignore errors
|
||||
// copy .app folder and use as template (this is exactly what Atom editor does)
|
||||
ncp(electronApp, newApp, function copied (err) {
|
||||
if (err) return cb(err)
|
||||
buildMacApp(opts, cb, newApp)
|
||||
// Update plist files
|
||||
var defaultBundleName = 'com.electron.' + opts.name.toLowerCase().replace(/ /g, '_')
|
||||
var appVersion = opts['app-version']
|
||||
|
||||
appPlist.CFBundleDisplayName = opts.name
|
||||
appPlist.CFBundleIdentifier = opts['app-bundle-id'] || defaultBundleName
|
||||
appPlist.CFBundleName = opts.name
|
||||
helperPlist.CFBundleIdentifier = opts['helper-bundle-id'] || defaultBundleName + '.helper'
|
||||
helperPlist.CFBundleName = opts.name
|
||||
|
||||
if (appVersion) {
|
||||
appPlist.CFBundleVersion = appVersion
|
||||
}
|
||||
|
||||
if (opts.protocols) {
|
||||
helperPlist.CFBundleURLTypes = appPlist.CFBundleURLTypes = opts.protocols.map(function (protocol) {
|
||||
return {
|
||||
CFBundleURLName: protocol.name,
|
||||
CFBundleURLSchemes: [].concat(protocol.schemes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fs.writeFileSync(appPlistFilename, plist.build(appPlist))
|
||||
fs.writeFileSync(helperPlistFilename, plist.build(helperPlist))
|
||||
|
||||
var operations = []
|
||||
|
||||
if (opts.icon) {
|
||||
operations.push(function (cb) {
|
||||
common.normalizeExt(opts.icon, '.icns', function (err, icon) {
|
||||
if (err) {
|
||||
// Ignore error if icon doesn't exist, in case it's only available for other OS
|
||||
cb(null)
|
||||
} else {
|
||||
ncp(icon, path.join(contentsPath, 'Resources', 'atom.icns'), cb)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Move Helper binary, then Helper.app, then top-level .app
|
||||
var finalAppPath = path.join(tempPath, opts.name + '.app')
|
||||
operations.push(function (cb) {
|
||||
var helperBinaryPath = path.join(helperPath, 'Contents', 'MacOS')
|
||||
mv(path.join(helperBinaryPath, 'Electron Helper'), path.join(helperBinaryPath, opts.name + ' Helper'), cb)
|
||||
}, function (cb) {
|
||||
mv(helperPath, path.join(path.dirname(helperPath), opts.name + ' Helper.app'), cb)
|
||||
}, function (cb) {
|
||||
mv(path.dirname(contentsPath), finalAppPath, cb)
|
||||
})
|
||||
|
||||
if (opts.sign) {
|
||||
operations.push(function (cb) {
|
||||
child.exec('codesign --deep --force --sign "' + opts.sign + '" ' + finalAppPath, cb)
|
||||
})
|
||||
}
|
||||
|
||||
series(operations, function () {
|
||||
common.moveApp(opts, tempPath, callback)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function buildMacApp (opts, cb, newApp) {
|
||||
var paths = {
|
||||
info1: path.join(newApp, 'Contents', 'Info.plist'),
|
||||
info2: path.join(newApp, 'Contents', 'Frameworks', 'Electron Helper.app', 'Contents', 'Info.plist'),
|
||||
app: path.join(newApp, 'Contents', 'Resources', 'app'),
|
||||
helper: path.join(newApp, 'Contents', 'Frameworks', 'Electron Helper.app')
|
||||
}
|
||||
|
||||
// update plist files
|
||||
var pl1 = plist.parse(fs.readFileSync(paths.info1).toString())
|
||||
var pl2 = plist.parse(fs.readFileSync(paths.info2).toString())
|
||||
|
||||
var bundleId = opts['app-bundle-id'] || 'com.electron.' + opts.name.toLowerCase()
|
||||
var bundleHelperId = opts['helper-bundle-id'] || 'com.electron.' + opts.name.toLowerCase() + '.helper'
|
||||
var appVersion = opts['app-version']
|
||||
|
||||
pl1.CFBundleDisplayName = opts.name
|
||||
pl1.CFBundleIdentifier = bundleId
|
||||
pl1.CFBundleName = opts.name
|
||||
pl2.CFBundleIdentifier = bundleHelperId
|
||||
pl2.CFBundleName = opts.name
|
||||
|
||||
if (appVersion) {
|
||||
pl1.CFBundleVersion = appVersion
|
||||
}
|
||||
|
||||
if (opts.protocols) {
|
||||
pl2.CFBundleURLTypes = pl1.CFBundleURLTypes = opts.protocols.map(function (protocol) {
|
||||
return {
|
||||
CFBundleURLName: protocol.name,
|
||||
CFBundleURLSchemes: [].concat(protocol.schemes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fs.writeFileSync(paths.info1, plist.build(pl1))
|
||||
fs.writeFileSync(paths.info2, plist.build(pl2))
|
||||
|
||||
// copy users app into .app
|
||||
ncp(opts.dir, paths.app, {filter: common.userIgnoreFilter(opts), dereference: true}, function copied (err) {
|
||||
if (err) return cb(err)
|
||||
|
||||
function moveHelper () {
|
||||
// Move helper binary before moving the parent helper app directory itself
|
||||
var helperDestination = path.join(path.dirname(paths.helper), opts.name + ' Helper.app')
|
||||
var helperBinary = path.join(paths.helper, 'Contents', 'MacOS', 'Electron Helper')
|
||||
var helperBinaryDestination = path.join(path.dirname(helperBinary), opts.name + ' Helper')
|
||||
|
||||
fs.rename(helperBinary, helperBinaryDestination, function (err) {
|
||||
if (err) return cb(err)
|
||||
fs.rename(paths.helper, helperDestination, function (err) {
|
||||
if (err) return cb(err)
|
||||
moveApp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function moveApp () {
|
||||
// finally, move app into cwd
|
||||
var outdir = opts.out || process.cwd()
|
||||
var finalPath = path.join(outdir, opts.name + '.app')
|
||||
|
||||
mkdirp(outdir, function mkoutdirp () {
|
||||
if (err) return cb(err)
|
||||
if (opts.overwrite) {
|
||||
fs.exists(finalPath, function (exists) {
|
||||
if (exists) {
|
||||
console.log('Overwriting existing ' + finalPath + ' ...')
|
||||
rimraf(finalPath, deploy)
|
||||
} else {
|
||||
deploy()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
deploy()
|
||||
}
|
||||
|
||||
function deploy (err) {
|
||||
if (err) return cb(err)
|
||||
mv(newApp, finalPath, function moved (err) {
|
||||
if (err) return cb(err)
|
||||
if (opts.asar) {
|
||||
var finalPath = path.join(opts.out || process.cwd(), opts.name + '.app', 'Contents', 'Resources')
|
||||
common.asarApp(finalPath, function (err) {
|
||||
if (err) return cb(err)
|
||||
updateMacIcon(function (err) {
|
||||
if (err) return cb(err)
|
||||
codesign()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
updateMacIcon(function (err) {
|
||||
if (err) return cb(err)
|
||||
codesign()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateMacIcon (cb) {
|
||||
var finalPath = path.join(opts.out || process.cwd(), opts.name + '.app')
|
||||
|
||||
if (!opts.icon) {
|
||||
return cb(null)
|
||||
}
|
||||
|
||||
ncp(opts.icon, path.join(finalPath, 'Contents', 'Resources', 'atom.icns'), function copied (err) {
|
||||
cb(err)
|
||||
})
|
||||
}
|
||||
|
||||
function codesign () {
|
||||
var appPath = path.join(opts.out || process.cwd(), opts.name + '.app')
|
||||
|
||||
if (!opts.sign) return cb(null, appPath)
|
||||
|
||||
child.exec('codesign --deep --force --sign "' + opts.sign + '" ' + appPath, function (err, stdout, stderr) {
|
||||
cb(err, appPath)
|
||||
})
|
||||
}
|
||||
|
||||
common.prune(opts, paths.app, cb, moveHelper)
|
||||
})
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
33
readme.md
33
readme.md
@ -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
273
test/basic.js
Normal 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
4
test/config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"timeout": 15000,
|
||||
"version": "0.28.3"
|
||||
}
|
2
test/fixtures/basic/electron-packager/readme.txt
vendored
Normal file
2
test/fixtures/basic/electron-packager/readme.txt
vendored
Normal 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
0
test/fixtures/basic/ignore/this.txt
vendored
Normal file
1
test/fixtures/basic/ignorethis.txt
vendored
Normal file
1
test/fixtures/basic/ignorethis.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
|
4
test/fixtures/basic/index.html
vendored
Normal file
4
test/fixtures/basic/index.html
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>Hello, world!</body>
|
||||
</html>
|
22
test/fixtures/basic/main.js
vendored
Normal file
22
test/fixtures/basic/main.js
vendored
Normal 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
9
test/fixtures/basic/package.json
vendored
Normal 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
BIN
test/fixtures/monochrome.icns
vendored
Normal file
Binary file not shown.
27
test/index.js
Normal file
27
test/index.js
Normal 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
94
test/mac.js
Normal 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
145
test/multitarget.js
Normal 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()
|
45
test/test.js
45
test/test.js
@ -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()
|
||||
})
|
||||
})
|
@ -1,6 +0,0 @@
|
||||
var app = require('app')
|
||||
|
||||
app.on('ready', function () {
|
||||
console.log('pizza')
|
||||
app.exit()
|
||||
})
|
@ -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
100
test/util.js
Normal 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()
|
||||
}
|
15
usage.txt
15
usage.txt
@ -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
104
win32.js
@ -1,92 +1,40 @@
|
||||
var os = require('os')
|
||||
var path = require('path')
|
||||
|
||||
var mkdirp = require('mkdirp')
|
||||
var rimraf = require('rimraf')
|
||||
var ncp = require('ncp').ncp
|
||||
var mv = require('mv')
|
||||
var rcedit = require('rcedit')
|
||||
var series = require('run-series')
|
||||
var common = require('./common')
|
||||
|
||||
module.exports = {
|
||||
createApp: function createApp (opts, electronApp, cb) {
|
||||
var tmpDir = path.join(os.tmpdir(), 'electron-packager-windows')
|
||||
createApp: function createApp (opts, templatePath, callback) {
|
||||
common.initializeApp(opts, templatePath, path.join('resources', 'app'), function buildWinApp (err, tempPath) {
|
||||
if (err) return callback(err)
|
||||
|
||||
var newApp = path.join(tmpDir, opts.name + '-win32')
|
||||
// reset build folders + copy template app
|
||||
rimraf(tmpDir, function rmrfd () {
|
||||
// ignore errors
|
||||
mkdirp(newApp, function mkdirpd () {
|
||||
// ignore errors
|
||||
// copy app folder and use as template (this is exactly what Atom editor does)
|
||||
ncp(electronApp, newApp, function copied (err) {
|
||||
if (err) return cb(err)
|
||||
// rename electron.exe
|
||||
mv(path.join(newApp, 'electron.exe'), path.join(newApp, opts.name + '.exe'), function (err) {
|
||||
if (err) return cb(err)
|
||||
var newExePath = path.join(tempPath, opts.name + '.exe')
|
||||
var operations = [
|
||||
function (cb) {
|
||||
mv(path.join(tempPath, 'electron.exe'), newExePath, cb)
|
||||
}
|
||||
]
|
||||
|
||||
buildWinApp(opts, cb, newApp)
|
||||
if (opts.icon || opts['version-string']) {
|
||||
operations.push(function (cb) {
|
||||
common.normalizeExt(opts.icon, '.ico', function (err, icon) {
|
||||
var rcOpts = {}
|
||||
if (opts['version-string']) rcOpts['version-string'] = opts['version-string']
|
||||
|
||||
// Icon might be omitted or only exist in one OS's format, so skip it if normalizeExt reports an error
|
||||
if (!err) {
|
||||
rcOpts.icon = icon
|
||||
}
|
||||
|
||||
require('rcedit')(newExePath, rcOpts, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
series(operations, function () {
|
||||
common.moveApp(opts, tempPath, callback)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function copy (from, to, cb) {
|
||||
rimraf(to, function () {
|
||||
mkdirp(to, function () {
|
||||
ncp(from, to, function (err) {
|
||||
if (err) { return cb(err) }
|
||||
cb()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function buildWinApp (opts, cb, newApp) {
|
||||
var paths = {
|
||||
app: path.join(newApp, 'resources', 'app')
|
||||
}
|
||||
|
||||
// copy users app into destination path
|
||||
ncp(opts.dir, paths.app, {filter: common.userIgnoreFilter(opts), dereference: true}, function copied (err) {
|
||||
if (err) return cb(err)
|
||||
|
||||
function moveApp () {
|
||||
// finally, move app into cwd
|
||||
var finalPath = path.join(opts.out || process.cwd(), opts.name + '-win32')
|
||||
copy(newApp, finalPath, function moved (err) {
|
||||
if (err) return cb(err)
|
||||
if (opts.asar) {
|
||||
var finalPath = path.join(opts.out || process.cwd(), opts.name + '-win32', 'resources')
|
||||
common.asarApp(finalPath, function (err) {
|
||||
if (err) return cb(err)
|
||||
updateResourceData()
|
||||
})
|
||||
} else {
|
||||
updateResourceData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateResourceData () {
|
||||
var finalPath = path.join(opts.out || process.cwd(), opts.name + '-win32')
|
||||
|
||||
if (!opts.icon && !opts['version-string']) {
|
||||
return cb(null, finalPath)
|
||||
}
|
||||
|
||||
var exePath = path.join(opts.out || process.cwd(), opts.name + '-win32', opts.name + '.exe')
|
||||
var rcOptions = {
|
||||
icon: opts.icon,
|
||||
'version-string': opts['version-string']
|
||||
}
|
||||
rcedit(exePath, rcOptions, function (err) {
|
||||
cb(err, finalPath)
|
||||
})
|
||||
}
|
||||
|
||||
common.prune(opts, paths.app, cb, moveApp)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user