diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml
index 8c6210c4..ea726d5d 100644
--- a/.github/workflows/packaging-lint.yml
+++ b/.github/workflows/packaging-lint.yml
@@ -54,4 +54,15 @@ jobs:
desktop-file-validate packaging/linux/ai.storyteller.effectcraft.desktop
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.effectcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.effectcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.effectcraft.metainfo.xml"
- python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.effectcraft.yml')); assert m['id'] == 'ai.storyteller.effectcraft' and m['command'] == 'effectcraft'"
+ python3 - <<'EOF'
+ import yaml
+ src = yaml.safe_load(open("packaging/linux/flatpak/ai.storyteller.effectcraft.yml"))
+ bundle = yaml.safe_load(open("packaging/linux/flatpak/ai.storyteller.effectcraft.bundle.yml"))
+ for m in (src, bundle):
+ assert m["id"] == "ai.storyteller.effectcraft" and m["command"] == "effectcraft", 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
+ - name: FreeBSD package layout (stub binaries)
+ run: bash packaging/freebsd/package.sh --dry-run
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ce8e57d0..1abb43f4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,9 +1,12 @@
# Release pipeline: every push to `release` builds signed installers for every platform and
# creates or updates the draft GitHub Release "EffectCraft v". See docs/releasing.md.
#
-# All jobs run in the `release` environment (restricted to the `release` branch), which holds
-# the signing secrets. Every secret is optional: missing ones produce unsigned artifacts and a
-# warning, never a failed build.
+# The jobs that sign (macOS, Windows) and the one that publishes the draft run in the `release`
+# environment (restricted to the `release` branch), which holds the signing secrets. Every secret
+# is optional: missing ones produce unsigned artifacts and a warning, never a failed build. The
+# jobs that sign nothing (version, Linux, Flatpak, FreeBSD, web) run without it, so they get no
+# secrets and a dispatch on any branch builds and checks them as a dry run; there the signed jobs
+# are refused by the environment's branch rule and the draft release job is skipped.
name: Release
on:
@@ -35,7 +38,7 @@ jobs:
version:
name: version
runs-on: ubuntu-latest
- environment: release
+ # No `environment: release`: it reads Cargo.toml only (see the header).
outputs:
version: ${{ steps.v.outputs.version }}
date: ${{ steps.v.outputs.date }}
@@ -157,7 +160,7 @@ jobs:
# The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35 (Ubuntu 22.04+,
# Debian 12+, Fedora 36+, RHEL 10). See docs/releasing.md.
runs-on: ${{ matrix.runner }}
- environment: release
+ # No `environment: release`: this job signs nothing, so it gets no release secrets.
strategy:
fail-fast: false
matrix:
@@ -175,7 +178,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev libasound2-dev \
- desktop-file-utils appstream python3-yaml
+ desktop-file-utils appstream python3-yaml zsync
curl -fsSL -o "$RUNNER_TEMP/nfpm.deb" \
"https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${{ matrix.deb }}.deb"
sudo dpkg -i "$RUNNER_TEMP/nfpm.deb"
@@ -197,6 +200,9 @@ jobs:
for appimage in dist/release/*.AppImage; do
chmod +x "$appimage"
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
+ # Update information is embedded, and the .zsync for delta updates sits beside it.
+ "$appimage" --appimage-updateinformation | grep -F 'gh-releases-zsync|'
+ test -s "$appimage.zsync"
done
- uses: actions/upload-artifact@v4
with:
@@ -204,11 +210,93 @@ 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:
+ EFFECTCRAFT_VERSION: ${{ needs.version.outputs.version }}
+ EFFECTCRAFT_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/effectcraft-$EFFECTCRAFT_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
+
+ freebsd:
+ name: FreeBSD x86_64
+ needs: version
+ # GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM, the same image and packages as
+ # .github/workflows/freebsd.yml.
+ runs-on: ubuntu-latest
+ timeout-minutes: 240
+ # No `environment: release`: this job signs nothing, so it gets no release secrets.
+ env:
+ EFFECTCRAFT_VERSION: ${{ needs.version.outputs.version }}
+ EFFECTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build and package in a FreeBSD VM (tar.gz)
+ # vmactions/freebsd-vm v1.5.9, pinned by commit (it runs our build).
+ uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc
+ with:
+ release: "14.3"
+ usesh: true
+ cpu: 4
+ mem: 12288
+ # Passed into the VM by name; never templated into the script below.
+ envs: "EFFECTCRAFT_VERSION EFFECTCRAFT_BUILD_DATE EFFECTCRAFT_BUILD_SHA INPUT_VERSION"
+ # The FreeBSD CI packages, plus bash for the packaging script.
+ prepare: |
+ pkg install -y bash git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
+ run: |
+ set -e
+ . "$HOME/.cargo/env"
+ git config --global --add safe.directory "$PWD"
+ # Build outside the checkout so only dist/ is copied back out of the VM (copying the
+ # target dir back fails on large builds). 4 jobs: more runs the VM out of memory.
+ export CARGO_TARGET_DIR=/var/tmp/effectcraft-target CARGO_BUILD_JOBS=4
+ if [ -n "$INPUT_VERSION" ]; then cargo xtask version set "$EFFECTCRAFT_VERSION"; fi
+ bash packaging/freebsd/package.sh
+ tar -tzf dist/release/effectcraft-*-freebsd-x86_64.tar.gz \
+ | grep -E '/bin/effectcraft$|/bin/effectcraft-cli$|/share/applications/ai\.storyteller\.effectcraft\.desktop$|/share/doc/effectcraft/LICENSE-MIT$'
+ - uses: actions/upload-artifact@v4
+ with:
+ name: release-freebsd-x86_64
+ path: dist/release/*
+ if-no-files-found: error
+
web:
name: Web (wasm)
needs: version
runs-on: ubuntu-latest
- environment: release
+ # No `environment: release`: this job signs nothing, so it gets no release secrets.
env:
EFFECTCRAFT_VERSION: ${{ needs.version.outputs.version }}
EFFECTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
@@ -241,7 +329,8 @@ jobs:
release:
name: Draft GitHub Release
- needs: [version, macos, windows, linux, web]
+ # Needs the signed jobs, so a dry run on a branch (where they are refused) never gets here.
+ needs: [version, macos, windows, linux, flatpak, freebsd, web]
runs-on: ubuntu-latest
environment: release
permissions:
diff --git a/README.md b/README.md
index f738bb1d..7c7a148f 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@
Get started ·
Status ·
How it's made ·
+ Downloads ·
The Crafting Apps ·
License
@@ -182,8 +183,9 @@ See [docs/agents.md](docs/agents.md) and [docs/control-protocol.md](docs/control
## Get started
Installers for each version are on the [Releases](https://github.com/storytold/effectcraft/releases)
-page: a universal macOS app; Windows MSIs and portable zips for x64, x86 and ARM64; and Linux
-AppImage, deb, rpm and tar.gz for x86_64 and aarch64. The Windows ARM64 build runs natively on
+page: a universal macOS app; Windows MSIs and portable zips for x64, x86 and ARM64; Linux
+AppImage, Flatpak, deb, rpm and tar.gz for x86_64 and aarch64; a FreeBSD x86_64 tarball; and the
+web build. [Downloads](#downloads) lists every file. The Windows ARM64 build runs natively on
Windows on ARM, without x64 emulation. CI installs that MSI on Windows 11 ARM64 hardware, checks
both programs are ARM64 and runs the command-line tool there, but it doesn't open the app's window
or run the test suite natively on ARM64 yet, so please report anything that behaves differently.
@@ -235,6 +237,50 @@ send us right now.
- **One family.** EffectCraft shares its time model and text engine design with FilmCraft, and
gets its video and audio codecs from it.
+## Downloads
+
+Every [release](https://github.com/storytold/effectcraft/releases/latest) ships these builds. `` is the
+version number; `SHA256SUMS.txt` lists a checksum for every file.
+
+### macOS
+
+| Build | File | Notes |
+|---|---|---|
+| App, universal (Apple silicon + Intel) | `effectcraft--macos-universal.dmg` | Signed and notarized |
+| Command-line tool, universal | `effectcraft-cli--macos-universal.zip` | Signed and notarized |
+
+### Windows
+
+| Build | Installer | Portable |
+|---|---|---|
+| x64 (64-bit Intel/AMD) | `effectcraft--windows-x64.msi` | `effectcraft--windows-x64-portable.zip` |
+| arm64 (Snapdragon and other ARM PCs) | `effectcraft--windows-arm64.msi` | `effectcraft--windows-arm64-portable.zip` |
+| x86 (32-bit) | `effectcraft--windows-x86.msi` | `effectcraft--windows-x86-portable.zip` |
+
+Installers and executables are code-signed.
+
+### Linux
+
+| Format | x86_64 | aarch64 (ARM64) | Notes |
+|---|---|---|---|
+| AppImage | `effectcraft--linux-x86_64.AppImage` | `effectcraft--linux-aarch64.AppImage` | Runs anywhere; updates itself with [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) (`.zsync` files) |
+| Flatpak | `effectcraft--linux-x86_64.flatpak` | `effectcraft--linux-aarch64.flatpak` | Sandboxed; `flatpak install --user ` |
+| Debian/Ubuntu | `effectcraft--linux-x86_64.deb` | `effectcraft--linux-aarch64.deb` | |
+| Fedora/RHEL/openSUSE | `effectcraft--linux-x86_64.rpm` | `effectcraft--linux-aarch64.rpm` | |
+| Tarball | `effectcraft--linux-x86_64.tar.gz` | `effectcraft--linux-aarch64.tar.gz` | Unpack anywhere |
+
+### FreeBSD
+
+| Build | File |
+|---|---|
+| x86_64 | `effectcraft--freebsd-x86_64.tar.gz` |
+
+### Web (WebAssembly)
+
+| Build | File | Notes |
+|---|---|---|
+| Static site | `effectcraft-web-.zip` | Runs in a modern browser; host it on any static server |
+
## The Crafting Apps
EffectCraft is one of the **Crafting Apps**: free, open-source creative tools from the
diff --git a/docs/releasing.md b/docs/releasing.md
index e8e26cbc..746bc2ec 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -1,7 +1,8 @@
# Releasing EffectCraft
Every push to the `release` branch runs [`.github/workflows/release.yml`](../.github/workflows/release.yml).
-It builds signed installers for macOS, Windows, Linux and the web, then creates or updates a
+It builds signed installers for macOS and Windows, packages for Linux and FreeBSD, and the web
+build, then creates or updates a
**draft** GitHub Release named `EffectCraft v`. Nobody sees a draft until a maintainer
publishes it.
@@ -41,9 +42,11 @@ pushing to release.`), so bump it first.
**Test runs:** *Actions › Release › Run workflow* runs the whole pipeline by hand. The optional
`version` input (such as `0.4.0-rc.1`) overrides `Cargo.toml` for that run only: the jobs apply it
-with `cargo xtask version set` before building, so the binaries report it too. The run still
-needs the `release` environment, which only the `release` branch can use, so pick that branch in
-the dialog.
+with `cargo xtask version set` before building, so the binaries report it too. Signing and the
+draft release need the `release` environment, which only the `release` branch can use. Run on
+any other branch (`gh workflow run release.yml --ref `), it is a dry run: the jobs that
+sign nothing (Linux, Flatpak, FreeBSD, web) build and check everything, the macOS and Windows
+jobs are refused by the environment's branch rule, and no draft release is made.
## What gets built
@@ -55,6 +58,9 @@ the dialog.
| Windows 11 ARM64 | `effectcraft--windows-arm64.msi`, `effectcraft--windows-arm64-portable.zip` | cross-compiled on `windows-latest` |
| Linux x86_64 | `effectcraft--linux-x86_64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04` |
| Linux aarch64 | `effectcraft--linux-aarch64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` |
+| Linux AppImage updates | `effectcraft--linux-{x86_64,aarch64}.AppImage.zsync` | with the AppImages |
+| Flatpak x86_64, aarch64 | `effectcraft--linux-{x86_64,aarch64}.flatpak` (the Linux tarball, repackaged) | `ubuntu-24.04`, `ubuntu-24.04-arm` |
+| FreeBSD 14+ x86_64 | `effectcraft--freebsd-x86_64.tar.gz` | a FreeBSD 14.3 VM on `ubuntu-latest` |
| Web | `effectcraft-web-.zip` (a static site; see [web.md](web.md)) | `ubuntu-latest` |
The ARM64 MSI is installed and run on ARM64 hardware by
@@ -72,10 +78,33 @@ RHEL 10. Building on a newer image would silently raise that floor. `packaging/l
builds the AppImage, `.deb`, `.rpm` and tarball; the workflow then runs the AppImage's
`--version` as a smoke test.
+### Linux: AppImage updates and Flatpak
+
+Each AppImage embeds update information
+(`gh-releases-zsync|storytold|effectcraft|latest|effectcraft-*-linux-.AppImage.zsync`), and
+the `.zsync` file published beside it lets [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate)
+and AppImageLauncher fetch only the changed blocks from the newest published (non-pre-release)
+version. `package.sh` writes the `.zsync` when `zsyncmake` (the `zsync` package) is installed; the
+workflow checks both.
+
+The `.flatpak` bundles repackage the Linux job's tarball with
+`packaging/linux/flatpak-bundle.sh` and `packaging/linux/flatpak/ai.storyteller.effectcraft.bundle.yml`
+(no Rust build inside flatpak-builder), then install the bundle and run `effectcraft-cli --version`
+in the sandbox. `ai.storyteller.effectcraft.yml` is the from-source manifest for Flathub; packaging
+lint keeps the runtime and sandbox permissions of the two identical.
+
+### FreeBSD
+
+GitHub has no FreeBSD runners, so the job builds in a FreeBSD 14.3 VM
+(`vmactions/freebsd-vm`, pinned by commit) with the packages
+[`freebsd.yml`](../.github/workflows/freebsd.yml) uses, and `packaging/freebsd/package.sh` makes a
+`/usr/local`-style tarball: `tar -xzf effectcraft--freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local`.
+
## Signing
-Every job runs in the `release` environment, which only the `release` branch can use and which
-holds the signing secrets. Every secret is optional: a missing one produces an unsigned artifact
+The macOS and Windows jobs and the draft-release job run in the `release` environment, which only
+the `release` branch can use and which holds the signing secrets. The jobs that sign nothing run
+without it and get no secrets. Every secret is optional: a missing one produces an unsigned artifact
and a warning in the job summary, never a failed build.
| Platform | Secrets |
diff --git a/packaging/freebsd/package.sh b/packaging/freebsd/package.sh
new file mode 100755
index 00000000..236c464a
--- /dev/null
+++ b/packaging/freebsd/package.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env bash
+# Build and package EffectCraft for FreeBSD:
+#
+# $DIST/effectcraft--freebsd-x86_64.tar.gz a /usr/local-style tree:
+# effectcraft--freebsd-x86_64/{bin, share/applications, share/icons, share/mime,
+# share/metainfo, share/doc/effectcraft}
+#
+# Install by copying the tree's contents into /usr/local:
+# tar -xzf effectcraft--freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local
+#
+# Usage: packaging/freebsd/package.sh [--skip-build] [--dry-run]
+# --dry-run stage the tree from stub binaries and list it, without building (works on any OS)
+#
+# Needs: bash, cargo, and the packages installed in .github/workflows/freebsd.yml.
+set -euo pipefail
+# shellcheck source=../env.sh
+. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
+LINUX="$ROOT/packaging/linux"
+APP_ID=ai.storyteller.effectcraft
+
+SKIP_BUILD=0
+DRY_RUN=0
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --skip-build) SKIP_BUILD=1; shift ;;
+ --dry-run) DRY_RUN=1; SKIP_BUILD=1; shift ;;
+ -h | --help) sed -n '2,15p' "$0"; exit 0 ;;
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
+ esac
+done
+
+# FreeBSD's uname says amd64; release file names use x86_64 like the Linux and Windows ones.
+MACHINE="$(uname -m)"
+case "$MACHINE" in
+ amd64 | x86_64) ARCH=x86_64 ;;
+ arm64 | aarch64) ARCH=aarch64 ;;
+ *) echo "unsupported architecture $MACHINE" >&2; exit 2 ;;
+esac
+if [ "$DRY_RUN" = 0 ] && [ "$(uname -s)" != FreeBSD ]; then
+ echo "error: build on FreeBSD (or pass --dry-run to check the tree layout)" >&2
+ exit 2
+fi
+BASENAME="effectcraft-$VERSION-freebsd-$ARCH"
+
+echo "==> EffectCraft $VERSION for FreeBSD $ARCH"
+
+# The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory.
+export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}"
+if [ "$SKIP_BUILD" = 0 ]; then
+ (cd "$ROOT" && cargo build --release --locked -p effectcraft -p effectcraft-cli)
+fi
+WORK="$CARGO_TARGET_DIR/freebsd-package"
+STAGE="$WORK/$BASENAME"
+rm -rf "$WORK"
+mkdir -p "$WORK"
+
+BIN="$CARGO_TARGET_DIR/release"
+OUT_DIR="$DIST"
+if [ "$DRY_RUN" = 1 ]; then
+ # Stub binaries, and the tarball stays in the work dir so dist/ only ever holds real builds.
+ BIN="$WORK/stub-bin"
+ OUT_DIR="$WORK"
+ mkdir -p "$BIN"
+ for b in effectcraft effectcraft-cli; do
+ printf '#!/bin/sh\necho "%s %s (dry-run stub)"\n' "$b" "$VERSION" >"$BIN/$b"
+ chmod 755 "$BIN/$b"
+ done
+fi
+
+# ---- stage a /usr/local-style tree --------------------------------------------------------------
+# FreeBSD's install(1) has no -D: create the directories first.
+mkdir -p "$STAGE/bin" "$STAGE/share/applications" "$STAGE/share/mime/packages" \
+ "$STAGE/share/metainfo" "$STAGE/share/icons" "$STAGE/share/doc/effectcraft"
+install -m 755 "$BIN/effectcraft" "$BIN/effectcraft-cli" "$STAGE/bin/"
+strip "$STAGE/bin/effectcraft" "$STAGE/bin/effectcraft-cli" 2>/dev/null || true
+# The desktop entry, MIME type, metainfo and icons are the freedesktop files Linux ships.
+install -m 644 "$LINUX/$APP_ID.desktop" "$STAGE/share/applications/$APP_ID.desktop"
+install -m 644 "$LINUX/$APP_ID.mime.xml" "$STAGE/share/mime/packages/$APP_ID.xml"
+sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$EFFECTCRAFT_BUILD_DATE/g" \
+ "$LINUX/$APP_ID.metainfo.xml.in" >"$STAGE/share/metainfo/$APP_ID.metainfo.xml"
+cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/"
+copy_docs "$STAGE/share/doc/effectcraft"
+for f in NOTICE ATTRIBUTION.md; do
+ if [ -f "$ROOT/$f" ]; then install -m 644 "$ROOT/$f" "$STAGE/share/doc/effectcraft/"; fi
+done
+
+for f in bin/effectcraft bin/effectcraft-cli "share/applications/$APP_ID.desktop" \
+ "share/icons/hicolor/256x256/apps/$APP_ID.png" "share/icons/hicolor/scalable/apps/$APP_ID.svg" \
+ share/doc/effectcraft/LICENSE-MIT share/doc/effectcraft/LICENSE-APACHE; do
+ if [ ! -e "$STAGE/$f" ]; then echo "error: $f is missing from the package" >&2; exit 1; fi
+done
+
+# ---- .tar.gz ------------------------------------------------------------------------------------
+mkdir -p "$OUT_DIR"
+tar -C "$WORK" -czf "$OUT_DIR/$BASENAME.tar.gz" "$BASENAME"
+echo "wrote $OUT_DIR/$BASENAME.tar.gz"
+
+"$STAGE/bin/effectcraft-cli" --version
+if [ "$DRY_RUN" = 1 ]; then
+ echo "==> dry run: tarball contents"
+ tar -tzvf "$OUT_DIR/$BASENAME.tar.gz"
+fi
+echo "==> done"
diff --git a/packaging/linux/flatpak-bundle.sh b/packaging/linux/flatpak-bundle.sh
new file mode 100755
index 00000000..d5dcdfc0
--- /dev/null
+++ b/packaging/linux/flatpak-bundle.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Repackage a EffectCraft Linux tarball as a single-file Flatpak bundle:
+#
+# $DIST/effectcraft--linux-.flatpak
+#
+# Usage: packaging/linux/flatpak-bundle.sh [--no-test] [TARBALL]
+#
+# TARBALL defaults to $DIST/effectcraft--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
+# `effectcraft-cli --version` is run inside the sandbox as a smoke test.
+# Manifest: packaging/linux/flatpak/ai.storyteller.effectcraft.bundle.yml.
+set -euo pipefail
+# shellcheck source=../env.sh
+. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
+HERE="$ROOT/packaging/linux"
+APP_ID=ai.storyteller.effectcraft
+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="effectcraft-$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 "==> EffectCraft $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/effectcraft" ] || { echo "error: $TARBALL has no bin/effectcraft" >&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=effectcraft-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.effectcraft.bundle.yml b/packaging/linux/flatpak/ai.storyteller.effectcraft.bundle.yml
new file mode 100644
index 00000000..f1074cdb
--- /dev/null
+++ b/packaging/linux/flatpak/ai.storyteller.effectcraft.bundle.yml
@@ -0,0 +1,56 @@
+# Flatpak manifest for the single-file bundle attached to every GitHub release
+# (effectcraft--linux-.flatpak). Build it with packaging/linux/flatpak-bundle.sh.
+#
+# It repackages the release's own Linux tarball (effectcraft--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, ALSA 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.effectcraft.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.effectcraft
+runtime: org.freedesktop.Platform
+runtime-version: '25.08'
+sdk: org.freedesktop.Sdk
+command: effectcraft
+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 and renderer (Vulkan, or GL as a fallback).
+ - --device=dri
+ # Audio preview and playback (cpal through ALSA, which the runtime routes to PulseAudio or
+ # PipeWire's Pulse server).
+ - --socket=pulseaudio
+ # The optional trained models (Settings ▸ Roto Brush, Settings ▸ Face Tracking) download only
+ # when the user asks, over HTTPS with curl. It also lets host agents reach the optional
+ # --control server (127.0.0.1). No telemetry: nothing else uses the network.
+ - --share=network
+ # Open/Save and import 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, Videos and Documents are granted so files passed by path (the
+ # command line, Open Recent, footage a project links to, Render Queue output) work in the
+ # usual places.
+ - --filesystem=xdg-pictures
+ - --filesystem=xdg-videos
+ - --filesystem=xdg-documents
+ # Deliberately not granted: home/host filesystem access.
+modules:
+ - name: effectcraft
+ buildsystem: simple
+ build-commands:
+ - install -Dm755 bin/effectcraft /app/bin/effectcraft
+ - install -Dm755 bin/effectcraft-cli /app/bin/effectcraft-cli
+ - install -Dm644 share/applications/ai.storyteller.effectcraft.desktop /app/share/applications/ai.storyteller.effectcraft.desktop
+ - install -Dm644 share/mime/packages/ai.storyteller.effectcraft.xml /app/share/mime/packages/ai.storyteller.effectcraft.xml
+ # Already has the release's version and date substituted by package.sh.
+ - install -Dm644 share/metainfo/ai.storyteller.effectcraft.metainfo.xml /app/share/metainfo/ai.storyteller.effectcraft.metainfo.xml
+ - mkdir -p /app/share/icons /app/share/doc && cp -R share/icons/hicolor /app/share/icons/ && cp -R share/doc/effectcraft /app/share/doc/
+ sources:
+ - type: dir
+ path: stage
diff --git a/packaging/linux/flatpak/ai.storyteller.effectcraft.yml b/packaging/linux/flatpak/ai.storyteller.effectcraft.yml
index baa6fa62..be9f898e 100644
--- a/packaging/linux/flatpak/ai.storyteller.effectcraft.yml
+++ b/packaging/linux/flatpak/ai.storyteller.effectcraft.yml
@@ -8,6 +8,10 @@
# flatpak run ai.storyteller.effectcraft
#
# For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit.
+#
+# ai.storyteller.effectcraft.bundle.yml (same directory) builds the .flatpak attached to each
+# release from the release's Linux tarball. Keep runtime-version and finish-args identical in both
+# (packaging-lint checks this).
id: ai.storyteller.effectcraft
runtime: org.freedesktop.Platform
runtime-version: '25.08'
@@ -16,15 +20,29 @@ sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: effectcraft
finish-args:
- - --share=ipc
+ # Display: Wayland first; X11 only when no Wayland compositor is running.
- --socket=wayland
- --socket=fallback-x11
- - --socket=pulseaudio
+ # X11 shared memory (MIT-SHM). Without it the X11 fallback is slow or fails.
+ - --share=ipc
+ # GPU access for the wgpu compositor and renderer (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.
+ # Audio preview and playback (cpal through ALSA, which the runtime routes to PulseAudio or
+ # PipeWire's Pulse server).
+ - --socket=pulseaudio
+ # The optional trained models (Settings ▸ Roto Brush, Settings ▸ Face Tracking) download only
+ # when the user asks, over HTTPS with curl. It also lets host agents reach the optional
+ # --control server (127.0.0.1). No telemetry: nothing else uses the network.
+ - --share=network
+ # Open/Save and import 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, Videos and Documents are granted so files passed by path (the
+ # command line, Open Recent, footage a project links to, Render Queue output) work in the
+ # usual places.
- --filesystem=xdg-pictures
+ - --filesystem=xdg-videos
+ - --filesystem=xdg-documents
+ # Deliberately not granted: home/host filesystem access.
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
modules:
@@ -35,8 +53,9 @@ modules:
CARGO_HOME: /run/build/effectcraft/cargo
CARGO_NET_OFFLINE: 'true'
build-commands:
- - cargo --offline build --release --locked -p effectcraft
+ - cargo --offline build --release --locked -p effectcraft -p effectcraft-cli
- install -Dm755 target/release/effectcraft /app/bin/effectcraft
+ - install -Dm755 target/release/effectcraft-cli /app/bin/effectcraft-cli
- install -Dm644 packaging/linux/ai.storyteller.effectcraft.desktop /app/share/applications/ai.storyteller.effectcraft.desktop
- install -Dm644 packaging/linux/ai.storyteller.effectcraft.mime.xml /app/share/mime/packages/ai.storyteller.effectcraft.xml
- mkdir -p /app/share/icons && cp -R assets/app-icon/hicolor /app/share/icons/
diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh
index 8a298078..799a0970 100755
--- a/packaging/linux/package.sh
+++ b/packaging/linux/package.sh
@@ -2,6 +2,7 @@
# Build and package EffectCraft for Linux ( is x86_64 or aarch64):
#
# $DIST/effectcraft--linux-.AppImage any distro with glibc >= the build host's
+# $DIST/effectcraft--linux-.AppImage.zsync delta updates (needs zsyncmake)
# $DIST/effectcraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ...
# $DIST/effectcraft--linux-.rpm Fedora, openSUSE, RHEL, ...
# $DIST/effectcraft--linux-.tar.gz plain FHS-style tree (bin/, share/)
@@ -10,7 +11,8 @@
#
# Needs: cargo; nfpm for deb/rpm (https://nfpm.goreleaser.com); appimagetool for the AppImage
# (downloaded into $CARGO_TARGET_DIR if missing). Build on an old distro (CI: Ubuntu 22.04,
-# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli.
+# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli,
+# zsyncmake (the zsync package) for the AppImage's .zsync.
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
@@ -23,7 +25,7 @@ while [ $# -gt 0 ]; do
case "$1" in
--skip-build) SKIP_BUILD=1; shift ;;
--formats) FORMATS="$2"; shift 2 ;;
- -h | --help) sed -n '2,15p' "$0"; exit 0 ;;
+ -h | --help) sed -n '2,17p' "$0"; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
@@ -108,11 +110,29 @@ if has appimage; then
chmod +x "$TOOL"
fi
fi
- OUT="$DIST/$BASENAME.AppImage"
+ # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
+ # relative).
+ OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
+ APPDIR="$(cd "$APPDIR" && pwd)"
+ TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")"
+ # A .zsync left by an earlier run would hide a missing zsyncmake and describe another file.
+ rm -f "$OUT.zsync"
+ # Update information: AppImageUpdate, AppImageLauncher and the like read it from the file and
+ # fetch only the blocks that changed in a newer release, through the .zsync published next to
+ # each AppImage on GitHub Releases. `latest` is the newest published release that is not a
+ # pre-release. A fork's builds point at its own releases through GITHUB_REPOSITORY.
+ REPO="${GITHUB_REPOSITORY:-storytold/effectcraft}"
+ UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|effectcraft-*-linux-$ARCH.AppImage.zsync"
# Extract-and-run: works without FUSE (containers, CI). The output embeds the static runtime,
- # so users don't need libfuse2 either.
- ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream "$APPDIR" "$OUT"
+ # so users don't need libfuse2 either. With zsyncmake on the host (CI installs the zsync
+ # package) appimagetool also writes the .zsync, into its working directory, hence the cd.
+ (cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT")
echo "wrote $OUT"
+ if [ -s "$OUT.zsync" ]; then
+ echo "wrote $OUT.zsync"
+ else
+ warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it"
+ fi
fi
"$STAGE/usr/bin/effectcraft-cli" --version