electron-packager
Version:
Package and distribute your Electron app with OS-specific bundles (.app, .exe etc) via JS or CLI
580 lines (509 loc) • 23.2 kB
JavaScript
var config = require('./config.json')
var exec = require('child_process').exec
var fs = require('fs')
var mac = require('../mac')
var packager = require('..')
var path = require('path')
var plist = require('plist')
var test = require('tape')
var util = require('./util')
var waterfall = require('run-waterfall')
function createIconTest (baseOpts, icon, iconPath) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(baseOpts)
opts.icon = icon
var resourcesPath
var plistPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
resourcesPath = path.join(paths[0], util.generateResourcesPath(opts))
plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist')
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
util.areFilesEqual(iconPath, path.join(resourcesPath, obj.CFBundleIconFile), cb)
}, function (equal, cb) {
t.true(equal, 'installed icon file should be identical to the specified icon file')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createExtraResourceTest (baseOpts) {
return function (t) {
t.timeoutAfter(config.timeout)
var extra1Base = 'data1.txt'
var extra1Path = path.join(__dirname, 'fixtures', extra1Base)
var opts = Object.create(baseOpts)
opts['extra-resource'] = extra1Path
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(extra1Path, path.join(resourcesPath, extra1Base), cb)
}, function (equal, cb) {
t.true(equal, 'resource file data1.txt should match')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createExtraResource2Test (baseOpts) {
return function (t) {
t.timeoutAfter(config.timeout)
var extra1Base = 'data1.txt'
var extra1Path = path.join(__dirname, 'fixtures', extra1Base)
var extra2Base = 'extrainfo.plist'
var extra2Path = path.join(__dirname, 'fixtures', extra2Base)
var opts = Object.create(baseOpts)
opts['extra-resource'] = [ extra1Path, extra2Path ]
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(extra1Path, path.join(resourcesPath, extra1Base), cb)
}, function (equal, cb) {
t.true(equal, 'resource file data1.txt should match')
util.areFilesEqual(extra2Path, path.join(resourcesPath, extra2Base), cb)
}, function (equal, cb) {
t.true(equal, 'resource file extrainfo.plist should match')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createExtendInfoTest (baseOpts, extraPath) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(baseOpts)
opts['extend-info'] = extraPath
opts['build-version'] = '3.2.1'
opts['app-bundle-id'] = 'com.electron.extratest'
opts['app-category-type'] = 'public.app-category.music'
var plistPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.TestKeyString, 'String data', 'TestKeyString should come from extend-info')
t.equal(obj.TestKeyInt, 12345, 'TestKeyInt should come from extend-info')
t.equal(obj.TestKeyBool, true, 'TestKeyBool should come from extend-info')
t.deepEqual(obj.TestKeyArray, ['public.content', 'public.data'], 'TestKeyArray should come from extend-info')
t.deepEqual(obj.TestKeyDict, { Number: 98765, CFBundleVersion: '0.0.0' }, 'TestKeyDict should come from extend-info')
t.equal(obj.CFBundleVersion, opts['build-version'], 'CFBundleVersion should reflect build-version argument')
t.equal(obj.CFBundleIdentifier, 'com.electron.extratest', 'CFBundleIdentifier should reflect app-bundle-id argument')
t.equal(obj.LSApplicationCategoryType, 'public.app-category.music', 'LSApplicationCategoryType should reflect app-category-type argument')
t.equal(obj.CFBundlePackageType, 'APPL', 'CFBundlePackageType should be Electron default')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAppVersionTest (baseOpts, appVersion, buildVersion) {
return function (t) {
t.timeoutAfter(config.timeout)
var plistPath
var opts = Object.create(baseOpts)
opts['app-version'] = opts['build-version'] = appVersion
if (buildVersion) {
opts['build-version'] = buildVersion
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.CFBundleVersion, '' + opts['build-version'], 'CFBundleVersion should reflect build-version')
t.equal(obj.CFBundleShortVersionString, '' + opts['app-version'], 'CFBundleShortVersionString should reflect app-version')
t.equal(typeof obj.CFBundleVersion, 'string', 'CFBundleVersion should be a string')
t.equal(typeof obj.CFBundleShortVersionString, 'string', 'CFBundleShortVersionString should be a string')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAppCategoryTypeTest (baseOpts, appCategoryType) {
return function (t) {
t.timeoutAfter(config.timeout)
var plistPath
var opts = Object.create(baseOpts)
opts['app-category-type'] = appCategoryType
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.LSApplicationCategoryType, opts['app-category-type'], 'LSApplicationCategoryType should reflect opts["app-category-type"]')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAppBundleTest (baseOpts, appBundleId) {
return function (t) {
t.timeoutAfter(config.timeout)
var plistPath
var opts = Object.create(baseOpts)
if (appBundleId) {
opts['app-bundle-id'] = appBundleId
}
var defaultBundleName = 'com.electron.' + opts.name.toLowerCase()
var appBundleIdentifier = mac.filterCFBundleIdentifier(opts['app-bundle-id'] || defaultBundleName)
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.CFBundleDisplayName, opts.name, 'CFBundleDisplayName should reflect opts.name')
t.equal(obj.CFBundleName, opts.name, 'CFBundleName should reflect opts.name')
t.equal(obj.CFBundleIdentifier, appBundleIdentifier, 'CFBundleName should reflect opts["app-bundle-id"] or fallback to default')
t.equal(typeof obj.CFBundleDisplayName, 'string', 'CFBundleDisplayName should be a string')
t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string')
t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string')
t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAppHelpersBundleTest (baseOpts, helperBundleId, appBundleId) {
return function (t) {
t.timeoutAfter(config.timeout)
var tempPath, plistPath
var opts = Object.create(baseOpts)
if (helperBundleId) {
opts['helper-bundle-id'] = appBundleId
}
if (appBundleId) {
opts['app-bundle-id'] = appBundleId
}
var defaultBundleName = 'com.electron.' + opts.name.toLowerCase()
var appBundleIdentifier = mac.filterCFBundleIdentifier(opts['app-bundle-id'] || defaultBundleName)
var helperBundleIdentifier = mac.filterCFBundleIdentifier(opts['helper-bundle-id'] || appBundleIdentifier + '.helper')
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
tempPath = paths[0]
plistPath = path.join(tempPath, opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist in helper app')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.CFBundleName, opts.name, 'CFBundleName should reflect opts.name in helper app')
t.equal(obj.CFBundleIdentifier, helperBundleIdentifier, 'CFBundleName should reflect opts["helper-bundle-id"], opts["app-bundle-id"] or fallback to default in helper app')
t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string in helper app')
t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string in helper app')
t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)')
// check helper EH
plistPath = path.join(tempPath, opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper EH.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist in helper EH app')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.CFBundleName, opts.name + ' Helper EH', 'CFBundleName should reflect opts.name in helper EH app')
t.equal(obj.CFBundleDisplayName, opts.name + ' Helper EH', 'CFBundleDisplayName should reflect opts.name in helper EH app')
t.equal(obj.CFBundleExecutable, opts.name + ' Helper EH', 'CFBundleExecutable should reflect opts.name in helper EH app')
t.equal(obj.CFBundleIdentifier, helperBundleIdentifier + '.EH', 'CFBundleName should reflect opts["helper-bundle-id"], opts["app-bundle-id"] or fallback to default in helper EH app')
t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string in helper EH app')
t.equal(typeof obj.CFBundleDisplayName, 'string', 'CFBundleDisplayName should be a string in helper EH app')
t.equal(typeof obj.CFBundleExecutable, 'string', 'CFBundleExecutable should be a string in helper EH app')
t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string in helper EH app')
t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)')
// check helper NP
plistPath = path.join(tempPath, opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper NP.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist in helper NP app')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.CFBundleName, opts.name + ' Helper NP', 'CFBundleName should reflect opts.name in helper NP app')
t.equal(obj.CFBundleDisplayName, opts.name + ' Helper NP', 'CFBundleDisplayName should reflect opts.name in helper NP app')
t.equal(obj.CFBundleExecutable, opts.name + ' Helper NP', 'CFBundleExecutable should reflect opts.name in helper NP app')
t.equal(obj.CFBundleIdentifier, helperBundleIdentifier + '.NP', 'CFBundleName should reflect opts["helper-bundle-id"], opts["app-bundle-id"] or fallback to default in helper NP app')
t.equal(typeof obj.CFBundleName, 'string', 'CFBundleName should be a string in helper NP app')
t.equal(typeof obj.CFBundleDisplayName, 'string', 'CFBundleDisplayName should be a string in helper NP app')
t.equal(typeof obj.CFBundleExecutable, 'string', 'CFBundleExecutable should be a string in helper NP app')
t.equal(typeof obj.CFBundleIdentifier, 'string', 'CFBundleIdentifier should be a string in helper NP app')
t.equal(/^[a-zA-Z0-9-.]*$/.test(obj.CFBundleIdentifier), true, 'CFBundleIdentifier should allow only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.)')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAppHumanReadableCopyrightTest (baseOpts, humanReadableCopyright) {
return function (t) {
t.timeoutAfter(config.timeout)
var plistPath
var opts = Object.create(baseOpts)
opts['app-copyright'] = humanReadableCopyright
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
plistPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Info.plist')
fs.stat(plistPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected Info.plist file should exist')
fs.readFile(plistPath, 'utf8', cb)
}, function (file, cb) {
var obj = plist.parse(file)
t.equal(obj.NSHumanReadableCopyright, opts['app-copyright'], 'NSHumanReadableCopyright should reflect opts["app-copyright"]')
cb()
}
], function (err) {
t.end(err)
})
}
}
// Share testing script with platform darwin and mas
module.exports = function (baseOpts) {
util.setup()
test('helper app paths test', function (t) {
t.timeoutAfter(config.timeout)
function getHelperExecutablePath (helperName) {
return path.join(helperName + '.app', 'Contents', 'MacOS', helperName)
}
var opts = Object.create(baseOpts)
var frameworksPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
frameworksPath = path.join(paths[0], opts.name + '.app', 'Contents', 'Frameworks')
// main Helper.app is already tested in basic test suite; test its executable and the other helpers
fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper')), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The Helper.app executable should reflect opts.name')
fs.stat(path.join(frameworksPath, opts.name + ' Helper EH.app'), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The Helper EH.app should reflect opts.name')
fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper EH')), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The Helper EH.app executable should reflect opts.name')
fs.stat(path.join(frameworksPath, opts.name + ' Helper NP.app'), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The Helper NP.app should reflect opts.name')
fs.stat(path.join(frameworksPath, getHelperExecutablePath(opts.name + ' Helper NP')), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The Helper NP.app executable should reflect opts.name')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
var iconBase = path.join(__dirname, 'fixtures', 'monochrome')
var icnsPath = iconBase + '.icns'
util.setup()
test('icon test: .icns specified', createIconTest(baseOpts, icnsPath, icnsPath))
util.teardown()
var el0374Opts = {
name: 'el0374Test',
dir: path.join(__dirname, 'fixtures', 'el-0374'),
version: '0.37.4',
arch: 'x64',
platform: 'darwin'
}
// use iconBase, icnsPath from previous test
util.setup()
test('icon test: el-0.37.4, .icns specified', createIconTest(el0374Opts, icnsPath, icnsPath))
util.teardown()
util.setup()
test('icon test: .ico specified (should replace with .icns)', createIconTest(baseOpts, iconBase + '.ico', icnsPath))
util.teardown()
util.setup()
test('icon test: basename only (should add .icns)', createIconTest(baseOpts, iconBase, icnsPath))
util.teardown()
var extraInfoPath = path.join(__dirname, 'fixtures', 'extrainfo.plist')
util.setup()
test('extend-info test', createExtendInfoTest(baseOpts, extraInfoPath))
util.teardown()
util.setup()
test('extra-resource test: one arg', createExtraResourceTest(baseOpts))
util.teardown()
util.setup()
test('extra-resource test: two arg', createExtraResource2Test(baseOpts))
util.teardown()
test('osx-sign argument test: default args', function (t) {
var args = true
var sign_opts = mac.createSignOpts(args, 'darwin', 'out')
t.same(sign_opts, {identity: null, app: 'out', platform: 'darwin'})
t.end()
})
test('osx-sign argument test: identity=true sets autodiscovery mode', function (t) {
var args = {identity: true}
var sign_opts = mac.createSignOpts(args, 'darwin', 'out')
t.same(sign_opts, {identity: null, app: 'out', platform: 'darwin'})
t.end()
})
test('osx-sign argument test: entitlements passed to electron-osx-sign', function (t) {
var args = {entitlements: 'path-to-entitlements'}
var sign_opts = mac.createSignOpts(args, 'darwin', 'out')
t.same(sign_opts, {app: 'out', platform: 'darwin', entitlements: args.entitlements})
t.end()
})
test('osx-sign argument test: app not overwritten', function (t) {
var args = {app: 'some-other-path'}
var sign_opts = mac.createSignOpts(args, 'darwin', 'out')
t.same(sign_opts, {app: 'out', platform: 'darwin'})
t.end()
})
test('osx-sign argument test: platform not overwritten', function (t) {
var args = {platform: 'mas'}
var sign_opts = mac.createSignOpts(args, 'darwin', 'out')
t.same(sign_opts, {app: 'out', platform: 'darwin'})
t.end()
})
util.setup()
test('codesign test', function (t) {
t.timeoutAfter(config.macExecTimeout)
var opts = Object.create(baseOpts)
opts['osx-sign'] = {identity: 'Developer CodeCert'}
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 -v ' + 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()
util.setup()
test('binary naming test', function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(baseOpts)
var binaryPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
binaryPath = path.join(paths[0], opts.name + '.app', 'Contents', 'MacOS')
fs.stat(path.join(binaryPath, opts.name), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The binary should reflect opts.name')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
util.setup()
test('app and build version test', createAppVersionTest(baseOpts, '1.1.0', '1.1.0.1234'))
util.teardown()
util.setup()
test('app version test', createAppVersionTest(baseOpts, '1.1.0'))
util.teardown()
util.setup()
test('app and build version integer test', createAppVersionTest(baseOpts, 12, 1234))
util.teardown()
util.setup()
test('app categoryType test', createAppCategoryTypeTest(baseOpts, 'public.app-category.developer-tools'))
util.teardown()
util.setup()
test('app bundle test', createAppBundleTest(baseOpts, 'com.electron.basetest'))
util.teardown()
util.setup()
test('app bundle (w/ special characters) test', createAppBundleTest(baseOpts, 'com.electron."bãśè tëßt!@#$%^&*()?\''))
util.teardown()
util.setup()
test('app bundle app-bundle-id fallback test', createAppBundleTest(baseOpts))
util.teardown()
util.setup()
test('app helpers bundle test', createAppHelpersBundleTest(baseOpts, 'com.electron.basetest.helper'))
util.teardown()
util.setup()
test('app helpers bundle (w/ special characters) test', createAppHelpersBundleTest(baseOpts, 'com.electron."bãśè tëßt!@#$%^&*()?\'.hęłpėr'))
util.teardown()
util.setup()
test('app helpers bundle helper-bundle-id fallback to app-bundle-id test', createAppHelpersBundleTest(baseOpts, null, 'com.electron.basetest'))
util.teardown()
util.setup()
test('app helpers bundle helper-bundle-id fallback to app-bundle-id (w/ special characters) test', createAppHelpersBundleTest(baseOpts, null, 'com.electron."bãśè tëßt!!@#$%^&*()?\''))
util.teardown()
util.setup()
test('app helpers bundle helper-bundle-id & app-bundle-id fallback test', createAppHelpersBundleTest(baseOpts))
util.teardown()
util.setup()
test('app humanReadableCopyright test', createAppHumanReadableCopyrightTest(baseOpts, 'Copyright © 2003–2015 Organization. All rights reserved.'))
util.teardown()
}