Skip to content

Commit f67b7d2

Browse files
committed
fix: do not fail if cannot rebuild optional dep
Closes #1075
1 parent 811be55 commit f67b7d2

File tree

8 files changed

+209
-59
lines changed

8 files changed

+209
-59
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
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.
33

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

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

23+
[Yarn](http://yarnpkg.com/) is recommended instead of npm.
24+
2325
## Configuration
2426

2527
See [options](https://github.com/electron-userland/electron-builder/wiki/Options) for a full reference but consider following the simple guide outlined below first.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"docker-images": "docker/build.sh",
1717
"test-deps-mac": "brew install rpm dpkg mono lzip gnu-tar graphicsmagick xz && brew install wine --without-x11",
1818
"postinstall": "lerna bootstrap",
19-
"update-deps": "lerna exec -- npm-check-updates --reject 'electron-builder-http,electron-builder-util' -a",
19+
"update-deps": "lerna exec -- npm-check-updates --reject 'electron-builder-http,electron-builder-util,electron-builder-core' -a",
2020
"lerna-publish": "node test/out/helpers/setVersions.js p && lerna publish --skip-npm --skip-git && node test/out/helpers/setVersions.js",
2121
"npm-publish": "yarn compile && ./packages/npm-publish.sh && conventional-changelog -p angular -i CHANGELOG.md -s"
2222
},
@@ -52,7 +52,7 @@
5252
"test/out"
5353
],
5454
"transform": {
55-
"node_modules[\\/]{1}electron-builder-[a-z]+[\\/]{1}.+\\.js$": "<rootDir>/test/babel-jest.js",
55+
"node_modules[\\/]{1}electron-builder-[a-z]+[\\/]{1}(?!.+[\\/]{1}node_modules[\\/]{1}.+).+\\.js$": "<rootDir>/test/babel-jest.js",
5656
"^(?!.+[\\/]{1}node_modules[\\/]{1}.+).+\\.js$": "<rootDir>/test/babel-jest.js"
5757
},
5858
"transformIgnorePatterns": [],

packages/electron-builder-util/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"bluebird-lst-c": "^1.0.5",
1818
"chalk": "^1.1.3",
1919
"debug": "2.6.0",
20-
"node-emoji": "^1.4.3",
20+
"node-emoji": "^1.5.0",
2121
"pretty-ms": "^2.1.0",
2222
"cli-cursor": "^1.0.2",
2323
"ansi-escapes": "^1.4.0",

