From 231bf5fe8194ac9c4b383e44a3e96ab6d1df51a4 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 5 Oct 2026 04:15:21 -0400 Subject: [PATCH] Flatpak single-file bundles as release artifacts (#163) * Flatpak single-file bundle as a release artifact (#140) packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml repackages the release's Linux tarball (no Rust build, no network) on the freedesktop 26.08 runtime; packaging/linux/flatpak-bundle.sh builds it with flatpak-builder and flatpak build-bundle and smoke-tests it. release.yml gains a Flatpak job per arch that feeds the draft release and SHA256SUMS. The Flathub manifest moves to 26.08 with the same documented finish-args (portals + Pictures/Documents), and packaging-lint checks the two manifests agree. Font scanning also looks in /run/host/fonts and /run/host/user-fonts so host fonts show up in the sandbox. Co-Authored-By: Claude Opus 5.5 * Docs: installing the Flatpak bundle (#140) Co-Authored-By: Claude Opus 5.5 * Flatpak job: install the SVG pixbuf loader for appstream compose Without librsvg2-common (absent on the x86_64 runner image) the host's appstreamcli compose fails with file-read-error on the scalable icon. Co-Authored-By: Claude Opus 5.5 * release.yml: the Flatpak job runs without the release environment It only repackages the Linux job's tarball and signs nothing, so it doesn't need (and shouldn't hold) the signing secrets while running third-party tooling. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .github/workflows/packaging-lint.yml | 11 ++- .github/workflows/release.yml | 40 ++++++++++- README.md | 9 ++- crates/text/src/fonts.rs | 22 ++++++ docs/releasing.md | 40 ++++++++--- packaging/linux/flatpak-bundle.sh | 72 +++++++++++++++++++ .../ai.storyteller.photocraft.bundle.yml | 50 +++++++++++++ .../flatpak/ai.storyteller.photocraft.yml | 20 ++++-- 8 files changed, 248 insertions(+), 16 deletions(-) create mode 100755 packaging/linux/flatpak-bundle.sh create mode 100644 packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index 05b09c2242..7b0b619fd7 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -50,4 +50,13 @@ jobs: desktop-file-validate packaging/linux/ai.storyteller.photocraft.desktop sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.photocraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.photocraft.metainfo.xml" appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.photocraft.metainfo.xml" - python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.photocraft.yml')); assert m['id'] == 'ai.storyteller.photocraft' and m['command'] == 'photocraft'" + python3 - <<'EOF' + import yaml + src = yaml.safe_load(open("packaging/linux/flatpak/ai.storyteller.photocraft.yml")) + bundle = yaml.safe_load(open("packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml")) + for m in (src, bundle): + assert m["id"] == "ai.storyteller.photocraft" and m["command"] == "photocraft", m["id"] + for key in ("runtime", "runtime-version", "sdk", "finish-args"): + assert src[key] == bundle[key], f"Flatpak manifests disagree on {key}: {src[key]} vs {bundle[key]}" + print("Flatpak manifests ok") + EOF diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 065094f381..49c317cb79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -201,6 +201,44 @@ jobs: path: dist/release/* if-no-files-found: error + flatpak: + name: Flatpak ${{ matrix.arch }} + needs: [version, linux] + # Repackages the Linux job's tarball (the same binaries as the AppImage/deb/rpm); no Rust + # build here. 24.04 for a newer flatpak-builder (1.4) than 22.04 ships. + runs-on: ${{ matrix.runner }} + # No `environment: release`: this job signs nothing, so it gets no release secrets. + strategy: + fail-fast: false + matrix: + include: + - { arch: x86_64, runner: ubuntu-24.04 } + - { arch: aarch64, runner: ubuntu-24.04-arm } + env: + PHOTOCRAFT_VERSION: ${{ needs.version.outputs.version }} + PHOTOCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: release-linux-${{ matrix.arch }} + path: dist/linux + - name: Install flatpak-builder + run: | + sudo apt-get update + # librsvg2-common: the SVG pixbuf loader. flatpak-builder runs the host's + # `appstreamcli compose`, which renders the scalable icon and fails without it. + sudo apt-get install -y --no-install-recommends flatpak flatpak-builder librsvg2-common + # Ubuntu 24.04's AppArmor blocks the unprivileged user namespaces bubblewrap needs. + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Build the Flatpak bundle + run: packaging/linux/flatpak-bundle.sh "dist/linux/photocraft-$PHOTOCRAFT_VERSION-linux-${{ matrix.arch }}.tar.gz" + - uses: actions/upload-artifact@v4 + with: + name: release-flatpak-${{ matrix.arch }} + path: dist/release/*.flatpak + if-no-files-found: error + web: name: Web (wasm) needs: version @@ -236,7 +274,7 @@ jobs: release: name: Draft GitHub Release - needs: [version, macos, windows, linux, web] + needs: [version, macos, windows, linux, flatpak, web] runs-on: ubuntu-latest environment: release permissions: diff --git a/README.md b/README.md index 8e14ed6d51..e34683e0c7 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,14 @@ cargo test --workspace # the test suite New contributors and AI agents: start with [`AGENTS.md`](AGENTS.md), then [`docs/`](docs/). -Installers for macOS, Windows, Linux and the web are attached to each [GitHub release](https://github.com/storytold/photocraft/releases). Maintainers: [`docs/releasing.md`](docs/releasing.md) explains how releases are built, signed and published. +Installers for macOS, Windows, Linux and the web are attached to each [GitHub release](https://github.com/storytold/photocraft/releases). On Linux you can pick an AppImage, a `.deb`, an `.rpm`, a tarball or a Flatpak bundle. The bundle needs the freedesktop runtime from [Flathub](https://flathub.org/setup), which `flatpak` offers to install along with it: + +```sh +flatpak install --user photocraft--linux-x86_64.flatpak # or -linux-aarch64 +flatpak run ai.storyteller.photocraft +``` + +Maintainers: [`docs/releasing.md`](docs/releasing.md) explains how releases are built, signed and published. > [!IMPORTANT] > **Status:** PhotoCraft is in early alpha. The core editing workflow is here, and we're working toward full Photoshop parity, milestone by milestone (see [`docs/roadmap.md`](docs/roadmap.md)). Progress is measured, not guessed: `cargo xtask parity` checks every item in Photoshop's menu tree against the live command registry and writes [`docs/parity.md`](docs/parity.md). Expect rough edges, and please file issues. You can also tell us what broke on [Discord](https://discord.gg/artcraft). diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index b1b09854f9..c8d060c47a 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -340,6 +340,10 @@ fn system_font_dirs() -> Vec { } else { v.push(PathBuf::from("/usr/share/fonts")); v.push(PathBuf::from("/usr/local/share/fonts")); + // Inside a Flatpak sandbox the host's system and per-user fonts are mounted here + // (`/usr/share/fonts` is the runtime's own small set). Missing dirs are skipped. + v.push(PathBuf::from("/run/host/fonts")); + v.push(PathBuf::from("/run/host/user-fonts")); if let Some(d) = std::env::var_os("XDG_DATA_HOME") { v.push(PathBuf::from(d).join("fonts")); } @@ -350,3 +354,21 @@ fn system_font_dirs() -> Vec { } v } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + #[test] + #[cfg(target_os = "linux")] + fn scans_flatpak_host_fonts() { + let dirs = super::system_font_dirs(); + assert!(dirs.iter().any(|d| d.ends_with("run/host/fonts"))); + assert!(dirs.iter().any(|d| d.ends_with("run/host/user-fonts"))); + } + + #[test] + fn missing_font_dirs_are_skipped() { + let mut files = Vec::new(); + super::collect_font_files(std::path::Path::new("/nonexistent/photocraft/fonts"), 0, &mut files); + assert!(files.is_empty()); + } +} diff --git a/docs/releasing.md b/docs/releasing.md index aa3ab7ee98..c908658afd 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -44,8 +44,8 @@ in the dialog. | macOS 11+ (universal: Apple silicon + Intel) | `photocraft--macos-universal.dmg`, `photocraft-cli--macos-universal.zip` | `macos-15` | | Windows 10+ x64 | `photocraft--windows-x64.msi`, `photocraft--windows-x64-portable.zip` | `windows-latest` | | Windows 10+ x86 (32-bit) | `photocraft--windows-x86.msi`, `photocraft--windows-x86-portable.zip` | `windows-latest` | -| Linux x86_64 | `photocraft--linux-x86_64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04` | -| Linux aarch64 | `photocraft--linux-aarch64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` | +| Linux x86_64 | `photocraft--linux-x86_64.{AppImage,deb,rpm,tar.gz,flatpak}` | `ubuntu-22.04` (Flatpak: `ubuntu-24.04`) | +| Linux aarch64 | `photocraft--linux-aarch64.{AppImage,deb,rpm,tar.gz,flatpak}` | `ubuntu-22.04-arm` (Flatpak: `ubuntu-24.04-arm`) | | Web | `photocraft-web-.zip` (static site; see [`packaging/web/README.md`](../packaging/web/README.md)) | `ubuntu-latest` | Every binary reports its version, the commit and the build date: `photocraft --version`, @@ -128,11 +128,34 @@ Why these formats: (`nfpm.yaml`). It doesn't need cargo-deb and cargo-generate-rpm metadata in our crates, and it can build both formats on Ubuntu. - **.tar.gz** is for people who manage their own `/opt` or `~/.local`. -- **Flatpak** is the route into Flathub, GNOME Software and KDE Discover. - `packaging/linux/flatpak/ai.storyteller.photocraft.yml` is ready for a Flathub submission. - CI only validates it. A real build needs vendored crate sources (`cargo-sources.json` from - `flatpak-cargo-generator.py`) and adds 20+ minutes per architecture. Build it by hand with - the commands in the manifest's header. +- **Flatpak bundle** (`.flatpak`) is a single file that installs into Flatpak, sandboxed and + updatable by installing a newer bundle, for people who prefer Flatpak to AppImage. + `packaging/linux/flatpak-bundle.sh` repackages the job's `.tar.gz` with + `packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml` on the freedesktop 26.08 + runtime. It does no Rust build and needs no network inside `flatpak-builder`, so the bundle + holds the same binaries as the other formats. A separate `flatpak` job per architecture + (`ubuntu-24.04` and `ubuntu-24.04-arm`, for flatpak-builder 1.4) downloads the Linux job's + artifact, runs the script, installs the bundle and runs `photocraft-cli --version` inside the + sandbox as a smoke test. The bundle names Flathub as its runtime repo, so users install + it with: + + ```sh + flatpak install --user photocraft--linux-x86_64.flatpak # pulls org.freedesktop.Platform//26.08 from Flathub if missing + flatpak run ai.storyteller.photocraft + ``` + + Sandbox permissions (justified in the manifest): Wayland with X11 fallback, IPC (X11 + shared memory), `dri` for the GPU, and read/write access to Pictures and Documents. Every + other file goes through the file-chooser and document portals. There's no network, so the + `--control` server is only reachable from inside the sandbox until the user runs + `flatpak override --user --share=network ai.storyteller.photocraft`. Host fonts are read from + `/run/host/fonts` and `/run/host/user-fonts`. Locally (on Linux, with `flatpak` and + `flatpak-builder`): `packaging/linux/package.sh --formats tar && packaging/linux/flatpak-bundle.sh`. +- **Flathub**: `packaging/linux/flatpak/ai.storyteller.photocraft.yml` builds from source and + is ready for a Flathub submission (it keeps the same runtime and `finish-args` as the bundle + manifest, which packaging-lint checks). CI doesn't build it. A real build needs vendored crate + sources (`cargo-sources.json` from `flatpak-cargo-generator.py`) and adds 20+ minutes per + architecture. Build it by hand with the commands in the manifest's header. All Linux binaries are built on Ubuntu 22.04 and need **glibc ≥ 2.35**: Ubuntu 22.04+, Debian 12+, Fedora 36+, RHEL 10, openSUSE Tumbleweed. They link only glibc and libgcc_s. X11, @@ -192,4 +215,5 @@ The outputs are committed, so packaging never needs those tools. `.github/workflows/packaging-lint.yml` runs in seconds on any change to `packaging/`, the workflows or the icons. It runs actionlint, shellcheck, a PowerShell parse, xmllint, -`desktop-file-validate`, `appstreamcli validate`, and a YAML check of the Flatpak manifest. +`desktop-file-validate`, `appstreamcli validate`, and a YAML check that the two Flatpak +manifests agree on the runtime and permissions. diff --git a/packaging/linux/flatpak-bundle.sh b/packaging/linux/flatpak-bundle.sh new file mode 100755 index 0000000000..597866523a --- /dev/null +++ b/packaging/linux/flatpak-bundle.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Repackage a PhotoCraft Linux tarball as a single-file Flatpak bundle: +# +# $DIST/photocraft--linux-.flatpak +# +# Usage: packaging/linux/flatpak-bundle.sh [--no-test] [TARBALL] +# +# TARBALL defaults to $DIST/photocraft--linux-.tar.gz from package.sh. The bundle +# is built for the host architecture (x86_64 or aarch64), which must match the tarball's. +# Needs flatpak, flatpak-builder and the SVG pixbuf loader (librsvg2-common) for the host's +# `appstreamcli compose`; the freedesktop runtime and SDK named in the manifest are +# installed per-user from Flathub. Unless --no-test, the bundle is then installed per-user and +# `photocraft-cli --version` is run inside the sandbox as a smoke test. +# Manifest: packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml. +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" +APP_ID=ai.storyteller.photocraft +FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo + +TEST=1 +TARBALL="" +while [ $# -gt 0 ]; do + case "$1" in + --no-test) TEST=0; shift ;; + -h | --help) sed -n '2,14p' "$0"; exit 0 ;; + -*) echo "unknown argument: $1" >&2; exit 2 ;; + *) TARBALL="$1"; shift ;; + esac +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) ;; + aarch64 | arm64) ARCH=aarch64 ;; + *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; +esac +BASENAME="photocraft-$VERSION-linux-$ARCH" +TARBALL="${TARBALL:-$DIST/$BASENAME.tar.gz}" +[ -f "$TARBALL" ] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; } +for tool in flatpak flatpak-builder; do + command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; } +done + +echo "==> PhotoCraft $VERSION Flatpak bundle for $ARCH from $(basename "$TARBALL")" + +WORK="$CARGO_TARGET_DIR/flatpak-bundle" +rm -rf "$WORK" +mkdir -p "$WORK/stage" +tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1 +[ -x "$WORK/stage/bin/photocraft" ] || { echo "error: $TARBALL has no bin/photocraft" >&2; exit 1; } +cp "$HERE/flatpak/$APP_ID.bundle.yml" "$WORK/$APP_ID.yml" + +flatpak remote-add --user --if-not-exists flathub "$FLATHUB" +# --disable-rofiles-fuse: no FUSE in containers and CI runners. +flatpak-builder --user --install-deps-from=flathub --disable-rofiles-fuse --force-clean \ + --default-branch=stable --repo="$WORK/repo" --state-dir="$WORK/state" "$WORK/build" "$WORK/$APP_ID.yml" + +OUT="$DIST/$BASENAME.flatpak" +# --runtime-repo lets `flatpak install` fetch the freedesktop runtime from Flathub if missing. +flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable +echo "wrote $OUT" + +if [ "$TEST" = 1 ]; then + flatpak install --user -y --noninteractive --reinstall "$OUT" + flatpak info --user "$APP_ID" + flatpak run --command=photocraft-cli "$APP_ID" --version + flatpak run --command=sh "$APP_ID" -c 'ls /app/share/applications /app/share/metainfo /app/share/mime/packages /app/share/icons/hicolor/scalable/apps' +fi +echo "==> done" +ls -lh "$OUT" diff --git a/packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml b/packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml new file mode 100644 index 0000000000..0b771ae7c7 --- /dev/null +++ b/packaging/linux/flatpak/ai.storyteller.photocraft.bundle.yml @@ -0,0 +1,50 @@ +# Flatpak manifest for the single-file bundle attached to every GitHub release +# (photocraft--linux-.flatpak). Build it with packaging/linux/flatpak-bundle.sh. +# +# It repackages the release's own Linux tarball (photocraft--linux-.tar.gz, +# built on Ubuntu 22.04 by package.sh) instead of compiling Rust inside flatpak-builder: no +# network and no vendored crate sources during the build, a few seconds instead of 20+ minutes, +# and byte-for-byte the same binaries as the AppImage/deb/rpm. The script unpacks the tarball +# into `stage/` next to a copy of this file. The binaries need glibc >= 2.35 and load X11, +# Wayland, xkbcommon and Vulkan at runtime; the freedesktop runtime has all of them, and the +# GPU driver comes from its org.freedesktop.Platform.GL extension. +# +# ai.storyteller.photocraft.yml (same directory) is the from-source manifest for Flathub. Keep +# runtime-version and finish-args identical in both (packaging-lint checks this). +id: ai.storyteller.photocraft +runtime: org.freedesktop.Platform +runtime-version: '26.08' +sdk: org.freedesktop.Sdk +command: photocraft +finish-args: + # Display: Wayland first; X11 only when no Wayland compositor is running. + - --socket=wayland + - --socket=fallback-x11 + # X11 shared memory (MIT-SHM). Without it the X11 fallback is slow or fails. + - --share=ipc + # GPU access for the wgpu compositor (Vulkan, or GL as a fallback). + - --device=dri + # Open/Save dialogs go through the file-chooser portal (rfd's xdg-portal backend) and + # drag-and-drop through the document portal, so picked files work anywhere without broad + # filesystem access. Pictures and Documents are granted so files passed by path (the command + # line, Open Recent, "Save" next to the original) work in the usual places. + - --filesystem=xdg-pictures + - --filesystem=xdg-documents + # Deliberately not granted: network (the optional --control server listens on 127.0.0.1 in + # the sandbox; `flatpak override --user --share=network ai.storyteller.photocraft` exposes it to + # host agents) and home/host filesystem access. +modules: + - name: photocraft + buildsystem: simple + build-commands: + - install -Dm755 bin/photocraft /app/bin/photocraft + - install -Dm755 bin/photocraft-cli /app/bin/photocraft-cli + - install -Dm644 share/applications/ai.storyteller.photocraft.desktop /app/share/applications/ai.storyteller.photocraft.desktop + # .pcraft, .psb and .qoi; the runtime's shared-mime-info already knows PSD and the rest. + - install -Dm644 share/mime/packages/ai.storyteller.photocraft.xml /app/share/mime/packages/ai.storyteller.photocraft.xml + # Already has the release's version and date substituted by package.sh. + - install -Dm644 share/metainfo/ai.storyteller.photocraft.metainfo.xml /app/share/metainfo/ai.storyteller.photocraft.metainfo.xml + - mkdir -p /app/share/icons /app/share/doc && cp -R share/icons/hicolor /app/share/icons/ && cp -R share/doc/photocraft /app/share/doc/ + sources: + - type: dir + path: stage diff --git a/packaging/linux/flatpak/ai.storyteller.photocraft.yml b/packaging/linux/flatpak/ai.storyteller.photocraft.yml index c05ea72409..0a6fae4cb7 100644 --- a/packaging/linux/flatpak/ai.storyteller.photocraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.photocraft.yml @@ -8,22 +8,32 @@ # flatpak run ai.storyteller.photocraft # # For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. +# The single-file .flatpak attached to each release comes from ai.storyteller.photocraft.bundle.yml, +# which repackages the prebuilt release binaries. Keep runtime-version and finish-args identical. id: ai.storyteller.photocraft runtime: org.freedesktop.Platform -runtime-version: '25.08' +runtime-version: '26.08' sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable command: photocraft finish-args: - - --share=ipc + # Display: Wayland first; X11 only when no Wayland compositor is running. - --socket=wayland - --socket=fallback-x11 + # X11 shared memory (MIT-SHM). Without it the X11 fallback is slow or fails. + - --share=ipc + # GPU access for the wgpu compositor (Vulkan, or GL as a fallback). - --device=dri - # Open/Save dialogs use the file-chooser portal (rfd), and drag-and-drop uses the document - # portal, so broad filesystem access isn't needed. Pictures is granted for opening files by - # path from the command line. + # Open/Save dialogs go through the file-chooser portal (rfd's xdg-portal backend) and + # drag-and-drop through the document portal, so picked files work anywhere without broad + # filesystem access. Pictures and Documents are granted so files passed by path (the command + # line, Open Recent, "Save" next to the original) work in the usual places. - --filesystem=xdg-pictures + - --filesystem=xdg-documents + # Deliberately not granted: network (the optional --control server listens on 127.0.0.1 in + # the sandbox; `flatpak override --user --share=network ai.storyteller.photocraft` exposes it to + # host agents) and home/host filesystem access. build-options: append-path: /usr/lib/sdk/rust-stable/bin modules: