266 lines
10 KiB
JavaScript
266 lines
10 KiB
JavaScript
|
|
"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
|