packages/electron-builder/package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
"chalk": "^1.1.3",
5050
"chromium-pickle-js": "^0.2.0",
5151
"cuint": "^0.2.2",
52+
"electron-builder-core": "0.0.0-semantic-release",
53+
"electron-builder-http": "0.0.0-semantic-release",
54+
"electron-builder-util": "0.0.0-semantic-release",
5255
"electron-download-tf": "3.1.0",
5356
"electron-macos-sign": "~1.4.0",
5457
"fs-extra-p": "^3.0.3",
@@ -63,16 +66,12 @@
6366
"parse-color": "^1.0.0",
6467
"plist": "^2.0.1",
6568
"progress": "^1.1.8",
66-
"read-installed": "^4.0.3",
6769
"sanitize-filename": "^1.6.1",
6870
"semver": "^5.3.0",
6971
"tunnel-agent": "^0.4.3",
7072
"update-notifier": "^1.0.3",
7173
"uuid-1345": "^0.99.6",
72-
"yargs": "^6.6.0",
73-
"electron-builder-http": "0.0.0-semantic-release",
74-
"electron-builder-util": "0.0.0-semantic-release",
75-
"electron-builder-core": "0.0.0-semantic-release"
74+
"yargs": "^6.6.0"
7675
},
7776
"typings": "./out/electron-builder.d.ts",
7877
"publishConfig": {

packages/electron-builder/src/platformPackager.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import { FileMatchOptions, FileMatcher, FilePattern, deprecatedUserIgnoreFilter
1515
import { BuildOptions } from "./builder"
1616
import { PublishConfiguration } from "electron-builder-http/out/publishOptions"
1717
import { getRepositoryInfo } from "./repositoryInfo"
18-
import { dependencies } from "./yarn"
1918
import { deepAssign } from "electron-builder-util/out/deepAssign"
2019
import { statOrNull, unlinkIfExists, copyDir } from "electron-builder-util/out/fs"
2120
import EventEmitter = NodeJS.EventEmitter
2221
import { Arch, Target, getArchSuffix, Platform } from "electron-builder-core"
2322
import { getResolvedPublishConfig } from "./publish/publisher"
23+
import { readInstalled } from "./readInstalled"
2424

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

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

643643
return asArray<PublishConfiguration | string>(publishers)
644644
.map(it => typeof it === "string" ? {provider: <any>it} : it)
645+
}
646+
647+
async function dependencies(dir: string, result: Set<string>): Promise<void> {
648+
const pathToDep = await readInstalled(dir)
649+
for (const dep of pathToDep.values()) {
650+
if (dep.extraneous) {
651+
result.add(dep.path)
652+
}
653+
}
645654
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import BluebirdPromise from "bluebird-lst-c"
2+
import * as path from "path"
3+
import { readJson, lstat, realpath, readdir } from "fs-extra-p"
4+
5+
export interface Dependency {
6+
name: string
7+
path: string
8+
extraneous: boolean
9+
optional: boolean
10+
11+
dependencies: { [name: string]: Dependency }
12+
}
13+
14+
export async function readInstalled(folder: string): Promise<Map<string, Dependency>> {
15+
const opts = {
16+
depth: Infinity,
17+
dev: false,
18+
}
19+
20+
const findUnmetSeen = new Set<any>()
21+
const pathToDep = new Map<string, Dependency>()
22+
const obj = await _readInstalled(folder, null, null, 0, opts, pathToDep, findUnmetSeen)
23+
24+
unmarkExtraneous(obj, opts.dev, true)
25+
return pathToDep
26+
}
27+
28+
async function _readInstalled(folder: string, parent: any | null, name: string | null, depth: number, opts: any, realpathSeen: Map<string, Dependency>, findUnmetSeen: Set<any>): Promise<any> {
29+
const realDir = await realpath(folder)
30+
31+
const processed = realpathSeen.get(realDir)
32+
if (processed != null) {
33+
return processed
34+
}
35+
36+
const obj = await readJson(path.resolve(folder, "package.json"))
37+
obj.realPath = realDir
38+
obj.path = obj.path || folder
39+
//noinspection ES6MissingAwait
40+
if ((await lstat(folder)).isSymbolicLink()) {
41+
obj.link = realDir
42+
}
43+
44+
obj.realName = name || obj.name
45+
obj.dependencyNames = obj.dependencies == null ? null : new Set(Object.keys(obj.dependencies))
46+
47+
// Mark as extraneous at this point.
48+
// This will be un-marked in unmarkExtraneous, where we mark as not-extraneous everything that is required in some way from the root object.
49+
obj.extraneous = true
50+
obj.optional = true
51+
52+
if (parent != null && obj.link == null) {
53+
obj.parent = parent
54+
}
55+
56+
realpathSeen.set(realDir, obj)
57+
58+
if (depth > opts.depth) {
59+
return obj
60+
}
61+
62+
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})
63+
if (obj.dependencies != null) {
64+
for (const dep of deps) {
65+
obj.dependencies[dep.realName] = dep
66+
}
67+
68+
// any strings in the obj.dependencies are unmet deps. However, if it's optional, then that's fine, so just delete it.
69+
if (obj.optionalDependencies != null) {
70+
for (const dep of Object.keys(obj.optionalDependencies)) {
71+
if (typeof obj.dependencies[dep] === "string") {
72+
delete obj.dependencies[dep]
73+
}
74+
}
75+
}
76+
}
77+
78+
return obj
79+
}
80+
81+
function unmark(deps: Iterable<string>, obj: any, dev: boolean, unsetOptional: boolean) {
82+
for (const name of deps) {
83+
const dep = findDep(obj, name)
84+
if (dep != null) {
85+
if (unsetOptional) {
86+
dep.optional = false
87+
}
88+
if (dep.extraneous) {
89+
unmarkExtraneous(dep, dev, false)
90+
}
91+
}
92+
}
93+
}
94+
95+
function unmarkExtraneous(obj: any, dev: boolean, isRoot: boolean) {
96+
// Mark all non-required deps as extraneous.
97+
// start from the root object and mark as non-extraneous all modules
98+
// that haven't been previously flagged as extraneous then propagate to all their dependencies
99+
100+
obj.extraneous = false
101+
102+
if (obj.dependencyNames != null) {
103+
unmark(obj.dependencyNames, obj, dev, true)
104+
}
105+
106+
if (dev && obj.devDependencies != null && (isRoot || obj.link)) {
107+
unmark(Object.keys(obj.devDependencies), obj, dev, true)
108+
}
109+
110+
if (obj.peerDependencies != null) {
111+
unmark(Object.keys(obj.peerDependencies), obj, dev, true)
112+
}
113+
114+
if (obj.optionalDependencies != null) {
115+
unmark(Object.keys(obj.optionalDependencies), obj, dev, false)
116+
}
117+
}
118+
119+
// find the one that will actually be loaded by require() so we can make sure it's valid
120+
function findDep(obj: any, name: string) {
121+
let r = obj
122+
let found = null
123+
while (r != null && found == null) {
124+
// if r is a valid choice, then use that.
125+
// kinda weird if a pkg depends on itself, but after the first iteration of this loop, it indicates a dep cycle.
126+
const dependency = r.dependencies == null ? null : r.dependencies[name]
127+
if (typeof dependency === "object") {
128+
found = dependency
129+
}
130+
if (found == null && r.realName === name) {
131+
found = r
132+
}
133+
r = r.link ? null : r.parent
134+
}
135+
return found
136+
}
137+
138+
async function readScopedDir(dir: string) {
139+
let files: Array<string>
140+
try {
141+
files = (await readdir(dir)).filter(it => !it.startsWith("."))
142+
}
143+
catch (e) {
144+
// error indicates that nothing is installed here
145+
return []
146+
}
147+
148+
files.sort()
149+
150+
const scopes = files.filter(it => it.startsWith("@"))
151+
if (scopes.length === 0) {
152+
return files
153+
}
154+
155+
const result = files.filter(it => !it.startsWith("@"))
156+
const scopeFileList = await BluebirdPromise.map(scopes, it => readdir(path.join(dir, it)))
157+
for (let i = 0; i < scopes.length; i++) {
158+
for (const file of scopeFileList[i]) {
159+
if (!file.startsWith(".")) {
160+
result.push(`${scopes[i]}/${file}`)
161+
}
162+
}
163+
}
164+
165+
result.sort()
166+
return result
167+
}

