Skip to content

fix: allow custom electron zip name to be provided when unpacking a provided electronDist #9126

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

Merged
merged 5 commits into from
May 29, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-onions-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"app-builder-lib": patch
---

fix: allow custom electron zip name to be provided when unpacking a provided electronDist
111 changes: 71 additions & 40 deletions packages/app-builder-lib/src/electron/ElectronFramework.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { asArray, copyDir, DO_NOT_USE_HARD_LINKS, executeAppBuilder, log, MAX_FILE_REQUESTS, statOrNull, unlinkIfExists } from "builder-util"
import { emptyDir, readdir, rename } from "fs-extra"
import * as fs from "fs/promises"
import { emptyDir, readdir, rename, rm } from "fs-extra"
import * as path from "path"
import asyncPool from "tiny-async-pool"
import { Configuration } from "../configuration"
Expand Down Expand Up @@ -107,7 +106,7 @@ async function removeUnusedLanguagesIfNeeded(options: BeforeCopyExtraFilesOption

const language = path.basename(file, langFileExt)
if (!wantedLanguages.includes(language)) {
return fs.rm(path.join(dir, file), { recursive: true, force: true })
return rm(path.join(dir, file), { recursive: true, force: true })
}
return
})
Expand Down Expand Up @@ -149,7 +148,9 @@ class ElectronFramework implements Framework {
}

