"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, `${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 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 ? `${appName}.desktop` : undefined,
            desktop: `meta/gui/${appName}.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