Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/beige-dolls-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"electron-builder-squirrel-windows": patch
---

Sign the vendor directory instead of using Squirrel.Windows' signing method
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ jobs:
- winCodeSignTest,differentialUpdateTest
- appxTest,msiTest,portableTest,assistedInstallerTest,protonTest
- BuildTest,oneClickInstallerTest,winPackagerTest,nsisUpdaterTest,webInstallerTest
- squirrelWindowsTest
steps:
- name: Checkout code repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
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 { 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,
Expand All @@ -20,6 +16,32 @@ export default class SquirrelWindowsTarget extends Target {
super("squirrel")
}

private async prepareSignedVendorDirectory(): Promise<string> {
// 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
Expand All @@ -28,34 +50,42 @@ 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 = artifactPath.replace(".exe", ".msi")

await packager.info.callArtifactBuildStarted({
targetPresentableName: "Squirrel.Windows",
file: artifactPath,
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")
}
Comment thread
mmaietta marked this conversation as resolved.
const distOptions = await this.computeEffectiveDistOptions(appOutDir, installerOutDir, setupFile, arch)
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)
}
Comment thread
mmaietta marked this conversation as resolved.

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,
})
}
Comment thread
mmaietta marked this conversation as resolved.

const packagePrefix = `${this.appName}-${convertVersion(version)}-`
packager.info.dispatchArtifactCreated({
file: path.join(installerOutDir, `${packagePrefix}full.nupkg`),
Expand Down Expand Up @@ -86,12 +116,26 @@ export default class SquirrelWindowsTarget extends Target {

private select7zipArch(vendorDirectory: string, arch: Arch) {
// Copy the 7-Zip executable for the configured architecture.
const resolvedArch = getArchSuffix(arch) === "" ? process.arch : getArchSuffix(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 resolvedArch = getArchSuffix(arch) || `-${process.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"))
}

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><%- copyright %></copyright>"
templateContent = templateContent.replace(searchString, `${searchString}\n <projectUrl>${projectUrl}</projectUrl>`)
await fs.promises.writeFile(nuspecTemplate, templateContent)
return nuspecTemplate
}
return templatePath
}

async computeEffectiveDistOptions(): Promise<SquirrelOptions> {
async computeEffectiveDistOptions(appDirectory: string, outputDirectory: string, setupFile: string, arch: Arch): Promise<SquirrelOptions> {
const packager = this.packager
let iconUrl = this.options.iconUrl
if (iconUrl == null) {
Expand All @@ -106,47 +150,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,
Comment thread
mmaietta marked this conversation as resolved.
}

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><%- copyright %></copyright>"
templateContent = templateContent.replace(searchString, `${searchString}\n <projectUrl>${projectUrl}</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, arch)
options.fixUpPaths = true
options.setupExe = setupFile
if (this.options.msi) {
options.setupMsi = setupFile.replace(".exe", ".msi")
}

if (isEmptyOrSpaces(options.description)) {
Expand Down
89 changes: 81 additions & 8 deletions test/snapshots/windows/squirrelWindowsTest.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Comment thread
mmaietta marked this conversation as resolved.
"package/",
"package/services/",
"package/services/metadata/",
"package/services/metadata/core-properties/",
Comment thread
mmaietta marked this conversation as resolved.
"TestApp.nuspec",
"[Content_Types].xml",
"_rels/",
"_rels/.rels",
]
`;
Expand Down Expand Up @@ -77,6 +83,69 @@ 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 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 msi 2`] = `
[
"lib/",
"lib/net45/",
"lib/net45/chrome_100_percent.pak",
"lib/net45/chrome_200_percent.pak",
"lib/net45/d3dcompiler_47.dll",
Expand All @@ -88,19 +157,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",
]
`;
10 changes: 6 additions & 4 deletions test/src/helpers/CheckingPackager.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,8 +19,10 @@ export class CheckingWinPackager extends WinPackager {
async pack(outDir: string, arch: Arch, targets: Array<Target>, taskManager: AsyncTaskManager): Promise<any> {
// 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, arch)
await this.sign(this.computeAppOutDir(outDir, arch))
}

Expand Down
Loading