Skip to content

Commit

Permalink
fix: do not fail if cannot rebuild optional dep
Browse files Browse the repository at this point in the history
Closes #1075
  • Loading branch information
develar committed Jan 9, 2017
1 parent 811be55 commit f67b7d2
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 59 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
A complete solution to package and build a ready for distribution Electron app for macOS, Windows and Linux with “auto update” support out of the box.

* NPM packages management:
* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation.
* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation (including [Yarn](http://yarnpkg.com/) support).
* Development dependencies are never included. You don't need to ignore them explicitly.
* [Code Signing](https://github.com/electron-userland/electron-builder/wiki/Code-Signing) on a CI server or development machine.
* [Auto Update](#auto-update) ready application packaging.
Expand All @@ -20,6 +20,8 @@ _Note: Platform specific `7zip-bin-*` packages are `optionalDependencies`, which

Real project example — [onshape-desktop-shell](https://github.com/develar/onshape-desktop-shell).

[Yarn](http://yarnpkg.com/) is recommended instead of npm.

## Configuration

See [options](https://github.com/electron-userland/electron-builder/wiki/Options) for a full reference but consider following the simple guide outlined below first.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"docker-images": "docker/build.sh",
"test-deps-mac": "brew install rpm dpkg mono lzip gnu-tar graphicsmagick xz && brew install wine --without-x11",
"postinstall": "lerna bootstrap",
"update-deps": "lerna exec -- npm-check-updates --reject 'electron-builder-http,electron-builder-util' -a",
"update-deps": "lerna exec -- npm-check-updates --reject 'electron-builder-http,electron-builder-util,electron-builder-core' -a",
"lerna-publish": "node test/out/helpers/setVersions.js p && lerna publish --skip-npm --skip-git && node test/out/helpers/setVersions.js",
"npm-publish": "yarn compile && ./packages/npm-publish.sh && conventional-changelog -p angular -i CHANGELOG.md -s"
},
Expand Down Expand Up @@ -52,7 +52,7 @@
"test/out"
],
"transform": {
"node_modules[\\/]{1}electron-builder-[a-z]+[\\/]{1}.+\\.js$": "<rootDir>/test/babel-jest.js",
"node_modules[\\/]{1}electron-builder-[a-z]+[\\/]{1}(?!.+[\\/]{1}node_modules[\\/]{1}.+).+\\.js$": "<rootDir>/test/babel-jest.js",
"^(?!.+[\\/]{1}node_modules[\\/]{1}.+).+\\.js$": "<rootDir>/test/babel-jest.js"
},
"transformIgnorePatterns": [],
Expand Down
2 changes: 1 addition & 1 deletion packages/electron-builder-util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"bluebird-lst-c": "^1.0.5",
"chalk": "^1.1.3",
"debug": "2.6.0",
"node-emoji": "^1.4.3",
"node-emoji": "^1.5.0",
"pretty-ms": "^2.1.0",
"cli-cursor": "^1.0.2",
"ansi-escapes": "^1.4.0",
Expand Down
9 changes: 4 additions & 5 deletions packages/electron-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
"chalk": "^1.1.3",
"chromium-pickle-js": "^0.2.0",
"cuint": "^0.2.2",
"electron-builder-core": "0.0.0-semantic-release",
"electron-builder-http": "0.0.0-semantic-release",
"electron-builder-util": "0.0.0-semantic-release",
"electron-download-tf": "3.1.0",
"electron-macos-sign": "~1.4.0",
"fs-extra-p": "^3.0.3",
Expand All @@ -63,16 +66,12 @@
"parse-color": "^1.0.0",
"plist": "^2.0.1",
"progress": "^1.1.8",
"read-installed": "^4.0.3",
"sanitize-filename": "^1.6.1",
"semver": "^5.3.0",
"tunnel-agent": "^0.4.3",
"update-notifier": "^1.0.3",
"uuid-1345": "^0.99.6",
"yargs": "^6.6.0",
"electron-builder-http": "0.0.0-semantic-release",
"electron-builder-util": "0.0.0-semantic-release",
"electron-builder-core": "0.0.0-semantic-release"
"yargs": "^6.6.0"
},
"typings": "./out/electron-builder.d.ts",
"publishConfig": {
Expand Down
13 changes: 11 additions & 2 deletions packages/electron-builder/src/platformPackager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import { FileMatchOptions, FileMatcher, FilePattern, deprecatedUserIgnoreFilter
import { BuildOptions } from "./builder"
import { PublishConfiguration } from "electron-builder-http/out/publishOptions"
import { getRepositoryInfo } from "./repositoryInfo"
import { dependencies } from "./yarn"
import { deepAssign } from "electron-builder-util/out/deepAssign"
import { statOrNull, unlinkIfExists, copyDir } from "electron-builder-util/out/fs"
import EventEmitter = NodeJS.EventEmitter
import { Arch, Target, getArchSuffix, Platform } from "electron-builder-core"
import { getResolvedPublishConfig } from "./publish/publisher"
import { readInstalled } from "./readInstalled"

export interface PackagerOptions {
targets?: Map<Platform, Map<Arch, string[]>>
Expand Down Expand Up @@ -202,7 +202,7 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
const ignoreFiles = new Set([path.resolve(appDir, outDir), path.resolve(appDir, this.buildResourcesDir)])
// prune dev or not listed dependencies
await BluebirdPromise.all([
dependencies(appDir, true, ignoreFiles),
dependencies(appDir, ignoreFiles),
unpackElectron(this, appOutDir, platformName, Arch[arch], this.info.electronVersion),
])

Expand Down Expand Up @@ -642,4 +642,13 @@ export function getPublishConfigs(packager: PlatformPackager<any>, platformSpeci

return asArray<PublishConfiguration | string>(publishers)
.map(it => typeof it === "string" ? {provider: <any>it} : it)
}

async function dependencies(dir: string, result: Set<string>): Promise<void> {
const pathToDep = await readInstalled(dir)
for (const dep of pathToDep.values()) {
if (dep.extraneous) {
result.add(dep.path)
}
}
}
167 changes: 167 additions & 0 deletions packages/electron-builder/src/readInstalled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import BluebirdPromise from "bluebird-lst-c"
import * as path from "path"
import { readJson, lstat, realpath, readdir } from "fs-extra-p"

export interface Dependency {
name: string
path: string
extraneous: boolean
optional: boolean

dependencies: { [name: string]: Dependency }
}

export async function readInstalled(folder: string): Promise<Map<string, Dependency>> {
const opts = {
depth: Infinity,
dev: false,
}

const findUnmetSeen = new Set<any>()
const pathToDep = new Map<string, Dependency>()
const obj = await _readInstalled(folder, null, null, 0, opts, pathToDep, findUnmetSeen)

unmarkExtraneous(obj, opts.dev, true)
return pathToDep
}

async function _readInstalled(folder: string, parent: any | null, name: string | null, depth: number, opts: any, realpathSeen: Map<string, Dependency>, findUnmetSeen: Set<any>): Promise<any> {
const realDir = await realpath(folder)

const processed = realpathSeen.get(realDir)
if (processed != null) {
return processed
}

const obj = await readJson(path.resolve(folder, "package.json"))
obj.realPath = realDir
obj.path = obj.path || folder
//noinspection ES6MissingAwait
if ((await lstat(folder)).isSymbolicLink()) {
obj.link = realDir
}

obj.realName = name || obj.name
obj.dependencyNames = obj.dependencies == null ? null : new Set(Object.keys(obj.dependencies))

// Mark as extraneous at this point.
// This will be un-marked in unmarkExtraneous, where we mark as not-extraneous everything that is required in some way from the root object.
obj.extraneous = true
obj.optional = true

if (parent != null && obj.link == null) {
obj.parent = parent
}

realpathSeen.set(realDir, obj)

if (depth > opts.depth) {
return obj
}

const deps = await BluebirdPromise.map(await readScopedDir(path.join(folder, "node_modules")), pkg => _readInstalled(path.join(folder, "node_modules", pkg), obj, pkg, depth + 1, opts, realpathSeen, findUnmetSeen), {concurrency: 8})
if (obj.dependencies != null) {
for (const dep of deps) {
obj.dependencies[dep.realName] = dep
}

// any strings in the obj.dependencies are unmet deps. However, if it's optional, then that's fine, so just delete it.
if (obj.optionalDependencies != null) {
for (const dep of Object.keys(obj.optionalDependencies)) {
if (typeof obj.dependencies[dep] === "string") {
delete obj.dependencies[dep]
}
}
}
}

return obj
}

function unmark(deps: Iterable<string>, obj: any, dev: boolean, unsetOptional: boolean) {
for (const name of deps) {
const dep = findDep(obj, name)
if (dep != null) {
if (unsetOptional) {
dep.optional = false
}
if (dep.extraneous) {
unmarkExtraneous(dep, dev, false)
}
}
}
}

function unmarkExtraneous(obj: any, dev: boolean, isRoot: boolean) {
// Mark all non-required deps as extraneous.
// start from the root object and mark as non-extraneous all modules
// that haven't been previously flagged as extraneous then propagate to all their dependencies

obj.extraneous = false

if (obj.dependencyNames != null) {
unmark(obj.dependencyNames, obj, dev, true)
}

if (dev && obj.devDependencies != null && (isRoot || obj.link)) {
unmark(Object.keys(obj.devDependencies), obj, dev, true)
}

if (obj.peerDependencies != null) {
unmark(Object.keys(obj.peerDependencies), obj, dev, true)
}

if (obj.optionalDependencies != null) {
unmark(Object.keys(obj.optionalDependencies), obj, dev, false)
}
}

// find the one that will actually be loaded by require() so we can make sure it's valid
function findDep(obj: any, name: string) {
let r = obj
let found = null
while (r != null && found == null) {
// if r is a valid choice, then use that.
// kinda weird if a pkg depends on itself, but after the first iteration of this loop, it indicates a dep cycle.
const dependency = r.dependencies == null ? null : r.dependencies[name]
if (typeof dependency === "object") {
found = dependency
}
if (found == null && r.realName === name) {
found = r
}
r = r.link ? null : r.parent
}
return found
}

async function readScopedDir(dir: string) {
let files: Array<string>
try {
files = (await readdir(dir)).filter(it => !it.startsWith("."))
}
catch (e) {
// error indicates that nothing is installed here
return []
}

files.sort()

const scopes = files.filter(it => it.startsWith("@"))
if (scopes.length === 0) {
return files
}

const result = files.filter(it => !it.startsWith("@"))
const scopeFileList = await BluebirdPromise.map(scopes, it => readdir(path.join(dir, it)))
for (let i = 0; i < scopes.length; i++) {
for (const file of scopeFileList[i]) {
if (!file.startsWith(".")) {
result.push(`${scopes[i]}/${file}`)
}
}
}

result.sort()
return result
}
64 changes: 18 additions & 46 deletions packages/electron-builder/src/yarn.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import BluebirdPromise from "bluebird-lst-c"
import * as path from "path"
import { log } from "electron-builder-util/out/log"
import { log, warn} from "electron-builder-util/out/log"
import { homedir } from "os"
import { spawn, asArray } from "electron-builder-util"
import { BuildMetadata } from "./metadata"
import { exists } from "electron-builder-util/out/fs"
import { readInstalled } from "./readInstalled"

export async function installOrRebuild(options: BuildMetadata, appDir: string, electronVersion: string, platform: string, arch: string, forceInstall: boolean = false) {
const args = asArray(options.npmArgs)
Expand Down Expand Up @@ -59,44 +60,6 @@ function installDependencies(appDir: string, electronVersion: string, platform:
})
}

let readInstalled: any = null
export function dependencies(dir: string, extraneousOnly: boolean, result: Set<string>): Promise<Array<string>> {
if (readInstalled == null) {
readInstalled = BluebirdPromise.promisify(require("read-installed"))
}
return readInstalled(dir)
.then((it: any) => flatDependencies(it, result, new Set(), extraneousOnly))
}

function flatDependencies(data: any, result: Set<string>, seen: Set<string>, extraneousOnly: boolean): void {
if (data.dependencies == null) {
return
}

const queue: Array<any> = [data.dependencies]
while (queue.length > 0) {
const deps = queue.pop()
for (const name of Object.keys(deps)) {
const dep = deps[name]
if (typeof dep !== "object" || (!extraneousOnly && dep.extraneous) || seen.has(dep)) {
continue
}

seen.add(dep)

if (extraneousOnly === dep.extraneous) {
result.add(dep.path)
}
else {
const childDeps = dep.dependencies
if (childDeps != null) {
queue.push(childDeps)
}
}
}
}
}

function getPackageToolPath() {
if (process.env.FORCE_YARN === "true") {
return process.platform === "win32" ? "yarn.cmd" : "yarn"
Expand All @@ -107,14 +70,12 @@ function getPackageToolPath() {
}

function isYarnPath(execPath: string | null) {
return execPath != null && path.basename(execPath).startsWith("yarn")
return process.env.FORCE_YARN === "true" || (execPath != null && path.basename(execPath).startsWith("yarn"))
}

export async function rebuild(appDir: string, electronVersion: string, platform: string = process.platform, arch: string = process.arch, additionalArgs: Array<string>, buildFromSource: boolean) {
const deps = new Set<string>()
await dependencies(appDir, false, deps)
const nativeDeps = await BluebirdPromise.filter(deps, it => exists(path.join(it, "binding.gyp")), {concurrency: 8})

const pathToDep = await readInstalled(appDir)
const nativeDeps = await BluebirdPromise.filter(pathToDep.values(), it => it.extraneous ? false : exists(path.join(it.path, "binding.gyp")), {concurrency: 8})
if (nativeDeps.length === 0) {
log(`No native production dependencies`)
return
Expand All @@ -137,12 +98,23 @@ export async function rebuild(appDir: string, electronVersion: string, platform:
if (isYarn) {
execArgs.push("run", "install", "--")
execArgs.push(...additionalArgs)
await BluebirdPromise.each(nativeDeps, it => spawn(execPath, execArgs, {cwd: it, env: env}))
await BluebirdPromise.each(nativeDeps, dep => {
log(`Rebuilding native dependency ${dep.name}`)
return spawn(execPath, execArgs, {cwd: dep.path, env: env})
.catch(error => {
if (dep.optional) {
warn(`Cannot build optional native dep ${dep.name}`)
}
else {
throw error
}
})
})
}
else {
execArgs.push("rebuild")
execArgs.push(...additionalArgs)
execArgs.push(...nativeDeps.map(it => path.basename(it)))
execArgs.push(...nativeDeps.map(it => it.name))
await spawn(execPath, execArgs, {cwd: appDir, env: env})
}
}
5 changes: 3 additions & 2 deletions test/babel-jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ function createTransformer(options) {

let plugins = options.plugins || []

// inputSourceMap: JSON.parse(fs.readFileSync(filename + ".map", "utf-8"))
const finalOptions = Object.assign({}, options, {filename, plugins})
const finalOptions = Object.assign({
inputSourceMap: JSON.parse(fs.readFileSync(filename + ".map", "utf-8")),
}, options, {filename, plugins})
if (transformOptions && transformOptions.instrument) {
finalOptions.auxiliaryCommentBefore = ' istanbul ignore next '
plugins = plugins.concat(require('babel-plugin-istanbul').default);
Expand Down

0 comments on commit f67b7d2

Please sign in to comment.