packages/electron-builder/src/yarn.ts

Lines changed: 18 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import BluebirdPromise from "bluebird-lst-c"
22
import * as path from "path"
3-
import { log } from "electron-builder-util/out/log"
3+
import { log, warn} from "electron-builder-util/out/log"
44
import { homedir } from "os"
55
import { spawn, asArray } from "electron-builder-util"
66
import { BuildMetadata } from "./metadata"
77
import { exists } from "electron-builder-util/out/fs"
8+
import { readInstalled } from "./readInstalled"
89

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

62-
let readInstalled: any = null
63-
export function dependencies(dir: string, extraneousOnly: boolean, result: Set<string>): Promise<Array<string>> {
64-
if (readInstalled == null) {
65-
readInstalled = BluebirdPromise.promisify(require("read-installed"))
66-
}
67-
return readInstalled(dir)
68-
.then((it: any) => flatDependencies(it, result, new Set(), extraneousOnly))
69-
}
70-
71-
function flatDependencies(data: any, result: Set<string>, seen: Set<string>, extraneousOnly: boolean): void {
72-
if (data.dependencies == null) {
73-
return
74-
}
75-
76-
const queue: Array<any> = [data.dependencies]
77-
while (queue.length > 0) {
78-
const deps = queue.pop()
79-
for (const name of Object.keys(deps)) {
80-
const dep = deps[name]
81-
if (typeof dep !== "object" || (!extraneousOnly && dep.extraneous) || seen.has(dep)) {
82-
continue
83-
}
84-
85-
seen.add(dep)
86-
87-
if (extraneousOnly === dep.extraneous) {
88-
result.add(dep.path)
89-
}
90-
else {
91-
const childDeps = dep.dependencies
92-
if (childDeps != null) {
93-
queue.push(childDeps)
94-
}
95-
}
96-
}
97-
}
98-
}
99-
10063
function getPackageToolPath() {
10164
if (process.env.FORCE_YARN === "true") {
10265
return process.platform === "win32" ? "yarn.cmd" : "yarn"
@@ -107,14 +70,12 @@ function getPackageToolPath() {
10770
}
10871

10972
function isYarnPath(execPath: string | null) {
110-
return execPath != null && path.basename(execPath).startsWith("yarn")
73+
return process.env.FORCE_YARN === "true" || (execPath != null && path.basename(execPath).startsWith("yarn"))
11174
}
11275

11376
export async function rebuild(appDir: string, electronVersion: string, platform: string = process.platform, arch: string = process.arch, additionalArgs: Array<string>, buildFromSource: boolean) {
114-
const deps = new Set<string>()
115-
await dependencies(appDir, false, deps)
116-
const nativeDeps = await BluebirdPromise.filter(deps, it => exists(path.join(it, "binding.gyp")), {concurrency: 8})
117-
77+
const pathToDep = await readInstalled(appDir)
78+
const nativeDeps = await BluebirdPromise.filter(pathToDep.values(), it => it.extraneous ? false : exists(path.join(it.path, "binding.gyp")), {concurrency: 8})
11879
if (nativeDeps.length === 0) {
11980
log(`No native production dependencies`)
12081
return
@@ -137,12 +98,23 @@ export async function rebuild(appDir: string, electronVersion: string, platform:
13798
if (isYarn) {
13899
execArgs.push("run", "install", "--")
139100
execArgs.push(...additionalArgs)
140-
await BluebirdPromise.each(nativeDeps, it => spawn(execPath, execArgs, {cwd: it, env: env}))
101+
await BluebirdPromise.each(nativeDeps, dep => {
102+
log(`Rebuilding native dependency ${dep.name}`)
103+
return spawn(execPath, execArgs, {cwd: dep.path, env: env})
104+
.catch(error => {
105+
if (dep.optional) {
106+
warn(`Cannot build optional native dep ${dep.name}`)
107+
}
108+
else {
109+
throw error
110+
}
111+
})
112+
})
141113
}
142114
else {
143115
execArgs.push("rebuild")
144116
execArgs.push(...additionalArgs)
145-
execArgs.push(...nativeDeps.map(it => path.basename(it)))
117+
execArgs.push(...nativeDeps.map(it => it.name))
146118
await spawn(execPath, execArgs, {cwd: appDir, env: env})
147119
}
148120
}

test/babel-jest.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ function createTransformer(options) {
6363

6464
let plugins = options.plugins || []
6565

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

0 commit comments

Comments
 (0)