Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for prebuilt asars #823

Merged
merged 2 commits into from Oct 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS.md
Expand Up @@ -4,6 +4,10 @@

[Unreleased]: https://github.com/electron-userland/electron-packager/compare/v12.2.0...master

### Added

* `prebuiltAsar` option to specify a prebuilt ASAR file (#823)

## [12.2.0] - 2018-10-02

[12.2.0]: https://github.com/electron-userland/electron-packager/compare/v12.1.2...v12.2.0
Expand Down
28 changes: 27 additions & 1 deletion docs/api.md
Expand Up @@ -67,6 +67,8 @@ packager({
})
```

**Note:** `afterCopy` will not be called if [`prebuiltAsar`](#prebuiltasar) is set.

##### `afterExtract`

*Array of Functions*
Expand Down Expand Up @@ -118,7 +120,7 @@ in the temporary directory. Each function is called with five parameters:
- `arch` (*String*): The target architecture you are packaging for
- `callback` (*Function*): Must be called once you have completed your actions

**NOTE:** None of these functions will be called if the `prune` option is `false`.
**Note:** None of these functions will be called if the `prune` option is `false` or `prebuiltAsar` is set.

By default, the functions are called in parallel (via
[`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)).
Expand Down Expand Up @@ -194,6 +196,8 @@ Whether to package the application's source code into an archive, using [Electro
- `asar.unpackDir = '**/{sub_dir1/sub_sub_dir,sub_dir2}/**'` will unpack the subdirectories of the directories `/<dir>/sub_dir1/sub_sub_dir` and `/<dir>/sub_dir2`.
- `asar.unpackDir = '**/{sub_dir1/sub_sub_dir,sub_dir2}/**/*'` will unpack the directories `/<dir>/sub_dir1/sub_sub_dir` and `/<dir>/sub_dir2` and their subdirectories.

