import { bundle as bundleFlatpak, FlatpakBundlerBuildOptions, FlatpakManifest } from "@malept/flatpak-bundler" import { Arch, copyFile, toLinuxArchString } from "builder-util" import { chmod, outputFile } from "fs-extra" import * as path from "path" import { Target } from "../core" import { LinuxPackager } from "../linuxPackager" import { FlatpakOptions } from "../options/linuxOptions" import { getNotLocalizedLicenseFile } from "../util/license" import { LinuxTargetHelper } from "./LinuxTargetHelper" import { createStageDir, StageDir } from "./targetUtil" export default class FlatpakTarget extends Target { readonly options: FlatpakOptions = { ...this.packager.platformSpecificBuildOptions, ...(this.packager.config as any)[this.name], } constructor( name: string, private readonly packager: LinuxPackager, private helper: LinuxTargetHelper, readonly outDir: string ) { super(name) } get appId(): string { return filterFlatpakAppIdentifier(this.packager.appInfo.id) } async build(appOutDir: string, arch: Arch): Promise { const { packager, options } = this const artifactName = packager.expandArtifactNamePattern(options, "flatpak", arch, undefined, false) const artifactPath = path.join(this.outDir, artifactName) await packager.info.emitArtifactBuildStarted({ targetPresentableName: "flatpak", file: artifactPath, arch, }) const stageDir = await this.prepareStageDir(arch) const { manifest, buildOptions } = this.getFlatpakBuilderOptions(appOutDir, stageDir.dir, artifactName, arch) await bundleFlatpak(manifest, buildOptions) await stageDir.cleanup() await packager.info.emitArtifactBuildCompleted({ file: artifactPath, safeArtifactName: packager.computeSafeArtifactName(artifactName, "flatpak", arch, false), target: this, arch, packager, isWriteUpdateInfo: false, }) } private async prepareStageDir(arch: Arch): Promise { const stageDir = await createStageDir(this, this.packager, arch) await Promise.all([this.createSandboxBinWrapper(stageDir), this.createDesktopFile(stageDir), this.copyLicenseFile(stageDir), this.copyIcons(stageDir)]) return stageDir } private async createSandboxBinWrapper(stageDir: StageDir) { const useWaylandFlags = !!this.options.useWaylandFlags const electronWrapperPath = stageDir.getTempFile(path.join("bin", "electron-wrapper")) await outputFile(electronWrapperPath, getElectronWrapperScript(this.packager.executableName, useWaylandFlags)) await chmod(electronWrapperPath, 0o755) } private async createDesktopFile(stageDir: StageDir) { const appIdentifier = this.appId const desktopFile = stageDir.getTempFile(path.join("share", "applications", `${appIdentifier}.desktop`)) await this.helper.writeDesktopEntry(this.options, "electron-wrapper %U", desktopFile, { Icon: appIdentifier }) } private async copyLicenseFile(stageDir: StageDir) { const licenseSrc = await getNotLocalizedLicenseFile(this.options.license, this.packager, ["txt", "html"]) if (licenseSrc) { const licenseDst = stageDir.getTempFile(path.join("share", "doc", this.appId, "copyright")) await copyFile(licenseSrc, licenseDst) } } private async copyIcons(stageDir: StageDir) { const icons = await this.helper.icons const copyIcons = icons.map(async icon => { if (icon.size > 512) { // Flatpak does not allow icons larger than 512 pixels return Promise.resolve() } const extWithDot = path.extname(icon.file) const sizeName = extWithDot === ".svg" ? "scalable" : `${icon.size}x${icon.size}` const iconDst = stageDir.getTempFile(path.join("share", "icons", "hicolor", sizeName, "apps", `${this.appId}${extWithDot}`)) return copyFile(icon.file, iconDst) }) await Promise.all(copyIcons) } private getFlatpakBuilderOptions(appOutDir: string, stageDir: string, artifactName: string, arch: Arch): { manifest: FlatpakManifest; buildOptions: FlatpakBundlerBuildOptions } { const appIdentifier = this.appId const { executableName } = this.packager const flatpakArch = toLinuxArchString(arch, "flatpak") const manifest: FlatpakManifest = { id: appIdentifier, command: "electron-wrapper", runtime: this.options.runtime || flatpakBuilderDefaults.runtime, runtimeVersion: this.options.runtimeVersion || flatpakBuilderDefaults.runtimeVersion, sdk: this.options.sdk || flatpakBuilderDefaults.sdk, base: this.options.base || flatpakBuilderDefaults.base, baseVersion: this.options.baseVersion || flatpakBuilderDefaults.baseVersion, finishArgs: this.options.finishArgs || flatpakBuilderDefaults.finishArgs, branch: this.options.branch, modules: this.options.modules, } const buildOptions: FlatpakBundlerBuildOptions = { baseFlatpakref: `app/${manifest.base}/${flatpakArch}/${manifest.baseVersion}`, runtimeFlatpakref: `runtime/${manifest.runtime}/${flatpakArch}/${manifest.runtimeVersion}`, sdkFlatpakref: `runtime/${manifest.sdk}/${flatpakArch}/${manifest.runtimeVersion}`, arch: flatpakArch as any, bundlePath: path.join(this.outDir, artifactName), files: [[stageDir, "/"], [appOutDir, path.join("/lib", appIdentifier)], ...(this.options.files || [])], symlinks: [[path.join("/lib", appIdentifier, executableName), path.join("/bin", executableName)], ...(this.options.symlinks || [])], } return { manifest, buildOptions } } } const flatpakBuilderDefaults: Omit = { runtime: "org.freedesktop.Platform", runtimeVersion: "20.08", sdk: "org.freedesktop.Sdk", base: "org.electronjs.Electron2.BaseApp", baseVersion: "20.08", finishArgs: [ // Wayland/X11 Rendering "--socket=wayland", "--socket=x11", "--share=ipc", // Open GL "--device=dri", // Audio output "--socket=pulseaudio", // Read/write home directory access "--filesystem=home", // Allow communication with network "--share=network", // System notifications with libnotify "--talk-name=org.freedesktop.Notifications", ], } function getElectronWrapperScript(executableName: string, useWaylandFlags: boolean): string { return useWaylandFlags ? `#!/bin/sh export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" if [ "\${XDG_SESSION_TYPE}" == "wayland" ]; then zypak-wrapper "${executableName}" --enable-features=UseOzonePlatform --ozone-platform=wayland "$@" else zypak-wrapper "${executableName}" "$@" fi ` : `#!/bin/sh export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" zypak-wrapper "${executableName}" "$@" ` } function filterFlatpakAppIdentifier(identifier: string) { // Remove special characters and allow only alphanumeric (A-Z,a-z,0-9), underscore (_), and period (.) // Flatpak documentation: https://docs.flatpak.org/en/latest/conventions.html#application-ids return identifier.replace(/-/g, "_").replace(/[^a-zA-Z0-9._]/g, "") }