update electron to v43

This commit is contained in:
olcxja 2026-07-09 22:38:33 +02:00
commit fb6c8b6ee9
5385 changed files with 513060 additions and 123058 deletions

View file

@ -1,14 +0,0 @@
import { Arch } from "builder-util";
import { Target } from "../core";
import { LinuxPackager } from "../linuxPackager";
import { AppImageOptions } from "../options/linuxOptions";
import { LinuxTargetHelper } from "./LinuxTargetHelper";
export default class AppImageTarget extends Target {
private readonly packager;
private readonly helper;
readonly outDir: string;
readonly options: AppImageOptions;
private readonly desktopEntry;
constructor(ignored: string, packager: LinuxPackager, helper: LinuxTargetHelper, outDir: string);
build(appOutDir: string, arch: Arch): Promise<any>;
}

View file

@ -1,97 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const builder_util_1 = require("builder-util");
const fs_extra_1 = require("fs-extra");
const lazy_val_1 = require("lazy-val");
const path = require("path");
const core_1 = require("../core");
const PublishManager_1 = require("../publish/PublishManager");
const appBuilder_1 = require("../util/appBuilder");
const license_1 = require("../util/license");
const targetUtil_1 = require("./targetUtil");
// https://unix.stackexchange.com/questions/375191/append-to-sub-directory-inside-squashfs-file
class AppImageTarget extends core_1.Target {
constructor(ignored, packager, helper, outDir) {
super("appImage");
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
this.options = { ...this.packager.platformSpecificBuildOptions, ...this.packager.config[this.name] };
this.desktopEntry = new lazy_val_1.Lazy(() => {
var _a;
const args = ((_a = this.options.executableArgs) === null || _a === void 0 ? void 0 : _a.join(" ")) || "--no-sandbox";
return helper.computeDesktopEntry(this.options, `AppRun ${args} %U`, {
"X-AppImage-Version": `${packager.appInfo.buildVersion}`,
});
});
}
async build(appOutDir, arch) {
const packager = this.packager;
const options = this.options;
// https://github.com/electron-userland/electron-builder/issues/775
// https://github.com/electron-userland/electron-builder/issues/1726
// tslint:disable-next-line:no-invalid-template-strings
const artifactName = packager.expandArtifactNamePattern(options, "AppImage", arch);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
targetPresentableName: "AppImage",
file: artifactPath,
arch,
});
const c = await Promise.all([
this.desktopEntry.value,
this.helper.icons,
PublishManager_1.getAppUpdatePublishConfiguration(packager, arch, false /* in any case validation will be done on publish */),
license_1.getNotLocalizedLicenseFile(options.license, this.packager, ["txt", "html"]),
targetUtil_1.createStageDir(this, packager, arch),
]);
const license = c[3];
const stageDir = c[4];
const publishConfig = c[2];
if (publishConfig != null) {
await fs_extra_1.outputFile(path.join(packager.getResourcesDir(stageDir.dir), "app-update.yml"), builder_util_1.serializeToYaml(publishConfig));
}
if (this.packager.packagerOptions.effectiveOptionComputed != null &&
(await this.packager.packagerOptions.effectiveOptionComputed({ desktop: await this.desktopEntry.value }))) {
return;
}
const args = [
"appimage",
"--stage",
stageDir.dir,
"--arch",
builder_util_1.Arch[arch],
"--output",
artifactPath,
"--app",
appOutDir,
"--configuration",
JSON.stringify({
productName: this.packager.appInfo.productName,
productFilename: this.packager.appInfo.productFilename,
desktopEntry: c[0],
executableName: this.packager.executableName,
icons: c[1],
fileAssociations: this.packager.fileAssociations,
...options,
}),
];
appBuilder_1.objectToArgs(args, {
license,
});
if (packager.compression === "maximum") {
args.push("--compression", "xz");
}
await packager.info.callArtifactBuildCompleted({
file: artifactPath,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "AppImage", arch, false),
target: this,
arch,
packager,
isWriteUpdateInfo: true,
updateInfo: await appBuilder_1.executeAppBuilderAsJson(args),
});
}
}
exports.default = AppImageTarget;
//# sourceMappingURL=AppImageTarget.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,8 @@
export interface Capability {
readonly nsAlias: string | null;
readonly nsURI: string | null;
readonly name: string;
toXMLString(): string;
}
export declare const CAPABILITIES: Capability[];
export declare function isValidCapabilityName(name: string | null | undefined): boolean;

View file

@ -0,0 +1,258 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CAPABILITIES = void 0;
exports.isValidCapabilityName = isValidCapabilityName;
class AppxCapability {
constructor(nsAlias, nsURI, declareNS, elementName, name) {
this.nsAlias = nsAlias;
this.nsURI = nsURI;
this.declareNS = declareNS;
this.elementName = elementName;
this.name = name;
if (!this.nsAlias && this.declareNS) {
throw new Error("local declaration of namespace without prefix is not supported");
}
}
toXMLString() {
const tagName = this.nsAlias ? `${this.nsAlias}:${this.elementName}` : this.elementName;
if (this.declareNS) {
return `<${tagName} xmlns:${this.nsAlias}="${this.nsURI}" Name="${this.name}"/>`;
}
return `<${tagName} Name="${this.name}"/>`;
}
}
// Capability type configurations
const CAPABILITY_TYPES = {
common: {
nsAlias: null,
nsURI: "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
declareNS: false,
elementName: "Capability",
},
uap: {
nsAlias: "uap",
nsURI: "http://schemas.microsoft.com/appx/manifest/uap/windows10",
declareNS: false, // ns already declared in template
elementName: "Capability",
},
device: {
nsAlias: null,
nsURI: "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
declareNS: false,
elementName: "DeviceCapability",
},
uap6: {
nsAlias: "uap6",
nsURI: "http://schemas.microsoft.com/appx/manifest/uap/windows10/6",
declareNS: true,
elementName: "Capability",
},
uap7: {
nsAlias: "uap7",
nsURI: "http://schemas.microsoft.com/appx/manifest/uap/windows10/7",
declareNS: true,
elementName: "Capability",
},
mobile: {
nsAlias: "mobile",
nsURI: "http://schemas.microsoft.com/appx/manifest/mobile/windows10",
declareNS: true,
elementName: "Capability",
},
rescap: {
nsAlias: "rescap",
nsURI: "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities",
declareNS: false, // ns already declared in template
elementName: "Capability",
},
};
// Map of capability types to their capability names (grouped by type)
const CAPABILITY_MAP = new Map([
// Common capabilities
[
"common",
[
"internetClient",
"internetClientServer",
"privateNetworkClientServer",
"codeGeneration",
"allJoyn",
"backgroundMediaPlayback",
"remoteSystem",
"spatialPerception",
"userDataTasks",
"userNotificationListener",
],
],
// UAP capabilities
[
"uap",
[
"musicLibrary",
"picturesLibrary",
"videosLibrary",
"removableStorage",
"appointments",
"contacts",
"phoneCall",
"phoneCallHistoryPublic",
"userAccountInformation",
"voipCall",
"objects3D",
"chat",
"blockedChatMessages",
"enterpriseAuthentication",
"sharedUserCertificates",
"documentsLibrary",
],
],
// Mobile capabilities
["mobile", ["recordedCallsFolder"]],
// Restricted capabilities
[
"rescap",
[
"enterpriseDataPolicy",
"appCaptureSettings",
"cellularDeviceControl",
"cellularDeviceIdentity",
"cellularMessaging",
"deviceUnlock",
"dualSimTiles",
"enterpriseDeviceLockdown",
"inputInjectionBrokered",
"inputObservation",
"inputSuppression",
"networkingVpnProvider",
"packageManagement",
"screenDuplication",
"userPrincipalName",
"walletSystem",
"locationHistory",
"confirmAppClose",
"phoneCallHistory",
"appointmentsSystem",
"chatSystem",
"contactsSystem",
"email",
"emailSystem",
"phoneCallHistorySystem",
"smsSend",
"userDataSystem",
"previewStore",
"firstSignInSettings",
"teamEditionExperience",
"remotePassportAuthentication",
"previewUiComposition",
"secureAssessment",
"networkConnectionManagerProvisioning",
"networkDataPlanProvisioning",
"slapiQueryLicenseValue",
"extendedBackgroundTaskTime",
"extendedExecutionBackgroundAudio",
"extendedExecutionCritical",
"extendedExecutionUnconstrained",
"deviceManagementDmAccount",
"deviceManagementFoundation",
"deviceManagementWapSecurityPolicies",
"deviceManagementEmailAccount",
"packagePolicySystem",
"gameList",
"xboxAccessoryManagement",
"cortanaSpeechAccessory",
"accessoryManager",
"interopServices",
"inputForegroundObservation",
"oemDeployment",
"oemPublicDirectory",
"appLicensing",
"locationSystem",
"userDataAccountsProvider",
"previewPenWorkspace",
"secondaryAuthenticationFactor",
"storeLicenseManagement",
"userSystemId",
"targetedContent",
"uiAutomation",
"gameBarServices",
"appCaptureServices",
"appBroadcastServices",
"audioDeviceConfiguration",
"backgroundMediaRecording",
"previewInkWorkspace",
"startScreenManagement",
"cortanaPermissions",
"allAppMods",
"expandedResources",
"protectedApp",
"gameMonitor",
"appDiagnostics",
"devicePortalProvider",
"enterpriseCloudSSO",
"backgroundVoIP",
"oneProcessVoIP",
"developmentModeNetwork",
"broadFileSystemAccess",
"smbios",
"runFullTrust",
"allowElevation",
"teamEditionDeviceCredential",
"teamEditionView",
"cameraProcessingExtension",
"networkDataUsageManagement",
"phoneLineTransportManagement",
"unvirtualizedResources",
"modifiableApp",
"packageWriteRedirectionCompatibilityShim",
"customInstallActions",
"packagedServices",
"localSystemServices",
"backgroundSpatialPerception",
"uiAccess",
],
],
// UAP6 capabilities
["uap6", ["graphicsCapture"]],
// UAP7 capabilities
["uap7", ["globalMediaControl"]],
// Device capabilities
[
"device",
[
"location",
"microphone",
"webcam",
"proximity",
"pointOfService",
"wiFiControl",
"radios",
"optical",
"activity",
"humanPresence",
"serialcommunication",
"gazeInput",
"lowLevel",
"packageQuery",
],
],
]);
// Factory function to create capabilities
function createCapability(type, name) {
const config = CAPABILITY_TYPES[type];
if (!config) {
throw new Error(`unknown capability type '${type}'`);
}
return new AppxCapability(config.nsAlias, config.nsURI, config.declareNS, config.elementName, name);
}
// Export ordered list of all capabilities (order matters per Microsoft docs)
// Schema: https://learn.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/schema-root
// Packaging: https://learn.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations
// !! the docs are not clear in which order is correct. the schema doc specifies an order that differs from the order described in packaging doc !!
// https://learn.microsoft.com/en-us/answers/questions/92754/why-does-the-order-of-items-in-(capabilities)-of-p
// as stated in the above post the packaging docs is the one to follow as MakeAppx.exe is following this specification when it checks the manifests validity
exports.CAPABILITIES = Array.from(CAPABILITY_MAP.entries()).flatMap(([type, names]) => names.map(name => createCapability(type, name)));
const CAPABILITY_NAMES = new Set(Array.from(CAPABILITY_MAP.values()).flat());
function isValidCapabilityName(name) {
return !!name && CAPABILITY_NAMES.has(name);
}
//# sourceMappingURL=AppxCapabilities.js.map

File diff suppressed because one or more lines are too long

View file

@ -6,10 +6,12 @@ export default class AppXTarget extends Target {
private readonly packager;
readonly outDir: string;
readonly options: AppXOptions;
isAsyncSupported: boolean;
constructor(packager: WinPackager, outDir: string);
build(appOutDir: string, arch: Arch): Promise<any>;
private static computeUserAssets;
private computePublisherName;
private writeManifest;
private getCapabilities;
private getExtensions;
}

View file

@ -1,14 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const bluebird_lst_1 = require("bluebird-lst");
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const windowsCodeSign_1 = require("../codeSign/windowsCodeSign");
const windows_1 = require("../toolsets/windows");
const core_1 = require("../core");
const pathManager_1 = require("../util/pathManager");
const targetUtil_1 = require("./targetUtil");
const windows_2 = require("../toolsets/windows");
const AppxCapabilities_1 = require("./AppxCapabilities");
const APPX_ASSETS_DIR_NAME = "appx";
const vendorAssetsForDefaultAssets = {
"StoreLogo.png": "SampleAppx.50x50.png",
@ -16,58 +17,83 @@ const vendorAssetsForDefaultAssets = {
"Square44x44Logo.png": "SampleAppx.44x44.png",
"Wide310x150Logo.png": "SampleAppx.310x150.png",
};
const restrictedApplicationIdValues = [
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
];
const DEFAULT_RESOURCE_LANG = "en-US";
class AppXTarget extends core_1.Target {
constructor(packager, outDir) {
super("appx");
this.packager = packager;
this.outDir = outDir;
this.options = builder_util_1.deepAssign({}, this.packager.platformSpecificBuildOptions, this.packager.config.appx);
if (process.platform !== "darwin" && (process.platform !== "win32" || windowsCodeSign_1.isOldWin6())) {
this.options = (0, builder_util_runtime_1.deepAssign)({}, this.packager.platformSpecificBuildOptions, this.packager.config.appx);
this.isAsyncSupported = false;
if (process.platform !== "darwin" && (process.platform !== "win32" || (0, windows_2.isOldWin6)())) {
throw new Error("AppX is supported only on Windows 10 or Windows Server 2012 R2 (version number 6.3+)");
}
}
// https://docs.microsoft.com/en-us/windows/uwp/packaging/create-app-package-with-makeappx-tool#mapping-files
async build(appOutDir, arch) {
var _a;
const packager = this.packager;
const artifactName = packager.expandArtifactBeautyNamePattern(this.options, "appx", arch);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
await packager.info.emitArtifactBuildStarted({
targetPresentableName: "AppX",
file: artifactPath,
arch,
});
const vendorPath = await windowsCodeSign_1.getSignVendorPath();
const vendorPath = await (0, windows_1.getWindowsKitsBundle)({ winCodeSign: (_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.winCodeSign, arch: arch });
const vm = await packager.vm.value;
const stageDir = await targetUtil_1.createStageDir(this, packager, arch);
const stageDir = await (0, targetUtil_1.createStageDir)(this, packager, arch);
const mappingFile = stageDir.getTempFile("mapping.txt");
const makeAppXArgs = ["pack", "/o" /* overwrite the output file if it exists */, "/f", vm.toVmFile(mappingFile), "/p", vm.toVmFile(artifactPath)];
if (packager.compression === "store") {
makeAppXArgs.push("/nc");
}
const mappingList = [];
mappingList.push(await bluebird_lst_1.default.map(fs_1.walk(appOutDir), file => {
mappingList.push(await Promise.all((await (0, builder_util_1.walk)(appOutDir)).map(file => {
let appxPath = file.substring(appOutDir.length + 1);
if (path.sep !== "\\") {
appxPath = appxPath.replace(/\//g, "\\");
}
return `"${vm.toVmFile(file)}" "app\\${appxPath}"`;
}));
})));
const userAssetDir = await this.packager.getResource(undefined, APPX_ASSETS_DIR_NAME);
const assetInfo = await AppXTarget.computeUserAssets(vm, vendorPath, userAssetDir);
const assetInfo = await AppXTarget.computeUserAssets(vm, vendorPath.appxAssets, userAssetDir);
const userAssets = assetInfo.userAssets;
const manifestFile = stageDir.getTempFile("AppxManifest.xml");
await this.writeManifest(manifestFile, arch, await this.computePublisherName(), userAssets);
await packager.info.callAppxManifestCreated(manifestFile);
await packager.info.emitAppxManifestCreated(manifestFile);
mappingList.push(assetInfo.mappings);
mappingList.push([`"${vm.toVmFile(manifestFile)}" "AppxManifest.xml"`]);
const signToolArch = arch === builder_util_1.Arch.arm64 ? "x64" : builder_util_1.Arch[arch];
if (isScaledAssetsProvided(userAssets)) {
const outFile = vm.toVmFile(stageDir.getTempFile("resources.pri"));
const makePriPath = vm.toVmFile(path.join(vendorPath, "windows-10", signToolArch, "makepri.exe"));
const makePriPath = vm.toVmFile(path.join(vendorPath.kit, "makepri.exe"));
const assetRoot = stageDir.getTempFile("appx/assets");
await fs_extra_1.emptyDir(assetRoot);
await bluebird_lst_1.default.map(assetInfo.allAssets, it => fs_1.copyOrLinkFile(it, path.join(assetRoot, path.basename(it))));
await (0, fs_extra_1.emptyDir)(assetRoot);
await Promise.all(assetInfo.allAssets.map(it => (0, builder_util_1.copyOrLinkFile)(it, path.join(assetRoot, path.basename(it)))));
await vm.exec(makePriPath, [
"new",
"/Overwrite",
@ -76,12 +102,12 @@ class AppXTarget extends core_1.Target {
"/ProjectRoot",
vm.toVmFile(path.dirname(assetRoot)),
"/ConfigXml",
vm.toVmFile(path.join(pathManager_1.getTemplatePath("appx"), "priconfig.xml")),
vm.toVmFile(path.join((0, pathManager_1.getTemplatePath)("appx"), "priconfig.xml")),
"/OutputFile",
outFile,
]);
// in addition to resources.pri, resources.scale-140.pri and other such files will be generated
for (const resourceFile of (await fs_extra_1.readdir(stageDir.dir)).filter(it => it.startsWith("resources.")).sort()) {
for (const resourceFile of (await (0, fs_extra_1.readdir)(stageDir.dir)).filter(it => it.startsWith("resources.")).sort()) {
mappingList.push([`"${vm.toVmFile(stageDir.getTempFile(resourceFile))}" "${resourceFile}"`]);
}
makeAppXArgs.push("/l");
@ -90,21 +116,23 @@ class AppXTarget extends core_1.Target {
for (const list of mappingList) {
mapping += "\r\n" + list.join("\r\n");
}
await fs_extra_1.writeFile(mappingFile, mapping);
await (0, fs_extra_1.writeFile)(mappingFile, mapping);
packager.debugLogger.add("appx.mapping", mapping);
if (this.options.makeappxArgs != null) {
makeAppXArgs.push(...this.options.makeappxArgs);
}
await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", signToolArch, "makeappx.exe")), makeAppXArgs);
await packager.sign(artifactPath);
await stageDir.cleanup();
await packager.info.callArtifactBuildCompleted({
file: artifactPath,
packager,
arch,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "appx"),
target: this,
isWriteUpdateInfo: this.options.electronUpdaterAware,
this.buildQueueManager.add(async () => {
await vm.exec(vm.toVmFile(path.join(vendorPath.kit, "makeappx.exe")), makeAppXArgs);
await packager.signIf(artifactPath);
await stageDir.cleanup();
await packager.info.emitArtifactBuildCompleted({
file: artifactPath,
packager,
arch,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "appx"),
target: this,
isWriteUpdateInfo: this.options.electronUpdaterAware,
});
});
}
static async computeUserAssets(vm, vendorPath, userAssetDir) {
@ -115,7 +143,7 @@ class AppXTarget extends core_1.Target {
userAssets = [];
}
else {
userAssets = (await fs_extra_1.readdir(userAssetDir)).filter(it => !it.startsWith(".") && !it.endsWith(".db") && it.includes("."));
userAssets = (await (0, fs_extra_1.readdir)(userAssetDir)).filter(it => !it.startsWith(".") && !it.endsWith(".db") && it.includes("."));
for (const name of userAssets) {
mappings.push(`"${vm.toVmFile(userAssetDir)}${vm.pathSep}${name}" "assets\\${name}"`);
allAssets.push(path.join(userAssetDir, name));
@ -133,24 +161,23 @@ class AppXTarget extends core_1.Target {
}
// https://github.com/electron-userland/electron-builder/issues/2108#issuecomment-333200711
async computePublisherName() {
if ((await this.packager.cscInfo.value) == null) {
builder_util_1.log.info({ reason: "Windows Store only build" }, "AppX is not signed");
return this.options.publisher || "CN=ms";
}
const certInfo = await this.packager.lazyCertInfo.value;
const publisher = this.options.publisher || (certInfo == null ? null : certInfo.bloodyMicrosoftSubjectDn);
if (publisher == null) {
throw new Error("Internal error: cannot compute subject using certificate info");
}
return publisher;
const signtoolManager = await this.packager.signingManager.value;
return signtoolManager.computePublisherName(this, this.options.publisher);
}
async writeManifest(outFile, arch, publisher, userAssets) {
const appInfo = this.packager.appInfo;
const options = this.options;
const executable = `app\\${appInfo.productFilename}.exe`;
const displayName = options.displayName || appInfo.productName;
const capabilities = this.getCapabilities();
const extensions = await this.getExtensions(executable, displayName);
const manifest = (await fs_extra_1.readFile(path.join(pathManager_1.getTemplatePath("appx"), "appxmanifest.xml"), "utf8")).replace(/\${([a-zA-Z0-9]+)}/g, (match, p1) => {
const archSpecificMinVersion = arch === builder_util_1.Arch.arm64 ? "10.0.16299.0" : "10.0.14316.0";
const customManifestPath = await this.packager.getResource(this.options.customManifestPath);
if (customManifestPath) {
builder_util_1.log.info({ manifestPath: builder_util_1.log.filePath(customManifestPath) }, "custom appx manifest found");
}
const manifestFileContent = await (0, fs_extra_1.readFile)(customManifestPath || path.join((0, pathManager_1.getTemplatePath)("appx"), "appxmanifest.xml"), "utf8");
const manifest = manifestFileContent.replace(/\${([a-zA-Z0-9]+)}/g, (match, p1) => {
switch (p1) {
case "publisher":
return publisher;
@ -164,18 +191,64 @@ class AppXTarget extends core_1.Target {
case "version":
return appInfo.getVersionInWeirdWindowsForm(options.setBuildNumber === true);
case "applicationId": {
const result = options.applicationId || options.identityName || appInfo.name;
if (!isNaN(parseInt(result[0], 10))) {
let message = `AppX Application.Id cant start with numbers: "${result}"`;
if (options.applicationId == null) {
message += `\nPlease set appx.applicationId (or correct appx.identityName or name)`;
const validCharactersRegex = /^([A-Za-z][A-Za-z0-9]*)(\.[A-Za-z][A-Za-z0-9]*)*$/;
const identitynumber = parseInt(options.identityName, 10) || NaN;
let result;
if (options.applicationId) {
result = options.applicationId;
}
else if (!isNaN(identitynumber) && options.identityName !== null && options.identityName !== undefined) {
if (options.identityName[0] === "0") {
builder_util_1.log.warn(`Remove the 0${identitynumber}`);
result = options.identityName.replace("0" + identitynumber.toString(), "");
}
else {
builder_util_1.log.warn(`Remove the ${identitynumber}`);
result = options.identityName.replace(identitynumber.toString(), "");
}
}
else {
result = options.identityName || appInfo.name;
}
if (result.length < 1 || result.length > 64) {
const message = `Appx Application.Id must be between 1 and 64 characters in length: ${result}`;
throw new builder_util_1.InvalidConfigurationError(message);
}
else if (!validCharactersRegex.test(result)) {
const message = `AppX Application.Id cannot contain alpha-numeric, period, and dash characters: ${result}"`;
throw new builder_util_1.InvalidConfigurationError(message);
}
else if (restrictedApplicationIdValues.includes(result.toUpperCase())) {
const message = `AppX Application.Id cannot contain restricted values ${JSON.stringify(restrictedApplicationIdValues)}: ${result}`;
throw new builder_util_1.InvalidConfigurationError(message);
}
else if (result == null && options.applicationId == null) {
const message = `Please set appx.applicationId (or correct appx.identityName or name)`;
throw new builder_util_1.InvalidConfigurationError(message);
}
return result;
}
case "identityName": {
const result = options.identityName || appInfo.name;
const validCharactersRegex = /^[a-zA-Z0-9.-]+$/;
if (result.length < 3 || result.length > 50) {
const message = `Appx identityName.Id must be between 3 and 50 characters in length: ${result}`;
throw new builder_util_1.InvalidConfigurationError(message);
}
else if (!validCharactersRegex.test(result)) {
const message = `AppX identityName.Id cannot contain of alpha-numeric, period, and dash characters: ${result}`;
throw new builder_util_1.InvalidConfigurationError(message);
}
else if (restrictedApplicationIdValues.includes(result.toUpperCase())) {
const message = `AppX identityName.Id cannot contain restricted values ${JSON.stringify(restrictedApplicationIdValues)}: ${result}`;
throw new builder_util_1.InvalidConfigurationError(message);
}
else if (result == null && options.identityName == null) {
const message = `Please set appx.identityName or name`;
throw new builder_util_1.InvalidConfigurationError(message);
}
return result;
}
case "identityName":
return options.identityName || appInfo.name;
case "executable":
return executable;
case "displayName":
@ -199,22 +272,37 @@ class AppXTarget extends core_1.Target {
case "arch":
return arch === builder_util_1.Arch.ia32 ? "x86" : arch === builder_util_1.Arch.arm64 ? "arm64" : "x64";
case "resourceLanguages":
return resourceLanguageTag(builder_util_1.asArray(options.languages));
return resourceLanguageTag((0, builder_util_1.asArray)(options.languages));
case "capabilities":
return capabilities;
case "extensions":
return extensions;
case "minVersion":
return arch === builder_util_1.Arch.arm64 ? "10.0.16299.0" : "10.0.14316.0";
return options.minVersion || archSpecificMinVersion;
case "maxVersionTested":
return arch === builder_util_1.Arch.arm64 ? "10.0.16299.0" : "10.0.14316.0";
return options.maxVersionTested || options.minVersion || archSpecificMinVersion;
default:
throw new Error(`Macro ${p1} is not defined`);
}
});
await fs_extra_1.writeFile(outFile, manifest);
await (0, fs_extra_1.writeFile)(outFile, manifest);
}
getCapabilities() {
const caps = (0, builder_util_1.asArray)(this.options.capabilities);
const capSet = new Set(caps);
const invalid = Array.from(capSet).filter(cap => !(0, AppxCapabilities_1.isValidCapabilityName)(cap));
if (invalid.length > 0) {
throw new Error(`invalid windows capabilit${invalid.length === 1 ? "y" : "ies"} specified: ${invalid.join(", ")}`);
}
// Ensure runFullTrust is always included
capSet.add("runFullTrust");
// Filter and map in one pass
const capabilityStrings = AppxCapabilities_1.CAPABILITIES.filter(cap => capSet.has(cap.name)).map(cap => ` ${cap.toXMLString()}`);
return `<Capabilities>\n${capabilityStrings.join("\n")}\n</Capabilities>`;
}
async getExtensions(executable, displayName) {
const uriSchemes = builder_util_1.asArray(this.packager.config.protocols).concat(builder_util_1.asArray(this.packager.platformSpecificBuildOptions.protocols));
const fileAssociations = builder_util_1.asArray(this.packager.config.fileAssociations).concat(builder_util_1.asArray(this.packager.platformSpecificBuildOptions.fileAssociations));
const uriSchemes = (0, builder_util_1.asArray)(this.packager.config.protocols).concat((0, builder_util_1.asArray)(this.packager.platformSpecificBuildOptions.protocols));
const fileAssociations = (0, builder_util_1.asArray)(this.packager.config.fileAssociations).concat((0, builder_util_1.asArray)(this.packager.platformSpecificBuildOptions.fileAssociations));
let isAddAutoLaunchExtension = this.options.addAutoLaunchExtension;
if (isAddAutoLaunchExtension === undefined) {
const deps = this.packager.info.metadata.dependencies;
@ -231,7 +319,7 @@ class AppXTarget extends core_1.Target {
</desktop:Extension>`;
}
for (const protocol of uriSchemes) {
for (const scheme of builder_util_1.asArray(protocol.schemes)) {
for (const scheme of (0, builder_util_1.asArray)(protocol.schemes)) {
extensions += `
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="${scheme}">
@ -241,7 +329,7 @@ class AppXTarget extends core_1.Target {
}
}
for (const fileAssociation of fileAssociations) {
for (const ext of builder_util_1.asArray(fileAssociation.ext)) {
for (const ext of (0, builder_util_1.asArray)(fileAssociation.ext)) {
extensions += `
<uap:Extension Category="windows.fileTypeAssociation">
<uap:FileTypeAssociation Name="${ext}">
@ -254,7 +342,7 @@ class AppXTarget extends core_1.Target {
}
if (this.options.customExtensionsPath !== undefined) {
const extensionsPath = path.resolve(this.packager.info.appDir, this.options.customExtensionsPath);
extensions += await fs_extra_1.readFile(extensionsPath, "utf8");
extensions += await (0, fs_extra_1.readFile)(extensionsPath, "utf8");
}
extensions += "</Extensions>";
return extensions;
@ -266,7 +354,7 @@ function resourceLanguageTag(userLanguages) {
if (userLanguages == null || userLanguages.length === 0) {
userLanguages = [DEFAULT_RESOURCE_LANG];
}
return userLanguages.map(it => `<Resource Language="${it.replace(/_/g, "-")}" />`).join("\n");
return userLanguages.map(it => `<Resource Language="${it.trim().replace(/_/g, "-")}" />`).join("\n");
}
function lockScreenTag(userAssets) {
if (isDefaultAssetIncluded(userAssets, "BadgeLogo.png")) {

File diff suppressed because one or more lines are too long

View file

@ -20,7 +20,7 @@ class ArchiveTarget extends core_1.Target {
const isMac = packager.platform === core_1.Platform.MAC;
const format = this.name;
let defaultPattern;
const defaultArch = builder_util_1.defaultArchFromString(packager.platformSpecificBuildOptions.defaultArch);
const defaultArch = (0, builder_util_1.defaultArchFromString)(packager.platformSpecificBuildOptions.defaultArch);
if (packager.platform === core_1.Platform.LINUX) {
// tslint:disable-next-line:no-invalid-template-strings
defaultPattern = "${name}-${version}" + (arch === defaultArch ? "" : "-${arch}") + ".${ext}";
@ -29,55 +29,59 @@ class ArchiveTarget extends core_1.Target {
// tslint:disable-next-line:no-invalid-template-strings
defaultPattern = "${productName}-${version}" + (arch === defaultArch ? "" : "-${arch}") + "-${os}.${ext}";
}
const artifactName = packager.expandArtifactNamePattern(this.options, format, arch, defaultPattern, false);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
targetPresentableName: `${isMac ? "macOS " : ""}${format}`,
file: artifactPath,
arch,
});
let updateInfo = null;
if (format.startsWith("tar.")) {
await archive_1.tar(packager.compression, format, artifactPath, appOutDir, isMac, packager.info.tempDirManager);
}
else {
let withoutDir = !isMac;
let dirToArchive = appOutDir;
if (isMac) {
dirToArchive = path.dirname(appOutDir);
const fileMatchers = fileMatcher_1.getFileMatchers(packager.config, "extraDistFiles", dirToArchive, packager.createGetFileMatchersOptions(this.outDir, arch, packager.platformSpecificBuildOptions));
if (fileMatchers == null) {
dirToArchive = appOutDir;
}
else {
await fileMatcher_1.copyFiles(fileMatchers, null, true);
withoutDir = true;
}
this.buildQueueManager.add(async () => {
const artifactName = packager.expandArtifactNamePattern(this.options, format, arch, defaultPattern, false);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.emitArtifactBuildStarted({
targetPresentableName: `${isMac ? "macOS " : ""}${format}`,
file: artifactPath,
arch,
});
let updateInfo = null;
if (format.startsWith("tar.")) {
await (0, archive_1.tar)({ compression: packager.compression, format, outFile: artifactPath, dirToArchive: appOutDir, isMacApp: isMac, tempDirManager: packager.info.tempDirManager });
}
const archiveOptions = {
compression: packager.compression,
withoutDir,
};
await archive_1.archive(format, artifactPath, dirToArchive, archiveOptions);
if (this.isWriteUpdateInfo && format === "zip") {
else {
let withoutDir = !isMac;
let dirToArchive = appOutDir;
if (isMac) {
updateInfo = await differentialUpdateInfoBuilder_1.createBlockmap(artifactPath, this, packager, artifactName);
dirToArchive = path.dirname(appOutDir);
const fileMatchers = (0, fileMatcher_1.getFileMatchers)(packager.config, "extraDistFiles", dirToArchive, packager.createGetFileMatchersOptions(this.outDir, arch, packager.platformSpecificBuildOptions));
if (fileMatchers == null) {
dirToArchive = appOutDir;
}
else {
await (0, fileMatcher_1.copyFiles)(fileMatchers, null, true);
withoutDir = true;
}
}
else {
updateInfo = await differentialUpdateInfoBuilder_1.appendBlockmap(artifactPath);
const archiveOptions = {
compression: packager.compression,
withoutDir,
preserveSymlinks: isMac,
};
await (0, archive_1.archive)(format, artifactPath, dirToArchive, archiveOptions);
if (this.isWriteUpdateInfo && format === "zip") {
if (isMac) {
updateInfo = await (0, differentialUpdateInfoBuilder_1.createBlockmap)(artifactPath, this, packager, artifactName);
}
else {
updateInfo = await (0, differentialUpdateInfoBuilder_1.appendBlockmap)(artifactPath);
}
}
}
}
await packager.info.callArtifactBuildCompleted({
updateInfo,
file: artifactPath,
// tslint:disable-next-line:no-invalid-template-strings
safeArtifactName: packager.computeSafeArtifactName(artifactName, format, arch, false, packager.platformSpecificBuildOptions.defaultArch, defaultPattern.replace("${productName}", "${name}")),
target: this,
arch,
packager,
isWriteUpdateInfo: this.isWriteUpdateInfo,
await packager.info.emitArtifactBuildCompleted({
updateInfo,
file: artifactPath,
// tslint:disable-next-line:no-invalid-template-strings
safeArtifactName: packager.computeSafeArtifactName(artifactName, format, arch, false, packager.platformSpecificBuildOptions.defaultArch, defaultPattern.replace("${productName}", "${name}")),
target: this,
arch,
packager,
isWriteUpdateInfo: this.isWriteUpdateInfo,
});
});
return Promise.resolve();
}
}
exports.ArchiveTarget = ArchiveTarget;

File diff suppressed because one or more lines are too long

View file

@ -7,16 +7,14 @@ const path = require("path");
const core_1 = require("../core");
const license_1 = require("../util/license");
const targetUtil_1 = require("./targetUtil");
const builder_util_runtime_1 = require("builder-util-runtime");
class FlatpakTarget extends core_1.Target {
constructor(name, packager, helper, outDir) {
super(name);
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
this.options = {
...this.packager.platformSpecificBuildOptions,
...this.packager.config[this.name],
};
this.options = (0, builder_util_runtime_1.deepAssign)({}, this.packager.platformSpecificBuildOptions, this.packager.config[this.name]);
}
get appId() {
return filterFlatpakAppIdentifier(this.packager.appInfo.id);
@ -25,16 +23,16 @@ class FlatpakTarget extends core_1.Target {
const { packager, options } = this;
const artifactName = packager.expandArtifactNamePattern(options, "flatpak", arch, undefined, false);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
await packager.info.emitArtifactBuildStarted({
targetPresentableName: "flatpak",
file: artifactPath,
arch,
});
const stageDir = await this.prepareStageDir(arch);
const { manifest, buildOptions } = this.getFlatpakBuilderOptions(appOutDir, stageDir.dir, artifactName, arch);
await flatpak_bundler_1.bundle(manifest, buildOptions);
await (0, flatpak_bundler_1.bundle)(manifest, buildOptions);
await stageDir.cleanup();
await packager.info.callArtifactBuildCompleted({
await packager.info.emitArtifactBuildCompleted({
file: artifactPath,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "flatpak", arch, false),
target: this,
@ -44,15 +42,15 @@ class FlatpakTarget extends core_1.Target {
});
}
async prepareStageDir(arch) {
const stageDir = await targetUtil_1.createStageDir(this, this.packager, arch);
const stageDir = await (0, targetUtil_1.createStageDir)(this, this.packager, arch);
await Promise.all([this.createSandboxBinWrapper(stageDir), this.createDesktopFile(stageDir), this.copyLicenseFile(stageDir), this.copyIcons(stageDir)]);
return stageDir;
}
async createSandboxBinWrapper(stageDir) {
const useWaylandFlags = !!this.options.useWaylandFlags;
const electronWrapperPath = stageDir.getTempFile(path.join("bin", "electron-wrapper"));
await fs_extra_1.outputFile(electronWrapperPath, getElectronWrapperScript(this.packager.executableName, useWaylandFlags));
await fs_extra_1.chmod(electronWrapperPath, 0o755);
await (0, fs_extra_1.outputFile)(electronWrapperPath, getElectronWrapperScript(this.packager.executableName, this.options.executableArgs, useWaylandFlags));
await (0, fs_extra_1.chmod)(electronWrapperPath, 0o755);
}
async createDesktopFile(stageDir) {
const appIdentifier = this.appId;
@ -60,26 +58,30 @@ class FlatpakTarget extends core_1.Target {
await this.helper.writeDesktopEntry(this.options, "electron-wrapper %U", desktopFile, { Icon: appIdentifier });
}
async copyLicenseFile(stageDir) {
const licenseSrc = await license_1.getNotLocalizedLicenseFile(this.options.license, this.packager, ["txt", "html"]);
const licenseSrc = await (0, license_1.getNotLocalizedLicenseFile)(this.options.license, this.packager, ["txt", "html"]);
if (licenseSrc) {
const licenseDst = stageDir.getTempFile(path.join("share", "doc", this.appId, "copyright"));
await builder_util_1.copyFile(licenseSrc, licenseDst);
await (0, builder_util_1.copyFile)(licenseSrc, licenseDst);
}
}
async copyIcons(stageDir) {
const icons = await this.helper.icons;
const copyIcons = icons.map(async (icon) => {
if (icon.size > 512) {
// Flatpak does not allow icons larger than 512 pixels
return Promise.resolve();
}
const extWithDot = path.extname(icon.file);
const sizeName = extWithDot === ".svg" ? "scalable" : `${icon.size}x${icon.size}`;
const iconDst = stageDir.getTempFile(path.join("share", "icons", "hicolor", sizeName, "apps", `${this.appId}${extWithDot}`));
return builder_util_1.copyFile(icon.file, iconDst);
return (0, builder_util_1.copyFile)(icon.file, iconDst);
});
await Promise.all(copyIcons);
}
getFlatpakBuilderOptions(appOutDir, stageDir, artifactName, arch) {
const appIdentifier = this.appId;
const { executableName } = this.packager;
const flatpakArch = builder_util_1.toLinuxArchString(arch, "flatpak");
const flatpakArch = (0, builder_util_1.toLinuxArchString)(arch, "flatpak");
const manifest = {
id: appIdentifier,
command: "electron-wrapper",
@ -128,23 +130,25 @@ const flatpakBuilderDefaults = {
"--talk-name=org.freedesktop.Notifications",
],
};
function getElectronWrapperScript(executableName, useWaylandFlags) {
return useWaylandFlags
? `#!/bin/sh
function getElectronWrapperScript(executableName, executableArgs, useWaylandFlags) {
const stringifiedExecutableArgs = (executableArgs === null || executableArgs === void 0 ? void 0 : executableArgs.join(" ")) || "";
if (useWaylandFlags) {
return `#!/bin/sh
export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
if [ "\${XDG_SESSION_TYPE}" == "wayland" ]; then
zypak-wrapper "${executableName}" --enable-features=UseOzonePlatform --ozone-platform=wayland "$@"
zypak-wrapper "${executableName}" ${stringifiedExecutableArgs} --enable-features=UseOzonePlatform --ozone-platform=wayland "$@"
else
zypak-wrapper "${executableName}" "$@"
zypak-wrapper "${executableName}" ${stringifiedExecutableArgs} "$@"
fi
`
: `#!/bin/sh
`;
}
return `#!/bin/sh
export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
zypak-wrapper "${executableName}" "$@"
zypak-wrapper "${executableName}" ${stringifiedExecutableArgs} "$@"
`;
}
function filterFlatpakAppIdentifier(identifier) {

File diff suppressed because one or more lines are too long

View file

@ -14,4 +14,9 @@ export default class FpmTarget extends Target {
checkOptions(): Promise<any>;
private computeFpmMetaInfoOptions;
build(appOutDir: string, arch: Arch): Promise<any>;
private executeFpm;
private supportsAutoUpdate;
private getDefaultDepends;
private getDefaultRecommends;
private configureTargetSpecificOptions;
}

View file

@ -0,0 +1,372 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_extra_1 = require("fs-extra");
const promises_1 = require("fs/promises");
const path = require("path");
const appInfo_1 = require("../appInfo");
const core_1 = require("../core");
const errorMessages = require("../errorMessages");
const PublishManager_1 = require("../publish/PublishManager");
const builder_util_runtime_2 = require("builder-util-runtime");
const bundledTool_1 = require("../util/bundledTool");
const hash_1 = require("../util/hash");
const macosVersion_1 = require("../util/macosVersion");
const pathManager_1 = require("../util/pathManager");
const LinuxTargetHelper_1 = require("./LinuxTargetHelper");
const linux_1 = require("../toolsets/linux");
class FpmTarget extends core_1.Target {
constructor(name, packager, helper, outDir) {
super(name, false);
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
this.options = (0, builder_util_runtime_1.deepAssign)({}, this.packager.platformSpecificBuildOptions, this.packager.config[this.name]);
this.scriptFiles = this.createScripts();
}
async createScripts() {
const defaultTemplatesDir = (0, pathManager_1.getTemplatePath)("linux");
const packager = this.packager;
/** Escape a string value for safe interpolation inside a bash single-quoted
* string (`'...'`). The only character that can break out of a POSIX
* single-quoted context is a literal single-quote; we replace it with the
* standard `'\''` sequence (end quote escaped quote start quote).
*
* sanitize-filename removes characters that are illegal in filenames but
* deliberately keeps `'` because it is valid on POSIX systems. Without
* this extra step, an app named `O'Brien` would terminate the quoted path
* inside the generated after-install / after-remove shell scripts, enabling
* arbitrary command injection when the package is installed as root. */
function bashSingleQuoteEscape(value) {
return value.replace(/'/g, "'\\''");
}
// Bash-script templates embed executable and sanitizedProductName inside
// single-quoted shell paths — escape single quotes to prevent injection.
const bashTemplateOptions = {
// old API compatibility
executable: bashSingleQuoteEscape(packager.executableName),
sanitizedProductName: bashSingleQuoteEscape(packager.appInfo.sanitizedProductName),
productFilename: packager.appInfo.productFilename,
...packager.platformSpecificBuildOptions,
};
// The AppArmor profile template uses these values inside double-quoted
// AppArmor path patterns — no single-quote escaping needed or wanted.
const appArmorTemplateOptions = {
executable: packager.executableName,
sanitizedProductName: packager.appInfo.sanitizedProductName,
productFilename: packager.appInfo.productFilename,
...packager.platformSpecificBuildOptions,
};
function getResource(value, defaultFile) {
if (value == null) {
return path.join(defaultTemplatesDir, defaultFile);
}
return path.resolve(packager.projectDir, value);
}
return {
afterInstall: await writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterInstall, "after-install.tpl"), bashTemplateOptions),
afterRemove: await writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterRemove, "after-remove.tpl"), bashTemplateOptions),
appArmor: await writeConfigFile(packager.info.tempDirManager, getResource(this.options.appArmorProfile, "apparmor-profile.tpl"), appArmorTemplateOptions),
};
}
checkOptions() {
return this.computeFpmMetaInfoOptions();
}
async computeFpmMetaInfoOptions() {
var _a;
const packager = this.packager;
const projectUrl = await packager.appInfo.computePackageUrl();
const errors = [];
if (projectUrl == null) {
errors.push("Please specify project homepage, see https://www.electron.build/configuration#metadata");
}
const options = this.options;
let author = options.maintainer;
if (author == null) {
const a = packager.info.metadata.author;
if (a == null || a.email == null) {
errors.push(errorMessages.authorEmailIsMissed);
}
else {
author = `${a.name} <${a.email}>`;
}
}
if (errors.length > 0) {
throw new Error(errors.join("\n\n"));
}
return {
name: (_a = options.packageName) !== null && _a !== void 0 ? _a : this.packager.appInfo.linuxPackageName,
maintainer: author,
url: projectUrl,
vendor: options.vendor || author,
};
}
async build(appOutDir, arch) {
var _a, _b;
const target = this.name;
// tslint:disable:no-invalid-template-strings
let nameFormat = "${name}-${version}-${arch}.${ext}";
let isUseArchIfX64 = false;
if (target === "deb") {
nameFormat = "${name}_${version}_${arch}.${ext}";
isUseArchIfX64 = true;
}
else if (target === "rpm") {
nameFormat = "${name}-${version}.${arch}.${ext}";
isUseArchIfX64 = true;
}
const packager = this.packager;
const artifactName = packager.expandArtifactNamePattern(this.options, target, arch, nameFormat, !isUseArchIfX64);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.emitArtifactBuildStarted({
targetPresentableName: target,
file: artifactPath,
arch,
});
await (0, builder_util_1.unlinkIfExists)(artifactPath);
if (packager.packagerOptions.prepackaged != null) {
await (0, promises_1.mkdir)(this.outDir, { recursive: true });
}
const linuxDistType = packager.packagerOptions.prepackaged || path.join(this.outDir, `linux${(0, builder_util_1.getArchSuffix)(arch)}-unpacked`);
const resourceDir = packager.getResourcesDir(linuxDistType);
const publishConfig = this.supportsAutoUpdate(target)
? await (0, PublishManager_1.getAppUpdatePublishConfiguration)(packager, this.options, arch, false /* in any case validation will be done on publish step */)
: null;
if (publishConfig != null) {
builder_util_1.log.info({ resourceDir: builder_util_1.log.filePath(resourceDir) }, `adding autoupdate files for: ${target}`);
await (0, fs_extra_1.outputFile)(path.join(resourceDir, "app-update.yml"), (0, builder_util_1.serializeToYaml)(publishConfig));
// Extra file needed for auto-updater to detect installation method
await (0, fs_extra_1.outputFile)(path.join(resourceDir, "package-type"), target);
}
const scripts = await this.scriptFiles;
// Install AppArmor support for ubuntu 24+
// https://github.com/electron-userland/electron-builder/issues/8635
await (0, fs_extra_1.copyFile)(scripts.appArmor, path.join(resourceDir, "apparmor-profile"));
const appInfo = packager.appInfo;
const options = this.options;
const synopsis = options.synopsis;
const args = [
"--architecture",
(0, builder_util_1.toLinuxArchString)(arch, target),
"--after-install",
scripts.afterInstall,
"--after-remove",
scripts.afterRemove,
"--description",
(0, appInfo_1.smarten)(target === "rpm" ? this.helper.getDescription(options) : `${synopsis || ""}\n ${this.helper.getDescription(options)}`),
"--version",
this.helper.getSanitizedVersion(target),
"--package",
artifactPath,
];
const meta = await this.computeFpmMetaInfoOptions();
args.push(...(0, builder_util_runtime_2.objectToArgs)({ name: meta.name, maintainer: (_a = meta.maintainer) !== null && _a !== void 0 ? _a : null, vendor: meta.vendor, url: meta.url }));
const packageCategory = options.packageCategory;
if (packageCategory != null) {
args.push("--category", packageCategory);
}
if (target === "deb") {
args.push("--deb-priority", (_b = options.priority) !== null && _b !== void 0 ? _b : "optional");
}
else if (target === "rpm") {
if (synopsis != null) {
args.push("--rpm-summary", (0, appInfo_1.smarten)(synopsis));
}
}
const fpmConfiguration = {
args,
target,
};
if (options.compression != null) {
fpmConfiguration.compression = options.compression;
}
// noinspection JSDeprecatedSymbols
const depends = options.depends;
if (depends != null) {
if (Array.isArray(depends)) {
fpmConfiguration.customDepends = depends;
}
else if (typeof depends === "string") {
fpmConfiguration.customDepends = [depends];
}
else {
throw new Error(`depends must be Array or String, but specified as: ${depends}`);
}
}
else {
fpmConfiguration.customDepends = this.getDefaultDepends(target);
}
if (target === "deb") {
const recommends = options.recommends;
if (recommends) {
fpmConfiguration.customRecommends = (0, builder_util_1.asArray)(recommends);
}
else {
fpmConfiguration.customRecommends = this.getDefaultRecommends(target);
}
}
(0, builder_util_1.use)(packager.info.metadata.license, it => args.push("--license", it));
(0, builder_util_1.use)(appInfo.buildNumber, it => args.push("--iteration",
// dashes are not supported for iteration in older versions of fpm
// https://github.com/jordansissel/fpm/issues/1833
it.split("-").join("_")));
(0, builder_util_1.use)(options.fpm, it => args.push(...it));
args.push(`${appOutDir}/=${LinuxTargetHelper_1.installPrefix}/${appInfo.sanitizedProductName}`);
for (const icon of await this.helper.icons) {
const extWithDot = path.extname(icon.file);
const sizeName = extWithDot === ".svg" ? "scalable" : `${icon.size}x${icon.size}`;
args.push(`${icon.file}=/usr/share/icons/hicolor/${sizeName}/apps/${packager.executableName}${extWithDot}`);
}
const mimeTypeFilePath = await this.helper.mimeTypeFiles;
if (mimeTypeFilePath != null) {
args.push(`${mimeTypeFilePath}=/usr/share/mime/packages/${packager.executableName}.xml`);
}
const desktopFilePath = await this.helper.writeDesktopEntry(this.options);
args.push(`${desktopFilePath}=/usr/share/applications/${this.helper.getDesktopFileName()}.desktop`);
if (packager.packagerOptions.effectiveOptionComputed != null && (await packager.packagerOptions.effectiveOptionComputed([args, desktopFilePath]))) {
return;
}
const env = {
...(0, builder_util_1.stripSensitiveEnvVars)(process.env),
};
// rpmbuild wants directory rpm with some default config files. Even if we can use dylibbundler, path to such config files are not changed (we need to replace in the binary)
// so, for now, brew install rpm is still required.
if (target !== "rpm" && (await (0, macosVersion_1.isMacOsSierra)())) {
const linuxToolsPath = await (0, linux_1.getLinuxToolsPath)();
Object.assign(env, {
PATH: (0, bundledTool_1.computeEnv)(process.env.PATH, [path.join(linuxToolsPath, "bin")]),
DYLD_LIBRARY_PATH: (0, bundledTool_1.computeEnv)(process.env.DYLD_LIBRARY_PATH, [path.join(linuxToolsPath, "lib")]),
});
}
await this.executeFpm(target, fpmConfiguration, env);
let info = {
file: artifactPath,
target: this,
arch,
packager,
};
if (publishConfig != null) {
info = {
...info,
safeArtifactName: packager.computeSafeArtifactName(artifactName, target, arch, !isUseArchIfX64),
isWriteUpdateInfo: true,
updateInfo: {
sha512: await (0, hash_1.hashFile)(artifactPath),
size: (await (0, fs_extra_1.stat)(artifactPath)).size,
},
};
}
await packager.info.emitArtifactBuildCompleted(info);
}
async executeFpm(target, fpmConfiguration, env) {
var _a, _b, _c;
const fpmArgs = ["-s", "dir", "--force", "-t", target];
const forceDebugLogging = process.env.FPM_DEBUG === "true";
if (forceDebugLogging) {
fpmArgs.push("--debug");
}
if (builder_util_1.log.isDebugEnabled) {
fpmArgs.push("--log", "debug");
}
(_a = fpmConfiguration.customDepends) === null || _a === void 0 ? void 0 : _a.forEach(it => fpmArgs.push("-d", it));
if (target === "deb") {
(_b = fpmConfiguration.customRecommends) === null || _b === void 0 ? void 0 : _b.forEach(it => fpmArgs.push("--deb-recommends", it));
}
const defaultCompression = target === "rpm" ? "xzmt" : "xz";
fpmArgs.push(...this.configureTargetSpecificOptions(target, (_c = fpmConfiguration.compression) !== null && _c !== void 0 ? _c : defaultCompression));
fpmArgs.push(...fpmConfiguration.args);
const fpmPath = await (0, linux_1.getFpmPath)();
await (0, builder_util_1.exec)(fpmPath, fpmArgs, { env }).catch(e => {
if (e.message.includes("Need executable 'rpmbuild' to convert dir to rpm")) {
const hint = "to build rpm, executable rpmbuild is required, please install rpm package on your system. ";
if (process.platform === "darwin") {
builder_util_1.log.error(null, hint + "(brew install rpm)");
}
else {
builder_util_1.log.error(null, hint + "(sudo apt-get install rpm)");
}
}
if (e.message.includes("xz: not found")) {
const hint = "to build rpm, executable xz is required, please install xz package on your system. ";
if (process.platform === "darwin") {
builder_util_1.log.error(null, hint + "(brew install xz)");
}
else {
builder_util_1.log.error(null, hint + "(sudo apt-get install xz-utils)");
}
}
if (e.message.includes("error: File not found")) {
builder_util_1.log.error({ fpmArgs, ...fpmConfiguration }, "fpm failed to find the specified files. Please check your configuration and ensure all paths are correct. To see what files triggered this, set the environment variable FPM_DEBUG=true");
if (forceDebugLogging) {
builder_util_1.log.error(null, e.message);
}
throw new Error(`FPM failed to find the specified files. Please check your configuration and ensure all paths are correct. Command: ${fpmPath} ${fpmArgs.join(" ")}`);
}
throw e;
});
}
supportsAutoUpdate(target) {
return ["deb", "rpm", "pacman"].includes(target);
}
getDefaultDepends(target) {
switch (target) {
case "deb":
return ["libgtk-3-0", "libnotify4", "libnss3", "libxss1", "libxtst6", "xdg-utils", "libatspi2.0-0", "libuuid1", "libsecret-1-0"];
case "rpm":
return [
"gtk3" /* for electron 2+ (electron 1 uses gtk2, but this old version is not supported anymore) */,
"libnotify",
"nss",
"libXScrnSaver",
"(libXtst or libXtst6)",
"xdg-utils",
"at-spi2-core" /* since 5.0.0 */,
"(libuuid or libuuid1)" /* since 4.0.0 */,
];
case "pacman":
return ["c-ares", "ffmpeg", "gtk3", "http-parser", "libevent", "libvpx", "libxslt", "libxss", "minizip", "nss", "re2", "snappy", "libnotify", "libappindicator-gtk3"];
default:
return [];
}
}
getDefaultRecommends(target) {
switch (target) {
case "deb":
return ["libappindicator3-1"];
default:
return [];
}
}
configureTargetSpecificOptions(target, compression) {
switch (target) {
case "rpm":
return ["--rpm-os", "linux", "--rpm-compression", compression === "xz" ? "xzmt" : compression];
case "deb":
return ["--deb-compression", compression];
case "pacman":
return ["--pacman-compression", compression];
}
return [];
}
}
exports.default = FpmTarget;
async function writeConfigFile(tmpDir, templatePath, options) {
//noinspection JSUnusedLocalSymbols
function replacer(match, p1) {
if (p1 in options) {
return options[p1];
}
else {
throw new Error(`Macro ${p1} is not defined`);
}
}
const config = (await (0, promises_1.readFile)(templatePath, "utf8")).replace(/\${([a-zA-Z]+)}/g, replacer).replace(/<%=([a-zA-Z]+)%>/g, (match, p1) => {
builder_util_1.log.warn("<%= varName %> is deprecated, please use ${varName} instead");
return replacer(match, p1.trim());
});
const outputPath = await tmpDir.getTempFile({ suffix: path.basename(templatePath, ".tpl") });
await (0, fs_extra_1.outputFile)(outputPath, config);
return outputPath;
}
//# sourceMappingURL=FpmTarget.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,7 @@
import { LinuxPackager } from "../linuxPackager";
import { LinuxTargetSpecificOptions } from "../options/linuxOptions";
import { IconInfo } from "../platformPackager";
import { CommonLinuxOptions } from "../options/linuxOptions";
import { SnapCore } from "./snap/SnapTarget";
import { IconInfo } from "../util/iconConverter";
export declare const installPrefix = "/opt";
export declare class LinuxTargetHelper {
private packager;
@ -10,13 +11,13 @@ export declare class LinuxTargetHelper {
constructor(packager: LinuxPackager);
get icons(): Promise<Array<IconInfo>>;
get mimeTypeFiles(): Promise<string | null>;
getSnapCore(): SnapCore<any>;
isElectronVersionGreaterOrEqualThan(version: string, fallback?: string): boolean;
private computeMimeTypeFiles;
private computeDesktopIcons;
getDescription(options: LinuxTargetSpecificOptions): string;
writeDesktopEntry(targetSpecificOptions: LinuxTargetSpecificOptions, exec?: string, destination?: string | null, extra?: {
[key: string]: string;
}): Promise<string>;
computeDesktopEntry(targetSpecificOptions: LinuxTargetSpecificOptions, exec?: string, extra?: {
[key: string]: string;
}): Promise<string>;
getDescription(options: CommonLinuxOptions): string;
getSanitizedVersion(target: string): string;
writeDesktopEntry(targetSpecificOptions: CommonLinuxOptions, exec?: string, destination?: string | null, extra?: Record<string, string>): Promise<string>;
getDesktopFileName(fallback?: string): string;
computeDesktopEntry(targetSpecificOptions: CommonLinuxOptions, exec?: string, extra?: Record<string, string>): Promise<string>;
}

View file

@ -2,9 +2,77 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.LinuxTargetHelper = exports.installPrefix = void 0;
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_extra_1 = require("fs-extra");
const lazy_val_1 = require("lazy-val");
const path_1 = require("path");
const semver = require("semver");
const core24_1 = require("./snap/core24");
const coreCustom_1 = require("./snap/coreCustom");
const coreLegacy_1 = require("./snap/coreLegacy");
/**
* Escape a string value for use in a freedesktop .desktop file string field
* (Name, Comment, StartupWMClass, etc.).
*
* The freedesktop Desktop Entry Specification requires that the following
* characters be escaped in string / localestring values:
* \n \\n (newline would inject new key=value lines otherwise)
* \r \\r
* \t \\t
* \\ \\\\
*
* Without escaping, a productName or description containing a literal newline
* can inject arbitrary key=value pairs into the generated .desktop file,
* potentially overriding the Exec key.
*
* @see https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#value-types
*/
function desktopStringEscape(value) {
return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
}
/**
* Characters that require an Exec argument to be double-quoted per the
* freedesktop Desktop Entry Specification. Plain alphanumeric args and
* field codes must NOT be wrapped in quotes.
*
* @see https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
*/
const EXEC_RESERVED_RE = /[\s"'`\\<>~|&;$*?#()]/;
/**
* Quote a single argument for use in a .desktop file Exec key.
*
* Field codes (`%f`, `%u`, `%F`, `%U`, etc.) MUST be left unquoted the
* desktop launcher only expands them in unquoted token positions. Wrapping
* them in `"…"` causes the launcher to treat them as literal strings, which
* breaks file-association / drag-and-drop functionality.
*
* For all other arguments, double-quoting is used when the argument contains
* any character that would be misinterpreted by the launcher without quoting
* (spaces, shell metacharacters, etc.). Safe plain-word args are passed
* through unchanged to keep the Exec line readable.
*
* @see https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
*/
function desktopExecArgEscape(arg) {
// Field codes (%f, %u, %F, %U, %i, %c, %k, …) must never be quoted.
if (/^%[a-zA-Z]$/.test(arg)) {
return arg;
}
// Only quote when the arg actually contains characters that need it.
if (EXEC_RESERVED_RE.test(arg)) {
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}
return arg;
}
function mapLinuxCompressionToSnap(level) {
if (level === "store") {
return "lzo";
}
if (level === "maximum") {
return "xz";
}
return undefined;
}
exports.installPrefix = "/opt";
class LinuxTargetHelper {
constructor(packager) {
@ -19,6 +87,60 @@ class LinuxTargetHelper {
get mimeTypeFiles() {
return this.mimeTypeFilesPromise.value;
}
getSnapCore() {
const { snapcraft, snap: legacySnap } = this.packager.config;
if (snapcraft != null && legacySnap != null) {
builder_util_1.log.warn("Both `snapcraft` and `snap` configurations are present. `snapcraft` takes precedence; please remove the `snap` key to silence this warning.");
}
// Merge linux-level options (category, description, mimeTypes, etc.) as the base so they
// propagate into the generated snapcraft.yaml and .desktop file without requiring users to
// duplicate them under core24/core18/etc. Per-core options always win for conflicts.
// linux.compression is a CompressionLevel ("store"/"normal"/"maximum"); snap compression is an
// algorithm ("xz"/"lzo"). Map the level to the nearest algorithm; per-core options override.
const { compression: linuxCompression, ...linuxOptions } = this.packager.platformSpecificBuildOptions;
const snapLinuxOptions = { ...linuxOptions, compression: mapLinuxCompressionToSnap(linuxCompression) };
if (snapcraft != null) {
const core = snapcraft.base;
const options = snapcraft[core] || {};
switch (core) {
case "core18":
case "core20":
case "core22":
if (!this.isElectronVersionGreaterOrEqualThan("4.0.0")) {
if (!this.isElectronVersionGreaterOrEqualThan("2.0.0-beta.1")) {
throw new builder_util_1.InvalidConfigurationError("Electron 2 and higher is required to build Snap with core18/core20/core22");
}
builder_util_1.log.warn(null, "electron 4 and higher is highly recommended for Snap with core18/core20/core22");
}
return new coreLegacy_1.SnapCoreLegacy(this.packager, this, (0, builder_util_runtime_1.deepAssign)({}, snapLinuxOptions, { base: core, ...options }));
case "core24":
if (!this.isElectronVersionGreaterOrEqualThan("28.0.0")) {
if (!this.isElectronVersionGreaterOrEqualThan("25.0.0")) {
throw new builder_util_1.InvalidConfigurationError("Electron 25 and higher is required to build Snap with core24");
}
builder_util_1.log.warn(null, "electron 28 and higher is highly recommended for Snap with core24");
}
return new core24_1.SnapCore24(this.packager, this, (0, builder_util_runtime_1.deepAssign)({}, snapLinuxOptions, options));
case "custom":
// Pass-through: do not inject linux options into user-supplied yaml
return new coreCustom_1.SnapCoreCustom(this.packager, this, snapcraft.custom || {});
}
}
if (legacySnap != null) {
builder_util_1.log.warn({
reason: "`snap` configuration is deprecated",
docs: "https://www.electron.build/snapcraft",
}, "please consider migrating `snap` configuration to `snapcraft.<core>` and remove `snap` configuration");
}
return new coreLegacy_1.SnapCoreLegacy(this.packager, this, (0, builder_util_runtime_1.deepAssign)({}, snapLinuxOptions, legacySnap !== null && legacySnap !== void 0 ? legacySnap : {}));
}
isElectronVersionGreaterOrEqualThan(version, fallback) {
const electronVersion = this.packager.config.electronVersion;
if (!electronVersion) {
return fallback ? semver.gte(fallback, version) : true;
}
return semver.gte(electronVersion, version);
}
async computeMimeTypeFiles() {
const items = [];
for (const fileAssociation of this.packager.fileAssociations) {
@ -36,7 +158,7 @@ class LinuxTargetHelper {
return null;
}
const file = await this.packager.getTempFile(".xml");
await fs_extra_1.outputFile(file, '<?xml version="1.0" encoding="utf-8"?>\n<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">\n' + items.join("\n") + "\n</mime-info>");
await (0, fs_extra_1.outputFile)(file, '<?xml version="1.0" encoding="utf-8"?>\n<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">\n' + items.join("\n") + "\n</mime-info>");
return file;
}
// must be name without spaces and other special characters, but not product name used
@ -46,31 +168,62 @@ class LinuxTargetHelper {
const { platformSpecificBuildOptions, config } = packager;
const sources = [platformSpecificBuildOptions.icon, (_b = (_a = config.mac) === null || _a === void 0 ? void 0 : _a.icon) !== null && _b !== void 0 ? _b : config.icon].filter(str => !!str);
// If no explicit sources are defined, fallback to buildResources directory, then default framework icon
let fallbackSources = [...builder_util_1.asArray(packager.getDefaultFrameworkIcon())];
let fallbackSources = [...(0, builder_util_1.asArray)(packager.getDefaultFrameworkIcon())];
const buildResources = (_c = config.directories) === null || _c === void 0 ? void 0 : _c.buildResources;
if (buildResources && (await builder_util_1.exists(path_1.join(buildResources, "icons")))) {
if (buildResources && (await (0, builder_util_1.exists)((0, path_1.join)(buildResources, "icons")))) {
fallbackSources = [buildResources, ...fallbackSources];
}
// need to put here and not as default because need to resolve image size
const result = await packager.resolveIcon(sources, fallbackSources, "set");
this.maxIconPath = result[result.length - 1].file;
return result;
// Ignore .icon files for linux (they are exclusive for macOS)
return result.filter(icon => !icon.file.endsWith(".icon"));
}
getDescription(options) {
return options.description || this.packager.appInfo.description;
}
getSanitizedVersion(target) {
const { appInfo: { version }, } = this.packager;
switch (target) {
case "pacman":
return version.replace(/-/g, "_");
case "rpm":
case "deb":
return version.replace(/-/g, "~");
default:
return version;
}
}
async writeDesktopEntry(targetSpecificOptions, exec, destination, extra) {
const data = await this.computeDesktopEntry(targetSpecificOptions, exec, extra);
const file = destination || (await this.packager.getTempFile(`${this.packager.appInfo.productFilename}.desktop`));
await fs_extra_1.outputFile(file, data);
await (0, fs_extra_1.outputFile)(file, data);
return file;
}
getDesktopFileName(fallback = this.packager.executableName) {
var _a;
if (!this.packager.platformSpecificBuildOptions.syncDesktopName) {
return fallback;
}
const trimmedDesktopName = (_a = this.packager.info.metadata.desktopName) === null || _a === void 0 ? void 0 : _a.trim();
if ((0, builder_util_1.isEmptyOrSpaces)(trimmedDesktopName)) {
return fallback;
}
const basename = trimmedDesktopName.replace(/\.desktop$/, "");
// Guard against path traversal: desktopName flows into filesystem paths
// (snap/gui/<name>.desktop, /usr/share/applications/<name>.desktop, etc.).
if (/[/\\]/.test(basename) || [...basename].some(c => c.charCodeAt(0) === 0)) {
throw new builder_util_1.InvalidConfigurationError(`desktopName "${trimmedDesktopName}" produces an invalid .desktop filename — remove any path separators or NUL characters`);
}
return basename;
}
computeDesktopEntry(targetSpecificOptions, exec, extra) {
var _a, _b, _c, _d, _e, _f, _g;
if (exec != null && exec.length === 0) {
throw new Error("Specified exec is empty");
}
// https://github.com/electron-userland/electron-builder/issues/3418
if (targetSpecificOptions.desktop != null && targetSpecificOptions.desktop.Exec != null) {
if ((_b = (_a = targetSpecificOptions.desktop) === null || _a === void 0 ? void 0 : _a.entry) === null || _b === void 0 ? void 0 : _b.Exec) {
throw new Error("Please specify executable name as linux.executableName instead of linux.desktop.Exec");
}
const packager = this.packager;
@ -83,7 +236,9 @@ class LinuxTargetHelper {
}
if (executableArgs) {
exec += " ";
exec += executableArgs.join(" ");
// Each arg is double-quoted per the freedesktop Exec key spec so that
// spaces, $, ;, & and other reserved characters are not misinterpreted.
exec += executableArgs.map(desktopExecArgEscape).join(" ");
}
// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
const execCodes = ["%f", "%u", "%F", "%U"];
@ -91,33 +246,47 @@ class LinuxTargetHelper {
exec += " %U";
}
}
const desktopMeta = {
Name: appInfo.productName,
// https://github.com/electron-userland/electron-builder/issues/9103
// Electron derives app_id from desktopName in package.json; StartupWMClass must match.
// https://github.com/electron/electron/blob/9a7b73b5334f1d72c08e2d5e94106706ed751186/lib/browser/init.ts#L128-L133
const trimmedDesktopName = (_c = packager.info.metadata.desktopName) === null || _c === void 0 ? void 0 : _c.trim();
if ((0, builder_util_1.isEmptyOrSpaces)(trimmedDesktopName)) {
builder_util_1.log.warn({
reason: "desktopName is not set in package.json",
docs: "https://www.electron.build/linux#window-association-desktopname--syncdesktopname",
}, "electron uses desktopName as app_id / WM_CLASS for window association. Without it desktop environments may not link running windows to this .desktop entry. Set desktopName in package.json and linux.syncDesktopName: true to fix.");
}
const wmClass = !(0, builder_util_1.isEmptyOrSpaces)(trimmedDesktopName) ? trimmedDesktopName.replace(/\.desktop$/, "") : appInfo.productName;
const desktopMeta = (0, builder_util_runtime_1.deepAssign)({
// String values are escaped per the freedesktop spec (\\, \n, \r, \t)
// so that a product name containing a newline cannot inject new key=value
// pairs into the .desktop file (e.g. overriding the Exec key).
Name: desktopStringEscape(appInfo.productName),
Exec: exec,
Terminal: "false",
Type: "Application",
Icon: packager.executableName,
// https://askubuntu.com/questions/367396/what-represent-the-startupwmclass-field-of-a-desktop-file
// must be set to package.json name (because it is Electron set WM_CLASS)
// Set to desktopName (minus .desktop suffix) when provided, so it matches Electron's
// app_id and desktop environments can associate running windows with this entry.
// Falls back to productName for apps that don't set desktopName.
// to get WM_CLASS of running window: xprop WM_CLASS
// StartupWMClass doesn't work for unicode
// https://github.com/electron/electron/blob/2-0-x/atom/browser/native_window_views.cc#L226
StartupWMClass: appInfo.productName,
...extra,
...targetSpecificOptions.desktop,
};
StartupWMClass: desktopStringEscape(wmClass),
}, extra, (_e = (_d = targetSpecificOptions.desktop) === null || _d === void 0 ? void 0 : _d.entry) !== null && _e !== void 0 ? _e : {});
const description = this.getDescription(targetSpecificOptions);
if (!builder_util_1.isEmptyOrSpaces(description)) {
desktopMeta.Comment = description;
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
desktopMeta.Comment = desktopStringEscape(description);
}
const mimeTypes = builder_util_1.asArray(targetSpecificOptions.mimeTypes);
const mimeTypes = (0, builder_util_1.asArray)(targetSpecificOptions.mimeTypes);
for (const fileAssociation of packager.fileAssociations) {
if (fileAssociation.mimeType != null) {
mimeTypes.push(fileAssociation.mimeType);
}
}
for (const protocol of builder_util_1.asArray(packager.config.protocols).concat(builder_util_1.asArray(packager.platformSpecificBuildOptions.protocols))) {
for (const scheme of builder_util_1.asArray(protocol.schemes)) {
for (const protocol of (0, builder_util_1.asArray)(packager.config.protocols).concat((0, builder_util_1.asArray)(packager.platformSpecificBuildOptions.protocols))) {
for (const scheme of (0, builder_util_1.asArray)(protocol.schemes)) {
mimeTypes.push(`x-scheme-handler/${scheme}`);
}
}
@ -125,7 +294,7 @@ class LinuxTargetHelper {
desktopMeta.MimeType = mimeTypes.join(";") + ";";
}
let category = targetSpecificOptions.category;
if (builder_util_1.isEmptyOrSpaces(category)) {
if ((0, builder_util_1.isEmptyOrSpaces)(category)) {
const macCategory = (packager.config.mac || {}).category;
if (macCategory != null) {
category = macToLinuxCategory[macCategory];
@ -137,7 +306,7 @@ class LinuxTargetHelper {
}
builder_util_1.log.warn({
reason: "linux.category is not set and cannot map from macOS",
docs: "https://www.electron.build/configuration/linux",
docs: "https://www.electron.build/linux",
}, 'application Linux category is set to default "Utility"');
category = "Utility";
}
@ -148,6 +317,17 @@ class LinuxTargetHelper {
data += `\n${name}=${desktopMeta[name]}`;
}
data += "\n";
const desktopActions = (_g = (_f = targetSpecificOptions.desktop) === null || _f === void 0 ? void 0 : _f.desktopActions) !== null && _g !== void 0 ? _g : {};
for (const [actionName, config] of Object.entries(desktopActions)) {
if (!Object.keys(config !== null && config !== void 0 ? config : {}).length) {
continue;
}
data += `\n[Desktop Action ${actionName}]`;
for (const [key, value] of Object.entries(config !== null && config !== void 0 ? config : {})) {
data += `\n${key}=${value}`;
}
data += "\n";
}
return Promise.resolve(data);
}
}
@ -161,5 +341,6 @@ const macToLinuxCategory = {
"public.app-category.utilities": "Utility",
"public.app-category.social-networking": "Network;Chat",
"public.app-category.finance": "Office;Finance",
"public.app-category.music": "Audio;AudioVideo",
};
//# sourceMappingURL=LinuxTargetHelper.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,22 +1,28 @@
import { Arch } from "builder-util";
import { Lazy } from "lazy-val";
import { MsiOptions } from "../";
import { Target } from "../core";
import { FinalCommonWindowsInstallerOptions } from "../options/CommonWindowsInstallerConfiguration";
import { VmManager } from "../vm/vm";
import { WinPackager } from "../winPackager";
export default class MsiTarget extends Target {
private readonly packager;
protected readonly packager: WinPackager;
readonly outDir: string;
private readonly vm;
protected readonly vm: VmManager;
readonly options: MsiOptions;
constructor(packager: WinPackager, outDir: string);
constructor(packager: WinPackager, outDir: string, name?: string, isAsyncSupported?: boolean);
protected projectTemplate: Lazy<(data: any) => string>;
/**
* A product-specific string that can be used in an [MSI Identifier](https://docs.microsoft.com/en-us/windows/win32/msi/identifier).
*/
private get productMsiIdPrefix();
private get iconId();
private get upgradeCode();
protected get iconId(): string;
protected get upgradeCode(): string;
build(appOutDir: string, arch: Arch): Promise<void>;
private light;
private getAdditionalLightArgs;
private getCommonWixArgs;
private writeManifest;
protected writeManifest(appOutDir: string, wixArch: Arch, commonOptions: FinalCommonWindowsInstallerOptions): Promise<string>;
protected getBaseOptions(commonOptions: FinalCommonWindowsInstallerOptions): Promise<any>;
private computeFileDeclaration;
}

View file

@ -1,39 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const bluebird_lst_1 = require("bluebird-lst");
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const binDownload_1 = require("../binDownload");
const fs_1 = require("builder-util/out/fs");
const crypto_1 = require("crypto");
const ejs = require("ejs");
const promises_1 = require("fs/promises");
const lazy_val_1 = require("lazy-val");
const path = require("path");
const binDownload_1 = require("../binDownload");
const core_1 = require("../core");
const CommonWindowsInstallerConfiguration_1 = require("../options/CommonWindowsInstallerConfiguration");
const platformPackager_1 = require("../platformPackager");
const pathManager_1 = require("../util/pathManager");
const vm_1 = require("../vm/vm");
const WineVm_1 = require("../vm/WineVm");
const toolsetLock_1 = require("../util/toolsetLock");
const targetUtil_1 = require("./targetUtil");
const ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID = builder_util_runtime_1.UUID.parse("d752fe43-5d44-44d5-9fc9-6dd1bf19d5cc");
const ROOT_DIR_ID = "APPLICATIONFOLDER";
const projectTemplate = new lazy_val_1.Lazy(async () => {
const template = (await promises_1.readFile(path.join(pathManager_1.getTemplatePath("msi"), "template.xml"), "utf8"))
.replace(/{{/g, "<%")
.replace(/}}/g, "%>")
.replace(/\${([^}]+)}/g, "<%=$1%>");
return ejs.compile(template);
});
// WiX doesn't support Mono, so, dontnet462 is required to be installed for wine (preinstalled in our bundled wine)
class MsiTarget extends core_1.Target {
constructor(packager, outDir) {
super("msi");
constructor(packager, outDir, name = "msi", isAsyncSupported = true) {
var _a;
super(name, isAsyncSupported);
this.packager = packager;
this.outDir = outDir;
this.vm = process.platform === "win32" ? new vm_1.VmManager() : new WineVm_1.WineVmManager();
this.options = builder_util_1.deepAssign(this.packager.platformSpecificBuildOptions, this.packager.config.msi);
this.vm = process.platform === "win32" ? new vm_1.VmManager() : new WineVm_1.WineVmManager((_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.wine);
this.options = (0, builder_util_runtime_1.deepAssign)(this.packager.platformSpecificBuildOptions, this.packager.config.msi);
this.projectTemplate = new lazy_val_1.Lazy(async () => {
const template = (await (0, promises_1.readFile)(path.join((0, pathManager_1.getTemplatePath)(this.name), "template.xml"), "utf8"))
.replace(/{{/g, "<%")
.replace(/}}/g, "%>")
.replace(/\${([^}]+)}/g, "<%=$1%>");
return ejs.compile(template);
});
}
/**
* A product-specific string that can be used in an [MSI Identifier](https://docs.microsoft.com/en-us/windows/win32/msi/identifier).
@ -52,30 +52,37 @@ class MsiTarget extends core_1.Target {
const packager = this.packager;
const artifactName = packager.expandArtifactBeautyNamePattern(this.options, "msi", arch);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
await packager.info.emitArtifactBuildStarted({
targetPresentableName: "MSI",
file: artifactPath,
arch,
});
const stageDir = await targetUtil_1.createStageDir(this, packager, arch);
const stageDir = await (0, targetUtil_1.createStageDir)(this, packager, arch);
const vm = this.vm;
const commonOptions = CommonWindowsInstallerConfiguration_1.getEffectiveOptions(this.options, this.packager);
const commonOptions = (0, CommonWindowsInstallerConfiguration_1.getEffectiveOptions)(this.options, this.packager);
// wix 4.0.0.5512.2 doesn't support the arm64 architecture so default to x64 when building for arm64.
// This will result in an x64 MSI installer that installs an arm64 version of the application. This is a
// stopgap until the electron-builder-binaries wix version is upgraded to a version that supports arm64:
// https://github.com/electron-userland/electron-builder/issues/6077
const wixArch = arch == builder_util_1.Arch.arm64 ? builder_util_1.Arch.x64 : arch;
const projectFile = stageDir.getTempFile("project.wxs");
const objectFiles = ["project.wixobj"];
await promises_1.writeFile(projectFile, await this.writeManifest(appOutDir, arch, commonOptions));
await packager.info.callMsiProjectCreated(projectFile);
await (0, promises_1.writeFile)(projectFile, await this.writeManifest(appOutDir, wixArch, commonOptions));
await packager.info.emitMsiProjectCreated(projectFile);
// noinspection SpellCheckingInspection
const vendorPath = await binDownload_1.getBinFromUrl("wix", "4.0.0.5512.2", "/X5poahdCc3199Vt6AP7gluTlT1nxi9cbbHhZhCMEu+ngyP1LiBMn+oZX7QAZVaKeBMc2SjVp7fJqNLqsUnPNQ==");
const vendorPath = await (0, binDownload_1.getBinFromUrl)("wix-4.0.0.5512.2", "wix-4.0.0.5512.2.7z", "fe677fcd837b18c9b912985d91636bbd8a1e800c3b3a6a841b6f96e89624e839");
// noinspection SpellCheckingInspection
const candleArgs = ["-arch", arch === builder_util_1.Arch.ia32 ? "x86" : arch === builder_util_1.Arch.arm64 ? "arm64" : "x64", `-dappDir=${vm.toVmFile(appOutDir)}`].concat(this.getCommonWixArgs());
const candleArgs = ["-arch", wixArch === builder_util_1.Arch.ia32 ? "x86" : "x64", `-dappDir=${vm.toVmFile(appOutDir)}`].concat(this.getCommonWixArgs());
candleArgs.push("project.wxs");
await vm.exec(vm.toVmFile(path.join(vendorPath, "candle.exe")), candleArgs, {
cwd: stageDir.dir,
await (0, toolsetLock_1.withToolsetLock)(async () => {
await vm.exec(vm.toVmFile(path.join(vendorPath, "candle.exe")), candleArgs, {
cwd: stageDir.dir,
});
await this.light(objectFiles, vm, artifactPath, appOutDir, vendorPath, stageDir.dir);
});
await this.light(objectFiles, vm, artifactPath, appOutDir, vendorPath, stageDir.dir);
await stageDir.cleanup();
await packager.sign(artifactPath);
await packager.info.callArtifactBuildCompleted({
await packager.signIf(artifactPath);
await packager.info.emitArtifactBuildCompleted({
file: artifactPath,
packager,
arch,
@ -97,7 +104,9 @@ class MsiTarget extends core_1.Target {
"-sw1076",
`-dappDir=${vm.toVmFile(appOutDir)}`,
// "-dcl:high",
].concat(this.getCommonWixArgs());
]
.concat(this.getCommonWixArgs())
.concat(this.getAdditionalLightArgs());
// http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/Build-3-5-2229-0-give-me-the-following-error-error-LGHT0216-An-unexpected-Win32-exception-with-errorn-td5707443.html
if (process.platform !== "win32") {
// noinspection SpellCheckingInspection
@ -112,6 +121,13 @@ class MsiTarget extends core_1.Target {
cwd: tempDir,
});
}
getAdditionalLightArgs() {
const args = [];
if (this.options.additionalLightArgs != null) {
args.push(...this.options.additionalLightArgs);
}
return args;
}
getCommonWixArgs() {
const args = ["-pedantic"];
if (this.options.warningsAsErrors !== false) {
@ -122,20 +138,32 @@ class MsiTarget extends core_1.Target {
}
return args;
}
async writeManifest(appOutDir, arch, commonOptions) {
async writeManifest(appOutDir, wixArch, commonOptions) {
const appInfo = this.packager.appInfo;
const { files, dirs } = await this.computeFileDeclaration(appOutDir);
const options = this.options;
return (await this.projectTemplate.value)({
...(await this.getBaseOptions(commonOptions)),
isCreateDesktopShortcut: commonOptions.isCreateDesktopShortcut !== CommonWindowsInstallerConfiguration_1.DesktopShortcutCreationPolicy.NEVER,
isRunAfterFinish: options.runAfterFinish !== false,
// https://stackoverflow.com/questions/1929038/compilation-error-ice80-the-64bitcomponent-uses-32bitdirectory
programFilesId: wixArch === builder_util_1.Arch.x64 ? "ProgramFiles64Folder" : "ProgramFilesFolder",
// wix in the name because special wix format can be used in the name
installationDirectoryWixName: (0, targetUtil_1.getWindowsInstallationDirName)(appInfo, commonOptions.isAssisted || commonOptions.isPerMachine === true),
dirs,
files,
});
}
async getBaseOptions(commonOptions) {
const appInfo = this.packager.appInfo;
const iconPath = await this.packager.getIconPath();
const compression = this.packager.compression;
const companyName = appInfo.companyName;
if (!companyName) {
builder_util_1.log.warn(`Manufacturer is not set for MSI — please set "author" in the package.json`);
}
const compression = this.packager.compression;
const options = this.options;
const iconPath = await this.packager.getIconPath();
return (await projectTemplate.value)({
return {
...commonOptions,
isCreateDesktopShortcut: commonOptions.isCreateDesktopShortcut !== CommonWindowsInstallerConfiguration_1.DesktopShortcutCreationPolicy.NEVER,
isRunAfterFinish: options.runAfterFinish !== false,
iconPath: iconPath == null ? null : this.vm.toVmFile(iconPath),
iconId: this.iconId,
compressionLevel: compression === "store" ? "none" : "high",
@ -144,13 +172,7 @@ class MsiTarget extends core_1.Target {
upgradeCode: this.upgradeCode,
manufacturer: companyName || appInfo.productName,
appDescription: appInfo.description,
// https://stackoverflow.com/questions/1929038/compilation-error-ice80-the-64bitcomponent-uses-32bitdirectory
programFilesId: arch === builder_util_1.Arch.x64 ? "ProgramFiles64Folder" : "ProgramFilesFolder",
// wix in the name because special wix format can be used in the name
installationDirectoryWixName: targetUtil_1.getWindowsInstallationDirName(appInfo, commonOptions.isAssisted || commonOptions.isPerMachine === true),
dirs,
files,
});
};
}
async computeFileDeclaration(appOutDir) {
const appInfo = this.packager.appInfo;
@ -158,8 +180,8 @@ class MsiTarget extends core_1.Target {
const dirNames = new Set();
const dirs = [];
const fileSpace = " ".repeat(6);
const commonOptions = CommonWindowsInstallerConfiguration_1.getEffectiveOptions(this.options, this.packager);
const files = await bluebird_lst_1.default.map(fs_1.walk(appOutDir), file => {
const commonOptions = (0, CommonWindowsInstallerConfiguration_1.getEffectiveOptions)(this.options, this.packager);
const files = (await (0, builder_util_1.walk)(appOutDir)).map(file => {
const packagePath = file.substring(appOutDir.length + 1);
const lastSlash = packagePath.lastIndexOf(path.sep);
const fileName = lastSlash > 0 ? packagePath.substring(lastSlash + 1) : packagePath;
@ -172,7 +194,7 @@ class MsiTarget extends core_1.Target {
// This syntax is a shortcut to defining each directory in an individual Directory element.
dirName = packagePath.substring(0, lastSlash);
// https://github.com/electron-userland/electron-builder/issues/3027
directoryId = "d" + crypto_1.createHash("md5").update(dirName).digest("base64").replace(/\//g, "_").replace(/\+/g, ".").replace(/=+$/, "");
directoryId = "d" + (0, crypto_1.createHash)("md5").update(dirName).digest("base64").replace(/\//g, "_").replace(/\+/g, ".").replace(/=+$/, "");
if (!dirNames.has(dirName)) {
dirNames.add(dirName);
dirs.push(`<Directory Id="${directoryId}" Name="${ROOT_DIR_ID}:\\${dirName.replace(/\//g, "\\")}\\"/>`);
@ -220,7 +242,7 @@ class MsiTarget extends core_1.Target {
const fileAssociations = this.packager.fileAssociations;
if (isMainExecutable && fileAssociations.length !== 0) {
for (const item of fileAssociations) {
const extensions = builder_util_1.asArray(item.ext).map(platformPackager_1.normalizeExt);
const extensions = (0, builder_util_1.asArray)(item.ext).map(platformPackager_1.normalizeExt);
for (const ext of extensions) {
result += `${fileSpace} <ProgId Id="${this.productMsiIdPrefix}.${ext}" Advertise="yes" Icon="${this.iconId}" ${item.description ? `Description="${item.description}"` : ""}>\n`;
result += `${fileSpace} <Extension Id="${ext}" Advertise="yes">\n`;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,19 @@
import { Arch } from "builder-util";
import { MsiWrappedOptions } from "../";
import { FinalCommonWindowsInstallerOptions } from "../options/CommonWindowsInstallerConfiguration";
import { WinPackager } from "../winPackager";
import MsiTarget from "./MsiTarget";
export default class MsiWrappedTarget extends MsiTarget {
readonly outDir: string;
readonly options: MsiWrappedOptions;
/** @private */
private readonly archs;
constructor(packager: WinPackager, outDir: string);
private get productId();
private validatePrerequisites;
build(appOutDir: string, arch: Arch): Promise<any>;
finishBuild(): Promise<any>;
protected get installerFilenamePattern(): string;
private getExeSourcePath;
protected writeManifest(_appOutDir: string, arch: Arch, commonOptions: FinalCommonWindowsInstallerOptions): Promise<string>;
}

View file

@ -0,0 +1,80 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const path = require("path");
const MsiTarget_1 = require("./MsiTarget");
const ELECTRON_MSI_WRAPPED_NS_UUID = builder_util_runtime_1.UUID.parse("467f7bb2-a83c-442f-b776-394d316e8e53");
class MsiWrappedTarget extends MsiTarget_1.default {
constructor(packager, outDir) {
// must be synchronous so it can run after nsis
super(packager, outDir, "msiWrapped", false);
this.outDir = outDir;
this.options = (0, builder_util_runtime_1.deepAssign)(this.packager.platformSpecificBuildOptions, this.packager.config.msiWrapped);
/** @private */
this.archs = new Map();
}
get productId() {
// this id is only required to build the installer
// however it serves no purpose as this msi is just
// a wrapper for an exe
return builder_util_runtime_1.UUID.v5(this.packager.appInfo.id, ELECTRON_MSI_WRAPPED_NS_UUID).toUpperCase();
}
validatePrerequisites() {
const config = this.packager.config;
// this target requires nsis to be configured and executed
// as this build re-bundles the nsis executable and wraps it in an msi
if (!config.win || !config.win.target || !Array.isArray(config.win.target)) {
throw new Error("No windows target found!");
}
const target = config.win.target;
const nsisTarget = "nsis";
if (!target
.map((t) => {
const result = typeof t === "string" ? t : t.target;
return result.toLowerCase().trim();
})
.some(t => t === nsisTarget)) {
throw new Error("No nsis target found! Please specify an nsis target");
}
}
build(appOutDir, arch) {
this.archs.set(arch, appOutDir);
return Promise.resolve();
}
async finishBuild() {
await super.finishBuild();
const [arch, appOutDir] = this.archs.entries().next().value;
this.validatePrerequisites();
const exeSourcePath = this.getExeSourcePath(arch);
if (!(await (0, builder_util_1.exists)(exeSourcePath))) {
throw new builder_util_1.InvalidConfigurationError(`NSIS executable not found at ${exeSourcePath} - ensure the NSIS target builds before msiWrapped. Try listing 'nsis' before 'msiWrapped' in your target configuration.`);
}
return super.build(appOutDir, arch);
}
get installerFilenamePattern() {
// big assumption is made here for the moment that the pattern didn't change
// tslint:disable:no-invalid-template-strings
return "${productName} Setup ${version}.${ext}";
}
getExeSourcePath(arch) {
const packager = this.packager;
// in this case, we want .exe, this way we can wrap the existing package if it exists
const artifactName = packager.expandArtifactNamePattern(this.options, "exe", arch, this.installerFilenamePattern, false, this.packager.platformSpecificBuildOptions.defaultArch);
const artifactPath = path.join(this.outDir, artifactName);
return artifactPath;
}
async writeManifest(_appOutDir, arch, commonOptions) {
const exeSourcePath = this.getExeSourcePath(arch);
const options = this.options;
return (await this.projectTemplate.value)({
...(await this.getBaseOptions(commonOptions)),
exeSourcePath: exeSourcePath,
productId: this.productId,
impersonate: options.impersonate === true ? "yes" : "no",
wrappedInstallerArgs: options.wrappedInstallerArgs,
});
}
}
exports.default = MsiWrappedTarget;
//# sourceMappingURL=MsiWrappedTarget.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
import { Arch } from "builder-util";
import { Target } from "../../core";
import { LinuxPackager } from "../../linuxPackager";
import { AppImageOptions } from "../../options/linuxOptions";
import { LinuxTargetHelper } from "../LinuxTargetHelper";
export declare const APP_RUN_ENTRYPOINT = "AppRun";
export default class AppImageTarget extends Target {
private readonly packager;
private readonly helper;
readonly outDir: string;
readonly options: AppImageOptions;
private readonly desktopEntry;
constructor(_ignored: string, packager: LinuxPackager, helper: LinuxTargetHelper, outDir: string);
build(appOutDir: string, arch: Arch): Promise<any>;
}

View file

@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.APP_RUN_ENTRYPOINT = void 0;
const builder_util_1 = require("builder-util");
const fs_extra_1 = require("fs-extra");
const lazy_val_1 = require("lazy-val");
const path = require("path");
const core_1 = require("../../core");
const PublishManager_1 = require("../../publish/PublishManager");
const license_1 = require("../../util/license");
const targetUtil_1 = require("../targetUtil");
const appImageUtil_1 = require("./appImageUtil");
const builder_util_runtime_1 = require("builder-util-runtime");
// https://unix.stackexchange.com/questions/375191/append-to-sub-directory-inside-squashfs-file
exports.APP_RUN_ENTRYPOINT = "AppRun";
class AppImageTarget extends core_1.Target {
constructor(_ignored, packager, helper, outDir) {
super("appImage");
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
this.options = (0, builder_util_runtime_1.deepAssign)({}, this.packager.platformSpecificBuildOptions, this.packager.config[this.name]);
this.desktopEntry = new lazy_val_1.Lazy(() => {
var _a, _b;
const appimageTool = (_a = packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.appimage;
const defaultArgs = appimageTool == null || appimageTool === "0.0.0" ? ["--no-sandbox"] : [];
const args = (_b = this.options.executableArgs) !== null && _b !== void 0 ? _b : defaultArgs;
const exec = [exports.APP_RUN_ENTRYPOINT, ...args, "%U"].join(" ");
return helper.computeDesktopEntry(this.options, exec, {
"X-AppImage-Version": `${packager.appInfo.buildVersion}`,
});
});
}
async build(appOutDir, arch) {
var _a;
const packager = this.packager;
const options = this.options;
// https://github.com/electron-userland/electron-builder/issues/775
// https://github.com/electron-userland/electron-builder/issues/1726
const artifactName = packager.expandArtifactNamePattern(options, "AppImage", arch);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.emitArtifactBuildStarted({
targetPresentableName: "AppImage",
file: artifactPath,
arch,
});
// Parallelize independent async operations
const [publishConfig, stageDir, desktopEntry, icons, license] = await Promise.all([
(0, PublishManager_1.getAppUpdatePublishConfiguration)(packager, options, arch, false),
(0, targetUtil_1.createStageDir)(this, packager, arch),
this.desktopEntry.value,
this.helper.icons,
(0, license_1.getNotLocalizedLicenseFile)(options.license, this.packager, ["txt", "html"]),
]);
if (publishConfig != null) {
await (0, fs_extra_1.outputFile)(path.join(packager.getResourcesDir(appOutDir), "app-update.yml"), (0, builder_util_1.serializeToYaml)(publishConfig));
}
// Validated once here; throws InvalidConfigurationError for path traversal / NUL.
const desktopBaseName = this.helper.getDesktopFileName();
if (this.packager.packagerOptions.effectiveOptionComputed != null &&
(await this.packager.packagerOptions.effectiveOptionComputed({ desktop: desktopEntry, desktopFileName: `${desktopBaseName}.desktop` }))) {
await stageDir.cleanup();
return;
}
let updateInfo;
try {
const appimageTool = (_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.appimage;
if (appimageTool == null || appimageTool === "0.0.0") {
updateInfo = await (0, appImageUtil_1.buildLegacyFuse2AppImage)({
appDir: appOutDir,
stageDir: stageDir.dir,
arch,
output: artifactPath,
options: {
productName: packager.appInfo.productName,
productFilename: packager.appInfo.productFilename,
executableName: packager.executableName,
license,
desktopEntry,
icons,
fileAssociations: packager.fileAssociations,
desktopBaseName,
compression: (() => {
const c = options.compression;
if (c === "xz" || c === "gzip") {
return c;
}
if (packager.compression === "maximum") {
return "xz";
}
return undefined; // normal/store/unset/zstd → mksquashfs defaults to gzip
})(),
},
});
}
else {
updateInfo = await (0, appImageUtil_1.buildStaticRuntimeAppImage)(appimageTool, {
appDir: appOutDir,
stageDir: stageDir.dir,
arch,
output: artifactPath,
options: {
productName: packager.appInfo.productName,
productFilename: packager.appInfo.productFilename,
executableName: packager.executableName,
license,
desktopEntry,
icons,
fileAssociations: packager.fileAssociations,
desktopBaseName,
compression: (() => {
const c = options.compression;
if (c === "gzip" || c === "zstd") {
return c;
}
if (c === "xz") {
return "zstd"; // nearest equivalent; static runtime does not support xz
}
if (packager.compression === "store") {
return "gzip";
}
return "zstd"; // maximum/normal/unset → zstd for static runtime
})(),
},
});
}
}
catch (error) {
builder_util_1.log.error({ error: error.message }, "failed to build AppImage");
throw error;
}
finally {
await stageDir.cleanup().catch(() => { });
}
await packager.info.emitArtifactBuildCompleted({
file: artifactPath,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "AppImage", arch, false),
target: this,
arch,
packager,
isWriteUpdateInfo: true,
updateInfo,
});
}
}
exports.default = AppImageTarget;
//# sourceMappingURL=AppImageTarget.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,56 @@
import { Arch } from "builder-util";
import { FileAssociation } from "../../options/FileAssociation";
import { BlockMapDataHolder } from "builder-util-runtime";
import { ToolsetConfig } from "../../configuration";
import { IconInfo } from "../../util/iconConverter";
interface Options {
productName: string;
productFilename: string;
executableName: string;
desktopEntry: string;
icons: IconInfo[];
license?: string | null;
fileAssociations: FileAssociation[];
/**
* The compression type available for static runtime is limited as it's only compiled with support for gzip and zstd.
* "xz" is only valid for the legacy FUSE2 (0.0.0) toolset.
*
* [stderr] Squashfs image uses lzo compression, this version supports only zlib, zstd.
* Failed to open squashfs image
* Failed to extract AppImage
*
*/
compression?: "gzip" | "zstd" | "xz";
/** Pre-computed desktop basename (already validated). When absent, falls back to `executableName`. */
desktopBaseName?: string;
}
export interface AppImageBuilderOptions {
appDir: string;
stageDir: string;
arch: Arch;
output: string;
options: Options;
}
export declare function buildStaticRuntimeAppImage(appimageToolVersion: ToolsetConfig["appimage"], opts: AppImageBuilderOptions): Promise<BlockMapDataHolder>;
export declare function buildLegacyFuse2AppImage(opts: AppImageBuilderOptions): Promise<BlockMapDataHolder>;
/**
* Validates that critical path fields (executable name, product filename, license filename)
* contain only characters that are safe for use in filesystem paths and embedded bash strings.
* Allowed: Unicode letters, digits, dots, underscores, hyphens, and spaces.
*/
export declare function validateCriticalPathString(str: string, fieldName: string): void;
export type AppRunScriptBase = {
ExecutableName: string;
DesktopFileName: string;
ProductFilename: string;
ProductName: string;
ResourceName: string;
MimeTypeFile?: string;
};
export type AppRunScriptWithEula = AppRunScriptBase & {
EulaFile: string;
IsHtmlEula: boolean;
};
export type AppRunScript = AppRunScriptBase | AppRunScriptWithEula;
export declare function generateAppRunScript(config: AppRunScript): string;
export {};

View file

@ -0,0 +1,323 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildStaticRuntimeAppImage = buildStaticRuntimeAppImage;
exports.buildLegacyFuse2AppImage = buildLegacyFuse2AppImage;
exports.validateCriticalPathString = validateCriticalPathString;
exports.generateAppRunScript = generateAppRunScript;
const builder_util_1 = require("builder-util");
const fs = require("fs-extra");
const path = require("path");
const linux_1 = require("../../toolsets/linux");
const appLauncher_1 = require("./appLauncher");
const differentialUpdateInfoBuilder_1 = require("../differentialUpdateInfoBuilder");
const AppImageTarget_1 = require("./AppImageTarget");
async function buildStaticRuntimeAppImage(appimageToolVersion, opts) {
const { stageDir, output, appDir, options, arch } = opts;
try {
await fs.remove(output);
// Write AppRun launcher and related files
await writeAppLauncherAndRelatedFiles(opts);
const { runtimeLibraries: libraries, runtime, mksquashfs } = await (0, linux_1.getAppImageTools)(appimageToolVersion, arch);
await (0, builder_util_1.copyDir)(libraries, path.join(stageDir, "usr", "lib"));
// Copy app directory to stage
// mksquashfs doesn't support merging, so we copy the entire app dir
await (0, builder_util_1.copyDir)(appDir, stageDir);
const runtimeData = await fs.readFile(runtime);
// Create squashfs with offset for runtime
const args = [stageDir, output, "-offset", runtimeData.length.toString(), "-all-root", "-noappend", "-no-progress", "-quiet", "-no-xattrs", "-no-fragments"];
if (options.compression) {
args.push("-comp", options.compression);
}
await (0, builder_util_1.exec)(mksquashfs, args, {
cwd: stageDir,
});
// Write runtime data at the beginning of the file
await writeRuntimeData(output, runtimeData);
// Make executable
await fs.chmod(output, 0o755);
// Append blockmap inside try block to ensure cleanup on failure
const updateInfo = await (0, differentialUpdateInfoBuilder_1.appendBlockmap)(output);
return updateInfo;
}
catch (error) {
// Clean up partial build on failure
await fs.remove(output).catch(() => { });
throw error;
}
}
async function buildLegacyFuse2AppImage(opts) {
const { stageDir, output, appDir, options, arch } = opts;
try {
await fs.remove(output);
await writeAppLauncherAndRelatedFiles(opts);
const { runtime, mksquashfs, runtimeLibraries } = await (0, linux_1.getAppImageTools)("0.0.0", arch);
// Mirror the app-builder-lib Go implementation: bundle lib/<arch> into usr/lib for x64 and ia32.
// arm targets don't have a dedicated lib dir in the FUSE2 toolset.
if (arch === builder_util_1.Arch.x64 || arch === builder_util_1.Arch.ia32) {
await (0, builder_util_1.copyDir)(runtimeLibraries, path.join(stageDir, "usr", "lib"));
}
await (0, builder_util_1.copyDir)(appDir, stageDir);
const runtimeData = await fs.readFile(runtime);
const args = [stageDir, output, "-offset", runtimeData.length.toString(), "-all-root", "-noappend", "-no-progress", "-quiet", "-no-xattrs", "-no-fragments"];
if (options.compression) {
args.push("-comp", options.compression);
if (options.compression === "xz") {
// Match the dictionary/block-size settings used by the original app-builder Go implementation
args.push("-Xdict-size", "100%", "-b", "1048576");
}
}
await (0, builder_util_1.exec)(mksquashfs, args, { cwd: stageDir });
await writeRuntimeData(output, runtimeData);
await fs.chmod(output, 0o755);
const updateInfo = await (0, differentialUpdateInfoBuilder_1.appendBlockmap)(output);
return updateInfo;
}
catch (error) {
await fs.remove(output).catch(() => { });
throw error;
}
}
async function writeRuntimeData(filePath, runtimeData) {
const fd = await fs.open(filePath, "r+");
try {
await fs.write(fd, runtimeData, 0, runtimeData.length, 0);
}
finally {
try {
await fs.close(fd);
}
catch (closeError) {
// Log but don't throw - preserve original error if any
builder_util_1.log.warn({ message: closeError.message, file: filePath }, `failed to close file descriptor`);
}
}
}
/**
* Escapes a string for safe use in shell scripts by wrapping in single quotes
* and escaping any single quotes within the string.
*
* This allows strings with spaces, special characters, etc. to be safely used.
*/
function escapeShellString(str) {
// Escape single quotes by replacing ' with '\''
// Then wrap the whole string in single quotes
return `'${str.replace(/'/g, "'\\''")}'`;
}
/**
* Validates that critical path fields (executable name, product filename, license filename)
* contain only characters that are safe for use in filesystem paths and embedded bash strings.
* Allowed: Unicode letters, digits, dots, underscores, hyphens, and spaces.
*/
function validateCriticalPathString(str, fieldName) {
if (!/^[\p{L}\p{N}._\- ]+$/u.test(str)) {
throw new builder_util_1.InvalidConfigurationError(`${fieldName} contains characters that cannot be safely used in file paths: ${str}. Please use only letters, digits, hyphens, underscores, dots, and spaces.`);
}
}
async function writeAppLauncherAndRelatedFiles(opts) {
const { stageDir, options: { license, executableName, productFilename, productName, desktopEntry, desktopBaseName }, } = opts;
// executableName and productFilename are embedded directly into double-quoted bash strings
// and used as filesystem paths, so they must pass the whitelist.
// productName is not validated here because it is only ever passed through escapeShellString().
validateCriticalPathString(executableName, "executableName");
validateCriticalPathString(productFilename, "productFilename");
// Write desktop file
const desktopFileName = `${desktopBaseName !== null && desktopBaseName !== void 0 ? desktopBaseName : executableName}.desktop`;
await fs.writeFile(path.join(stageDir, desktopFileName), desktopEntry, { mode: 0o644 });
await (0, appLauncher_1.copyIcons)(opts);
const templateConfig = {
DesktopFileName: desktopFileName,
ExecutableName: executableName,
ProductName: productName,
ProductFilename: productFilename,
ResourceName: `appimagekit-${executableName}`,
};
const mimeTypeFile = await (0, appLauncher_1.copyMimeTypes)(stageDir, opts.options);
if (mimeTypeFile) {
templateConfig.MimeTypeFile = mimeTypeFile;
}
let finalConfig = templateConfig;
// Copy license file if provided
if (license) {
// Validate license file exists
if (!(await (0, builder_util_1.exists)(license))) {
throw new builder_util_1.InvalidConfigurationError(`License file not found: ${license}`);
}
const licenseBaseName = path.basename(license);
const ext = path.extname(license).toLowerCase();
// Validate license filename for path safety
validateCriticalPathString(licenseBaseName, "licenseBaseName");
// Validate extension
if (![".txt", ".html"].includes(ext)) {
builder_util_1.log.warn({ license, expected: ".txt or .html" }, `license file has unexpected extension`);
}
await (0, builder_util_1.copyFile)(license, path.join(stageDir, licenseBaseName));
finalConfig = {
...templateConfig,
EulaFile: licenseBaseName,
IsHtmlEula: ext === ".html",
};
}
const appRunContent = generateAppRunScript(finalConfig);
await fs.writeFile(path.join(stageDir, AppImageTarget_1.APP_RUN_ENTRYPOINT), appRunContent, { mode: 0o755 });
}
function hasEula(config) {
return "EulaFile" in config && typeof config.EulaFile === "string";
}
function generateAppRunScript(config) {
const eulaEnabled = hasEula(config);
return `#!/usr/bin/env bash
set -e
THIS="$0"
# http://stackoverflow.com/questions/3190818/
args=("$@")
NUMBER_OF_ARGS="$#"
if [ -z "$APPDIR" ] ; then
# Find the AppDir. It is the directory that contains AppRun.
# This assumes that this script resides inside the AppDir or a subdirectory.
# If this script is run inside an AppImage, then the AppImage runtime likely has already set $APPDIR
path="$(dirname "$(readlink -f "\${THIS}")")"
while [[ "$path" != "" && ! -e "$path/${AppImageTarget_1.APP_RUN_ENTRYPOINT}" ]]; do
path=\${path%/*}
done
APPDIR="$path"
fi
if [ -z "$APPDIR" ] ; then
echo "ERROR: could not locate the AppDir. Ensure this script is run from within a properly structured AppImage." >&2
exit 1
fi
export PATH="\${APPDIR}:\${APPDIR}/usr/sbin\${PATH:+:\${PATH}}"
export XDG_DATA_DIRS="\${APPDIR}/usr/share/\${XDG_DATA_DIRS:+:\${XDG_DATA_DIRS}}:/usr/share/gnome:/usr/local/share/:/usr/share/"
export LD_LIBRARY_PATH="\${APPDIR}/usr/lib\${LD_LIBRARY_PATH:+:\${LD_LIBRARY_PATH}}"
export GSETTINGS_SCHEMA_DIR="\${APPDIR}/usr/share/glib-2.0/schemas\${GSETTINGS_SCHEMA_DIR:+:\${GSETTINGS_SCHEMA_DIR}}"
BIN="$APPDIR/${config.ExecutableName}"
if [ -z "$APPIMAGE_EXIT_AFTER_INSTALL" ] ; then
trap atexit EXIT
fi
isEulaAccepted=1
HAVE_NO_SANDBOX=0
for arg in "\${args[@]}" ; do
if [ "$arg" = --no-sandbox ] ; then
HAVE_NO_SANDBOX=1
break
fi
done
NO_SANDBOX=()
# Use 'unshare -Ur true' as a heuristic to detect whether user namespaces are available.
# Notes:
# - When running as root, this check will always succeed even if the sandbox configuration
# actually relies on unprivileged user namespaces. In practice, Chrome/Electron usually
# disables or adjusts the sandbox separately when running as root, so this probe is mostly
# a no-op in that scenario.
# - On minimal systems (e.g. Alpine or stripped-down containers) 'unshare' may not exist.
# In that case the shell will return exit code 127 ("command not found"), which will cause
# us to add '--no-sandbox'. This is an intentional fail-safe: we prefer the app to start
# without sandboxing rather than crash on startup.
if [ $HAVE_NO_SANDBOX -eq 0 ] && ! unshare -Ur true 2>/dev/null ; then
NO_SANDBOX=(--no-sandbox)
fi
atexit()
{
if [ $isEulaAccepted == 1 ] ; then
if [ $NUMBER_OF_ARGS -eq 0 ] ; then
exec "$BIN" "\${NO_SANDBOX[@]}"
else
exec "$BIN" "\${NO_SANDBOX[@]}" "\${args[@]}"
fi
fi
}
error()
{
if [ -x /usr/bin/zenity ] ; then
LD_LIBRARY_PATH="" zenity --error --text "\${1}" 2>/dev/null
elif [ -x /usr/bin/kdialog ] ; then
LD_LIBRARY_PATH="" kdialog --msgbox "\${1}" 2>/dev/null
elif [ -x /usr/bin/Xdialog ] ; then
LD_LIBRARY_PATH="" Xdialog --msgbox "\${1}" 2>/dev/null
else
echo "\${1}"
fi
exit 1
}
yesno()
{
TITLE=$1
TEXT=$2
if [ -x /usr/bin/zenity ] ; then
LD_LIBRARY_PATH="" zenity --question --title="$TITLE" --text="$TEXT" 2>/dev/null || exit 0
elif [ -x /usr/bin/kdialog ] ; then
LD_LIBRARY_PATH="" kdialog --title "$TITLE" --yesno "$TEXT" || exit 0
elif [ -x /usr/bin/Xdialog ] ; then
LD_LIBRARY_PATH="" Xdialog --title "$TITLE" --clear --yesno "$TEXT" 10 80 || exit 0
else
echo "zenity, kdialog, Xdialog missing. Skipping \${THIS}."
exit 0
fi
}
check_dep()
{
DEP=$1
if ! command -v "$DEP" &>/dev/null ; then
echo "$DEP is missing. Skipping \${THIS}."
exit 0
fi
}
if [ -z "$APPIMAGE" ] ; then
APPIMAGE="$APPDIR/${AppImageTarget_1.APP_RUN_ENTRYPOINT}"
# not running from within an AppImage; hence using the AppRun for Exec=
fi
${eulaEnabled
? `if [ -z "$APPIMAGE_SILENT_INSTALL" ] ; then
EULA_MARK_DIR="\${XDG_CONFIG_HOME:-$HOME/.config}/${config.ProductFilename}"
EULA_MARK_FILE="$EULA_MARK_DIR/eulaAccepted"
# show EULA only if desktop file doesn't exist
if [ ! -e "$EULA_MARK_FILE" ] ; then
if [ -x /usr/bin/zenity ] ; then
# on cancel simply exits and our trap handler launches app, so, $isEulaAccepted is set here to 0 and then to 1 if EULA accepted
isEulaAccepted=0
LD_LIBRARY_PATH="" zenity --text-info --title=${escapeShellString(config.ProductName)} --filename="$APPDIR/${config.EulaFile}" --ok-label=Agree --cancel-label=Disagree ${config.IsHtmlEula ? "--html" : ""}
elif [ -x /usr/bin/Xdialog ] ; then
isEulaAccepted=0
LD_LIBRARY_PATH="" Xdialog --title ${escapeShellString(config.ProductName)} --textbox "$APPDIR/${config.EulaFile}" 30 80 --ok-label Agree --cancel-label Disagree
elif [ -x /usr/bin/kdialog ] ; then
# cannot find any option to force Agree/Disagree buttons for kdialog. And official example exactly with OK button https://techbase.kde.org/Development/Tutorials/Shell_Scripting_with_KDE_Dialogs#Example_21._--textbox_dialog_box
# in any case we pass labels text
isEulaAccepted=0
LD_LIBRARY_PATH="" kdialog --textbox "$APPDIR/${config.EulaFile}" --yes-label Agree --cancel-label "Disagree"
fi
case $? in
0)
isEulaAccepted=1
echo "License accepted"
mkdir -p "$EULA_MARK_DIR"
touch "$EULA_MARK_FILE"
;;
1)
echo "License not accepted"
exit 0
;;
-1)
echo "An unexpected error has occurred."
isEulaAccepted=1
;;
esac
fi
fi`
: ""}
`;
}
//# sourceMappingURL=appImageUtil.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
import { AppImageBuilderOptions } from "./appImageUtil";
export declare function copyIcons(options: AppImageBuilderOptions): Promise<void>;
export declare function copyMimeTypes(stageDir: string, options: Pick<AppImageBuilderOptions["options"], "fileAssociations" | "productName" | "executableName">): Promise<string | null>;

View file

@ -0,0 +1,95 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.copyIcons = copyIcons;
exports.copyMimeTypes = copyMimeTypes;
const path = require("path");
const fs = require("fs-extra");
const builder_util_1 = require("builder-util");
const ICON_DIR_RELATIVE_PATH = "usr/share/icons/hicolor";
const MIME_TYPE_DIR_RELATIVE_PATH = "usr/share/mime/packages";
/**
* Escapes special XML characters to prevent injection
*/
function xmlEscape(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
async function copyIcons(options) {
const { stageDir, options: configuration } = options;
const iconCommonDir = path.join(stageDir, ICON_DIR_RELATIVE_PATH);
await fs.ensureDir(iconCommonDir);
const icons = configuration.icons;
if (!icons || icons.length === 0) {
throw new Error("At least one icon is required for AppImage");
}
const iconExtWithDot = path.extname(icons[0].file);
const iconFileName = `${configuration.executableName}${iconExtWithDot}`;
const maxIconIndex = icons.length - 1;
const iconInfoList = icons.map(icon => {
if (path.extname(icon.file) !== iconExtWithDot) {
throw new Error(`All icons must have the same extension: expected ${iconExtWithDot}, but got ${icon.file}`);
}
let iconSizeDir;
if (iconExtWithDot === ".svg") {
// SVG icons go in scalable/apps directory per freedesktop icon theme spec
iconSizeDir = "scalable/apps";
}
else {
iconSizeDir = `${icon.size}x${icon.size}/apps`;
}
const iconRelativeToStageFile = path.join(ICON_DIR_RELATIVE_PATH, iconSizeDir, iconFileName);
const iconDir = path.join(iconCommonDir, iconSizeDir);
const iconFile = path.join(iconDir, iconFileName);
return { icon, iconDir, iconFile, iconRelativeToStageFile };
});
await Promise.all(iconInfoList.map(async ({ icon, iconDir, iconFile }) => {
await fs.ensureDir(iconDir);
await (0, builder_util_1.copyOrLinkFile)(icon.file, iconFile);
}));
// Create symlinks for the last (largest) icon
const { iconRelativeToStageFile } = iconInfoList[maxIconIndex];
await fs.symlink(iconRelativeToStageFile, path.join(stageDir, iconFileName));
await fs.symlink(iconRelativeToStageFile, path.join(stageDir, ".DirIcon"));
}
async function copyMimeTypes(stageDir, options) {
const { fileAssociations, productName, executableName } = options;
if (!fileAssociations || fileAssociations.length === 0) {
return null;
}
if (!fileAssociations || fileAssociations.length === 0) {
return null;
}
const mimeTypeParts = [];
for (const fileAssociation of fileAssociations) {
if (!fileAssociation.mimeType) {
continue;
}
// XML-escape to prevent injection
mimeTypeParts.push(`<mime-type type="${xmlEscape(fileAssociation.mimeType)}">`);
mimeTypeParts.push(` <comment>${xmlEscape(productName)} document</comment>`);
// Handle extension(s)
const extensions = Array.isArray(fileAssociation.ext) ? fileAssociation.ext : [fileAssociation.ext];
for (const ext of extensions) {
// Validate extension doesn't contain dangerous characters
if (!/^[a-zA-Z0-9_-]+$/.test(ext)) {
builder_util_1.log.warn({ extension: ext }, `file extension contains unexpected characters and will be skipped`);
continue;
}
mimeTypeParts.push(` <glob pattern="*.${xmlEscape(ext)}"/>`);
}
mimeTypeParts.push(' <generic-icon name="x-office-document"/>');
mimeTypeParts.push("</mime-type>");
}
// If no mime-types were generated, return null
if (mimeTypeParts.length === 0) {
return null;
}
const mimeTypeDir = path.join(stageDir, MIME_TYPE_DIR_RELATIVE_PATH);
const fileName = `${executableName}.xml`;
const mimeTypeFile = path.join(mimeTypeDir, fileName);
await fs.ensureDir(mimeTypeDir);
const xmlContent = ['<?xml version="1.0"?>', '<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">', ...mimeTypeParts, "</mime-info>"].join("\n");
// Use 0o644 (rw-r--r--) instead of 0o666 to avoid world-writable permissions
await fs.writeFile(mimeTypeFile, xmlContent, { mode: 0o644 });
return path.join(MIME_TYPE_DIR_RELATIVE_PATH, fileName);
}
//# sourceMappingURL=appLauncher.js.map

File diff suppressed because one or more lines are too long

View file

@ -17,5 +17,10 @@ export interface ArchiveOptions {
excluded?: Array<string> | null;
method?: "Copy" | "LZMA" | "Deflate" | "DEFAULT";
isRegularFile?: boolean;
/**
* Use native macOS `zip` to preserve symlinks. Required for .framework bundles.
* @default false
*/
preserveSymlinks?: boolean;
}
export declare function compute7zCompressArgs(format: string, options?: ArchiveOptions): string[];

View file

@ -1,15 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.archive = exports.compute7zCompressArgs = exports.tar = void 0;
const _7zip_bin_1 = require("7zip-bin");
exports.tar = tar;
exports.compute7zCompressArgs = compute7zCompressArgs;
exports.archive = archive;
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const tar_1 = require("tar");
const tools_1 = require("./tools");
const linux_1 = require("../toolsets/linux");
const _7zip_1 = require("../toolsets/7zip");
const ALLOWED_7Z_FILTERS = new Set(["BCJ", "BCJ2", "ARM", "ARMT", "IA64", "PPC", "SPARC", "DELTA"]);
function validateCompressionLevel(level) {
if (!/^[0-9]$/.test(level)) {
throw new Error(`ELECTRON_BUILDER_COMPRESSION_LEVEL must be a single digit 0-9, got: "${level}"`);
}
}
/** @internal */
async function tar(compression, format, outFile, dirToArchive, isMacApp, tempDirManager) {
async function tar({ compression, format, outFile, dirToArchive, isMacApp, tempDirManager }) {
const tarFile = await tempDirManager.getTempFile({ suffix: ".tar" });
const tarArgs = {
file: tarFile,
@ -24,59 +31,49 @@ async function tar(compression, format, outFile, dirToArchive, isMacApp, tempDir
tarDirectory = path.basename(dirToArchive);
}
await Promise.all([
tar_1.create(tarArgs, [tarDirectory]),
(0, tar_1.create)(tarArgs, [tarDirectory]),
// remove file before - 7z doesn't overwrite file, but update
fs_1.unlinkIfExists(outFile),
(0, builder_util_1.unlinkIfExists)(outFile),
]);
if (format === "tar.lz") {
// noinspection SpellCheckingInspection
let lzipPath = "lzip";
if (process.platform === "darwin") {
lzipPath = path.join(await tools_1.getLinuxToolsPath(), "bin", lzipPath);
}
await builder_util_1.exec(lzipPath, [compression === "store" ? "-1" : "-9", "--keep" /* keep (don't delete) input files */, tarFile]);
// bloody lzip creates file in the same dir where input file with postfix `.lz`, option --output doesn't work
await fs_extra_1.move(`${tarFile}.lz`, outFile);
const lzipPath = process.platform === "darwin" ? (await (0, linux_1.getLinuxToolsMacToolset)()).lzip : "lzip";
await (0, builder_util_1.exec)(lzipPath, [compression === "store" ? "-1" : "-9", "--keep" /* keep (don't delete) input files */, tarFile]);
// lzip creates the output file in the same directory as the input with a .lz suffix
await (0, fs_extra_1.move)(`${tarFile}.lz`, outFile);
return;
}
const args = compute7zCompressArgs(format === "tar.xz" ? "xz" : format === "tar.bz2" ? "bzip2" : "gzip", {
isRegularFile: true,
method: "DEFAULT",
compression,
});
const compressFormat = format === "tar.xz" ? "xz" : format === "tar.bz2" ? "bzip2" : "gzip";
const args = compute7zCompressArgs(compressFormat, { isRegularFile: true, method: "DEFAULT", compression });
args.push(outFile, tarFile);
await builder_util_1.exec(_7zip_bin_1.path7za, args, {
cwd: path.dirname(dirToArchive),
}, builder_util_1.debug7z.enabled);
await (0, builder_util_1.exec)(await (0, _7zip_1.getPath7za)(), args, { cwd: path.dirname(dirToArchive) }, builder_util_1.debug7z.enabled);
}
exports.tar = tar;
function compute7zCompressArgs(format, options = {}) {
let storeOnly = options.compression === "store";
const args = debug7zArgs("a");
let isLevelSet = false;
if (process.env.ELECTRON_BUILDER_COMPRESSION_LEVEL != null) {
storeOnly = false;
args.push(`-mx=${process.env.ELECTRON_BUILDER_COMPRESSION_LEVEL}`);
isLevelSet = true;
const compressionLevel = process.env.ELECTRON_BUILDER_COMPRESSION_LEVEL;
if (compressionLevel != null) {
validateCompressionLevel(compressionLevel);
storeOnly = false; // env var overrides "store" config
args.push(`-mx=${compressionLevel}`);
}
const isZip = format === "zip";
if (!storeOnly) {
else if (storeOnly) {
// -mx=0 is the universal "no compression" flag across all formats (zip, 7z, gzip, xz, bzip2).
// -mm=Copy would only be valid for zip/7z and causes E_INVALIDARG on xz/gzip/bzip2.
args.push("-mx=0");
}
else {
const isZip = format === "zip";
// ZIP uses level 7 by default; everything else (7z, gzip, xz, bzip2) uses level 9
args.push("-mx=" + (isZip && options.compression !== "maximum" ? "7" : "9"));
if (isZip && options.compression === "maximum") {
// http://superuser.com/a/742034
args.push("-mfb=258", "-mpass=15");
}
if (!isLevelSet) {
// https://github.com/electron-userland/electron-builder/pull/3032
args.push("-mx=" + (!isZip || options.compression === "maximum" ? "9" : "7"));
}
}
if (options.dictSize != null) {
args.push(`-md=${options.dictSize}m`);
}
// https://sevenzip.osdn.jp/chm/cmdline/switches/method.htm#7Z
// https://stackoverflow.com/questions/27136783/7zip-produces-different-output-from-identical-input
// tc and ta are off by default, but to be sure, we explicitly set it to off
// disable "Stores NTFS timestamps for files: Modification time, Creation time, Last access time." to produce the same archive for the same data
// Disable NTFS timestamps for reproducible archives
if (!options.isRegularFile) {
args.push("-mtc=off");
}
@ -87,55 +84,90 @@ function compute7zCompressArgs(format, options = {}) {
if (options.isArchiveHeaderCompressed === false) {
args.push("-mhc=off");
}
// args valid only for 7z
// -mtm=off disable "Stores last Modified timestamps for files."
const sevenZFilter = process.env.ELECTRON_BUILDER_7Z_FILTER;
if (sevenZFilter) {
if (!ALLOWED_7Z_FILTERS.has(sevenZFilter.toUpperCase())) {
throw new Error(`ELECTRON_BUILDER_7Z_FILTER must be one of: ${[...ALLOWED_7Z_FILTERS].join(", ")}`);
}
args.push(`-mf=${sevenZFilter}`);
}
args.push("-mtm=off", "-mta=off");
}
if (options.method != null) {
if (options.method !== "DEFAULT") {
args.push(`-mm=${options.method}`);
}
if (options.method != null && options.method !== "DEFAULT") {
args.push(`-mm=${options.method}`);
}
else if (isZip || storeOnly) {
else if (format === "zip") {
// -mm is only set explicitly for zip (Deflate/Copy) and includes the UTF-8 flag.
// For all other formats the codec is implicit from the output file extension.
args.push(`-mm=${storeOnly ? "Copy" : "Deflate"}`);
}
if (isZip) {
// -mcu switch: 7-Zip uses UTF-8, if there are non-ASCII symbols.
// because default mode: 7-Zip uses UTF-8, if the local code page doesn't contain required symbols.
// but archive should be the same regardless where produced
args.push("-mcu");
}
return args;
}
exports.compute7zCompressArgs = compute7zCompressArgs;
// 7z is very fast, so, use ultra compression
/** @internal */
async function archive(format, outFile, dirToArchive, options = {}) {
const args = compute7zCompressArgs(format, options);
// remove file before - 7z doesn't overwrite file, but update
await fs_1.unlinkIfExists(outFile);
args.push(outFile, options.withoutDir ? "." : path.basename(dirToArchive));
if (options.excluded != null) {
for (const mask of options.excluded) {
args.push(`-xr!${mask}`);
const outFileStat = await (0, builder_util_1.statOrNull)(outFile);
const dirStat = await (0, builder_util_1.statOrNull)(dirToArchive);
if (outFileStat && dirStat && outFileStat.mtime > dirStat.mtime) {
builder_util_1.log.info({ reason: "Archive file is up to date", outFile }, `skipped archiving`);
return outFile;
}
// On macOS, use native `zip` when symlink preservation is required (e.g. .framework bundles).
// 7zip dereferences symlinks, corrupting .framework structure and breaking codesign.
// Only opt in via preserveSymlinks — Windows zip targets built on macOS still use 7z
// so that the UTF-8 bit (-mcu) is set correctly in the zip header.
const use7z = !(process.platform === "darwin" && format === "zip" && options.preserveSymlinks);
if (use7z) {
const args = compute7zCompressArgs(format, options);
await (0, builder_util_1.unlinkIfExists)(outFile);
args.push(outFile, options.withoutDir ? "." : path.basename(dirToArchive));
if (options.excluded != null) {
for (const mask of options.excluded) {
if (mask.includes("..")) {
throw new Error(`Excluded archive pattern contains path traversal sequence: "${mask}"`);
}
args.push(`-xr!${mask}`);
}
}
try {
await (0, builder_util_1.exec)(await (0, _7zip_1.getPath7za)(), args, { cwd: options.withoutDir ? dirToArchive : path.dirname(dirToArchive) }, builder_util_1.debug7z.enabled);
}
catch (e) {
if (e.code === "ENOENT" && !(await (0, builder_util_1.exists)(dirToArchive))) {
throw new Error(`Cannot create archive: "${dirToArchive}" doesn't exist`);
}
else {
throw e;
}
}
}
try {
await builder_util_1.exec(_7zip_bin_1.path7za, args, {
cwd: options.withoutDir ? dirToArchive : path.dirname(dirToArchive),
}, builder_util_1.debug7z.enabled);
}
catch (e) {
if (e.code === "ENOENT" && !(await fs_1.exists(dirToArchive))) {
throw new Error(`Cannot create archive: "${dirToArchive}" doesn't exist`);
else {
// macOS native zip: -y preserves symlinks (required for .framework bundles)
const args = ["-q", "-r", "-y"];
if (builder_util_1.debug7z.enabled) {
args.push("-v");
}
if (options.compression === "store") {
args.push("-0");
}
else {
throw e;
args.push(options.compression === "maximum" ? "-9" : "-7");
}
await (0, builder_util_1.unlinkIfExists)(outFile);
args.push(outFile, options.withoutDir ? "." : path.basename(dirToArchive));
if (options.excluded != null) {
for (const mask of options.excluded) {
if (mask.includes("..")) {
throw new Error(`Excluded archive pattern contains path traversal sequence: "${mask}"`);
}
args.push(`-x${mask}`);
}
}
await (0, builder_util_1.exec)("zip", args, { cwd: options.withoutDir ? dirToArchive : path.dirname(dirToArchive) }, builder_util_1.debug7z.enabled);
}
return outFile;
}
exports.archive = archive;
function debug7zArgs(command) {
const args = [command, "-bd"];
if (builder_util_1.debug7z.enabled) {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
import { BlockMapDataHolder } from "builder-util-runtime";
export type CompressionFormat = "deflate" | "gzip";
/**
* Build a content-defined block map for `inFile` using Rabin fingerprinting.
*
* Files are processed via streaming (peak memory RABIN_MAX = 32 KB per chunk,
* not the full file size), making this safe for large installers.
*
* - If `outFile` is omitted: compressed blockmap is appended to `inFile`
* (used for NSIS web installer / AppImage embed); `blockMapSize` is returned.
* - If `outFile` is provided: compressed blockmap is written to that file;
* `blockMapSize` is not included in the result.
*
* Returned `sha512` is SHA-512 of the full file as it exists after the call.
*/
export declare function buildBlockMap(inFile: string, compressionFormat: CompressionFormat, outFile?: string): Promise<BlockMapDataHolder>;

View file

@ -0,0 +1,153 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildBlockMap = buildBlockMap;
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const zlib = require("zlib");
const blake2_js_1 = require("@noble/hashes/blake2.js");
// Rabin fingerprinting constants from github.com/aclements/go-rabin:
// Poly64 = 0xbfe6b8a5bf378d83 (degree-63 irreducible polynomial over GF(2))
// Window = 64 bytes, Avg = 16 KB, Min = 8 KB, Max = 32 KB
// Boundary condition: (hash & (AVG-1)) === (AVG-1), i.e. all mask bits set.
//
// The tables below are precomputed offline (avoiding BigInt in production):
// PUSH[i] = (i * x^63 + (i * x^63 mod p)) truncated to 64 bits
// POP[i] = (i * x^(63*8)) mod p
// Each stored as signed Int32 hi/lo pair; hash = hi*2^32 + lo (unsigned 64-bit).
// prettier-ignore
const PUSH_HI = new Int32Array([0, -1075398491, 2144170315, -1070872082, 1081891379, -6626666, 1068575608, -2141744163, 1058933955, -2131184538, 1087602056, -13253331, 2137151216, -1065030059, 11478971, -1085694178, 2117867910, -1042636509, 32598221, -1105735576, 1044399029, -2119763184, 1099772670, -26506661, 1092752709, -20664864, 1055876110, -2130060117, 22957942, -1095174189, 2123578941, -1049262440, 1134463913, -59231476, 1012136674, -2085273017, 65196442, -1140561601, 2083496145, -1010231180, 2088798058, -1016709169, 55440929, -1129623932, 1023204697, -2095421956, 1127328786, -53013321, 1034064431, -2109461878, 1114628964, -41329727, 2111752220, -1036488519, 34847063, -1108016654, 45915884, -1118165431, 2104618919, -1030269182, 1119929567, -47809414, 1024308628, -2098524879, 953921527, -2026039470, 1192681148, -118462951, 2024273348, -952021663, 124421263, -1198769110, 130392884, -1205658735, 2013844095, -940676390, 1203370247, -127975006, 947161164, -2020462359, 1189590641, -117371180, 959104826, -2033418337, 110881858, -1182968601, 2035719433, -961534548, 2046409394, -971046377, 104123385, -1177390244, 965075073, -2040309724, 1179161034, -106026641, 2068128862, -995910405, 76043541, -1150358096, 993623661, -2065709368, 1156845350, -82659453, 1146824861, -71462856, 999709142, -2072977037, 69694126, -1144927733, 2078933989, -1005798592, 91831768, -1163950723, 2058636435, -984419274, 1157982187, -85729458, 986191520, -2060538363, 979841307, -2055108162, 1168785488, -95618827, 2048617256, -973220979, 97917539, -1171217722, 1907843054, -833493173, 242888357, -1315138048, 835388893, -1909605000, 1309045910, -236925901, 1319719725, -246420600, 828645990, -1904043325, 248842526, -1322012229, 1897429077, -822165264, 260785768, -1334968627, 1883649827, -811561082, 1341594715, -267279106, 809135376, -1881352779, 815090347, -1888226802, 1331182560, -255950011, 1894322328, -821057475, 254042579, -1329407626, 841602119, -1915786014, 1306830092, -234742359, 1918209652, -843893039, 228130623, -1300346982, 221763716, -1294901215, 1929030095, -853798550, 1296794295, -223528430, 847705084, -1923069095, 1276497345, -202148508, 869842058, -1942092753, 208246770, -1282461865, 1940186809, -868065764, 1930150146, -856852057, 214347849, -1289746196, 863476529, -1936645228, 1287318138, -212053281, 1231876121, -158709572, 916554066, -1991820809, 152087082, -1225387377, 1994251105, -918854716, 1987247322, -913030017, 163548561, -1235667660, 906929897, -1981276596, 1237571490, -165318905, 928049567, -2001317574, 1218287828, -142925711, 1999418284, -926282999, 149013223, -1224246718, 139388252, -1213702663, 2005111831, -932893518, 1211285359, -137099318, 939511332, -2011597183, 183663536, -1256930539, 1967065851, -891702690, 1250828675, -177694426, 893604040, -1968838547, 904689523, -1979002922, 1243678264, -171458915, 1972383040, -898198043, 173890571, -1245977426, 1959682614, -886515053, 184750973, -1260016680, 884094981, -1957396320, 1266633038, -191237653, 1271951093, -197732784, 874323902, -1946441957, 195835078, -1270182813, 1952531853, -880280280]);
// prettier-ignore
const PUSH_LO = new Int32Array([0, -1086878333, 2121210630, -1051158907, 1139391375, -52546036, 1032233097, -2102317814, 954530461, -2016184546, 1183572379, -105092072, 2064466194, -1002779503, 90331668, -1168779369, 1909060922, -822183751, 262598204, -1332649025, 840978101, -1927822538, 1280267699, -210184144, 1227689895, -166034908, 927077537, -2005559006, 180663336, -1242351189, 1957408558, -878959955, 1554278391, -476845452, 583761137, -1644367502, 525196408, -1602596357, 1629669246, -569029891, 1681956202, -612953879, 439322220, -1525150737, 665396965, -1734431898, 1506164195, -420368288, 762155725, -1839587506, 1392675275, -332069816, 1854155074, -776756031, 283849284, -1344487481, 361326672, -1430329901, 1810264918, -724435243, 1449186271, -380150180, 672123097, -1757919910, 109003373, -1186410514, 2014337387, -953690904, 1167522274, -90082207, 1006232292, -2066845849, 1050392816, -2119420557, 1089774582, -3985803, 2104623999, -1035628804, 52238457, -1138059782, 2008463191, -931054892, 165260369, -1225907758, 878644440, -1956085413, 1244665822, -184051107, 1330793930, -261767095, 826103500, -1911891121, 213644869, -1282638906, 1926556995, -840736576, 1524311450, -437475303, 615792284, -1685867745, 422747669, -1509616746, 1734182163, -664139632, 1648353031, -586657148, 475055105, -1553512062, 567698568, -1629361909, 1605992334, -527502835, 722653344, -1809490653, 1434307494, -364231131, 1761307439, -674437460, 378826793, -1448870486, 334899773, -1396594754, 1838756155, -760300360, 1344246194, -282584015, 779127476, -1857616073, 218006746, -1278670503, 1922146268, -844754337, 1326923605, -266292522, 829957203, -1907381808, 874151495, -1959922748, 1249207617, -180164414, 2012464584, -926660533, 161275598, -1230286003, 2100785632, -1040122781, 56126182, -1133516955, 1054788207, -2115418132, 1085395305, -7971606, 1171491709, -85719298, 1002213499, -2071257608, 104476914, -1190281871, 2018847732, -949836169, 1348133677, -278040914, 775288875, -1862109784, 330520738, -1400580831, 1843151780, -756298201, 1757288880, -678849485, 382796470, -1444507851, 727163455, -1805635652, 1429780793, -368102214, 563287575, -1633379436, 1610353937, -523534190, 1652207000, -582148069, 471185054, -1558037731, 427289738, -1505730295, 1729689484, -667977201, 1520326405, -441853306, 619793411, -1681473152, 176277175, -1246344396, 1961811377, -874950606, 1231584568, -161484613, 923231806, -2010059843, 845495338, -1923960407, 1275733804, -214062417, 1905035173, -826602970, 266575011, -1328279264, 2068329357, -998261234, 86452363, -1173314296, 950110210, -2020211327, 1187943172, -101114233, 1135397136, -56933229, 1036243478, -2097913963, 4551327, -1082982628, 2116708761, -1055005670, 1445306688, -384684861, 675985990, -1753401403, 365697743, -1426352308, 1805844937, -728462262, 1858165725, -772352418, 279855323, -1348874920, 757653586, -1843434031, 1397226324, -328173865, 669799546, -1730422279, 1501777788, -424361217, 1678110709, -617454986, 443217139, -1520600720, 520662759, -1606474908, 1634186721, -565168030, 1558254952, -472475413, 579735150, -1648786451]);
// prettier-ignore
const POP_HI = new Int32Array([0, 2138251555, 1090583266, 1047780289, 1038568800, 1117023299, 2095560578, 60069537, 2077137601, 79543266, 986603555, 1170030848, 1177147297, 962262658, 120139075, 2019167328, 1216056615, 923315204, 159086533, 1980258022, 1973207111, 183492964, 882653861, 1273961350, 867506150, 1288058565, 1924525316, 231132199, 240278150, 1898019749, 1330815076, 807502151, 789684970, 1348633545, 1846630408, 291666219, 318173066, 1837485737, 1408636264, 746927179, 1423915051, 732698888, 366985929, 1789715434, 1765307723, 374035560, 674795433, 1464577674, 1735012301, 404295406, 644535599, 1494873100, 1519215277, 637420430, 462264399, 1694415212, 480556300, 1675072559, 1571049454, 584543949, 558102636, 1580259663, 1615004302, 523248557, 1579369941, 558916854, 522436407, 1615896084, 1673791669, 481765782, 583332439, 1572328308, 636346132, 1520369207, 1693263350, 463340757, 403346036, 1736029015, 1493854358, 645482933, 374917362, 1764485585, 1465397776, 673911603, 733971858, 1422697649, 1790934896, 365715027, 1838567987, 317027088, 748071121, 1407551986, 1349590867, 788676208, 292677041, 1845675154, 1899161407, 239191580, 808590813, 1329675518, 1289071199, 866552700, 232083645, 1923510686, 184319486, 1972329693, 1274840860, 881829439, 924528798, 1214779837, 1981532796, 157870943, 961112600, 1178225467, 2018087162, 121287129, 78522232, 2078082651, 1169087898, 987626681, 1116205273, 1039454714, 59181627, 2096376600, 2137046457, 1285274, 1046497115, 1091790456, 60830479, 2094207532, 1117833709, 1037297870, 1044872815, 1093951308, 2135393421, 3450286, 2020518350, 119376109, 963531564, 1176334863, 1166664878, 989513101, 76095052, 2079997807, 1272692264, 883465995, 182142154, 1973970409, 1983705928, 156226155, 926681514, 1213147273, 806692073, 1332086218, 1897258507, 241631016, 233990537, 1921075370, 1290965867, 864137800, 749834725, 1405268166, 1840343815, 314722852, 290905221, 1847983526, 1347823207, 790955844, 1467943716, 671885831, 377483718, 1762447589, 1788364356, 367748967, 731430054, 1424727429, 1690967234, 465124833, 634054176, 1522124547, 1496142242, 643723393, 405646144, 1734249059, 520390147, 1618454304, 1577352417, 561470914, 585354083, 1569778240, 1675833729, 479203490, 1568896730, 586176505, 478383160, 1676717339, 1617181626, 521607833, 560251224, 1578623099, 642640923, 1497287992, 1733105401, 406730714, 464167291, 1691975768, 1521114009, 635009722, 368638973, 1787550430, 1425539359, 730538044, 673167005, 1466734526, 1763658879, 376204636, 1849057596, 289751071, 792107998, 1346747133, 1406217308, 748817791, 315741886, 1839396765, 1922225200, 232912147, 865218258, 1289818097, 1333107024, 805746803, 242574258, 1896235665, 157044465, 1982820306, 1214034963, 925865264, 884671377, 1271407282, 1975253363, 180934736, 988371223, 1167751220, 2078909429, 77234902, 118363255, 2021471572, 1175383701, 964546486, 1093125078, 1045750517, 2570548, 2136217623, 2092994230, 62107541, 1036022868, 1119049079]);
// prettier-ignore
const POP_LO = new Int32Array([0, -1826189856, -1720019389, 173808547, -1916624635, 518270181, 347617094, -2019640666, 461718026, -2002530326, -2097163191, 299456937, -1773673713, 90271471, 255685964, -1675888468, -2009265769, 454788215, 289906644, -2107567564, 100640914, -1764153998, -1668989231, 262386481, -1816638563, 10405501, 180542942, -1713090498, 511371928, -1923324040, -2030011173, 338096443, -1353867603, 1013650253, 909576430, -1525578482, 579813288, -1314278840, -1141528085, 684925963, -1261904729, 669929799, 766659300, -1097545980, 956988834, -1439895486, -1603212319, 861300225, 661690170, -1269425446, -1106640519, 757894297, -1431161281, 966048735, 868786300, -1595003492, 1022743856, -1345103664, -1517337741, 917098131, -1321765835, 571603413, 676192886, -1150586986, 1587232090, -843206470, -941511911, 1422337785, -749101985, 1082069439, 1243810332, -653948932, 1159626576, -700910928, -597366509, 1329751283, -925049259, 1543132085, 1369851926, -1031748106, -693750579, 1166063917, 1339859598, -587583634, 1533318600, -935192536, -1038220405, 1362660971, -853313849, 1577450279, 1415176324, -947950236, 1088542658, -741909982, -644136575, 1253952609, -237591561, 1659907607, 1756116404, -74794924, 2081686258, -281899246, -445737807, 1984436561, -363601411, 2037738525, 1932097470, -535823778, 1737572600, -189281000, -18098501, 1842174811, 2045487712, -355653760, -527352797, 1941418435, -198636699, 1729070725, 1834196262, -25882426, 1651435626, -246913654, -82543063, 1748169673, -273921681, 2089469071, 1993793324, -437234996, 33683255, -1859683625, -1686412940, 140373140, -1883023822, 484826066, 381272177, -2053157487, 428127549, -1969080099, -2130828418, 332967582, -1807346632, 123771352, 222069371, -1642459237, -1975714144, 421163840, 323381475, -2141133565, 134111141, -1797729723, -1635464730, 228738054, -1850098518, 43987274, 147008233, -1679448311, 477830575, -1889693617, -2063496212, 371656204, -1387501158, 1047192698, 876019673, -1492094407, 546228383, -1280817793, -1175167268, 718459708, -1228330096, 636462704, 800308691, -1131073485, 990612117, -1473443979, -1569645354, 827822390, 628187149, -1235751443, -1140066738, 791509934, -1464614648, 999640296, 835278667, -1561339221, 1056186887, -1378701337, -1483819964, 883440036, -1288273150, 537923298, 709629249, -1184196447, 1553648237, -809744499, -975152082, 1455870414, -782734488, 1115612808, 1210252587, -620465973, 1193248871, -734460537, -563798492, 1296274372, -891475614, 1509663874, 1403502369, -1065274687, -727202822, 1199656474, 1306351033, -553920423, 1499816703, -901517537, -1071647556, 1396275548, -819822096, 1543769104, 1448613811, -981558701, 1121984757, -775508715, -610617674, 1220295510, -203991872, 1626462496, 1789772419, -108310685, 2115368389, -315394011, -412130426, 1951002214, -397273398, 2071239466, 1898479753, -502395543, 1703983055, -155829713, -51764852, 1875684460, 2078956887, -389230409, -493827308, 1907771124, -165086126, 1695445426, 1867672081, -59447311, 1617895261, -213282115, -116029154, 1781728510, -307380648, 2123051960, 1960257563, -403593733]);
// Chunker parameters
const RABIN_MIN = 8192;
const RABIN_AVG = 16384;
const RABIN_MAX = 32768;
const RABIN_WINDOW = 64;
// Boundary: all mask bits (14 bits) set in lo word. RABIN_AVG is a power of 2.
const RABIN_HASH_MASK = RABIN_AVG - 1; // 0x3FFF
function compress(data, format) {
return format === "deflate" ? zlib.deflateRawSync(data, { level: 9 }) : zlib.gzipSync(data, { level: 9 });
}
/**
* Build a content-defined block map for `inFile` using Rabin fingerprinting.
*
* Files are processed via streaming (peak memory RABIN_MAX = 32 KB per chunk,
* not the full file size), making this safe for large installers.
*
* - If `outFile` is omitted: compressed blockmap is appended to `inFile`
* (used for NSIS web installer / AppImage embed); `blockMapSize` is returned.
* - If `outFile` is provided: compressed blockmap is written to that file;
* `blockMapSize` is not included in the result.
*
* Returned `sha512` is SHA-512 of the full file as it exists after the call.
*/
async function buildBlockMap(inFile, compressionFormat, outFile) {
const fileHash = (0, crypto_1.createHash)("sha512");
const checksums = [];
const sizes = [];
let totalSize = 0;
// Per-chunk Rabin state. Peak memory ≈ RABIN_MAX (32 KB) — not file size.
const chunkBuf = Buffer.allocUnsafe(RABIN_MAX);
let chunkN = 0; // bytes accumulated for current chunk
let hi = 0;
let lo = 0;
const win = new Uint8Array(RABIN_WINDOW); // rolling window ring buffer
let wpos = 0;
function emitChunk() {
checksums.push(Buffer.from((0, blake2_js_1.blake2b)(chunkBuf.subarray(0, chunkN), { dkLen: 18 })).toString("base64"));
sizes.push(chunkN);
chunkN = 0;
hi = 0;
lo = 0;
win.fill(0);
wpos = 0;
}
// Called once per byte; inlines the three-phase Rabin state machine:
// skip (< MIN-WINDOW bytes) → prime (next WINDOW bytes) → scan (≥ MIN bytes)
function processByte(b) {
chunkBuf[chunkN++] = b;
if (chunkN <= RABIN_MIN - RABIN_WINDOW) {
// Skip phase: not close enough to MIN to start priming yet
return;
}
if (chunkN <= RABIN_MIN) {
// Prime phase: non-windowed hash update, filling the ring buffer
win[wpos] = b;
wpos = (wpos + 1) % RABIN_WINDOW;
const top = (hi >>> 23) & 0xff;
const nhi = ((hi << 8) | (lo >>> 24)) & 0x7fffffff;
const nlo = ((lo << 8) | b) & 0xffffffff;
hi = (nhi ^ PUSH_HI[top]) | 0;
lo = (nlo ^ PUSH_LO[top]) | 0;
// At exactly MIN: check whether the primed hash is already a boundary
if (chunkN === RABIN_MIN && (lo & RABIN_HASH_MASK) === RABIN_HASH_MASK) {
emitChunk();
}
return;
}
// Scan phase: sliding window — pop oldest byte, push new byte
const old = win[wpos];
win[wpos] = b;
wpos = (wpos + 1) % RABIN_WINDOW;
hi = (hi ^ POP_HI[old]) | 0;
lo = (lo ^ POP_LO[old]) | 0;
const top = (hi >>> 23) & 0xff;
const nhi = ((hi << 8) | (lo >>> 24)) & 0x7fffffff;
const nlo = ((lo << 8) | b) & 0xffffffff;
hi = (nhi ^ PUSH_HI[top]) | 0;
lo = (nlo ^ PUSH_LO[top]) | 0;
if ((lo & RABIN_HASH_MASK) === RABIN_HASH_MASK || chunkN >= RABIN_MAX) {
emitChunk();
}
}
// Stream-read the file, feeding SHA-512 and the Rabin chunker simultaneously
await new Promise((resolve, reject) => {
const rs = (0, fs_1.createReadStream)(inFile, { highWaterMark: 256 * 1024 });
rs.on("data", (chunk) => {
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
chunk = buf;
fileHash.update(chunk);
totalSize += chunk.length;
for (let i = 0; i < chunk.length; i++) {
processByte(chunk[i]);
}
});
rs.on("end", () => {
if (chunkN > 0) {
emitChunk();
}
resolve();
});
rs.on("error", reject);
});
const blockMap = {
version: "2",
files: [{ name: "file", offset: 0, checksums, sizes }],
};
const compressed = compress(Buffer.from(JSON.stringify(blockMap)), compressionFormat);
if (outFile == null || outFile === "") {
// Append mode: write compressed blockmap + 4-byte size in one atomic call
// to avoid a partial-write window between the two separate appendFile calls.
const sizeHeader = Buffer.allocUnsafe(4);
sizeHeader.writeUInt32BE(compressed.length, 0);
const appended = Buffer.concat([compressed, sizeHeader]);
await (0, promises_1.appendFile)(inFile, appended);
fileHash.update(appended);
return {
size: totalSize + appended.length,
sha512: fileHash.digest("base64"),
blockMapSize: compressed.length,
};
}
await (0, promises_1.writeFile)(outFile, compressed);
return {
size: totalSize,
sha512: fileHash.digest("base64"),
};
}
//# sourceMappingURL=blockmap.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBlockmap = exports.appendBlockmap = exports.configureDifferentialAwareArchiveOptions = exports.createNsisWebDifferentialUpdateInfo = exports.BLOCK_MAP_FILE_SUFFIX = void 0;
exports.BLOCK_MAP_FILE_SUFFIX = void 0;
exports.createNsisWebDifferentialUpdateInfo = createNsisWebDifferentialUpdateInfo;
exports.configureDifferentialAwareArchiveOptions = configureDifferentialAwareArchiveOptions;
exports.appendBlockmap = appendBlockmap;
exports.createBlockmap = createBlockmap;
const builder_util_1 = require("builder-util");
const path = require("path");
const appBuilder_1 = require("../util/appBuilder");
const blockmap_1 = require("./blockmap/blockmap");
exports.BLOCK_MAP_FILE_SUFFIX = ".blockmap";
function createNsisWebDifferentialUpdateInfo(artifactPath, packageFiles) {
if (packageFiles == null) {
@ -26,7 +30,6 @@ function createNsisWebDifferentialUpdateInfo(artifactPath, packageFiles) {
}
return { packages };
}
exports.createNsisWebDifferentialUpdateInfo = createNsisWebDifferentialUpdateInfo;
function configureDifferentialAwareArchiveOptions(archiveOptions) {
/*
* dict size 64 MB: Full: 33,744.88 KB, To download: 17,630.3 KB (52%)
@ -56,17 +59,15 @@ function configureDifferentialAwareArchiveOptions(archiveOptions) {
archiveOptions.compression = "normal";
return archiveOptions;
}
exports.configureDifferentialAwareArchiveOptions = configureDifferentialAwareArchiveOptions;
async function appendBlockmap(file) {
builder_util_1.log.info({ file: builder_util_1.log.filePath(file) }, "building embedded block map");
return await appBuilder_1.executeAppBuilderAsJson(["blockmap", "--input", file, "--compression", "deflate"]);
return (0, blockmap_1.buildBlockMap)(file, "deflate");
}
exports.appendBlockmap = appendBlockmap;
async function createBlockmap(file, target, packager, safeArtifactName) {
const blockMapFile = `${file}${exports.BLOCK_MAP_FILE_SUFFIX}`;
builder_util_1.log.info({ blockMapFile: builder_util_1.log.filePath(blockMapFile) }, "building block map");
const updateInfo = await appBuilder_1.executeAppBuilderAsJson(["blockmap", "--input", file, "--output", blockMapFile]);
await packager.info.callArtifactBuildCompleted({
const updateInfo = await (0, blockmap_1.buildBlockMap)(file, "gzip", blockMapFile);
await packager.info.emitArtifactBuildCompleted({
file: blockMapFile,
safeArtifactName: safeArtifactName == null ? null : `${safeArtifactName}${exports.BLOCK_MAP_FILE_SUFFIX}`,
target,
@ -76,5 +77,4 @@ async function createBlockmap(file, target, packager, safeArtifactName) {
});
return updateInfo;
}
exports.createBlockmap = createBlockmap;
//# sourceMappingURL=differentialUpdateInfoBuilder.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,217 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const _7zip_bin_1 = require("7zip-bin");
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const fs_extra_1 = require("fs-extra");
const promises_1 = require("fs/promises");
const path = require("path");
const appInfo_1 = require("../appInfo");
const core_1 = require("../core");
const errorMessages = require("../errorMessages");
const appBuilder_1 = require("../util/appBuilder");
const bundledTool_1 = require("../util/bundledTool");
const macosVersion_1 = require("../util/macosVersion");
const pathManager_1 = require("../util/pathManager");
const LinuxTargetHelper_1 = require("./LinuxTargetHelper");
const tools_1 = require("./tools");
class FpmTarget extends core_1.Target {
constructor(name, packager, helper, outDir) {
super(name, false);
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
this.options = { ...this.packager.platformSpecificBuildOptions, ...this.packager.config[this.name] };
this.scriptFiles = this.createScripts();
}
async createScripts() {
const defaultTemplatesDir = pathManager_1.getTemplatePath("linux");
const packager = this.packager;
const templateOptions = {
// old API compatibility
executable: packager.executableName,
sanitizedProductName: packager.appInfo.sanitizedProductName,
productFilename: packager.appInfo.productFilename,
...packager.platformSpecificBuildOptions,
};
function getResource(value, defaultFile) {
if (value == null) {
return path.join(defaultTemplatesDir, defaultFile);
}
return path.resolve(packager.projectDir, value);
}
return await Promise.all([
writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterInstall, "after-install.tpl"), templateOptions),
writeConfigFile(packager.info.tempDirManager, getResource(this.options.afterRemove, "after-remove.tpl"), templateOptions),
]);
}
checkOptions() {
return this.computeFpmMetaInfoOptions();
}
async computeFpmMetaInfoOptions() {
var _a;
const packager = this.packager;
const projectUrl = await packager.appInfo.computePackageUrl();
const errors = [];
if (projectUrl == null) {
errors.push("Please specify project homepage, see https://electron.build/configuration/configuration#Metadata-homepage");
}
const options = this.options;
let author = options.maintainer;
if (author == null) {
const a = packager.info.metadata.author;
if (a == null || a.email == null) {
errors.push(errorMessages.authorEmailIsMissed);
}
else {
author = `${a.name} <${a.email}>`;
}
}
if (errors.length > 0) {
throw new Error(errors.join("\n\n"));
}
return {
name: (_a = options.packageName) !== null && _a !== void 0 ? _a : this.packager.appInfo.linuxPackageName,
maintainer: author,
url: projectUrl,
vendor: options.vendor || author,
};
}
async build(appOutDir, arch) {
var _a;
const target = this.name;
// tslint:disable:no-invalid-template-strings
let nameFormat = "${name}-${version}-${arch}.${ext}";
let isUseArchIfX64 = false;
if (target === "deb") {
nameFormat = "${name}_${version}_${arch}.${ext}";
isUseArchIfX64 = true;
}
else if (target === "rpm") {
nameFormat = "${name}-${version}.${arch}.${ext}";
isUseArchIfX64 = true;
}
const packager = this.packager;
const artifactPath = path.join(this.outDir, packager.expandArtifactNamePattern(this.options, target, arch, nameFormat, !isUseArchIfX64));
await packager.info.callArtifactBuildStarted({
targetPresentableName: target,
file: artifactPath,
arch,
});
await fs_1.unlinkIfExists(artifactPath);
if (packager.packagerOptions.prepackaged != null) {
await promises_1.mkdir(this.outDir, { recursive: true });
}
const scripts = await this.scriptFiles;
const appInfo = packager.appInfo;
const options = this.options;
const synopsis = options.synopsis;
const args = [
"--architecture",
builder_util_1.toLinuxArchString(arch, target),
"--after-install",
scripts[0],
"--after-remove",
scripts[1],
"--description",
appInfo_1.smarten(target === "rpm" ? this.helper.getDescription(options) : `${synopsis || ""}\n ${this.helper.getDescription(options)}`),
"--version",
appInfo.version,
"--package",
artifactPath,
];
appBuilder_1.objectToArgs(args, (await this.computeFpmMetaInfoOptions()));
const packageCategory = options.packageCategory;
if (packageCategory != null) {
args.push("--category", packageCategory);
}
if (target === "deb") {
args.push("--deb-priority", (_a = options.priority) !== null && _a !== void 0 ? _a : "optional");
}
else if (target === "rpm") {
if (synopsis != null) {
args.push("--rpm-summary", appInfo_1.smarten(synopsis));
}
}
const fpmConfiguration = {
args,
target,
};
if (options.compression != null) {
fpmConfiguration.compression = options.compression;
}
// noinspection JSDeprecatedSymbols
const depends = options.depends;
if (depends != null) {
if (Array.isArray(depends)) {
fpmConfiguration.customDepends = depends;
}
else {
// noinspection SuspiciousTypeOfGuard
if (typeof depends === "string") {
fpmConfiguration.customDepends = [depends];
}
else {
throw new Error(`depends must be Array or String, but specified as: ${depends}`);
}
}
}
builder_util_1.use(packager.info.metadata.license, it => args.push("--license", it));
builder_util_1.use(appInfo.buildNumber, it => args.push("--iteration",
// dashes are not supported for iteration in older versions of fpm
// https://github.com/jordansissel/fpm/issues/1833
it.split("-").join("_")));
builder_util_1.use(options.fpm, it => args.push(...it));
args.push(`${appOutDir}/=${LinuxTargetHelper_1.installPrefix}/${appInfo.sanitizedProductName}`);
for (const icon of await this.helper.icons) {
const extWithDot = path.extname(icon.file);
const sizeName = extWithDot === ".svg" ? "scalable" : `${icon.size}x${icon.size}`;
args.push(`${icon.file}=/usr/share/icons/hicolor/${sizeName}/apps/${packager.executableName}${extWithDot}`);
}
const mimeTypeFilePath = await this.helper.mimeTypeFiles;
if (mimeTypeFilePath != null) {
args.push(`${mimeTypeFilePath}=/usr/share/mime/packages/${packager.executableName}.xml`);
}
const desktopFilePath = await this.helper.writeDesktopEntry(this.options);
args.push(`${desktopFilePath}=/usr/share/applications/${packager.executableName}.desktop`);
if (packager.packagerOptions.effectiveOptionComputed != null && (await packager.packagerOptions.effectiveOptionComputed([args, desktopFilePath]))) {
return;
}
const env = {
...process.env,
SZA_PATH: _7zip_bin_1.path7za,
SZA_COMPRESSION_LEVEL: packager.compression === "store" ? "0" : "9",
};
// rpmbuild wants directory rpm with some default config files. Even if we can use dylibbundler, path to such config files are not changed (we need to replace in the binary)
// so, for now, brew install rpm is still required.
if (target !== "rpm" && (await macosVersion_1.isMacOsSierra())) {
const linuxToolsPath = await tools_1.getLinuxToolsPath();
Object.assign(env, {
PATH: bundledTool_1.computeEnv(process.env.PATH, [path.join(linuxToolsPath, "bin")]),
DYLD_LIBRARY_PATH: bundledTool_1.computeEnv(process.env.DYLD_LIBRARY_PATH, [path.join(linuxToolsPath, "lib")]),
});
}
await builder_util_1.executeAppBuilder(["fpm", "--configuration", JSON.stringify(fpmConfiguration)], undefined, { env });
await packager.dispatchArtifactCreated(artifactPath, this, arch);
}
}
exports.default = FpmTarget;
async function writeConfigFile(tmpDir, templatePath, options) {
//noinspection JSUnusedLocalSymbols
function replacer(match, p1) {
if (p1 in options) {
return options[p1];
}
else {
throw new Error(`Macro ${p1} is not defined`);
}
}
const config = (await promises_1.readFile(templatePath, "utf8")).replace(/\${([a-zA-Z]+)}/g, replacer).replace(/<%=([a-zA-Z]+)%>/g, (match, p1) => {
builder_util_1.log.warn("<%= varName %> is deprecated, please use ${varName} instead");
return replacer(match, p1.trim());
});
const outputPath = await tmpDir.getTempFile({ suffix: path.basename(templatePath, ".tpl") });
await fs_extra_1.outputFile(outputPath, config);
return outputPath;
}
//# sourceMappingURL=fpm.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
export declare type Commands = {
export type Commands = {
OutFile: string;
VIProductVersion?: string;
VIAddVersionKey: Array<string>;

View file

@ -1,12 +1,10 @@
/// <reference types="node" />
import { PortableOptions } from "./nsisOptions";
import { PathLike } from "fs";
/**
* Parameters declared as environment variables in NSIS scripts.
* The documentation vaguely explains "All other electron-builder specific flags (e.g. ONE_CLICK) are still defined."
* Parameters with null values in TypeScript can be treated as Boolean values using "!Ifdef" in NSIS Script.
*/
export declare type Defines = {
export type Defines = {
APP_ID: string;
APP_GUID: unknown;
UNINSTALL_APP_KEY: unknown;
@ -35,13 +33,16 @@ export declare type Defines = {
APP_64_HASH?: string;
APP_ARM64_HASH?: string;
APP_32_HASH?: string;
APP_64_UNPACKED_SIZE?: string;
APP_ARM64_UNPACKED_SIZE?: string;
APP_32_UNPACKED_SIZE?: string;
REQUEST_EXECUTION_LEVEL?: PortableOptions["requestExecutionLevel"];
UNPACK_DIR_NAME?: string | false;
SPLASH_IMAGE?: unknown;
ESTIMATED_SIZE?: number;
COMPRESS?: "auto";
BUILD_UNINSTALLER?: null;
UNINSTALLER_OUT_FILE?: PathLike;
UNINSTALLER_OUT_FILE?: string;
ONE_CLICK?: null;
RUN_AFTER_FINISH?: null;
HEADER_ICO?: string;
@ -53,6 +54,7 @@ export declare type Defines = {
MUI_UNWELCOMEFINISHPAGE_BITMAP?: string;
MULTIUSER_INSTALLMODE_ALLOW_ELEVATION?: null;
INSTALL_MODE_PER_ALL_USERS?: null;
INSTALL_MODE_PER_ALL_USERS_DEFAULT?: null;
INSTALL_MODE_PER_ALL_USERS_REQUIRED?: null;
allowToChangeInstallationDirectory?: null;
removeDefaultUninstallWelcomePage?: null;
@ -61,6 +63,10 @@ export declare type Defines = {
DELETE_APP_DATA_ON_UNINSTALL?: null;
UNINSTALLER_ICON?: string;
UNINSTALL_DISPLAY_NAME?: string;
UNINSTALL_URL_HELP?: string;
UNINSTALL_URL_INFO_ABOUT?: string;
UNINSTALL_URL_UPDATE_INFO?: string;
UNINSTALL_URL_README?: string;
RECREATE_DESKTOP_SHORTCUT?: null;
DO_NOT_CREATE_DESKTOP_SHORTCUT?: null;
DO_NOT_CREATE_START_MENU_SHORTCUT?: null;

View file

@ -1 +1 @@
{"version":3,"file":"Defines.js","sourceRoot":"","sources":["../../../src/targets/nsis/Defines.ts"],"names":[],"mappings":"","sourcesContent":["import { PortableOptions } from \"./nsisOptions\"\nimport { PathLike } from \"fs\"\n/**\n * Parameters declared as environment variables in NSIS scripts.\n * The documentation vaguely explains \"All other electron-builder specific flags (e.g. ONE_CLICK) are still defined.\"\n * Parameters with null values in TypeScript can be treated as Boolean values using \"!Ifdef\" in NSIS Script.\n */\nexport type Defines = {\n APP_ID: string\n APP_GUID: unknown\n UNINSTALL_APP_KEY: unknown\n PRODUCT_NAME: string\n PRODUCT_FILENAME: string\n APP_FILENAME: string\n APP_DESCRIPTION: string\n VERSION: string\n\n PROJECT_DIR: string\n BUILD_RESOURCES_DIR: string\n\n APP_PACKAGE_NAME: string\n\n ENABLE_LOGGING_ELECTRON_BUILDER?: null\n UNINSTALL_REGISTRY_KEY_2?: string\n\n MUI_ICON?: unknown\n MUI_UNICON?: unknown\n\n APP_DIR_64?: string\n APP_DIR_ARM64?: string\n APP_DIR_32?: string\n\n APP_BUILD_DIR?: string\n\n APP_64?: string\n APP_ARM64?: string\n APP_32?: string\n\n APP_64_NAME?: string\n APP_ARM64_NAME?: string\n APP_32_NAME?: string\n\n APP_64_HASH?: string\n APP_ARM64_HASH?: string\n APP_32_HASH?: string\n\n REQUEST_EXECUTION_LEVEL?: PortableOptions[\"requestExecutionLevel\"]\n\n UNPACK_DIR_NAME?: string | false\n\n SPLASH_IMAGE?: unknown\n\n ESTIMATED_SIZE?: number\n\n COMPRESS?: \"auto\"\n\n BUILD_UNINSTALLER?: null\n UNINSTALLER_OUT_FILE?: PathLike\n\n ONE_CLICK?: null\n RUN_AFTER_FINISH?: null\n HEADER_ICO?: string\n HIDE_RUN_AFTER_FINISH?: null\n\n MUI_HEADERIMAGE?: null\n MUI_HEADERIMAGE_RIGHT?: null\n MUI_HEADERIMAGE_BITMAP?: string\n\n MUI_WELCOMEFINISHPAGE_BITMAP?: string\n MUI_UNWELCOMEFINISHPAGE_BITMAP?: string\n\n MULTIUSER_INSTALLMODE_ALLOW_ELEVATION?: null\n\n INSTALL_MODE_PER_ALL_USERS?: null\n INSTALL_MODE_PER_ALL_USERS_REQUIRED?: null\n\n allowToChangeInstallationDirectory?: null\n\n removeDefaultUninstallWelcomePage?: null\n\n MENU_FILENAME?: string\n\n SHORTCUT_NAME?: string\n\n DELETE_APP_DATA_ON_UNINSTALL?: null\n\n UNINSTALLER_ICON?: string\n UNINSTALL_DISPLAY_NAME?: string\n\n RECREATE_DESKTOP_SHORTCUT?: null\n\n DO_NOT_CREATE_DESKTOP_SHORTCUT?: null\n\n DO_NOT_CREATE_START_MENU_SHORTCUT?: null\n\n DISPLAY_LANG_SELECTOR?: null\n\n COMPANY_NAME?: string\n\n APP_PRODUCT_FILENAME?: string\n\n APP_PACKAGE_STORE_FILE?: string\n\n APP_INSTALLER_STORE_FILE?: string\n\n ZIP_COMPRESSION?: null\n\n COMPRESSION_METHOD?: \"zip\" | \"7z\"\n}\n"]}
{"version":3,"file":"Defines.js","sourceRoot":"","sources":["../../../src/targets/nsis/Defines.ts"],"names":[],"mappings":"","sourcesContent":["import { PortableOptions } from \"./nsisOptions\"\n/**\n * Parameters declared as environment variables in NSIS scripts.\n * The documentation vaguely explains \"All other electron-builder specific flags (e.g. ONE_CLICK) are still defined.\"\n * Parameters with null values in TypeScript can be treated as Boolean values using \"!Ifdef\" in NSIS Script.\n */\nexport type Defines = {\n APP_ID: string\n APP_GUID: unknown\n UNINSTALL_APP_KEY: unknown\n PRODUCT_NAME: string\n PRODUCT_FILENAME: string\n APP_FILENAME: string\n APP_DESCRIPTION: string\n VERSION: string\n\n PROJECT_DIR: string\n BUILD_RESOURCES_DIR: string\n\n APP_PACKAGE_NAME: string\n\n ENABLE_LOGGING_ELECTRON_BUILDER?: null\n UNINSTALL_REGISTRY_KEY_2?: string\n\n MUI_ICON?: unknown\n MUI_UNICON?: unknown\n\n APP_DIR_64?: string\n APP_DIR_ARM64?: string\n APP_DIR_32?: string\n\n APP_BUILD_DIR?: string\n\n APP_64?: string\n APP_ARM64?: string\n APP_32?: string\n\n APP_64_NAME?: string\n APP_ARM64_NAME?: string\n APP_32_NAME?: string\n\n APP_64_HASH?: string\n APP_ARM64_HASH?: string\n APP_32_HASH?: string\n\n APP_64_UNPACKED_SIZE?: string\n APP_ARM64_UNPACKED_SIZE?: string\n APP_32_UNPACKED_SIZE?: string\n\n REQUEST_EXECUTION_LEVEL?: PortableOptions[\"requestExecutionLevel\"]\n\n UNPACK_DIR_NAME?: string | false\n\n SPLASH_IMAGE?: unknown\n\n ESTIMATED_SIZE?: number\n\n COMPRESS?: \"auto\"\n\n BUILD_UNINSTALLER?: null\n UNINSTALLER_OUT_FILE?: string\n\n ONE_CLICK?: null\n RUN_AFTER_FINISH?: null\n HEADER_ICO?: string\n HIDE_RUN_AFTER_FINISH?: null\n\n MUI_HEADERIMAGE?: null\n MUI_HEADERIMAGE_RIGHT?: null\n MUI_HEADERIMAGE_BITMAP?: string\n\n MUI_WELCOMEFINISHPAGE_BITMAP?: string\n MUI_UNWELCOMEFINISHPAGE_BITMAP?: string\n\n MULTIUSER_INSTALLMODE_ALLOW_ELEVATION?: null\n\n INSTALL_MODE_PER_ALL_USERS?: null\n INSTALL_MODE_PER_ALL_USERS_DEFAULT?: null\n INSTALL_MODE_PER_ALL_USERS_REQUIRED?: null\n\n allowToChangeInstallationDirectory?: null\n\n removeDefaultUninstallWelcomePage?: null\n\n MENU_FILENAME?: string\n\n SHORTCUT_NAME?: string\n\n DELETE_APP_DATA_ON_UNINSTALL?: null\n\n UNINSTALLER_ICON?: string\n UNINSTALL_DISPLAY_NAME?: string\n UNINSTALL_URL_HELP?: string\n UNINSTALL_URL_INFO_ABOUT?: string\n UNINSTALL_URL_UPDATE_INFO?: string\n UNINSTALL_URL_README?: string\n\n RECREATE_DESKTOP_SHORTCUT?: null\n\n DO_NOT_CREATE_DESKTOP_SHORTCUT?: null\n\n DO_NOT_CREATE_START_MENU_SHORTCUT?: null\n\n DISPLAY_LANG_SELECTOR?: null\n\n COMPANY_NAME?: string\n\n APP_PRODUCT_FILENAME?: string\n\n APP_PACKAGE_STORE_FILE?: string\n\n APP_INSTALLER_STORE_FILE?: string\n\n ZIP_COMPRESSION?: null\n\n COMPRESSION_METHOD?: \"zip\" | \"7z\"\n}\n"]}

View file

@ -12,17 +12,19 @@ export declare class NsisTarget extends Target {
readonly options: NsisOptions;
/** @private */
readonly archs: Map<Arch, string>;
readonly isAsyncSupported = false;
constructor(packager: WinPackager, outDir: string, targetName: string, packageHelper: AppPackageHelper);
build(appOutDir: string, arch: Arch): Promise<void>;
get shouldBuildUniversalInstaller(): boolean;
build(appOutDir: string, arch: Arch): Promise<any>;
get isBuildDifferentialAware(): boolean;
private getPreCompressedFileExtensions;
/** @private */
buildAppPackage(appOutDir: string, arch: Arch): Promise<PackageFileInfo>;
protected get installerFilenamePattern(): string;
protected installerFilenamePattern(primaryArch?: Arch | null, defaultArch?: string): string;
private get isPortable();
finishBuild(): Promise<any>;
private buildInstaller;
protected generateGitHubInstallerName(): string;
protected generateGitHubInstallerName(primaryArch: Arch | null, defaultArch: string | undefined): string;
private get isUnicodeEnabled();
get isWebInstaller(): boolean;
private computeScriptAndSignUninstaller;

View file

@ -1,35 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NsisTarget = void 0;
const _7zip_bin_1 = require("7zip-bin");
const bluebird_lst_1 = require("bluebird-lst");
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_1 = require("builder-util/out/fs");
const debug_1 = require("debug");
const fs = require("fs");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const binDownload_1 = require("../../binDownload");
const core_1 = require("../../core");
const CommonWindowsInstallerConfiguration_1 = require("../../options/CommonWindowsInstallerConfiguration");
const platformPackager_1 = require("../../platformPackager");
const hash_1 = require("../../util/hash");
const macosVersion_1 = require("../../util/macosVersion");
const timer_1 = require("../../util/timer");
const wine_1 = require("../../wine");
const archive_1 = require("../archive");
const differentialUpdateInfoBuilder_1 = require("../differentialUpdateInfoBuilder");
const targetUtil_1 = require("../targetUtil");
const nsisLang_1 = require("./nsisLang");
const nsisLicense_1 = require("./nsisLicense");
const nsisScriptGenerator_1 = require("./nsisScriptGenerator");
const windows_1 = require("../../toolsets/windows");
const nsisUtil_1 = require("./nsisUtil");
const debug = debug_1.default("electron-builder:nsis");
const nsisValidation_1 = require("./nsisValidation");
const WineVm_1 = require("../../vm/WineVm");
const debug = (0, debug_1.default)("electron-builder:nsis");
// noinspection SpellCheckingInspection
const ELECTRON_BUILDER_NS_UUID = builder_util_runtime_1.UUID.parse("50e065bc-3134-11e6-9bab-38c9862bdaf3");
// noinspection SpellCheckingInspection
const nsisResourcePathPromise = () => binDownload_1.getBinFromUrl("nsis-resources", "3.4.1", "Dqd6g+2buwwvoG1Vyf6BHR1b+25QMmPcwZx40atOT57gH27rkjOei1L0JTldxZu4NFoEmW4kJgZ3DlSWVON3+Q==");
const USE_NSIS_BUILT_IN_COMPRESSOR = false;
class NsisTarget extends core_1.Target {
constructor(packager, outDir, targetName, packageHelper) {
@ -39,6 +35,7 @@ class NsisTarget extends core_1.Target {
this.packageHelper = packageHelper;
/** @private */
this.archs = new Map();
this.isAsyncSupported = false;
this.packageHelper.refCount++;
this.options =
targetName === "portable"
@ -48,16 +45,22 @@ class NsisTarget extends core_1.Target {
...this.packager.config.nsis,
};
if (targetName !== "nsis") {
Object.assign(this.options, this.packager.config[targetName === "nsis-web" ? "nsisWeb" : targetName]);
(0, builder_util_runtime_1.deepAssign)(this.options, this.packager.config[targetName === "nsis-web" ? "nsisWeb" : targetName]);
}
const deps = packager.info.metadata.dependencies;
if (deps != null && deps["electron-squirrel-startup"] != null) {
builder_util_1.log.warn('"electron-squirrel-startup" dependency is not required for NSIS');
}
nsisUtil_1.NsisTargetOptions.resolve(this.options);
}
get shouldBuildUniversalInstaller() {
const buildSeparateInstallers = this.options.buildUniversalInstaller === false;
return !buildSeparateInstallers;
}
build(appOutDir, arch) {
this.archs.set(arch, appOutDir);
if (!this.shouldBuildUniversalInstaller) {
return this.buildInstaller(new Map().set(arch, appOutDir));
}
return Promise.resolve();
}
get isBuildDifferentialAware() {
@ -65,7 +68,7 @@ class NsisTarget extends core_1.Target {
}
getPreCompressedFileExtensions() {
const result = this.isWebInstaller ? null : this.options.preCompressedFileExtensions;
return result == null ? null : builder_util_1.asArray(result).map(it => (it.startsWith(".") ? it : `.${it}`));
return result == null ? null : (0, builder_util_1.asArray)(result).map(it => (it.startsWith(".") ? it : `.${it}`));
}
/** @private */
async buildAppPackage(appOutDir, arch) {
@ -80,11 +83,11 @@ class NsisTarget extends core_1.Target {
compression: packager.compression,
excluded: preCompressedFileExtensions == null ? null : preCompressedFileExtensions.map(it => `*${it}`),
};
const timer = timer_1.time(`nsis package, ${builder_util_1.Arch[arch]}`);
await archive_1.archive(format, archiveFile, appOutDir, isBuildDifferentialAware ? differentialUpdateInfoBuilder_1.configureDifferentialAwareArchiveOptions(archiveOptions) : archiveOptions);
const timer = (0, timer_1.time)(`nsis package, ${builder_util_1.Arch[arch]}`);
await (0, archive_1.archive)(format, archiveFile, appOutDir, isBuildDifferentialAware ? (0, differentialUpdateInfoBuilder_1.configureDifferentialAwareArchiveOptions)(archiveOptions) : archiveOptions);
timer.end();
if (isBuildDifferentialAware && this.isWebInstaller) {
const data = await differentialUpdateInfoBuilder_1.appendBlockmap(archiveFile);
const data = await (0, differentialUpdateInfoBuilder_1.appendBlockmap)(archiveFile);
return {
...data,
path: archiveFile,
@ -94,16 +97,21 @@ class NsisTarget extends core_1.Target {
return await createPackageFileInfo(archiveFile);
}
}
get installerFilenamePattern() {
// tslint:disable:no-invalid-template-strings
return "${productName} " + (this.isPortable ? "" : "Setup ") + "${version}.${ext}";
installerFilenamePattern(primaryArch, defaultArch) {
const setupText = this.isPortable ? "" : "Setup ";
const archSuffix = !this.shouldBuildUniversalInstaller && primaryArch != null ? (0, builder_util_1.getArchSuffix)(primaryArch, defaultArch) : "";
return "${productName} " + setupText + "${version}" + archSuffix + ".${ext}";
}
get isPortable() {
return this.name === "portable";
}
async finishBuild() {
if (!this.shouldBuildUniversalInstaller) {
await super.finishBuild();
return this.packageHelper.finishBuild();
}
try {
const { pattern } = this.packager.artifactPatternConfig(this.options, this.installerFilenamePattern);
const { pattern } = this.packager.artifactPatternConfig(this.options, this.installerFilenamePattern());
const builds = new Set([this.archs]);
if (pattern.includes("${arch}") && this.archs.size > 1) {
;
@ -115,16 +123,18 @@ class NsisTarget extends core_1.Target {
}
}
finally {
await super.finishBuild();
await this.packageHelper.finishBuild();
}
}
async buildInstaller(archs) {
var _a;
const primaryArch = archs.size === 1 ? archs.keys().next().value : null;
var _a, _b, _c;
const primaryArch = archs.size === 1 ? ((_a = archs.keys().next().value) !== null && _a !== void 0 ? _a : null) : null;
const packager = this.packager;
const appInfo = packager.appInfo;
const options = this.options;
const installerFilename = packager.expandArtifactNamePattern(options, "exe", primaryArch, this.installerFilenamePattern, false, this.packager.platformSpecificBuildOptions.defaultArch);
const defaultArch = (_b = (0, platformPackager_1.chooseNotNull)(this.packager.platformSpecificBuildOptions.defaultArch, this.packager.config.defaultArch)) !== null && _b !== void 0 ? _b : undefined;
const installerFilename = packager.expandArtifactNamePattern(options, "exe", primaryArch, this.installerFilenamePattern(primaryArch, defaultArch), false, defaultArch);
const oneClick = options.oneClick !== false;
const installerPath = path.join(this.outDir, installerFilename);
const logFields = {
@ -139,7 +149,7 @@ class NsisTarget extends core_1.Target {
logFields.oneClick = oneClick;
logFields.perMachine = isPerMachine;
}
await packager.info.callArtifactBuildStarted({
await packager.info.emitArtifactBuildStarted({
targetPresentableName: this.name,
file: installerPath,
arch: primaryArch,
@ -153,19 +163,24 @@ class NsisTarget extends core_1.Target {
UNINSTALL_APP_KEY: uninstallAppKey,
PRODUCT_NAME: appInfo.productName,
PRODUCT_FILENAME: appInfo.productFilename,
APP_FILENAME: targetUtil_1.getWindowsInstallationDirName(appInfo, !oneClick || isPerMachine),
APP_FILENAME: (0, targetUtil_1.getWindowsInstallationDirName)(appInfo, !oneClick || isPerMachine),
APP_DESCRIPTION: appInfo.description,
VERSION: appInfo.version,
PROJECT_DIR: packager.projectDir,
BUILD_RESOURCES_DIR: packager.info.buildResourcesDir,
APP_PACKAGE_NAME: targetUtil_1.getWindowsInstallationAppPackageName(appInfo.name),
APP_PACKAGE_NAME: (0, targetUtil_1.getWindowsInstallationAppPackageName)(appInfo.name),
};
if ((_a = options.customNsisBinary) === null || _a === void 0 ? void 0 : _a.debugLogging) {
if ((_c = options.customNsisBinary) === null || _c === void 0 ? void 0 : _c.debugLogging) {
defines.ENABLE_LOGGING_ELECTRON_BUILDER = null;
}
if (uninstallAppKey !== guid) {
defines.UNINSTALL_REGISTRY_KEY_2 = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${guid}`;
}
const { homepage } = this.packager.info.metadata;
(0, builder_util_1.use)(options.uninstallUrlHelp || homepage, it => (defines.UNINSTALL_URL_HELP = it));
(0, builder_util_1.use)(options.uninstallUrlInfoAbout || homepage, it => (defines.UNINSTALL_URL_INFO_ABOUT = it));
(0, builder_util_1.use)(options.uninstallUrlUpdateInfo || homepage, it => (defines.UNINSTALL_URL_UPDATE_INFO = it));
(0, builder_util_1.use)(options.uninstallUrlReadme || homepage, it => (defines.UNINSTALL_URL_README = it));
const commands = {
OutFile: `"${installerPath}"`,
VIProductVersion: appInfo.getVersionInWeirdWindowsForm(),
@ -191,11 +206,12 @@ class NsisTarget extends core_1.Target {
}
}
else if (USE_NSIS_BUILT_IN_COMPRESSOR && archs.size === 1) {
defines.APP_BUILD_DIR = archs.get(archs.keys().next().value);
const value = archs.keys().next().value;
(0, builder_util_1.use)(value, v => (defines.APP_BUILD_DIR = archs.get(v)));
}
else {
await bluebird_lst_1.default.map(archs.keys(), async (arch) => {
const fileInfo = await this.packageHelper.packArch(arch, this);
await Promise.all(Array.from(archs.keys()).map(async (arch) => {
const { fileInfo, unpackedSize } = await this.packageHelper.packArch(arch, this);
const file = fileInfo.path;
const defineKey = arch === builder_util_1.Arch.x64 ? "APP_64" : arch === builder_util_1.Arch.arm64 ? "APP_ARM64" : "APP_32";
defines[defineKey] = file;
@ -206,20 +222,21 @@ class NsisTarget extends core_1.Target {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const defineHashKey = `${defineKey}_HASH`;
defines[defineHashKey] = Buffer.from(fileInfo.sha512, "base64").toString("hex").toUpperCase();
// NSIS accepts size in KiloBytes and supports only whole numbers
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const defineUnpackedSizeKey = `${defineKey}_UNPACKED_SIZE`;
defines[defineUnpackedSizeKey] = Math.ceil(unpackedSize / 1024).toString();
if (this.isWebInstaller) {
await packager.dispatchArtifactCreated(file, this, arch);
await packager.info.emitArtifactBuildCompleted({
file,
target: this,
arch,
packager,
});
packageFiles[builder_util_1.Arch[arch]] = fileInfo;
}
const archiveInfo = (await builder_util_1.exec(_7zip_bin_1.path7za, ["l", file])).trim();
// after adding blockmap data will be "Warnings: 1" in the end of output
const match = /(\d+)\s+\d+\s+\d+\s+files/.exec(archiveInfo);
if (match == null) {
builder_util_1.log.warn({ output: archiveInfo }, "cannot compute size of app package");
}
else {
estimatedSize += parseInt(match[1], 10);
}
});
estimatedSize += unpackedSize;
}));
}
this.configureDefinesForAllTypeOfInstaller(defines);
if (isPortable) {
@ -227,7 +244,7 @@ class NsisTarget extends core_1.Target {
defines.REQUEST_EXECUTION_LEVEL = requestExecutionLevel || "user";
// https://github.com/electron-userland/electron-builder/issues/5764
if (typeof unpackDirName === "string" || !unpackDirName) {
defines.UNPACK_DIR_NAME = unpackDirName || (await builder_util_1.executeAppBuilder(["ksuid"]));
defines.UNPACK_DIR_NAME = unpackDirName || (0, builder_util_1.generateKsuid)();
}
if (splashImage != null) {
defines.SPLASH_IMAGE = path.resolve(packager.projectDir, splashImage);
@ -265,39 +282,51 @@ class NsisTarget extends core_1.Target {
commandsUninstaller.VIProductVersion = appInfo.shortVersionWindows;
commandsUninstaller.VIAddVersionKey = this.computeVersionKey(true);
}
const sharedHeader = await this.computeCommonInstallerScriptHeader();
const script = isPortable
? await fs_extra_1.readFile(path.join(nsisUtil_1.nsisTemplatesDir, "portable.nsi"), "utf8")
: await this.computeScriptAndSignUninstaller(definesUninstaller, commandsUninstaller, installerPath, sharedHeader, archs);
// copy outfile name into main options, as the computeScriptAndSignUninstaller function was kind enough to add important data to temporary defines.
defines.UNINSTALLER_OUT_FILE = definesUninstaller.UNINSTALLER_OUT_FILE;
await this.executeMakensis(defines, commands, sharedHeader + (await this.computeFinalScript(script, true, archs)));
await Promise.all([packager.sign(installerPath), defines.UNINSTALLER_OUT_FILE == null ? Promise.resolve() : fs_extra_1.unlink(defines.UNINSTALLER_OUT_FILE)]);
const safeArtifactName = platformPackager_1.computeSafeArtifactNameIfNeeded(installerFilename, () => this.generateGitHubInstallerName());
let updateInfo;
if (this.isWebInstaller) {
updateInfo = differentialUpdateInfoBuilder_1.createNsisWebDifferentialUpdateInfo(installerPath, packageFiles);
}
else if (this.isBuildDifferentialAware) {
updateInfo = await differentialUpdateInfoBuilder_1.createBlockmap(installerPath, this, packager, safeArtifactName);
}
if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) {
updateInfo.isAdminRightsRequired = true;
}
await packager.info.callArtifactBuildCompleted({
file: installerPath,
updateInfo,
target: this,
packager,
arch: primaryArch,
safeArtifactName,
isWriteUpdateInfo: !this.isPortable,
this.buildQueueManager.add(async () => {
const sharedHeader = await this.computeCommonInstallerScriptHeader();
let rawScript;
let isCustomScript = false;
if (isPortable) {
rawScript = await (0, fs_extra_1.readFile)(path.join(nsisUtil_1.nsisTemplatesDir, "portable.nsi"), "utf8");
}
else {
const result = await this.computeScriptAndSignUninstaller(definesUninstaller, commandsUninstaller, installerPath, sharedHeader, archs);
rawScript = result.script;
isCustomScript = result.isCustomScript;
}
// copy outfile name into main options, as the computeScriptAndSignUninstaller function was kind enough to add important data to temporary defines.
defines.UNINSTALLER_OUT_FILE = definesUninstaller.UNINSTALLER_OUT_FILE;
// Skip size verification for portable (NSIS recompresses the archive, installer < archive is normal)
// and for custom scripts (may not embed archives).
await this.executeMakensis(defines, commands, sharedHeader + (await this.computeFinalScript(rawScript, true, archs)), { skipSizeVerification: isPortable || isCustomScript });
await Promise.all([packager.signIf(installerPath), defines.UNINSTALLER_OUT_FILE == null ? Promise.resolve() : (0, fs_extra_1.unlink)(defines.UNINSTALLER_OUT_FILE)]);
const safeArtifactName = (0, platformPackager_1.computeSafeArtifactNameIfNeeded)(installerFilename, () => this.generateGitHubInstallerName(primaryArch, defaultArch));
let updateInfo;
if (this.isWebInstaller) {
updateInfo = (0, differentialUpdateInfoBuilder_1.createNsisWebDifferentialUpdateInfo)(installerPath, packageFiles);
}
else if (this.isBuildDifferentialAware) {
updateInfo = await (0, differentialUpdateInfoBuilder_1.createBlockmap)(installerPath, this, packager, safeArtifactName);
}
if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) {
updateInfo.isAdminRightsRequired = true;
}
await packager.info.emitArtifactBuildCompleted({
file: installerPath,
updateInfo,
target: this,
packager,
arch: primaryArch,
safeArtifactName,
isWriteUpdateInfo: !this.isPortable,
});
});
}
generateGitHubInstallerName() {
generateGitHubInstallerName(primaryArch, defaultArch) {
const appInfo = this.packager.appInfo;
const classifier = appInfo.name.toLowerCase() === appInfo.name ? "setup-" : "Setup-";
return `${appInfo.name}-${this.isPortable ? "" : classifier}${appInfo.version}.exe`;
const archSuffix = !this.shouldBuildUniversalInstaller && primaryArch != null ? (0, builder_util_1.getArchSuffix)(primaryArch, defaultArch) : "";
return `${appInfo.name}-${this.isPortable ? "" : classifier}${appInfo.version}${archSuffix}.exe`;
}
get isUnicodeEnabled() {
return this.options.unicode !== false;
@ -306,22 +335,25 @@ class NsisTarget extends core_1.Target {
return false;
}
async computeScriptAndSignUninstaller(defines, commands, installerPath, sharedHeader, archs) {
var _a;
const packager = this.packager;
const customScriptPath = await packager.getResource(this.options.script, "installer.nsi");
const script = await fs_extra_1.readFile(customScriptPath || path.join(nsisUtil_1.nsisTemplatesDir, "installer.nsi"), "utf8");
const script = await (0, fs_extra_1.readFile)(customScriptPath || path.join(nsisUtil_1.nsisTemplatesDir, "installer.nsi"), "utf8");
if (customScriptPath != null) {
builder_util_1.log.info({ reason: "custom NSIS script is used" }, "uninstaller is not signed by electron-builder");
return script;
return { script, isCustomScript: true };
}
// https://github.com/electron-userland/electron-builder/issues/2103
// it is more safe and reliable to write uninstaller to our out dir
const uninstallerPath = path.join(this.outDir, `__uninstaller-${this.name}-${this.packager.appInfo.sanitizedName}.exe`);
// to support parallel builds, the uninstaller path must be unique to each target and arch combination
const uninstallerPath = path.join(this.outDir, `${path.basename(installerPath, "exe")}__uninstaller.exe`);
const isWin = process.platform === "win32";
defines.BUILD_UNINSTALLER = null;
defines.UNINSTALLER_OUT_FILE = isWin ? uninstallerPath : path.win32.join("Z:", uninstallerPath);
await this.executeMakensis(defines, commands, sharedHeader + (await this.computeFinalScript(script, false, archs)));
// http://forums.winamp.com/showthread.php?p=3078545
if (macosVersion_1.isMacOsCatalina()) {
// TODO: remove workaround when wine is fully upgraded to 11
if ((0, macosVersion_1.isMacOsCatalina)()) {
try {
await nsisUtil_1.UninstallerReader.exec(installerPath, uninstallerPath);
}
@ -331,21 +363,21 @@ class NsisTarget extends core_1.Target {
await vm.exec(installerPath, []);
// Parallels VM can exit after command execution, but NSIS continue to be running
let i = 0;
while (!(await fs_1.exists(uninstallerPath)) && i++ < 100) {
while (!(await (0, builder_util_1.exists)(uninstallerPath)) && i++ < 100) {
// noinspection JSUnusedLocalSymbols
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await new Promise((resolve, _reject) => setTimeout(resolve, 300));
}
}
}
else {
await wine_1.execWine(installerPath, null, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });
const wineVm = new WineVm_1.WineVmManager((_a = packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.wine);
await wineVm.exec(installerPath, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });
}
await packager.sign(uninstallerPath, " Signing NSIS uninstaller");
await packager.signIf(uninstallerPath);
delete defines.BUILD_UNINSTALLER;
// platform-specific path, not wine
defines.UNINSTALLER_OUT_FILE = uninstallerPath;
return script;
return { script, isCustomScript: false };
}
computeVersionKey(short = false) {
// Error: invalid VIProductVersion format, should be X.X.X.X
@ -353,18 +385,18 @@ class NsisTarget extends core_1.Target {
const localeId = this.options.language || "1033";
const appInfo = this.packager.appInfo;
const versionKey = [
`/LANG=${localeId} ProductName "${appInfo.productName}"`,
`/LANG=${localeId} ProductVersion "${appInfo.version}"`,
`/LANG=${localeId} LegalCopyright "${appInfo.copyright}"`,
`/LANG=${localeId} FileDescription "${appInfo.description}"`,
`/LANG=${localeId} FileVersion "${appInfo.buildVersion}"`,
`/LANG=${localeId} ProductName "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.productName)}"`,
`/LANG=${localeId} ProductVersion "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.version)}"`,
`/LANG=${localeId} LegalCopyright "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.copyright)}"`,
`/LANG=${localeId} FileDescription "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.description)}"`,
`/LANG=${localeId} FileVersion "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.buildVersion)}"`,
];
if (short) {
versionKey[1] = `/LANG=${localeId} ProductVersion "${appInfo.shortVersion}"`;
versionKey[4] = `/LANG=${localeId} FileVersion "${appInfo.shortVersion}"`;
versionKey[1] = `/LANG=${localeId} ProductVersion "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.shortVersion)}"`;
versionKey[4] = `/LANG=${localeId} FileVersion "${(0, nsisScriptGenerator_1.nsisEscapeString)(appInfo.shortVersion)}"`;
}
builder_util_1.use(this.packager.platformSpecificBuildOptions.legalTrademarks, it => versionKey.push(`/LANG=${localeId} LegalTrademarks "${it}"`));
builder_util_1.use(appInfo.companyName, it => versionKey.push(`/LANG=${localeId} CompanyName "${it}"`));
(0, builder_util_1.use)(this.packager.platformSpecificBuildOptions.legalTrademarks, it => versionKey.push(`/LANG=${localeId} LegalTrademarks "${(0, nsisScriptGenerator_1.nsisEscapeString)(it)}"`));
(0, builder_util_1.use)(appInfo.companyName, it => versionKey.push(`/LANG=${localeId} CompanyName "${(0, nsisScriptGenerator_1.nsisEscapeString)(it)}"`));
return versionKey;
}
configureDefines(oneClick, defines) {
@ -407,6 +439,9 @@ class NsisTarget extends core_1.Target {
if (options.perMachine === true) {
defines.INSTALL_MODE_PER_ALL_USERS = null;
}
if (options.selectPerMachineByDefault === true) {
defines.INSTALL_MODE_PER_ALL_USERS_DEFAULT = null;
}
if (!oneClick || options.perMachine === true) {
defines.INSTALL_MODE_PER_ALL_USERS_REQUIRED = null;
}
@ -419,7 +454,7 @@ class NsisTarget extends core_1.Target {
if (options.removeDefaultUninstallWelcomePage) {
defines.removeDefaultUninstallWelcomePage = null;
}
const commonOptions = CommonWindowsInstallerConfiguration_1.getEffectiveOptions(options, packager);
const commonOptions = (0, CommonWindowsInstallerConfiguration_1.getEffectiveOptions)(options, packager);
if (commonOptions.menuCategory != null) {
defines.MENU_FILENAME = commonOptions.menuCategory;
}
@ -474,7 +509,8 @@ class NsisTarget extends core_1.Target {
defines.COMPRESSION_METHOD = options.useZip ? "zip" : "7z";
}
}
async executeMakensis(defines, commands, script) {
async executeMakensis(defines, commands, script, opts = {}) {
var _a, _b;
const args = this.options.warningsAsErrors === false ? [] : ["-WX"];
args.push("-INPUTCHARSET", "UTF8");
for (const name of Object.keys(defines)) {
@ -483,7 +519,17 @@ class NsisTarget extends core_1.Target {
args.push(`-D${name}`);
}
else {
args.push(`-D${name}=${value}`);
// nsisEscapeString prevents three classes of injection:
// 1. Newlines → replaced with spaces; a bare \n in a define value
// would terminate the current script line and let whatever follows
// be parsed as a new preprocessor directive (e.g. !system, !include).
// 2. bare $ → escaped to $$; unescaped $ in a define value would cause
// NSIS to expand an unintended variable reference. ${...} references
// are left intact so NSIS compile-time defines like ${NSISDIR} still
// expand correctly.
// 3. " chars → escaped to $\"; an unescaped " would break out of
// double-quoted NSIS string literals where ${DEFINE} is expanded.
args.push(`-D${name}=${(0, nsisScriptGenerator_1.nsisEscapeString)(String(value))}`);
}
}
for (const name of Object.keys(commands)) {
@ -501,18 +547,26 @@ class NsisTarget extends core_1.Target {
if (this.packager.debugLogger.isEnabled) {
this.packager.debugLogger.add("nsis.script", script);
}
const nsisPath = await nsisUtil_1.NSIS_PATH();
const command = path.join(nsisPath, process.platform === "darwin" ? "mac" : process.platform === "win32" ? "Bin" : "linux", process.platform === "win32" ? "makensis.exe" : "makensis");
// if (process.platform === "win32") {
// fix for an issue caused by virus scanners, locking the file during write
// https://github.com/electron-userland/electron-builder/issues/5005
await ensureNotBusy(commands["OutFile"].replace(/"/g, ""));
// }
await builder_util_1.spawnAndWrite(command, args, script, {
// we use NSIS_CONFIG_CONST_DATA_PATH=no to build makensis on Linux, but in any case it doesn't use stubs as MacOS/Windows version, so, we explicitly set NSISDIR
env: { ...process.env, NSISDIR: nsisPath },
if (process.platform === "win32") {
// fix for an issue caused by virus scanners, locking the file during write
// https://github.com/electron-userland/electron-builder/issues/5005
await ensureNotBusy(commands["OutFile"].replace(/"/g, ""));
}
const makensis = await (0, windows_1.getMakeNsisPath)((_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.nsis, this.options.customNsisBinary);
const { stdout, stderr } = await (0, builder_util_1.spawnAndWriteWithOutput)(makensis.path, args, script, {
env: { ...process.env, ...((_b = makensis.env) !== null && _b !== void 0 ? _b : {}) },
cwd: nsisUtil_1.nsisTemplatesDir,
});
(0, nsisValidation_1.checkMakensisOutput)(stdout, stderr);
// Only verify output size for the final, non-web installer.
// BUILD_UNINSTALLER: intermediate uninstaller build — no embedded archives.
// APP_PACKAGE_STORE_FILE: nsis-web — archives downloaded at install-time.
// skipSizeVerification: portable builds (NSIS recompresses the archive, so
// installer < archive_size is normal) and custom scripts (may not embed archives).
if (!("BUILD_UNINSTALLER" in defines) && !("APP_PACKAGE_STORE_FILE" in defines) && !opts.skipSizeVerification) {
const outFile = commands["OutFile"].replace(/^"|"$/g, "");
await (0, nsisValidation_1.verifyInstallerSize)(outFile, defines);
}
}
async computeCommonInstallerScriptHeader() {
const packager = this.packager;
@ -523,23 +577,24 @@ class NsisTarget extends core_1.Target {
const includeDir = path.join(nsisUtil_1.nsisTemplatesDir, "include");
scriptGenerator.addIncludeDir(includeDir);
scriptGenerator.flags(["updated", "force-run", "keep-shortcuts", "no-desktop-shortcut", "delete-app-data", "allusers", "currentuser"]);
nsisLang_1.createAddLangsMacro(scriptGenerator, langConfigurator);
(0, nsisLang_1.createAddLangsMacro)(scriptGenerator, langConfigurator);
const taskManager = new builder_util_1.AsyncTaskManager(packager.info.cancellationToken);
const pluginArch = this.isUnicodeEnabled ? "x86-unicode" : "x86-ansi";
taskManager.add(async () => {
scriptGenerator.addPluginDir(pluginArch, path.join(await nsisResourcePathPromise(), "plugins", pluginArch));
var _a;
scriptGenerator.addPluginDir(pluginArch, path.join(await (0, windows_1.getNsisPluginsPath)((_a = this.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.nsis, this.options.customNsisResources), pluginArch));
});
taskManager.add(async () => {
const userPluginDir = path.join(packager.info.buildResourcesDir, pluginArch);
const stat = await fs_1.statOrNull(userPluginDir);
const stat = await (0, builder_util_1.statOrNull)(userPluginDir);
if (stat != null && stat.isDirectory()) {
scriptGenerator.addPluginDir(pluginArch, userPluginDir);
}
});
taskManager.addTask(nsisLang_1.addCustomMessageFileInclude("messages.yml", packager, scriptGenerator, langConfigurator));
taskManager.addTask((0, nsisLang_1.addCustomMessageFileInclude)("messages.yml", packager, scriptGenerator, langConfigurator));
if (!this.isPortable) {
if (options.oneClick === false) {
taskManager.addTask(nsisLang_1.addCustomMessageFileInclude("assistedMessages.yml", packager, scriptGenerator, langConfigurator));
taskManager.addTask((0, nsisLang_1.addCustomMessageFileInclude)("assistedMessages.yml", packager, scriptGenerator, langConfigurator));
}
taskManager.add(async () => {
const customInclude = await packager.getResource(this.options.include, "installer.nsh");
@ -560,7 +615,7 @@ class NsisTarget extends core_1.Target {
const taskManager = new builder_util_1.AsyncTaskManager(packager.info.cancellationToken);
if (isInstaller) {
// http://stackoverflow.com/questions/997456/nsis-license-file-based-on-language-selection
taskManager.add(() => nsisLicense_1.computeLicensePage(packager, options, scriptGenerator, langConfigurator.langs));
taskManager.add(() => (0, nsisLicense_1.computeLicensePage)(packager, options, scriptGenerator, langConfigurator.langs));
}
await taskManager.awaitTasks();
if (this.isPortable) {
@ -578,18 +633,18 @@ class NsisTarget extends core_1.Target {
if (isInstaller) {
const registerFileAssociationsScript = new nsisScriptGenerator_1.NsisScriptGenerator();
for (const item of fileAssociations) {
const extensions = builder_util_1.asArray(item.ext).map(platformPackager_1.normalizeExt);
const extensions = (0, builder_util_1.asArray)(item.ext).map(platformPackager_1.normalizeExt);
for (const ext of extensions) {
const customIcon = await packager.getResource(builder_util_1.getPlatformIconFileName(item.icon, false), `${extensions[0]}.ico`);
const customIcon = await packager.getResource((0, builder_util_1.getPlatformIconFileName)(item.icon, false), `${extensions[0]}.ico`);
let installedIconPath = "$appExe,0";
if (customIcon != null) {
installedIconPath = `$INSTDIR\\resources\\${path.basename(customIcon)}`;
installedIconPath = `$INSTDIR\\resources\\${(0, nsisScriptGenerator_1.nsisEscapeString)(path.basename(customIcon))}`;
registerFileAssociationsScript.file(installedIconPath, customIcon);
}
const icon = `"${installedIconPath}"`;
const commandText = `"Open with ${packager.appInfo.productName}"`;
const commandText = `"Open with ${(0, nsisScriptGenerator_1.nsisEscapeString)(packager.appInfo.productName)}"`;
const command = '"$appExe $\\"%1$\\""';
registerFileAssociationsScript.insertMacro("APP_ASSOCIATE", `"${ext}" "${item.name || ext}" "${item.description || ""}" ${icon} ${commandText} ${command}`);
registerFileAssociationsScript.insertMacro("APP_ASSOCIATE", `"${(0, nsisScriptGenerator_1.nsisEscapeString)(ext)}" "${(0, nsisScriptGenerator_1.nsisEscapeString)(item.name || ext)}" "${(0, nsisScriptGenerator_1.nsisEscapeString)(item.description || "")}" ${icon} ${commandText} ${command}`);
}
}
scriptGenerator.macro("registerFileAssociations", registerFileAssociationsScript);
@ -597,8 +652,8 @@ class NsisTarget extends core_1.Target {
else {
const unregisterFileAssociationsScript = new nsisScriptGenerator_1.NsisScriptGenerator();
for (const item of fileAssociations) {
for (const ext of builder_util_1.asArray(item.ext)) {
unregisterFileAssociationsScript.insertMacro("APP_UNASSOCIATE", `"${platformPackager_1.normalizeExt(ext)}" "${item.name || ext}"`);
for (const ext of (0, builder_util_1.asArray)(item.ext)) {
unregisterFileAssociationsScript.insertMacro("APP_UNASSOCIATE", `"${(0, platformPackager_1.normalizeExt)(ext)}" "${item.name || ext}"`);
}
}
scriptGenerator.macro("unregisterFileAssociations", unregisterFileAssociationsScript);
@ -610,12 +665,12 @@ class NsisTarget extends core_1.Target {
exports.NsisTarget = NsisTarget;
async function generateForPreCompressed(preCompressedFileExtensions, dir, arch, scriptGenerator) {
const resourcesDir = path.join(dir, "resources");
const dirInfo = await fs_1.statOrNull(resourcesDir);
const dirInfo = await (0, builder_util_1.statOrNull)(resourcesDir);
if (dirInfo == null || !dirInfo.isDirectory()) {
return;
}
const nodeModules = `${path.sep}node_modules`;
const preCompressedAssets = await fs_1.walk(resourcesDir, (file, stat) => {
const preCompressedAssets = await (0, builder_util_1.walk)(resourcesDir, (file, stat) => {
if (stat.isDirectory()) {
return !file.endsWith(nodeModules);
}
@ -626,7 +681,7 @@ async function generateForPreCompressed(preCompressedFileExtensions, dir, arch,
if (preCompressedAssets.length !== 0) {
const macro = new nsisScriptGenerator_1.NsisScriptGenerator();
for (const file of preCompressedAssets) {
macro.file(`$INSTDIR\\${path.relative(dir, file).replace(/\//g, "\\")}`, file);
macro.file(`$INSTDIR\\${(0, nsisScriptGenerator_1.nsisEscapeString)(path.relative(dir, file).replace(/\//g, "\\"))}`, file);
}
scriptGenerator.macro(`customFiles_${builder_util_1.Arch[arch]}`, macro);
}
@ -667,8 +722,8 @@ async function ensureNotBusy(outFile) {
async function createPackageFileInfo(file) {
return {
path: file,
size: (await fs_extra_1.stat(file)).size,
sha512: await hash_1.hashFile(file),
size: (await (0, fs_extra_1.stat)(file)).size,
sha512: await (0, hash_1.hashFile)(file),
};
}
//# sourceMappingURL=NsisTarget.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,4 @@
import { Arch } from "builder-util";
import { WinPackager } from "../../winPackager";
import { NsisTarget } from "./NsisTarget";
import { AppPackageHelper } from "./nsisUtil";
@ -6,6 +7,7 @@ export declare class WebInstallerTarget extends NsisTarget {
constructor(packager: WinPackager, outDir: string, targetName: string, packageHelper: AppPackageHelper);
get isWebInstaller(): boolean;
protected configureDefines(oneClick: boolean, defines: any): Promise<any>;
protected get installerFilenamePattern(): string;
get shouldBuildUniversalInstaller(): boolean;
protected installerFilenamePattern(_primaryArch?: Arch | null, _defaultArch?: string): string;
protected generateGitHubInstallerName(): string;
}

View file

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebInstallerTarget = void 0;
const builder_util_1 = require("builder-util");
const PublishManager_1 = require("../../publish/PublishManager");
const NsisTarget_1 = require("./NsisTarget");
/** @private */
@ -18,17 +19,22 @@ class WebInstallerTarget extends NsisTarget_1.NsisTarget {
const options = this.options;
let appPackageUrl = options.appPackageUrl;
if (appPackageUrl == null) {
const publishConfigs = await PublishManager_1.getPublishConfigsForUpdateInfo(packager, await PublishManager_1.getPublishConfigs(packager, packager.info.config, null, false), null);
const publishConfigs = await (0, PublishManager_1.getPublishConfigsForUpdateInfo)(packager, await (0, PublishManager_1.getPublishConfigs)(packager, this.options, null, false), null);
if (publishConfigs == null || publishConfigs.length === 0) {
throw new Error("Cannot compute app package download URL");
}
appPackageUrl = PublishManager_1.computeDownloadUrl(publishConfigs[0], null, packager);
appPackageUrl = (0, PublishManager_1.computeDownloadUrl)(publishConfigs[0], null, packager);
defines.APP_PACKAGE_URL_IS_INCOMPLETE = null;
}
defines.APP_PACKAGE_URL_IS_INCOMPLETE = null;
defines.APP_PACKAGE_URL = appPackageUrl;
}
get installerFilenamePattern() {
// tslint:disable:no-invalid-template-strings
get shouldBuildUniversalInstaller() {
if (this.options.buildUniversalInstaller === false) {
builder_util_1.log.warn({ buildUniversalInstaller: true }, "only universal builds are supported for nsis-web installers, overriding setting");
}
return true;
}
installerFilenamePattern(_primaryArch, _defaultArch) {
return "${productName} Web Setup ${version}.${ext}";
}
generateGitHubInstallerName() {

View file

@ -1 +1 @@
{"version":3,"file":"WebInstallerTarget.js","sourceRoot":"","sources":["../../../src/targets/nsis/WebInstallerTarget.ts"],"names":[],"mappings":";;;AAAA,iEAAoH;AAGpH,6CAAyC;AAGzC,eAAe;AACf,MAAa,kBAAmB,SAAQ,uBAAU;IAChD,YAAY,QAAqB,EAAE,MAAc,EAAE,UAAkB,EAAE,aAA+B;QACpG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,CAAA;IACpD,CAAC;IAED,IAAI,cAAc;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IAES,KAAK,CAAC,gBAAgB,CAAC,QAAiB,EAAE,OAAY;QAC9D,8BAA8B;QAC9B,MAAO,uBAAU,CAAC,SAAgC,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;QAEjG,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAyB,CAAA;QAE9C,IAAI,aAAa,GAAG,OAAO,CAAC,aAAa,CAAA;QACzC,IAAI,aAAa,IAAI,IAAI,EAAE;YACzB,MAAM,cAAc,GAAG,MAAM,+CAA8B,CAAC,QAAQ,EAAE,MAAM,kCAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;YACjJ,IAAI,cAAc,IAAI,IAAI,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE;gBACzD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;aAC3D;YAED,aAAa,GAAG,mCAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;SACtE;QAED,OAAO,CAAC,6BAA6B,GAAG,IAAI,CAAA;QAC5C,OAAO,CAAC,eAAe,GAAG,aAAa,CAAA;IACzC,CAAC;IAED,IAAc,wBAAwB;QACpC,6CAA6C;QAC7C,OAAO,4CAA4C,CAAA;IACrD,CAAC;IAES,2BAA2B;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAA;QACrC,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAA;QACzF,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,UAAU,IAAI,OAAO,CAAC,OAAO,MAAM,CAAA;IAC/D,CAAC;CACF;AAxCD,gDAwCC","sourcesContent":["import { computeDownloadUrl, getPublishConfigs, getPublishConfigsForUpdateInfo } from \"../../publish/PublishManager\"\nimport { WinPackager } from \"../../winPackager\"\nimport { NsisWebOptions } from \"./nsisOptions\"\nimport { NsisTarget } from \"./NsisTarget\"\nimport { AppPackageHelper } from \"./nsisUtil\"\n\n/** @private */\nexport class WebInstallerTarget extends NsisTarget {\n constructor(packager: WinPackager, outDir: string, targetName: string, packageHelper: AppPackageHelper) {\n super(packager, outDir, targetName, packageHelper)\n }\n\n get isWebInstaller(): boolean {\n return true\n }\n\n protected async configureDefines(oneClick: boolean, defines: any): Promise<any> {\n //noinspection ES6MissingAwait\n await (NsisTarget.prototype as WebInstallerTarget).configureDefines.call(this, oneClick, defines)\n\n const packager = this.packager\n const options = this.options as NsisWebOptions\n\n let appPackageUrl = options.appPackageUrl\n if (appPackageUrl == null) {\n const publishConfigs = await getPublishConfigsForUpdateInfo(packager, await getPublishConfigs(packager, packager.info.config, null, false), null)\n if (publishConfigs == null || publishConfigs.length === 0) {\n throw new Error(\"Cannot compute app package download URL\")\n }\n\n appPackageUrl = computeDownloadUrl(publishConfigs[0], null, packager)\n }\n\n defines.APP_PACKAGE_URL_IS_INCOMPLETE = null\n defines.APP_PACKAGE_URL = appPackageUrl\n }\n\n protected get installerFilenamePattern(): string {\n // tslint:disable:no-invalid-template-strings\n return \"${productName} Web Setup ${version}.${ext}\"\n }\n\n protected generateGitHubInstallerName(): string {\n const appInfo = this.packager.appInfo\n const classifier = appInfo.name.toLowerCase() === appInfo.name ? \"web-setup\" : \"WebSetup\"\n return `${appInfo.name}-${classifier}-${appInfo.version}.exe`\n }\n}\n"]}
{"version":3,"file":"WebInstallerTarget.js","sourceRoot":"","sources":["../../../src/targets/nsis/WebInstallerTarget.ts"],"names":[],"mappings":";;;AAAA,+CAAwC;AACxC,iEAAoH;AAGpH,6CAAyC;AAGzC,eAAe;AACf,MAAa,kBAAmB,SAAQ,uBAAU;IAChD,YAAY,QAAqB,EAAE,MAAc,EAAE,UAAkB,EAAE,aAA+B;QACpG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,CAAA;IACpD,CAAC;IAED,IAAI,cAAc;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IAES,KAAK,CAAC,gBAAgB,CAAC,QAAiB,EAAE,OAAY;QAC9D,8BAA8B;QAC9B,MAAO,uBAAU,CAAC,SAAgC,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;QAEjG,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAyB,CAAA;QAE9C,IAAI,aAAa,GAAG,OAAO,CAAC,aAAa,CAAA;QACzC,IAAI,aAAa,IAAI,IAAI,EAAE,CAAC;YAC1B,MAAM,cAAc,GAAG,MAAM,IAAA,+CAA8B,EAAC,QAAQ,EAAE,MAAM,IAAA,kCAAiB,EAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;YACzI,IAAI,cAAc,IAAI,IAAI,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1D,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;YAC5D,CAAC;YAED,aAAa,GAAG,IAAA,mCAAkB,EAAC,cAAc,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;YACrE,OAAO,CAAC,6BAA6B,GAAG,IAAI,CAAA;QAC9C,CAAC;QAED,OAAO,CAAC,eAAe,GAAG,aAAa,CAAA;IACzC,CAAC;IAED,IAAI,6BAA6B;QAC/B,IAAI,IAAI,CAAC,OAAO,CAAC,uBAAuB,KAAK,KAAK,EAAE,CAAC;YACnD,kBAAG,CAAC,IAAI,CAAC,EAAE,uBAAuB,EAAE,IAAI,EAAE,EAAE,iFAAiF,CAAC,CAAA;QAChI,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAES,wBAAwB,CAAC,YAA0B,EAAE,YAAqB;QAClF,OAAO,4CAA4C,CAAA;IACrD,CAAC;IAES,2BAA2B;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAA;QACrC,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAA;QACzF,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,UAAU,IAAI,OAAO,CAAC,OAAO,MAAM,CAAA;IAC/D,CAAC;CACF;AA9CD,gDA8CC","sourcesContent":["import { Arch, log } from \"builder-util\"\nimport { computeDownloadUrl, getPublishConfigs, getPublishConfigsForUpdateInfo } from \"../../publish/PublishManager\"\nimport { WinPackager } from \"../../winPackager\"\nimport { NsisWebOptions } from \"./nsisOptions\"\nimport { NsisTarget } from \"./NsisTarget\"\nimport { AppPackageHelper } from \"./nsisUtil\"\n\n/** @private */\nexport class WebInstallerTarget extends NsisTarget {\n constructor(packager: WinPackager, outDir: string, targetName: string, packageHelper: AppPackageHelper) {\n super(packager, outDir, targetName, packageHelper)\n }\n\n get isWebInstaller(): boolean {\n return true\n }\n\n protected async configureDefines(oneClick: boolean, defines: any): Promise<any> {\n //noinspection ES6MissingAwait\n await (NsisTarget.prototype as WebInstallerTarget).configureDefines.call(this, oneClick, defines)\n\n const packager = this.packager\n const options = this.options as NsisWebOptions\n\n let appPackageUrl = options.appPackageUrl\n if (appPackageUrl == null) {\n const publishConfigs = await getPublishConfigsForUpdateInfo(packager, await getPublishConfigs(packager, this.options, null, false), null)\n if (publishConfigs == null || publishConfigs.length === 0) {\n throw new Error(\"Cannot compute app package download URL\")\n }\n\n appPackageUrl = computeDownloadUrl(publishConfigs[0], null, packager)\n defines.APP_PACKAGE_URL_IS_INCOMPLETE = null\n }\n\n defines.APP_PACKAGE_URL = appPackageUrl\n }\n\n get shouldBuildUniversalInstaller() {\n if (this.options.buildUniversalInstaller === false) {\n log.warn({ buildUniversalInstaller: true }, \"only universal builds are supported for nsis-web installers, overriding setting\")\n }\n return true\n }\n\n protected installerFilenamePattern(_primaryArch?: Arch | null, _defaultArch?: string): string {\n return \"${productName} Web Setup ${version}.${ext}\"\n }\n\n protected generateGitHubInstallerName(): string {\n const appInfo = this.packager.appInfo\n const classifier = appInfo.name.toLowerCase() === appInfo.name ? \"web-setup\" : \"WebSetup\"\n return `${appInfo.name}-${classifier}-${appInfo.version}.exe`\n }\n}\n"]}

View file

@ -1,14 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addCustomMessageFileInclude = exports.createAddLangsMacro = exports.LangConfigurator = void 0;
exports.LangConfigurator = void 0;
exports.createAddLangsMacro = createAddLangsMacro;
exports.addCustomMessageFileInclude = addCustomMessageFileInclude;
const builder_util_1 = require("builder-util");
const langs_1 = require("../../util/langs");
const debug_1 = require("debug");
const fs_extra_1 = require("fs-extra");
const js_yaml_1 = require("js-yaml");
const path = require("path");
const langs_1 = require("../../util/langs");
const nsisUtil_1 = require("./nsisUtil");
const debug = debug_1.default("electron-builder:nsis");
const debug = (0, debug_1.default)("electron-builder:nsis");
class LangConfigurator {
constructor(options) {
const rawList = options.installerLanguages;
@ -19,7 +21,7 @@ class LangConfigurator {
this.isMultiLang = options.multiLanguageInstaller !== false;
}
if (this.isMultiLang) {
this.langs = rawList == null ? langs_1.bundledLanguages.slice() : builder_util_1.asArray(rawList).map(it => langs_1.toLangWithRegion(it.replace("-", "_")));
this.langs = rawList == null ? langs_1.bundledLanguages.slice() : (0, builder_util_1.asArray)(rawList).map(it => (0, langs_1.toLangWithRegion)(it.replace("-", "_")));
}
else {
this.langs = ["en_US"];
@ -57,19 +59,17 @@ function createAddLangsMacro(scriptGenerator, langConfigurator) {
}
scriptGenerator.macro("addLangs", result);
}
exports.createAddLangsMacro = createAddLangsMacro;
async function writeCustomLangFile(data, packager) {
const file = await packager.getTempFile("messages.nsh");
await fs_extra_1.outputFile(file, data);
await (0, fs_extra_1.outputFile)(file, data);
return file;
}
async function addCustomMessageFileInclude(input, packager, scriptGenerator, langConfigurator) {
const data = js_yaml_1.load(await fs_extra_1.readFile(path.join(nsisUtil_1.nsisTemplatesDir, input), "utf-8"));
const data = (0, js_yaml_1.load)(await (0, fs_extra_1.readFile)(path.join(nsisUtil_1.nsisTemplatesDir, input), "utf-8"));
const instructions = computeCustomMessageTranslations(data, langConfigurator).join("\n");
debug(instructions);
scriptGenerator.include(await writeCustomLangFile(instructions, packager));
}
exports.addCustomMessageFileInclude = addCustomMessageFileInclude;
function computeCustomMessageTranslations(messages, langConfigurator) {
const result = [];
const includedLangs = new Set(langConfigurator.langs);
@ -77,7 +77,7 @@ function computeCustomMessageTranslations(messages, langConfigurator) {
const langToTranslations = messages[messageId];
const unspecifiedLangs = new Set(langConfigurator.langs);
for (const lang of Object.keys(langToTranslations)) {
const langWithRegion = langs_1.toLangWithRegion(lang);
const langWithRegion = (0, langs_1.toLangWithRegion)(lang);
if (!includedLangs.has(langWithRegion)) {
continue;
}

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeLicensePage = void 0;
exports.computeLicensePage = computeLicensePage;
const builder_util_1 = require("builder-util");
const fs = require("fs");
const path = require("path");
const langs_1 = require("../../util/langs");
const license_1 = require("../../util/license");
const path = require("path");
const nsisUtil_1 = require("./nsisUtil");
function convertFileToUtf8WithBOMSync(filePath) {
var _a;
try {
const UTF8_BOM_HEADER = Buffer.from([0xef, 0xbb, 0xbf]);
const data = fs.readFileSync(filePath);
// Check if the file already starts with a UTF-8 BOM
builder_util_1.log.debug({ file: builder_util_1.log.filePath(filePath) }, "checking file for BOM header");
if (data.length >= UTF8_BOM_HEADER.length && data.subarray(0, UTF8_BOM_HEADER.length).equals(UTF8_BOM_HEADER)) {
builder_util_1.log.debug({ file: builder_util_1.log.filePath(filePath) }, "file is already in BOM format, skipping conversion.");
return true;
}
// If not, add the BOM
const dataWithBOM = Buffer.concat([UTF8_BOM_HEADER, data]);
fs.writeFileSync(filePath, dataWithBOM);
builder_util_1.log.debug({ file: builder_util_1.log.filePath(filePath) }, "file successfully converted to UTF-8 with BOM");
return true;
}
catch (err) {
builder_util_1.log.error({ file: builder_util_1.log.filePath(filePath), message: (_a = err.message) !== null && _a !== void 0 ? _a : err.stack }, "unable to convert file to UTF-8 with BOM");
return false;
}
}
async function computeLicensePage(packager, options, scriptGenerator, languages) {
const license = await license_1.getNotLocalizedLicenseFile(options.license, packager);
const license = await (0, license_1.getNotLocalizedLicenseFile)(options.license, packager);
if (license != null) {
let licensePage;
if (license.endsWith(".html")) {
@ -29,7 +53,7 @@ async function computeLicensePage(packager, options, scriptGenerator, languages)
}
return;
}
const licenseFiles = await license_1.getLicenseFiles(packager);
const licenseFiles = await (0, license_1.getLicenseFiles)(packager);
if (licenseFiles.length === 0) {
return;
}
@ -38,6 +62,7 @@ async function computeLicensePage(packager, options, scriptGenerator, languages)
let defaultFile = null;
for (const item of licenseFiles) {
unspecifiedLangs.delete(item.langWithRegion);
convertFileToUtf8WithBOMSync(item.file);
if (defaultFile == null) {
defaultFile = item.file;
}
@ -49,5 +74,4 @@ async function computeLicensePage(packager, options, scriptGenerator, languages)
licensePage.push('!insertmacro MUI_PAGE_LICENSE "$(MUILicense)"');
scriptGenerator.macro("licensePage", licensePage);
}
exports.computeLicensePage = computeLicensePage;
//# sourceMappingURL=nsisLicense.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,8 @@
import { TargetSpecificOptions } from "../../core";
import { CommonWindowsInstallerConfiguration } from "../..";
interface CustomNsisBinary {
import { TargetSpecificOptions } from "../../core";
export interface CustomNsisBinary {
/**
* @default https://github.com/electron-userland/electron-builder-binaries/releases/download
* @default https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-3.0.4.1/nsis-3.0.4.1.7z
*/
readonly url: string | null;
/**
@ -21,6 +21,20 @@ interface CustomNsisBinary {
*/
readonly debugLogging?: boolean | null;
}
export interface CustomNsisResources {
/**
* @default https://github.com/electron-userland/electron-builder-binaries/releases/download/nsis-resources-3.4.1/nsis-resources-3.4.1.7z
*/
readonly url: string;
/**
* @default Dqd6g+2buwwvoG1Vyf6BHR1b+25QMmPcwZx40atOT57gH27rkjOei1L0JTldxZu4NFoEmW4kJgZ3DlSWVON3+Q==
*/
readonly checksum: string;
/**
* @default 3.4.1
*/
readonly version: string;
}
export interface CommonNsisOptions {
/**
* Whether to create [Unicode installer](http://nsis.sourceforge.net/Docs/Chapter1.html#intro-unicode).
@ -28,7 +42,11 @@ export interface CommonNsisOptions {
*/
readonly unicode?: boolean;
/**
* See [GUID vs Application Name](../configuration/nsis#guid-vs-application-name).
* The GUID for the installer. Used to identify the application for upgrade and uninstall operations.
* If not specified, a deterministic GUID is generated from the app ID (`appId`) but this means
* changing your `appId` will break silent upgrades of existing installs.
*
* @see [GUID vs Application Name](https://www.electron.build/docs/nsis#guid-vs-application-name)
*/
readonly guid?: string | null;
/**
@ -37,7 +55,7 @@ export interface CommonNsisOptions {
*/
readonly warningsAsErrors?: boolean;
/**
* @private
* Forces zip compression format instead of LZMA. Used internally for differential update packages.
* @default false
*/
readonly useZip?: boolean;
@ -45,6 +63,10 @@ export interface CommonNsisOptions {
* Allows you to provide your own `makensis`, such as one with support for debug logging via LogSet and LogText. (Logging also requires option `debugLogging = true`)
*/
readonly customNsisBinary?: CustomNsisBinary | null;
/**
* Allows you to provide your own `nsis-resources`
*/
readonly customNsisResources?: CustomNsisResources | null;
}
export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerConfiguration, TargetSpecificOptions {
/**
@ -63,6 +85,12 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
* @default false
*/
readonly perMachine?: boolean;
/**
* Whether to set per-machine or per-user installation as default selection on the install mode installer page.
*
* @default false
*/
readonly selectPerMachineByDefault?: boolean;
/**
* *assisted installer only.* Allow requesting for elevation. If false, user will have to restart installer with elevated permissions.
* @default true
@ -79,32 +107,32 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
*/
readonly removeDefaultUninstallWelcomePage?: boolean;
/**
* The path to installer icon, relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory.
* The path to installer icon, relative to the [build resources](https://www.electron.build/docs/contents#extraresources) or to the project directory.
* Defaults to `build/installerIcon.ico` or application icon.
*/
readonly installerIcon?: string | null;
/**
* The path to uninstaller icon, relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory.
* The path to uninstaller icon, relative to the [build resources](https://www.electron.build/docs/contents#extraresources) or to the project directory.
* Defaults to `build/uninstallerIcon.ico` or application icon.
*/
readonly uninstallerIcon?: string | null;
/**
* *assisted installer only.* `MUI_HEADERIMAGE`, relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory.
* *assisted installer only.* `MUI_HEADERIMAGE`, relative to the [build resources](https://www.electron.build/docs/contents#extraresources) or to the project directory.
* @default build/installerHeader.bmp
*/
readonly installerHeader?: string | null;
/**
* *one-click installer only.* The path to header icon (above the progress bar), relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory.
* *one-click installer only.* The path to header icon (above the progress bar), relative to the [build resources](https://www.electron.build/docs/contents#extraresources) or to the project directory.
* Defaults to `build/installerHeaderIcon.ico` or application icon.
*/
readonly installerHeaderIcon?: string | null;
/**
* *assisted installer only.* `MUI_WELCOMEFINISHPAGE_BITMAP`, relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory.
* *assisted installer only.* `MUI_WELCOMEFINISHPAGE_BITMAP`, relative to the [build resources](https://www.electron.build/docs/contents#extraresources) or to the project directory.
* Defaults to `build/installerSidebar.bmp` or `${NSISDIR}\\Contrib\\Graphics\\Wizard\\nsis3-metro.bmp`. Image size 164 × 314 pixels.
*/
readonly installerSidebar?: string | null;
/**
* *assisted installer only.* `MUI_UNWELCOMEFINISHPAGE_BITMAP`, relative to the [build resources](/configuration/configuration#MetadataDirectories-buildResources) or to the project directory.
* *assisted installer only.* `MUI_UNWELCOMEFINISHPAGE_BITMAP`, relative to the [build resources](https://www.electron.build/docs/contents#extraresources) or to the project directory.
* Defaults to `installerSidebar` option or `build/uninstallerSidebar.bmp` or `build/installerSidebar.bmp` or `${NSISDIR}\\Contrib\\Graphics\\Wizard\\nsis3-metro.bmp`
*/
readonly uninstallerSidebar?: string | null;
@ -112,7 +140,23 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
* The uninstaller display name in the control panel.
* @default ${productName} ${version}
*/
readonly uninstallDisplayName?: string;
readonly uninstallDisplayName?: string | null;
/**
* The URL to the uninstaller help page in the control panel. Defaults to [homepage](https://www.electron.build/docs/configuration#homepage) from application package.json.
*/
readonly uninstallUrlHelp?: string | null;
/**
* The URL to the uninstaller info about page in the control panel. Defaults to [homepage](https://www.electron.build/docs/configuration#homepage) from application package.json.
*/
readonly uninstallUrlInfoAbout?: string | null;
/**
* The URL to the uninstaller update info page in the control panel. Defaults to [homepage](https://www.electron.build/docs/configuration#homepage) from application package.json.
*/
readonly uninstallUrlUpdateInfo?: string | null;
/**
* The URL to the uninstaller readme page in the control panel. Defaults to [homepage](https://www.electron.build/docs/configuration#homepage) from application package.json.
*/
readonly uninstallUrlReadme?: string | null;
/**
* The path to NSIS include script to customize installer. Defaults to `build/installer.nsh`. See [Custom NSIS script](#custom-nsis-script).
*/
@ -131,7 +175,7 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
*/
readonly license?: string | null;
/**
* The [artifact file name template](/configuration/configuration#artifact-file-name-template). Defaults to `${productName} Setup ${version}.${ext}`.
* The [artifact file name template](https://www.electron.build/docs/configuration#artifact-file-name-template). Defaults to `${productName} Setup ${version}.${ext}`.
*/
readonly artifactName?: string | null;
/**
@ -140,9 +184,9 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
*/
readonly deleteAppDataOnUninstall?: boolean;
/**
* @private
* Marks the package as built with differential download support for the update server.
*/
differentialPackage?: boolean;
readonly differentialPackage?: boolean;
/**
* Whether to display a language selection dialog. Not recommended (by default will be detected using OS language).
* @default false
@ -170,6 +214,12 @@ export interface NsisOptions extends CommonNsisOptions, CommonWindowsInstallerCo
* @default [".avi", ".mov", ".m4v", ".mp4", ".m4p", ".qt", ".mkv", ".webm", ".vmdk"]
*/
readonly preCompressedFileExtensions?: Array<string> | string | null;
/**
* Disable building an universal installer of the archs specified in the target configuration
* *Not supported for nsis-web*
* @default true
*/
readonly buildUniversalInstaller?: boolean;
}
/**
* Portable options.
@ -193,6 +243,11 @@ export interface PortableOptions extends TargetSpecificOptions, CommonNsisOption
* The image to show while the portable executable is extracting. This image must be a bitmap (`.bmp`) image.
*/
readonly splashImage?: string | null;
/**
* Disable building an universal installer of the archs specified in the target configuration
* @default true
*/
readonly buildUniversalInstaller?: boolean;
}
/**
* Web Installer options.
@ -208,8 +263,12 @@ export interface NsisWebOptions extends NsisOptions {
*/
readonly appPackageUrl?: string | null;
/**
* The [artifact file name template](/configuration/configuration#artifact-file-name-template). Defaults to `${productName} Web Setup ${version}.${ext}`.
* The [artifact file name template](https://www.electron.build/docs/configuration#artifact-file-name-template). Defaults to `${productName} Web Setup ${version}.${ext}`.
*/
readonly artifactName?: string | null;
/**
* Override for `NsisOptions.buildUniversalInstaller`. nsis-web requires universal installer
* @default true
*/
readonly buildUniversalInstaller?: true;
}
export {};

File diff suppressed because one or more lines are too long

View file

@ -9,3 +9,4 @@ export declare class NsisScriptGenerator {
flags(flags: Array<string>): void;
build(): string;
}
export declare function nsisEscapeString(s: string): string;

View file

@ -1,6 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NsisScriptGenerator = void 0;
exports.nsisEscapeString = nsisEscapeString;
const builder_util_1 = require("builder-util");
class NsisScriptGenerator {
constructor() {
this.lines = [];
@ -40,6 +42,16 @@ class NsisScriptGenerator {
}
}
exports.NsisScriptGenerator = NsisScriptGenerator;
function nsisEscapeString(s) {
const escaped = s
.replace(/\r\n|\r|\n/g, " ") // newlines break NSIS string literals
.replace(/\$(?!\{)/g, "$$$$") // bare $ → $$ (prevents NSIS variable expansion); ${...} references are left intact
.replace(/"/g, '$\\"'); // " → $\" (NSIS escape for double-quote)
if (escaped !== s) {
builder_util_1.log.debug({ original: s, final: escaped }, "nsis was escaped");
}
return escaped;
}
function getVarNameForFlag(flagName) {
if (flagName === "allusers") {
return "isForAllUsers";

View file

@ -1 +1 @@
{"version":3,"file":"nsisScriptGenerator.js","sourceRoot":"","sources":["../../../src/targets/nsis/nsisScriptGenerator.ts"],"names":[],"mappings":";;;AAAA,MAAa,mBAAmB;IAAhC;QACmB,UAAK,GAAkB,EAAE,CAAA;IA0C5C,CAAC;IAxCC,aAAa,CAAC,IAAY;QACxB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,GAAG,CAAC,CAAA;IAC7C,CAAC;IAED,YAAY,CAAC,UAAkB,EAAE,GAAW;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,kBAAkB,UAAU,KAAK,GAAG,GAAG,CAAC,CAAA;IAC1D,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,IAAI,GAAG,CAAC,CAAA;IACvC,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,KAA0C;QAC5D,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC,CAAA;IACpH,CAAC;IAED,IAAI,CAAC,UAAyB,EAAE,IAAY;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,UAAU,GAAG,KAAK,IAAI,GAAG,CAAC,CAAA;IACzF,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,UAAkB;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,UAAU,EAAE,CAAC,CAAA;IACvD,CAAC;IAED,iBAAiB;IACjB,KAAK,CAAC,KAAoB;QACxB,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE;YAC5B,MAAM,YAAY,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;YACpG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,YAAY;oCACT,QAAQ;;;UAGlC,YAAY,SAAS,YAAY;CAC1C,CAAC,CAAA;SACG;IACH,CAAC;IAED,KAAK;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACrC,CAAC;CACF;AA3CD,kDA2CC;AAED,SAAS,iBAAiB,CAAC,QAAgB;IACzC,IAAI,QAAQ,KAAK,UAAU,EAAE;QAC3B,OAAO,eAAe,CAAA;KACvB;IACD,IAAI,QAAQ,KAAK,aAAa,EAAE;QAC9B,OAAO,kBAAkB,CAAA;KAC1B;IACD,OAAO,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC","sourcesContent":["export class NsisScriptGenerator {\n private readonly lines: Array<string> = []\n\n addIncludeDir(file: string) {\n this.lines.push(`!addincludedir \"${file}\"`)\n }\n\n addPluginDir(pluginArch: string, dir: string) {\n this.lines.push(`!addplugindir /${pluginArch} \"${dir}\"`)\n }\n\n include(file: string) {\n this.lines.push(`!include \"${file}\"`)\n }\n\n macro(name: string, lines: Array<string> | NsisScriptGenerator) {\n this.lines.push(`!macro ${name}`, ` ${(Array.isArray(lines) ? lines : lines.lines).join(\"\\n \")}`, `!macroend\\n`)\n }\n\n file(outputName: string | null, file: string) {\n this.lines.push(`File${outputName == null ? \"\" : ` \"/oname=${outputName}\"`} \"${file}\"`)\n }\n\n insertMacro(name: string, parameters: string) {\n this.lines.push(`!insertmacro ${name} ${parameters}`)\n }\n\n // without -- !!!\n flags(flags: Array<string>) {\n for (const flagName of flags) {\n const variableName = getVarNameForFlag(flagName).replace(/[-]+(\\w|$)/g, (m, p1) => p1.toUpperCase())\n this.lines.push(`!macro _${variableName} _a _b _t _f\n $\\{StdUtils.TestParameter} $R9 \"${flagName}\"\n StrCmp \"$R9\" \"true\" \\`$\\{_t}\\` \\`$\\{_f}\\`\n!macroend\n!define ${variableName} \\`\"\" ${variableName} \"\"\\`\n`)\n }\n }\n\n build() {\n return this.lines.join(\"\\n\") + \"\\n\"\n }\n}\n\nfunction getVarNameForFlag(flagName: string): string {\n if (flagName === \"allusers\") {\n return \"isForAllUsers\"\n }\n if (flagName === \"currentuser\") {\n return \"isForCurrentUser\"\n }\n return \"is\" + flagName[0].toUpperCase() + flagName.substring(1)\n}\n"]}
{"version":3,"file":"nsisScriptGenerator.js","sourceRoot":"","sources":["../../../src/targets/nsis/nsisScriptGenerator.ts"],"names":[],"mappings":";;;AA+CA,4CASC;AAxDD,+CAAkC;AAElC,MAAa,mBAAmB;IAAhC;QACmB,UAAK,GAAkB,EAAE,CAAA;IA0C5C,CAAC;IAxCC,aAAa,CAAC,IAAY;QACxB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,IAAI,GAAG,CAAC,CAAA;IAC7C,CAAC;IAED,YAAY,CAAC,UAAkB,EAAE,GAAW;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,kBAAkB,UAAU,KAAK,GAAG,GAAG,CAAC,CAAA;IAC1D,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,IAAI,GAAG,CAAC,CAAA;IACvC,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,KAA0C;QAC5D,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC,CAAA;IACpH,CAAC;IAED,IAAI,CAAC,UAAyB,EAAE,IAAY;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,UAAU,GAAG,KAAK,IAAI,GAAG,CAAC,CAAA;IACzF,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,UAAkB;QAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,UAAU,EAAE,CAAC,CAAA;IACvD,CAAC;IAED,iBAAiB;IACjB,KAAK,CAAC,KAAoB;QACxB,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;YAC7B,MAAM,YAAY,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;YACpG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,YAAY;oCACT,QAAQ;;;UAGlC,YAAY,SAAS,YAAY;CAC1C,CAAC,CAAA;QACE,CAAC;IACH,CAAC;IAED,KAAK;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACrC,CAAC;CACF;AA3CD,kDA2CC;AAED,SAAgB,gBAAgB,CAAC,CAAS;IACxC,MAAM,OAAO,GAAG,CAAC;SACd,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,sCAAsC;SAClE,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,oFAAoF;SACjH,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA,CAAC,yCAAyC;IAClE,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;QAClB,kBAAG,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,kBAAkB,CAAC,CAAA;IAChE,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB;IACzC,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5B,OAAO,eAAe,CAAA;IACxB,CAAC;IACD,IAAI,QAAQ,KAAK,aAAa,EAAE,CAAC;QAC/B,OAAO,kBAAkB,CAAA;IAC3B,CAAC;IACD,OAAO,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC","sourcesContent":["import { log } from \"builder-util\"\n\nexport class NsisScriptGenerator {\n private readonly lines: Array<string> = []\n\n addIncludeDir(file: string) {\n this.lines.push(`!addincludedir \"${file}\"`)\n }\n\n addPluginDir(pluginArch: string, dir: string) {\n this.lines.push(`!addplugindir /${pluginArch} \"${dir}\"`)\n }\n\n include(file: string) {\n this.lines.push(`!include \"${file}\"`)\n }\n\n macro(name: string, lines: Array<string> | NsisScriptGenerator) {\n this.lines.push(`!macro ${name}`, ` ${(Array.isArray(lines) ? lines : lines.lines).join(\"\\n \")}`, `!macroend\\n`)\n }\n\n file(outputName: string | null, file: string) {\n this.lines.push(`File${outputName == null ? \"\" : ` \"/oname=${outputName}\"`} \"${file}\"`)\n }\n\n insertMacro(name: string, parameters: string) {\n this.lines.push(`!insertmacro ${name} ${parameters}`)\n }\n\n // without -- !!!\n flags(flags: Array<string>) {\n for (const flagName of flags) {\n const variableName = getVarNameForFlag(flagName).replace(/[-]+(\\w|$)/g, (m, p1) => p1.toUpperCase())\n this.lines.push(`!macro _${variableName} _a _b _t _f\n $\\{StdUtils.TestParameter} $R9 \"${flagName}\"\n StrCmp \"$R9\" \"true\" \\`$\\{_t}\\` \\`$\\{_f}\\`\n!macroend\n!define ${variableName} \\`\"\" ${variableName} \"\"\\`\n`)\n }\n }\n\n build() {\n return this.lines.join(\"\\n\") + \"\\n\"\n }\n}\n\nexport function nsisEscapeString(s: string): string {\n const escaped = s\n .replace(/\\r\\n|\\r|\\n/g, \" \") // newlines break NSIS string literals\n .replace(/\\$(?!\\{)/g, \"$$$$\") // bare $ → $$ (prevents NSIS variable expansion); ${...} references are left intact\n .replace(/\"/g, '$\\\\\"') // \" → $\\\" (NSIS escape for double-quote)\n if (escaped !== s) {\n log.debug({ original: s, final: escaped }, \"nsis was escaped\")\n }\n return escaped\n}\n\nfunction getVarNameForFlag(flagName: string): string {\n if (flagName === \"allusers\") {\n return \"isForAllUsers\"\n }\n if (flagName === \"currentuser\") {\n return \"isForCurrentUser\"\n }\n return \"is\" + flagName[0].toUpperCase() + flagName.substring(1)\n}\n"]}

View file

@ -1,21 +1,19 @@
import { Arch } from "builder-util";
import { PackageFileInfo } from "builder-util-runtime";
import { NsisTarget } from "./NsisTarget";
import { NsisOptions } from "./nsisOptions";
export declare const nsisTemplatesDir: string;
export declare const NsisTargetOptions: {
then: (callback: (options: NsisOptions) => any) => Promise<string>;
resolve: (options: NsisOptions) => any;
};
export declare const NSIS_PATH: () => Promise<string>;
export interface PackArchResult {
fileInfo: PackageFileInfo;
unpackedSize: number;
}
export declare class AppPackageHelper {
private readonly elevateHelper;
private readonly archToFileInfo;
private readonly archToResult;
private readonly infoToIsDelete;
/** @private */
refCount: number;
constructor(elevateHelper: CopyElevateHelper);
packArch(arch: Arch, target: NsisTarget): Promise<PackageFileInfo>;
packArch(arch: Arch, target: NsisTarget): Promise<PackArchResult>;
finishBuild(): Promise<any>;
}
export declare class CopyElevateHelper {

View file

@ -1,64 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UninstallerReader = exports.CopyElevateHelper = exports.AppPackageHelper = exports.NSIS_PATH = exports.NsisTargetOptions = exports.nsisTemplatesDir = void 0;
exports.UninstallerReader = exports.CopyElevateHelper = exports.AppPackageHelper = exports.nsisTemplatesDir = void 0;
const builder_util_1 = require("builder-util");
const binDownload_1 = require("../../binDownload");
const fs_1 = require("builder-util/out/fs");
const path = require("path");
const pathManager_1 = require("../../util/pathManager");
const fs = require("fs/promises");
const path = require("path");
const zlib = require("zlib");
exports.nsisTemplatesDir = pathManager_1.getTemplatePath("nsis");
exports.NsisTargetOptions = (() => {
let _resolve;
const promise = new Promise(resolve => (_resolve = resolve));
return {
then: (callback) => promise.then(callback),
resolve: (options) => _resolve(options),
};
})();
const NSIS_PATH = () => {
const custom = process.env.ELECTRON_BUILDER_NSIS_DIR;
if (custom != null && custom.length > 0) {
return Promise.resolve(custom.trim());
}
return exports.NsisTargetOptions.then((options) => {
if (options.customNsisBinary) {
const { checksum, url, version } = options.customNsisBinary;
if (checksum && url) {
const binaryVersion = version || checksum.substr(0, 8);
return binDownload_1.getBinFromCustomLoc("nsis", binaryVersion, url, checksum);
}
}
// Warning: Don't use v3.0.4.2 - https://github.com/electron-userland/electron-builder/issues/6334
// noinspection SpellCheckingInspection
return binDownload_1.getBinFromUrl("nsis", "3.0.4.1", "VKMiizYdmNdJOWpRGz4trl4lD++BvYP2irAXpMilheUP0pc93iKlWAoP843Vlraj8YG19CVn0j+dCo/hURz9+Q==");
});
};
exports.NSIS_PATH = NSIS_PATH;
const windows_1 = require("../../toolsets/windows");
const pathManager_1 = require("../../util/pathManager");
exports.nsisTemplatesDir = (0, pathManager_1.getTemplatePath)("nsis");
class AppPackageHelper {
constructor(elevateHelper) {
this.elevateHelper = elevateHelper;
this.archToFileInfo = new Map();
this.archToResult = new Map();
this.infoToIsDelete = new Map();
/** @private */
this.refCount = 0;
}
async packArch(arch, target) {
let infoPromise = this.archToFileInfo.get(arch);
if (infoPromise == null) {
let resultPromise = this.archToResult.get(arch);
if (resultPromise == null) {
const appOutDir = target.archs.get(arch);
infoPromise = this.elevateHelper.copy(appOutDir, target).then(() => target.buildAppPackage(appOutDir, arch));
this.archToFileInfo.set(arch, infoPromise);
resultPromise = this.elevateHelper
.copy(appOutDir, target)
.then(() => target.buildAppPackage(appOutDir, arch))
.then(async (fileInfo) => ({
fileInfo,
unpackedSize: await (0, builder_util_1.dirSize)(appOutDir),
}));
this.archToResult.set(arch, resultPromise);
}
const info = await infoPromise;
const result = await resultPromise;
const { fileInfo: info } = result;
if (target.isWebInstaller) {
this.infoToIsDelete.set(info, false);
}
else if (!this.infoToIsDelete.has(info)) {
this.infoToIsDelete.set(info, true);
}
return info;
return result;
}
async finishBuild() {
if (--this.refCount > 0) {
@ -79,6 +58,7 @@ class CopyElevateHelper {
this.copied = new Map();
}
copy(appOutDir, target) {
var _a;
if (!target.packager.info.framework.isCopyElevateHelper) {
return Promise.resolve();
}
@ -94,11 +74,12 @@ class CopyElevateHelper {
if (promise != null) {
return promise;
}
promise = exports.NSIS_PATH().then(it => {
promise = (0, windows_1.getNsisElevatePath)((_a = target.packager.config.toolsets) === null || _a === void 0 ? void 0 : _a.nsis, target.options.customNsisBinary).then(elevatePath => {
const outFile = path.join(appOutDir, "resources", "elevate.exe");
const promise = fs_1.copyFile(path.join(it, "elevate.exe"), outFile, false);
if (target.packager.platformSpecificBuildOptions.signAndEditExecutable !== false) {
return promise.then(() => target.packager.sign(outFile));
const promise = (0, builder_util_1.copyFile)(elevatePath, outFile, false);
const { signAndEditExecutable, signExecutable } = target.packager.platformSpecificBuildOptions;
if (signAndEditExecutable !== false && signExecutable !== false) {
return promise.then(() => target.packager.signIf(outFile));
}
return promise;
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,18 @@
import type { Defines } from "./Defines";
/**
* Validates makensis stdout/stderr after a zero-exit run.
*
* NSIS can emit "Error:" lines on stderr and still exit 0 (e.g. disk-full
* scenarios where the OS silently drops writes). Also checks the
* "Install data: <written> / <expected> bytes" progress line for truncation.
*/
export declare function checkMakensisOutput(stdout: string, stderr: string): void;
/**
* Verifies the generated installer is at least as large as the sum of the
* embedded archive(s). An installer smaller than its payload is definitively
* truncated regardless of the makensis exit code.
*
* Only runs when APP_64/APP_32/APP_ARM64 defines are present (i.e. normal,
* non-portable installers). Skipped for the intermediate uninstaller build.
*/
export declare function verifyInstallerSize(outFile: string, defines: Defines): Promise<void>;

View file

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkMakensisOutput = checkMakensisOutput;
exports.verifyInstallerSize = verifyInstallerSize;
const builder_util_1 = require("builder-util");
/**
* Validates makensis stdout/stderr after a zero-exit run.
*
* NSIS can emit "Error:" lines on stderr and still exit 0 (e.g. disk-full
* scenarios where the OS silently drops writes). Also checks the
* "Install data: <written> / <expected> bytes" progress line for truncation.
*/
function checkMakensisOutput(stdout, stderr) {
const errorLines = stderr
.split("\n")
.map(l => l.trim())
.filter(l => /^Error:/i.test(l));
if (errorLines.length > 0) {
throw new builder_util_1.ExecError("makensis", 0, stdout, stderr);
}
}
/**
* Verifies the generated installer is at least as large as the sum of the
* embedded archive(s). An installer smaller than its payload is definitively
* truncated regardless of the makensis exit code.
*
* Only runs when APP_64/APP_32/APP_ARM64 defines are present (i.e. normal,
* non-portable installers). Skipped for the intermediate uninstaller build.
*/
async function verifyInstallerSize(outFile, defines) {
let archiveSize = 0;
for (const key of ["APP_64", "APP_32", "APP_ARM64"]) {
const p = defines[key];
if (typeof p === "string") {
const s = await (0, builder_util_1.statOrNull)(p);
if (s != null) {
archiveSize += s.size;
}
}
}
if (archiveSize === 0) {
return;
}
const outStat = await (0, builder_util_1.statOrNull)(outFile);
if (outStat == null) {
throw new Error(`Generated installer was not created at "${outFile}" — output may be incomplete. Check available disk space and try again.`);
}
if (outStat.size < archiveSize) {
throw new Error(`Generated installer (${outStat.size} bytes) is smaller than the embedded archive(s) (${archiveSize} bytes) — ` +
`output may be incomplete. Check available disk space and try again.`);
}
}
//# sourceMappingURL=nsisValidation.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"nsisValidation.js","sourceRoot":"","sources":["../../../src/targets/nsis/nsisValidation.ts"],"names":[],"mappings":";;AAUA,kDASC;AAUD,kDA0BC;AAvDD,+CAAoD;AAGpD;;;;;;GAMG;AACH,SAAgB,mBAAmB,CAAC,MAAc,EAAE,MAAc;IAChE,MAAM,UAAU,GAAG,MAAM;SACtB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SAClB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAElC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,wBAAS,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACpD,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,mBAAmB,CAAC,OAAe,EAAE,OAAgB;IACzE,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,CAAU,EAAE,CAAC;QAC7D,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;QACtB,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,MAAM,IAAA,yBAAU,EAAC,CAAC,CAAC,CAAA;YAC7B,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;gBACd,WAAW,IAAI,CAAC,CAAC,IAAI,CAAA;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;QACtB,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,IAAA,yBAAU,EAAC,OAAO,CAAC,CAAA;IACzC,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,2CAA2C,OAAO,yEAAyE,CAAC,CAAA;IAC9I,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,GAAG,WAAW,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,wBAAwB,OAAO,CAAC,IAAI,oDAAoD,WAAW,YAAY;YAC7G,qEAAqE,CACxE,CAAA;IACH,CAAC;AACH,CAAC","sourcesContent":["import { ExecError, statOrNull } from \"builder-util\"\nimport type { Defines } from \"./Defines\"\n\n/**\n * Validates makensis stdout/stderr after a zero-exit run.\n *\n * NSIS can emit \"Error:\" lines on stderr and still exit 0 (e.g. disk-full\n * scenarios where the OS silently drops writes). Also checks the\n * \"Install data: <written> / <expected> bytes\" progress line for truncation.\n */\nexport function checkMakensisOutput(stdout: string, stderr: string): void {\n const errorLines = stderr\n .split(\"\\n\")\n .map(l => l.trim())\n .filter(l => /^Error:/i.test(l))\n\n if (errorLines.length > 0) {\n throw new ExecError(\"makensis\", 0, stdout, stderr)\n }\n}\n\n/**\n * Verifies the generated installer is at least as large as the sum of the\n * embedded archive(s). An installer smaller than its payload is definitively\n * truncated regardless of the makensis exit code.\n *\n * Only runs when APP_64/APP_32/APP_ARM64 defines are present (i.e. normal,\n * non-portable installers). Skipped for the intermediate uninstaller build.\n */\nexport async function verifyInstallerSize(outFile: string, defines: Defines): Promise<void> {\n let archiveSize = 0\n for (const key of [\"APP_64\", \"APP_32\", \"APP_ARM64\"] as const) {\n const p = defines[key]\n if (typeof p === \"string\") {\n const s = await statOrNull(p)\n if (s != null) {\n archiveSize += s.size\n }\n }\n }\n\n if (archiveSize === 0) {\n return\n }\n\n const outStat = await statOrNull(outFile)\n if (outStat == null) {\n throw new Error(`Generated installer was not created at \"${outFile}\" — output may be incomplete. Check available disk space and try again.`)\n }\n if (outStat.size < archiveSize) {\n throw new Error(\n `Generated installer (${outStat.size} bytes) is smaller than the embedded archive(s) (${archiveSize} bytes) — ` +\n `output may be incomplete. Check available disk space and try again.`\n )\n }\n}\n"]}

View file

@ -1,15 +1,30 @@
import { Arch } from "builder-util";
import { PkgOptions } from "../options/pkgOptions";
import { Nullish } from "builder-util-runtime";
import { Identity } from "../codeSign/macCodeSign";
import { Target } from "../core";
import MacPackager from "../macPackager";
import { MacPackager } from "../macPackager";
import { PkgOptions } from "../options/pkgOptions";
export declare class PkgTarget extends Target {
private readonly packager;
readonly outDir: string;
readonly options: PkgOptions;
constructor(packager: MacPackager, outDir: string);
build(appPath: string, arch: Arch): Promise<any>;
private getExtraPackages;
private customizeDistributionConfiguration;
private buildComponentPackage;
}
export declare function prepareProductBuildArgs(identity: Identity | null, keychain: string | null | undefined): Array<string>;
export declare function prepareProductBuildArgs(identity: Identity | null, keychain: string | Nullish): Array<string>;
/**
* Resolves the version string to pass as `--version` to pkgbuild.
* Reads CFBundleShortVersionString from the app bundle's Info.plist (what pkgbuild
* previously inferred automatically), falling back to the appInfo version.
*/
export declare function resolvePkgBuildVersion(appPath: string, fallback: string): Promise<string>;
/**
* Resolves the scripts directory path for pkgbuild.
* Returns null when scripts is explicitly set to null (disabled).
* Returns a custom path when scripts is a non-empty string.
* Falls back to the default "pkg-scripts" directory otherwise.
*/
export declare function resolveScriptsDir(buildResourcesDir: string, scripts: string | null | undefined): string | null;

View file

@ -1,20 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.prepareProductBuildArgs = exports.PkgTarget = void 0;
exports.PkgTarget = void 0;
exports.prepareProductBuildArgs = prepareProductBuildArgs;
exports.resolvePkgBuildVersion = resolvePkgBuildVersion;
exports.resolveScriptsDir = resolveScriptsDir;
const builder_util_1 = require("builder-util");
const fs_1 = require("builder-util/out/fs");
const appBuilder_1 = require("../util/appBuilder");
const license_1 = require("../util/license");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const path = require("path");
const appInfo_1 = require("../appInfo");
const macCodeSign_1 = require("../codeSign/macCodeSign");
const core_1 = require("../core");
const plist_1 = require("../util/plist");
const license_1 = require("../util/license");
const certType = "Developer ID Installer";
// Maps electron-builder Arch to Apple's architecture names for productbuild requirements plist
function archToAppleArchitectures(arch) {
switch (arch) {
case builder_util_1.Arch.arm64:
return ["arm64"];
case builder_util_1.Arch.x64:
return ["x86_64"];
case builder_util_1.Arch.universal:
return ["arm64", "x86_64"];
case builder_util_1.Arch.ia32:
return ["i386"];
default:
return ["arm64", "x86_64"];
}
}
// http://www.shanekirk.com/2013/10/creating-flat-packages-in-osx/
// to use --scripts, we must build .app bundle separately using pkgbuild
// productbuild --scripts doesn't work (because scripts in this case not added to our package)
// https://github.com/electron-userland/electron-osx-sign/issues/96#issuecomment-274986942
// https://github.com/electron-userland/@electron/osx-sign/issues/96#issuecomment-274986942
class PkgTarget extends core_1.Target {
constructor(packager, outDir) {
super("pkg");
@ -34,7 +52,7 @@ class PkgTarget extends core_1.Target {
// pkg doesn't like not ASCII symbols (Could not open package to list files: /Volumes/test/t-gIjdGK/test-project-0/dist/Test App ßW-1.1.0.pkg)
const artifactName = packager.expandArtifactNamePattern(options, "pkg", arch);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
await packager.info.emitArtifactBuildStarted({
targetPresentableName: "pkg",
file: artifactPath,
arch,
@ -43,11 +61,12 @@ class PkgTarget extends core_1.Target {
const appOutDir = this.outDir;
// https://developer.apple.com/library/content/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
const distInfoFile = path.join(appOutDir, "distribution.xml");
const innerPackageFile = path.join(appOutDir, `${appInfo_1.filterCFBundleIdentifier(appInfo.id)}.pkg`);
const componentPropertyListFile = path.join(appOutDir, `${appInfo_1.filterCFBundleIdentifier(appInfo.id)}.plist`);
const extraPackages = this.getExtraPackages();
const innerPackageFile = path.join(appOutDir, `${(0, appInfo_1.filterCFBundleIdentifier)(appInfo.id)}.pkg`);
const componentPropertyListFile = path.join(appOutDir, `${(0, appInfo_1.filterCFBundleIdentifier)(appInfo.id)}.plist`);
const identity = (await Promise.all([
macCodeSign_1.findIdentity(certType, options.identity || packager.platformSpecificBuildOptions.identity, keychainFile),
this.customizeDistributionConfiguration(distInfoFile, appPath),
(0, macCodeSign_1.findIdentity)(certType, options.identity || packager.platformSpecificBuildOptions.identity, keychainFile),
this.customizeDistributionConfiguration(distInfoFile, appPath, extraPackages, arch),
this.buildComponentPackage(appPath, componentPropertyListFile, innerPackageFile),
]))[0];
if (identity == null && packager.forceCodeSigning) {
@ -55,20 +74,75 @@ class PkgTarget extends core_1.Target {
}
const args = prepareProductBuildArgs(identity, keychainFile);
args.push("--distribution", distInfoFile);
if (extraPackages) {
args.push("--package-path", extraPackages.packagePath);
}
args.push(artifactPath);
builder_util_1.use(options.productbuild, it => args.push(...it));
await builder_util_1.exec("productbuild", args, {
(0, builder_util_1.use)(options.productbuild, it => args.push(...it));
await (0, builder_util_1.exec)("productbuild", args, {
cwd: appOutDir,
});
await Promise.all([promises_1.unlink(innerPackageFile), promises_1.unlink(distInfoFile)]);
await packager.dispatchArtifactCreated(artifactPath, this, arch, packager.computeSafeArtifactName(artifactName, "pkg", arch));
await Promise.all([(0, promises_1.unlink)(innerPackageFile), (0, promises_1.unlink)(distInfoFile)]);
await packager.helper.notarizeIfProvided(artifactPath);
await packager.info.emitArtifactBuildCompleted({
file: artifactPath,
target: this,
arch,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "pkg", arch),
packager,
});
}
async customizeDistributionConfiguration(distInfoFile, appPath) {
await builder_util_1.exec("productbuild", ["--synthesize", "--component", appPath, distInfoFile], {
getExtraPackages() {
const extraPkgsDir = this.options.extraPkgsDir;
if (extraPkgsDir == null) {
return null;
}
const packagePath = path.join(this.packager.info.buildResourcesDir, extraPkgsDir);
let files;
try {
files = (0, fs_1.readdirSync)(packagePath);
}
catch (e) {
if (e.code === "ENOENT") {
return null;
}
else {
throw e;
}
}
const packages = files.filter(file => file.endsWith(".pkg"));
if (packages.length === 0) {
return null;
}
return { packagePath, packages };
}
async customizeDistributionConfiguration(distInfoFile, appPath, extraPackages, arch) {
const options = this.options;
// Build requirements plist for productbuild to generate correct hostArchitectures and allowed-os-versions
// This is the Apple-recommended way to specify architecture and OS requirements
// See: man productbuild, section "PRE-INSTALL REQUIREMENTS PROPERTY LIST"
const requirements = {};
// Set architecture based on build target - productbuild will generate correct hostArchitectures in distribution XML
// On macOS Big Sur+, productbuild defaults to both arm64 and x86_64 unless we specify otherwise
requirements.arch = archToAppleArchitectures(arch);
// Set minimum OS version - productbuild will generate allowed-os-versions in distribution XML
const minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
if (minimumSystemVersion != null) {
requirements.os = [minimumSystemVersion];
}
const requirementsPlistFile = await this.packager.info.tempDirManager.getTempFile({ suffix: ".plist", prefix: "productbuild-requirements" });
await (0, plist_1.savePlistFile)(requirementsPlistFile, requirements);
const args = ["--synthesize", "--product", requirementsPlistFile, "--component", appPath];
if (extraPackages) {
extraPackages.packages.forEach(pkg => {
args.push("--package", path.join(extraPackages.packagePath, pkg));
});
}
args.push(distInfoFile);
await (0, builder_util_1.exec)("productbuild", args, {
cwd: this.outDir,
});
const options = this.options;
let distInfo = await promises_1.readFile(distInfoFile, "utf-8");
let distInfo = await (0, promises_1.readFile)(distInfoFile, "utf-8");
if (options.mustClose != null && options.mustClose.length !== 0) {
const startContent = ` <pkg-ref id="${this.packager.appInfo.id}">\n <must-close>\n`;
const endContent = " </must-close>\n </pkg-ref>\n</installer-gui-script>";
@ -98,7 +172,7 @@ class PkgTarget extends core_1.Target {
if (welcome != null) {
distInfo = distInfo.substring(0, insertIndex) + ` <welcome file="${welcome}"/>\n` + distInfo.substring(insertIndex);
}
const license = await license_1.getNotLocalizedLicenseFile(options.license, this.packager);
const license = await (0, license_1.getNotLocalizedLicenseFile)(options.license, this.packager);
if (license != null) {
distInfo = distInfo.substring(0, insertIndex) + ` <license file="${license}"/>\n` + distInfo.substring(insertIndex);
}
@ -106,18 +180,20 @@ class PkgTarget extends core_1.Target {
if (conclusion != null) {
distInfo = distInfo.substring(0, insertIndex) + ` <conclusion file="${conclusion}"/>\n` + distInfo.substring(insertIndex);
}
builder_util_1.debug(distInfo);
await promises_1.writeFile(distInfoFile, distInfo);
(0, builder_util_1.debug)(distInfo);
await (0, promises_1.writeFile)(distInfoFile, distInfo);
}
async buildComponentPackage(appPath, propertyListOutputFile, packageOutputFile) {
var _a;
const options = this.options;
const rootPath = path.dirname(appPath);
// first produce a component plist template
await builder_util_1.exec("pkgbuild", ["--analyze", "--root", rootPath, propertyListOutputFile]);
await (0, builder_util_1.exec)("pkgbuild", ["--analyze", "--root", rootPath, propertyListOutputFile]);
// process the template plist
const plistInfo = (await appBuilder_1.executeAppBuilderAsJson(["decode-plist", "-f", propertyListOutputFile]))[0].filter((it) => it.RootRelativeBundlePath !== "Electron.dSYM");
const plistInfo = (await (0, plist_1.parsePlistFile)(propertyListOutputFile)).filter((it) => it.RootRelativeBundlePath !== "Electron.dSYM");
let packageInfo = {};
if (plistInfo.length > 0) {
const packageInfo = plistInfo[0];
packageInfo = plistInfo[0];
// ChildBundles lists all of electron binaries within the .app.
// There is no particular reason for removing that key, except to be as close as possible to
// the PackageInfo generated by previous versions of electron-builder.
@ -134,29 +210,31 @@ class PkgTarget extends core_1.Target {
if (options.overwriteAction != null) {
packageInfo.BundleOverwriteAction = options.overwriteAction;
}
await appBuilder_1.executeAppBuilderAndWriteJson(["encode-plist"], { [propertyListOutputFile]: plistInfo });
}
// now build the package
const args = [
"--root",
rootPath,
// "--identifier", this.packager.appInfo.id,
"--component-plist",
propertyListOutputFile,
];
builder_util_1.use(this.options.installLocation || "/Applications", it => args.push("--install-location", it));
if (options.scripts != null) {
args.push("--scripts", path.resolve(this.packager.info.buildResourcesDir, options.scripts));
const args = ["--root", rootPath, "--identifier", this.packager.appInfo.id, "--component-plist", propertyListOutputFile];
(0, builder_util_1.use)(this.options.installLocation || "/Applications", it => args.push("--install-location", it));
// Pass the version explicitly — macOS 15+ pkgbuild no longer infers it from the bundle
const pkgVersion = await resolvePkgBuildVersion(appPath, this.packager.appInfo.version);
args.push("--version", pkgVersion);
const scriptsDir = resolveScriptsDir(this.packager.info.buildResourcesDir, options.scripts);
if (scriptsDir && ((_a = (await (0, builder_util_1.statOrNull)(scriptsDir))) === null || _a === void 0 ? void 0 : _a.isDirectory())) {
const dirContents = (0, fs_1.readdirSync)(scriptsDir);
dirContents.forEach(name => {
if (name.includes("preinstall")) {
packageInfo.BundlePreInstallScriptPath = name;
}
else if (name.includes("postinstall")) {
packageInfo.BundlePostInstallScriptPath = name;
}
});
args.push("--scripts", scriptsDir);
}
else if (options.scripts !== null) {
const dir = path.join(this.packager.info.buildResourcesDir, "pkg-scripts");
const stat = await fs_1.statOrNull(dir);
if (stat != null && stat.isDirectory()) {
args.push("--scripts", dir);
}
if (plistInfo.length > 0) {
await (0, plist_1.savePlistFile)(propertyListOutputFile, plistInfo);
}
args.push(packageOutputFile);
await builder_util_1.exec("pkgbuild", args);
await (0, builder_util_1.exec)("pkgbuild", args);
}
}
exports.PkgTarget = PkgTarget;
@ -170,5 +248,32 @@ function prepareProductBuildArgs(identity, keychain) {
}
return args;
}
exports.prepareProductBuildArgs = prepareProductBuildArgs;
/**
* Resolves the version string to pass as `--version` to pkgbuild.
* Reads CFBundleShortVersionString from the app bundle's Info.plist (what pkgbuild
* previously inferred automatically), falling back to the appInfo version.
*/
async function resolvePkgBuildVersion(appPath, fallback) {
const infoPlistPath = path.join(appPath, "Contents", "Info.plist");
try {
const plist = await (0, plist_1.parsePlistFile)(infoPlistPath);
const version = plist.CFBundleShortVersionString;
return typeof version === "string" && version.length > 0 ? version : fallback;
}
catch {
return fallback;
}
}
/**
* Resolves the scripts directory path for pkgbuild.
* Returns null when scripts is explicitly set to null (disabled).
* Returns a custom path when scripts is a non-empty string.
* Falls back to the default "pkg-scripts" directory otherwise.
*/
function resolveScriptsDir(buildResourcesDir, scripts) {
if (scripts === null) {
return null;
}
return scripts != null ? path.resolve(buildResourcesDir, scripts) : path.join(buildResourcesDir, "pkg-scripts");
}
//# sourceMappingURL=pkg.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,17 +0,0 @@
import { Arch } from "builder-util";
import { Target } from "../core";
import { LinuxPackager } from "../linuxPackager";
import { SnapOptions } from "../options/SnapOptions";
import { LinuxTargetHelper } from "./LinuxTargetHelper";
export default class SnapTarget extends Target {
private readonly packager;
private readonly helper;
readonly outDir: string;
readonly options: SnapOptions;
isUseTemplateApp: boolean;
constructor(name: string, packager: LinuxPackager, helper: LinuxTargetHelper, outDir: string);
private replaceDefault;
private createDescriptor;
build(appOutDir: string, arch: Arch): Promise<any>;
private isElectronVersionGreaterOrEqualThan;
}

View file

@ -1,267 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const fs_extra_1 = require("fs-extra");
const js_yaml_1 = require("js-yaml");
const path = require("path");
const semver = require("semver");
const core_1 = require("../core");
const pathManager_1 = require("../util/pathManager");
const targetUtil_1 = require("./targetUtil");
const defaultPlugs = ["desktop", "desktop-legacy", "home", "x11", "wayland", "unity7", "browser-support", "network", "gsettings", "audio-playback", "pulseaudio", "opengl"];
class SnapTarget extends core_1.Target {
constructor(name, packager, helper, outDir) {
super(name);
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
this.options = { ...this.packager.platformSpecificBuildOptions, ...this.packager.config[this.name] };
this.isUseTemplateApp = false;
}
replaceDefault(inList, defaultList) {
const result = builder_util_1.replaceDefault(inList, defaultList);
if (result !== defaultList) {
this.isUseTemplateApp = false;
}
return result;
}
async createDescriptor(arch) {
if (!this.isElectronVersionGreaterOrEqualThan("4.0.0")) {
if (!this.isElectronVersionGreaterOrEqualThan("2.0.0-beta.1")) {
throw new builder_util_1.InvalidConfigurationError("Electron 2 and higher is required to build Snap");
}
builder_util_1.log.warn("Electron 4 and higher is highly recommended for Snap");
}
const appInfo = this.packager.appInfo;
const snapName = this.packager.executableName.toLowerCase();
const options = this.options;
const plugs = normalizePlugConfiguration(this.options.plugs);
const plugNames = this.replaceDefault(plugs == null ? null : Object.getOwnPropertyNames(plugs), defaultPlugs);
const slots = normalizePlugConfiguration(this.options.slots);
const buildPackages = builder_util_runtime_1.asArray(options.buildPackages);
const defaultStagePackages = getDefaultStagePackages();
const stagePackages = this.replaceDefault(options.stagePackages, defaultStagePackages);
this.isUseTemplateApp =
this.options.useTemplateApp !== false &&
(arch === builder_util_1.Arch.x64 || arch === builder_util_1.Arch.armv7l) &&
buildPackages.length === 0 &&
isArrayEqualRegardlessOfSort(stagePackages, defaultStagePackages);
const appDescriptor = {
command: "command.sh",
plugs: plugNames,
adapter: "none",
};
const snap = js_yaml_1.load(await fs_extra_1.readFile(path.join(pathManager_1.getTemplatePath("snap"), "snapcraft.yaml"), "utf-8"));
if (this.isUseTemplateApp) {
delete appDescriptor.adapter;
}
if (options.grade != null) {
snap.grade = options.grade;
}
if (options.confinement != null) {
snap.confinement = options.confinement;
}
if (options.appPartStage != null) {
snap.parts.app.stage = options.appPartStage;
}
if (options.layout != null) {
snap.layout = options.layout;
}
if (slots != null) {
appDescriptor.slots = Object.getOwnPropertyNames(slots);
for (const slotName of appDescriptor.slots) {
const slotOptions = slots[slotName];
if (slotOptions == null) {
continue;
}
if (!snap.slots) {
snap.slots = {};
}
snap.slots[slotName] = slotOptions;
}
}
builder_util_1.deepAssign(snap, {
name: snapName,
version: appInfo.version,
title: options.title || appInfo.productName,
summary: options.summary || appInfo.productName,
compression: options.compression,
description: this.helper.getDescription(options),
architectures: [builder_util_1.toLinuxArchString(arch, "snap")],
apps: {
[snapName]: appDescriptor,
},
parts: {
app: {
"stage-packages": stagePackages,
},
},
});
if (options.autoStart) {
appDescriptor.autostart = `${snap.name}.desktop`;
}
if (options.confinement === "classic") {
delete appDescriptor.plugs;
delete snap.plugs;
}
else {
const archTriplet = archNameToTriplet(arch);
appDescriptor.environment = {
DISABLE_WAYLAND: options.allowNativeWayland ? "" : "1",
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
LD_LIBRARY_PATH: [
"$SNAP_LIBRARY_PATH",
"$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/" + archTriplet + ":$SNAP/usr/lib/" + archTriplet,
"$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib",
"$SNAP/lib/" + archTriplet + ":$SNAP/usr/lib/" + archTriplet,
].join(":"),
...options.environment,
};
if (plugs != null) {
for (const plugName of plugNames) {
const plugOptions = plugs[plugName];
if (plugOptions == null) {
continue;
}
snap.plugs[plugName] = plugOptions;
}
}
}
if (buildPackages.length > 0) {
snap.parts.app["build-packages"] = buildPackages;
}
if (options.after != null) {
snap.parts.app.after = options.after;
}
if (options.assumes != null) {
snap.assumes = builder_util_runtime_1.asArray(options.assumes);
}
return snap;
}
async build(appOutDir, arch) {
const packager = this.packager;
const options = this.options;
// tslint:disable-next-line:no-invalid-template-strings
const artifactName = packager.expandArtifactNamePattern(this.options, "snap", arch, "${name}_${version}_${arch}.${ext}", false);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.callArtifactBuildStarted({
targetPresentableName: "snap",
file: artifactPath,
arch,
});
const snap = await this.createDescriptor(arch);
const stageDir = await targetUtil_1.createStageDirPath(this, packager, arch);
const snapArch = builder_util_1.toLinuxArchString(arch, "snap");
const args = ["snap", "--app", appOutDir, "--stage", stageDir, "--arch", snapArch, "--output", artifactPath, "--executable", this.packager.executableName];
await this.helper.icons;
if (this.helper.maxIconPath != null) {
if (!this.isUseTemplateApp) {
snap.icon = "snap/gui/icon.png";
}
args.push("--icon", this.helper.maxIconPath);
}
// snapcraft.yaml inside a snap directory
const snapMetaDir = path.join(stageDir, this.isUseTemplateApp ? "meta" : "snap");
const desktopFile = path.join(snapMetaDir, "gui", `${snap.name}.desktop`);
await this.helper.writeDesktopEntry(this.options, packager.executableName + " %U", desktopFile, {
// tslint:disable:no-invalid-template-strings
Icon: "${SNAP}/meta/gui/icon.png",
});
if (this.isElectronVersionGreaterOrEqualThan("5.0.0") && !isBrowserSandboxAllowed(snap)) {
args.push("--extraAppArgs=--no-sandbox");
if (this.isUseTemplateApp) {
args.push("--exclude", "chrome-sandbox");
}
}
if (snap.compression != null) {
args.push("--compression", snap.compression);
}
if (this.isUseTemplateApp) {
// remove fields that are valid in snapcraft.yaml, but not snap.yaml
const fieldsToStrip = ["compression", "contact", "donation", "issues", "parts", "source-code", "website"];
for (const field of fieldsToStrip) {
delete snap[field];
}
}
if (packager.packagerOptions.effectiveOptionComputed != null && (await packager.packagerOptions.effectiveOptionComputed({ snap, desktopFile, args }))) {
return;
}
await fs_extra_1.outputFile(path.join(snapMetaDir, this.isUseTemplateApp ? "snap.yaml" : "snapcraft.yaml"), builder_util_1.serializeToYaml(snap));
const hooksDir = await packager.getResource(options.hooks, "snap-hooks");
if (hooksDir != null) {
args.push("--hooks", hooksDir);
}
if (this.isUseTemplateApp) {
args.push("--template-url", `electron4:${snapArch}`);
}
await builder_util_1.executeAppBuilder(args);
await packager.info.callArtifactBuildCompleted({
file: artifactPath,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "snap", arch, false),
target: this,
arch,
packager,
publishConfig: options.publish == null ? { provider: "snapStore" } : null,
});
}
isElectronVersionGreaterOrEqualThan(version) {
return semver.gte(this.packager.config.electronVersion || "7.0.0", version);
}
}
exports.default = SnapTarget;
function archNameToTriplet(arch) {
switch (arch) {
case builder_util_1.Arch.x64:
return "x86_64-linux-gnu";
case builder_util_1.Arch.ia32:
return "i386-linux-gnu";
case builder_util_1.Arch.armv7l:
// noinspection SpellCheckingInspection
return "arm-linux-gnueabihf";
case builder_util_1.Arch.arm64:
return "aarch64-linux-gnu";
default:
throw new Error(`Unsupported arch ${arch}`);
}
}
function isArrayEqualRegardlessOfSort(a, b) {
a = a.slice();
b = b.slice();
a.sort();
b.sort();
return a.length === b.length && a.every((value, index) => value === b[index]);
}
function normalizePlugConfiguration(raw) {
if (raw == null) {
return null;
}
const result = {};
for (const item of Array.isArray(raw) ? raw : [raw]) {
if (typeof item === "string") {
result[item] = null;
}
else {
Object.assign(result, item);
}
}
return result;
}
function isBrowserSandboxAllowed(snap) {
if (snap.plugs != null) {
for (const plugName of Object.keys(snap.plugs)) {
const plug = snap.plugs[plugName];
if (plug.interface === "browser-support" && plug["allow-sandbox"] === true) {
return true;
}
}
}
return false;
}
function getDefaultStagePackages() {
// libxss1 - was "error while loading shared libraries: libXss.so.1" on Xubuntu 16.04
// noinspection SpellCheckingInspection
return ["libnspr4", "libnss3", "libxss1", "libappindicator3-1", "libsecret-1-0"];
}
//# sourceMappingURL=snap.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,36 @@
import { Arch } from "builder-util";
import { SnapStoreOptions } from "builder-util-runtime";
import { Configuration } from "../../configuration";
import { Target } from "../../core";
import { LinuxPackager } from "../../linuxPackager";
import { SnapcraftOptions, SnapOptions } from "../../options/SnapOptions";
import { LinuxTargetHelper } from "../LinuxTargetHelper";
import { SnapcraftYAML } from "./snapcraft";
/** Abstract base for all snap build strategies (core24, legacy core18/20/22, custom pass-through). */
export declare abstract class SnapCore<T> {
protected readonly packager: LinuxPackager;
protected readonly helper: LinuxTargetHelper;
protected readonly options: T;
protected abstract defaultPlugs: Array<string>;
constructor(packager: LinuxPackager, helper: LinuxTargetHelper, options: T);
abstract createDescriptor(arch: Arch): Promise<SnapcraftYAML>;
abstract buildSnap(params: {
snap: SnapcraftYAML;
appOutDir: string;
stageDir: string;
snapArch: Arch;
artifactPath: string;
}): Promise<void>;
}
/** Snap build target — merges `snapcraft` (preferred) and legacy `snap` config, then delegates to the appropriate `SnapCore` strategy. */
export default class SnapTarget extends Target {
protected readonly packager: LinuxPackager;
protected readonly helper: LinuxTargetHelper;
readonly outDir: string;
readonly options: SnapcraftOptions | SnapOptions;
constructor(name: string, packager: LinuxPackager, helper: LinuxTargetHelper, outDir: string);
build(appOutDir: string, arch: Arch): Promise<any>;
protected findSnapPublishConfig(config?: Configuration): SnapStoreOptions | null;
private findSnapPublishConfigInPublishNode;
private isSnapStoreOptions;
}

View file

@ -0,0 +1,105 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapCore = void 0;
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const path = require("path");
const core_1 = require("../../core");
const targetUtil_1 = require("../targetUtil");
/** Abstract base for all snap build strategies (core24, legacy core18/20/22, custom pass-through). */
class SnapCore {
constructor(packager, helper, options) {
this.packager = packager;
this.helper = helper;
this.options = options;
}
}
exports.SnapCore = SnapCore;
/** Snap build target — merges `snapcraft` (preferred) and legacy `snap` config, then delegates to the appropriate `SnapCore` strategy. */
class SnapTarget extends core_1.Target {
constructor(name, packager, helper, outDir) {
var _a;
super(name);
this.packager = packager;
this.helper = helper;
this.outDir = outDir;
const { config: { snapcraft, snap }, platformSpecificBuildOptions, } = packager;
this.options = (0, builder_util_runtime_1.deepAssign)({}, platformSpecificBuildOptions, (_a = snapcraft !== null && snapcraft !== void 0 ? snapcraft : snap) !== null && _a !== void 0 ? _a : {});
}
async build(appOutDir, arch) {
const packager = this.packager;
// tslint:disable-next-line:no-invalid-template-strings
const artifactName = packager.expandArtifactNamePattern(this.options, "snap", arch, "${name}_${version}_${arch}.${ext}", false);
const artifactPath = path.join(this.outDir, artifactName);
await packager.info.emitArtifactBuildStarted({
targetPresentableName: "snap",
file: artifactPath,
arch,
});
const core = this.helper.getSnapCore();
const snap = await core.createDescriptor(arch);
builder_util_1.log.debug({ snap }, "snapcraft.yaml descriptor created");
await core.buildSnap({
snap,
appOutDir,
stageDir: await (0, targetUtil_1.createStageDirPath)(this, packager, arch),
snapArch: arch,
artifactPath,
});
const publishConfig = this.findSnapPublishConfig(packager.config);
await packager.info.emitArtifactBuildCompleted({
file: artifactPath,
safeArtifactName: packager.computeSafeArtifactName(artifactName, "snap", arch, false),
target: this,
arch,
packager,
publishConfig,
});
}
findSnapPublishConfig(config) {
var _a, _b;
const fallback = { provider: "snapStore" };
if (!config) {
return fallback;
}
const snapConfig = (_a = config.snapcraft) !== null && _a !== void 0 ? _a : config.snap;
if (snapConfig === null || snapConfig === void 0 ? void 0 : snapConfig.publish) {
return this.findSnapPublishConfigInPublishNode(snapConfig.publish);
}
if ((_b = config.linux) === null || _b === void 0 ? void 0 : _b.publish) {
const configCandidate = this.findSnapPublishConfigInPublishNode(config.linux.publish);
if (configCandidate) {
return configCandidate;
}
}
if (config.publish) {
const configCandidate = this.findSnapPublishConfigInPublishNode(config.publish);
if (configCandidate) {
return configCandidate;
}
}
return fallback;
}
findSnapPublishConfigInPublishNode(configPublishNode) {
if (!configPublishNode) {
return null;
}
if (Array.isArray(configPublishNode)) {
for (const configObj of configPublishNode) {
if (this.isSnapStoreOptions(configObj)) {
return configObj;
}
}
}
if (typeof configPublishNode === `object` && this.isSnapStoreOptions(configPublishNode)) {
return configPublishNode;
}
return null;
}
isSnapStoreOptions(configPublishNode) {
const snapStoreOptionsCandidate = configPublishNode;
return (snapStoreOptionsCandidate === null || snapStoreOptionsCandidate === void 0 ? void 0 : snapStoreOptionsCandidate.provider) === `snapStore`;
}
}
exports.default = SnapTarget;
//# sourceMappingURL=SnapTarget.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,52 @@
import { Arch } from "builder-util";
import { PlugDescriptor, SlotDescriptor, SnapOptions24 } from "../../options/SnapOptions";
import { SnapCore } from "./SnapTarget";
import { SnapcraftYAML } from "./snapcraft";
import { Nullish } from "builder-util-runtime";
/** Snap build strategy for core24 — generates a native snapcraft.yaml and invokes the snapcraft CLI. */
export declare class SnapCore24 extends SnapCore<SnapOptions24> {
defaultPlugs: string[];
readonly configRelativePath = "snap";
readonly guiRelativePath: string;
createDescriptor(arch: Arch): Promise<SnapcraftYAML>;
private isHostMode;
/** Writes the snapcraft.yaml, stages app files, then invokes `buildSnap()` to run the actual snapcraft build. */
buildSnap(params: {
snap: SnapcraftYAML;
appOutDir: string;
stageDir: string;
snapArch: Arch;
artifactPath: string;
}): Promise<void>;
/** Converts `SnapOptions24` into a fully resolved `SnapcraftYAML` descriptor for the given architecture. */
mapSnapOptionsToSnapcraftYAML(arch: Arch): Promise<SnapcraftYAML>;
/**
* Build environment variables with proper defaults
*/
private buildEnvironment;
/**
* Build default layout for core24 with GNOME platform content snaps (non-extension mode)
* This allows the app to access libraries from the gnome-46-2404 and mesa-2404 content snaps
*/
private buildDefaultLayout;
/**
* Process hooks directory into hook definitions
*/
private processHooks;
/**
* Normalize assumes list (can be string or array)
*/
normalizeAssumesList(assumes: Array<string> | string | Nullish): string[] | undefined;
/**
* Process plugs or slots into root-level definitions and app-level references
*/
processPlugOrSlots<T extends Array<string | SlotDescriptor | PlugDescriptor> | SlotDescriptor | PlugDescriptor | null>(items: T): {
root: Record<string, unknown> | undefined;
app: string[] | undefined;
};
private isBrowserSandboxAllowed;
/**
* Expand "default" keyword in arrays of anything
*/
private expandDefaultsInArray;
}

View file

@ -0,0 +1,449 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapCore24 = void 0;
const builder_util_1 = require("builder-util");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const SnapTarget_1 = require("./SnapTarget");
const snapcraftBuilder_1 = require("./snapcraftBuilder");
const yaml = require("js-yaml");
const builder_util_runtime_1 = require("builder-util-runtime");
/** Snap build strategy for core24 — generates a native snapcraft.yaml and invokes the snapcraft CLI. */
class SnapCore24 extends SnapTarget_1.SnapCore {
constructor() {
super(...arguments);
// browser-support is intentionally absent here; it is auto-injected in mapSnapOptionsToSnapcraftYAML
// when the user has not provided custom plugs, so it always lands in both root plugs and app plugs.
this.defaultPlugs = ["desktop", "desktop-legacy", "home", "x11", "wayland", "unity7", "network", "gsettings", "audio-playback", "pulseaudio", "opengl"];
// Snap file hierarchy:
// - snap/gui/ gets automatically copied to meta/gui/ in the final snap
// - Desktop files in meta/gui/ are used for menu integration
this.configRelativePath = "snap";
this.guiRelativePath = path.join(this.configRelativePath, "gui");
}
async createDescriptor(arch) {
return await this.mapSnapOptionsToSnapcraftYAML(arch);
}
isHostMode() {
return this.options.useDestructiveMode === true;
}
/** Writes the snapcraft.yaml, stages app files, then invokes `buildSnap()` to run the actual snapcraft build. */
async buildSnap(params) {
const { snap, appOutDir, stageDir, artifactPath } = params;
const snapDirResolved = path.resolve(stageDir, this.configRelativePath);
const snapcraftYamlPath = path.join(snapDirResolved, "snapcraft.yaml");
// Create snap/gui directory for desktop files and icons
// Snapcraft will automatically copy snap/gui/ contents to meta/gui/ in the final snap
const guiOutput = path.resolve(stageDir, this.guiRelativePath);
await (0, fs_extra_1.mkdir)(guiOutput, { recursive: true });
const yamlContent = yaml.dump(snap, snapcraftBuilder_1.SNAPCRAFT_YAML_OPTIONS);
await (0, fs_extra_1.writeFile)(snapcraftYamlPath, yamlContent, "utf8");
builder_util_1.log.debug(snap, "generated snapcraft.yaml");
// Copy icon to snap/gui/ directory
// Snapcraft will automatically copy this to meta/gui/ in the final snap
const desktopExtraProps = {};
const icon = this.helper.maxIconPath;
if (icon) {
const iconFileName = `${snap.name}${path.extname(icon)}`;
await (0, fs_extra_1.copy)(icon, path.join(guiOutput, iconFileName));
// Icon path will be available at ${SNAP}/meta/gui/<icon-file> after installation
desktopExtraProps.Icon = `\${SNAP}/meta/gui/${iconFileName}`;
}
// Create desktop file in snap/gui/ directory
// Snapcraft will automatically copy this to meta/gui/ in the final snap
const desktopFilePath = path.join(guiOutput, `${this.helper.getDesktopFileName(snap.name)}.desktop`);
await this.helper.writeDesktopEntry(this.options, this.packager.executableName + " %U", desktopFilePath, desktopExtraProps);
// Copy app files to the project root `app` directory so `source: app`
// in the generated `snapcraft.yaml` (which is under `snap/`) can be
// resolved by snapcraft running in the build environment.
const appDir = path.resolve(stageDir, "app");
if (path.resolve(appDir) !== path.resolve(appOutDir)) {
builder_util_1.log.debug({ to: builder_util_1.log.filePath(appDir), from: builder_util_1.log.filePath(appOutDir) }, "copying app files to project root app directory");
await (0, builder_util_1.copyDir)(appOutDir, appDir);
}
// Auto-generate `organize` mapping for the app part so top-level helper
// binaries and resources are placed under `app/` inside the snap. Update
// the already-written `snapcraft.yaml` so the build sees the mapping.
try {
const appPart = snap.parts[snap.name];
if (appPart) {
const entries = (await (0, fs_extra_1.readdir)(appOutDir)).sort();
const organize = appPart.organize || {};
for (const entry of entries) {
if (!entry) {
continue;
}
if (organize[entry]) {
continue;
}
organize[entry] = `app/${entry}`;
}
appPart.organize = organize;
const updatedYaml = yaml.dump(snap, snapcraftBuilder_1.SNAPCRAFT_YAML_OPTIONS);
await (0, fs_extra_1.writeFile)(snapcraftYamlPath, updatedYaml, "utf8");
builder_util_1.log.debug({ organize }, "updated snapcraft.yaml with organize mapping");
}
}
catch (e) {
builder_util_1.log.warn({ error: e.message }, "failed to generate and update organize mapping");
}
const buildMode = {
useLXD: this.options.useLXD === true,
useMultipass: this.options.useMultipass === true,
useDestructiveMode: this.options.useDestructiveMode === true,
remoteBuild: this.options.remoteBuild || undefined,
};
if (this.packager.packagerOptions.effectiveOptionComputed != null) {
const shouldSkip = await this.packager.packagerOptions.effectiveOptionComputed({ snap, ...buildMode });
if (shouldSkip) {
return;
}
}
const rootOptions = this.packager.config.snapcraft;
await (0, snapcraftBuilder_1.buildSnap)({
snapcraftConfig: snap,
artifactPath,
stageDir,
packager: this.packager,
cscLink: rootOptions === null || rootOptions === void 0 ? void 0 : rootOptions.cscLink,
...buildMode,
});
}
/** Converts `SnapOptions24` into a fully resolved `SnapcraftYAML` descriptor for the given architecture. */
async mapSnapOptionsToSnapcraftYAML(arch) {
var _a, _b, _c, _d;
const appInfo = this.packager.appInfo;
const appName = this.packager.executableName.toLowerCase();
const options = this.options;
// Default to ["gnome"] in normal builds; no extensions in host/destructive-mode (where the
// gnome extension is incompatible). Throw if the user explicitly includes "gnome" in host mode.
const hostMode = this.isHostMode();
const extensionsList = options.extensions != null ? [...options.extensions] : hostMode ? [] : ["gnome"];
if (hostMode && extensionsList.includes("gnome")) {
throw new builder_util_1.InvalidConfigurationError(`The "gnome" snapcraft extension is incompatible with host/destructive-mode builds.\n` +
`In this mode snapcraft cannot resolve the extension's command-chain source ` +
`(/usr/share/snapcraft/extensions/desktop/command-chain) and will fail.\n\n` +
`To resolve this, choose one of:\n` +
` 1. Remove "gnome" from snapcraft.core24.extensions (or set it to []) and add any\n` +
` required stage-packages manually.\n` +
` 2. Switch to an isolated build environment by setting snapcraft.core24.useLXD: true\n` +
` or snapcraft.core24.useMultipass: true instead of useDestructiveMode.\n\n` +
`See: https://snapcraft.io/docs/gnome-extension`);
}
const resolvedExtensions = extensionsList.length > 0 ? extensionsList : undefined;
const useGnomeExtension = extensionsList.includes("gnome");
// Create the app part
const appPart = {
plugin: "dump",
source: "app",
"build-packages": ((_a = options.buildPackages) === null || _a === void 0 ? void 0 : _a.length) ? options.buildPackages : undefined,
"stage-packages": this.expandDefaultsInArray(options.stagePackages, snapcraftBuilder_1.DEFAULT_STAGE_PACKAGES),
after: this.expandDefaultsInArray(options.after, []),
stage: ((_b = options.appPartStage) === null || _b === void 0 ? void 0 : _b.length) ? options.appPartStage : undefined,
};
// Process plugs and slots
// When using GNOME extension, we don't need to manually configure content snaps
// The extension will handle: gnome-46-2404, gtk-3-themes, icon-themes, sound-themes
let rootPlugs;
let appPlugs;
if (useGnomeExtension) {
// With GNOME extension, only process user-provided custom plugs
const result = options.plugs ? this.processPlugOrSlots(options.plugs) : { root: undefined, app: undefined };
rootPlugs = result.root;
// Extension automatically adds common plugs, so we only add custom ones
appPlugs = result.app;
}
else {
// Without GNOME extension, we need manual content snaps
const defaultRootPlugs = {
"gtk-3-themes": {
interface: "content",
target: "$SNAP/data-dir/themes",
"default-provider": "gtk-common-themes",
},
"icon-themes": {
interface: "content",
target: "$SNAP/data-dir/icons",
"default-provider": "gtk-common-themes",
},
"sound-themes": {
interface: "content",
target: "$SNAP/data-dir/sounds",
"default-provider": "gtk-common-themes",
},
"gnome-46-2404": {
interface: "content",
target: "$SNAP/gnome-platform",
"default-provider": "gnome-46-2404",
},
"gpu-2404": {
interface: "content",
target: "$SNAP/gpu-2404",
"default-provider": "mesa-2404",
},
};
const result = options.plugs
? this.processPlugOrSlots(options.plugs)
: hostMode
? { root: undefined, app: this.defaultPlugs }
: {
root: defaultRootPlugs,
app: this.defaultPlugs,
};
rootPlugs = result.root;
appPlugs = result.app;
}
// Always add browser-support with allow-sandbox so Chromium's internal sandbox
// can create user namespaces under strict confinement. Without allow-sandbox: true
// the app crashes immediately with "FATAL: Permission denied (13)" in credentials.cc.
// Skip the injection only when the user has explicitly provided their own plugs
// (they are responsible for including browser-support in that case).
if (!options.plugs) {
rootPlugs = { ...rootPlugs, "browser-support": { interface: "browser-support", "allow-sandbox": true } };
if (!(appPlugs === null || appPlugs === void 0 ? void 0 : appPlugs.includes("browser-support"))) {
appPlugs = [...(appPlugs !== null && appPlugs !== void 0 ? appPlugs : []), "browser-support"];
}
}
const { root: rootSlots, app: appSlots } = options.slots ? this.processPlugOrSlots(options.slots) : { root: undefined, app: undefined };
// Build the effective arg list for the snap command.
// Start with any user-supplied executableArgs, then conditionally add --no-sandbox
// if browser-support with allow-sandbox:true is not present in the resolved plugs
// (mirrors the same logic in SnapCoreLegacy.buildSnap).
const extraArgs = [...((_c = this.options.executableArgs) !== null && _c !== void 0 ? _c : [])];
if (this.options.forceX11 === true) {
if (!extraArgs.includes("--ozone-platform=x11")) {
extraArgs.push("--ozone-platform=x11");
}
}
if (this.helper.isElectronVersionGreaterOrEqualThan("5.0.0") && !this.isBrowserSandboxAllowed(rootPlugs)) {
if (!extraArgs.includes("--no-sandbox")) {
extraArgs.push("--no-sandbox");
}
}
const commandSuffix = extraArgs.length > 0 ? ` ${extraArgs.join(" ")}` : "";
// Create the app configuration
const desktopBaseName = this.helper.getDesktopFileName(appName);
const app = {
command: `app/${this.packager.executableName}${commandSuffix}`,
"command-chain": undefined, // explicitly undefined so removeNullish strips it; extensions supply their own command-chain
plugs: appPlugs,
slots: appSlots,
autostart: options.autoStart ? `${desktopBaseName}.desktop` : undefined,
desktop: `meta/gui/${desktopBaseName}.desktop`,
extensions: resolvedExtensions,
};
// Icon path — build-time relative path so snapcraft can find the file in snap/gui/
await this.helper.icons;
const iconPath = this.helper.maxIconPath != null ? `snap/gui/${appName}${path.extname(this.helper.maxIconPath)}` : undefined;
// Process hooks if configured
const hooksConfig = options.hooks;
const hooks = hooksConfig ? await this.processHooks(hooksConfig) : undefined;
// Parts configuration - the extension automatically adds a gnome/sdk part
// Don't manually add desktop-launch when using the extension
const parts = {
[appName]: appPart,
};
// Note: `organize` will be generated later in `buildSnap` based on the
// actual contents of the built app directory so helper binaries and
// resources are automatically moved under `app/` in the snap.
// Build the snapcraft configuration
const snapcraft = {
// Required fields
name: appName,
base: "core24",
confinement: options.confinement || "strict",
parts: parts,
// Architecture/Platform — only needed for cross-compilation; snapcraft 8
// defaults to host arch and snapcraft 7 rejects this field entirely.
...(arch !== (0, builder_util_1.archFromString)(process.arch)
? {
platforms: {
[(0, builder_util_1.toLinuxArchString)(arch, "snap")]: {
"build-for": (0, builder_util_1.toLinuxArchString)(arch, "snap"),
"build-on": (0, builder_util_1.toLinuxArchString)((0, builder_util_1.archFromString)(process.arch), "snap"),
},
},
}
: {}),
// Metadata - with fallbacks from appInfo
version: appInfo.version,
summary: options.summary || appInfo.productName,
description: this.helper.getDescription(options),
grade: options.grade || "stable",
title: options.title || appInfo.productName,
icon: iconPath,
// Build configuration
compression: options.compression || undefined,
assumes: this.normalizeAssumesList(options.assumes),
// Environment
environment: this.buildEnvironment(options),
// User-supplied layout always wins. Without gnome extension and not in host mode, fall back to content-snap defaults.
layout: (_d = options.layout) !== null && _d !== void 0 ? _d : (useGnomeExtension || hostMode ? undefined : this.buildDefaultLayout(options)),
// Interfaces
plugs: rootPlugs,
slots: rootSlots,
// Hooks
hooks: hooks,
// Apps
apps: {
[appName]: app,
},
};
return (0, builder_util_1.removeNullish)(snapcraft);
}
/**
* Build environment variables with proper defaults
*/
buildEnvironment(options) {
var _a;
const env = {};
// Add default TMPDIR for Electron/Chromium apps
if (!((_a = options.environment) === null || _a === void 0 ? void 0 : _a.TMPDIR)) {
env.TMPDIR = "$XDG_RUNTIME_DIR";
}
if (options.environment) {
(0, builder_util_runtime_1.deepAssign)(env, options.environment);
}
return Object.keys(env).length > 0 ? env : undefined;
}
/**
* Build default layout for core24 with GNOME platform content snaps (non-extension mode)
* This allows the app to access libraries from the gnome-46-2404 and mesa-2404 content snaps
*/
buildDefaultLayout(options) {
// If user provides custom layout, use that instead
if (options.layout) {
return options.layout;
}
// Default layout for core24 Electron apps using GNOME content snaps WITHOUT extension
return {
"/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkit2gtk-4.0": {
bind: "$SNAP/gnome-platform/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkit2gtk-4.0",
},
"/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkit2gtk-4.1": {
bind: "$SNAP/gnome-platform/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/webkit2gtk-4.1",
},
"/usr/share/xml/iso-codes": {
bind: "$SNAP/gnome-platform/usr/share/xml/iso-codes",
},
"/usr/share/libdrm": {
bind: "$SNAP/gpu-2404/libdrm",
},
"/usr/share/drirc.d": {
symlink: "$SNAP/gpu-2404/drirc.d",
},
};
}
/**
* Process hooks directory into hook definitions
*/
async processHooks(hooksPath) {
try {
const buildResourcesDir = this.packager.buildResourcesDir;
const hooksDir = path.resolve(buildResourcesDir, hooksPath);
if (!hooksDir.startsWith(buildResourcesDir + path.sep) && hooksDir !== buildResourcesDir) {
throw new builder_util_1.InvalidConfigurationError(`snapcraft.core24.hooks must resolve within the build resources directory (got "${hooksDir}")`);
}
const hookFiles = await (0, fs_extra_1.readdir)(hooksDir);
if (hookFiles.length === 0) {
return undefined;
}
const hooks = {};
for (const hookFile of hookFiles) {
const hookName = path.basename(hookFile, path.extname(hookFile));
if (!(0, builder_util_runtime_1.isValidKey)(hookName)) {
throw new builder_util_1.InvalidConfigurationError(`Invalid hook name: ${hookName}`);
}
hooks[hookName] = {
// Hook definitions will be populated by snapcraft from the files
// Just register that these hooks exist
};
}
return hooks;
}
catch (e) {
builder_util_1.log.error({ message: e.message }, "error processing Snap hooks directory");
throw e;
}
}
/**
* Normalize assumes list (can be string or array)
*/
normalizeAssumesList(assumes) {
if (!assumes) {
return undefined;
}
if (typeof assumes === "string") {
return [assumes];
}
return assumes.length > 0 ? assumes : undefined;
}
/**
* Process plugs or slots into root-level definitions and app-level references
*/
processPlugOrSlots(items) {
if (!items || (Array.isArray(items) && items.length === 0)) {
return { root: undefined, app: undefined };
}
const root = {};
const app = [];
// Handle single descriptor object
if (!Array.isArray(items)) {
Object.entries(items).forEach(([name, config]) => {
if (!(0, builder_util_runtime_1.isValidKey)(name)) {
throw new Error(`Invalid plug/slot name: ${name}`);
}
root[name] = config;
app.push(name);
});
return { root, app };
}
// Handle array - support "default" keyword
const processedItems = this.expandDefaultsInArray(items, this.defaultPlugs);
for (const item of processedItems !== null && processedItems !== void 0 ? processedItems : []) {
if (typeof item === "string") {
// Simple string reference
app.push(item);
}
else {
// Descriptor object with configuration
Object.entries(item).forEach(([name, config]) => {
if (!(0, builder_util_runtime_1.isValidKey)(name)) {
throw new Error(`Invalid plug/slot name: ${name}`);
}
root[name] = config;
app.push(name);
});
}
}
return { root: Object.keys(root).length > 0 ? root : undefined, app: app.length > 0 ? app : undefined };
}
isBrowserSandboxAllowed(plugs) {
if (!plugs) {
return false;
}
for (const plug of Object.values(plugs)) {
if ((plug === null || plug === void 0 ? void 0 : plug.interface) === "browser-support" && plug["allow-sandbox"] === true) {
return true;
}
}
return false;
}
/**
* Expand "default" keyword in arrays of anything
*/
expandDefaultsInArray(items, defaults) {
const result = [];
for (const item of items !== null && items !== void 0 ? items : []) {
if (typeof item === "string" && item === "default") {
result.push(...defaults);
}
else {
result.push(item);
}
}
return result.length > 0 ? result : undefined;
}
}
exports.SnapCore24 = SnapCore24;
//# sourceMappingURL=core24.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,28 @@
import { Arch } from "builder-util";
import { SnapOptionsCustom } from "../../options/SnapOptions";
import { SnapCore } from "./SnapTarget";
import { SnapcraftYAML } from "./snapcraft";
/**
* Pass-through snap builder for `base: "custom"`.
*
* electron-builder reads the file at `snapcraft.custom.yaml` (or the inline object),
* writes it into the stage directory, and invokes snapcraft. **Nothing is injected or
* modified in any way** no plugs, extensions, organize mappings, desktop files,
* environment variables, layout entries, or stage packages. `linux.*` configuration
* is also not cascaded into the descriptor.
*
* Because electron-builder exerts no control over the descriptor's content, GitHub
* issue support for snap runtime problems encountered with custom yaml files is limited.
* Prefer a structured base (`core24`, `core22`, etc.) for a fully managed build.
*/
export declare class SnapCoreCustom extends SnapCore<SnapOptionsCustom> {
readonly defaultPlugs: string[];
createDescriptor(_arch: Arch): Promise<SnapcraftYAML>;
buildSnap(params: {
snap: SnapcraftYAML;
appOutDir: string;
stageDir: string;
snapArch: Arch;
artifactPath: string;
}): Promise<void>;
}

View file

@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapCoreCustom = void 0;
const builder_util_1 = require("builder-util");
const fs_extra_1 = require("fs-extra");
const yaml = require("js-yaml");
const path = require("path");
const SnapTarget_1 = require("./SnapTarget");
const snapcraftBuilder_1 = require("./snapcraftBuilder");
/**
* Pass-through snap builder for `base: "custom"`.
*
* electron-builder reads the file at `snapcraft.custom.yaml` (or the inline object),
* writes it into the stage directory, and invokes snapcraft. **Nothing is injected or
* modified in any way** no plugs, extensions, organize mappings, desktop files,
* environment variables, layout entries, or stage packages. `linux.*` configuration
* is also not cascaded into the descriptor.
*
* Because electron-builder exerts no control over the descriptor's content, GitHub
* issue support for snap runtime problems encountered with custom yaml files is limited.
* Prefer a structured base (`core24`, `core22`, etc.) for a fully managed build.
*/
class SnapCoreCustom extends SnapTarget_1.SnapCore {
constructor() {
super(...arguments);
this.defaultPlugs = [];
}
async createDescriptor(_arch) {
const { yaml: yamlPath } = this.options;
if (!yamlPath) {
throw new builder_util_1.InvalidConfigurationError('snapcraft.base = "custom" requires an entry in snapcraft.custom.yaml (either a path to a snapcraft.yaml file or a SnapcraftYAML object directly in the configuration)');
}
if (typeof yamlPath !== "string") {
return yamlPath; // fully defined SnapcraftYAML object provided directly in configuration, no file reading necessary
}
const resolved = path.resolve(this.packager.buildResourcesDir, yamlPath);
const buildResourcesDir = this.packager.buildResourcesDir;
if (!resolved.startsWith(buildResourcesDir + path.sep) && resolved !== buildResourcesDir) {
throw new builder_util_1.InvalidConfigurationError(`snapcraft.custom.yaml must resolve within the build resources directory (got "${resolved}")`);
}
const raw = await (0, fs_extra_1.readFile)(resolved, "utf8");
return yaml.load(raw);
}
async buildSnap(params) {
const { snap, stageDir, artifactPath } = params;
const snapDirResolved = path.resolve(stageDir, "snap");
const snapcraftYamlPath = path.join(snapDirResolved, "snapcraft.yaml");
const yamlContent = yaml.dump(snap, snapcraftBuilder_1.SNAPCRAFT_YAML_OPTIONS);
await (0, fs_extra_1.outputFile)(snapcraftYamlPath, yamlContent, "utf8");
builder_util_1.log.debug(snap, "using custom snapcraft.yaml (pass-through, no injection)");
if (this.packager.packagerOptions.effectiveOptionComputed != null && (await this.packager.packagerOptions.effectiveOptionComputed({ snap }))) {
return;
}
await (0, snapcraftBuilder_1.buildSnap)({
snapcraftConfig: snap,
artifactPath,
stageDir,
packager: this.packager,
});
}
}
exports.SnapCoreCustom = SnapCoreCustom;
//# sourceMappingURL=coreCustom.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"coreCustom.js","sourceRoot":"","sources":["../../../src/targets/snap/coreCustom.ts"],"names":[],"mappings":";;;AAAA,+CAAmE;AACnE,uCAA+C;AAC/C,gCAA+B;AAC/B,6BAA4B;AAE5B,6CAAuC;AAEvC,yDAAsE;AAEtE;;;;;;;;;;;;GAYG;AACH,MAAa,cAAe,SAAQ,qBAA2B;IAA/D;;QACW,iBAAY,GAAa,EAAE,CAAA;IA0CtC,CAAC;IAxCC,KAAK,CAAC,gBAAgB,CAAC,KAAW;QAChC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,OAAO,CAAA;QACvC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,wCAAyB,CACjC,uKAAuK,CACxK,CAAA;QACH,CAAC;QACD,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,QAAQ,CAAA,CAAC,mGAAmG;QACrH,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAA;QACxE,MAAM,iBAAiB,GAAG,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAA;QACzD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,iBAAiB,EAAE,CAAC;YACzF,MAAM,IAAI,wCAAyB,CAAC,iFAAiF,QAAQ,IAAI,CAAC,CAAA;QACpI,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,IAAA,mBAAQ,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAkB,CAAA;IACxC,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,MAA0G;QACxH,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,CAAA;QAE/C,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QACtD,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,gBAAgB,CAAC,CAAA;QAEtE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,yCAAsB,CAAC,CAAA;QAC3D,MAAM,IAAA,qBAAU,EAAC,iBAAiB,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;QACxD,kBAAG,CAAC,KAAK,CAAC,IAAI,EAAE,0DAA0D,CAAC,CAAA;QAE3E,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,uBAAuB,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,uBAAuB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;YAC7I,OAAM;QACR,CAAC;QAED,MAAM,IAAA,4BAAS,EAAC;YACd,eAAe,EAAE,IAAI;YACrB,YAAY;YACZ,QAAQ;YACR,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC,CAAA;IACJ,CAAC;CACF;AA3CD,wCA2CC","sourcesContent":["import { Arch, InvalidConfigurationError, log } from \"builder-util\"\nimport { outputFile, readFile } from \"fs-extra\"\nimport * as yaml from \"js-yaml\"\nimport * as path from \"path\"\nimport { SnapOptionsCustom } from \"../../options/SnapOptions\"\nimport { SnapCore } from \"./SnapTarget\"\nimport { SnapcraftYAML } from \"./snapcraft\"\nimport { buildSnap, SNAPCRAFT_YAML_OPTIONS } from \"./snapcraftBuilder\"\n\n/**\n * Pass-through snap builder for `base: \"custom\"`.\n *\n * electron-builder reads the file at `snapcraft.custom.yaml` (or the inline object),\n * writes it into the stage directory, and invokes snapcraft. **Nothing is injected or\n * modified in any way** — no plugs, extensions, organize mappings, desktop files,\n * environment variables, layout entries, or stage packages. `linux.*` configuration\n * is also not cascaded into the descriptor.\n *\n * Because electron-builder exerts no control over the descriptor's content, GitHub\n * issue support for snap runtime problems encountered with custom yaml files is limited.\n * Prefer a structured base (`core24`, `core22`, etc.) for a fully managed build.\n */\nexport class SnapCoreCustom extends SnapCore<SnapOptionsCustom> {\n readonly defaultPlugs: string[] = []\n\n async createDescriptor(_arch: Arch): Promise<SnapcraftYAML> {\n const { yaml: yamlPath } = this.options\n if (!yamlPath) {\n throw new InvalidConfigurationError(\n 'snapcraft.base = \"custom\" requires an entry in snapcraft.custom.yaml (either a path to a snapcraft.yaml file or a SnapcraftYAML object directly in the configuration)'\n )\n }\n if (typeof yamlPath !== \"string\") {\n return yamlPath // fully defined SnapcraftYAML object provided directly in configuration, no file reading necessary\n }\n const resolved = path.resolve(this.packager.buildResourcesDir, yamlPath)\n const buildResourcesDir = this.packager.buildResourcesDir\n if (!resolved.startsWith(buildResourcesDir + path.sep) && resolved !== buildResourcesDir) {\n throw new InvalidConfigurationError(`snapcraft.custom.yaml must resolve within the build resources directory (got \"${resolved}\")`)\n }\n const raw = await readFile(resolved, \"utf8\")\n return yaml.load(raw) as SnapcraftYAML\n }\n\n async buildSnap(params: { snap: SnapcraftYAML; appOutDir: string; stageDir: string; snapArch: Arch; artifactPath: string }): Promise<void> {\n const { snap, stageDir, artifactPath } = params\n\n const snapDirResolved = path.resolve(stageDir, \"snap\")\n const snapcraftYamlPath = path.join(snapDirResolved, \"snapcraft.yaml\")\n\n const yamlContent = yaml.dump(snap, SNAPCRAFT_YAML_OPTIONS)\n await outputFile(snapcraftYamlPath, yamlContent, \"utf8\")\n log.debug(snap, \"using custom snapcraft.yaml (pass-through, no injection)\")\n\n if (this.packager.packagerOptions.effectiveOptionComputed != null && (await this.packager.packagerOptions.effectiveOptionComputed({ snap }))) {\n return\n }\n\n await buildSnap({\n snapcraftConfig: snap,\n artifactPath,\n stageDir,\n packager: this.packager,\n })\n }\n}\n"]}

View file

@ -0,0 +1,43 @@
import { Arch } from "builder-util";
import { SnapOptions } from "../../options/SnapOptions";
import { SnapCore } from "./SnapTarget";
import { SnapcraftYAML } from "./snapcraft";
export declare class SnapCoreLegacy extends SnapCore<SnapOptions> {
private isUseTemplateApp;
defaultPlugs: string[];
private replaceDefault;
createDescriptor(arch: Arch): Promise<SnapcraftYAML>;
buildSnap(props: {
snap: any;
appOutDir: string;
stageDir: string;
snapArch: Arch;
artifactPath: string;
}): Promise<void>;
private buildWithTemplate;
private buildWithoutTemplate;
private stageSnapFiles;
private normalizePlugConfiguration;
private isBrowserSandboxAllowed;
private archNameToTriplet;
}
/** Single-quote a shell argument, escaping any embedded single quotes. */
export declare function shellQuote(arg: string): string;
/**
* Builds the content of command.sh for a snap package.
*
* For both template and no-template builds, the desktop-integration scripts are
* sourced from the snap root ($SNAP):
* - Template: scripts are embedded in the template tarball at the snap root.
* - No-template: the snapcraft.yaml `launch-scripts` part uses `plugin: dump,
* source: scripts`, which stages stageDir/scripts/ contents directly into the
* snap root so $SNAP/desktop-init.sh is correct in both cases.
*
* The only difference between template and no-template is the app executable prefix:
* template apps are at $SNAP/<name>; no-template apps are at $SNAP/app/<name>.
*/
export declare function buildCommandShContent(opts: {
isTemplate: boolean;
executableName: string;
extraAppArgs: string[];
}): string;

View file

@ -0,0 +1,416 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapCoreLegacy = void 0;
exports.shellQuote = shellQuote;
exports.buildCommandShContent = buildCommandShContent;
const builder_util_1 = require("builder-util");
const builder_util_runtime_1 = require("builder-util-runtime");
const promises_1 = require("fs/promises");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const linux_1 = require("../../toolsets/linux");
const electronGet_1 = require("../../util/electronGet");
const pathManager_1 = require("../../util/pathManager");
const LibUiFramework_1 = require("../../frameworks/LibUiFramework");
const SnapTarget_1 = require("./SnapTarget");
const snapcraftBuilder_1 = require("./snapcraftBuilder");
const js_yaml_1 = require("js-yaml");
// Snap template release info from electron-userland/electron-builder-binaries
const SNAP_TEMPLATES = {
amd64: {
releaseName: "snap-template-4.0-2",
filenameWithExt: "snap-template-electron-4.0-2-amd64.tar.7z",
checksums: { "snap-template-electron-4.0-2-amd64.tar.7z": "5e3ab4e09364ac06f0072b1c2dab9138318c933f6b2c7374f893b5ec44d19e6f" },
},
armhf: {
releaseName: "snap-template-4.0-1",
filenameWithExt: "snap-template-electron-4.0-1-armhf.tar.7z",
checksums: { "snap-template-electron-4.0-1-armhf.tar.7z": "6f7553e904f4e043bc3019f0899d05e01a283b00b61fec22e932296490e3be6b" },
},
};
// Handles core18/core20/core22 snaps via mksquashfs (template) or snapcraft CLI (no-template).
// See: https://github.com/develar/app-builder/blob/master/pkg/package-format/snap
class SnapCoreLegacy extends SnapTarget_1.SnapCore {
constructor() {
super(...arguments);
this.isUseTemplateApp = false;
this.defaultPlugs = ["desktop", "desktop-legacy", "home", "x11", "wayland", "unity7", "browser-support", "network", "gsettings", "audio-playback", "pulseaudio", "opengl"];
}
replaceDefault(inList, defaultList) {
const result = (0, builder_util_1.replaceDefault)(inList, defaultList);
if (result !== defaultList) {
this.isUseTemplateApp = false;
}
return result;
}
async createDescriptor(arch) {
const appInfo = this.packager.appInfo;
const snapName = this.packager.executableName.toLowerCase();
const options = this.options;
const plugs = this.normalizePlugConfiguration(this.options.plugs);
const plugNames = this.replaceDefault(plugs == null ? null : Object.getOwnPropertyNames(plugs), this.defaultPlugs);
const slots = this.normalizePlugConfiguration(this.options.slots);
const buildPackages = (0, builder_util_runtime_1.asArray)(options.buildPackages);
const stagePackages = this.replaceDefault(options.stagePackages, snapcraftBuilder_1.DEFAULT_STAGE_PACKAGES);
const stageSet = new Set(stagePackages);
const stageMatchesDefaults = stagePackages.length === snapcraftBuilder_1.DEFAULT_STAGE_PACKAGES.length && snapcraftBuilder_1.DEFAULT_STAGE_PACKAGES.every((p) => stageSet.has(p));
// Template app is only available for x64/armv7l, and only when no packages are customised.
this.isUseTemplateApp = this.options.useTemplateApp !== false && (arch === builder_util_1.Arch.x64 || arch === builder_util_1.Arch.armv7l) && buildPackages.length === 0 && stageMatchesDefaults;
const appDescriptor = {
command: "command.sh",
plugs: plugNames,
adapter: "none",
};
const snap = (0, js_yaml_1.load)(await (0, fs_extra_1.readFile)(path.join((0, pathManager_1.getTemplatePath)("snap"), "snapcraft.yaml"), "utf-8"));
if (this.isUseTemplateApp) {
delete appDescriptor.adapter;
}
if (options.base != null) {
snap.base = options.base;
if (Number(snap.base.split("core")[1]) >= 22) {
delete appDescriptor.adapter;
}
}
if (options.grade != null) {
snap.grade = options.grade;
}
if (options.confinement != null) {
snap.confinement = options.confinement;
}
if (options.appPartStage != null) {
snap.parts.app.stage = options.appPartStage;
}
if (options.layout != null) {
snap.layout = options.layout;
}
if (slots != null) {
appDescriptor.slots = Object.getOwnPropertyNames(slots);
for (const slotName of appDescriptor.slots) {
if (!(0, builder_util_runtime_1.isValidKey)(slotName)) {
throw new Error(`Invalid plug/slot name: ${slotName}`);
}
const slotOptions = slots[slotName];
if (slotOptions == null) {
continue;
}
if (!snap.slots) {
snap.slots = {};
}
snap.slots[slotName] = slotOptions;
}
}
(0, builder_util_runtime_1.deepAssign)(snap, {
name: snapName,
version: appInfo.version,
title: options.title || appInfo.productName,
summary: options.summary || appInfo.productName,
compression: options.compression,
description: this.helper.getDescription(options),
architectures: [(0, builder_util_1.toLinuxArchString)(arch, "snap")],
apps: {
[snapName]: appDescriptor,
},
parts: {
app: {
"stage-packages": stagePackages,
},
},
});
if (options.autoStart) {
appDescriptor.autostart = `${this.helper.getDesktopFileName(snap.name)}.desktop`;
}
if (options.confinement === "classic") {
delete appDescriptor.plugs;
delete snap.plugs;
}
else {
const archTriplet = this.archNameToTriplet(arch);
const environment = {
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
LD_LIBRARY_PATH: [
"$SNAP_LIBRARY_PATH",
"$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/" + archTriplet + ":$SNAP/usr/lib/" + archTriplet,
"$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib",
"$SNAP/lib/" + archTriplet + ":$SNAP/usr/lib/" + archTriplet,
].join(":"),
...options.environment,
};
const allow = options.allowNativeWayland;
const isOldElectron = !this.helper.isElectronVersionGreaterOrEqualThan("38.0.0", "7.0.0");
if ((allow == null && isOldElectron) || allow === false) {
environment.DISABLE_WAYLAND = "1";
}
appDescriptor.environment = environment;
if (plugs != null) {
for (const plugName of plugNames) {
if (!(0, builder_util_runtime_1.isValidKey)(plugName)) {
throw new Error(`Invalid plug/slot name: ${plugName}`);
}
const plugOptions = plugs[plugName];
if (plugOptions == null) {
continue;
}
if (!snap.plugs) {
snap.plugs = {};
}
snap.plugs[plugName] = plugOptions;
}
}
}
if (buildPackages.length > 0) {
snap.parts.app["build-packages"] = buildPackages;
}
if (options.after != null) {
snap.parts.app.after = options.after;
}
if (options.assumes != null) {
snap.assumes = (0, builder_util_runtime_1.asArray)(options.assumes);
}
return snap;
}
async buildSnap(props) {
var _a, _b;
const { snap, appOutDir, stageDir, snapArch, artifactPath } = props;
// Build the args array for effectiveOptionComputed compatibility — tests inspect this.
const args = [
"snap",
"--app",
appOutDir,
"--stage",
stageDir,
"--arch",
(0, builder_util_1.toLinuxArchString)(snapArch, "snap"),
"--output",
artifactPath,
"--executable",
this.packager.executableName,
];
await this.helper.icons;
if (this.helper.maxIconPath != null) {
if (!this.isUseTemplateApp) {
snap.icon = "snap/gui/icon.png";
}
args.push("--icon", this.helper.maxIconPath);
}
const snapMetaDir = path.join(stageDir, this.isUseTemplateApp ? "meta" : "snap");
const desktopFile = path.join(snapMetaDir, "gui", `${this.helper.getDesktopFileName(snap.name)}.desktop`);
await this.helper.writeDesktopEntry(this.options, this.packager.executableName + " %U", desktopFile, {
Icon: "${SNAP}/meta/gui/icon.png",
});
const extraAppArgs = (_a = this.options.executableArgs) !== null && _a !== void 0 ? _a : [];
if (this.helper.isElectronVersionGreaterOrEqualThan("5.0.0") && !this.isBrowserSandboxAllowed(snap)) {
const noSandboxArg = "--no-sandbox";
if (!extraAppArgs.includes(noSandboxArg)) {
extraAppArgs.push(noSandboxArg);
}
if (this.isUseTemplateApp) {
args.push("--exclude", "chrome-sandbox");
}
}
if (extraAppArgs.length > 0) {
args.push("--extraAppArgs=" + extraAppArgs.join(" "));
}
// Capture compression BEFORE it gets stripped from snap for template builds.
const compression = (_b = this.options.compression) !== null && _b !== void 0 ? _b : "xz";
if (snap.compression != null) {
args.push("--compression", snap.compression);
}
if (this.isUseTemplateApp) {
const fieldsToStrip = ["compression", "contact", "donation", "issues", "parts", "source-code", "website"];
for (const field of fieldsToStrip) {
delete snap[field];
}
}
if (this.packager.packagerOptions.effectiveOptionComputed != null && (await this.packager.packagerOptions.effectiveOptionComputed({ snap, desktopFile, args }))) {
return;
}
await (0, fs_extra_1.outputFile)(path.join(snapMetaDir, this.isUseTemplateApp ? "snap.yaml" : "snapcraft.yaml"), (0, builder_util_1.serializeToYaml)(snap));
const hooksDir = await this.packager.getResource(this.options.hooks, "snap-hooks");
if (hooksDir != null) {
args.push("--hooks", hooksDir);
}
if (this.isUseTemplateApp) {
const templateArch = snapArch === builder_util_1.Arch.x64 ? "amd64" : "armhf";
args.push("--template-url", `electron4:${templateArch}`);
await this.buildWithTemplate({ appOutDir, stageDir, snapArch, artifactPath, compression, hooksDir, extraAppArgs });
}
else {
await this.buildWithoutTemplate({ appOutDir, stageDir, artifactPath, hooksDir, extraAppArgs });
}
}
async buildWithTemplate(opts) {
const { appOutDir, stageDir, snapArch, artifactPath, compression, hooksDir, extraAppArgs } = opts;
const templateArch = snapArch === builder_util_1.Arch.x64 ? "amd64" : "armhf";
const { releaseName, filenameWithExt, checksums } = SNAP_TEMPLATES[templateArch];
builder_util_1.log.info({ releaseName }, "downloading snap template");
const templateDir = await (0, electronGet_1.downloadBuilderToolset)({ releaseName, filenameWithExt, checksums, githubOrgRepo: "electron-userland/electron-builder-binaries" });
await this.stageSnapFiles({ stageDir, appOutDir, hooksDir, extraAppArgs, isTemplate: true });
// Best-effort: remove chrome-sandbox from app dir before mksquashfs scans it.
await (0, promises_1.rm)(path.join(appOutDir, "chrome-sandbox"), { force: true });
// chmod -R g-s to avoid setgid bits in final image
for (const dir of [stageDir, appOutDir, templateDir]) {
await (0, builder_util_1.exec)("chmod", ["-R", "g-s", dir]).catch(err => builder_util_1.log.warn({ error: err.message }, "chmod g-s failed"));
}
const mksquashfsPath = await getMksquashfsPath(snapArch);
// Collect top-level entries from each dir as individual path args (mirrors Go ReadDirContentTo)
const mksquashfsArgs = [
...(await readDirPaths(templateDir)),
...(await readDirPaths(stageDir)),
...(await readDirPaths(appOutDir, name => name !== "LICENSES.chromium.html" && name !== "LICENSE.electron.txt" && name !== "chrome-sandbox")),
artifactPath,
"-no-progress",
"-quiet",
"-noappend",
"-comp",
compression,
"-no-xattrs",
"-no-fragments",
"-all-root",
];
await (0, builder_util_1.exec)(mksquashfsPath, mksquashfsArgs, { cwd: stageDir });
}
async buildWithoutTemplate(opts) {
const { appOutDir, stageDir, artifactPath, hooksDir, extraAppArgs } = opts;
await this.stageSnapFiles({ stageDir, appOutDir, hooksDir, extraAppArgs, isTemplate: false });
// Write desktop integration scripts (embedded in the Go binary, now in our templates)
const snapTemplateDir = (0, pathManager_1.getTemplatePath)("snap");
for (const script of ["desktop-init.sh", "desktop-common.sh", "desktop-gnome-specific.sh"]) {
const src = path.join(snapTemplateDir, script);
const dest = path.join(stageDir, "scripts", script);
await (0, promises_1.copyFile)(src, dest);
// copyFile doesn't preserve mode; chmod 755 explicitly
await (0, promises_1.chmod)(dest, 0o755);
}
// Copy app dir into stage/app/ so snapcraft can pick it up
await (0, builder_util_1.copyDir)(appOutDir, path.join(stageDir, "app"));
// Run snapcraft (legacy `snap` subcommand)
const isDestructiveMode = process.env.SNAP_DESTRUCTIVE_MODE === "true";
const snapOutputName = "out.snap";
const snapArgs = ["snap", "--output", isDestructiveMode ? artifactPath : snapOutputName];
if (isDestructiveMode) {
snapArgs.push("--destructive-mode");
}
await (0, builder_util_1.exec)("snapcraft", snapArgs, {
cwd: stageDir,
env: { ...process.env, SNAPCRAFT_HAS_TTY: "false" },
});
if (!isDestructiveMode) {
await (0, promises_1.rename)(path.join(stageDir, snapOutputName), artifactPath);
}
}
async stageSnapFiles(opts) {
const { stageDir, hooksDir, extraAppArgs, isTemplate } = opts;
const snapMetaDir = path.join(stageDir, isTemplate ? "meta" : "snap");
// No-template builds place command.sh + desktop scripts in scripts/ which snapcraft stages to snap root.
// Template builds write command.sh to the stage root directly; no scripts/ dir is needed.
const scriptDir = path.join(stageDir, "scripts");
if (!isTemplate) {
await (0, promises_1.mkdir)(scriptDir, { recursive: true });
}
if (this.helper.maxIconPath != null) {
const iconDest = path.join(snapMetaDir, "gui", `icon${path.extname(this.helper.maxIconPath)}`);
await (0, promises_1.mkdir)(path.dirname(iconDest), { recursive: true });
await (0, promises_1.copyFile)(this.helper.maxIconPath, iconDest);
}
if (hooksDir != null) {
await (0, builder_util_1.copyDir)(hooksDir, path.join(snapMetaDir, "hooks"));
}
// command.sh — template builds write to stage root; no-template writes to scripts/
const commandWrapperPath = isTemplate ? path.join(stageDir, "command.sh") : path.join(scriptDir, "command.sh");
const commandContent = buildCommandShContent({ isTemplate, executableName: this.packager.executableName, extraAppArgs });
await (0, promises_1.writeFile)(commandWrapperPath, commandContent, { mode: 0o755 });
await (0, promises_1.chmod)(commandWrapperPath, 0o755);
}
normalizePlugConfiguration(raw) {
if (raw == null) {
return null;
}
const result = {};
for (const item of Array.isArray(raw) ? raw : [raw]) {
if (typeof item === "string") {
if (!(0, builder_util_runtime_1.isValidKey)(item)) {
throw new Error(`Invalid plug/slot name: ${item}`);
}
result[item] = null;
}
else {
(0, builder_util_runtime_1.deepAssign)(result, item);
}
}
return result;
}
isBrowserSandboxAllowed(snap) {
if (snap.plugs != null) {
for (const plugName of Object.keys(snap.plugs)) {
const plug = snap.plugs[plugName];
if (plug.interface === "browser-support" && plug["allow-sandbox"] === true) {
return true;
}
}
}
return false;
}
archNameToTriplet(arch) {
switch (arch) {
case builder_util_1.Arch.x64:
return "x86_64-linux-gnu";
case builder_util_1.Arch.ia32:
return "i386-linux-gnu";
case builder_util_1.Arch.armv7l:
return "arm-linux-gnueabihf";
case builder_util_1.Arch.arm64:
return "aarch64-linux-gnu";
default:
throw new Error(`Unsupported arch ${arch}`);
}
}
}
exports.SnapCoreLegacy = SnapCoreLegacy;
async function getMksquashfsPath(arch) {
const envPath = process.env.MKSQUASHFS_PATH;
if (envPath) {
return envPath;
}
const { mksquashfs } = await (0, linux_1.getAppImageTools)("0.0.0", arch);
return mksquashfs;
}
async function readDirPaths(dir, filter) {
const entries = await (0, promises_1.readdir)(dir);
const result = [];
for (const name of entries) {
if (!filter || filter(name)) {
result.push(path.join(dir, name));
}
}
return result;
}
/** Single-quote a shell argument, escaping any embedded single quotes. */
function shellQuote(arg) {
return "'" + arg.replace(/'/g, "'\\''") + "'";
}
/**
* Builds the content of command.sh for a snap package.
*
* For both template and no-template builds, the desktop-integration scripts are
* sourced from the snap root ($SNAP):
* - Template: scripts are embedded in the template tarball at the snap root.
* - No-template: the snapcraft.yaml `launch-scripts` part uses `plugin: dump,
* source: scripts`, which stages stageDir/scripts/ contents directly into the
* snap root so $SNAP/desktop-init.sh is correct in both cases.
*
* The only difference between template and no-template is the app executable prefix:
* template apps are at $SNAP/<name>; no-template apps are at $SNAP/app/<name>.
*/
function buildCommandShContent(opts) {
const { isTemplate, executableName, extraAppArgs } = opts;
(0, LibUiFramework_1.validateShellEmbeddable)(executableName, "executableName");
const appPrefix = isTemplate ? "" : "app/";
let content = `#!/bin/bash -e\nexec "$SNAP/desktop-init.sh" "$SNAP/desktop-common.sh" "$SNAP/desktop-gnome-specific.sh" "$SNAP/${appPrefix}${executableName}"`;
if (extraAppArgs.length > 0) {
content += " " + extraAppArgs.map(shellQuote).join(" ");
}
content += ' "$@"';
return content;
}
//# sourceMappingURL=coreLegacy.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,39 @@
import { LinuxPackager } from "../../linuxPackager";
import { RemoteBuildOptions } from "../../options/SnapOptions";
import { SnapcraftYAML } from "./snapcraft";
export declare const SNAPCRAFT_YAML_OPTIONS: {
readonly indent: 2;
readonly lineWidth: -1;
readonly noRefs: true;
};
export declare const DEFAULT_STAGE_PACKAGES: string[];
interface BuildSnapOptions {
/** The snapcraft YAML configuration */
snapcraftConfig: SnapcraftYAML;
/** Working directory where snapcraft.yaml is written and the build executes */
stageDir: string;
/** Whether to use remote build (builds on Launchpad) */
remoteBuild?: RemoteBuildOptions;
/** Whether to use LXD for local builds */
useLXD?: boolean;
/** Whether to use Multipass for local builds */
useMultipass?: boolean;
/** Whether to use destructive mode (builds directly on host, Linux only) */
useDestructiveMode?: boolean;
/** The snap output path */
artifactPath: string;
/** LinuxPackager instance, used to resolve workspace dir for remote build authentication */
packager: LinuxPackager;
/** Snap Store credentials from SnapcraftOptions root — base64 string or file path */
cscLink?: string;
}
/**
* Builds a snap package from SnapcraftYAML configuration.
*
* `SNAPCRAFT_NO_NETWORK` is intentionally **not** forced to `"1"` here.
* All build modes (destructive-mode, LXD, Multipass, remote) require network
* access to download stage-packages, the base image, and extensions.
* To opt into an offline build, set `SNAPCRAFT_NO_NETWORK=1` in your environment.
*/
export declare function buildSnap(options: BuildSnapOptions): Promise<string>;
export {};

View file

@ -0,0 +1,419 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_STAGE_PACKAGES = exports.SNAPCRAFT_YAML_OPTIONS = void 0;
exports.buildSnap = buildSnap;
const builder_util_1 = require("builder-util");
const childProcess = require("child_process");
const crypto_1 = require("crypto");
const electron_publish_1 = require("electron-publish");
const fs_extra_1 = require("fs-extra");
const path = require("path");
const util = require("util");
const builder_util_runtime_1 = require("builder-util-runtime");
const execAsync = util.promisify(childProcess.exec);
exports.SNAPCRAFT_YAML_OPTIONS = { indent: 2, lineWidth: -1, noRefs: true };
exports.DEFAULT_STAGE_PACKAGES = ["libnspr4", "libnss3", "libxss1", "libappindicator3-1", "libsecret-1-0"];
/**
* Validates snapcraft.yaml using snapcraft's built-in `expand-extensions` command.
* Throws on failure. The caller in `buildSnap` catches this and treats it as a non-fatal warning.
*/
async function validateSnapcraftYamlWithCLI(workDir) {
try {
const { stdout } = await execAsync("snapcraft expand-extensions", {
cwd: workDir,
timeout: 30000,
});
builder_util_1.log.debug({ expandedYaml: stdout }, "validated extended snapcraft.yaml");
}
catch (error) {
builder_util_1.log.error({ error: error.message, stderr: error.stderr }, "snapcraft.yaml validation failed");
throw new Error(`Invalid snapcraft.yaml: ${error.message}\n` +
`Snapcraft output: ${error.stderr || error.stdout || "No output"}\n` +
`Run 'snapcraft expand-extensions' in ${workDir} for more details`);
}
}
/**
* Validates snapcraft.yaml configuration with basic client-side checks
* This is a fast pre-check before running the full CLI validation
*/
function validateSnapcraftConfig(config) {
const errors = [];
const warnings = [];
// Required fields
if (!config.name) {
errors.push("name is required");
}
if (!config.base) {
errors.push("base is required");
}
if (!config.confinement) {
errors.push("confinement is required");
}
if (!config.parts || Object.keys(config.parts).length === 0) {
errors.push("at least one part is required");
}
// Name validation
if (config.name) {
if (!/^[a-z0-9-]*$/.test(config.name)) {
errors.push("name must only contain lowercase letters, numbers, and hyphens");
}
if (config.name.length > 40) {
errors.push("name must be 40 characters or less");
}
if (config.name.startsWith("-") || config.name.endsWith("-")) {
errors.push("name cannot start or end with a hyphen");
}
}
// Parts validation
Object.entries(config.parts).forEach(([partName, part]) => {
if (!part.plugin) {
errors.push(`part '${partName}' missing required 'plugin' field`);
}
});
// Apps validation
if (config.apps) {
Object.entries(config.apps).forEach(([appName, app]) => {
if (!app.command) {
errors.push(`app '${appName}' missing required 'command' field`);
}
});
}
// Summary validation
if (config.summary && config.summary.length > 78) {
warnings.push(`summary is ${config.summary.length} characters (recommended: 78 or less)`);
}
// Log results
if (errors.length > 0) {
builder_util_1.log.error({ errors }, "snapcraft.yaml validation failed");
throw new builder_util_1.InvalidConfigurationError(`Invalid snapcraft.yaml: ${errors.join(", ")}`);
}
if (warnings.length > 0) {
builder_util_1.log.warn({ warnings }, "snapcraft.yaml validation warnings");
}
}
/**
* Retry wrapper for operations that may fail transiently
*/
async function executeWithRetry(fn, options = {}) {
var _a;
const { maxRetries = 3, retryDelay = 5000, retryableErrors = ["network timeout", "connection refused", "temporary failure", "snap store error"] } = options;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
}
catch (error) {
lastError = error;
const errorMessage = ((_a = error.message) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || "";
const isRetryable = retryableErrors.some(pattern => errorMessage.includes(pattern));
if (attempt < maxRetries && isRetryable) {
builder_util_1.log.warn({ attempt, maxRetries, error: error.message, retryIn: retryDelay }, "build failed with retryable error, retrying...");
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
else {
break;
}
}
}
throw lastError;
}
/**
* Cleans up build artifacts
*/
async function cleanupBuildArtifacts(workDir) {
const artifactsToClean = ["parts", "stage", "prime"];
for (const artifact of artifactsToClean) {
const artifactPath = path.join(workDir, artifact);
try {
await (0, fs_extra_1.remove)(artifactPath);
builder_util_1.log.debug({ artifact }, "cleaned build artifact");
}
catch (e) {
builder_util_1.log.debug({ artifact, error: e.message }, "no build artifact to clean");
}
}
try {
const files = await (0, fs_extra_1.readdir)(workDir);
for (const file of files) {
if (file.endsWith(".snap")) {
await (0, fs_extra_1.remove)(path.join(workDir, file));
builder_util_1.log.debug({ file }, "cleaned snap file");
}
}
}
catch (e) {
builder_util_1.log.debug({ error: e.message }, "no snap files to clean");
}
}
async function copySnapToArtifactPath(workDir, outputBasename, outputFileName) {
const snapInWorkDir = path.join(workDir, outputBasename);
if (snapInWorkDir !== outputFileName) {
await (0, fs_extra_1.ensureDir)(path.dirname(outputFileName));
await (0, fs_extra_1.copyFile)(snapInWorkDir, outputFileName);
builder_util_1.log.debug({ from: snapInWorkDir, to: outputFileName }, "copied snap from build dir to artifact path");
}
return outputFileName;
}
/**
* Builds a snap package from SnapcraftYAML configuration.
*
* `SNAPCRAFT_NO_NETWORK` is intentionally **not** forced to `"1"` here.
* All build modes (destructive-mode, LXD, Multipass, remote) require network
* access to download stage-packages, the base image, and extensions.
* To opt into an offline build, set `SNAPCRAFT_NO_NETWORK=1` in your environment.
*/
async function buildSnap(options) {
const { SNAPCRAFT_NO_NETWORK } = process.env;
const { snapcraftConfig, artifactPath, remoteBuild, stageDir, useLXD = false, useMultipass = false, useDestructiveMode = false, cscLink } = options;
const isolatedEnv = {
...(SNAPCRAFT_NO_NETWORK != null ? { SNAPCRAFT_NO_NETWORK } : {}),
};
if (useDestructiveMode) {
isolatedEnv.SNAPCRAFT_BUILD_ENVIRONMENT = "host";
}
if (useLXD && process.platform !== "linux") {
throw new builder_util_1.InvalidConfigurationError(`useLXD is only supported on Linux. On ${process.platform}, use useMultipass or remoteBuild instead.`);
}
if (useDestructiveMode && process.platform !== "linux") {
throw new builder_util_1.InvalidConfigurationError(`useDestructiveMode is only supported on Linux (requires Ubuntu 24.04 host for core24). On ${process.platform}, use useMultipass or remoteBuild instead.`);
}
// Config validation — throws InvalidConfigurationError, no build artifacts exist yet.
validateSnapcraftConfig(snapcraftConfig);
try {
await validateSnapcraftYamlWithCLI(stageDir);
}
catch (validationError) {
builder_util_1.log.warn({ error: validationError.message }, "snapcraft CLI pre-validation failed (non-fatal), continuing build");
}
await ensureSnapcraftInstalled();
// Inject credentials for all build modes from snapcraft.cscLink / SNAP_CSC_LINK.
const credEnv = await (0, electron_publish_1.resolveSnapCredentials)(cscLink, options.packager.buildResourcesDir);
(0, builder_util_runtime_1.deepAssign)(isolatedEnv, credEnv);
if (remoteBuild === null || remoteBuild === void 0 ? void 0 : remoteBuild.enabled) {
// Remote-build auth does additional checks (interactive session, throws on missing creds)
// and overrides any credential already set above.
const authEnv = await ensureRemoteBuildAuthentication(cscLink, options.packager.buildResourcesDir);
(0, builder_util_runtime_1.deepAssign)(isolatedEnv, authEnv);
}
const projectAppDir = path.join(stageDir, "app");
if (!(await (0, fs_extra_1.pathExists)(projectAppDir))) {
throw new builder_util_1.InvalidConfigurationError(`snap build failed: expected app directory not found at ${projectAppDir}`);
}
builder_util_1.log.debug({ appFiles: (await (0, fs_extra_1.readdir)(projectAppDir)).slice(0, 20) }, "app directory contents (truncated)");
if (!(remoteBuild === null || remoteBuild === void 0 ? void 0 : remoteBuild.enabled) && !useLXD && !useMultipass && !useDestructiveMode && process.platform !== "linux") {
throw new builder_util_1.InvalidConfigurationError(`No snap build environment specified for ${process.platform}. Set one of: useMultipass, useLXD (Linux only), useDestructiveMode (Linux only), or remoteBuild.enabled`);
}
// Actual build — only this step can leave partial artifacts that need cleanup.
try {
return await executeWithRetry(() => executeSnapcraftBuild({
workDir: stageDir,
remoteBuild,
outputSnap: artifactPath,
useLXD,
useMultipass,
useDestructiveMode,
isolatedEnv: isolatedEnv,
}), { maxRetries: (remoteBuild === null || remoteBuild === void 0 ? void 0 : remoteBuild.enabled) ? 3 : 1, retryDelay: 10000 });
}
catch (error) {
builder_util_1.log.error({ error: error.message }, "snap build failed");
await cleanupBuildArtifacts(stageDir).catch((cleanupError) => {
builder_util_1.log.warn({ error: cleanupError.message }, "failed to cleanup build artifacts");
});
throw error;
}
}
/**
* Ensures snapcraft is installed on the system
*/
async function ensureSnapcraftInstalled() {
try {
const { stdout } = await execAsync("snapcraft --version");
builder_util_1.log.info({ version: stdout.trim() }, "snapcraft found");
}
catch (error) {
builder_util_1.log.error({ error: error.message }, "snapcraft is not installed");
const platform = process.platform;
if (platform === "linux") {
builder_util_1.log.error(null, "Install with: sudo snap install snapcraft --classic");
}
else if (platform === "darwin") {
builder_util_1.log.error(null, "Install snapcraft with: pip3 install snapcraft");
builder_util_1.log.error(null, "On macOS, useMultipass or remoteBuild are the only supported build modes for core24");
}
else if (platform === "win32") {
builder_util_1.log.error(null, "Install snapcraft via WSL2 or use remote-build");
builder_util_1.log.error(null, "See: https://snapcraft.io/docs/snapcraft-overview");
}
throw new builder_util_1.InvalidConfigurationError("snapcraft not found - please install snapcraft to continue");
}
}
/**
* Resolves Snapcraft Store authentication for remote builds and returns the credential
* env entries to inject. Returns an empty map when snapcraft can authenticate itself
* (interactive session). Throws when no credential source is found.
*/
async function ensureRemoteBuildAuthentication(cscLink, resourcesDir) {
var _a;
builder_util_1.log.debug(null, "resolving remote build authentication...");
// 1. snapcraft.cscLink / SNAP_CSC_LINK — config-level or env credential (base64 or file path).
// resolveSnapCredentials already ran for all build modes; re-run here so remote-build
// gets the same result and the interactive-session fallback is only reached when neither is set.
const credEnv = await (0, electron_publish_1.resolveSnapCredentials)(cscLink, resourcesDir);
if (Object.keys(credEnv).length > 0) {
return credEnv;
}
// 2. SNAPCRAFT_STORE_CREDENTIALS env var — directly provide the credentials string (not base64-encoded).
const SNAPCRAFT_STORE_CREDENTIALS = (_a = process.env.SNAPCRAFT_STORE_CREDENTIALS) === null || _a === void 0 ? void 0 : _a.trim();
if (!(0, builder_util_1.isEmptyOrSpaces)(SNAPCRAFT_STORE_CREDENTIALS)) {
builder_util_1.log.debug(null, "using SNAPCRAFT_STORE_CREDENTIALS from environment, verbatim");
return { SNAPCRAFT_STORE_CREDENTIALS };
}
// 3. Interactive snapcraft session.
try {
const { stdout } = await execAsync("snapcraft whoami");
if (stdout.includes("email:")) {
builder_util_1.log.debug({ account: stdout.trim() }, "already authenticated with snapcraft");
return {};
}
}
catch {
// Not logged in, fall through to error.
}
throw new builder_util_1.InvalidConfigurationError("Snapcraft authentication required for remote build.\n" +
"Authenticate with one of any:\n" +
" 1. Set SNAP_CSC_LINK\n" +
" 2. Set snapcraft.cscLink in your build config\n" +
" 3. Run: snapcraft login\n" +
" 4. Set SNAPCRAFT_STORE_CREDENTIALS environment variable directly");
}
/**
* Executes the snapcraft build command
*/
async function executeSnapcraftBuild(options) {
const { workDir, outputSnap: outputFileName, remoteBuild, useLXD, useMultipass, useDestructiveMode, isolatedEnv } = options;
let processedEnv = { ...(0, builder_util_1.stripSensitiveEnvVars)(process.env), ...isolatedEnv };
// Use a UUID-based temp name as the --output target so the copy below doesn't
// depend on snapcraft's naming convention (which always uses underscores).
const tmpSnap = `eb-snap-${(0, crypto_1.randomUUID)().replace(/-/g, "")}.snap`;
if (useDestructiveMode && !(remoteBuild === null || remoteBuild === void 0 ? void 0 : remoteBuild.enabled)) {
return await runDestructiveBuild(workDir, processedEnv, tmpSnap, outputFileName);
}
const command = "snapcraft";
const args = [];
if (remoteBuild === null || remoteBuild === void 0 ? void 0 : remoteBuild.enabled) {
const remoteBuildArgs = generateRemoteBuildArgs(remoteBuild, workDir);
// Remote build on Launchpad (works from any platform)
args.push(...remoteBuildArgs.args);
processedEnv = { ...processedEnv, ...remoteBuildArgs.isolatedEnv };
}
else {
// `snapcraft pack` runs the full lifecycle (pull → build → stage → prime → pack).
// snapcraft 8.x removed --use-multipass entirely; Multipass is now configured
// via the SNAPCRAFT_BUILD_ENVIRONMENT env var (or auto-selected on macOS).
// --use-lxd remains a supported CLI flag on `pack`.
args.push("pack");
if (useLXD) {
args.push("--use-lxd");
builder_util_1.log.debug(null, "using LXD for build");
}
else if (useMultipass) {
processedEnv.SNAPCRAFT_BUILD_ENVIRONMENT = "multipass";
builder_util_1.log.debug(null, "using Multipass for build (via SNAPCRAFT_BUILD_ENVIRONMENT)");
}
else {
args.push("--output", tmpSnap);
}
}
if (builder_util_1.log.isDebugEnabled) {
args.push("--verbose");
}
builder_util_1.log.info({ workDir: builder_util_1.log.filePath(workDir) }, "executing snapcraft");
await (0, builder_util_1.spawn)(command, args, {
cwd: workDir,
env: processedEnv,
});
if ((remoteBuild === null || remoteBuild === void 0 ? void 0 : remoteBuild.enabled) || useLXD || useMultipass) {
// snapcraft names the output snap itself (e.g. <name>_<version>_<arch>.snap).
// Each electron-builder build invocation targets exactly one arch, so exactly one snap is expected.
const files = await (0, fs_extra_1.readdir)(workDir);
const builtSnap = files.find(f => f.endsWith(".snap"));
if (!builtSnap) {
throw new Error(`Build succeeded but no .snap file found in ${workDir}`);
}
return copySnapToArtifactPath(workDir, builtSnap, outputFileName);
}
return copySnapToArtifactPath(workDir, tmpSnap, outputFileName);
}
function generateRemoteBuildArgs(remoteBuild, workDir) {
const isolatedEnv = {};
const args = ["remote-build"];
builder_util_1.log.debug(null, "using remote-build (Launchpad)");
// Add remote build specific options
if (remoteBuild.launchpadUsername) {
args.push("--user", remoteBuild.launchpadUsername);
}
if (remoteBuild.acceptPublicUpload) {
args.push("--launchpad-accept-public-upload");
}
else {
builder_util_1.log.warn(null, "your project will be publicly uploaded to Launchpad. Use `acceptPublicUpload: true` to suppress this warning");
}
if (remoteBuild.privateProject) {
args.push("--project", remoteBuild.privateProject);
builder_util_1.log.debug({ project: remoteBuild.privateProject }, "using private Launchpad project");
}
if (remoteBuild.buildFor) {
args.push("--build-for", remoteBuild.buildFor);
builder_util_1.log.debug({ arch: remoteBuild.buildFor }, "building for architecture");
}
if (remoteBuild.recover) {
args.push("--recover");
builder_util_1.log.debug(null, "recovering previous build");
}
if (remoteBuild.strategy) {
isolatedEnv.SNAPCRAFT_REMOTE_BUILD_STRATEGY = remoteBuild.strategy;
}
if (remoteBuild.timeout) {
args.push("--timeout", String(remoteBuild.timeout));
builder_util_1.log.debug({ timeout: `${remoteBuild.timeout}s` }, "build timeout configured");
}
// Remote-build downloads the finished snap into workDir.
// --output-dir (not --output <file>) lets snapcraft name the file itself.
args.push("--output-dir", workDir);
return { args, isolatedEnv };
}
// Snapcraft 8 (craft-application) hangs after a successful destructive-mode
// build in containerised environments (Docker/CI without snapd running).
// craft-application's PackageService teardown tries to reach
// /run/snapd-snap.socket (snapctl IPC) which doesn't exist without a live
// snapd daemon — causing an indefinite block.
//
// Work-around: split into two steps:
// 1. `snapcraft prime --destructive-mode` — runs pull/build/stage/prime,
// exits cleanly (no post-pack teardown executed).
// 2. `snapcraft pack <primeDir>` — packs the pre-primed directory without
// running the full build lifecycle, avoiding the problematic teardown.
async function runDestructiveBuild(workDir, processedEnv, tmpSnap, outputFileName) {
const primeArgs = ["prime", "--destructive-mode"];
if (builder_util_1.log.isDebugEnabled) {
primeArgs.push("--verbose");
}
builder_util_1.log.info({ command: `snapcraft ${primeArgs.join(" ")}`, workDir: builder_util_1.log.filePath(workDir) }, "snapcraft prime (1/2)");
await (0, builder_util_1.spawn)("snapcraft", primeArgs, {
cwd: workDir,
env: processedEnv,
});
const primeDir = path.join(workDir, "prime");
const snapcraftPackArgs = ["pack", "--output", tmpSnap, primeDir];
if (builder_util_1.log.isDebugEnabled) {
snapcraftPackArgs.push("--verbose");
}
builder_util_1.log.info({ command: `snapcraft ${snapcraftPackArgs.join(" ")}`, workDir: builder_util_1.log.filePath(workDir) }, "snapcraft pack prime dir (2/2)");
await (0, builder_util_1.spawn)("snapcraft", snapcraftPackArgs, {
cwd: workDir,
env: processedEnv,
});
return copySnapToArtifactPath(workDir, tmpSnap, outputFileName);
}
//# sourceMappingURL=snapcraftBuilder.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NoOpTarget = exports.createCommonTarget = exports.createTargets = exports.computeArchToTargetNamesMap = void 0;
exports.NoOpTarget = void 0;
exports.computeArchToTargetNamesMap = computeArchToTargetNamesMap;
exports.createTargets = createTargets;
exports.createCommonTarget = createCommonTarget;
const builder_util_1 = require("builder-util");
const core_1 = require("../core");
const ArchiveTarget_1 = require("./ArchiveTarget");
@ -14,7 +17,7 @@ function computeArchToTargetNamesMap(raw, platformPackager, platform) {
}
const defaultArchs = raw.size === 0 ? [process.arch] : Array.from(raw.keys()).map(it => builder_util_1.Arch[it]);
const result = new Map(raw);
for (const target of builder_util_1.asArray(platformPackager.platformSpecificBuildOptions.target).map(it => (typeof it === "string" ? { target: it } : it))) {
for (const target of (0, builder_util_1.asArray)(platformPackager.platformSpecificBuildOptions.target).map(it => (typeof it === "string" ? { target: it } : it))) {
let name = target.target;
let archs = target.arch;
const suffixPos = name.lastIndexOf(":");
@ -24,8 +27,8 @@ function computeArchToTargetNamesMap(raw, platformPackager, platform) {
archs = target.target.substring(suffixPos + 1);
}
}
for (const arch of archs == null ? defaultArchs : builder_util_1.asArray(archs)) {
builder_util_1.addValue(result, builder_util_1.archFromString(arch), name);
for (const arch of archs == null ? defaultArchs : (0, builder_util_1.asArray)(archs)) {
(0, builder_util_1.addValue)(result, (0, builder_util_1.archFromString)(arch), name);
}
}
if (result.size === 0) {
@ -37,13 +40,12 @@ function computeArchToTargetNamesMap(raw, platformPackager, platform) {
}
else {
for (const arch of defaultArchs) {
result.set(builder_util_1.archFromString(arch), defaultTarget);
result.set((0, builder_util_1.archFromString)(arch), defaultTarget);
}
}
}
return result;
}
exports.computeArchToTargetNamesMap = computeArchToTargetNamesMap;
function createTargets(nameToTarget, rawList, outDir, packager) {
const result = [];
const mapper = (name, factory) => {
@ -58,7 +60,6 @@ function createTargets(nameToTarget, rawList, outDir, packager) {
packager.createTargets(targets, mapper);
return result;
}
exports.createTargets = createTargets;
function normalizeTargets(targets, defaultTarget) {
const list = [];
for (const t of targets) {
@ -83,7 +84,6 @@ function createCommonTarget(target, outDir, packager) {
throw new Error(`Unknown target: ${target}`);
}
}
exports.createCommonTarget = createCommonTarget;
class NoOpTarget extends core_1.Target {
constructor(name) {
super(name);

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
import { Target, AppInfo } from "../";
import { Arch } from "builder-util";
import { AppInfo, Target } from "../";
import { PlatformPackager } from "../platformPackager";
export declare class StageDir {
readonly dir: string;

View file

@ -1,9 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getWindowsInstallationAppPackageName = exports.getWindowsInstallationDirName = exports.createStageDirPath = exports.createStageDir = exports.StageDir = void 0;
const path = require("path");
exports.StageDir = void 0;
exports.createStageDir = createStageDir;
exports.createStageDirPath = createStageDirPath;
exports.getWindowsInstallationDirName = getWindowsInstallationDirName;
exports.getWindowsInstallationAppPackageName = getWindowsInstallationAppPackageName;
const builder_util_1 = require("builder-util");
const fs = require("fs/promises");
const path = require("path");
class StageDir {
constructor(dir) {
this.dir = dir;
@ -25,23 +29,19 @@ exports.StageDir = StageDir;
async function createStageDir(target, packager, arch) {
return new StageDir(await createStageDirPath(target, packager, arch));
}
exports.createStageDir = createStageDir;
async function createStageDirPath(target, packager, arch) {
const tempDir = packager.info.stageDirPathCustomizer(target, packager, arch);
await fs.rm(tempDir, { recursive: true, force: true });
await fs.mkdir(tempDir, { recursive: true });
return tempDir;
}
exports.createStageDirPath = createStageDirPath;
// https://github.com/electron-userland/electron-builder/issues/3100
// https://github.com/electron-userland/electron-builder/commit/2539cfba20dc639128e75c5b786651b652bb4b78
function getWindowsInstallationDirName(appInfo, isTryToUseProductName) {
return isTryToUseProductName && /^[-_+0-9a-zA-Z .]+$/.test(appInfo.productFilename) ? appInfo.productFilename : appInfo.sanitizedName;
}
exports.getWindowsInstallationDirName = getWindowsInstallationDirName;
// https://github.com/electron-userland/electron-builder/issues/6747
function getWindowsInstallationAppPackageName(appName) {
return appName.replace(/\//g, "\\");
}
exports.getWindowsInstallationAppPackageName = getWindowsInstallationAppPackageName;
//# sourceMappingURL=targetUtil.js.map

View file

@ -1 +1 @@
{"version":3,"file":"targetUtil.js","sourceRoot":"","sources":["../../src/targets/targetUtil.ts"],"names":[],"mappings":";;;AAAA,6BAA4B;AAE5B,+CAA0C;AAE1C,kCAAiC;AAEjC,MAAa,QAAQ;IACnB,YAAqB,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAEpC,WAAW,CAAC,IAAY;QACtB,OAAO,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAA;IACnC,CAAC;IAED,OAAO;QACL,IAAI,CAAC,oBAAK,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,2CAA2C,KAAK,MAAM,EAAE;YACxF,OAAO,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;SACzD;QACD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,GAAG,CAAA;IACjB,CAAC;CACF;AAjBD,4BAiBC;AAEM,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,QAA+B,EAAE,IAAU;IAC9F,OAAO,IAAI,QAAQ,CAAC,MAAM,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAA;AACvE,CAAC;AAFD,wCAEC;AAEM,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,QAA+B,EAAE,IAAU;IAClG,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC5E,MAAM,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACtD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,OAAO,OAAO,CAAA;AAChB,CAAC;AALD,gDAKC;AAED,oEAAoE;AACpE,wGAAwG;AACxG,SAAgB,6BAA6B,CAAC,OAAgB,EAAE,qBAA8B;IAC5F,OAAO,qBAAqB,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAA;AACvI,CAAC;AAFD,sEAEC;AAED,oEAAoE;AACpE,SAAgB,oCAAoC,CAAC,OAAe;IAClE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;AACrC,CAAC;AAFD,oFAEC","sourcesContent":["import * as path from \"path\"\nimport { Target, AppInfo } from \"../\"\nimport { Arch, debug } from \"builder-util\"\nimport { PlatformPackager } from \"../platformPackager\"\nimport * as fs from \"fs/promises\"\n\nexport class StageDir {\n constructor(readonly dir: string) {}\n\n getTempFile(name: string) {\n return this.dir + path.sep + name\n }\n\n cleanup() {\n if (!debug.enabled || process.env.ELECTRON_BUILDER_REMOVE_STAGE_EVEN_IF_DEBUG === \"true\") {\n return fs.rm(this.dir, { recursive: true, force: true })\n }\n return Promise.resolve()\n }\n\n toString() {\n return this.dir\n }\n}\n\nexport async function createStageDir(target: Target, packager: PlatformPackager<any>, arch: Arch): Promise<StageDir> {\n return new StageDir(await createStageDirPath(target, packager, arch))\n}\n\nexport async function createStageDirPath(target: Target, packager: PlatformPackager<any>, arch: Arch): Promise<string> {\n const tempDir = packager.info.stageDirPathCustomizer(target, packager, arch)\n await fs.rm(tempDir, { recursive: true, force: true })\n await fs.mkdir(tempDir, { recursive: true })\n return tempDir\n}\n\n// https://github.com/electron-userland/electron-builder/issues/3100\n// https://github.com/electron-userland/electron-builder/commit/2539cfba20dc639128e75c5b786651b652bb4b78\nexport function getWindowsInstallationDirName(appInfo: AppInfo, isTryToUseProductName: boolean): string {\n return isTryToUseProductName && /^[-_+0-9a-zA-Z .]+$/.test(appInfo.productFilename) ? appInfo.productFilename : appInfo.sanitizedName\n}\n\n// https://github.com/electron-userland/electron-builder/issues/6747\nexport function getWindowsInstallationAppPackageName(appName: string): string {\n return appName.replace(/\\//g, \"\\\\\")\n}\n"]}
{"version":3,"file":"targetUtil.js","sourceRoot":"","sources":["../../src/targets/targetUtil.ts"],"names":[],"mappings":";;;AAyBA,wCAEC;AAED,gDAKC;AAID,sEAEC;AAGD,oFAEC;AA7CD,+CAA0C;AAC1C,kCAAiC;AACjC,6BAA4B;AAI5B,MAAa,QAAQ;IACnB,YAAqB,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAEpC,WAAW,CAAC,IAAY;QACtB,OAAO,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAA;IACnC,CAAC;IAED,OAAO;QACL,IAAI,CAAC,oBAAK,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,2CAA2C,KAAK,MAAM,EAAE,CAAC;YACzF,OAAO,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1D,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,GAAG,CAAA;IACjB,CAAC;CACF;AAjBD,4BAiBC;AAEM,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,QAA+B,EAAE,IAAU;IAC9F,OAAO,IAAI,QAAQ,CAAC,MAAM,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAA;AACvE,CAAC;AAEM,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,QAA+B,EAAE,IAAU;IAClG,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC5E,MAAM,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACtD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,oEAAoE;AACpE,wGAAwG;AACxG,SAAgB,6BAA6B,CAAC,OAAgB,EAAE,qBAA8B;IAC5F,OAAO,qBAAqB,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAA;AACvI,CAAC;AAED,oEAAoE;AACpE,SAAgB,oCAAoC,CAAC,OAAe;IAClE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;AACrC,CAAC","sourcesContent":["import { Arch, debug } from \"builder-util\"\nimport * as fs from \"fs/promises\"\nimport * as path from \"path\"\nimport { AppInfo, Target } from \"../\"\nimport { PlatformPackager } from \"../platformPackager\"\n\nexport class StageDir {\n constructor(readonly dir: string) {}\n\n getTempFile(name: string) {\n return this.dir + path.sep + name\n }\n\n cleanup() {\n if (!debug.enabled || process.env.ELECTRON_BUILDER_REMOVE_STAGE_EVEN_IF_DEBUG === \"true\") {\n return fs.rm(this.dir, { recursive: true, force: true })\n }\n return Promise.resolve()\n }\n\n toString() {\n return this.dir\n }\n}\n\nexport async function createStageDir(target: Target, packager: PlatformPackager<any>, arch: Arch): Promise<StageDir> {\n return new StageDir(await createStageDirPath(target, packager, arch))\n}\n\nexport async function createStageDirPath(target: Target, packager: PlatformPackager<any>, arch: Arch): Promise<string> {\n const tempDir = packager.info.stageDirPathCustomizer(target, packager, arch)\n await fs.rm(tempDir, { recursive: true, force: true })\n await fs.mkdir(tempDir, { recursive: true })\n return tempDir\n}\n\n// https://github.com/electron-userland/electron-builder/issues/3100\n// https://github.com/electron-userland/electron-builder/commit/2539cfba20dc639128e75c5b786651b652bb4b78\nexport function getWindowsInstallationDirName(appInfo: AppInfo, isTryToUseProductName: boolean): string {\n return isTryToUseProductName && /^[-_+0-9a-zA-Z .]+$/.test(appInfo.productFilename) ? appInfo.productFilename : appInfo.sanitizedName\n}\n\n// https://github.com/electron-userland/electron-builder/issues/6747\nexport function getWindowsInstallationAppPackageName(appName: string): string {\n return appName.replace(/\\//g, \"\\\\\")\n}\n"]}

View file

@ -1 +0,0 @@
export declare function getLinuxToolsPath(): Promise<string>;

View file

@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLinuxToolsPath = void 0;
const binDownload_1 = require("../binDownload");
function getLinuxToolsPath() {
//noinspection SpellCheckingInspection
return binDownload_1.getBinFromUrl("linux-tools", "mac-10.12.3", "SQ8fqIRVXuQVWnVgaMTDWyf2TLAJjJYw3tRSqQJECmgF6qdM7Kogfa6KD49RbGzzMYIFca9Uw3MdsxzOPRWcYw==");
}
exports.getLinuxToolsPath = getLinuxToolsPath;
//# sourceMappingURL=tools.js.map

View file

@ -1 +0,0 @@
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/targets/tools.ts"],"names":[],"mappings":";;;AAAA,gDAA8C;AAE9C,SAAgB,iBAAiB;IAC/B,sCAAsC;IACtC,OAAO,2BAAa,CAAC,aAAa,EAAE,aAAa,EAAE,0FAA0F,CAAC,CAAA;AAChJ,CAAC;AAHD,8CAGC","sourcesContent":["import { getBinFromUrl } from \"../binDownload\"\n\nexport function getLinuxToolsPath() {\n //noinspection SpellCheckingInspection\n return getBinFromUrl(\"linux-tools\", \"mac-10.12.3\", \"SQ8fqIRVXuQVWnVgaMTDWyf2TLAJjJYw3tRSqQJECmgF6qdM7Kogfa6KD49RbGzzMYIFca9Uw3MdsxzOPRWcYw==\")\n}\n"]}