Skip to content

Commit 106640d

Browse files
authored
chore(refactor): explore parallelization of builds (electron-userland#8962)
1 parent 580007d commit 106640d

File tree

23 files changed

+1330
-156
lines changed

23 files changed

+1330
-156
lines changed

.changeset/mighty-windows-wink.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"app-builder-lib": patch
3+
"builder-util": patch
4+
"dmg-builder": patch
5+
"electron-builder-squirrel-windows": patch
6+
---
7+
8+
chore(refactor): enable parallel packaging of archs and targets with `concurrency` config prop

.github/workflows/test.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ jobs:
7272
- snapTest,debTest,fpmTest,protonTest
7373
- winPackagerTest,winCodeSignTest,webInstallerTest
7474
- oneClickInstallerTest,assistedInstallerTest
75+
- concurrentBuildsTest
7576
steps:
7677
- name: Checkout code repository
7778
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -169,6 +170,7 @@ jobs:
169170
- winCodeSignTest,differentialUpdateTest,squirrelWindowsTest
170171
- appxTest,msiTest,portableTest,assistedInstallerTest,protonTest
171172
- BuildTest,oneClickInstallerTest,winPackagerTest,nsisUpdaterTest,webInstallerTest
173+
- concurrentBuildsTest
172174
steps:
173175
- name: Checkout code repository
174176
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -196,6 +198,7 @@ jobs:
196198
- oneClickInstallerTest,assistedInstallerTest
197199
- winPackagerTest,winCodeSignTest,webInstallerTest
198200
- masTest,dmgTest,filesTest,macPackagerTest,differentialUpdateTest,macArchiveTest
201+
- concurrentBuildsTest
199202
steps:
200203
- name: Checkout code repository
201204
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -206,10 +209,11 @@ jobs:
206209
cache-path: ~/Library/Caches/electron
207210
cache-key: v-23.3.10-macos-electron
208211

209-
- name: Install pwsh and wine via brew
212+
- name: Install toolset via brew
210213
run: |
211214
brew install powershell/tap/powershell
212215
brew install --cask wine-stable
216+
brew install rpm
213217
214218
- name: Test
215219
run: pnpm ci:test

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"console": "integratedTerminal",
1111
"internalConsoleOptions": "openOnFirstSessionStart",
1212
"env": {
13-
"TEST_FILES": "BuildTest"
13+
"TEST_FILES": "macPackagerTest",
14+
"UPDATE_SNAPSHOT": "false"
1415
}
1516
}
1617
]

