UNPKG

electron-builder-squirrel-windows

Version:
289 lines 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const builder_util_1 = require("builder-util"); const binDownload_1 = require("app-builder-lib/out/binDownload"); const filename_1 = require("builder-util/out/filename"); const app_builder_lib_1 = require("app-builder-lib"); const toolsetLock_1 = require("app-builder-lib/out/util/toolsetLock"); const path = require("path"); const fs = require("fs"); const os = require("os"); const electron_winstaller_1 = require("electron-winstaller"); const WineVm_1 = require("app-builder-lib/out/vm/WineVm"); class SquirrelWindowsTarget extends app_builder_lib_1.Target { constructor(packager, outDir) { super("squirrel"); this.packager = packager; this.outDir = outDir; //tslint:disable-next-line:no-object-literal-type-assertion this.options = { ...this.packager.platformSpecificBuildOptions, ...this.packager.config.squirrelWindows }; this.isAsyncSupported = false; } async prepareSignedVendorDirectory() { const customSquirrelVendorDirectory = this.options.customSquirrelVendorDir; const tmpVendorDirectory = await this.packager.info.tempDirManager.createTempDir({ prefix: "squirrel-windows-vendor" }); if (customSquirrelVendorDirectory && (await (0, builder_util_1.exists)(customSquirrelVendorDirectory))) { await fs.promises.cp(customSquirrelVendorDirectory, tmpVendorDirectory, { recursive: true }); } else { if (!(0, builder_util_1.isEmptyOrSpaces)(customSquirrelVendorDirectory)) { builder_util_1.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([ (0, binDownload_1.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 }), ]); await fs.promises.cp(path.join(squirrelBin, "electron-winstaller", "vendor"), tmpVendorDirectory, { recursive: true }); } const files = await fs.promises.readdir(tmpVendorDirectory); const squirrelExe = files.find(f => f === "Squirrel.exe"); if (squirrelExe) { const filePath = path.join(tmpVendorDirectory, squirrelExe); builder_util_1.log.debug({ file: filePath }, "signing vendor executable"); await this.packager.signIf(filePath); } else { builder_util_1.log.warn("Squirrel.exe not found in vendor directory, skipping signing"); } return tmpVendorDirectory; } assertShellSafePath(filePath, description) { if (/[\r\n`$;&|<>]/.test(filePath)) { throw new builder_util_1.InvalidConfigurationError(`${description} contains unsafe shell characters: ${filePath}`); } } async ensurePathInside(baseDir, targetPath, description) { const resolvedBaseDir = path.resolve(baseDir); const resolvedTargetPath = path.resolve(targetPath); let canonicalBaseDir = resolvedBaseDir; let canonicalTargetPath = resolvedTargetPath; try { canonicalBaseDir = await fs.promises.realpath(resolvedBaseDir); } catch { canonicalBaseDir = resolvedBaseDir; } try { canonicalTargetPath = await fs.promises.realpath(resolvedTargetPath); } catch { // Target may not exist yet; resolve the parent to handle symlinks/junctions consistently try { const resolvedTargetParent = path.dirname(resolvedTargetPath); const canonicalTargetParent = await fs.promises.realpath(resolvedTargetParent); const relativeFromResolvedParent = path.relative(resolvedTargetParent, resolvedTargetPath); if ((0, builder_util_1.isEmptyOrSpaces)(relativeFromResolvedParent) || path.isAbsolute(relativeFromResolvedParent) || relativeFromResolvedParent.split(path.sep).includes("..") || /[\0\r\n]/.test(relativeFromResolvedParent)) { throw new builder_util_1.InvalidConfigurationError(`${description} contains invalid path segments`); } canonicalTargetPath = path.resolve(canonicalTargetParent, relativeFromResolvedParent); } catch { canonicalTargetPath = resolvedTargetPath; } } const relativePath = path.relative(canonicalBaseDir, canonicalTargetPath); if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) { throw new builder_util_1.InvalidConfigurationError(`${description} must be inside ${canonicalBaseDir}`); } this.assertShellSafePath(canonicalTargetPath, description); return canonicalTargetPath; } async generateStubExecutableExe(appOutDir, vendorDir) { var _a; if (!path.isAbsolute(appOutDir) || !path.isAbsolute(vendorDir)) { throw new builder_util_1.InvalidConfigurationError("appOutDir and vendorDir must be absolute paths"); } const files = await fs.promises.readdir(appOutDir, { withFileTypes: true }); const appExe = files.find(f => f.name === `${this.exeName}.exe`); if (!appExe) { throw new Error(`App executable not found in app directory: ${appOutDir}`); } const filePath = await this.ensurePathInside(appOutDir, path.join(appOutDir, appExe.name), "App executable path"); const stubExePath = await this.ensurePathInside(appOutDir, path.join(appOutDir, `${this.exeName}_ExecutionStub.exe`), "Stub executable path"); const stubExecutableSource = await this.ensurePathInside(vendorDir, path.join(vendorDir, "StubExecutable.exe"), "Stub executable source"); const writeZipToSetupExe = await this.ensurePathInside(vendorDir, path.join(vendorDir, "WriteZipToSetup.exe"), "WriteZipToSetup executable"); await fs.promises.copyFile(stubExecutableSource, stubExePath); const wineVm = new WineVm_1.WineVmManager((_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.wine); await wineVm.exec(writeZipToSetupExe, ["--copy-stub-resources", filePath, stubExePath]); await this.packager.signIf(stubExePath); builder_util_1.log.debug({ file: filePath }, "signing app executable"); await this.packager.signIf(filePath); } async build(appOutDir, arch) { const packager = this.packager; const version = packager.appInfo.version; const sanitizedName = (0, filename_1.sanitizeFileName)(this.appName); const setupFile = packager.expandArtifactNamePattern(this.options, "exe", arch, "${productName} Setup ${version}.${ext}"); const installerOutDir = path.join(this.outDir, `squirrel-windows${(0, app_builder_lib_1.getArchSuffix)(arch)}`); const artifactPath = path.join(installerOutDir, setupFile); const msiArtifactPath = path.join(installerOutDir, packager.expandArtifactNamePattern(this.options, "msi", arch, "${productName} Setup ${version}.${ext}")); this.buildQueueManager.add(async () => { await packager.info.emitArtifactBuildStarted({ targetPresentableName: "Squirrel.Windows", file: artifactPath, arch, }); const distOptions = await this.computeEffectiveDistOptions(appOutDir, installerOutDir, setupFile); await this.generateStubExecutableExe(appOutDir, distOptions.vendorDirectory); await (0, toolsetLock_1.withToolsetLock)(() => (0, electron_winstaller_1.createWindowsInstaller)(distOptions)); await packager.signAndEditResources(artifactPath, arch, installerOutDir); if (this.options.msi) { await packager.signIf(msiArtifactPath); } const safeArtifactName = (ext) => `${sanitizedName}-Setup-${version}${(0, app_builder_lib_1.getArchSuffix)(arch)}.${ext}`; await packager.info.emitArtifactBuildCompleted({ file: artifactPath, target: this, arch, safeArtifactName: safeArtifactName("exe"), packager: this.packager, }); if (this.options.msi) { await packager.info.emitArtifactCreated({ file: msiArtifactPath, target: this, arch, safeArtifactName: safeArtifactName("msi"), packager: this.packager, }); } const packagePrefix = `${this.appName}-${(0, electron_winstaller_1.convertVersion)(version)}-`; await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}full.nupkg`), target: this, arch, packager, }); if (distOptions.remoteReleases != null) { await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, `${packagePrefix}delta.nupkg`), target: this, arch, packager, }); } await packager.info.emitArtifactCreated({ file: path.join(installerOutDir, "RELEASES"), target: this, arch, packager, }); }); return Promise.resolve(); } get appName() { return this.options.name || this.packager.appInfo.name; } get exeName() { const name = this.packager.appInfo.productFilename || this.options.name || this.packager.appInfo.productName; return (0, filename_1.sanitizeFileName)(name); } select7zipArch(vendorDirectory) { // 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 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(appDirectory, outputDirectory, setupFile) { const packager = this.packager; let iconUrl = this.options.iconUrl; if (iconUrl == null) { const info = await packager.info.repositoryInfo; if (info != null) { iconUrl = `https://github.com/${info.user}/${info.project}/blob/master/${packager.info.relativeBuildResourcesDirname}/icon.ico?raw=true`; } if (iconUrl == null) { throw new builder_util_1.InvalidConfigurationError("squirrelWindows.iconUrl is not specified, please see https://www.electron.build/squirrel-windows#SquirrelWindowsOptions-iconUrl"); } } checkConflictingOptions(this.options); const appInfo = packager.appInfo; const options = { 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 ((0, builder_util_1.isEmptyOrSpaces)(options.description)) { options.description = this.options.name || appInfo.productName; } if (options.remoteToken == null) { options.remoteToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; } if (this.options.remoteReleases === true) { const info = await packager.info.repositoryInfo; if (info == null) { builder_util_1.log.warn("remoteReleases set to true, but cannot get repository info"); } else { options.remoteReleases = `https://github.com/${info.user}/${info.project}`; builder_util_1.log.info({ remoteReleases: options.remoteReleases }, `remoteReleases is set`); } } else if (typeof this.options.remoteReleases === "string" && !(0, builder_util_1.isEmptyOrSpaces)(this.options.remoteReleases)) { options.remoteReleases = this.options.remoteReleases; } if (this.options.loadingGif) { options.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"); } } return options; } } exports.default = SquirrelWindowsTarget; function checkConflictingOptions(options) { for (const name of ["outputDirectory", "appDirectory", "exe", "fixUpPaths", "usePackageJson", "extraFileSpecs", "extraMetadataSpecs", "skipUpdateIcon", "setupExe"]) { if (name in options) { throw new builder_util_1.InvalidConfigurationError(`Option ${name} is ignored, do not specify it.`); } } if ("noMsi" in options) { builder_util_1.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 builder_util_1.InvalidConfigurationError(`msi expected to be boolean value, but string '"${msi}"' was specified`); } } //# sourceMappingURL=SquirrelWindowsTarget.js.map