**Note:** `asar` will be ignored if [`prebuiltAsar`](#prebuiltasar) is set.

##### `buildVersion`

*String*
Expand All @@ -206,6 +210,8 @@ The build version of the application. Defaults to the value of [`appVersion`](#a

Whether symlinks should be dereferenced during the copying of the application source.

**Note:** `derefSymlinks` will be ignored if [`prebuiltAsar`](#prebuiltasar) is set.

##### `download`

*Object*
Expand Down Expand Up @@ -300,6 +306,8 @@ Alternatively, this can be a predicate function that, given an absolute file pat
the file should be ignored, or `false` if the file should be kept. *This does not use any of the
default ignored directories listed above.*

**Note:** `ignore` will be ignored if [`prebuiltAsar`](#prebuiltasar) is set.

##### `name`

*String*
Expand Down Expand Up @@ -334,13 +342,31 @@ Arbitrary combinations of individual platforms are also supported via a comma-de
The non-`all` values correspond to the platform names used by [Electron releases]. This value
is not restricted to the official set if [`download.mirror`](#download) is set.

##### `prebuiltAsar`

*String*

The path to a prebuilt ASAR file.

**Note:** Setting this option prevents the following options from being used, as the functionality
gets skipped over:

* [`asar`](#asar)
* [`afterCopy`](#aftercopy)
* [`afterPrune`](#afterprune)
* [`derefSymlinks`](#derefsymlinks)
* [`ignore`](#ignore)
* [`prune`](#prune)

##### `prune`

*Boolean* (default: `true`)

Walks the `node_modules` dependency tree to remove all of the packages specified in the
`devDependencies` section of `package.json` from the outputted Electron app.

**Note:** `prune` will be ignored if [`prebuiltAsar`](#prebuiltasar) is set.

##### `quiet`

*Boolean* (default: `false`)
Expand Down
1 change: 1 addition & 0 deletions ignore.js
Expand Up @@ -13,6 +13,7 @@ const DEFAULT_IGNORES = [
]

function generateIgnores (opts) {
opts.originalIgnore = opts.ignore
if (typeof (opts.ignore) !== 'function') {
if (opts.ignore) {
opts.ignore = common.ensureArray(opts.ignore).concat(DEFAULT_IGNORES)
Expand Down
98 changes: 73 additions & 25 deletions platform.js
Expand Up @@ -14,6 +14,7 @@ class App {
constructor (opts, templatePath) {
this.opts = opts
this.templatePath = templatePath
this.asarOptions = common.createAsarOpts(opts)

if (this.opts.prune === undefined) {
this.opts.prune = true
Expand Down Expand Up @@ -68,6 +69,10 @@ class App {
}
}

get appAsarPath () {
return path.join(this.originalResourcesDir, 'app.asar')
}

relativeRename (basePath, oldName, newName) {
debug(`Renaming ${oldName} to ${newName} in ${basePath}`)
return fs.rename(path.join(basePath, oldName), path.join(basePath, newName))
Expand All @@ -80,11 +85,14 @@ class App {
/**
* 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 either truthy or undefined)
* * Remove default_app (which is either a folder or an asar file)
* * Creates an asar (if opts.asar is set)
* * If a prebuilt asar is specified:
* * Copies asar into temporary directory as app.asar
* * Otherwise:
* * Copies template into temporary directory
* * Copies user's app into temporary directory
* * Prunes non-production node_modules (if opts.prune is either truthy or undefined)
* * Creates an asar (if opts.asar is set)
*
* Prune and asar are performed before platform-specific logic, primarily so that
* this.originalResourcesAppDir is predictable (e.g. before .app is renamed for Mac)
Expand All @@ -93,32 +101,40 @@ class App {
debug(`Initializing app in ${this.stagingPath} from ${this.templatePath} template`)

return fs.move(this.templatePath, this.stagingPath, { clobber: true })
.then(() => this.copyTemplate())
.then(() => this.removeDefaultApp())
.then(() => {
if (this.opts.prebuiltAsar) {
return this.copyPrebuiltAsar()
} else {
return this.buildApp()
}
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather have a real if statement than a multi-line ternary statement.

}

buildApp () {
return this.copyTemplate()
.then(() => this.asarApp())
}

copyTemplate () {
return fs.copy(this.opts.dir, this.originalResourcesAppDir, {
filter: ignore.userIgnoreFilter(this.opts),
dereference: this.opts.derefSymlinks
}).then(() => hooks.promisifyHooks(this.opts.afterCopy, [
const hookArgs = [
this.originalResourcesAppDir,
this.opts.electronVersion,
this.opts.platform,
this.opts.arch
])).then(() => {
if (this.opts.prune) {
return hooks.promisifyHooks(this.opts.afterPrune, [
this.originalResourcesAppDir,
this.opts.electronVersion,
this.opts.platform,
this.opts.arch
])
} else {
return true
}
]

return fs.copy(this.opts.dir, this.originalResourcesAppDir, {
filter: ignore.userIgnoreFilter(this.opts),
dereference: this.opts.derefSymlinks
})
.then(() => hooks.promisifyHooks(this.opts.afterCopy, hookArgs))
.then(() => {
if (this.opts.prune) {
return hooks.promisifyHooks(this.opts.afterPrune, hookArgs)
}
return true
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether afterPrune and afterCopy hooks should be run, since neither of those operations actually took place, and this.originalResourcesAppDir doesn't actually exist.

Copy link
Contributor Author

@jsg2021 jsg2021 Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't know what those hooks were for... I've changed it to just return early.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think returning early is the right solution... we may have to adjust the hook signature so that it's either the path to the resources app directory, or the path to the app.asar file (which would be a breaking change).

@MarshallOfSound since you have dealt with a lot of code relating to the hooks, you might have some insight here.

}

removeDefaultApp () {
Expand Down Expand Up @@ -146,15 +162,47 @@ class App {
.catch(/* istanbul ignore next */ () => null)
}

prebuiltAsarWarning (option, triggerWarning) {
if (triggerWarning) {
common.warning(`prebuiltAsar and ${option} are incompatible, ignoring the ${option} option`)
}
}

copyPrebuiltAsar () {
if (this.asarOptions) {
common.warning('prebuiltAsar has been specified, all asar options will be ignored')
}

for (const hookName of ['afterCopy', 'afterPrune']) {
if (this.opts[hookName]) {
throw new Error(`${hookName} is incompatible with prebuiltAsar`)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean? Test that it warns? or test that the other options are ignored? If the later, how should that be done?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test that it warns. This is an example of doing that (although it should probably be genericized into a function):

https://github.com/electron-userland/electron-packager/blob/a7437b908e68e97b288173759d0538efe7798c82/test/mas.js#L18-L33

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

}

this.prebuiltAsarWarning('ignore', this.opts.originalIgnore)
this.prebuiltAsarWarning('prune', !this.opts.prune)
this.prebuiltAsarWarning('derefSymlinks', this.opts.derefSymlinks !== undefined)

const src = path.resolve(this.opts.prebuiltAsar)

return fs.stat(src)
.then(stat => {
if (!stat.isFile()) {
throw new Error(`${src} specified in prebuiltAsar must be an asar file.`)
}

debug(`Copying asar: ${src} to ${this.appAsarPath}`)
return fs.copy(src, this.appAsarPath, {overwrite: false, errorOnExist: true})
})
}

asarApp () {
const asarOptions = common.createAsarOpts(this.opts)
if (!asarOptions) {
if (!this.asarOptions) {
return Promise.resolve()
}

const dest = path.join(this.originalResourcesDir, 'app.asar')
debug(`Running asar with the options ${JSON.stringify(asarOptions)}`)
return pify(asar.createPackageWithOptions)(this.originalResourcesAppDir, dest, asarOptions)
debug(`Running asar with the options ${JSON.stringify(this.asarOptions)}`)
return pify(asar.createPackageWithOptions)(this.originalResourcesAppDir, this.appAsarPath, this.asarOptions)
.then(() => fs.remove(this.originalResourcesAppDir))
}

Expand Down
1 change: 1 addition & 0 deletions test/_setup.js
Expand Up @@ -74,6 +74,7 @@ function npmInstallForFixture (fixture) {

function npmInstallForFixtures () {
const fixtures = [
'asar-prebuilt',
'basic',
'basic-renamed-to-electron',
'electron-in-dependencies',
Expand Down
13 changes: 11 additions & 2 deletions test/_util.js
Expand Up @@ -37,6 +37,12 @@ test.afterEach.always(t => {
.then(() => fs.remove(t.context.tempDir))
})

test.serial.afterEach.always(() => {
if (console.warn.restore) {
console.warn.restore()
}
})

function testSinglePlatform (name, testFunction, testFunctionArgs, parallel) {
module.exports.packagerTest(name, (t, opts) => {
Object.assign(opts, module.exports.singlePlatformOptions())
Expand Down Expand Up @@ -73,14 +79,17 @@ module.exports = {
return fs.lstat(pathToCheck)
.then(stats => t.true(stats.isSymbolicLink(), message))
},
assertWarning: function assertWarning (t, message) {
t.true(console.warn.calledWithExactly(message), `console.warn should be called with: ${message}`)
},
fixtureSubdir: setup.fixtureSubdir,
generateResourcesPath: function generateResourcesPath (opts) {
return common.isPlatformMac(opts.platform)
? path.join(opts.name + '.app', 'Contents', 'Resources')
: 'resources'
},
invalidOptionTest: function invalidOptionTest (opts) {
return t => t.throws(packager(opts))
invalidOptionTest: function invalidOptionTest (opts, err, message) {
return t => t.throws(packager(opts), err, message)
},
packageAndEnsureResourcesPath: function packageAndEnsureResourcesPath (t, opts) {
let resourcesPath
Expand Down
45 changes: 45 additions & 0 deletions test/asar.js
Expand Up @@ -3,6 +3,7 @@
const common = require('../common')
const path = require('path')
const test = require('ava')
const sinon = require('sinon')
const util = require('./_util')

test('asar argument test: asar is not set', t => {
Expand Down Expand Up @@ -49,3 +50,47 @@ util.testSinglePlatform('asar test', (t, opts) => {
])
})
})

util.testSinglePlatform('prebuilt asar test', (t, opts) => {
opts.name = 'prebuiltAsarTest'
opts.dir = util.fixtureSubdir('asar-prebuilt')
opts.prebuiltAsar = path.join(opts.dir, 'app.asar')
opts.asar = {
'unpack': '*.pac',
'unpackDir': 'dir_to_unpack'
}
opts.ignore = ['foo']
opts.prune = false
opts.derefSymlinks = false
sinon.spy(console, 'warn')

let resourcesPath
return util.packageAndEnsureResourcesPath(t, opts)
.then(generatedResourcesPath => {
util.assertWarning(t, 'WARNING: prebuiltAsar has been specified, all asar options will be ignored')
for (const incompatibleOption of ['ignore', 'prune', 'derefSymlinks']) {
util.assertWarning(t, `WARNING: prebuiltAsar and ${incompatibleOption} are incompatible, ignoring the ${incompatibleOption} option`)
}

resourcesPath = generatedResourcesPath
return util.assertFile(t, path.join(resourcesPath, 'app.asar'), 'app.asar should exist under the resources subdirectory when opts.prebuiltAsar points to a prebuilt asar')
}).then(() => util.assertFilesEqual(t, opts.prebuiltAsar, path.join(resourcesPath, 'app.asar'), 'app.asar should equal the prebuilt asar'))
.then(() => util.assertPathNotExists(t, path.join(resourcesPath, 'app'), 'app subdirectory should NOT exist when app.asar is built'))
})

function testFailedPrebuiltAsar (name, extraOpts, errorRegex) {
const dir = util.fixtureSubdir('asar-prebuilt')
util.testSinglePlatform(`prebuilt asar: fail on ${name}`, util.invalidOptionTest(Object.assign({
name: 'prebuiltAsarFailingTest',
dir: dir,
prebuiltAsar: path.join(dir, 'app.asar')
}, extraOpts), errorRegex))
}

function testIncompatibleOptionWithPrebuiltAsar (extraOpts) {
testFailedPrebuiltAsar(`specifying prebuiltAsar and ${Object.keys(extraOpts).join(',')}`, extraOpts, /is incompatible with prebuiltAsar/)
}

testFailedPrebuiltAsar('prebuiltAsar set to directory', { prebuiltAsar: util.fixtureSubdir('asar-prebuilt') }, /must be an asar file/)
testIncompatibleOptionWithPrebuiltAsar({ afterCopy: [] })
testIncompatibleOptionWithPrebuiltAsar({ afterPrune: [] })
Binary file added test/fixtures/asar-prebuilt/app.asar
Binary file not shown.
9 changes: 9 additions & 0 deletions test/fixtures/asar-prebuilt/package.json
@@ -0,0 +1,9 @@
{
"name": "PrebuiltAsar",
"version": "0.0.1",
"description": "A prebuilt asar app",
"main": "main.js",
"devDependencies": {
"electron": "1.3.1"
}
}
2 changes: 2 additions & 0 deletions usage.txt
Expand Up @@ -59,6 +59,8 @@ overwrite if output directory for a platform already exists, replaces i
skipping it
platform all, or one or more of: darwin, linux, mas, win32 (comma-delimited if multiple).
Defaults to the host platform
prebuilt-asar path to a prebuilt asar file (asar, ignore, no-prune, and no-deref-symlinks
options are incompatible with this option and will be ignored)
quiet Do not print informational or warning messages
tmpdir temp directory. Defaults to system temp directory, use --no-tmpdir to disable
use of a temporary directory.
Expand Down