UNPKG

app-builder-lib

Version:
266 lines (253 loc) 10.5 kB
"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