diff --git a/.changeset/beige-dolls-boil.md b/.changeset/beige-dolls-boil.md new file mode 100644 index 00000000000..2cc2eba61da --- /dev/null +++ b/.changeset/beige-dolls-boil.md @@ -0,0 +1,5 @@ +--- +"electron-builder-squirrel-windows": patch +--- + +Sign the vendor directory instead of using Squirrel.Windows' signing method diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 95e37acbaad..fc67b27a5e7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -169,7 +169,7 @@ jobs: fail-fast: false matrix: testFiles: - - winCodeSignTest,differentialUpdateTest + - winCodeSignTest,differentialUpdateTest,squirrelWindowsTest - appxTest,msiTest,portableTest,assistedInstallerTest,protonTest - BuildTest,oneClickInstallerTest,winPackagerTest,nsisUpdaterTest,webInstallerTest steps: diff --git a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts index 6e23c93778b..0d83498b26e 100644 --- a/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts +++ b/packages/electron-builder-squirrel-windows/src/SquirrelWindowsTarget.ts @@ -1,17 +1,14 @@ import { InvalidConfigurationError, log, isEmptyOrSpaces } from "builder-util" -import { Arch, getArchSuffix, SquirrelWindowsOptions, Target } from "app-builder-lib" -import { WinPackager } from "app-builder-lib/out/winPackager" import { sanitizeFileName } from "builder-util/out/filename" +import { Arch, getArchSuffix, SquirrelWindowsOptions, Target, WinPackager } from "app-builder-lib" import * as path from "path" import * as fs from "fs" -import { readFile, writeFile } from "fs/promises" +import * as os from "os" import { Options as SquirrelOptions, createWindowsInstaller, convertVersion } from "electron-winstaller" export default class SquirrelWindowsTarget extends Target { //tslint:disable-next-line:no-object-literal-type-assertion readonly options: SquirrelWindowsOptions = { ...this.packager.platformSpecificBuildOptions, ...this.packager.config.squirrelWindows } as SquirrelWindowsOptions - private appDirectory: string = "" - private outputDirectory: string = "" constructor( private readonly packager: WinPackager, @@ -20,6 +17,32 @@ export default class SquirrelWindowsTarget extends Target { super("squirrel") } + private async prepareSignedVendorDirectory(): Promise { + // 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. + let vendorDirectory = this.options.customSquirrelVendorDir || path.join(require.resolve("electron-winstaller/package.json"), "..", "vendor") + if (isEmptyOrSpaces(vendorDirectory) || !fs.existsSync(vendorDirectory)) { + log.warn({ vendorDirectory }, "unable to access Squirrel.Windows vendor directory, falling back to default electron-winstaller") + vendorDirectory = path.join(require.resolve("electron-winstaller/package.json"), "..", "vendor") + } + + const tmpVendorDirectory = await this.packager.info.tempDirManager.createTempDir({ prefix: "squirrel-windows-vendor" }) + // Copy entire vendor directory to temp directory + await fs.promises.cp(vendorDirectory, tmpVendorDirectory, { recursive: true }) + log.debug({ from: vendorDirectory, to: tmpVendorDirectory }, "copied vendor directory") + + const files = await fs.promises.readdir(tmpVendorDirectory) + for (const file of files) { + if (["Squirrel.exe", "StubExecutable.exe"].includes(file)) { + const filePath = path.join(tmpVendorDirectory, file) + log.debug({ file: filePath }, "signing vendor executable") + await this.packager.sign(filePath) + } + } + + return tmpVendorDirectory + } + async build(appOutDir: string, arch: Arch) { const packager = this.packager const version = packager.appInfo.version @@ -28,6 +51,7 @@ export default class SquirrelWindowsTarget extends Target { const setupFile = packager.expandArtifactNamePattern(this.options, "exe", arch, "${productName} Setup ${version}.${ext}") const installerOutDir = path.join(this.outDir, `squirrel-windows${getArchSuffix(arch)}`) const artifactPath = path.join(installerOutDir, setupFile) + const msiArtifactPath = path.join(installerOutDir, packager.expandArtifactNamePattern(this.options, "msi", arch, "${productName} Setup ${version}.${ext}")) await packager.info.callArtifactBuildStarted({ targetPresentableName: "Squirrel.Windows", @@ -35,27 +59,34 @@ export default class SquirrelWindowsTarget extends Target { arch, }) - if (arch === Arch.ia32) { - log.warn("For windows consider only distributing 64-bit or use nsis target, see https://github.com/electron-userland/electron-builder/issues/359#issuecomment-214851130") - } + const distOptions = await this.computeEffectiveDistOptions(appOutDir, installerOutDir, setupFile) + await createWindowsInstaller(distOptions) - this.appDirectory = appOutDir - this.outputDirectory = installerOutDir - const distOptions = await this.computeEffectiveDistOptions() - if (distOptions.vendorDirectory) { - this.select7zipArch(distOptions.vendorDirectory, arch) + await packager.signAndEditResources(artifactPath, arch, installerOutDir) + if (this.options.msi) { + await packager.sign(msiArtifactPath) } - await createWindowsInstaller(distOptions) + const safeArtifactName = (ext: string) => `${sanitizedName}-Setup-${version}${getArchSuffix(arch)}.${ext}` await packager.info.callArtifactBuildCompleted({ file: artifactPath, target: this, arch, - safeArtifactName: `${sanitizedName}-Setup-${version}${getArchSuffix(arch)}.exe`, + safeArtifactName: safeArtifactName("exe"), packager: this.packager, }) + if (this.options.msi) { + await packager.info.callArtifactBuildCompleted({ + file: msiArtifactPath, + target: this, + arch, + safeArtifactName: safeArtifactName("msi"), + packager: this.packager, + }) + } + const packagePrefix = `${this.appName}-${convertVersion(version)}-` packager.info.dispatchArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}full.nupkg`), @@ -84,14 +115,30 @@ export default class SquirrelWindowsTarget extends Target { return this.options.name || this.packager.appInfo.name } - private select7zipArch(vendorDirectory: string, arch: Arch) { - // Copy the 7-Zip executable for the configured architecture. - const resolvedArch = getArchSuffix(arch) === "" ? process.arch : getArchSuffix(arch) + 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 + 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")) } - async computeEffectiveDistOptions(): Promise { + private async createNuspecTemplateWithProjectUrl() { + const templatePath = path.resolve(__dirname, "..", "template.nuspectemplate") + const projectUrl = await this.packager.appInfo.computePackageUrl() + if (projectUrl != null) { + const nuspecTemplate = await this.packager.info.tempDirManager.getTempFile({ prefix: "template", suffix: ".nuspectemplate" }) + let templateContent = await fs.promises.readFile(templatePath, "utf8") + const searchString = "<%- copyright %>" + templateContent = templateContent.replace(searchString, `${searchString}\n ${projectUrl}`) + await fs.promises.writeFile(nuspecTemplate, templateContent) + return nuspecTemplate + } + return templatePath + } + + async computeEffectiveDistOptions(appDirectory: string, outputDirectory: string, setupFile: string): Promise { const packager = this.packager let iconUrl = this.options.iconUrl if (iconUrl == null) { @@ -106,47 +153,29 @@ export default class SquirrelWindowsTarget extends Target { } checkConflictingOptions(this.options) - const appInfo = packager.appInfo - // 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. - let vendorDirectory = this.options.customSquirrelVendorDir - if (isEmptyOrSpaces(vendorDirectory) || !fs.existsSync(vendorDirectory)) { - log.warn({ vendorDirectory }, "unable to access Squirrel.Windows vendor directory, falling back to default electron-winstaller") - vendorDirectory = undefined - } - const options: SquirrelOptions = { - appDirectory: this.appDirectory, - outputDirectory: this.outputDirectory, + 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.packager.platformSpecificBuildOptions.executableName || this.options.name || appInfo.productName}.exe`, + exe: `${appInfo.productFilename || this.options.name || appInfo.productName}.exe`, authors: appInfo.companyName || "", + nuspecTemplate: await this.createNuspecTemplateWithProjectUrl(), iconUrl, copyright: appInfo.copyright, - vendorDirectory, - nuspecTemplate: path.join(__dirname, "..", "template.nuspectemplate"), noMsi: !this.options.msi, + usePackageJson: false, } - const projectUrl = await appInfo.computePackageUrl() - if (projectUrl != null) { - const nuspecTemplate = await this.packager.info.tempDirManager.getTempFile({ prefix: "template", suffix: ".nuspectemplate" }) - let templateContent = await readFile(path.resolve(__dirname, "..", "template.nuspectemplate"), "utf8") - const searchString = "<%- copyright %>" - templateContent = templateContent.replace(searchString, `${searchString}\n ${projectUrl}`) - await writeFile(nuspecTemplate, templateContent) - options.nuspecTemplate = nuspecTemplate - } - - if (await (await packager.signingManager.value).cscInfo.value) { - options.windowsSign = { - hookFunction: async (file: string) => { - await packager.sign(file) - }, - } + 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)) { diff --git a/test/snapshots/windows/squirrelWindowsTest.js.snap b/test/snapshots/windows/squirrelWindowsTest.js.snap index fd8244e4c63..065e4078b90 100644 --- a/test/snapshots/windows/squirrelWindowsTest.js.snap +++ b/test/snapshots/windows/squirrelWindowsTest.js.snap @@ -22,6 +22,8 @@ exports[`Squirrel.Windows 1`] = ` exports[`Squirrel.Windows 2`] = ` [ + "lib/", + "lib/net45/", "lib/net45/chrome_100_percent.pak", "lib/net45/chrome_200_percent.pak", "lib/net45/d3dcompiler_47.dll", @@ -33,19 +35,23 @@ exports[`Squirrel.Windows 2`] = ` "lib/net45/LICENSES.chromium.html", "lib/net45/resources.pak", "lib/net45/snapshot_blob.bin", - "lib/net45/Test App ßW.exe", - "lib/net45/Test App ßW_ExecutionStub.exe", - "lib/net45/Update.exe", + "lib/net45/squirrel.exe", + "lib/net45/test with spaces.exe", + "lib/net45/test with spaces_ExecutionStub.exe", "lib/net45/v8_context_snapshot.bin", "lib/net45/vk_swiftshader.dll", "lib/net45/vk_swiftshader_icd.json", "lib/net45/vulkan-1.dll", "lib/net45/locales/en-US.pak", + "lib/net45/resources/", "lib/net45/resources/app.asar", - "lib/net45/swiftshader/libEGL.dll", - "lib/net45/swiftshader/libGLESv2.dll", + "package/", + "package/services/", + "package/services/metadata/", + "package/services/metadata/core-properties/", "TestApp.nuspec", "[Content_Types].xml", + "_rels/", "_rels/.rels", ] `; @@ -77,6 +83,190 @@ exports[`artifactName 1`] = ` exports[`artifactName 2`] = ` [ + "lib/", + "lib/net45/", + "lib/net45/chrome_100_percent.pak", + "lib/net45/chrome_200_percent.pak", + "lib/net45/d3dcompiler_47.dll", + "lib/net45/ffmpeg.dll", + "lib/net45/icudtl.dat", + "lib/net45/libEGL.dll", + "lib/net45/libGLESv2.dll", + "lib/net45/LICENSE.electron.txt", + "lib/net45/LICENSES.chromium.html", + "lib/net45/resources.pak", + "lib/net45/snapshot_blob.bin", + "lib/net45/squirrel.exe", + "lib/net45/Test App ßW.exe", + "lib/net45/Test App ßW_ExecutionStub.exe", + "lib/net45/v8_context_snapshot.bin", + "lib/net45/vk_swiftshader.dll", + "lib/net45/vk_swiftshader_icd.json", + "lib/net45/vulkan-1.dll", + "lib/net45/locales/en-US.pak", + "lib/net45/resources/", + "lib/net45/resources/app.asar", + "package/", + "package/services/", + "package/services/metadata/", + "package/services/metadata/core-properties/", + "TestApp.nuspec", + "[Content_Types].xml", + "_rels/", + "_rels/.rels", +] +`; + +exports[`squirrel window arm64 msi 1`] = ` +{ + "win": [ + { + "arch": "arm64", + "file": "RELEASES", + }, + { + "arch": "arm64", + "file": "Test App ßW Setup 1.1.0.exe", + "safeArtifactName": "TestApp-Setup-1.1.0-arm64.exe", + }, + { + "arch": "arm64", + "file": "Test App ßW Setup 1.1.0.msi", + "safeArtifactName": "TestApp-Setup-1.1.0-arm64.msi", + }, + { + "arch": "arm64", + "file": "TestApp-1.1.0-full.nupkg", + }, + ], +} +`; + +exports[`squirrel window arm64 msi 2`] = ` +[ + "lib/", + "lib/net45/", + "lib/net45/chrome_100_percent.pak", + "lib/net45/chrome_200_percent.pak", + "lib/net45/ffmpeg.dll", + "lib/net45/icudtl.dat", + "lib/net45/libEGL.dll", + "lib/net45/libGLESv2.dll", + "lib/net45/LICENSE.electron.txt", + "lib/net45/LICENSES.chromium.html", + "lib/net45/resources.pak", + "lib/net45/snapshot_blob.bin", + "lib/net45/squirrel.exe", + "lib/net45/Test App ßW.exe", + "lib/net45/Test App ßW_ExecutionStub.exe", + "lib/net45/v8_context_snapshot.bin", + "lib/net45/vk_swiftshader.dll", + "lib/net45/vk_swiftshader_icd.json", + "lib/net45/vulkan-1.dll", + "lib/net45/locales/en-US.pak", + "lib/net45/resources/", + "lib/net45/resources/app.asar", + "package/", + "package/services/", + "package/services/metadata/", + "package/services/metadata/core-properties/", + "TestApp.nuspec", + "[Content_Types].xml", + "_rels/", + "_rels/.rels", +] +`; + +exports[`squirrel window ia32 msi 1`] = ` +{ + "win": [ + { + "arch": "ia32", + "file": "RELEASES", + }, + { + "arch": "ia32", + "file": "Test App ßW Setup 1.1.0.exe", + "safeArtifactName": "TestApp-Setup-1.1.0-ia32.exe", + }, + { + "arch": "ia32", + "file": "Test App ßW Setup 1.1.0.msi", + "safeArtifactName": "TestApp-Setup-1.1.0-ia32.msi", + }, + { + "arch": "ia32", + "file": "TestApp-1.1.0-full.nupkg", + }, + ], +} +`; + +exports[`squirrel window ia32 msi 2`] = ` +[ + "lib/", + "lib/net45/", + "lib/net45/chrome_100_percent.pak", + "lib/net45/chrome_200_percent.pak", + "lib/net45/d3dcompiler_47.dll", + "lib/net45/ffmpeg.dll", + "lib/net45/icudtl.dat", + "lib/net45/libEGL.dll", + "lib/net45/libGLESv2.dll", + "lib/net45/LICENSE.electron.txt", + "lib/net45/LICENSES.chromium.html", + "lib/net45/resources.pak", + "lib/net45/snapshot_blob.bin", + "lib/net45/squirrel.exe", + "lib/net45/Test App ßW.exe", + "lib/net45/Test App ßW_ExecutionStub.exe", + "lib/net45/v8_context_snapshot.bin", + "lib/net45/vk_swiftshader.dll", + "lib/net45/vk_swiftshader_icd.json", + "lib/net45/vulkan-1.dll", + "lib/net45/locales/en-US.pak", + "lib/net45/resources/", + "lib/net45/resources/app.asar", + "package/", + "package/services/", + "package/services/metadata/", + "package/services/metadata/core-properties/", + "TestApp.nuspec", + "[Content_Types].xml", + "_rels/", + "_rels/.rels", +] +`; + +exports[`squirrel window x64 msi 1`] = ` +{ + "win": [ + { + "arch": "x64", + "file": "RELEASES", + }, + { + "arch": "x64", + "file": "Test App ßW Setup 1.1.0.exe", + "safeArtifactName": "TestApp-Setup-1.1.0.exe", + }, + { + "arch": "x64", + "file": "Test App ßW Setup 1.1.0.msi", + "safeArtifactName": "TestApp-Setup-1.1.0.msi", + }, + { + "arch": "x64", + "file": "TestApp-1.1.0-full.nupkg", + }, + ], +} +`; + +exports[`squirrel window x64 msi 2`] = ` +[ + "lib/", + "lib/net45/", "lib/net45/chrome_100_percent.pak", "lib/net45/chrome_200_percent.pak", "lib/net45/d3dcompiler_47.dll", @@ -88,19 +278,23 @@ exports[`artifactName 2`] = ` "lib/net45/LICENSES.chromium.html", "lib/net45/resources.pak", "lib/net45/snapshot_blob.bin", + "lib/net45/squirrel.exe", "lib/net45/Test App ßW.exe", "lib/net45/Test App ßW_ExecutionStub.exe", - "lib/net45/Update.exe", "lib/net45/v8_context_snapshot.bin", "lib/net45/vk_swiftshader.dll", "lib/net45/vk_swiftshader_icd.json", "lib/net45/vulkan-1.dll", "lib/net45/locales/en-US.pak", + "lib/net45/resources/", "lib/net45/resources/app.asar", - "lib/net45/swiftshader/libEGL.dll", - "lib/net45/swiftshader/libGLESv2.dll", + "package/", + "package/services/", + "package/services/metadata/", + "package/services/metadata/core-properties/", "TestApp.nuspec", "[Content_Types].xml", + "_rels/", "_rels/.rels", ] `; diff --git a/test/src/helpers/CheckingPackager.ts b/test/src/helpers/CheckingPackager.ts index c039dcd5ca4..e3a3faf87fd 100644 --- a/test/src/helpers/CheckingPackager.ts +++ b/test/src/helpers/CheckingPackager.ts @@ -1,12 +1,12 @@ import { SignOptions as MacSignOptions } from "@electron/osx-sign/dist/cjs/types" import { Identity } from "app-builder-lib/out/codeSign/macCodeSign" -import { MacPackager } from "app-builder-lib/out/macPackager" import { DoPackOptions } from "app-builder-lib/out/platformPackager" -import { WinPackager } from "app-builder-lib/out/winPackager" +import { WinPackager, getArchSuffix, MacPackager } from "app-builder-lib" import { AsyncTaskManager } from "builder-util" import { DmgTarget } from "dmg-builder" import { Arch, MacConfiguration, Packager, Target } from "electron-builder" import SquirrelWindowsTarget from "electron-builder-squirrel-windows" +import * as path from "path" export class CheckingWinPackager extends WinPackager { effectiveDistOptions: any @@ -19,8 +19,10 @@ export class CheckingWinPackager extends WinPackager { async pack(outDir: string, arch: Arch, targets: Array, taskManager: AsyncTaskManager): Promise { // skip pack const helperClass: typeof SquirrelWindowsTarget = require("electron-builder-squirrel-windows").default - this.effectiveDistOptions = await new helperClass(this, outDir).computeEffectiveDistOptions() - + const newClass = new helperClass(this, outDir) + const setupFile = this.expandArtifactNamePattern(newClass.options, "exe", arch, "${productName} Setup ${version}.${ext}") + const installerOutDir = path.join(outDir, `squirrel-windows${getArchSuffix(arch)}`) + this.effectiveDistOptions = await newClass.computeEffectiveDistOptions(installerOutDir, outDir, setupFile) await this.sign(this.computeAppOutDir(outDir, arch)) } diff --git a/test/src/windows/squirrelWindowsTest.ts b/test/src/windows/squirrelWindowsTest.ts index 25cb0a7a601..f87342006a7 100644 --- a/test/src/windows/squirrelWindowsTest.ts +++ b/test/src/windows/squirrelWindowsTest.ts @@ -12,7 +12,7 @@ test.ifAll.ifNotCiMac( win: { compression: "normal", }, - executableName: " test with spaces", + executableName: "test with spaces", electronFuses: { runAsNode: true, enableCookieEncryption: true, @@ -56,6 +56,51 @@ test.skip( }) ) +test.ifAll( + "squirrel window arm64 msi", + app( + { + targets: Platform.WINDOWS.createTarget("squirrel", Arch.arm64), + config: { + squirrelWindows: { + msi: true, + }, + }, + }, + { signedWin: true } + ) +) + +test.ifAll( + "squirrel window x64 msi", + app( + { + targets: Platform.WINDOWS.createTarget("squirrel", Arch.x64), + config: { + squirrelWindows: { + msi: true, + }, + }, + }, + { signedWin: true } + ) +) + +test.ifAll( + "squirrel window ia32 msi", + app( + { + targets: Platform.WINDOWS.createTarget("squirrel", Arch.ia32), + config: { + squirrelWindows: { + msi: true, + }, + }, + }, + { signedWin: true } + ) +) + test.ifAll("detect install-spinner", () => { let platformPackager: CheckingWinPackager | null = null let loadingGifPath: string | null = null