UNPKG

app-builder-lib

Version:
449 lines 22.4 kB
"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, `${this.helper.getDesktopFileName(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 desktopBaseName = this.helper.getDesktopFileName(appName); 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 ? `${desktopBaseName}.desktop` : undefined, desktop: `meta/gui/${desktopBaseName}.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