Files
FredApps a64b3d37d8 Narration: ship natural voices in official packages (#2318, #3773)
Official packages now build the desktop app with --features filmcraft/neural-voices, and the Voices panel offers the natural-voice download only when the build includes that feature.

The Linux riscv64 cross package leaves the feature off and relies on the UI gate. The 336 MB Kokoro model is still a one-time user download, not bundled.

Closes #2318
Closes #3773
2026-11-10 20:13:22 +01:00

196 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build and package FilmCraft for Linux (<arch> is x86_64 or aarch64, or riscv64 when cross-compiling):
#
# $DIST/filmcraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
# $DIST/filmcraft-<version>-linux-<arch>.AppImage.zsync delta updates (needs zsyncmake)
# $DIST/filmcraft-<version>-linux-<arch>.deb Debian, Ubuntu, Mint, Pop!_OS, ...
# $DIST/filmcraft-<version>-linux-<arch>.rpm Fedora, openSUSE, RHEL, ...
# $DIST/filmcraft-<version>-linux-<arch>.tar.gz plain FHS-style tree (bin/, share/)
#
# Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"]
#
# 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,
# zsyncmake (the zsync package) for the AppImage's .zsync.
#
# Cross-compiling (CI's riscv64 job, tar only): set CROSS_ARCH (riscv64), CROSS_TARGET (the Rust
# target), CROSS_COMPILE (binutils prefix, for strip) and optionally EMULATOR (qemu-riscv64) to
# smoke-test the CLI.
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
HERE="$ROOT/packaging/linux"
APP_ID=ai.storyteller.filmcraft
SKIP_BUILD=0
FORMATS="appimage deb rpm tar"
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-build) SKIP_BUILD=1; shift ;;
--formats) FORMATS="$2"; shift 2 ;;
-h | --help) sed -n '2,21p' "$0"; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
ARCH="${CROSS_ARCH:-$(uname -m)}"
case "$ARCH" in
riscv64) DEB_ARCH=riscv64 ;;
x86_64) DEB_ARCH=amd64 ;;
aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;;
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
esac
export FILMCRAFT_MAINTAINER="${FILMCRAFT_MAINTAINER:-FilmCraft maintainers <filmcraft@storyteller.ai>}"
BASENAME="filmcraft-$VERSION-linux-$ARCH"
echo "==> FilmCraft $VERSION for Linux $ARCH ($FORMATS)"
if [[ "$SKIP_BUILD" = 0 ]]; then
if [[ -n "${CROSS_TARGET:-}" ]]; then
# The riscv64 cross job is not assumed to build candle/Kokoro. Official native packages
# include natural voices (downloaded on first use); the UI hides the download when this
# feature is off (#2318, #3773).
(cd "$ROOT" && cargo build --release --locked -p filmcraft -p filmcraft-cli --target "$CROSS_TARGET")
else
# Official packages include Kokoro natural voices (downloaded on first use). #2318 #3773
(cd "$ROOT" && cargo build --release --locked -p filmcraft -p filmcraft-cli --features filmcraft/neural-voices)
fi
fi
BIN="$CARGO_TARGET_DIR/${CROSS_TARGET:+$CROSS_TARGET/}release"
WORK="$CARGO_TARGET_DIR/linux-package"
STAGE="$WORK/root"
rm -rf "$WORK"
# ---- stage an FHS tree (shared by every format) -------------------------------------------------
install -Dm755 "$BIN/filmcraft" "$STAGE/usr/bin/filmcraft"
install -Dm755 "$BIN/filmcraft-cli" "$STAGE/usr/bin/filmcraft-cli"
"${CROSS_COMPILE:-}strip" "$STAGE/usr/bin/filmcraft" "$STAGE/usr/bin/filmcraft-cli" 2>/dev/null || true
install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop"
install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml"
mkdir -p "$STAGE/usr/share/metainfo"
sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$FILMCRAFT_BUILD_DATE/g" \
"$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
mkdir -p "$STAGE/usr/share/icons"
cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/"
mkdir -p "$STAGE/usr/share/doc/filmcraft"
copy_docs "$STAGE/usr/share/doc/filmcraft"
if command -v desktop-file-validate >/dev/null; then
desktop-file-validate "$STAGE/usr/share/applications/$APP_ID.desktop"
fi
if command -v appstreamcli >/dev/null; then
appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
fi
has() { local fmt="$1"; case " $FORMATS " in *" $fmt "*) return 0 ;; *) return 1 ;; esac; }
# ---- .tar.gz ------------------------------------------------------------------------------------
if has tar; then
mkdir -p "$WORK/tar"
cp -R "$STAGE/usr" "$WORK/tar/$BASENAME"
install -m755 "$HERE/install.sh" "$WORK/tar/$BASENAME/install.sh"
tar -C "$WORK/tar" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME"
echo "wrote $DIST/$BASENAME.tar.gz"
fi
# ---- .deb / .rpm --------------------------------------------------------------------------------
if has deb || has rpm; then
command -v nfpm >/dev/null || { echo "error: nfpm not found (https://nfpm.goreleaser.com/install/)" >&2; exit 1; }
export VERSION
export NFPM_ARCH="$DEB_ARCH"
# nfpm expands env vars in fields like `version` and `arch`, but not in `contents[].src`.
sed "s|\${STAGE}|$STAGE|g" "$HERE/nfpm.yaml" >"$WORK/nfpm.yaml"
for fmt in deb rpm; do
if has "$fmt"; then (cd "$ROOT" && nfpm package -f "$WORK/nfpm.yaml" -p "$fmt" -t "$DIST/$BASENAME.$fmt"); fi
done
fi
# ---- AppImage -----------------------------------------------------------------------------------
if has appimage; then
APPIMAGETOOL_VERSION=1.9.1
# appimagetool otherwise embeds type2-runtime's unchecked `continuous` build in every AppImage.
RUNTIME_VERSION=20251108
case "$ARCH" in
x86_64)
APPIMAGETOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
RUNTIME_SHA256=2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d
;;
aarch64)
APPIMAGETOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158
RUNTIME_SHA256=00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444
;;
*) APPIMAGETOOL_SHA256='' RUNTIME_SHA256='' ;;
esac
APPDIR="$WORK/FilmCraft.AppDir"
cp -R "$STAGE" "$APPDIR"
ln -s usr/bin/filmcraft "$APPDIR/AppRun"
cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop"
cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png"
ln -s "$APP_ID.png" "$APPDIR/.DirIcon"
if [[ -z "$RUNTIME_SHA256" ]]; then
echo "no pinned AppImage runtime for $ARCH" >&2
exit 2
fi
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$RUNTIME_VERSION-$ARCH"
if [[ ! -f "$RUNTIME" ]]; then
download="$RUNTIME.download"
curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/type2-runtime/releases/download/$RUNTIME_VERSION/runtime-$ARCH"
printf '%s %s\n' "$RUNTIME_SHA256" "$download" | sha256sum -c -
mv "$download" "$RUNTIME"
fi
printf '%s %s\n' "$RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
if [[ -z "$TOOL" ]]; then
if [[ -z "$APPIMAGETOOL_SHA256" ]]; then
echo "no pinned appimagetool for $ARCH; set APPIMAGETOOL or put appimagetool on PATH" >&2
exit 2
fi
TOOL="$CARGO_TARGET_DIR/appimagetool-$APPIMAGETOOL_VERSION-$ARCH.AppImage"
if [[ ! -x "$TOOL" ]]; then
download="$TOOL.download"
curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
chmod 755 "$download"
mv "$download" "$TOOL"
fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
fi
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative, e.g. target/agent-<name>).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
APPDIR="$(cd "$APPDIR" && pwd)"
TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")"
RUNTIME="$(cd "$(dirname "$RUNTIME")" && pwd)/$(basename "$RUNTIME")"
# 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/filmcraft}"
UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|filmcraft-*-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. 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 --runtime-file "$RUNTIME" -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
if [[ -z "${CROSS_TARGET:-}" ]]; then
"$STAGE/usr/bin/filmcraft-cli" --version
elif [[ -n "${EMULATOR:-}" ]]; then
"$EMULATOR" "$STAGE/usr/bin/filmcraft-cli" --version
fi
echo "==> done"
ls -lh "$DIST"