Skip to content

Commit daaca09

Browse files
authored
Merge branch 'master' into fix/multi-space-string-scraping
2 parents 91a0434 + 1a6ea01 commit daaca09

30 files changed

+740
-125
lines changed

.changeset/famous-berries-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
fix(nsis): implement custom function to handle /D parameter with spaces

.changeset/flat-trees-return.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
feat(nsis): terminate only processes running in installation folder

.changeset/funny-bottles-occur.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"builder-util-runtime": patch
3+
"electron-updater": patch
4+
---
5+
6+
feat(electron-updater): add GitLab provider support

.changeset/heavy-hats-wonder.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"app-builder-lib": patch
3+
"builder-util": patch
4+
"builder-util-runtime": patch
5+
"dmg-builder": patch
6+
"electron-updater": patch
7+
---
8+
9+
fix: remove `shell: true` from node_modules collector so as to prevent shell console logging from malforming the json output

packages/app-builder-lib/src/codeSign/macCodeSign.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,11 @@ async function importCerts(keychainFile: string, paths: Array<string>, keyPasswo
214214
}
215215

216216
export async function sign(opts: SignOptions): Promise<void> {
217-
return retry(() => signAsync(opts), 3, 5000, 5000)
217+
return retry(() => signAsync(opts), {
218+
retries: 3,
219+
interval: 5000,
220+
backoff: 5000,
221+
})
218222
}
219223

220224
export let findIdentityRawResult: Promise<Array<string>> | null = null

packages/app-builder-lib/src/codeSign/windowsCodeSign.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,22 @@ export async function signWindows(options: WindowsSignOptions, packager: WinPack
2121
}
2222

2323
function signWithRetry(signer: () => Promise<boolean>): Promise<boolean> {
24-
return retry(signer, 3, 1000, 1000, 0, (e: any) => {
25-
const message = e.message
26-
if (
27-
// https://github.com/electron-userland/electron-builder/issues/1414
28-
message?.includes("Couldn't resolve host name") ||
29-
// https://github.com/electron-userland/electron-builder/issues/8615
30-
message?.includes("being used by another process.")
31-
) {
32-
log.warn({ error: message }, "attempt to sign failed, another attempt will be made")
33-
return true
34-
}
35-
return false
24+
return retry(signer, {
25+
retries: 3,
26+
interval: 1000,
27+
backoff: 1000,
28+
shouldRetry: (e: any) => {
29+
const message = e.message
30+
if (
31+
// https://github.com/electron-userland/electron-builder/issues/1414
32+
message?.includes("Couldn't resolve host name") ||
33+
// https://github.com/electron-userland/electron-builder/issues/8615
34+
message?.includes("being used by another process.")
35+
) {
36+
log.warn({ error: message }, "attempt to sign failed, another attempt will be made")
37+
return true
38+
}
39+
return false
40+
},
3641
})
3742
}

packages/app-builder-lib/src/codeSign/windowsSignToolManager.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -449,13 +449,11 @@ export class WindowsSignToolManager implements SignManager {
449449
}
450450
}
451451

452-
await retry(
453-
() => vm.exec(tool, args, { timeout, env }),
454-
2,
455-
15000,
456-
10000,
457-
0,
458-
(e: any) => {
452+
await retry(() => vm.exec(tool, args, { timeout, env }), {
453+
retries: 2,
454+
interval: 15000,
455+
backoff: 10000,
456+
shouldRetry: (e: any) => {
459457
if (
460458
e.message.includes("The file is being used by another process") ||
461459
e.message.includes("The specified timestamp server either could not be reached") ||
@@ -465,8 +463,8 @@ export class WindowsSignToolManager implements SignManager {
465463
return true
466464
}
467465
return false
468-
}
469-
)
466+
},
467+
})
470468
}
471469
}
472470

packages/app-builder-lib/src/node-module-collector/index.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,27 @@ import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector"
33
import { YarnNodeModulesCollector } from "./yarnNodeModulesCollector"
44
import { detectPackageManager, PM, getPackageManagerCommand } from "./packageManager"
55
import { NodeModuleInfo } from "./types"
6-
import { exec } from "builder-util"
6+
import { TmpDir } from "temp-file"
77

