#!/usr/bin/env bash # Build and package PhotoCraft for Linux ( is x86_64, aarch64 or riscv64): # # $DIST/photocraft--linux-.AppImage any distro with glibc >= the build host's # $DIST/photocraft--linux-.AppImage.zsync delta updates (needs zsyncmake) # $DIST/photocraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... # $DIST/photocraft--linux-.rpm Fedora, openSUSE, RHEL, ... # $DIST/photocraft--linux-.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). By default AppImage tooling # is downloaded at verified pins into $CARGO_TARGET_DIR. 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 builds (CI: riscv64, tar.gz only): set CROSS_ARCH (riscv64), CROSS_TARGET (the Rust # triple), CROSS_COMPILE (binutils prefix, for strip) and optionally EMULATOR (e.g. qemu-riscv64) # to smoke-test the CLI and run the example that writes its man pages and completions. set -euo pipefail # shellcheck source=../env.sh . "$(dirname "${BASH_SOURCE[0]}")/../env.sh" HERE="$ROOT/packaging/linux" # shellcheck source=verified-download.sh . "$HERE/verified-download.sh" APP_ID=ai.storyteller.photocraft 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,19p' "$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 PHOTOCRAFT_MAINTAINER="${PHOTOCRAFT_MAINTAINER:-PhotoCraft maintainers }" BASENAME="photocraft-$VERSION-linux-$ARCH" echo "==> PhotoCraft $VERSION for Linux $ARCH ($FORMATS)" # The man-and-completions example writes photocraft-cli's man pages and shell completions below. It # is built in its own cargo run: built with the binaries, its dev-dependencies' features would be # unified into them. if [[ "$SKIP_BUILD" = 0 ]]; then if [[ -n "${CROSS_TARGET:-}" ]]; then (cd "$ROOT" && cargo build --release --locked -p photocraft -p photocraft-cli --features heif --bins --target "$CROSS_TARGET") (cd "$ROOT" && cargo build --release --locked -p photocraft-cli --features heif --example man-and-completions --target "$CROSS_TARGET") else (cd "$ROOT" && cargo build --release --locked -p photocraft -p photocraft-cli --features heif --bins) (cd "$ROOT" && cargo build --release --locked -p photocraft-cli --features heif --example man-and-completions) 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/photocraft" "$STAGE/usr/bin/photocraft" install -Dm755 "$BIN/photocraft-cli" "$STAGE/usr/bin/photocraft-cli" "${CROSS_COMPILE:-}strip" "$STAGE/usr/bin/photocraft" "$STAGE/usr/bin/photocraft-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@/$PHOTOCRAFT_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/photocraft" copy_docs "$STAGE/usr/share/doc/photocraft" # photocraft-cli's man pages and shell completions, from its clap definition, written by the # example built above. A cross build runs it through $EMULATOR, like the --version check at the # end, and leaves them out without one, as a --skip-build run whose prebuilt binaries came without # the example does. zsh reads site-functions on Fedora and openSUSE; the deb also links the # completion into Debian's vendor-completions (nfpm.yaml). CLI_DOCS="$BIN/examples/man-and-completions" if [[ -n "${CROSS_TARGET:-}" ]] && [[ -z "${EMULATOR:-}" ]]; then warn "no EMULATOR to run the $CROSS_TARGET build, so photocraft-cli's man pages and completions are left out" elif [[ ! -x "$CLI_DOCS" ]]; then warn "$CLI_DOCS is not built, so photocraft-cli's man pages and completions are left out" else if [[ -n "${CROSS_TARGET:-}" ]]; then "$EMULATOR" "$CLI_DOCS" "$WORK/cli-docs" else "$CLI_DOCS" "$WORK/cli-docs" fi install -Dm644 -t "$STAGE/usr/share/man/man1" "$WORK"/cli-docs/*.1 install -Dm644 "$WORK/cli-docs/photocraft-cli.bash" "$STAGE/usr/share/bash-completion/completions/photocraft-cli" install -Dm644 "$WORK/cli-docs/_photocraft-cli" "$STAGE/usr/share/zsh/site-functions/_photocraft-cli" install -Dm644 "$WORK/cli-docs/photocraft-cli.fish" "$STAGE/usr/share/fish/vendor_completions.d/photocraft-cli.fish" fi 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() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } # ---- .tar.gz ------------------------------------------------------------------------------------ if has tar; then mkdir -p "$WORK/tar" cp -R "$STAGE/usr" "$WORK/tar/$BASENAME" 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`. # Without the completions (a cross build with no EMULATOR) the deb's zsh symlink would dangle, # so its entry is dropped too. DROP_ZSH_LINK= [[ -f "$STAGE/usr/share/zsh/site-functions/_photocraft-cli" ]] || DROP_ZSH_LINK='\|^ - src: /usr/share/zsh/site-functions/|,/packager: deb/d;' sed "${DROP_ZSH_LINK}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 APPDIR="$WORK/PhotoCraft.AppDir" cp -R "$STAGE" "$APPDIR" # AppRun is a script, not a symlink: it installs the .desktop entry and icons into # $XDG_DATA_HOME before launching, so Wayland compositors can resolve the window's # app_id to our icon instead of the generic Wayland logo (#593; opt-out env in the # script header). Logic is covered by packaging/linux/apprun-test.sh. install -Dm755 "$HERE/AppRun" "$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" # Digests from the official GitHub release asset metadata: # https://github.com/AppImage/appimagetool/releases/tag/1.9.1 # https://github.com/AppImage/type2-runtime/releases/tag/20251108 # Keep both pins together: # appimagetool otherwise downloads the mutable latest type2 runtime itself. TOOL_VERSION=1.9.1 RUNTIME_VERSION=20251108 case "$ARCH" in x86_64) TOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 RUNTIME_SHA256=2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d ;; aarch64) TOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 RUNTIME_SHA256=00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444 ;; esac mkdir -p "$CARGO_TARGET_DIR" TOOL="${APPIMAGETOOL:-$CARGO_TARGET_DIR/appimagetool-$TOOL_VERSION-$ARCH.AppImage}" if [[ -n "${APPIMAGETOOL:-}" ]]; then verify_appimagetool_override "$TOOL" "$TOOL_SHA256" "$TOOL_VERSION" "$ARCH" else download_verified \ "https://github.com/AppImage/appimagetool/releases/download/$TOOL_VERSION/appimagetool-$ARCH.AppImage" \ "$TOOL" "$TOOL_SHA256" true fi RUNTIME="$CARGO_TARGET_DIR/type2-runtime-$RUNTIME_VERSION-$ARCH" download_verified \ "https://github.com/AppImage/type2-runtime/releases/download/$RUNTIME_VERSION/runtime-$ARCH" \ "$RUNTIME" "$RUNTIME_SHA256" # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be # relative, e.g. target/agent-). 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 (#349): 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/photocraft}" UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|photocraft-*-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/photocraft-cli" --version elif [[ -n "${EMULATOR:-}" ]]; then "$EMULATOR" "$STAGE/usr/bin/photocraft-cli" --version fi echo "==> done" ls -lh "$DIST"