forked from olcxjas-softworks/LarpixClient
323 lines
No EOL
13 KiB
JavaScript
323 lines
No EOL
13 KiB
JavaScript
"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
|