packages/app-builder-lib/scheme.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,20 @@
428428
],
429429
"type": "object"
430430
},
431+
"Concurrency": {
432+
"additionalProperties": false,
433+
"properties": {
434+
"jobs": {
435+
"default": 1,
436+
"description": "The maximum number of concurrent jobs to run.",
437+
"type": "number"
438+
}
439+
},
440+
"required": [
441+
"jobs"
442+
],
443+
"type": "object"
444+
},
431445
"CustomNsisBinary": {
432446
"additionalProperties": false,
433447
"properties": {
@@ -6924,6 +6938,17 @@
69246938
"default": "normal",
69256939
"description": "The compression level. If you want to rapidly test build, `store` can reduce build time significantly. `maximum` doesn't lead to noticeable size difference, but increase build time."
69266940
},
6941+
"concurrency": {
6942+
"anyOf": [
6943+
{
6944+
"$ref": "#/definitions/Concurrency"
6945+
},
6946+
{
6947+
"type": "null"
6948+
}
6949+
],
6950+
"description": "[Experimental] Configuration for concurrent builds."
6951+
},
69276952
"copyright": {
69286953
"default": "Copyright © year ${author}",
69296954
"description": "The human-readable copyright line for the app.",

packages/app-builder-lib/src/configuration.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,13 @@ export interface CommonConfiguration {
187187
* Ref: https://github.com/electron/fuses
188188
*/
189189
readonly electronFuses?: FuseOptionsV1 | null
190+
191+
/**
192+
* [Experimental] Configuration for concurrent builds.
193+
*/
194+
readonly concurrency?: Concurrency | null
190195
}
196+
191197
export interface Configuration extends CommonConfiguration, PlatformSpecificBuildOptions, Hooks {
192198
/**
193199
* Whether to use [electron-compile](http://github.com/electron/electron-compile) to compile app. Defaults to `true` if `electron-compile` in the dependencies. And `false` if in the `devDependencies` or doesn't specified.
@@ -439,3 +445,11 @@ export interface FuseOptionsV1 {
439445
*/
440446
resetAdHocDarwinSignature?: boolean
441447
}
448+
449+
export interface Concurrency {
450+
/**
451+
* The maximum number of concurrent jobs to run.
452+
* @default 1
453+
*/
454+
jobs: number
455+
}

packages/app-builder-lib/src/core.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Arch, archFromString, ArchType } from "builder-util"
2-
import { AllPublishOptions, Nullish } from "builder-util-runtime"
1+
import { Arch, archFromString, ArchType, AsyncTaskManager } from "builder-util"
2+
import { AllPublishOptions, CancellationToken, Nullish } from "builder-util-runtime"
33

44
// https://github.com/YousefED/typescript-json-schema/issues/80
55
export type Publish = AllPublishOptions | Array<AllPublishOptions> | null
@@ -75,6 +75,9 @@ export abstract class Target {
7575
abstract readonly outDir: string
7676
abstract readonly options: TargetSpecificOptions | Nullish
7777

78+
// use only for tasks that cannot be executed in parallel (such as signing on windows and hdiutil on macOS due to file locking)
79+
readonly buildQueueManager = new AsyncTaskManager(new CancellationToken())
80+
7881
protected constructor(
7982
readonly name: string,
8083
readonly isAsyncSupported: boolean = true
@@ -86,8 +89,8 @@ export abstract class Target {
8689

8790
abstract build(appOutDir: string, arch: Arch): Promise<any>
8891

89-
finishBuild(): Promise<any> {
90-
return Promise.resolve()
92+
async finishBuild(): Promise<any> {
93+
await this.buildQueueManager.awaitTasks()
9194
}
9295
}
9396

packages/app-builder-lib/src/macPackager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,13 @@ export class MacPackager extends PlatformPackager<MacConfiguration> {
357357
const artifactName = this.expandArtifactNamePattern(masOptions, "pkg", arch)
358358
const artifactPath = path.join(outDir!, artifactName)
359359
await this.doFlat(appPath, artifactPath, masInstallerIdentity, keychainFile)
360-
await this.dispatchArtifactCreated(artifactPath, null, Arch.x64, this.computeSafeArtifactName(artifactName, "pkg", arch, true, this.platformSpecificBuildOptions.defaultArch))
360+
await this.info.emitArtifactBuildCompleted({
361+
file: artifactPath,
362+
target: null,
363+
arch: Arch.x64,
364+
safeArtifactName: this.computeSafeArtifactName(artifactName, "pkg", arch, true, this.platformSpecificBuildOptions.defaultArch),
365+
packager: this,
366+
})
361367
}
362368

363369
if (!isMas) {

packages/app-builder-lib/src/packager.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getArtifactArchName,
1010
InvalidConfigurationError,
1111
log,
12+
MAX_FILE_REQUESTS,
1213
orNullIfFileNotExist,
1314
safeStringifyJson,
1415
serializeToYaml,
@@ -41,6 +42,7 @@ import { resolveFunction } from "./util/resolve"
4142
import { installOrRebuild, nodeGypRebuild } from "./util/yarn"
4243
import { PACKAGE_VERSION } from "./version"
4344
import { AsyncEventEmitter, HandlerType } from "./util/asyncEventEmitter"
45+
import asyncPool from "tiny-async-pool"
4446

4547
async function createFrameworkInfo(configuration: Configuration, packager: Packager): Promise<Framework> {
4648
let framework = configuration.framework
@@ -479,6 +481,18 @@ export class Packager {
479481
const nameToTarget: Map<string, Target> = new Map()
480482
platformToTarget.set(platform, nameToTarget)
481483

484+
let poolCount = Math.floor(packager.config.concurrency?.jobs || 1)
485+
if (poolCount < 1) {
486+
log.warn({ concurrency: poolCount }, "concurrency is invalid, overriding with job count: 1")
487+
poolCount = 1
488+
} else if (poolCount > MAX_FILE_REQUESTS) {
489+
log.warn(
490+
{ concurrency: poolCount, MAX_FILE_REQUESTS },
491+
`job concurrency is greater than recommended MAX_FILE_REQUESTS, this may lead to File Descriptor errors (too many files open). Proceed with caution (e.g. this is an experimental feature)`
492+
)
493+
}
494+
const packPromises: Promise<any>[] = []
495+
482496
for (const [arch, targetNames] of computeArchToTargetNamesMap(archToType, packager, platform)) {
483497
if (this.cancellationToken.cancelled) {
484498
break
@@ -488,9 +502,21 @@ export class Packager {
488502
const outDir = path.resolve(this.projectDir, packager.expandMacro(this.config.directories!.output!, Arch[arch]))
489503
const targetList = createTargets(nameToTarget, targetNames.length === 0 ? packager.defaultTarget : targetNames, outDir, packager)
490504
await createOutDirIfNeed(targetList, createdOutDirs)
491-
await packager.pack(outDir, arch, targetList, taskManager)
505+
const promise = packager.pack(outDir, arch, targetList, taskManager)
506+
if (poolCount < 2) {
507+
await promise
508+
} else {
509+
packPromises.push(promise)
510+
}
492511
}
493512

513+
await asyncPool(poolCount, packPromises, async it => {
514+
if (this.cancellationToken.cancelled) {
515+
return
516+
}
517+
await it
518+
})
519+
494520
if (this.cancellationToken.cancelled) {
495521
break
496522
}
@@ -507,6 +533,9 @@ export class Packager {
507533
await taskManager.awaitTasks()
508534

509535
for (const target of syncTargetsIfAny) {
536+
if (this.cancellationToken.cancelled) {
537+
break
538+
}
510539
await target.finishBuild()
511540
}
512541
return platformToTarget

packages/app-builder-lib/src/platformPackager.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,6 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
153153
)
154154
}
155155

156-
dispatchArtifactCreated(file: string, target: Target | null, arch: Arch | null, safeArtifactName?: string | null): Promise<void> {
157-
return this.info.emitArtifactBuildCompleted({
158-
file,
159-
safeArtifactName,
160-
target,
161-
arch,
162-
packager: this,
163-
})
164-
}
165-
166156
async pack(outDir: string, arch: Arch, targets: Array<Target>, taskManager: AsyncTaskManager): Promise<any> {
167157
const appOutDir = this.computeAppOutDir(outDir, arch)
168158
await this.doPack({

packages/app-builder-lib/src/targets/AppxTarget.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const DEFAULT_RESOURCE_LANG = "en-US"
4949
export default class AppXTarget extends Target {
5050
readonly options: AppXOptions = deepAssign({}, this.packager.platformSpecificBuildOptions, this.packager.config.appx)
5151

52+
isAsyncSupported = false
53+
5254
constructor(
5355
private readonly packager: WinPackager,
5456
readonly outDir: string
@@ -145,18 +147,20 @@ export default class AppXTarget extends Target {
145147
if (this.options.makeappxArgs != null) {
146148
makeAppXArgs.push(...this.options.makeappxArgs)
147149
}
148-
await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", signToolArch, "makeappx.exe")), makeAppXArgs)
149-
await packager.sign(artifactPath)
150-
151-
await stageDir.cleanup()
152-
153-
await packager.info.emitArtifactBuildCompleted({
154-
file: artifactPath,
155-
packager,
156-
arch,
157-
safeArtifactName: packager.computeSafeArtifactName(artifactName, "appx"),
158-
target: this,
159-
isWriteUpdateInfo: this.options.electronUpdaterAware,
150+
this.buildQueueManager.add(async () => {
151+
await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", signToolArch, "makeappx.exe")), makeAppXArgs)
152+
await packager.sign(artifactPath)
153+
154+
await stageDir.cleanup()
155+
156+
await packager.info.emitArtifactBuildCompleted({
157+
file: artifactPath,
158+
packager,
159+
arch,
160+
safeArtifactName: packager.computeSafeArtifactName(artifactName, "appx"),
161+
target: this,
162+
isWriteUpdateInfo: this.options.electronUpdaterAware,
163+
})
160164
})
161165
}
162166

0 commit comments

Comments
 (0)