8-
async function isPnpmProjectHoisted(rootDir: string) {
9-
const command = getPackageManagerCommand(PM.PNPM)
10-
const config = await exec(command, ["config", "list"], { cwd: rootDir, shell: true })
11-
const lines = Object.fromEntries(config.split("\n").map(line => line.split("=").map(s => s.trim())))
12-
return lines["node-linker"] === "hoisted"
13-
}
14-
15-
export async function getCollectorByPackageManager(rootDir: string) {
8+
export async function getCollectorByPackageManager(rootDir: string, tempDirManager: TmpDir) {
169
const manager: PM = detectPackageManager(rootDir)
1710
switch (manager) {
1811
case PM.PNPM:
19-
if (await isPnpmProjectHoisted(rootDir)) {
20-
return new NpmNodeModulesCollector(rootDir)
12+
if (await PnpmNodeModulesCollector.isPnpmProjectHoisted(rootDir)) {
13+
return new NpmNodeModulesCollector(rootDir, tempDirManager)
2114
}
22-
return new PnpmNodeModulesCollector(rootDir)
15+
return new PnpmNodeModulesCollector(rootDir, tempDirManager)
2316
case PM.NPM:
24-
return new NpmNodeModulesCollector(rootDir)
17+
return new NpmNodeModulesCollector(rootDir, tempDirManager)
2518
case PM.YARN:
26-
return new YarnNodeModulesCollector(rootDir)
19+
return new YarnNodeModulesCollector(rootDir, tempDirManager)
2720
default:
28-
return new NpmNodeModulesCollector(rootDir)
21+
return new NpmNodeModulesCollector(rootDir, tempDirManager)
2922
}
3023
}
3124

32-
export async function getNodeModules(rootDir: string): Promise<NodeModuleInfo[]> {
33-
const collector = await getCollectorByPackageManager(rootDir)
25+
export async function getNodeModules(rootDir: string, tempDirManager: TmpDir): Promise<NodeModuleInfo[]> {
26+
const collector = await getCollectorByPackageManager(rootDir, tempDirManager)
3427
return collector.getNodeModules()
3528
}
3629

packages/app-builder-lib/src/node-module-collector/nodeModulesCollector.ts

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { hoist, type HoisterTree, type HoisterResult } from "./hoist"
22
import * as path from "path"
3-
import * as fs from "fs"
3+
import * as fs from "fs-extra"
44
import type { NodeModuleInfo, DependencyGraph, Dependency } from "./types"
5-
import { exec, log } from "builder-util"
5+
import { exists, log, retry, TmpDir } from "builder-util"
66
import { getPackageManagerCommand, PM } from "./packageManager"
7+
import { exec, spawn } from "child_process"
8+
import { promisify } from "util"
9+
import { createWriteStream } from "fs"
10+
11+
const execAsync = promisify(exec)
712

813
export abstract class NodeModulesCollector<T extends Dependency<T, OptionalsType>, OptionalsType> {
914
private nodeModules: NodeModuleInfo[] = []
1015
protected allDependencies: Map<string, T> = new Map()
1116
protected productionGraph: DependencyGraph = {}
1217

13-
constructor(private readonly rootDir: string) {}
18+
constructor(
19+
private readonly rootDir: string,
20+
private readonly tempDirManager: TmpDir
21+
) {}
1422

1523
public async getNodeModules(): Promise<NodeModuleInfo[]> {
1624
const tree: T = await this.getDependenciesTree()
@@ -37,11 +45,43 @@ export abstract class NodeModulesCollector<T extends Dependency<T, OptionalsType
3745
protected async getDependenciesTree(): Promise<T> {
3846
const command = getPackageManagerCommand(this.installOptions.manager)
3947
const args = this.getArgs()
40-
const dependencies = await exec(command, args, {
41-
cwd: this.rootDir,
42-
shell: true,
48+
49+
const tempOutputFile = await this.tempDirManager.getTempFile({
50+
prefix: path.basename(command, path.extname(command)),
51+
suffix: "output.json",
4352
})
44-
return this.parseDependenciesTree(dependencies)
53+
54+
return retry(
55+
async () => {
56+
await this.streamCollectorCommandToJsonFile(command, args, this.rootDir, tempOutputFile)
57+
const dependencies = await fs.readFile(tempOutputFile, { encoding: "utf8" })
58+
try {
59+
return this.parseDependenciesTree(dependencies)
60+
} catch (error: any) {
61+
log.debug({ message: error.message || error.stack, shellOutput: dependencies }, "error parsing dependencies tree")
62+
throw new Error(`Failed to parse dependencies tree: ${error.message || error.stack}. Use DEBUG=electron-builder env var to see the dependency query output.`)
63+
}
64+
},
65+
{
66+
retries: 2,
67+
interval: 2000,
68+
backoff: 2000,
69+
shouldRetry: async (error: any) => {
70+
if (!(await exists(tempOutputFile))) {
71+
log.error({ error: error.message || error.stack, tempOutputFile }, "error getting dependencies tree, unable to find output; retrying")
72+
return true
73+
}
74+
const dependencies = await fs.readFile(tempOutputFile, { encoding: "utf8" })
75+
if (dependencies.trim().length === 0 || error.message?.includes("Unexpected end of JSON input")) {
76+
// If the output file is empty or contains invalid JSON, we retry
77+
// This can happen if the command fails or if the output is not as expected
78+
log.error({ error: error.message || error.stack, tempOutputFile }, "dependency tree output file is empty, retrying")
79+
return true
80+
}
81+
return false
82+
},
83+
}
84+
)
4585
}
4686

4787
protected resolvePath(filePath: string): string {
@@ -117,4 +157,60 @@ export abstract class NodeModulesCollector<T extends Dependency<T, OptionalsType
117157
}
118158
result.sort((a, b) => a.name.localeCompare(b.name))
119159
}
160+
161+
static async safeExec(command: string, args: string[], cwd: string): Promise<string> {
162+
const payload = await execAsync([`"${command}"`, ...args].join(" "), { cwd, maxBuffer: 100 * 1024 * 1024 }) // 100MB buffer LOL, some projects can have extremely large dependency trees
163+
return payload.stdout.trim()
164+
}
165+
166+
async streamCollectorCommandToJsonFile(command: string, args: string[], cwd: string, tempOutputFile: string) {
167+
const execName = path.basename(command, path.extname(command))
168+
const isWindowsScriptFile = process.platform === "win32" && path.extname(command).toLowerCase() === ".cmd"
169+
if (isWindowsScriptFile) {
170+
// If the command is a Windows script file (.cmd), we need to wrap it in a .bat file to ensure it runs correctly with cmd.exe
171+
// This is necessary because .cmd files are not directly executable in the same way as .bat files.
172+
// We create a temporary .bat file that calls the .cmd file with the provided arguments. The .bat file will be executed by cmd.exe.
173+
const tempBatFile = await this.tempDirManager.getTempFile({
174+
prefix: execName,
175+
suffix: ".bat",
176+
})
177+
const batScript = `@echo off\r\n"${command}" %*\r\n` // <-- CRLF required for .bat
178+
await fs.writeFile(tempBatFile, batScript, { encoding: "utf8" })
179+
command = "cmd.exe"
180+
args = ["/c", tempBatFile, ...args]
181+
}
182+
183+
await new Promise<void>((resolve, reject) => {
184+
const outStream = createWriteStream(tempOutputFile)
185+
186+
const child = spawn(command, args, {
187+
cwd,
188+
shell: false, // required to prevent console logs polution from shell profile loading when `true`
189+
})
190+
191+
let stderr = ""
192+
child.stdout.pipe(outStream)
193+
child.stderr.on("data", chunk => {
194+
stderr += chunk.toString()
195+
})
196+
child.on("error", err => {
197+
reject(new Error(`Spawn failed: ${err.message}`))
198+
})
199+
200+
child.on("close", code => {
201+
outStream.close()
202+
// https://github.com/npm/npm/issues/17624
203+
if (code === 1 && execName.toLowerCase() === "npm" && args.includes("list")) {
204+
log.debug({ code, stderr }, "`npm list` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details.")
205+
// This is a known issue with npm list command, it can return code 1 even when the command is "technically" successful
206+
resolve()
207+
return
208+
}
209+
if (code !== 0) {
210+
return reject(new Error(`Process exited with code ${code}:\n${stderr}`))
211+
}
212+
resolve()
213+
})
214+
})
215+
}
120216
}

packages/app-builder-lib/src/node-module-collector/npmNodeModulesCollector.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import { PM } from "./packageManager"
33
import { NpmDependency } from "./types"
44

55
export class NpmNodeModulesCollector extends NodeModulesCollector<NpmDependency, string> {
6-
constructor(rootDir: string) {
7-
super(rootDir)
8-
}
9-
106
public readonly installOptions = { manager: PM.NPM, lockfile: "package-lock.json" }
117

128
protected getArgs(): string[] {

0 commit comments

Comments
 (0)