Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
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>;
|
||||
private buildFuse2AppImage;
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
"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 appBuilder_1 = require("../../util/appBuilder");
|
||||
const license_1 = require("../../util/license");
|
||||
const targetUtil_1 = require("../targetUtil");
|
||||
const appImageUtil_1 = require("./appImageUtil");
|
||||
// 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 = { ...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, `${exports.APP_RUN_ENTRYPOINT} ${args} %U`, {
|
||||
"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));
|
||||
}
|
||||
if (this.packager.packagerOptions.effectiveOptionComputed != null && (await this.packager.packagerOptions.effectiveOptionComputed({ desktop: desktopEntry }))) {
|
||||
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 this.buildFuse2AppImage({ stageDir, arch, artifactPath, appOutDir, options, packager, desktopEntry, icons, license });
|
||||
}
|
||||
else {
|
||||
updateInfo = await (0, appImageUtil_1.buildAppImage)({
|
||||
appDir: appOutDir,
|
||||
stageDir: stageDir.dir,
|
||||
arch,
|
||||
output: artifactPath,
|
||||
options: {
|
||||
productName: this.packager.appInfo.productName,
|
||||
productFilename: this.packager.appInfo.productFilename,
|
||||
executableName: this.packager.executableName,
|
||||
license,
|
||||
desktopEntry,
|
||||
icons,
|
||||
fileAssociations: this.packager.fileAssociations,
|
||||
compression: this.packager.compression === "maximum" ? "xz" : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
builder_util_1.log.error({ error: error.message }, "failed to build AppImage");
|
||||
await stageDir.cleanup().catch(() => { });
|
||||
throw error;
|
||||
}
|
||||
await stageDir.cleanup();
|
||||
await packager.info.emitArtifactBuildCompleted({
|
||||
file: artifactPath,
|
||||
safeArtifactName: packager.computeSafeArtifactName(artifactName, "AppImage", arch, false),
|
||||
target: this,
|
||||
arch,
|
||||
packager,
|
||||
isWriteUpdateInfo: true,
|
||||
updateInfo,
|
||||
});
|
||||
}
|
||||
async buildFuse2AppImage(props) {
|
||||
const { stageDir, arch, artifactPath, appOutDir, options, packager, desktopEntry, icons, license } = props;
|
||||
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,
|
||||
executableName: this.packager.executableName,
|
||||
icons,
|
||||
fileAssociations: this.packager.fileAssociations,
|
||||
...options,
|
||||
}),
|
||||
];
|
||||
(0, appBuilder_1.objectToArgs)(args, {
|
||||
license,
|
||||
});
|
||||
if (packager.compression === "maximum") {
|
||||
args.push("--compression", "xz");
|
||||
}
|
||||
const updateInfo = await (0, appBuilder_1.executeAppBuilderAsJson)(args);
|
||||
return updateInfo;
|
||||
}
|
||||
}
|
||||
exports.default = AppImageTarget;
|
||||
//# sourceMappingURL=AppImageTarget.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+23
@@ -0,0 +1,23 @@
|
||||
import { Arch } from "builder-util";
|
||||
import { FileAssociation } from "../../options/FileAssociation";
|
||||
import { IconInfo } from "../../platformPackager";
|
||||
import { BlockMapDataHolder } from "builder-util-runtime";
|
||||
interface Options {
|
||||
productName: string;
|
||||
productFilename: string;
|
||||
executableName: string;
|
||||
desktopEntry: string;
|
||||
icons: IconInfo[];
|
||||
license?: string | null;
|
||||
fileAssociations: FileAssociation[];
|
||||
compression?: "xz" | "lzo" | "zstd";
|
||||
}
|
||||
export interface AppImageBuilderOptions {
|
||||
appDir: string;
|
||||
stageDir: string;
|
||||
arch: Arch;
|
||||
output: string;
|
||||
options: Options;
|
||||
}
|
||||
export declare function buildAppImage(opts: AppImageBuilderOptions): Promise<BlockMapDataHolder>;
|
||||
export {};
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildAppImage = buildAppImage;
|
||||
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 buildAppImage(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)(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);
|
||||
if (options.compression === "xz") {
|
||||
args.push("-Xdict-size", "100%", "-b", "1048576");
|
||||
}
|
||||
}
|
||||
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 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 executable/filename fields don't contain dangerous characters
|
||||
* that could break paths or cause security issues even when escaped.
|
||||
*/
|
||||
function validateCriticalPathString(str, fieldName) {
|
||||
// Only reject characters that would break filesystem paths or cause severe issues
|
||||
// Allow quotes, spaces, etc. since they can be escaped
|
||||
if (/[`${}|&;<>\n\r\0]/.test(str) || str.includes("/") || str.includes("\\")) {
|
||||
throw new builder_util_1.InvalidConfigurationError(`${fieldName} contains characters that cannot be safely used in file paths: ${str}. ` +
|
||||
`Please use only alphanumeric characters, hyphens, underscores, dots, spaces, and quotes.`);
|
||||
}
|
||||
}
|
||||
async function writeAppLauncherAndRelatedFiles(opts) {
|
||||
const { stageDir, options: { license, executableName, productFilename, productName, desktopEntry }, } = opts;
|
||||
// Validate only critical path fields for severe path-breaking characters
|
||||
// productName and productFilename can contain quotes, spaces, etc. - they'll be escaped
|
||||
validateCriticalPathString(executableName, "executableName");
|
||||
validateCriticalPathString(productFilename, "productFilename");
|
||||
// Write desktop file
|
||||
const desktopFileName = `${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)(opts);
|
||||
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 `#!/bin/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
|
||||
|
||||
export PATH="\${APPDIR}:\${APPDIR}/usr/sbin:\${PATH}"
|
||||
export XDG_DATA_DIRS="./share/:/usr/share/gnome:/usr/local/share/:/usr/share/:\${XDG_DATA_DIRS}"
|
||||
export LD_LIBRARY_PATH="\${APPDIR}/usr/lib:\${LD_LIBRARY_PATH}"
|
||||
export XDG_DATA_DIRS="\${APPDIR}"/usr/share/:"\${XDG_DATA_DIRS}":/usr/share/gnome/:/usr/local/share/:/usr/share/
|
||||
export GSETTINGS_SCHEMA_DIR="\${APPDIR}/usr/share/glib-2.0/schemas:\${GSETTINGS_SCHEMA_DIR}"
|
||||
|
||||
BIN="$APPDIR/${config.ExecutableName}"
|
||||
|
||||
if [ -z "$APPIMAGE_EXIT_AFTER_INSTALL" ] ; then
|
||||
trap atexit EXIT
|
||||
fi
|
||||
|
||||
isEulaAccepted=1
|
||||
|
||||
atexit()
|
||||
{
|
||||
if [ $isEulaAccepted == 1 ] ; then
|
||||
if [ $NUMBER_OF_ARGS -eq 0 ] ; then
|
||||
exec "$BIN"
|
||||
else
|
||||
exec "$BIN" "\${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
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
import { AppImageBuilderOptions } from "./appImageUtil";
|
||||
export declare function copyIcons(options: AppImageBuilderOptions): Promise<void>;
|
||||
export declare function copyMimeTypes(options: AppImageBuilderOptions): Promise<string | null>;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
"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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
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(options) {
|
||||
const { stageDir, options: { fileAssociations, productName, executableName }, } = options;
|
||||
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 may not be supported`);
|
||||
}
|
||||
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
|
||||
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user