From 8083e317894f1c898c3df2f0d953624898306cfd Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Wed, 3 Jun 2026 16:28:16 -0700 Subject: [PATCH 01/20] feat(squirrel-windows): inline installer logic; remove electron-winstaller dependency and its vendored binaries --- .changeset/remove-electron-winstaller-dep.md | 5 + .../package.json | 7 +- .../src/SquirrelWindowsTarget.ts | 87 +++++------ .../src/windowsInstaller.ts | 140 ++++++++++++++++++ .../template.nuspectemplate | 4 +- pnpm-lock.yaml | 114 -------------- test/package.json | 1 - test/src/helpers/packTester.ts | 2 +- .../windows/squirrelWindowsInstallerTest.ts | 122 +++++++++++++++ 9 files changed, 308 insertions(+), 174 deletions(-) create mode 100644 .changeset/remove-electron-winstaller-dep.md create mode 100644 packages/electron-builder-squirrel-windows/src/windowsInstaller.ts create mode 100644 test/src/windows/squirrelWindowsInstallerTest.ts diff --git a/.changeset/remove-electron-winstaller-dep.md b/.changeset/remove-electron-winstaller-dep.md new file mode 100644 index 00000000000..9f9f091aaef --- /dev/null +++ b/.changeset/remove-electron-winstaller-dep.md @@ -0,0 +1,5 @@ +--- +"electron-builder-squirrel-windows": minor +--- + +feat(squirrel-windows): inline installer logic; remove electron-winstaller dependency and its vendored binaries diff --git a/packages/electron-builder-squirrel-windows/package.json b/packages/electron-builder-squirrel-windows/package.json index 5f019e4eba5..29f513e774a 100644 --- a/packages/electron-builder-squirrel-windows/package.json +++ b/packages/electron-builder-squirrel-windows/package.json @@ -17,12 +17,7 @@ ], "dependencies": { "app-builder-lib": "workspace:*", - "builder-util": "workspace:*", - "electron-winstaller": "5.4.0" - }, - "devDependencies": { - "@types/archiver": "5.3.1", - "@types/fs-extra": "9.0.13" + "builder-util": "workspace:*" }, "types": "./out/SquirrelWindowsTarget.d.ts" } diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 64364c2e1cc..8b9726e6f59 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -7,7 +7,7 @@ import { withToolsetLock } from "app-builder-lib/out/util/toolsetLock" import * as path from "path" import * as fs from "fs" import * as os from "os" -import { Options as SquirrelOptions, createWindowsInstaller, convertVersion } from "electron-winstaller" +import { InstallerOptions, createWindowsInstaller, convertVersion } from "./windowsInstaller" export default class SquirrelWindowsTarget extends Target { //tslint:disable-next-line:no-object-literal-type-assertion @@ -33,12 +33,7 @@ export default class SquirrelWindowsTarget extends Target { log.warn({ customSquirrelVendorDirectory }, "unable to access custom Squirrel.Windows vendor directory, falling back to default vendor") } - const windowInstallerPackage = require.resolve("electron-winstaller/package.json") - const [squirrelBin] = await Promise.all([ - getBinFromUrl("squirrel.windows@1.0.0", "squirrel.windows-2.0.1-patched.7z", "76851f0c192eaf9bc6f8f3eecdfe325857ebe70d7833ec62ed846a1acd50c846"), - fs.promises.cp(path.join(path.dirname(windowInstallerPackage), "vendor"), tmpVendorDirectory, { recursive: true }), - ]) - + const squirrelBin = await getBinFromUrl("squirrel.windows@1.0.0", "squirrel.windows-2.0.1-patched.7z", "76851f0c192eaf9bc6f8f3eecdfe325857ebe70d7833ec62ed846a1acd50c846") await fs.promises.cp(path.join(squirrelBin, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) } @@ -143,7 +138,7 @@ export default class SquirrelWindowsTarget extends Target { arch, }) const distOptions = await this.computeEffectiveDistOptions(appOutDir, installerOutDir, setupFile) - await this.generateStubExecutableExe(appOutDir, distOptions.vendorDirectory!) + await this.generateStubExecutableExe(appOutDir, distOptions.vendorDirectory) await withToolsetLock(() => createWindowsInstaller(distOptions)) await packager.signAndEditResources(artifactPath, arch, installerOutDir) @@ -211,7 +206,7 @@ export default class SquirrelWindowsTarget extends Target { // https://github.com/electron/windows-installer/blob/main/script/select-7z-arch.js // Even if we're cross-compiling for a different arch like arm64, // we still need to use the 7-Zip executable for the host arch - const resolvedArch = os.arch + const resolvedArch = os.arch() fs.copyFileSync(path.join(vendorDirectory, `7z-${resolvedArch}.exe`), path.join(vendorDirectory, "7z.exe")) fs.copyFileSync(path.join(vendorDirectory, `7z-${resolvedArch}.dll`), path.join(vendorDirectory, "7z.dll")) } @@ -230,7 +225,7 @@ export default class SquirrelWindowsTarget extends Target { return templatePath } - async computeEffectiveDistOptions(appDirectory: string, outputDirectory: string, setupFile: string): Promise { + async computeEffectiveDistOptions(appDirectory: string, outputDirectory: string, setupFile: string): Promise { const packager = this.packager let iconUrl = this.options.iconUrl if (iconUrl == null) { @@ -246,60 +241,54 @@ export default class SquirrelWindowsTarget extends Target { checkConflictingOptions(this.options) const appInfo = packager.appInfo - const options: SquirrelOptions = { - appDirectory: appDirectory, - outputDirectory: outputDirectory, - name: this.options.useAppIdAsId ? appInfo.id : this.appName, - title: appInfo.productName || appInfo.name, - version: appInfo.version, - description: appInfo.description, - exe: `${this.exeName}.exe`, - authors: appInfo.companyName || "", - nuspecTemplate: await this.createNuspecTemplateWithProjectUrl(), - iconUrl, - copyright: appInfo.copyright, - noMsi: !this.options.msi, - usePackageJson: false, - } - - options.vendorDirectory = await this.prepareSignedVendorDirectory() - this.select7zipArch(options.vendorDirectory) - options.fixUpPaths = true - options.setupExe = setupFile - if (this.options.msi) { - options.setupMsi = setupFile.replace(".exe", ".msi") - } - - if (isEmptyOrSpaces(options.description)) { - options.description = this.options.name || appInfo.productName - } - if (options.remoteToken == null) { - options.remoteToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN - } + const description = isEmptyOrSpaces(appInfo.description) ? (this.options.name || appInfo.productName) : appInfo.description + let remoteReleases: string | undefined if (this.options.remoteReleases === true) { const info = await packager.info.repositoryInfo if (info == null) { log.warn("remoteReleases set to true, but cannot get repository info") } else { - options.remoteReleases = `https://github.com/${info.user}/${info.project}` - log.info({ remoteReleases: options.remoteReleases }, `remoteReleases is set`) + remoteReleases = `https://github.com/${info.user}/${info.project}` + log.info({ remoteReleases }, `remoteReleases is set`) } } else if (typeof this.options.remoteReleases === "string" && !isEmptyOrSpaces(this.options.remoteReleases)) { - options.remoteReleases = this.options.remoteReleases + remoteReleases = this.options.remoteReleases } + let loadingGif: string | undefined if (this.options.loadingGif) { - options.loadingGif = path.resolve(packager.projectDir, this.options.loadingGif) + loadingGif = path.resolve(packager.projectDir, this.options.loadingGif) } else { - const resourceList = await packager.resourceList - if (resourceList.includes("install-spinner.gif")) { - options.loadingGif = path.join(packager.buildResourcesDir, "install-spinner.gif") - } + loadingGif = (await packager.getResource("install-spinner.gif")) ?? undefined } - return options + const vendorDirectory = await this.prepareSignedVendorDirectory() + this.select7zipArch(vendorDirectory) + + return { + appDirectory, + outputDirectory, + vendorDirectory, + name: this.options.useAppIdAsId ? appInfo.id : this.appName, + title: appInfo.productName || appInfo.name, + version: appInfo.version, + description, + exe: `${this.exeName}.exe`, + authors: appInfo.companyName || "", + nuspecTemplate: await this.createNuspecTemplateWithProjectUrl(), + iconUrl, + copyright: appInfo.copyright, + noMsi: !this.options.msi, + fixUpPaths: true, + setupExe: setupFile, + setupMsi: this.options.msi ? setupFile.replace(".exe", ".msi") : undefined, + loadingGif, + remoteReleases, + remoteToken: this.options.remoteToken || process.env.GH_TOKEN || process.env.GITHUB_TOKEN, + createTempDir: opts => this.packager.info.tempDirManager.createTempDir(opts), + } } } diff --git a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts new file mode 100644 index 00000000000..0e260fd560c --- /dev/null +++ b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts @@ -0,0 +1,140 @@ +import { exec, log } from "builder-util" +import * as fs from "fs/promises" +import * as path from "path" + +export function convertVersion(version: string): string { + const parts = version.split("+")[0].split("-") + const mainVersion = parts.shift() + return parts.length > 0 ? [mainVersion, parts.join("-").replace(/\./g, "")].join("-") : mainVersion! +} + +export function escapeXml(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'") +} + +export function renderNuspecTemplate(templateContent: string, vars: Record): string { + return Object.entries(vars).reduce((t, [k, v]) => t.split(`<%- ${k} %>`).join(v), templateContent) +} + +export async function buildAdditionalFilesXml(appDirectory: string): Promise { + const checks: Array<{ rel: string; src: string; target: string }> = [ + { rel: "swiftshader", src: "swiftshader\\**", target: "lib\\net45\\swiftshader" }, + { rel: "vk_swiftshader_icd.json", src: "vk_swiftshader_icd.json", target: "lib\\net45" }, + ] + const lines: string[] = [] + for (const { rel, src, target } of checks) { + try { + await fs.access(path.join(appDirectory, rel)) + lines.push(` `) + } catch { + // not present, skip + } + } + return lines.join("\n") +} + +export interface InstallerOptions { + appDirectory: string + outputDirectory: string + vendorDirectory: string + name: string + title?: string | null + version: string + description?: string | null + exe: string + authors: string + owners?: string | null + iconUrl?: string | null + copyright?: string | null + nuspecTemplate: string + loadingGif?: string | null + noMsi?: boolean + remoteReleases?: string | null + remoteToken?: string | null + setupExe?: string | null + setupMsi?: string | null + fixUpPaths?: boolean + createTempDir: (options: { prefix: string }) => Promise +} + +export async function createWindowsInstaller(options: InstallerOptions): Promise { + const { appDirectory, outputDirectory, vendorDirectory, name, version, exe, nuspecTemplate, loadingGif, noMsi, remoteReleases, remoteToken, setupExe, setupMsi, fixUpPaths = false, createTempDir } = options + + const useMono = process.platform !== "win32" + if (useMono) { + log.warn("building Squirrel.Windows on non-Windows requires mono in PATH; spawn will fail if it is missing") + } + + // Squirrel.exe must live in the app directory so the nuspec can reference it + await fs.copyFile(path.join(vendorDirectory, "Squirrel.exe"), path.join(appDirectory, "Squirrel.exe")) + + const nugetVersion = convertVersion(version) + const authors = options.authors || "" + const copyright = options.copyright || `Copyright © ${new Date().getFullYear()} ${authors}` + const owners = options.owners || authors + const additionalFilesXml = await buildAdditionalFilesXml(appDirectory) + + let templateContent = await fs.readFile(nuspecTemplate, "utf8") + if (path.sep === "/") { + templateContent = templateContent.replace(/\\/g, "/") + } + + const nuspecContent = renderNuspecTemplate(templateContent, { + name: escapeXml(name), + title: escapeXml(options.title || name), + version: escapeXml(nugetVersion), + authors: escapeXml(authors), + owners: escapeXml(owners), + iconUrl: escapeXml(options.iconUrl || ""), + description: escapeXml(options.description || options.title || name), + copyright: escapeXml(copyright), + exe: escapeXml(exe), + additionalFilesXml, + }) + + const nugetOutput = await createTempDir({ prefix: "squirrel-nuget-" }) + const nuspecPath = path.join(nugetOutput, `${name}.nuspec`) + await fs.writeFile(nuspecPath, nuspecContent) + + const nugetExe = path.join(vendorDirectory, "nuget.exe") + const nugetArgs = ["pack", nuspecPath, "-BasePath", appDirectory, "-OutputDirectory", nugetOutput, "-NoDefaultExcludes"] + await (useMono ? exec("mono", [nugetExe, ...nugetArgs]) : exec(nugetExe, nugetArgs)) + + const nupkgPath = path.join(nugetOutput, `${name}.${nugetVersion}.nupkg`) + + if (remoteReleases) { + const syncReleasesExe = path.join(vendorDirectory, "SyncReleases.exe") + const syncArgs = ["-u", remoteReleases, "-r", outputDirectory] + if (remoteToken) { + syncArgs.push("-t", remoteToken) + } + await (useMono ? exec("mono", [syncReleasesExe, ...syncArgs]) : exec(syncReleasesExe, syncArgs)) + } + + const releasifyArgs = ["--releasify", nupkgPath, "--releaseDir", outputDirectory] + if (loadingGif) { + releasifyArgs.push("--loadingGif", loadingGif) + } + if (noMsi) { + releasifyArgs.push("--no-msi") + } + + if (useMono) { + await exec("mono", [path.join(vendorDirectory, "Squirrel-Mono.exe"), ...releasifyArgs]) + } else { + await exec(path.join(vendorDirectory, "Squirrel.exe"), releasifyArgs) + } + + if (fixUpPaths) { + if (setupExe) { + await fs.rename(path.join(outputDirectory, "Setup.exe"), path.join(outputDirectory, setupExe)) + } + if (setupMsi) { + try { + await fs.rename(path.join(outputDirectory, "Setup.msi"), path.join(outputDirectory, setupMsi)) + } catch { + // Setup.msi won't exist when noMsi is true + } + } + } +} diff --git a/packages/electron-builder-squirrel-windows/template.nuspectemplate b/packages/electron-builder-squirrel-windows/template.nuspectemplate index 688a369d20d..e5bad0bb152 100644 --- a/packages/electron-builder-squirrel-windows/template.nuspectemplate +++ b/packages/electron-builder-squirrel-windows/template.nuspectemplate @@ -25,8 +25,6 @@ - <% additionalFiles.forEach(function(f) { %> - - <% }); %> +<%- additionalFilesXml %> \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 261f3e2315e..611b4ab729b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,16 +466,6 @@ importers: builder-util: specifier: workspace:* version: link:../builder-util - electron-winstaller: - specifier: 5.4.0 - version: 5.4.0 - devDependencies: - '@types/archiver': - specifier: 5.3.1 - version: 5.3.1 - '@types/fs-extra': - specifier: 9.0.13 - version: 9.0.13 packages/electron-forge-maker-appimage: dependencies: @@ -670,9 +660,6 @@ importers: electron-updater: specifier: workspace:* version: link:../packages/electron-updater - electron-winstaller: - specifier: 5.4.0 - version: 5.4.0 fast-xml-parser: specifier: 5.7.0 version: 5.7.0 @@ -2826,11 +2813,6 @@ packages: resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} engines: {node: '>=16.4'} - '@electron/windows-sign@1.2.1': - resolution: {integrity: sha512-YfASnrhJ+ve6Q43ZiDwmpBgYgi2u0bYjeAVi2tDfN7YWAKO8X9EEOuPGtqbJpPLM6TfAHimghICjWe2eaJ8BAg==} - engines: {node: '>=14.14'} - hasBin: true - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -4052,9 +4034,6 @@ packages: '@types/adm-zip@0.5.6': resolution: {integrity: sha512-lRlcSLg5Yoo7C2H2AUiAoYlvifWoCx/se7iUNiCBTfEVVYFVn+Tr9ZGed4K73tYgLe9O4PjdJvbxlkdAOx/qiw==} - '@types/archiver@5.3.1': - resolution: {integrity: sha512-wKYZaSXaDvTZuInAWjCeGG7BEAgTWG2zZW0/f7IYFcoHB2X2d9lkVFnrOlXl3W6NrvO6Ml3FLLu8Uksyymcpnw==} - '@types/aws4@1.11.6': resolution: {integrity: sha512-5CnVUkHNyLGpD9AnOcK66YyP0qvIh6nhJJoeK8zSl5YKikUcUbdB7SlHevUYVqicgeh6j5AJa1qa/h08dSZHoA==} @@ -4214,9 +4193,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/glob@8.1.0': - resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} - '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -4289,9 +4265,6 @@ packages: '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -5216,10 +5189,6 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -5390,9 +5359,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-dirname@0.1.0: - resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5912,10 +5878,6 @@ packages: electron-to-chromium@1.5.353: resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} - electron-winstaller@5.4.0: - resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} - engines: {node: '>=8.0.0'} - electron@39.8.5: resolution: {integrity: sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==} engines: {node: '>= 12.20.55'} @@ -7673,10 +7635,6 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -8477,11 +8435,6 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - postject@1.0.0-alpha.6: - resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} - engines: {node: '>=14.0.0'} - hasBin: true - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -8871,11 +8824,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -9307,10 +9255,6 @@ packages: temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} - temp@0.9.4: - resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} - engines: {node: '>=6.0.0'} - tempfile@5.0.0: resolution: {integrity: sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q==} engines: {node: '>=14.18'} @@ -13788,17 +13732,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@electron/windows-sign@1.2.1': - dependencies: - cross-dirname: 0.1.0 - debug: 4.4.3 - fs-extra: 11.3.0 - minimist: 1.2.8 - postject: 1.0.0-alpha.6 - transitivePeerDependencies: - - supports-color - optional: true - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -14984,10 +14917,6 @@ snapshots: dependencies: '@types/node': 22.13.17 - '@types/archiver@5.3.1': - dependencies: - '@types/glob': 8.1.0 - '@types/aws4@1.11.6': dependencies: '@types/node': 22.13.17 @@ -15195,11 +15124,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/glob@8.1.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 22.13.17 - '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.13.17 @@ -15268,8 +15192,6 @@ snapshots: '@types/minimatch@3.0.5': {} - '@types/minimatch@5.1.2': {} - '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -16352,9 +16274,6 @@ snapshots: commander@8.3.0: {} - commander@9.5.0: - optional: true - common-path-prefix@3.0.0: {} compare-func@2.0.0: @@ -16543,9 +16462,6 @@ snapshots: create-require@1.1.1: {} - cross-dirname@0.1.0: - optional: true - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -17135,18 +17051,6 @@ snapshots: electron-to-chromium@1.5.353: {} - electron-winstaller@5.4.0: - dependencies: - '@electron/asar': 3.4.1 - debug: 4.4.3 - fs-extra: 7.0.1 - lodash: 4.17.21 - temp: 0.9.4 - optionalDependencies: - '@electron/windows-sign': 1.2.1 - transitivePeerDependencies: - - supports-color - electron@39.8.5: dependencies: '@electron/get': 2.0.3 @@ -19392,10 +19296,6 @@ snapshots: dependencies: minipass: 7.1.3 - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - mri@1.2.0: {} mrmime@2.0.1: {} @@ -20244,11 +20144,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postject@1.0.0-alpha.6: - dependencies: - commander: 9.5.0 - optional: true - prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -20718,10 +20613,6 @@ snapshots: reusify@1.1.0: {} - rimraf@2.6.3: - dependencies: - glob: 7.2.3 - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -21228,11 +21119,6 @@ snapshots: async-exit-hook: 2.0.1 fs-extra: 10.1.0 - temp@0.9.4: - dependencies: - mkdirp: 0.5.6 - rimraf: 2.6.3 - tempfile@5.0.0: dependencies: temp-dir: 3.0.0 diff --git a/test/package.json b/test/package.json index 4148c450b92..cbda10ef7d8 100644 --- a/test/package.json +++ b/test/package.json @@ -31,7 +31,6 @@ "electron-builder-squirrel-windows": "workspace:*", "electron-publish": "workspace:*", "electron-updater": "workspace:*", - "electron-winstaller": "5.4.0", "fast-xml-parser": "5.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 005b68b7b1d..c8ad3a116e2 100644 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -7,7 +7,7 @@ import { AsarIntegrity } from "app-builder-lib/out/asar/integrity" import { addValue, copyDir, exec, executeFinally, exists, FileCopier, log, USE_HARD_LINKS, walk } from "builder-util" import { CancellationToken, deepAssign, UpdateFileInfo } from "builder-util-runtime" import { Arch, ArtifactCreated, Configuration, DIR_TARGET, getArchSuffix, MacOsTargetName, Packager, PackagerOptions, Platform, Target } from "electron-builder" -import { convertVersion } from "electron-winstaller" +import { convertVersion } from "electron-builder-squirrel-windows/out/windowsInstaller" import { PublishPolicy } from "electron-publish" import { copyFile, emptyDir, mkdir, writeJson } from "fs-extra" import * as fs from "fs/promises" diff --git a/test/src/windows/squirrelWindowsInstallerTest.ts b/test/src/windows/squirrelWindowsInstallerTest.ts new file mode 100644 index 00000000000..fb0385dfac9 --- /dev/null +++ b/test/src/windows/squirrelWindowsInstallerTest.ts @@ -0,0 +1,122 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest" +import { convertVersion, escapeXml, renderNuspecTemplate, buildAdditionalFilesXml } from "electron-builder-squirrel-windows/out/windowsInstaller" +import { mkdir, mkdtemp, rm, writeFile } from "fs/promises" +import { tmpdir } from "os" +import * as path from "path" + +describe("convertVersion", () => { + test.each([ + ["1.0.0", "1.0.0"], + ["1.2.3", "1.2.3"], + ["1.0.0-alpha", "1.0.0-alpha"], + ["1.0.0-alpha.1", "1.0.0-alpha1"], + ["1.0.0-alpha.beta", "1.0.0-alphabeta"], + ["1.0.0-rc.1", "1.0.0-rc1"], + ["2.0.0-beta.2", "2.0.0-beta2"], + ["1.0.0+build.123", "1.0.0"], + ["1.0.0-beta+build.1", "1.0.0-beta"], + ["2.0.0-rc.1+sha.abc123", "2.0.0-rc1"], + ["10.20.30", "10.20.30"], + ])("converts %s → %s", (input, expected) => { + expect(convertVersion(input)).toBe(expected) + }) +}) + +describe("escapeXml", () => { + test.each([ + ["Foo & Bar", "Foo & Bar"], + ["", "<script>alert(1)</script>"], + ['"double quoted"', ""double quoted""], + ["it's fine", "it's fine"], + ["normal text", "normal text"], + ["a & b < c > d", "a & b < c > d"], + ["", ""], + ])("escapes %s", (input, expected) => { + expect(escapeXml(input)).toBe(expected) + }) + + test("escapes all five XML special characters", () => { + expect(escapeXml(`&<>"'`)).toBe("&<>"'") + }) +}) + +describe("renderNuspecTemplate", () => { + test("substitutes a single variable", () => { + expect(renderNuspecTemplate("<%- name %>", { name: "MyApp" })).toBe("MyApp") + }) + + test("substitutes multiple variables", () => { + const template = "<%- name %><%- version %>" + expect(renderNuspecTemplate(template, { name: "App", version: "1.2.3" })).toBe("App1.2.3") + }) + + test("leaves unknown placeholders untouched", () => { + expect(renderNuspecTemplate("<%- unknown %>", {})).toBe("<%- unknown %>") + }) + + test("handles empty string values", () => { + expect(renderNuspecTemplate("<%- val %>", { val: "" })).toBe("") + }) + + test("substitutes the same key multiple times", () => { + const template = "<%- x %> and <%- x %>" + expect(renderNuspecTemplate(template, { x: "hello" })).toBe("hello and hello") + }) + + test("inserts pre-escaped XML values verbatim", () => { + const template = "<%- description %>" + const result = renderNuspecTemplate(template, { description: "A & B" }) + expect(result).toBe("A & B") + }) + + test("inserts additionalFilesXml block without double-escaping", () => { + const extra = ` ` + const template = "<%- additionalFilesXml %>" + expect(renderNuspecTemplate(template, { additionalFilesXml: extra })).toBe(extra) + }) +}) + +describe("buildAdditionalFilesXml", () => { + let tmpDir: string + + beforeEach(async () => { + tmpDir = await mkdtemp(path.join(tmpdir(), "eb-installer-test-")) + }) + + afterEach(() => rm(tmpDir, { recursive: true, force: true }).catch(() => {})) + + test("returns empty string when no optional GPU files present", async () => { + const result = await buildAdditionalFilesXml(tmpDir) + expect(result).toBe("") + }) + + test("includes swiftshader entry when directory present", async () => { + await mkdir(path.join(tmpDir, "swiftshader"), { recursive: true }) + const result = await buildAdditionalFilesXml(tmpDir) + expect(result).toContain("swiftshader") + expect(result).toContain(" { + await writeFile(path.join(tmpDir, "vk_swiftshader_icd.json"), "{}") + const result = await buildAdditionalFilesXml(tmpDir) + expect(result).toContain("vk_swiftshader_icd.json") + expect(result).toContain(" { + await mkdir(path.join(tmpDir, "swiftshader"), { recursive: true }) + await writeFile(path.join(tmpDir, "vk_swiftshader_icd.json"), "{}") + const result = await buildAdditionalFilesXml(tmpDir) + expect(result).toContain("swiftshader") + expect(result).toContain("vk_swiftshader_icd.json") + const lines = result.split("\n").filter((l: string) => l.trim()) + expect(lines).toHaveLength(2) + }) + + test("does not include unrelated files", async () => { + await writeFile(path.join(tmpDir, "some-other-file.dll"), "") + const result = await buildAdditionalFilesXml(tmpDir) + expect(result).toBe("") + }) +}) From 5a62ecc75418e2911d1afa3e29b8e1043f099fe9 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Wed, 3 Jun 2026 17:39:30 -0700 Subject: [PATCH 02/20] address PR --- .gitignore | 1 + .../src/SquirrelWindowsTarget.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a00c7e29ab6..0d9764f33a3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ _vitest-smart-cache.json yalc.lock .claude +test/vitest-scripts/_vitest-smart-cache.json \ No newline at end of file diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 8b9726e6f59..d9965a6724a 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -205,10 +205,18 @@ export default class SquirrelWindowsTarget extends Target { private select7zipArch(vendorDirectory: string) { // https://github.com/electron/windows-installer/blob/main/script/select-7z-arch.js // Even if we're cross-compiling for a different arch like arm64, - // we still need to use the 7-Zip executable for the host arch + // we still need to use the 7-Zip executable for the host arch. + // When arch-specific variants aren't present (vendor bundle ships a pre-selected 7z.exe), + // skip the copy — the bundle's 7z.exe is already correct. const resolvedArch = os.arch() - fs.copyFileSync(path.join(vendorDirectory, `7z-${resolvedArch}.exe`), path.join(vendorDirectory, "7z.exe")) - fs.copyFileSync(path.join(vendorDirectory, `7z-${resolvedArch}.dll`), path.join(vendorDirectory, "7z.dll")) + const archExe = path.join(vendorDirectory, `7z-${resolvedArch}.exe`) + const archDll = path.join(vendorDirectory, `7z-${resolvedArch}.dll`) + if (fs.existsSync(archExe)) { + fs.copyFileSync(archExe, path.join(vendorDirectory, "7z.exe")) + } + if (fs.existsSync(archDll)) { + fs.copyFileSync(archDll, path.join(vendorDirectory, "7z.dll")) + } } private async createNuspecTemplateWithProjectUrl() { @@ -261,7 +269,7 @@ export default class SquirrelWindowsTarget extends Target { if (this.options.loadingGif) { loadingGif = path.resolve(packager.projectDir, this.options.loadingGif) } else { - loadingGif = (await packager.getResource("install-spinner.gif")) ?? undefined + loadingGif = (await packager.getResource(undefined, "install-spinner.gif")) ?? undefined } const vendorDirectory = await this.prepareSignedVendorDirectory() From 6c914d324dc088a9c0e7a2241ce850f826839193 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Wed, 3 Jun 2026 22:49:14 -0700 Subject: [PATCH 03/20] combine wix and wincodesign toolsets for squirrel --- .../src/SquirrelWindowsTarget.ts | 14 ++++++++++++-- .../src/windowsInstaller.ts | 9 ++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index d9965a6724a..d644de8947a 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -33,7 +33,17 @@ export default class SquirrelWindowsTarget extends Target { log.warn({ customSquirrelVendorDirectory }, "unable to access custom Squirrel.Windows vendor directory, falling back to default vendor") } - const squirrelBin = await getBinFromUrl("squirrel.windows@1.0.0", "squirrel.windows-2.0.1-patched.7z", "76851f0c192eaf9bc6f8f3eecdfe325857ebe70d7833ec62ed846a1acd50c846") + const [squirrelBin, wixBin] = await Promise.all([ + getBinFromUrl("squirrel.windows@1.0.0", "squirrel.windows-2.0.1-patched.7z", "76851f0c192eaf9bc6f8f3eecdfe325857ebe70d7833ec62ed846a1acd50c846"), + getBinFromUrl( + "wix@1.0.0", + "wix-4.0.6.tar.gz", + "e84c8f37bdfd833b6c7d92489ca4102f001517e9dd50a1204146d616f6fdc611e2049c79f13325dbb491454d03247847ed426d8b4ad8556693c72eb95f08cde0" + ), + ]) + + // WiX tools first (lower precedence), then Squirrel executables overlay + await fs.promises.cp(wixBin, tmpVendorDirectory, { recursive: true }) await fs.promises.cp(path.join(squirrelBin, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) } @@ -294,7 +304,7 @@ export default class SquirrelWindowsTarget extends Target { setupMsi: this.options.msi ? setupFile.replace(".exe", ".msi") : undefined, loadingGif, remoteReleases, - remoteToken: this.options.remoteToken || process.env.GH_TOKEN || process.env.GITHUB_TOKEN, + remoteToken: this.options.remoteToken ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN, createTempDir: opts => this.packager.info.tempDirManager.createTempDir(opts), } } diff --git a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts index 0e260fd560c..0cff051031a 100644 --- a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts +++ b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts @@ -26,8 +26,10 @@ export async function buildAdditionalFilesXml(appDirectory: string): Promise`) - } catch { - // not present, skip + } catch (e: any) { + if (e?.code !== "ENOENT") { + throw e + } } } return lines.join("\n") @@ -72,11 +74,12 @@ export async function createWindowsInstaller(options: InstallerOptions): Promise const authors = options.authors || "" const copyright = options.copyright || `Copyright © ${new Date().getFullYear()} ${authors}` const owners = options.owners || authors - const additionalFilesXml = await buildAdditionalFilesXml(appDirectory) + let additionalFilesXml = await buildAdditionalFilesXml(appDirectory) let templateContent = await fs.readFile(nuspecTemplate, "utf8") if (path.sep === "/") { templateContent = templateContent.replace(/\\/g, "/") + additionalFilesXml = additionalFilesXml.replace(/\\/g, "/") } const nuspecContent = renderNuspecTemplate(templateContent, { From 8a18420bd3735f6171cf6ef5fbb95084446904a4 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 4 Jun 2026 06:54:27 -0700 Subject: [PATCH 04/20] migrate to updated squirrel windows vendor toolset bundle --- .../src/SquirrelWindowsTarget.ts | 29 +++++------ .../src/toolset.ts | 29 +++++++++++ test/src/electronGetTest.ts | 1 + .../windows/squirrelWindowsSecurityTest.ts | 9 ++++ test/src/windows/squirrelWindowsTargetTest.ts | 49 +++++++++++++++++++ .../src/windows/squirrelWindowsToolsetTest.ts | 49 +++++++++++++++++++ 6 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 packages/electron-builder-squirrel-windows/src/toolset.ts create mode 100644 test/src/windows/squirrelWindowsTargetTest.ts create mode 100644 test/src/windows/squirrelWindowsToolsetTest.ts diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index d644de8947a..070716ecbaf 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -1,6 +1,5 @@ import { InvalidConfigurationError, log, isEmptyOrSpaces, exists } from "builder-util" import { execWine } from "app-builder-lib/out/wine" -import { getBinFromUrl } from "app-builder-lib/out/binDownload" import { sanitizeFileName } from "builder-util/out/filename" import { Arch, getArchSuffix, SquirrelWindowsOptions, Target, WinPackager } from "app-builder-lib" import { withToolsetLock } from "app-builder-lib/out/util/toolsetLock" @@ -8,6 +7,7 @@ import * as path from "path" import * as fs from "fs" import * as os from "os" import { InstallerOptions, createWindowsInstaller, convertVersion } from "./windowsInstaller" +import { getSquirrelToolsetPath } from "./toolset" export default class SquirrelWindowsTarget extends Target { //tslint:disable-next-line:no-object-literal-type-assertion @@ -33,18 +33,8 @@ export default class SquirrelWindowsTarget extends Target { log.warn({ customSquirrelVendorDirectory }, "unable to access custom Squirrel.Windows vendor directory, falling back to default vendor") } - const [squirrelBin, wixBin] = await Promise.all([ - getBinFromUrl("squirrel.windows@1.0.0", "squirrel.windows-2.0.1-patched.7z", "76851f0c192eaf9bc6f8f3eecdfe325857ebe70d7833ec62ed846a1acd50c846"), - getBinFromUrl( - "wix@1.0.0", - "wix-4.0.6.tar.gz", - "e84c8f37bdfd833b6c7d92489ca4102f001517e9dd50a1204146d616f6fdc611e2049c79f13325dbb491454d03247847ed426d8b4ad8556693c72eb95f08cde0" - ), - ]) - - // WiX tools first (lower precedence), then Squirrel executables overlay - await fs.promises.cp(wixBin, tmpVendorDirectory, { recursive: true }) - await fs.promises.cp(path.join(squirrelBin, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) + const squirrelToolset = await getSquirrelToolsetPath() + await fs.promises.cp(path.join(squirrelToolset, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) } const files = await fs.promises.readdir(tmpVendorDirectory) @@ -60,7 +50,7 @@ export default class SquirrelWindowsTarget extends Target { } private assertShellSafePath(filePath: string, description: string): void { - if (/[\r\n`$;&|<>]/.test(filePath)) { + if (/[\r\n\0`$;&|<>]/.test(filePath)) { throw new InvalidConfigurationError(`${description} contains unsafe shell characters: ${filePath}`) } } @@ -95,7 +85,12 @@ export default class SquirrelWindowsTarget extends Target { throw new InvalidConfigurationError(`${description} contains invalid path segments`) } canonicalTargetPath = path.resolve(canonicalTargetParent, relativeFromResolvedParent) - } catch { + } catch (e: unknown) { + // Only fall back for filesystem errors (target parent not found, permission, etc.). + // Validation errors must propagate so callers get an actionable message. + if (e instanceof InvalidConfigurationError) { + throw e + } canonicalTargetPath = resolvedTargetPath } } @@ -257,7 +252,7 @@ export default class SquirrelWindowsTarget extends Target { } } - checkConflictingOptions(this.options) + normalizeSquirrelOptions(this.options) const appInfo = packager.appInfo const description = isEmptyOrSpaces(appInfo.description) ? (this.options.name || appInfo.productName) : appInfo.description @@ -310,7 +305,7 @@ export default class SquirrelWindowsTarget extends Target { } } -function checkConflictingOptions(options: any) { +function normalizeSquirrelOptions(options: any) { for (const name of ["outputDirectory", "appDirectory", "exe", "fixUpPaths", "usePackageJson", "extraFileSpecs", "extraMetadataSpecs", "skipUpdateIcon", "setupExe"]) { if (name in options) { throw new InvalidConfigurationError(`Option ${name} is ignored, do not specify it.`) diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts new file mode 100644 index 00000000000..19414b4bdf4 --- /dev/null +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -0,0 +1,29 @@ +import { downloadBuilderToolset } from "app-builder-lib/out/util/electronGet" +import { resolveEnvToolsetPath } from "builder-util" + +const squirrelWindowsChecksums = { + // TODO: update after the squirrel.windows@1.0.0 release is officially published to + // electron-userland/electron-builder-binaries. This value was computed from a local + // CI artifact build and may differ from the final published archive. + "squirrel.windows-2.0.1-patched.tar.gz": "140f625bd26e5022c333993de580fb028033d4940e368bc41960329f92ff6334455dde53cb0834f9529a871e86700abbbaaa681aece40892675b2b25bb22e870", +} as const + +/** + * Returns the path to the squirrel.windows toolset directory. The directory + * contains an `electron-winstaller/vendor/` subtree with all vendor executables + * (Squirrel.exe, nuget.exe, 7z, etc.). + * + * Override with ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR for local development or + * to pin a custom build. + */ +export async function getSquirrelToolsetPath(): Promise { + const envPath = await resolveEnvToolsetPath("ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR", "directory") + if (envPath != null) { + return envPath + } + return downloadBuilderToolset({ + releaseName: "squirrel.windows@1.0.0", + filenameWithExt: "squirrel.windows-2.0.1-patched.tar.gz", + checksums: squirrelWindowsChecksums, + }) +} diff --git a/test/src/electronGetTest.ts b/test/src/electronGetTest.ts index 737f00ca080..bbdcc786b48 100644 --- a/test/src/electronGetTest.ts +++ b/test/src/electronGetTest.ts @@ -293,6 +293,7 @@ describe("downloadBuilderToolset", { sequential: true }, () => { downloadBuilderToolset({ releaseName: "dmg-builder@1.2.2", filenameWithExt: "dmgbuild-bundle-arm64-75c8a6c.tar.gz", + checksums: {}, }) ).rejects.toThrow("mock-stop") diff --git a/test/src/windows/squirrelWindowsSecurityTest.ts b/test/src/windows/squirrelWindowsSecurityTest.ts index fae15922877..76b5f0b26f1 100644 --- a/test/src/windows/squirrelWindowsSecurityTest.ts +++ b/test/src/windows/squirrelWindowsSecurityTest.ts @@ -13,6 +13,7 @@ describe("SquirrelWindowsTarget.assertShellSafePath", () => { test.each([ ["newline \\n", "C:\\foo\nbar"], ["carriage return \\r", "C:\\foo\rbar"], + ["null byte \\0", "C:\\foo\0bar"], ["backtick", "C:\\foo`bar"], ["dollar sign", "C:\\foo$bar"], ["semicolon", "C:\\foo;bar"], @@ -63,4 +64,12 @@ describe("SquirrelWindowsTarget.ensurePathInside", () => { test("rejects path containing shell-unsafe characters", async () => { await expect(t.ensurePathInside(base, path.join(base, "file$evil.exe"), "file")).rejects.toThrow("unsafe shell characters") }) + + test("rejects a non-existent path whose filename contains a null byte", async () => { + // The target does not exist (parent base does), so we enter the parent-resolution + // branch. The null byte in the relative segment triggers inner path validation, + // and the assertShellSafePath guard also catches it — both layers must reject this. + const nullByteTarget = path.join(base, "file\0evil.exe") + await expect(t.ensurePathInside(base, nullByteTarget, "file")).rejects.toThrow() + }) }) diff --git a/test/src/windows/squirrelWindowsTargetTest.ts b/test/src/windows/squirrelWindowsTargetTest.ts new file mode 100644 index 00000000000..39189e68adb --- /dev/null +++ b/test/src/windows/squirrelWindowsTargetTest.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest" +import SquirrelWindowsTarget from "electron-builder-squirrel-windows/out/SquirrelWindowsTarget" +import { mkdtemp, readFile, rm, writeFile } from "fs/promises" +import { arch as osArch } from "os" +import * as path from "path" +import { tmpdir } from "os" + +describe("SquirrelWindowsTarget.select7zipArch", () => { + let tmpDir: string + let t: any + + beforeEach(async () => { + tmpDir = await mkdtemp(path.join(tmpdir(), "eb-7z-test-")) + t = Object.create(SquirrelWindowsTarget.prototype) + }) + + afterEach(() => rm(tmpDir, { recursive: true, force: true }).catch(() => {})) + + test("copies arch-specific 7z.exe and 7z.dll when both are present", async () => { + const arch = osArch() + await writeFile(path.join(tmpDir, `7z-${arch}.exe`), "exe-content") + await writeFile(path.join(tmpDir, `7z-${arch}.dll`), "dll-content") + + t.select7zipArch(tmpDir) + + expect((await readFile(path.join(tmpDir, "7z.exe"), "utf8"))).toBe("exe-content") + expect((await readFile(path.join(tmpDir, "7z.dll"), "utf8"))).toBe("dll-content") + }) + + test("skips copy and leaves existing 7z.exe unchanged when arch-specific exe is absent", async () => { + await writeFile(path.join(tmpDir, "7z.exe"), "original-exe") + + t.select7zipArch(tmpDir) + + expect((await readFile(path.join(tmpDir, "7z.exe"), "utf8"))).toBe("original-exe") + }) + + test("copies exe but skips dll when only arch-specific exe is present", async () => { + const arch = osArch() + await writeFile(path.join(tmpDir, `7z-${arch}.exe`), "exe-only") + await writeFile(path.join(tmpDir, "7z.exe"), "original-exe") + + t.select7zipArch(tmpDir) + + expect((await readFile(path.join(tmpDir, "7z.exe"), "utf8"))).toBe("exe-only") + // 7z.dll was never created — should still not exist + await expect(readFile(path.join(tmpDir, "7z.dll"), "utf8")).rejects.toThrow() + }) +}) diff --git a/test/src/windows/squirrelWindowsToolsetTest.ts b/test/src/windows/squirrelWindowsToolsetTest.ts new file mode 100644 index 00000000000..6c8e478e15a --- /dev/null +++ b/test/src/windows/squirrelWindowsToolsetTest.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest" +import { getSquirrelToolsetPath } from "electron-builder-squirrel-windows/out/toolset" +import { mkdtemp, rm, writeFile } from "fs/promises" +import { tmpdir } from "os" +import * as path from "path" + +const ENV_KEY = "ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR" + +describe("getSquirrelToolsetPath", () => { + let savedEnv: string | undefined + let tmpDir: string + + beforeEach(async () => { + savedEnv = process.env[ENV_KEY] + tmpDir = await mkdtemp(path.join(tmpdir(), "eb-squirrel-toolset-test-")) + }) + + afterEach(async () => { + if (savedEnv === undefined) { + delete process.env[ENV_KEY] + } else { + process.env[ENV_KEY] = savedEnv + } + await rm(tmpDir, { recursive: true, force: true }).catch(() => {}) + }) + + test("returns env var path when set to an existing directory", async () => { + process.env[ENV_KEY] = tmpDir + const result = await getSquirrelToolsetPath() + expect(result).toBe(tmpDir) + }) + + test("throws when env var points to a non-existent path", async () => { + process.env[ENV_KEY] = path.join(tmpDir, "does-not-exist") + await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) + }) + + test("throws when env var points to a file instead of a directory", async () => { + const filePath = path.join(tmpDir, "not-a-dir.txt") + await writeFile(filePath, "") + process.env[ENV_KEY] = filePath + await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) + }) + + test("throws when env var is a relative path", async () => { + process.env[ENV_KEY] = "relative/path" + await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) + }) +}) From 9d475414f52f4769a09014547cf3b1b97cb83ab5 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 4 Jun 2026 08:59:19 -0700 Subject: [PATCH 05/20] update with published electron-builder-binaries toolset --- .../src/SquirrelWindowsTarget.ts | 2 +- .../src/toolset.ts | 14 +++++++------- .../src/windowsInstaller.ts | 18 +++++++++++++++++- test/src/windows/squirrelWindowsTargetTest.ts | 8 ++++---- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 070716ecbaf..0951eb8b807 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -255,7 +255,7 @@ export default class SquirrelWindowsTarget extends Target { normalizeSquirrelOptions(this.options) const appInfo = packager.appInfo - const description = isEmptyOrSpaces(appInfo.description) ? (this.options.name || appInfo.productName) : appInfo.description + const description = isEmptyOrSpaces(appInfo.description) ? this.options.name || appInfo.productName : appInfo.description let remoteReleases: string | undefined if (this.options.remoteReleases === true) { diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index 19414b4bdf4..f086310e265 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -2,10 +2,9 @@ import { downloadBuilderToolset } from "app-builder-lib/out/util/electronGet" import { resolveEnvToolsetPath } from "builder-util" const squirrelWindowsChecksums = { - // TODO: update after the squirrel.windows@1.0.0 release is officially published to - // electron-userland/electron-builder-binaries. This value was computed from a local - // CI artifact build and may differ from the final published archive. - "squirrel.windows-2.0.1-patched.tar.gz": "140f625bd26e5022c333993de580fb028033d4940e368bc41960329f92ff6334455dde53cb0834f9529a871e86700abbbaaa681aece40892675b2b25bb22e870", + "1.1.0": { + "squirrel.windows-2.0.1-patched.zip": "86e6c3e9ebf10e29cfde99dfff98f3738c29c9562495c540270129ffde1f79cf", + }, } as const /** @@ -21,9 +20,10 @@ export async function getSquirrelToolsetPath(): Promise { if (envPath != null) { return envPath } + const toolset = "1.1.0" return downloadBuilderToolset({ - releaseName: "squirrel.windows@1.0.0", - filenameWithExt: "squirrel.windows-2.0.1-patched.tar.gz", - checksums: squirrelWindowsChecksums, + releaseName: `squirrel.windows@${toolset}`, + filenameWithExt: "squirrel.windows-2.0.1-patched.zip", + checksums: squirrelWindowsChecksums[toolset], }) } diff --git a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts index 0cff051031a..20b7f063bcf 100644 --- a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts +++ b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts @@ -60,7 +60,23 @@ export interface InstallerOptions { } export async function createWindowsInstaller(options: InstallerOptions): Promise { - const { appDirectory, outputDirectory, vendorDirectory, name, version, exe, nuspecTemplate, loadingGif, noMsi, remoteReleases, remoteToken, setupExe, setupMsi, fixUpPaths = false, createTempDir } = options + const { + appDirectory, + outputDirectory, + vendorDirectory, + name, + version, + exe, + nuspecTemplate, + loadingGif, + noMsi, + remoteReleases, + remoteToken, + setupExe, + setupMsi, + fixUpPaths = false, + createTempDir, + } = options const useMono = process.platform !== "win32" if (useMono) { diff --git a/test/src/windows/squirrelWindowsTargetTest.ts b/test/src/windows/squirrelWindowsTargetTest.ts index 39189e68adb..648828c81a9 100644 --- a/test/src/windows/squirrelWindowsTargetTest.ts +++ b/test/src/windows/squirrelWindowsTargetTest.ts @@ -23,8 +23,8 @@ describe("SquirrelWindowsTarget.select7zipArch", () => { t.select7zipArch(tmpDir) - expect((await readFile(path.join(tmpDir, "7z.exe"), "utf8"))).toBe("exe-content") - expect((await readFile(path.join(tmpDir, "7z.dll"), "utf8"))).toBe("dll-content") + expect(await readFile(path.join(tmpDir, "7z.exe"), "utf8")).toBe("exe-content") + expect(await readFile(path.join(tmpDir, "7z.dll"), "utf8")).toBe("dll-content") }) test("skips copy and leaves existing 7z.exe unchanged when arch-specific exe is absent", async () => { @@ -32,7 +32,7 @@ describe("SquirrelWindowsTarget.select7zipArch", () => { t.select7zipArch(tmpDir) - expect((await readFile(path.join(tmpDir, "7z.exe"), "utf8"))).toBe("original-exe") + expect(await readFile(path.join(tmpDir, "7z.exe"), "utf8")).toBe("original-exe") }) test("copies exe but skips dll when only arch-specific exe is present", async () => { @@ -42,7 +42,7 @@ describe("SquirrelWindowsTarget.select7zipArch", () => { t.select7zipArch(tmpDir) - expect((await readFile(path.join(tmpDir, "7z.exe"), "utf8"))).toBe("exe-only") + expect(await readFile(path.join(tmpDir, "7z.exe"), "utf8")).toBe("exe-only") // 7z.dll was never created — should still not exist await expect(readFile(path.join(tmpDir, "7z.dll"), "utf8")).rejects.toThrow() }) From fc7980d61950740d99339a20b8e369334048590e Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 4 Jun 2026 10:59:48 -0700 Subject: [PATCH 06/20] temp toolset test from electron-builder-binaries --- .../src/toolset.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index f086310e265..f5373033d2f 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -1,9 +1,11 @@ import { downloadBuilderToolset } from "app-builder-lib/out/util/electronGet" import { resolveEnvToolsetPath } from "builder-util" -const squirrelWindowsChecksums = { - "1.1.0": { - "squirrel.windows-2.0.1-patched.zip": "86e6c3e9ebf10e29cfde99dfff98f3738c29c9562495c540270129ffde1f79cf", +export const squirrelWindowsChecksums = { + // 1.1.0 bundled the Chocolatey shim instead of the standalone NuGet.CommandLine portable exe. + // 1.1.1 fixes that; checksum will be set once the new archive is published. + "1.1.1": { + "squirrel.windows-2.0.1-patched.zip": "TODO-update-after-squirrel.windows@1.1.1-is-published", }, } as const @@ -20,10 +22,21 @@ export async function getSquirrelToolsetPath(): Promise { if (envPath != null) { return envPath } - const toolset = "1.1.0" - return downloadBuilderToolset({ - releaseName: `squirrel.windows@${toolset}`, + const tempUpgrade = { filenameWithExt: "squirrel.windows-2.0.1-patched.zip", - checksums: squirrelWindowsChecksums[toolset], + checksums: { + "squirrel.windows-2.0.1-patched.zip": "00ce495de66c4e985474e58e07c2713f07db955c8da3f90a115e66aa31fb4ffe", + }, + overrideUrl: "https://github.com/electron-userland/electron-builder-binaries/actions/runs/26968100864/artifacts/7418460150", + } + return downloadBuilderToolset({ + releaseName: "squirrel.windows@1.1.1-temp-upgrade", + ...tempUpgrade, }) + // const toolset = "1.1.1" + // return downloadBuilderToolset({ + // releaseName: `squirrel.windows@${toolset}`, + // filenameWithExt: "squirrel.windows-2.0.1-patched.zip", + // checksums: squirrelWindowsChecksums[toolset], + // }) } From b73ca20740cb13fdba99fd86261e84dfd4aa0a92 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 4 Jun 2026 15:43:20 -0700 Subject: [PATCH 07/20] temporarily add logic to insert build job artifact via env var into test flow --- .github/workflows/test.yaml | 37 ++++++++++++++++++- .../src/toolset.ts | 21 +++-------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e96adfb60c7..e825d300513 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -168,12 +168,31 @@ jobs: env: CSC_KEY_PASSWORD_SECRET: ${{ secrets.WIN_CSC_KEY_PASSWORD }} + - name: Provision squirrel.windows toolset + run: | + # squirrel.windows@1.1.0 bundles the Chocolatey shim for nuget.exe which fails when + # relocated. Download the published bundle and replace the shim with a standalone + # portable nuget.exe (the mono-compatible PE32 from dist.nuget.org, CI-only download). + TOOLSET_DIR="${{ runner.temp }}/squirrel-toolset" + mkdir -p "$TOOLSET_DIR" + curl -fsSL -o "${{ runner.temp }}/squirrel.zip" \ + "https://github.com/electron-userland/electron-builder-binaries/releases/download/squirrel.windows%401.1.0/squirrel.windows-2.0.1-patched.zip" + unzip -q "${{ runner.temp }}/squirrel.zip" -d "$TOOLSET_DIR" + curl -fsSL -o "$TOOLSET_DIR/electron-winstaller/vendor/nuget.exe" \ + "https://dist.nuget.org/win-x86-commandline/v6.14.0/nuget.exe" + echo "SQUIRREL_TOOLSET_HOST_DIR=$TOOLSET_DIR" >> "$GITHUB_ENV" + - name: Run shard in docker run: | echo $TEST_RUNNER_IMAGE_TAG + export ADDITIONAL_DOCKER_ARGS="--env-file ${{ runner.temp }}/docker-secrets.env \ + -e VITEST_SHARD_INDEX=${{ matrix.shard }} \ + -e VITEST_SMART_CACHE_FILE=./test/vitest-scripts/_vitest-smart-cache.json \ + -e RESET_VITEST_SHARD_CACHE=${{ inputs.reset-vitest-smart-cache }} \ + -v ${SQUIRREL_TOOLSET_HOST_DIR}:/squirrel-toolset:ro \ + -e ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR=/squirrel-toolset" pnpm test-linux env: - ADDITIONAL_DOCKER_ARGS: "--env-file ${{ runner.temp }}/docker-secrets.env -e VITEST_SHARD_INDEX=${{ matrix.shard }} -e VITEST_SMART_CACHE_FILE=./test/vitest-scripts/_vitest-smart-cache.json -e RESET_VITEST_SHARD_CACHE=${{ inputs.reset-vitest-smart-cache }}" TEST_RUNNER_IMAGE_TAG: electronuserland/builder:${{ env.TEST_IMAGE_NODE_MAJOR_VERSION }}-wine-mono - name: Upload smart cache artifact @@ -218,6 +237,22 @@ jobs: # the CLR create its hashed subdirectory without error. New-Item -ItemType Directory -Force -Path "$env:LOCALAPPDATA\IsolatedStorage" | Out-Null + - name: Provision squirrel.windows toolset + shell: pwsh + run: | + # squirrel.windows@1.1.0 bundles the Chocolatey shim for nuget.exe. The shim resolves the + # real binary relative to its Chocolatey install path and fails when copied to a temp dir. + # Download the published bundle, then replace the shim with the standalone portable exe. + $toolsetDir = "$env:RUNNER_TEMP\squirrel-toolset" + $zipPath = "$env:RUNNER_TEMP\squirrel.zip" + Invoke-WebRequest ` + -Uri "https://github.com/electron-userland/electron-builder-binaries/releases/download/squirrel.windows%401.1.0/squirrel.windows-2.0.1-patched.zip" ` + -OutFile $zipPath + Expand-Archive -Path $zipPath -DestinationPath $toolsetDir -Force + $realNuget = Join-Path $env:ChocolateyInstall "lib\NuGet.CommandLine\tools\nuget.exe" + Copy-Item $realNuget "$toolsetDir\electron-winstaller\vendor\nuget.exe" -Force + "ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR=$toolsetDir" >> $env:GITHUB_ENV + - name: Run shard run: pnpm ci:test env: diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index f5373033d2f..14e64349f2d 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -3,7 +3,7 @@ import { resolveEnvToolsetPath } from "builder-util" export const squirrelWindowsChecksums = { // 1.1.0 bundled the Chocolatey shim instead of the standalone NuGet.CommandLine portable exe. - // 1.1.1 fixes that; checksum will be set once the new archive is published. + // 1.1.1 fixes that; update after squirrel.windows@1.1.1 is published to electron-builder-binaries. "1.1.1": { "squirrel.windows-2.0.1-patched.zip": "TODO-update-after-squirrel.windows@1.1.1-is-published", }, @@ -22,21 +22,10 @@ export async function getSquirrelToolsetPath(): Promise { if (envPath != null) { return envPath } - const tempUpgrade = { - filenameWithExt: "squirrel.windows-2.0.1-patched.zip", - checksums: { - "squirrel.windows-2.0.1-patched.zip": "00ce495de66c4e985474e58e07c2713f07db955c8da3f90a115e66aa31fb4ffe", - }, - overrideUrl: "https://github.com/electron-userland/electron-builder-binaries/actions/runs/26968100864/artifacts/7418460150", - } + const toolset = "1.1.1" return downloadBuilderToolset({ - releaseName: "squirrel.windows@1.1.1-temp-upgrade", - ...tempUpgrade, + releaseName: `squirrel.windows@${toolset}`, + filenameWithExt: "squirrel.windows-2.0.1-patched.zip", + checksums: squirrelWindowsChecksums[toolset], }) - // const toolset = "1.1.1" - // return downloadBuilderToolset({ - // releaseName: `squirrel.windows@${toolset}`, - // filenameWithExt: "squirrel.windows-2.0.1-patched.zip", - // checksums: squirrelWindowsChecksums[toolset], - // }) } From 2d1458844ae6eaa4b0b51e7486885e06a1b65f77 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Thu, 4 Jun 2026 17:14:55 -0700 Subject: [PATCH 08/20] try again, use rcedit toolset bundle to merge into the vendor directory --- .github/workflows/test.yaml | 9 +++++---- .../src/SquirrelWindowsTarget.ts | 11 +++++++++++ .../electron-builder-squirrel-windows/src/toolset.ts | 10 ++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e825d300513..c3103320945 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -171,14 +171,15 @@ jobs: - name: Provision squirrel.windows toolset run: | # squirrel.windows@1.1.0 bundles the Chocolatey shim for nuget.exe which fails when - # relocated. Download the published bundle and replace the shim with a standalone - # portable nuget.exe (the mono-compatible PE32 from dist.nuget.org, CI-only download). + # relocated. Replace the shim with a standalone portable nuget.exe (CI-only download). + # rcedit.exe is supplied at runtime from the win-codesign toolset by SquirrelWindowsTarget. TOOLSET_DIR="${{ runner.temp }}/squirrel-toolset" + VENDOR_DIR="$TOOLSET_DIR/electron-winstaller/vendor" mkdir -p "$TOOLSET_DIR" curl -fsSL -o "${{ runner.temp }}/squirrel.zip" \ "https://github.com/electron-userland/electron-builder-binaries/releases/download/squirrel.windows%401.1.0/squirrel.windows-2.0.1-patched.zip" unzip -q "${{ runner.temp }}/squirrel.zip" -d "$TOOLSET_DIR" - curl -fsSL -o "$TOOLSET_DIR/electron-winstaller/vendor/nuget.exe" \ + curl -fsSL -o "$VENDOR_DIR/nuget.exe" \ "https://dist.nuget.org/win-x86-commandline/v6.14.0/nuget.exe" echo "SQUIRREL_TOOLSET_HOST_DIR=$TOOLSET_DIR" >> "$GITHUB_ENV" @@ -242,7 +243,7 @@ jobs: run: | # squirrel.windows@1.1.0 bundles the Chocolatey shim for nuget.exe. The shim resolves the # real binary relative to its Chocolatey install path and fails when copied to a temp dir. - # Download the published bundle, then replace the shim with the standalone portable exe. + # rcedit.exe is supplied at runtime from the win-codesign toolset by SquirrelWindowsTarget. $toolsetDir = "$env:RUNNER_TEMP\squirrel-toolset" $zipPath = "$env:RUNNER_TEMP\squirrel.zip" Invoke-WebRequest ` diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 9b23873d4a1..47b9bdafecf 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -1,4 +1,5 @@ import { Arch, SquirrelWindowsOptions, Target, WinPackager, getArchSuffix } from "app-builder-lib" +import { getRceditBundle } from "app-builder-lib/out/toolsets/windows" import { withToolsetLock } from "app-builder-lib/out/util/toolsetLock" import { WineVmManager } from "app-builder-lib/out/vm/WineVm" import { InvalidConfigurationError, exists, isEmptyOrSpaces, log } from "builder-util" @@ -37,6 +38,16 @@ export default class SquirrelWindowsTarget extends Target { await fs.promises.cp(path.join(squirrelToolset, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) } + // Squirrel.exe --releasify calls rcedit.exe (via setPEVersionInfoAndIcon) to embed the app + // icon into Setup.exe. Resolve it from the win-codesign toolset which already versions and + // caches it across all platforms, rather than duplicating it in the squirrel.windows bundle. + // Squirrel-Mono.exe (used on non-Windows) does not call rcedit, so skip on other platforms. + if (process.platform === "win32") { + const rcedit = await getRceditBundle(this.packager.config.toolsets?.winCodeSign) + const rceditExe = os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 + await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) + } + const files = await fs.promises.readdir(tmpVendorDirectory) const squirrelExe = files.find(f => f === "Squirrel.exe") if (squirrelExe) { diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index 14e64349f2d..fcadb95779a 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -2,8 +2,10 @@ import { downloadBuilderToolset } from "app-builder-lib/out/util/electronGet" import { resolveEnvToolsetPath } from "builder-util" export const squirrelWindowsChecksums = { - // 1.1.0 bundled the Chocolatey shim instead of the standalone NuGet.CommandLine portable exe. - // 1.1.1 fixes that; update after squirrel.windows@1.1.1 is published to electron-builder-binaries. + "1.1.0": { + "squirrel.windows-2.0.1-patched.zip": "86e6c3e9ebf10e29cfde99dfff98f3738c29c9562495c540270129ffde1f79cf", + }, + // 1.1.1 will add rcedit.exe to the vendor directory directly; update after it is published. "1.1.1": { "squirrel.windows-2.0.1-patched.zip": "TODO-update-after-squirrel.windows@1.1.1-is-published", }, @@ -12,7 +14,7 @@ export const squirrelWindowsChecksums = { /** * Returns the path to the squirrel.windows toolset directory. The directory * contains an `electron-winstaller/vendor/` subtree with all vendor executables - * (Squirrel.exe, nuget.exe, 7z, etc.). + * (Squirrel.exe, nuget.exe, rcedit.exe, 7z, etc.). * * Override with ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR for local development or * to pin a custom build. @@ -22,7 +24,7 @@ export async function getSquirrelToolsetPath(): Promise { if (envPath != null) { return envPath } - const toolset = "1.1.1" + const toolset = "1.1.0" return downloadBuilderToolset({ releaseName: `squirrel.windows@${toolset}`, filenameWithExt: "squirrel.windows-2.0.1-patched.zip", From 4e3d038a931d722f82ba614d2c0e17874863ee5a Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 15:26:39 -0700 Subject: [PATCH 09/20] feat(squirrel-windows)!: remove customSquirrelVendorDir; offline nuget + default spinner - BREAKING: remove squirrelWindows.customSquirrelVendorDir. The vendor toolset source is overridden via ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR (same nuget/rcedit provisioning). prepareSignedVendorDirectory collapses to one code path. - nuget download is offline/mirror-friendly: skips when a real (multi-MB) nuget.exe is already present (future bundle / pre-staged toolset), and is redirectable via ELECTRON_BUILDER_SQUIRREL_NUGET_URL (+ _SHA256). Adds a network-free skip test. - bundle install-spinner.gif and use it as the loadingGif fallback, restoring electron-winstaller parity (default fresh-install spinner). - regenerate scheme.json; changeset bumped to major with the breaking note. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/remove-electron-winstaller-dep.md | 6 +- packages/app-builder-lib/scheme.json | 4 -- .../src/options/SquirrelWindowsOptions.ts | 6 -- .../install-spinner.gif | Bin 0 -> 44410 bytes .../package.json | 3 +- .../src/SquirrelWindowsTarget.ts | 62 ++++++++---------- .../src/toolset.ts | 34 ++++++++-- .../src/windows/squirrelWindowsToolsetTest.ts | 28 +++++++- 8 files changed, 91 insertions(+), 52 deletions(-) create mode 100644 packages/electron-builder-squirrel-windows/install-spinner.gif diff --git a/.changeset/remove-electron-winstaller-dep.md b/.changeset/remove-electron-winstaller-dep.md index 9f9f091aaef..ce5b196b6ce 100644 --- a/.changeset/remove-electron-winstaller-dep.md +++ b/.changeset/remove-electron-winstaller-dep.md @@ -1,5 +1,7 @@ --- -"electron-builder-squirrel-windows": minor +"electron-builder-squirrel-windows": major --- -feat(squirrel-windows): inline installer logic; remove electron-winstaller dependency and its vendored binaries +feat(squirrel-windows)!: inline installer logic; remove electron-winstaller dependency and its vendored binaries + +BREAKING: the `squirrelWindows.customSquirrelVendorDir` option has been removed. Override the vendor toolset source with the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` environment variable instead (it goes through the same nuget/rcedit provisioning). diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index a44267e0bc4..66b3e1519a8 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -7866,10 +7866,6 @@ "string" ] }, - "customSquirrelVendorDir": { - "description": "The custom squirrel vendor dir. If not specified will use the Squirrel.Windows that is shipped with electron-installer(https://github.com/electron/windows-installer/tree/main/vendor).\nAfter https://github.com/electron-userland/electron-builder-binaries/pull/56 merged, will add `electron-builder-binaries` to get the latest version of squirrel.", - "type": "string" - }, "iconUrl": { "description": "A URL to an ICO file to use as the application icon (displayed in Control Panel > Programs and Features). Defaults to the Electron icon.\n\nPlease note — [local icon file url is not accepted](https://github.com/atom/grunt-electron-installer/issues/73), must be https/http.\n\nIf you don't plan to build windows installer, you can omit it.\nIf your project repository is public on GitHub, it will be `https://github.com/${u}/${p}/blob/master/build/icon.ico?raw=true` by default.", "type": [ diff --git a/packages/app-builder-lib/src/options/SquirrelWindowsOptions.ts b/packages/app-builder-lib/src/options/SquirrelWindowsOptions.ts index c9176c70e7f..64a90a46c67 100644 --- a/packages/app-builder-lib/src/options/SquirrelWindowsOptions.ts +++ b/packages/app-builder-lib/src/options/SquirrelWindowsOptions.ts @@ -40,12 +40,6 @@ export interface SquirrelWindowsOptions extends TargetSpecificOptions { */ readonly useAppIdAsId?: boolean - /** - * The custom squirrel vendor dir. If not specified will use the Squirrel.Windows that is shipped with electron-installer(https://github.com/electron/windows-installer/tree/main/vendor). - * After https://github.com/electron-userland/electron-builder-binaries/pull/56 merged, will add `electron-builder-binaries` to get the latest version of squirrel. - */ - readonly customSquirrelVendorDir?: string - /** * https://github.com/electron-userland/electron-builder/issues/1743 * @private diff --git a/packages/electron-builder-squirrel-windows/install-spinner.gif b/packages/electron-builder-squirrel-windows/install-spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..9be998f3d336e896adebeffca103e293e91a1044 GIT binary patch literal 44410 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.12.0" diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index be04cf6c121..90a43a83eca 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -1,6 +1,6 @@ import { Arch, getArchSuffix, SquirrelWindowsOptions, Target, WinPackager } from "app-builder-lib" import { getRceditBundle, withToolsetLock, WineVmManager } from "app-builder-lib/internal" -import { InvalidConfigurationError, exists, isEmptyOrSpaces, log } from "builder-util" +import { InvalidConfigurationError, isEmptyOrSpaces, log } from "builder-util" import { sanitizeFileName } from "builder-util/internal" import * as fs from "fs" import * as os from "os" @@ -22,38 +22,31 @@ export default class SquirrelWindowsTarget extends Target { } private async prepareSignedVendorDirectory(): Promise { - const customSquirrelVendorDirectory = this.options.customSquirrelVendorDir const tmpVendorDirectory = await this.packager.tempDirManager.createTempDir({ prefix: "squirrel-windows-vendor" }) - if (customSquirrelVendorDirectory && (await exists(customSquirrelVendorDirectory))) { - await fs.promises.cp(customSquirrelVendorDirectory, tmpVendorDirectory, { recursive: true }) - } else { - if (!isEmptyOrSpaces(customSquirrelVendorDirectory)) { - log.warn({ customSquirrelVendorDirectory }, "unable to access custom Squirrel.Windows vendor directory, falling back to default vendor") - } - - const squirrelToolset = await getSquirrelToolsetPath() - await fs.promises.cp(path.join(squirrelToolset, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) - - // TEMPORARY: the published squirrel.windows@1.1.0 bundle ships the Chocolatey shim for nuget.exe, - // which resolves the real binary relative to its own install path and fails once relocated to a - // temp vendor directory. Overwrite it with a standalone portable nuget.exe, downloaded and cached - // at runtime. Remove once squirrel.windows bundles the standalone exe (electron-builder-binaries#203). - await prepareNugetExe(tmpVendorDirectory) - - // Squirrel.exe --releasify shells out to rcedit.exe (via setPEVersionInfoAndIcon) to embed the app - // icon into Setup.exe. The bundle does not ship rcedit, so resolve it from the win-codesign toolset, - // which already versions and caches it across all platforms. Squirrel-Mono.exe (used on non-Windows) - // does not call rcedit, so this is only needed on win32. - // - // getRceditBundle honors the user's `toolsets.winCodeSign` selection, so users retain a bypass if a - // newer toolset regresses: `"0.0.0"` falls back to the legacy winCodeSign-2.6.0 rcedit, a custom - // toolset object uses their own rcedit, and the default (unset/"latest") pulls rcedit-windows-2_0_0. - if (process.platform === "win32") { - const rcedit = await getRceditBundle(this.packager.config.toolsets?.winCodeSign, this.packager.buildResourcesDir) - const rceditExe = os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 - await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) - } + // The vendor toolset comes from the maintained squirrel.windows bundle. Override the bundle source + // (e.g. a local copy for air-gapped builds) with ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR. + const squirrelToolset = await getSquirrelToolsetPath() + await fs.promises.cp(path.join(squirrelToolset, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) + + // TEMPORARY: the published squirrel.windows@1.1.0 bundle ships the Chocolatey shim for nuget.exe, + // which resolves the real binary relative to its own install path and fails once relocated to a + // temp vendor directory. Replace it with a standalone portable nuget.exe (no-op when the bundle + // already ships a real one). Remove once squirrel.windows bundles it (electron-builder-binaries#203). + await prepareNugetExe(tmpVendorDirectory) + + // Squirrel.exe --releasify shells out to rcedit.exe (via setPEVersionInfoAndIcon) to embed the app + // icon into Setup.exe. The bundle does not ship rcedit, so resolve it from the win-codesign toolset, + // which already versions and caches it across all platforms. Squirrel-Mono.exe (used on non-Windows) + // does not call rcedit, so this is only needed on win32. + // + // getRceditBundle honors the user's `toolsets.winCodeSign` selection, so users retain a bypass if a + // newer toolset regresses: `"0.0.0"` falls back to the legacy winCodeSign-2.6.0 rcedit, a custom + // toolset object uses their own rcedit, and the default (unset/"latest") pulls rcedit-windows-2_0_0. + if (process.platform === "win32") { + const rcedit = await getRceditBundle(this.packager.config.toolsets?.winCodeSign, this.packager.buildResourcesDir) + const rceditExe = os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 + await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) } const files = await fs.promises.readdir(tmpVendorDirectory) @@ -290,11 +283,14 @@ export default class SquirrelWindowsTarget extends Target { remoteReleases = this.options.remoteReleases } - let loadingGif: string | undefined + let loadingGif: string if (this.options.loadingGif) { loadingGif = path.resolve(packager.projectDir, this.options.loadingGif) } else { - loadingGif = (await packager.getResource(undefined, "install-spinner.gif")) ?? undefined + // User-supplied buildResources gif wins; otherwise fall back to the bundled default spinner — + // parity with electron-winstaller, which always supplied resources/install-spinner.gif when none + // was configured (a fresh-install progress animation; never omitted). + loadingGif = (await packager.getResource(undefined, "install-spinner.gif")) ?? path.resolve(import.meta.dirname, "..", "install-spinner.gif") } const vendorDirectory = await this.prepareSignedVendorDirectory() diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index 4e2c0f49509..ad08e234309 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -1,5 +1,5 @@ import { download, downloadBuilderToolset } from "app-builder-lib/internal" -import { exists, log, resolveEnvShellValue } from "builder-util" +import { exists, log, parseValidEnvVarUrl, resolveEnvShellValue } from "builder-util" import { stat } from "fs/promises" import * as path from "path" @@ -63,11 +63,37 @@ export async function getSquirrelToolsetPath(): Promise { const NUGET_VERSION = "6.14.0" const NUGET_SHA256 = "92dbed160ddee0f64b901e907439e021211b428e57c089ecc12fc38dcc4bd9a5" const NUGET_URL = `https://dist.nuget.org/win-x86-commandline/v${NUGET_VERSION}/nuget.exe` +// Redirect the nuget.exe download for air-gapped / mirrored builds. +const NUGET_URL_ENV = "ELECTRON_BUILDER_SQUIRREL_NUGET_URL" +const NUGET_SHA256_ENV = "ELECTRON_BUILDER_SQUIRREL_NUGET_SHA256" +// A Chocolatey shim is ~0.4 MB; a real nuget.exe is several MB. Anything above this is already usable. +const MIN_USABLE_NUGET_BYTES = 2_000_000 + +async function isUsableNuget(file: string): Promise { + try { + return (await stat(file)).size >= MIN_USABLE_NUGET_BYTES + } catch { + return false + } +} /** - * Downloads (and caches) a standalone portable nuget.exe and writes it into `vendorDirectory`, - * overwriting the broken Chocolatey shim shipped in the squirrel.windows bundle. + * Ensures `vendorDirectory` holds a usable standalone nuget.exe. If the bundle already ships a real one + * (e.g. a future squirrel.windows release, or a pre-staged offline toolset dir), it is kept as-is. + * Otherwise a pinned NuGet.CommandLine build is downloaded and cached, replacing the broken Chocolatey + * shim. Air-gapped/mirrored builds can redirect the download with {@link NUGET_URL_ENV} (and + * {@link NUGET_SHA256_ENV} to supply a non-default build's checksum, or an empty value to skip it). */ export async function prepareNugetExe(vendorDirectory: string): Promise { - await download(NUGET_URL, path.join(vendorDirectory, "nuget.exe"), NUGET_SHA256) + const target = path.join(vendorDirectory, "nuget.exe") + if (await isUsableNuget(target)) { + return + } + const overrideUrl = parseValidEnvVarUrl(NUGET_URL_ENV) + const url = overrideUrl ?? NUGET_URL + // The pinned hash matches the official 6.14.0 build and any plain mirror of it. For a different + // build, set NUGET_SHA256_ENV to its sha256 (an empty value disables verification). + const shaOverride = overrideUrl != null ? process.env[NUGET_SHA256_ENV]?.trim() : undefined + const checksum = shaOverride !== undefined ? shaOverride || null : NUGET_SHA256 + await download(url, target, checksum) } diff --git a/test/src/windows/squirrelWindowsToolsetTest.ts b/test/src/windows/squirrelWindowsToolsetTest.ts index e6cd2bb214a..455dbd4dd60 100644 --- a/test/src/windows/squirrelWindowsToolsetTest.ts +++ b/test/src/windows/squirrelWindowsToolsetTest.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest" -import { getSquirrelToolsetPath } from "electron-builder-squirrel-windows/src/toolset" -import { mkdtemp, rm, writeFile } from "fs/promises" +import { getSquirrelToolsetPath, prepareNugetExe } from "electron-builder-squirrel-windows/src/toolset" +import { mkdtemp, rm, stat, writeFile } from "fs/promises" import { tmpdir } from "os" import * as path from "path" @@ -47,3 +47,27 @@ describe("getSquirrelToolsetPath", () => { await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) }) }) + +describe("prepareNugetExe", () => { + let tmpDir: string + + beforeEach(async () => { + tmpDir = await mkdtemp(path.join(tmpdir(), "eb-squirrel-nuget-test-")) + }) + + afterEach(() => rm(tmpDir, { recursive: true, force: true }).catch(() => {})) + + test("keeps an already-usable nuget.exe and performs no download", async () => { + // A real nuget.exe is several MB; staging one larger than the shim threshold must short-circuit + // the download entirely (this is the offline / pre-staged-toolset path, so it must not hit network). + const nugetExe = path.join(tmpDir, "nuget.exe") + await writeFile(nugetExe, Buffer.alloc(2_100_000, 1)) + const before = await stat(nugetExe) + + await prepareNugetExe(tmpDir) + + const after = await stat(nugetExe) + expect(after.size).toBe(before.size) + expect(after.mtimeMs).toBe(before.mtimeMs) + }) +}) From 0788d33bca093a0128799c7246130c914f2b2d44 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 16:10:21 -0700 Subject: [PATCH 10/20] docs(migration): document squirrelWindows.customSquirrelVendorDir removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the v27 breaking-change entry for the removed customSquirrelVendorDir option (at-a-glance row + detail section) and a manual-step bullet in the v26→v27 guide, explaining the ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR replacement, its different directory layout, and the offline nuget options. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/docs/migration/v26-to-v27.md | 1 + website/docs/migration/v27-breaking-changes.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/website/docs/migration/v26-to-v27.md b/website/docs/migration/v26-to-v27.md index d0715692de0..589c71bdc20 100644 --- a/website/docs/migration/v26-to-v27.md +++ b/website/docs/migration/v26-to-v27.md @@ -145,6 +145,7 @@ import { build } from "electron-builder" - [ ] [Replace `CI_BUILD_TAG`](./v27-breaking-changes#ci_build_tag-environment-variable) with `CI_COMMIT_TAG`. - [ ] [Replace toolset env-var overrides](./v27-breaking-changes#toolset-env-var-overrides-removed) (`APPIMAGE_TOOLS_PATH`, `ELECTRON_BUILDER_NSIS_DIR`, `USE_SYSTEM_WINE`, …) with `toolsets.X: { url, checksum }`. - [ ] [Validate toolset pins](./v27-breaking-changes#toolset-defaults-resolve-to-latest-newest-bundle) — defaults now resolve to `"latest"` (newest bundle); pin to `"0.0.0"` only if a legacy bundle is needed. +- [ ] [Remove `squirrelWindows.customSquirrelVendorDir`](./v27-breaking-changes#squirrelwindowscustomsquirrelvendordir) — the option is deleted; point at a custom Squirrel toolset with the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` env var instead (note the different directory layout). - [ ] **Plugin/custom-target authors:** [replace `packager.info.X` and `platformSpecificBuildOptions`](./v27-breaking-changes#programmatic--plugin-author-api-changes) with the new pass-through getters. --- diff --git a/website/docs/migration/v27-breaking-changes.md b/website/docs/migration/v27-breaking-changes.md index 50ab49f0e4c..e04eda5b6b4 100644 --- a/website/docs/migration/v27-breaking-changes.md +++ b/website/docs/migration/v27-breaking-changes.md @@ -61,6 +61,7 @@ To stay on a legacy bundle, pin the toolset to `"0.0.0"`. Because `winCodeSign` | [Root-level `directories` removed](#root-level-directories-in-packagejson) | ✓ | Move under `build.directories` | | [`build.helper-bundle-id` removed](#buildhelper-bundle-id) | ✓ | Moved to `mac.helperBundleId` | | [`squirrelWindows.noMsi` removed](#squirrelwindowsnomsi) | ✓ | Replaced by `msi` (inverted) | +| [`squirrelWindows.customSquirrelVendorDir` removed](#squirrelwindowscustomsquirrelvendordir) | — | Override the vendor toolset with the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` env var | | [`GithubOptions.vPrefixedTagName` removed](#githuboptions--gitlaboptions-vprefixedtagname) | ✓ | Use `tagNamePrefix` | | [`GitlabOptions.vPrefixedTagName` retained](#githuboptions--gitlaboptions-vprefixedtagname) | — | None — still functional; the migrator leaves GitLab entries untouched | | [`devMetadata` / `extraMetadata` in `PackagerOptions` removed](#devmetadata--extrametadata-programmatic-packageroptions) | — | Use `config` / `config.extraMetadata` | @@ -219,6 +220,23 @@ The `noMsi` boolean is removed in favor of its inverse, `msi`. { "build": { "squirrelWindows": { "msi": false } } } // After ``` +### `squirrelWindows.customSquirrelVendorDir` + +Removed. v27 inlines the Squirrel.Windows installer logic and drops the `electron-winstaller` npm dependency (and its vendored binaries); the Squirrel vendor toolset is now fetched from the maintained `squirrel.windows` electron-builder-binaries bundle. To pin a custom or local toolset — for example an air-gapped mirror — set the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` environment variable instead. + +The shape and behaviour differ, so this is not a 1-to-1 rename: + +- The old `customSquirrelVendorDir` pointed at a directory **whose contents were the vendor files** (`Squirrel.exe`, `nuget.exe`, …) and was copied verbatim, bypassing all provisioning. +- `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` points at a **toolset directory containing an `electron-winstaller/vendor/` subtree**, and still goes through normal provisioning: a working `nuget.exe` is ensured (downloaded unless your `vendor/` already ships a real, non-shim one) and, on Windows, `rcedit.exe` is supplied from the `winCodeSign` toolset. + +```bash +# Before (removed): "squirrelWindows": { "customSquirrelVendorDir": "./my-vendor" } +# After: point at a toolset dir whose electron-winstaller/vendor/ holds the binaries +export ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR=/abs/path/to/squirrel-toolset +``` + +For a fully offline build, stage a real (multi-MB, non-shim) `nuget.exe` inside that `vendor/` directory so no download is attempted, or redirect the download to an internal mirror with `ELECTRON_BUILDER_SQUIRREL_NUGET_URL` (and `ELECTRON_BUILDER_SQUIRREL_NUGET_SHA256` for a non-default build's checksum). + ### `GithubOptions` / `GitlabOptions` `vPrefixedTagName` The `vPrefixedTagName` boolean on `GithubOptions` is removed. Use `tagNamePrefix` to control the tag prefix. From b7c1f7fa29d7591315a464e018e22c6cc1db96d0 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 16:14:26 -0700 Subject: [PATCH 11/20] chore(squirrel-windows): add cleanup TODO for the temporary nuget download Mark prepareNugetExe/isUsableNuget/NUGET_* for removal once squirrel.windows@1.1.1 (electron-builder-binaries#203) bundles the pinned nuget.exe; the skip-if-real-nuget logic already makes the runtime download a no-op against such a bundle. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/electron-builder-squirrel-windows/src/toolset.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index ad08e234309..5e87c68a4cf 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -60,6 +60,11 @@ export async function getSquirrelToolsetPath(): Promise { // relocated to a temp vendor directory. Until squirrel.windows bundles the standalone portable exe // (electron-builder-binaries#203), download a pinned NuGet.CommandLine build at runtime and overwrite // the bundled shim. The same win-x86 exe runs natively on Windows and under mono on Linux/macOS. +// +// TODO(remove after squirrel.windows@1.1.1): once electron-builder-binaries#203 ships the pinned +// nuget.exe in the bundle, delete prepareNugetExe + isUsableNuget + the NUGET_* constants/env vars +// and bump getSquirrelToolsetPath's `toolset` to that release. (isUsableNuget already makes this a +// no-op against such a bundle, so the runtime download stops on its own once the version is bumped.) const NUGET_VERSION = "6.14.0" const NUGET_SHA256 = "92dbed160ddee0f64b901e907439e021211b428e57c089ecc12fc38dcc4bd9a5" const NUGET_URL = `https://dist.nuget.org/win-x86-commandline/v${NUGET_VERSION}/nuget.exe` From cb9a70bdba8006cc1c64fd25b4b94b33909d2b1a Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 16:35:19 -0700 Subject: [PATCH 12/20] feat(squirrel-windows): override the Squirrel toolset via toolsets.squirrel config Replace the bespoke ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR / ELECTRON_BUILDER_SQUIRREL_NUGET_* env vars with the standard ToolsetConfig pattern: - add a `squirrel` field to ToolsetConfig ("1.1.0" | ToolsetCustom | "latest"), resolved exactly like the other toolsets (version -> downloadBuilderToolset, object -> getCustomToolsetPath) - export getCustomToolsetPath + resolveToolsetVersion from app-builder-lib/internal - getSquirrelToolsetPath(toolset, resourcesDir) reads packager.config.toolsets.squirrel - offline/custom bundles now use toolsets.squirrel (a ToolsetCustom whose vendor/ may ship a real nuget.exe, which the skip-if-real check keeps); drop the nuget URL/SHA env overrides - rewrite the toolset test (custom-dir resolution + invalid-url) ; keep the nuget skip test - regenerate scheme.json; update v26->v27 + v27-breaking-changes docs Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/app-builder-lib/scheme.json | 16 ++++ packages/app-builder-lib/src/configuration.ts | 25 ++++++ packages/app-builder-lib/src/indexInternal.ts | 2 + .../src/SquirrelWindowsTarget.ts | 6 +- .../src/toolset.ts | 79 ++++++------------- .../src/windows/squirrelWindowsToolsetTest.ts | 41 +++------- website/docs/migration/v26-to-v27.md | 2 +- .../docs/migration/v27-breaking-changes.md | 18 +++-- 8 files changed, 89 insertions(+), 100 deletions(-) diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index 66b3e1519a8..03c3ad3e48a 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -8090,6 +8090,22 @@ "default": "latest", "description": "Version of the 7-Zip binary bundle used internally to extract `.7z` and `.tar.xz` archives.\n\nSet to a {@link ToolsetCustom} object to supply your own 7za binary.\nThe `url` must point to a directory (or a `.tar.gz`/`.zip` archive of one) that contains\n`bin/7za` (macOS/Linux) or `bin/7za.exe` (Windows).\n\n**Bootstrap constraint:** the custom bundle itself must be a `.tar.gz` or `.zip` archive\n(or a bare `file://` directory). `.7z` and `.tar.xz` archives cannot be used here because\nextracting them requires 7za — a circular dependency." }, + "squirrel": { + "anyOf": [ + { + "$ref": "#/definitions/ToolsetCustom" + }, + { + "enum": [ + "1.1.0", + "latest" + ], + "type": "string" + } + ], + "default": "latest", + "description": "Version of the `squirrel.windows` bundle used to build Squirrel.Windows installers.\n\nThe bundle ships the Squirrel vendor toolset under `electron-winstaller/vendor/`:\n- **`Squirrel.exe`** / **`Squirrel-Mono.exe`** — releasify the app into `Setup.exe` (and an optional MSI).\n- **`SyncReleases.exe`** — downloads prior releases to produce delta packages.\n- **`nuget.exe`**, **`7z`** — pack the app into a `.nupkg` and compress release assets.\n\n`rcedit.exe` is provisioned from the {@link winCodeSign} toolset at runtime, and a usable\n`nuget.exe` is ensured at runtime (the published bundle ships a shim that cannot be relocated).\n\nAvailable versions:\n| Version | Notes |\n|---------|-------|\n| `\"1.1.0\"` | Squirrel.Windows 2.0.1 (patched) |\n\nSet to a {@link ToolsetCustom} object to supply your own bundle — it must contain the\n`electron-winstaller/vendor/` subtree. Only used when building the `squirrelWindows` target.\n\nReleases: https://github.com/electron-userland/electron-builder-binaries/blob/master/packages/squirrel.windows/CHANGELOG.md" + }, "winCodeSign": { "anyOf": [ { diff --git a/packages/app-builder-lib/src/configuration.ts b/packages/app-builder-lib/src/configuration.ts index 7e81c655272..375331c9d82 100644 --- a/packages/app-builder-lib/src/configuration.ts +++ b/packages/app-builder-lib/src/configuration.ts @@ -697,6 +697,31 @@ export interface ToolsetConfig { * @default "latest" */ readonly icons?: "1.2.1" | ToolsetCustom | "latest" + + /** + * Version of the `squirrel.windows` bundle used to build Squirrel.Windows installers. + * + * The bundle ships the Squirrel vendor toolset under `electron-winstaller/vendor/`: + * - **`Squirrel.exe`** / **`Squirrel-Mono.exe`** — releasify the app into `Setup.exe` (and an optional MSI). + * - **`SyncReleases.exe`** — downloads prior releases to produce delta packages. + * - **`nuget.exe`**, **`7z`** — pack the app into a `.nupkg` and compress release assets. + * + * `rcedit.exe` is provisioned from the {@link winCodeSign} toolset at runtime, and a usable + * `nuget.exe` is ensured at runtime (the published bundle ships a shim that cannot be relocated). + * + * Available versions: + * | Version | Notes | + * |---------|-------| + * | `"1.1.0"` | Squirrel.Windows 2.0.1 (patched) | + * + * Set to a {@link ToolsetCustom} object to supply your own bundle — it must contain the + * `electron-winstaller/vendor/` subtree. Only used when building the `squirrelWindows` target. + * + * Releases: https://github.com/electron-userland/electron-builder-binaries/blob/master/packages/squirrel.windows/CHANGELOG.md + * + * @default "latest" + */ + readonly squirrel?: "1.1.0" | ToolsetCustom | "latest" } /** diff --git a/packages/app-builder-lib/src/indexInternal.ts b/packages/app-builder-lib/src/indexInternal.ts index ddce1804c4b..ff99a657b25 100644 --- a/packages/app-builder-lib/src/indexInternal.ts +++ b/packages/app-builder-lib/src/indexInternal.ts @@ -45,6 +45,8 @@ export { nsisEscapeString, NsisScriptGenerator } from "./targets/win/nsis/nsisSc export { ProgIdMaker } from "./targets/win/nsis/progId.js" export { checkMakensisOutput, verifyInstallerSize } from "./targets/win/nsis/nsisValidation.js" export { getLinuxToolsMacToolset, getLinuxToolsPath } from "./toolsets/linuxToolsMac.js" +export { getCustomToolsetPath } from "./toolsets/custom.js" +export { resolveToolsetVersion } from "./toolsets/version.js" export { getRceditBundle, getWindowsKitsBundle } from "./toolsets/winCodeSign.js" export { CacheState } from "./util/cacheState.js" export { computeDefaultAppDirectory, createProjectMetadataLazy, doMergeConfigs, getConfig, validateConfiguration } from "./util/config/config.js" diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 90a43a83eca..43a49c13a3a 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -24,9 +24,9 @@ export default class SquirrelWindowsTarget extends Target { private async prepareSignedVendorDirectory(): Promise { const tmpVendorDirectory = await this.packager.tempDirManager.createTempDir({ prefix: "squirrel-windows-vendor" }) - // The vendor toolset comes from the maintained squirrel.windows bundle. Override the bundle source - // (e.g. a local copy for air-gapped builds) with ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR. - const squirrelToolset = await getSquirrelToolsetPath() + // The vendor toolset comes from the maintained squirrel.windows bundle. Pin a version or supply a + // custom/local bundle (e.g. for air-gapped builds) via `toolsets.squirrel` (a ToolsetCustom object). + const squirrelToolset = await getSquirrelToolsetPath(this.packager.config.toolsets?.squirrel, this.packager.buildResourcesDir) await fs.promises.cp(path.join(squirrelToolset, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }) // TEMPORARY: the published squirrel.windows@1.1.0 bundle ships the Chocolatey shim for nuget.exe, diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index 5e87c68a4cf..75d9f36233e 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -1,9 +1,10 @@ -import { download, downloadBuilderToolset } from "app-builder-lib/internal" -import { exists, log, parseValidEnvVarUrl, resolveEnvShellValue } from "builder-util" +import { ToolsetConfig } from "app-builder-lib" +import { download, downloadBuilderToolset, getCustomToolsetPath, resolveToolsetVersion } from "app-builder-lib/internal" import { stat } from "fs/promises" import * as path from "path" -const ENV_KEY = "ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR" +// Newest squirrel.windows bundle — selected when `toolsets.squirrel` is unset / null / "latest". +const SQUIRREL_LATEST = "1.1.0" export const squirrelWindowsChecksums = { "1.1.0": { @@ -12,46 +13,23 @@ export const squirrelWindowsChecksums = { } as const /** - * Validates and resolves the {@link ENV_KEY} override. Returns the absolute directory path when the - * env var is set, or `null` when it is absent. Throws an actionable error when the value is set but - * invalid (relative path, missing, or not a directory) rather than silently falling back to download. - */ -async function resolveToolsetEnvDir(): Promise { - const value = resolveEnvShellValue(ENV_KEY) - if (value == null) { - return null - } - if (!path.isAbsolute(value)) { - throw new Error(`${ENV_KEY} must be an absolute path: ${value}`) - } - const dir = path.resolve(value) - if (!(await exists(dir))) { - throw new Error(`${ENV_KEY} path does not exist: ${dir}`) - } - if (!(await stat(dir)).isDirectory()) { - throw new Error(`${ENV_KEY} path must be a directory: ${dir}`) - } - log.info({ [ENV_KEY]: dir }, "resolved Squirrel.Windows toolset directory from environment variable") - return dir -} - -/** - * Returns the path to the squirrel.windows toolset directory. The directory - * contains an `electron-winstaller/vendor/` subtree with all vendor executables - * (Squirrel.exe, nuget.exe, SyncReleases.exe, 7z, etc.). + * Returns the path to the squirrel.windows toolset directory. It contains an + * `electron-winstaller/vendor/` subtree with the Squirrel vendor executables (Squirrel.exe, + * nuget.exe, SyncReleases.exe, 7z, …). * - * Override with {@link ENV_KEY} for local development or to pin a custom build. + * Honors `toolsets.squirrel`: a pinned version (or unset / `"latest"`) downloads the maintained + * electron-builder-binaries bundle; a {@link ToolsetCustom} object supplies a custom or local bundle + * (which must mirror the same `electron-winstaller/vendor/` layout). */ -export async function getSquirrelToolsetPath(): Promise { - const envPath = await resolveToolsetEnvDir() - if (envPath != null) { - return envPath +export async function getSquirrelToolsetPath(toolset: ToolsetConfig["squirrel"], resourcesDir: string): Promise { + if (typeof toolset === "object" && toolset != null) { + return getCustomToolsetPath(toolset, resourcesDir) } - const toolset = "1.1.0" + const version = resolveToolsetVersion(toolset, SQUIRREL_LATEST) return downloadBuilderToolset({ - releaseName: `squirrel.windows@${toolset}`, + releaseName: `squirrel.windows@${version}`, filenameWithExt: "squirrel.windows-2.0.1-patched.zip", - checksums: squirrelWindowsChecksums[toolset], + checksums: squirrelWindowsChecksums[version], }) } @@ -62,15 +40,12 @@ export async function getSquirrelToolsetPath(): Promise { // the bundled shim. The same win-x86 exe runs natively on Windows and under mono on Linux/macOS. // // TODO(remove after squirrel.windows@1.1.1): once electron-builder-binaries#203 ships the pinned -// nuget.exe in the bundle, delete prepareNugetExe + isUsableNuget + the NUGET_* constants/env vars -// and bump getSquirrelToolsetPath's `toolset` to that release. (isUsableNuget already makes this a -// no-op against such a bundle, so the runtime download stops on its own once the version is bumped.) +// nuget.exe in the bundle, delete prepareNugetExe + isUsableNuget + the NUGET_* constants and bump +// SQUIRREL_LATEST to that release. (isUsableNuget already makes this a no-op against such a bundle, +// so the runtime download stops on its own once the version is bumped.) const NUGET_VERSION = "6.14.0" const NUGET_SHA256 = "92dbed160ddee0f64b901e907439e021211b428e57c089ecc12fc38dcc4bd9a5" const NUGET_URL = `https://dist.nuget.org/win-x86-commandline/v${NUGET_VERSION}/nuget.exe` -// Redirect the nuget.exe download for air-gapped / mirrored builds. -const NUGET_URL_ENV = "ELECTRON_BUILDER_SQUIRREL_NUGET_URL" -const NUGET_SHA256_ENV = "ELECTRON_BUILDER_SQUIRREL_NUGET_SHA256" // A Chocolatey shim is ~0.4 MB; a real nuget.exe is several MB. Anything above this is already usable. const MIN_USABLE_NUGET_BYTES = 2_000_000 @@ -83,22 +58,14 @@ async function isUsableNuget(file: string): Promise { } /** - * Ensures `vendorDirectory` holds a usable standalone nuget.exe. If the bundle already ships a real one - * (e.g. a future squirrel.windows release, or a pre-staged offline toolset dir), it is kept as-is. - * Otherwise a pinned NuGet.CommandLine build is downloaded and cached, replacing the broken Chocolatey - * shim. Air-gapped/mirrored builds can redirect the download with {@link NUGET_URL_ENV} (and - * {@link NUGET_SHA256_ENV} to supply a non-default build's checksum, or an empty value to skip it). + * Ensures `vendorDirectory` holds a usable standalone nuget.exe. If the bundle already ships a real + * one (a future squirrel.windows release, or a custom `toolsets.squirrel` bundle), it is kept as-is; + * otherwise a pinned NuGet.CommandLine build is downloaded and cached, replacing the broken shim. */ export async function prepareNugetExe(vendorDirectory: string): Promise { const target = path.join(vendorDirectory, "nuget.exe") if (await isUsableNuget(target)) { return } - const overrideUrl = parseValidEnvVarUrl(NUGET_URL_ENV) - const url = overrideUrl ?? NUGET_URL - // The pinned hash matches the official 6.14.0 build and any plain mirror of it. For a different - // build, set NUGET_SHA256_ENV to its sha256 (an empty value disables verification). - const shaOverride = overrideUrl != null ? process.env[NUGET_SHA256_ENV]?.trim() : undefined - const checksum = shaOverride !== undefined ? shaOverride || null : NUGET_SHA256 - await download(url, target, checksum) + await download(NUGET_URL, target, NUGET_SHA256) } diff --git a/test/src/windows/squirrelWindowsToolsetTest.ts b/test/src/windows/squirrelWindowsToolsetTest.ts index 455dbd4dd60..3959193442c 100644 --- a/test/src/windows/squirrelWindowsToolsetTest.ts +++ b/test/src/windows/squirrelWindowsToolsetTest.ts @@ -4,47 +4,24 @@ import { mkdtemp, rm, stat, writeFile } from "fs/promises" import { tmpdir } from "os" import * as path from "path" -const ENV_KEY = "ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR" - describe("getSquirrelToolsetPath", () => { - let savedEnv: string | undefined let tmpDir: string beforeEach(async () => { - savedEnv = process.env[ENV_KEY] tmpDir = await mkdtemp(path.join(tmpdir(), "eb-squirrel-toolset-test-")) }) - afterEach(async () => { - if (savedEnv === undefined) { - delete process.env[ENV_KEY] - } else { - process.env[ENV_KEY] = savedEnv - } - await rm(tmpDir, { recursive: true, force: true }).catch(() => {}) - }) - - test("returns env var path when set to an existing directory", async () => { - process.env[ENV_KEY] = tmpDir - const result = await getSquirrelToolsetPath() - expect(result).toBe(tmpDir) - }) - - test("throws when env var points to a non-existent path", async () => { - process.env[ENV_KEY] = path.join(tmpDir, "does-not-exist") - await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) - }) + afterEach(() => rm(tmpDir, { recursive: true, force: true }).catch(() => {})) - test("throws when env var points to a file instead of a directory", async () => { - const filePath = path.join(tmpDir, "not-a-dir.txt") - await writeFile(filePath, "") - process.env[ENV_KEY] = filePath - await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) + test("resolves a ToolsetCustom bare directory in place (no download)", async () => { + // A `file://` directory custom toolset is used as-is — no checksum, no network — so this is the + // air-gapped / local-bundle path and must resolve without touching the network. + const result = await getSquirrelToolsetPath({ url: `file://${tmpDir}` }, tmpDir) + expect(result).toBe(path.resolve(tmpDir)) }) - test("throws when env var is a relative path", async () => { - process.env[ENV_KEY] = "relative/path" - await expect(getSquirrelToolsetPath()).rejects.toThrow(ENV_KEY) + test("rejects an invalid ToolsetCustom url", async () => { + await expect(getSquirrelToolsetPath({ url: "not-a-url" }, tmpDir)).rejects.toThrow(/Invalid custom toolset/) }) }) @@ -59,7 +36,7 @@ describe("prepareNugetExe", () => { test("keeps an already-usable nuget.exe and performs no download", async () => { // A real nuget.exe is several MB; staging one larger than the shim threshold must short-circuit - // the download entirely (this is the offline / pre-staged-toolset path, so it must not hit network). + // the download entirely (this is the offline / custom-bundle path, so it must not hit network). const nugetExe = path.join(tmpDir, "nuget.exe") await writeFile(nugetExe, Buffer.alloc(2_100_000, 1)) const before = await stat(nugetExe) diff --git a/website/docs/migration/v26-to-v27.md b/website/docs/migration/v26-to-v27.md index 589c71bdc20..a44a510824e 100644 --- a/website/docs/migration/v26-to-v27.md +++ b/website/docs/migration/v26-to-v27.md @@ -145,7 +145,7 @@ import { build } from "electron-builder" - [ ] [Replace `CI_BUILD_TAG`](./v27-breaking-changes#ci_build_tag-environment-variable) with `CI_COMMIT_TAG`. - [ ] [Replace toolset env-var overrides](./v27-breaking-changes#toolset-env-var-overrides-removed) (`APPIMAGE_TOOLS_PATH`, `ELECTRON_BUILDER_NSIS_DIR`, `USE_SYSTEM_WINE`, …) with `toolsets.X: { url, checksum }`. - [ ] [Validate toolset pins](./v27-breaking-changes#toolset-defaults-resolve-to-latest-newest-bundle) — defaults now resolve to `"latest"` (newest bundle); pin to `"0.0.0"` only if a legacy bundle is needed. -- [ ] [Remove `squirrelWindows.customSquirrelVendorDir`](./v27-breaking-changes#squirrelwindowscustomsquirrelvendordir) — the option is deleted; point at a custom Squirrel toolset with the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` env var instead (note the different directory layout). +- [ ] [Remove `squirrelWindows.customSquirrelVendorDir`](./v27-breaking-changes#squirrelwindowscustomsquirrelvendordir) — the option is deleted; supply a custom Squirrel bundle via `toolsets.squirrel` (a `ToolsetCustom` object) instead (note the different bundle layout). - [ ] **Plugin/custom-target authors:** [replace `packager.info.X` and `platformSpecificBuildOptions`](./v27-breaking-changes#programmatic--plugin-author-api-changes) with the new pass-through getters. --- diff --git a/website/docs/migration/v27-breaking-changes.md b/website/docs/migration/v27-breaking-changes.md index e04eda5b6b4..4ed8462d3aa 100644 --- a/website/docs/migration/v27-breaking-changes.md +++ b/website/docs/migration/v27-breaking-changes.md @@ -61,7 +61,7 @@ To stay on a legacy bundle, pin the toolset to `"0.0.0"`. Because `winCodeSign` | [Root-level `directories` removed](#root-level-directories-in-packagejson) | ✓ | Move under `build.directories` | | [`build.helper-bundle-id` removed](#buildhelper-bundle-id) | ✓ | Moved to `mac.helperBundleId` | | [`squirrelWindows.noMsi` removed](#squirrelwindowsnomsi) | ✓ | Replaced by `msi` (inverted) | -| [`squirrelWindows.customSquirrelVendorDir` removed](#squirrelwindowscustomsquirrelvendordir) | — | Override the vendor toolset with the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` env var | +| [`squirrelWindows.customSquirrelVendorDir` removed](#squirrelwindowscustomsquirrelvendordir) | — | Supply a custom Squirrel bundle via `toolsets.squirrel` (a `ToolsetCustom` object) | | [`GithubOptions.vPrefixedTagName` removed](#githuboptions--gitlaboptions-vprefixedtagname) | ✓ | Use `tagNamePrefix` | | [`GitlabOptions.vPrefixedTagName` retained](#githuboptions--gitlaboptions-vprefixedtagname) | — | None — still functional; the migrator leaves GitLab entries untouched | | [`devMetadata` / `extraMetadata` in `PackagerOptions` removed](#devmetadata--extrametadata-programmatic-packageroptions) | — | Use `config` / `config.extraMetadata` | @@ -222,20 +222,22 @@ The `noMsi` boolean is removed in favor of its inverse, `msi`. ### `squirrelWindows.customSquirrelVendorDir` -Removed. v27 inlines the Squirrel.Windows installer logic and drops the `electron-winstaller` npm dependency (and its vendored binaries); the Squirrel vendor toolset is now fetched from the maintained `squirrel.windows` electron-builder-binaries bundle. To pin a custom or local toolset — for example an air-gapped mirror — set the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` environment variable instead. +Removed. v27 inlines the Squirrel.Windows installer logic and drops the `electron-winstaller` npm dependency (and its vendored binaries); the Squirrel vendor toolset is now fetched from the maintained `squirrel.windows` electron-builder-binaries bundle. To pin a version or supply a custom/local bundle — for example an air-gapped mirror — use the standard `toolsets.squirrel` setting instead, the same way you would pin `toolsets.nsis` or `toolsets.winCodeSign`. The shape and behaviour differ, so this is not a 1-to-1 rename: - The old `customSquirrelVendorDir` pointed at a directory **whose contents were the vendor files** (`Squirrel.exe`, `nuget.exe`, …) and was copied verbatim, bypassing all provisioning. -- `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` points at a **toolset directory containing an `electron-winstaller/vendor/` subtree**, and still goes through normal provisioning: a working `nuget.exe` is ensured (downloaded unless your `vendor/` already ships a real, non-shim one) and, on Windows, `rcedit.exe` is supplied from the `winCodeSign` toolset. +- `toolsets.squirrel` accepts a version pin (`"1.1.0"` / `"latest"`) **or** a `ToolsetCustom` object whose `url` points at a bundle that **contains an `electron-winstaller/vendor/` subtree**. The bundle still goes through normal provisioning: a working `nuget.exe` is ensured (no download when your `vendor/` already ships a real, non-shim one) and, on Windows, `rcedit.exe` is supplied from the `winCodeSign` toolset. -```bash -# Before (removed): "squirrelWindows": { "customSquirrelVendorDir": "./my-vendor" } -# After: point at a toolset dir whose electron-winstaller/vendor/ holds the binaries -export ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR=/abs/path/to/squirrel-toolset +```json5 +// Before (removed): +{ "squirrelWindows": { "customSquirrelVendorDir": "./my-vendor" } } +// After: a custom toolset bundle whose electron-winstaller/vendor/ holds the binaries. +// A bare file:// directory is used as-is (no checksum); a remote/archive url needs a checksum. +{ "toolsets": { "squirrel": { "url": "file:///abs/path/to/squirrel-toolset" } } } ``` -For a fully offline build, stage a real (multi-MB, non-shim) `nuget.exe` inside that `vendor/` directory so no download is attempted, or redirect the download to an internal mirror with `ELECTRON_BUILDER_SQUIRREL_NUGET_URL` (and `ELECTRON_BUILDER_SQUIRREL_NUGET_SHA256` for a non-default build's checksum). +For a fully offline build, point `toolsets.squirrel` at a bundle whose `vendor/` already contains a real (multi-MB, non-shim) `nuget.exe` — the runtime nuget download is then skipped entirely. ### `GithubOptions` / `GitlabOptions` `vPrefixedTagName` From f75c69f74513d49943de41bcc5712365eda03841 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 17:49:49 -0700 Subject: [PATCH 13/20] update lockfile --- pnpm-lock.yaml | 125 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44334ec11eb..e3d968db650 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,16 @@ importers: builder-util: specifier: workspace:* version: link:../builder-util + electron-winstaller: + specifier: 5.4.0 + version: 5.4.0 + devDependencies: + '@types/archiver': + specifier: 5.3.1 + version: 5.3.1 + '@types/fs-extra': + specifier: 9.0.13 + version: 9.0.13 packages/electron-forge-maker-appimage: dependencies: @@ -617,6 +627,9 @@ importers: electron-updater: specifier: workspace:* version: link:../packages/electron-updater + electron-winstaller: + specifier: 5.4.0 + version: 5.4.0 fast-xml-parser: specifier: 5.7.0 version: 5.7.0 @@ -2662,6 +2675,11 @@ packages: resolution: {integrity: sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==} engines: {node: '>=20.0'} + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + '@electron/asar@4.1.1': resolution: {integrity: sha512-grbomy1TPEauyu+N4UdRQqSjOhsm0ZoTbprLpvJWQGYeEIMVBwSPV3Rv3E3EfqU5iFSufd/k+2KZvTDbgfesnw==} engines: {node: '>=22.12.0'} @@ -2707,6 +2725,11 @@ packages: resolution: {integrity: sha512-G7mNdfZIdPbx54dTorGlV7SN7nHAKlbJU1NjjeuEo7RzEMYBG62+tKpA2VrzocGcvFLxqJI5XYIknca9EijwOw==} engines: {node: '>=22.12.0'} + '@electron/windows-sign@1.2.1': + resolution: {integrity: sha512-YfASnrhJ+ve6Q43ZiDwmpBgYgi2u0bYjeAVi2tDfN7YWAKO8X9EEOuPGtqbJpPLM6TfAHimghICjWe2eaJ8BAg==} + engines: {node: '>=14.14'} + hasBin: true + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -3951,6 +3974,9 @@ packages: '@types/adm-zip@0.5.6': resolution: {integrity: sha512-lRlcSLg5Yoo7C2H2AUiAoYlvifWoCx/se7iUNiCBTfEVVYFVn+Tr9ZGed4K73tYgLe9O4PjdJvbxlkdAOx/qiw==} + '@types/archiver@5.3.1': + resolution: {integrity: sha512-wKYZaSXaDvTZuInAWjCeGG7BEAgTWG2zZW0/f7IYFcoHB2X2d9lkVFnrOlXl3W6NrvO6Ml3FLLu8Uksyymcpnw==} + '@types/aws4@1.11.6': resolution: {integrity: sha512-5CnVUkHNyLGpD9AnOcK66YyP0qvIh6nhJJoeK8zSl5YKikUcUbdB7SlHevUYVqicgeh6j5AJa1qa/h08dSZHoA==} @@ -4107,6 +4133,9 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/glob@8.1.0': + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -4176,6 +4205,9 @@ packages: '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -5087,6 +5119,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -5257,6 +5293,9 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5766,6 +5805,10 @@ packages: electron-to-chromium@1.5.353: resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + electron@39.8.5: resolution: {integrity: sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==} engines: {node: '>= 12.20.55'} @@ -7489,6 +7532,10 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -8290,6 +8337,11 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -8672,6 +8724,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -9092,6 +9149,10 @@ packages: temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + tempfile@5.0.0: resolution: {integrity: sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q==} engines: {node: '>=14.18'} @@ -13410,6 +13471,12 @@ snapshots: - uglify-js - webpack-cli + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + '@electron/asar@4.1.1': dependencies: commander: 13.1.0 @@ -13506,6 +13573,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/windows-sign@1.2.1': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.3 + fs-extra: 11.3.0 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -14687,6 +14765,10 @@ snapshots: dependencies: '@types/node': 22.13.17 + '@types/archiver@5.3.1': + dependencies: + '@types/glob': 8.1.0 + '@types/aws4@1.11.6': dependencies: '@types/node': 22.13.17 @@ -14890,6 +14972,11 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/glob@8.1.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.13.17 + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.13.17 @@ -14954,6 +15041,8 @@ snapshots: '@types/minimatch@3.0.5': {} + '@types/minimatch@5.1.2': {} + '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -16031,6 +16120,9 @@ snapshots: commander@8.3.0: {} + commander@9.5.0: + optional: true + common-path-prefix@3.0.0: {} compare-func@2.0.0: @@ -16219,6 +16311,9 @@ snapshots: create-require@1.1.1: {} + cross-dirname@0.1.0: + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -16845,6 +16940,18 @@ snapshots: electron-to-chromium@1.5.353: {} + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.3 + fs-extra: 7.0.1 + lodash: 4.18.1 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.1 + transitivePeerDependencies: + - supports-color + electron@39.8.5: dependencies: '@electron/get': 2.0.3 @@ -19043,6 +19150,10 @@ snapshots: dependencies: minipass: 7.1.3 + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mri@1.2.0: {} mrmime@2.0.1: {} @@ -20028,6 +20139,11 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -20488,6 +20604,10 @@ snapshots: reusify@1.1.0: {} + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -20988,6 +21108,11 @@ snapshots: async-exit-hook: 2.0.1 fs-extra: 10.1.0 + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + tempfile@5.0.0: dependencies: temp-dir: 3.0.0 From 2d8e89e7779f17855b6bf5a5636118bfa6d46725 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 17:54:03 -0700 Subject: [PATCH 14/20] refactor(squirrel-windows): drop deprecated noMsi; throw on missing requested MSI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InstallerOptions uses `msi` (positive form) instead of `noMsi`; createWindowsInstaller passes --no-msi when !msi. Removes the redundant runtime noMsi deprecation shim (migrate-schema already rewrites the deprecated squirrelWindows.noMsi config to msi). - fixUpPaths: the Setup.msi rename only runs when MSI output was requested, so a missing Setup.msi is a real failure — throw an explicit error instead of swallowing it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/SquirrelWindowsTarget.ts | 7 +----- .../src/windowsInstaller.ts | 23 +++++++++---------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 43a49c13a3a..872ad46acde 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -309,7 +309,7 @@ export default class SquirrelWindowsTarget extends Target { nuspecTemplate: await this.createNuspecTemplateWithProjectUrl(), iconUrl, copyright: appInfo.copyright, - noMsi: !this.options.msi, + msi: this.options.msi, fixUpPaths: true, setupExe: setupFile, setupMsi: this.options.msi ? setupFile.replace(".exe", ".msi") : undefined, @@ -328,11 +328,6 @@ function normalizeSquirrelOptions(options: any) { } } - if ("noMsi" in options) { - log.warn(`noMsi is deprecated, please specify as "msi": true if you want to create an MSI installer`) - options.msi = !options.noMsi - } - const msi = options.msi if (msi != null && typeof msi !== "boolean") { throw new InvalidConfigurationError(`msi expected to be boolean value, but string '"${msi}"' was specified`) diff --git a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts index b1b619719bb..6e0462b215d 100644 --- a/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts +++ b/packages/electron-builder-squirrel-windows/src/windowsInstaller.ts @@ -1,4 +1,4 @@ -import { exec, log } from "builder-util" +import { exec, exists, log } from "builder-util" import * as fs from "fs/promises" import * as path from "path" @@ -50,7 +50,7 @@ export interface InstallerOptions { copyright?: string | null nuspecTemplate: string loadingGif?: string | null - noMsi?: boolean + msi?: boolean remoteReleases?: string | null remoteToken?: string | null setupExe?: string | null @@ -69,7 +69,7 @@ export async function createWindowsInstaller(options: InstallerOptions): Promise exe, nuspecTemplate, loadingGif, - noMsi, + msi, remoteReleases, remoteToken, setupExe, @@ -134,7 +134,7 @@ export async function createWindowsInstaller(options: InstallerOptions): Promise if (loadingGif) { releasifyArgs.push("--loadingGif", loadingGif) } - if (noMsi) { + if (!msi) { releasifyArgs.push("--no-msi") } @@ -149,15 +149,14 @@ export async function createWindowsInstaller(options: InstallerOptions): Promise await fs.rename(path.join(outputDirectory, "Setup.exe"), path.join(outputDirectory, setupExe)) } if (setupMsi) { - try { - await fs.rename(path.join(outputDirectory, "Setup.msi"), path.join(outputDirectory, setupMsi)) - } catch (e: any) { - // Setup.msi is absent when releasify produced no MSI; only that case is expected. - // Surface real failures (permissions, cross-device) instead of silently dropping the artifact. - if (e?.code !== "ENOENT") { - throw e - } + // setupMsi is only set when MSI output was requested (msi: true), so releasify ran without + // --no-msi and Setup.msi must exist. A missing file here is a real failure, not an expected + // absence — surface it instead of silently dropping the artifact. + const setupMsiPath = path.join(outputDirectory, "Setup.msi") + if (!(await exists(setupMsiPath))) { + throw new Error(`MSI output was requested but Squirrel.Windows did not produce ${setupMsiPath}`) } + await fs.rename(setupMsiPath, path.join(outputDirectory, setupMsi)) } } } From 9aa26fd1e9262ad79d2c5b20801725d5171a4e71 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 17:57:34 -0700 Subject: [PATCH 15/20] update lockfile --- pnpm-lock.yaml | 125 -------------------------------------------- pnpm-workspace.yaml | 1 - 2 files changed, 126 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3d968db650..44334ec11eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,16 +420,6 @@ importers: builder-util: specifier: workspace:* version: link:../builder-util - electron-winstaller: - specifier: 5.4.0 - version: 5.4.0 - devDependencies: - '@types/archiver': - specifier: 5.3.1 - version: 5.3.1 - '@types/fs-extra': - specifier: 9.0.13 - version: 9.0.13 packages/electron-forge-maker-appimage: dependencies: @@ -627,9 +617,6 @@ importers: electron-updater: specifier: workspace:* version: link:../packages/electron-updater - electron-winstaller: - specifier: 5.4.0 - version: 5.4.0 fast-xml-parser: specifier: 5.7.0 version: 5.7.0 @@ -2675,11 +2662,6 @@ packages: resolution: {integrity: sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==} engines: {node: '>=20.0'} - '@electron/asar@3.4.1': - resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} - engines: {node: '>=10.12.0'} - hasBin: true - '@electron/asar@4.1.1': resolution: {integrity: sha512-grbomy1TPEauyu+N4UdRQqSjOhsm0ZoTbprLpvJWQGYeEIMVBwSPV3Rv3E3EfqU5iFSufd/k+2KZvTDbgfesnw==} engines: {node: '>=22.12.0'} @@ -2725,11 +2707,6 @@ packages: resolution: {integrity: sha512-G7mNdfZIdPbx54dTorGlV7SN7nHAKlbJU1NjjeuEo7RzEMYBG62+tKpA2VrzocGcvFLxqJI5XYIknca9EijwOw==} engines: {node: '>=22.12.0'} - '@electron/windows-sign@1.2.1': - resolution: {integrity: sha512-YfASnrhJ+ve6Q43ZiDwmpBgYgi2u0bYjeAVi2tDfN7YWAKO8X9EEOuPGtqbJpPLM6TfAHimghICjWe2eaJ8BAg==} - engines: {node: '>=14.14'} - hasBin: true - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -3974,9 +3951,6 @@ packages: '@types/adm-zip@0.5.6': resolution: {integrity: sha512-lRlcSLg5Yoo7C2H2AUiAoYlvifWoCx/se7iUNiCBTfEVVYFVn+Tr9ZGed4K73tYgLe9O4PjdJvbxlkdAOx/qiw==} - '@types/archiver@5.3.1': - resolution: {integrity: sha512-wKYZaSXaDvTZuInAWjCeGG7BEAgTWG2zZW0/f7IYFcoHB2X2d9lkVFnrOlXl3W6NrvO6Ml3FLLu8Uksyymcpnw==} - '@types/aws4@1.11.6': resolution: {integrity: sha512-5CnVUkHNyLGpD9AnOcK66YyP0qvIh6nhJJoeK8zSl5YKikUcUbdB7SlHevUYVqicgeh6j5AJa1qa/h08dSZHoA==} @@ -4133,9 +4107,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/glob@8.1.0': - resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} - '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -4205,9 +4176,6 @@ packages: '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -5119,10 +5087,6 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -5293,9 +5257,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-dirname@0.1.0: - resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5805,10 +5766,6 @@ packages: electron-to-chromium@1.5.353: resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} - electron-winstaller@5.4.0: - resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} - engines: {node: '>=8.0.0'} - electron@39.8.5: resolution: {integrity: sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==} engines: {node: '>= 12.20.55'} @@ -7532,10 +7489,6 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -8337,11 +8290,6 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - postject@1.0.0-alpha.6: - resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} - engines: {node: '>=14.0.0'} - hasBin: true - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -8724,11 +8672,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -9149,10 +9092,6 @@ packages: temp-file@3.4.0: resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} - temp@0.9.4: - resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} - engines: {node: '>=6.0.0'} - tempfile@5.0.0: resolution: {integrity: sha512-bX655WZI/F7EoTDw9JvQURqAXiPHi8o8+yFxPF2lWYyz1aHnmMRuXWqL6YB6GmeO0o4DIYWHLgGNi/X64T+X4Q==} engines: {node: '>=14.18'} @@ -13471,12 +13410,6 @@ snapshots: - uglify-js - webpack-cli - '@electron/asar@3.4.1': - dependencies: - commander: 5.1.0 - glob: 7.2.3 - minimatch: 3.1.5 - '@electron/asar@4.1.1': dependencies: commander: 13.1.0 @@ -13573,17 +13506,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@electron/windows-sign@1.2.1': - dependencies: - cross-dirname: 0.1.0 - debug: 4.4.3 - fs-extra: 11.3.0 - minimist: 1.2.8 - postject: 1.0.0-alpha.6 - transitivePeerDependencies: - - supports-color - optional: true - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -14765,10 +14687,6 @@ snapshots: dependencies: '@types/node': 22.13.17 - '@types/archiver@5.3.1': - dependencies: - '@types/glob': 8.1.0 - '@types/aws4@1.11.6': dependencies: '@types/node': 22.13.17 @@ -14972,11 +14890,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/glob@8.1.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 22.13.17 - '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.13.17 @@ -15041,8 +14954,6 @@ snapshots: '@types/minimatch@3.0.5': {} - '@types/minimatch@5.1.2': {} - '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -16120,9 +16031,6 @@ snapshots: commander@8.3.0: {} - commander@9.5.0: - optional: true - common-path-prefix@3.0.0: {} compare-func@2.0.0: @@ -16311,9 +16219,6 @@ snapshots: create-require@1.1.1: {} - cross-dirname@0.1.0: - optional: true - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -16940,18 +16845,6 @@ snapshots: electron-to-chromium@1.5.353: {} - electron-winstaller@5.4.0: - dependencies: - '@electron/asar': 3.4.1 - debug: 4.4.3 - fs-extra: 7.0.1 - lodash: 4.18.1 - temp: 0.9.4 - optionalDependencies: - '@electron/windows-sign': 1.2.1 - transitivePeerDependencies: - - supports-color - electron@39.8.5: dependencies: '@electron/get': 2.0.3 @@ -19150,10 +19043,6 @@ snapshots: dependencies: minipass: 7.1.3 - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - mri@1.2.0: {} mrmime@2.0.1: {} @@ -20139,11 +20028,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postject@1.0.0-alpha.6: - dependencies: - commander: 9.5.0 - optional: true - prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -20604,10 +20488,6 @@ snapshots: reusify@1.1.0: {} - rimraf@2.6.3: - dependencies: - glob: 7.2.3 - roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -21108,11 +20988,6 @@ snapshots: async-exit-hook: 2.0.1 fs-extra: 10.1.0 - temp@0.9.4: - dependencies: - mkdirp: 0.5.6 - rimraf: 2.6.3 - tempfile@5.0.0: dependencies: temp-dir: 3.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2765f5c48fc..d4ec743bdec 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,7 +8,6 @@ allowBuilds: '@swc/core': true core-js: false electron: false - electron-winstaller: true esbuild: false # 10080 minutes = 7 days From df1d2dcd7ca7071b1bdd5462ce44761f2da520ad Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 18:00:59 -0700 Subject: [PATCH 16/20] docs(changeset): point customSquirrelVendorDir migration at toolsets.squirrel Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/remove-electron-winstaller-dep.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/remove-electron-winstaller-dep.md b/.changeset/remove-electron-winstaller-dep.md index ce5b196b6ce..ff85ed28562 100644 --- a/.changeset/remove-electron-winstaller-dep.md +++ b/.changeset/remove-electron-winstaller-dep.md @@ -4,4 +4,4 @@ feat(squirrel-windows)!: inline installer logic; remove electron-winstaller dependency and its vendored binaries -BREAKING: the `squirrelWindows.customSquirrelVendorDir` option has been removed. Override the vendor toolset source with the `ELECTRON_BUILDER_SQUIRREL_TOOLSET_DIR` environment variable instead (it goes through the same nuget/rcedit provisioning). +BREAKING: the `squirrelWindows.customSquirrelVendorDir` option has been removed. Supply a custom Squirrel vendor bundle with the `toolsets.squirrel` config (a `ToolsetCustom` object) instead — it goes through the same nuget/rcedit provisioning as the default bundle. From d6fab954943f61d81fdd52140a86ddc23b5e3630 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 18:21:47 -0700 Subject: [PATCH 17/20] fix(squirrel-windows): provision rcedit.exe on all platforms, not just win32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squirrel-Mono.exe also shells out to rcedit.exe during --releasify (setPEVersionInfoAndIcon) to stamp version info + the app icon into Setup.exe, so the win32-only gate left rcedit.exe missing from the vendor dir on Linux/macOS, failing the build under mono/wine with "Failed to modify resources / ShellExecuteEx failed". electron-winstaller's vendor shipped a 32-bit rcedit.exe on all platforms; match that — use the 32-bit rcedit under wine (alongside the Win32 Squirrel vendor exes wine already runs), host-arch rcedit on Windows. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/SquirrelWindowsTarget.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 872ad46acde..4879b2aa381 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -35,19 +35,20 @@ export default class SquirrelWindowsTarget extends Target { // already ships a real one). Remove once squirrel.windows bundles it (electron-builder-binaries#203). await prepareNugetExe(tmpVendorDirectory) - // Squirrel.exe --releasify shells out to rcedit.exe (via setPEVersionInfoAndIcon) to embed the app - // icon into Setup.exe. The bundle does not ship rcedit, so resolve it from the win-codesign toolset, - // which already versions and caches it across all platforms. Squirrel-Mono.exe (used on non-Windows) - // does not call rcedit, so this is only needed on win32. + // Both Squirrel.exe and Squirrel-Mono.exe shell out to rcedit.exe (via setPEVersionInfoAndIcon) + // during --releasify to stamp version info and the app icon into Setup.exe, so rcedit must live in + // the vendor dir on every host — on non-Windows it runs under wine alongside the other Win32 vendor + // exes (Setup.exe, StubExecutable.exe, …). The bundle omits rcedit, so resolve it from the + // win-codesign toolset, which already versions and caches it across platforms. // // getRceditBundle honors the user's `toolsets.winCodeSign` selection, so users retain a bypass if a // newer toolset regresses: `"0.0.0"` falls back to the legacy winCodeSign-2.6.0 rcedit, a custom // toolset object uses their own rcedit, and the default (unset/"latest") pulls rcedit-windows-2_0_0. - if (process.platform === "win32") { - const rcedit = await getRceditBundle(this.packager.config.toolsets?.winCodeSign, this.packager.buildResourcesDir) - const rceditExe = os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 - await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) - } + const rcedit = await getRceditBundle(this.packager.config.toolsets?.winCodeSign, this.packager.buildResourcesDir) + // On non-Windows, run the 32-bit rcedit under wine (matching the Win32 vendor exes wine already runs); + // on Windows pick the host arch. + const rceditExe = process.platform !== "win32" || os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 + await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) const files = await fs.promises.readdir(tmpVendorDirectory) const squirrelExe = files.find(f => f === "Squirrel.exe") From c69746bd0593366a658c9077ccba7f2ca9be535b Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 18:28:45 -0700 Subject: [PATCH 18/20] fix(squirrel-windows): provision WiX toolset + template.wxs for MSI builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squirrel's createMsiPackage (msi: true) runs candle.exe/light.exe from its vendor dir and reads template.wxs there; the squirrel.windows bundle ships neither, so MSI builds failed at pathToWixTools(). When msi is requested, download the shared WiX 3.11 toolset (wix-4.0.0.5512.2 — the same candle/light electron-builder's MSI target uses) and merge it into the vendor dir, and ship Squirrel's template.wxs in the package and copy it alongside. Gated on msi so non-MSI builds skip the ~4MB download. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../package.json | 1 + .../src/SquirrelWindowsTarget.ts | 11 +++++- .../src/toolset.ts | 18 +++++++++ .../template.wxs | 39 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/electron-builder-squirrel-windows/template.wxs diff --git a/packages/electron-builder-squirrel-windows/package.json b/packages/electron-builder-squirrel-windows/package.json index df59d1f6041..c01c60a75ee 100644 --- a/packages/electron-builder-squirrel-windows/package.json +++ b/packages/electron-builder-squirrel-windows/package.json @@ -27,6 +27,7 @@ "files": [ "dist", "template.nuspectemplate", + "template.wxs", "install-spinner.gif" ], "engines": { diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 4879b2aa381..a76b8c879dd 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -5,7 +5,7 @@ import { sanitizeFileName } from "builder-util/internal" import * as fs from "fs" import * as os from "os" import * as path from "path" -import { getSquirrelToolsetPath, prepareNugetExe } from "./toolset.js" +import { getSquirrelToolsetPath, getWixToolsetPath, prepareNugetExe } from "./toolset.js" import { InstallerOptions, convertVersion, createWindowsInstaller } from "./windowsInstaller.js" export default class SquirrelWindowsTarget extends Target { @@ -50,6 +50,15 @@ export default class SquirrelWindowsTarget extends Target { const rceditExe = process.platform !== "win32" || os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) + // When building an MSI, Squirrel's createMsiPackage runs candle.exe/light.exe from the vendor dir and + // reads template.wxs there. The bundle ships neither, so merge the shared WiX 3.11 toolset (the same + // candle/light electron-builder's MSI target uses) into the vendor dir, plus the Squirrel MSI template. + if (this.options.msi) { + const wixToolset = await getWixToolsetPath() + await fs.promises.cp(wixToolset, tmpVendorDirectory, { recursive: true }) + await fs.promises.copyFile(path.resolve(import.meta.dirname, "..", "template.wxs"), path.join(tmpVendorDirectory, "template.wxs")) + } + const files = await fs.promises.readdir(tmpVendorDirectory) const squirrelExe = files.find(f => f === "Squirrel.exe") if (squirrelExe) { diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index 75d9f36233e..60ed6d8d24b 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -33,6 +33,24 @@ export async function getSquirrelToolsetPath(toolset: ToolsetConfig["squirrel"], }) } +// Squirrel's createMsiPackage (msi: true) runs candle.exe/light.exe with `-ext WixNetFxExtension` from +// its own vendor dir. The squirrel.windows bundle omits the WiX toolchain, so reuse the shared WiX 3.11 +// toolset (the same candle/light electron-builder's MSI target uses) and merge it into the vendor dir. +const WIX_TOOLSET_FILE = "wix-4.0.0.5512.2.7z" +const WIX_TOOLSET_SHA256 = "fe677fcd837b18c9b912985d91636bbd8a1e800c3b3a6a841b6f96e89624e839" + +/** + * Returns the path to the WiX 3.11 toolset directory (candle.exe, light.exe, WixNetFxExtension.dll, …). + * Only needed when building an MSI. + */ +export async function getWixToolsetPath(): Promise { + return downloadBuilderToolset({ + releaseName: "wix-4.0.0.5512.2", + filenameWithExt: WIX_TOOLSET_FILE, + checksums: { [WIX_TOOLSET_FILE]: WIX_TOOLSET_SHA256 }, + }) +} + // TEMPORARY: nuget.exe override. The published squirrel.windows@1.1.0 bundle ships the Chocolatey // shim for nuget.exe, which resolves the real binary relative to its own install path and fails once // relocated to a temp vendor directory. Until squirrel.windows bundles the standalone portable exe diff --git a/packages/electron-builder-squirrel-windows/template.wxs b/packages/electron-builder-squirrel-windows/template.wxs new file mode 100644 index 00000000000..22d6d69ca93 --- /dev/null +++ b/packages/electron-builder-squirrel-windows/template.wxs @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f4658e7b9f880a51081f3a942dcd2759a3b8f5d3 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 18:35:52 -0700 Subject: [PATCH 19/20] fix(squirrel-windows): host-arch rcedit (arm64 mac needs x64); throw on missing Squirrel.exe - Select rcedit by host arch (x64 for x64/arm64, x86 only for ia32) instead of forcing 32-bit on non-Windows: the 32-bit build fails on arm64 macOS even under Rosetta (which translates x64, not x86). - Throw when Squirrel.exe is absent from the vendor dir instead of warn-and-skip: it always ships in the bundle, and skipping signing both produces a broken installer and bypasses the forceCodeSigning enforcement in signIf. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/SquirrelWindowsTarget.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index a76b8c879dd..d8a6839e5d8 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -45,9 +45,10 @@ export default class SquirrelWindowsTarget extends Target { // newer toolset regresses: `"0.0.0"` falls back to the legacy winCodeSign-2.6.0 rcedit, a custom // toolset object uses their own rcedit, and the default (unset/"latest") pulls rcedit-windows-2_0_0. const rcedit = await getRceditBundle(this.packager.config.toolsets?.winCodeSign, this.packager.buildResourcesDir) - // On non-Windows, run the 32-bit rcedit under wine (matching the Win32 vendor exes wine already runs); - // on Windows pick the host arch. - const rceditExe = process.platform !== "win32" || os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 + // Pick rcedit by host arch (x64 for x64/arm64, x86 only for ia32). On non-Windows it runs under wine: + // the 32-bit build fails on arm64 macOS even under Rosetta (which translates x64, not x86), so the x64 + // build is required there. + const rceditExe = os.arch() === "ia32" ? rcedit.x86 : rcedit.x64 await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) // When building an MSI, Squirrel's createMsiPackage runs candle.exe/light.exe from the vendor dir and @@ -59,15 +60,17 @@ export default class SquirrelWindowsTarget extends Target { await fs.promises.copyFile(path.resolve(import.meta.dirname, "..", "template.wxs"), path.join(tmpVendorDirectory, "template.wxs")) } + // Squirrel.exe is the core updater binary the bundle always ships; a missing one means a broken + // vendor toolset. Fail loudly rather than skipping signing — skipping would produce a broken + // installer and bypass the forceCodeSigning enforcement inside signIf (its awaited promise rejects + // when the file can't be signed and forceCodeSigning is set). const files = await fs.promises.readdir(tmpVendorDirectory) - const squirrelExe = files.find(f => f === "Squirrel.exe") - if (squirrelExe) { - const filePath = path.join(tmpVendorDirectory, squirrelExe) - log.debug({ file: filePath }, "signing vendor executable") - await this.packager.signIf(filePath) - } else { - log.warn("Squirrel.exe not found in vendor directory, skipping signing") + if (!files.includes("Squirrel.exe")) { + throw new Error(`Squirrel.exe not found in vendor directory: ${tmpVendorDirectory}`) } + const squirrelExePath = path.join(tmpVendorDirectory, "Squirrel.exe") + log.debug({ file: squirrelExePath }, "signing vendor executable") + await this.packager.signIf(squirrelExePath) return tmpVendorDirectory } From 921b53f5750bc8391ae784dd423ef2a1c2e89843 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 20 Jun 2026 20:38:45 -0700 Subject: [PATCH 20/20] fix(squirrel-windows): author Squirrel MSI template against the WiX v4 namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared wix-4.0.0.5512.2 toolset (the one electron-builder's MSI target uses) is a transitional WiX 4: its candle accepts the v3-style element structure but requires the v4 namespace, so Squirrel's WiX 3 template was rejected with CNDL0199. Swap template.wxs to http://wixtoolset.org/schemas/v4/wxs (dropping the unused util/netfx namespace decls — only the core is used, supplied by candle's -ext WixNetFxExtension). Structure is unchanged, matching MsiTarget's template which compiles with the same toolset. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/SquirrelWindowsTarget.ts | 5 +++-- packages/electron-builder-squirrel-windows/src/toolset.ts | 6 ++++-- packages/electron-builder-squirrel-windows/template.wxs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index d8a6839e5d8..7265183ff53 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -52,8 +52,9 @@ export default class SquirrelWindowsTarget extends Target { await fs.promises.copyFile(rceditExe, path.join(tmpVendorDirectory, "rcedit.exe")) // When building an MSI, Squirrel's createMsiPackage runs candle.exe/light.exe from the vendor dir and - // reads template.wxs there. The bundle ships neither, so merge the shared WiX 3.11 toolset (the same - // candle/light electron-builder's MSI target uses) into the vendor dir, plus the Squirrel MSI template. + // reads template.wxs there. The bundle ships neither, so merge the shared WiX toolset (the same + // candle/light electron-builder's MSI target uses) into the vendor dir, plus the Squirrel MSI template + // (authored against the v4 namespace this transitional WiX 4 requires). if (this.options.msi) { const wixToolset = await getWixToolsetPath() await fs.promises.cp(wixToolset, tmpVendorDirectory, { recursive: true }) diff --git a/packages/electron-builder-squirrel-windows/src/toolset.ts b/packages/electron-builder-squirrel-windows/src/toolset.ts index 60ed6d8d24b..929bdbf0eb3 100644 --- a/packages/electron-builder-squirrel-windows/src/toolset.ts +++ b/packages/electron-builder-squirrel-windows/src/toolset.ts @@ -34,13 +34,15 @@ export async function getSquirrelToolsetPath(toolset: ToolsetConfig["squirrel"], } // Squirrel's createMsiPackage (msi: true) runs candle.exe/light.exe with `-ext WixNetFxExtension` from -// its own vendor dir. The squirrel.windows bundle omits the WiX toolchain, so reuse the shared WiX 3.11 +// its own vendor dir. The squirrel.windows bundle omits the WiX toolchain, so reuse the shared WiX // toolset (the same candle/light electron-builder's MSI target uses) and merge it into the vendor dir. +// This is a transitional WiX 4 (4.0.0.5512): it accepts the v3-style element structure but requires the +// v4 namespace, so template.wxs is authored against http://wixtoolset.org/schemas/v4/wxs. const WIX_TOOLSET_FILE = "wix-4.0.0.5512.2.7z" const WIX_TOOLSET_SHA256 = "fe677fcd837b18c9b912985d91636bbd8a1e800c3b3a6a841b6f96e89624e839" /** - * Returns the path to the WiX 3.11 toolset directory (candle.exe, light.exe, WixNetFxExtension.dll, …). + * Returns the path to the WiX toolset directory (candle.exe, light.exe, WixNetFxExtension.dll, …). * Only needed when building an MSI. */ export async function getWixToolsetPath(): Promise { diff --git a/packages/electron-builder-squirrel-windows/template.wxs b/packages/electron-builder-squirrel-windows/template.wxs index 22d6d69ca93..d11bd1e1c6d 100644 --- a/packages/electron-builder-squirrel-windows/template.wxs +++ b/packages/electron-builder-squirrel-windows/template.wxs @@ -1,4 +1,4 @@ - +