async prepareApplicationStageDirectory(options: PrepareApplicationStageDirectoryOptions) {
await unpack(options, createDownloadOpts(options.packager.config, options.platformName, options.arch, this.version), this.distMacOsAppName)
const downloadOptions = createDownloadOpts(options.packager.config, options.platformName, options.arch, this.version)
const shouldCleanup = await unpack(options, downloadOptions, this.distMacOsAppName)
await cleanupAfterUnpack(options, this.distMacOsAppName, shouldCleanup)
if (options.packager.config.downloadAlternateFFmpeg) {
await injectFFMPEG(options, this.version)
}
Expand Down Expand Up @@ -179,49 +180,79 @@ export async function createElectronFrameworkSupport(configuration: Configuratio
return new ElectronFramework(branding.projectName, version, `${branding.productName}.app`)
}

async function unpack(prepareOptions: PrepareApplicationStageDirectoryOptions, options: ElectronDownloadOptions, distMacOsAppName: string) {
/**
* Unpacks a custom or default Electron distribution into the app output directory.
*/
async function unpack(prepareOptions: PrepareApplicationStageDirectoryOptions, downloadOptions: ElectronDownloadOptions, distMacOsAppName: string) {
const downloadUsingAdjustedConfig = (options: ElectronDownloadOptions) => {
return executeAppBuilder(["unpack-electron", "--configuration", JSON.stringify([options]), "--output", appOutDir, "--distMacOsAppName", distMacOsAppName])
}

const { packager, appOutDir, platformName } = prepareOptions
const { version, arch } = downloadOptions
const defaultZipName = `electron-v${version}-${platformName}-${arch}.zip`

const electronDist = packager.config.electronDist || null
let dist: string | null = null
// check if supplied a custom electron distributable/fork/predownloaded directory
if (typeof electronDist === "string") {
let resolvedDist: string
// check if custom electron hook file for import resolving
if ((await statOrNull(electronDist))?.isFile()) {
const customElectronDist: any = await resolveFunction(packager.appInfo.type, electronDist, "electronDist")
resolvedDist = await Promise.resolve(typeof customElectronDist === "function" ? customElectronDist(prepareOptions) : customElectronDist)
} else {
resolvedDist = electronDist
}
dist = path.isAbsolute(resolvedDist) ? resolvedDist : path.resolve(packager.projectDir, resolvedDist)
}
if (dist != null) {
const zipFile = `electron-v${options.version}-${platformName}-${options.arch}.zip`
if ((await statOrNull(path.join(dist, zipFile))) != null) {
log.info({ dist, zipFile }, "resolved electronDist")
options.cache = dist
dist = null
} else {
log.info({ electronDist: log.filePath(dist), expectedFile: zipFile }, "custom electronDist provided but no zip found; assuming unpacked electron directory.")
}
let resolvedDist: string | null = null
try {
const electronDistHook: any = await resolveFunction(packager.appInfo.type, packager.config.electronDist, "electronDist")
resolvedDist = typeof electronDistHook === "function" ? await Promise.resolve(electronDistHook(prepareOptions)) : electronDistHook
} catch (error: any) {
throw new Error("Failed to resolve electronDist: " + error.message)
}

let isFullCleanup = false
if (dist == null) {
await executeAppBuilder(["unpack-electron", "--configuration", JSON.stringify([options]), "--output", appOutDir, "--distMacOsAppName", distMacOsAppName])
} else {
isFullCleanup = true
const source = packager.getElectronSrcDir(dist)
const destination = packager.getElectronDestinationDir(appOutDir)
log.info({ source, destination }, "copying Electron")
await emptyDir(appOutDir)
await copyDir(source, destination, {
isUseHardLink: DO_NOT_USE_HARD_LINKS,
if (resolvedDist == null) {
// if no custom electronDist is provided, use the default unpack logic
log.debug(null, "no custom electronDist provided, unpacking default Electron distribution")
await downloadUsingAdjustedConfig(downloadOptions)
return true // indicates that we should clean up after unpacking
}

if (!path.isAbsolute(resolvedDist)) {
// if it's a relative path, resolve it against the project directory
resolvedDist = path.resolve(packager.projectDir, resolvedDist)
}

const electronDistStats = await statOrNull(resolvedDist)
if (!electronDistStats) {
throw new Error(`The specified electronDist does not exist: ${resolvedDist}. Please provide a valid path to the Electron zip file or cache directory.`)
}

if (resolvedDist.endsWith(".zip")) {
log.info({ zipFile: resolvedDist }, "using custom electronDist zip file")
await downloadUsingAdjustedConfig({
...downloadOptions,
cache: path.dirname(resolvedDist), // set custom directory to the zip file's directory
customFilename: path.basename(resolvedDist), // set custom filename to the zip file's name
})
return false // do not clean up after unpacking, it's a custom bundle and we should respect its configuration/contents as required
}

await cleanupAfterUnpack(prepareOptions, distMacOsAppName, isFullCleanup)
if (electronDistStats.isDirectory()) {
// backward compatibility: if electronDist is a directory, check for the default zip file inside it
const files = await readdir(resolvedDist)
if (files.includes(defaultZipName)) {
log.info({ electronDist: log.filePath(resolvedDist) }, "using custom electronDist directory")
await downloadUsingAdjustedConfig({
...downloadOptions,
cache: resolvedDist,
customFilename: defaultZipName,
})
return false
}
}

// if we reach here, it means the provided electronDist is neither a zip file nor a directory with the default zip file
// e.g. we treat it as a custom already-unpacked Electron distribution
log.info({ electronDist: log.filePath(resolvedDist) }, "using custom unpacked Electron distribution")
const source = packager.getElectronSrcDir(resolvedDist)
const destination = packager.getElectronDestinationDir(appOutDir)
log.info({ source, destination }, "copying unpacked Electron")
await emptyDir(appOutDir)
await copyDir(source, destination, {
isUseHardLink: DO_NOT_USE_HARD_LINKS,
})

return false
}

function cleanupAfterUnpack(prepareOptions: PrepareApplicationStageDirectoryOptions, distMacOsAppName: string, isFullCleanup: boolean) {
Expand Down
2 changes: 0 additions & 2 deletions test/snapshots/mac/macPackagerTest.js.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`electronDist 1`] = `"corrupted Electron dist"`;

exports[`multiple asar resources 1`] = `
{
"mac": [
Expand Down
2 changes: 0 additions & 2 deletions test/snapshots/windows/winCodeSignTest.js.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`electronDist 1`] = `"ENOENT"`;

exports[`forceCodeSigning 1`] = `"ERR_ELECTRON_BUILDER_INVALID_CONFIGURATION"`;

exports[`parseDn 1`] = `
Expand Down
15 changes: 10 additions & 5 deletions test/src/mac/macPackagerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,17 @@ test.ifMac("yarn two package.json w/ native module", ({ expect }) =>
)

test.ifMac("electronDist", ({ expect }) =>
appThrows(expect, {
targets: Platform.MAC.createTarget(DIR_TARGET, Arch.x64),
config: {
electronDist: "foo",
appThrows(
expect,
{
targets: Platform.MAC.createTarget(DIR_TARGET, Arch.x64),
config: {
electronDist: "foo",
},
},
})
{},
error => expect(error.message).toContain("Failed to resolve electronDist")
)
)

test.ifWinCi("Build macOS on Windows is not supported", ({ expect }) => appThrows(expect, platform(Platform.MAC)))
Expand Down
2 changes: 1 addition & 1 deletion test/src/updater/linuxUpdaterTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const packageManagerMap: {
pms: ["pacman"],
updater: PacmanUpdater,
extension: "pacman",
}
},
}

for (const distro in packageManagerMap) {
Expand Down
15 changes: 10 additions & 5 deletions test/src/windows/winCodeSignTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,17 @@ test("forceCodeSigning", ({ expect }) =>
}))

test("electronDist", ({ expect }) =>
appThrows(expect, {
targets: windowsDirTarget,
config: {
electronDist: "foo",
appThrows(
expect,
{
targets: windowsDirTarget,
config: {
electronDist: "foo",
},
},
}))
{},
error => expect(error.message).toContain("Failed to resolve electronDist")
))

test("azure signing without credentials", ({ expect }) =>
appThrows(
Expand Down