diff --git a/.tangled/workflows/release.yml b/.tangled/workflows/release.yml index fd47231..92eefa6 100644 --- a/.tangled/workflows/release.yml +++ b/.tangled/workflows/release.yml @@ -3,8 +3,10 @@ # Release pipeline for Sheaf, run by spindle.tangled.sh on every annotated # v* tag push. It does three things: # -# 1. Builds the linux-amd64 binary (wails + upx), source tarball, and a -# prebuilt sheaf-bin Arch package (*.pkg.tar.zst). arm64 is dropped for +# 1. Builds the linux-amd64 binary (wails + upx), source tarball, a +# prebuilt sheaf-bin Arch package (*.pkg.tar.zst), and an AppImage with +# zsync delta-update info pointing at sheaf/latest/ on the pages site. +# arm64 is dropped for # now, because the default spindle microvm timeout (5m) will not fit two # from-scratch native CGO builds. Restore -platform linux/arm64 once the # timeout is raised (SPINDLE_MICROVM_PIPELINES_WORKFLOW_TIMEOUT). @@ -55,6 +57,8 @@ dependencies: - librsvg # rsvg-convert: render-icons.sh rasterises the hicolor PNG set - imagemagick # magick: render-icons.sh builds the Windows .ico - python3 # make-indexes.py is stdlib-only python + - appimagekit-appimage-tool # nixpkgs attr; provides the appimagetool binary + - zsync # provides zsyncmake for the AppImage delta-update file - jq - openssh - git @@ -145,6 +149,20 @@ steps: tar -tf .release/dist/sheaf-bin-*.pkg.tar.zst | head (cd .release/dist && sha256sum sheaf-bin-*.pkg.tar.zst >> SHA256SUMS) + - name: "Build AppImage + zsync" + command: | + # nixpkgs ships the tool as `appimagetool`; make-appimage.sh defaults + # to the upstream appimagetool-x86_64.AppImage filename used locally. + # The zsync URL points at sheaf/latest/, the stable alias refreshed on + # every release, so AppImageUpdate always finds the newest build. + set -e + cd /workspace/repo + VERSION=$(cat .release/VERSION) + APPIMAGETOOL=appimagetool bash scripts/make-appimage.sh "$VERSION" \ + build/bin/sheaf .release/dist \ + "${BASE_URL}/sheaf/latest/sheaf-latest-x86_64.AppImage.zsync" + (cd .release/dist && sha256sum sheaf-*.AppImage sheaf-*.AppImage.zsync >> SHA256SUMS) + - name: "Publish to pages repo (auto-deploys katsuricata.tngl.io)" command: | set -euo pipefail @@ -175,7 +193,19 @@ steps: mkdir -p "$REL_DIR" cp .release/dist/sheaf-linux-* .release/dist/SHA256SUMS \ .release/dist/sheaf-"${VERSION}"-source.tar.gz \ - .release/dist/sheaf-bin-*-x86_64.pkg.tar.zst "$REL_DIR/" + .release/dist/sheaf-bin-*-x86_64.pkg.tar.zst \ + .release/dist/sheaf-*.AppImage .release/dist/sheaf-*.AppImage.zsync \ + "$REL_DIR/" + + # refresh the stable latest/ alias the AppImage's zsync update info + # points at. AppImageUpdate downloads the .zsync from here and + # delta-patches against the sibling AppImage, whatever version it is. + LATEST_DIR="$DEPLOY/sheaf/latest" + mkdir -p "$LATEST_DIR" + cp ".release/dist/sheaf-${VERSION}-x86_64.AppImage" \ + "$LATEST_DIR/sheaf-latest-x86_64.AppImage" + cp ".release/dist/sheaf-${VERSION}-x86_64.AppImage.zsync" \ + "$LATEST_DIR/sheaf-latest-x86_64.AppImage.zsync" # regenerate every index.html from the on-disk tree + meta python3 scripts/make-indexes.py \ diff --git a/AGENTS.md b/AGENTS.md index b35f0ab..6ed702c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ There is no linter/formatter config. CI is `.tangled/workflows/ci.yml` (push/PR - **App icon is platform-split**: `build/appicon.png` is only the *build-time* source for Windows `.ico`/macOS `.icns` packaging (plus the `iconName` refs in `wails.json` for Linux file-association icons, and `build/windows/icon.ico`). The **Linux runtime window/titlebar icon** comes from `main.go`'s `//go:embed build/appicon.png` + `Linux: &linux.Options{Icon: appIcon}`. Wails never reads `build/appicon.png` on Linux. If `Icon` is nil, Wails falls back to its default "W" logo. To change the icon, replace `build/appicon.png`, regenerate the `.ico`, and keep the embed wired. If the titlebar still shows the W, the embed/`linux.Options` link is broken. **On Wayland the runtime icon is ignored** — the protocol has no client-set window icon. KWin/GNOME resolve the titlebar/taskbar icon from the app's `.desktop` file and its hicolor icon entry, matched by the window's **app-id**. The full chain must use the **reverse-DNS id `com.doubledecimal.sheaf`**: `Linux.ProgramName` (sets the app-id), the desktop filename `com.doubledecimal.sheaf.desktop`, its `StartupWMClass`/`Icon=` keys, and the installed icon names `com.doubledecimal.sheaf.{svg,png}`. A flat `sheaf` id matched the desktop file (the taskbar icon worked) but KWin's *titlebar* decoration stayed blank. The reverse-DNS id made the decoration resolve. `packaging/linux/` ships both files, and every packaging path installs them (`just desktop-install`, `scripts/make-pkg.sh` → `sheaf-bin`, `aur/PKGBUILD.sheaf-git`). If you run the bare `build/bin/sheaf` with no `.desktop` installed, the titlebar shows the fallback W regardless of `Linux.Icon`. Keep `main.go`, `packaging/`, the justfile, `make-pkg.sh`, and the AUR PKGBUILDs in sync. To debug app-id matching, inject a KWin script (`loadScript` → object path `/Scripting/ScriptN` → `org.kde.kwin.Script.run`) that `print()`s `w.desktopFileName` and `w.resourceClass` for every `workspace.windowList()` entry. Then read `journalctl --user -b`. - **The canonical icon is the SVG** (`packaging/linux/sheaf.svg`), not any PNG. `scripts/render-icons.sh` (needs `librsvg` + `imagemagick`) derives everything raster from it: the hicolor PNG set under `build/icons//com.doubledecimal.sheaf.png` (16/32/48/64/128/256/512/1024), the master `build/appicon.png`, and `build/windows/icon.ico`. After you edit the mark, re-run that script and rebuild. Never edit `build/appicon.png` directly, or the embed, hicolor set, and `.ico` drift apart. Packaging installs the **scalable SVG plus the full PNG size set**, because Qt's `QIcon::fromTheme` (KWin titlebar/taskbar) does not reliably downscale a lone 1024px PNG to 16–48px. **A user-local (`~/.local`) hicolor dir also needs an `index.theme` and `gtk-update-icon-cache` before Qt resolves anything**. The system-wide `/usr/share/icons/hicolor` already has an index (owned by the `hicolor-icon-theme` package), so only `just desktop-install` into `$HOME` hits this. `render-icons.sh` and `desktop-install` do not create the index. A fresh user dir needs it written once by hand. - **Copy HTML and Print** are one-line bound methods (`CopyHTML` → `runtime.ClipboardSetText`, `PrintDocument` → `runtime.WindowPrint`). Print output depends on the `@media print` block at the bottom of `frontend/src/style.css`, which flattens the rack (toolbar, scanlines, deckle edges, shadows) to plain black-on-white and unrolls the `.content` scroll container; without it only the visible viewport page would print. -- **Releases run on Spindle** (`.tangled/workflows/release.yml`, triggered by annotated `v*` tag pushes). The pipeline builds amd64/arm64 binaries + a `sheaf-bin` pacman package, pushes them to the separate `pages` git repo under `sheaf/vX.Y.Z/`, and that repo is the auto-deployed index site for `https://katsuricata.tngl.io`. `scripts/make-indexes.py` regenerates the site's `index.html`s and `.meta/.json` from the on-disk tree; `scripts/make-pkg.sh` assembles the pacman package without makepkg (NixOS CI has none). Two repo secrets gate it: `SHEAF_PAGES_SSH_KEY` (passphrase-less deploy key, public half on the Tangled account) and `ATP_APP_PASSWORD` (bsky.social app password for the `sh.tangled.repo.artifact` record). Full setup/cut list in `RELEASE.md`. +- **Releases run on Spindle** (`.tangled/workflows/release.yml`, triggered by annotated `v*` tag pushes). The pipeline builds amd64/arm64 binaries + a `sheaf-bin` pacman package + an x86_64 **AppImage with zsync delta-update info** (`scripts/make-appimage.sh`, deps `appimagekit-appimage-tool` + `zsync`; the nix binary is `appimagetool`, locally it's `appimagetool-x86_64.AppImage` — the script takes `$APPIMAGETOOL` to override), pushes them to the separate `pages` git repo under `sheaf/vX.Y.Z/`, and that repo is the auto-deployed index site for `https://katsuricata.tngl.io`. The AppImage's update info is `zsync|/sheaf/latest/sheaf-latest-x86_64.AppImage.zsync`, and the pipeline refreshes that `sheaf/latest/` alias on every release, so AppImageUpdate always finds the newest build; the `.zsync` uses a relative `URL:` header (`zsyncmake -u`) so it also works from the versioned dir. The AppImage bundles no runtime — it links the host's gtk3/webkit2gtk-4.1 like the raw binary. Local build: `just appimage` (no update info baked in, so AppImageUpdate correctly treats it as non-updatable). `scripts/make-indexes.py` regenerates the site's `index.html`s and `.meta/.json` from the on-disk tree (a dir becomes a version only if it matches semver — `latest/` is deliberately invisible to the indexes). `scripts/make-pkg.sh` assembles the pacman package without makepkg (NixOS CI has none). Two repo secrets gate it: `SHEAF_PAGES_SSH_KEY` (passphrase-less deploy key, public half on the Tangled account) and `ATP_APP_PASSWORD` (bsky.social app password for the `sh.tangled.repo.artifact` record). Full setup/cut list in `RELEASE.md`. ## Conventions diff --git a/README.md b/README.md index 28aa428..38dac4a 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,23 @@ just desktop-uninstall # remove The canonical app icon is `packaging/linux/sheaf.svg`. To change the icon, edit this SVG. Then run `bash scripts/render-icons.sh`. The script regenerates the PNG set, `build/appicon.png`, and the Windows `.ico` from the SVG. +### AppImage + +Every release ships an x86_64 AppImage, and the newest one always lives at a stable URL: + +```sh +curl -LO https://katsuricata.tngl.io/sheaf/latest/sheaf-latest-x86_64.AppImage +chmod +x sheaf-latest-x86_64.AppImage +``` + +Release AppImages carry zsync update information, so [AppImageUpdate](https://appimage.github.io/AppImageUpdate/) (or any compatible updater) can delta-update the file in place — only the changed blocks are downloaded. Like the raw binary, the AppImage dynamically links the host's gtk3/webkit2gtk-4.1 rather than bundling them, so it needs a distro that ships webkit2gtk-4.1. + +To build one locally (needs `appimagetool` and `zsync`): + +```sh +just appimage # -> dist/sheaf-0.0.0-dev-x86_64.AppImage, no update info +``` + ### Arch Linux AUR package files for `sheaf-git` live under `aur/` in this repository. To build and publish them, run: @@ -86,7 +103,7 @@ just aur-build # rebuild after changes ## Releases -Tagged releases are built by Spindle (`.tangled/workflows/release.yml`) and published to : raw binaries (amd64/arm64), tar/zip archives + `SHA256SUMS`, a source tarball, and a prebuilt `sheaf-bin` pacman package. Releases are cut with annotated tags (`git tag -a v1.0.0 -m ... && git push origin v1.0.0`). See [RELEASE.md](RELEASE.md) for the full pipeline and one-time setup. +Tagged releases are built by Spindle (`.tangled/workflows/release.yml`) and published to : raw binaries (amd64/arm64), tar/zip archives + `SHA256SUMS`, a source tarball, a prebuilt `sheaf-bin` pacman package, and an x86_64 AppImage with zsync delta-updates. Releases are cut with annotated tags (`git tag -a v1.0.0 -m ... && git push origin v1.0.0`). See [RELEASE.md](RELEASE.md) for the full pipeline and one-time setup. ## Development diff --git a/RELEASE.md b/RELEASE.md index 030b7fc..3cc7d23 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -5,8 +5,9 @@ Spindle pipeline + Tangled Pages. One annotated tag push does everything: 1. `spindle.tangled.sh` runs [.tangled/workflows/release.yml](.tangled/workflows/release.yml). 2. It builds `linux-amd64` + `linux-arm64` binaries, tar/zip archives, a - source tarball, `SHA256SUMS`, and a prebuilt `sheaf-bin-…​.pkg.tar.zst` - Arch package, then runs the full Go test suite first. + source tarball, `SHA256SUMS`, a prebuilt `sheaf-bin-…​.pkg.tar.zst` + Arch package, and an x86_64 AppImage with zsync delta-update info, then + runs the full Go test suite first. 3. It pushes all of that to the **`pages`** repo under `sheaf/vX.Y.Z/` and regenerates the Digital-Rust-styled `index.html` files (`scripts/make-indexes.py`). @@ -73,8 +74,26 @@ curl -s https://katsuricata.tngl.io/sheaf/v1.0.0/SHA256SUMS # checksums /sheaf/v1.0.0/sheaf-linux-arm64.zip zip /sheaf/v1.0.0/sheaf-1.0.0-source.tar.gz git archive of the tag /sheaf/v1.0.0/sheaf-bin-1.0.0-1-x86_64.pkg.tar.zst pacman package (see below) + /sheaf/v1.0.0/sheaf-1.0.0-x86_64.AppImage AppImage (zstd-compressed) + /sheaf/v1.0.0/sheaf-1.0.0-x86_64.AppImage.zsync zsync delta-update control file /sheaf/v1.0.0/SHA256SUMS sha256 for all of the above +The AppImage also lands in a stable alias directory that is refreshed on +every release: + + /sheaf/latest/sheaf-latest-x86_64.AppImage newest AppImage + /sheaf/latest/sheaf-latest-x86_64.AppImage.zsync its zsync control file + +That alias is what the AppImage's embedded update information points at +(`zsync|https://katsuricata.tngl.io/sheaf/latest/sheaf-latest-x86_64.AppImage.zsync`), +so AppImageUpdate always finds the newest build. The `.zsync` holds a +relative `URL:` header (generated with `zsyncmake -u`), so the control file +also works when downloaded from the versioned `vX.Y.Z/` directory. The zsync +client needs HTTP range-request support from the server (any static host +qualifies; Python's `http.server` does not, so don't smoke-test against it). +The AppImage bundles no runtime: it dynamically links the host's +gtk3/webkit2gtk-4.1, exactly like the raw binary. + ## Installing the Arch package There is no `sheaf-bin` AUR remote yet; install what CI published directly: @@ -105,6 +124,7 @@ record; the old one is harmless. |------|------| | `.tangled/workflows/release.yml` | the Spindle pipeline (the moving parts) | | `scripts/make-pkg.sh` | builds a `.pkg.tar.zst` without makepkg (NixOS CI + local use) | +| `scripts/make-appimage.sh` | builds the AppImage + `.zsync` from the release binary (CI + `just appimage`) | | `scripts/make-indexes.py` | regenerates all `index.html` + `.meta/*.json` in the pages repo | | `aur/PKGBUILD.sheaf-bin` | template + docs for the prebuilt package | | `pages` repo | the actual site (auto-deployed on push) | diff --git a/justfile b/justfile index c6e9544..186c6ba 100644 --- a/justfile +++ b/justfile @@ -72,6 +72,12 @@ desktop-uninstall: rm -f "{{dest}}/share/icons/hicolor/${s}x${s}/apps/com.doubledecimal.sheaf.png"; \ done +# Build a local AppImage into dist/. Needs appimagetool and zsync installed. +# No update info is baked in, so AppImageUpdate treats it as non-updatable; +# release AppImages get their zsync URL from the pipeline. +appimage: build + bash scripts/make-appimage.sh 0.0.0-dev build/bin/sheaf dist + # AUR packaging (sheaf-git). The AUR remote must already exist. See https://aur.archlinux.org # Clone the AUR package repo and seed it with the PKGBUILD from aur/ diff --git a/scripts/make-appimage.sh b/scripts/make-appimage.sh new file mode 100755 index 0000000..de1616e --- /dev/null +++ b/scripts/make-appimage.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# scripts/make-appimage.sh: build a Sheaf AppImage (+ .zsync delta-update +# file) from a prebuilt wails binary. Used by .tangled/workflows/release.yml; +# runnable locally with `just appimage` (which needs appimagetool and zsync). +# +# bash scripts/make-appimage.sh [zsync-url] +# +# e.g. bash scripts/make-appimage.sh 1.0.0 build/bin/sheaf dist \ +# "https://katsuricata.tngl.io/sheaf/latest/sheaf-latest-x86_64.AppImage.zsync" +# +# is baked into the AppImage as update information (what +# AppImageUpdate reads). Pass it only for releases; a local build gets no +# update info, which AppImageUpdate treats as "not updatable" — the correct +# behaviour for a throwaway binary. +# +# Sheaf dynamically links the host's gtk3/webkit2gtk-4.1, so the AppImage is +# a portable launcher + desktop integration, not a bundled-runtime container: +# it runs on any distro shipping webkit2gtk-4.1, same contract as the raw +# binary release. +set -euo pipefail + +VERSION="${1:?version (e.g. 1.0.0)}" +BINARY="${2:?path to built sheaf binary}" +OUT_DIR="${3:?output directory for the .AppImage}" +ZSYNC_URL="${4:-}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +APPIMAGETOOL="${APPIMAGETOOL:-appimagetool-x86_64.AppImage}" +PKG_NAME="sheaf-${VERSION}-x86_64.AppImage" + +ROOT="$(mktemp -d)"; trap 'rm -rf "$ROOT"' EXIT +APPDIR="$ROOT/Sheaf.AppDir"; mkdir -p "$APPDIR" + +# the hicolor PNG size set is generated from packaging/linux/sheaf.svg. Render +# it now if a from-source consumer has not run scripts/render-icons.sh yet. +if [ ! -f "$REPO_ROOT/build/icons/1024x1024/com.doubledecimal.sheaf.png" ]; then + bash "$REPO_ROOT/scripts/render-icons.sh" +fi + +# AppRun: the AppImage entry point. $APPDIR is set by the AppImage runtime; +# the fallback keeps the file runnable when the AppDir is inspected directly. +cat > "$APPDIR/AppRun" <<'EOF' +#!/bin/sh +exec "${APPDIR:-$(dirname "$(readlink -f "$0")")}/usr/bin/sheaf" "$@" +EOF +chmod 755 "$APPDIR/AppRun" + +install -Dm755 "$BINARY" "$APPDIR/usr/bin/sheaf" +install -Dm644 "$REPO_ROOT/packaging/linux/com.doubledecimal.sheaf.desktop" \ + "$APPDIR/com.doubledecimal.sheaf.desktop" +install -Dm644 "$REPO_ROOT/packaging/linux/com.doubledecimal.sheaf.metainfo.xml" \ + "$APPDIR/usr/share/metainfo/com.doubledecimal.sheaf.metainfo.xml" +# icon: canonical SVG + per-size PNGs (rendered by scripts/render-icons.sh), +# named for the reverse-DNS app-id. The scalable entry is what lets Qt/KWin +# resolve the Wayland titlebar/taskbar icon at any size. The PNGs are fallback. +install -Dm644 "$REPO_ROOT/packaging/linux/sheaf.svg" \ + "$APPDIR/com.doubledecimal.sheaf.svg" +install -Dm644 "$REPO_ROOT/packaging/linux/sheaf.svg" \ + "$APPDIR/usr/share/icons/hicolor/scalable/apps/com.doubledecimal.sheaf.svg" +for s in 16 32 48 64 128 256 512 1024; do + install -Dm644 "$REPO_ROOT/build/icons/${s}x${s}/com.doubledecimal.sheaf.png" \ + "$APPDIR/usr/share/icons/hicolor/${s}x${s}/apps/com.doubledecimal.sheaf.png" +done +# appimagetool's AppDir lint wants a 256x256 PNG at a conventional path. +install -Dm644 "$REPO_ROOT/build/icons/256x256/com.doubledecimal.sheaf.png" \ + "$APPDIR/usr/share/pixmaps/com.doubledecimal.sheaf.png" +install -Dm644 "$REPO_ROOT/LICENSE.md" \ + "$APPDIR/usr/share/licenses/sheaf/LICENSE.md" + +mkdir -p "$OUT_DIR" +# appimagetool is itself an AppImage and needs FUSE; extract-and-run keeps it +# working in containers/CI without it. ARCH is required when the tool's build +# arch string does not match what it detects on the host. +export ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 +UPDATE_ARGS=() +if [ -n "$ZSYNC_URL" ]; then + UPDATE_ARGS=(-u "zsync|$ZSYNC_URL") +fi +"$APPIMAGETOOL" --comp zstd "${UPDATE_ARGS[@]}" "$APPDIR" "$OUT_DIR/$PKG_NAME" + +# zsyncmake is bundled inside appimagetool's AppDir and only runs when the +# update-info flag is passed. The release workflow wants the .zsync even for +# local verification, so generate it unconditionally when zsyncmake exists. +if [ -n "$ZSYNC_URL" ] && command -v zsyncmake >/dev/null 2>&1; then + (cd "$OUT_DIR" && zsyncmake -u "$PKG_NAME" "$PKG_NAME") +fi + +echo "built $OUT_DIR/$PKG_NAME" +[ -z "$ZSYNC_URL" ] || echo "update info: zsync|$ZSYNC_URL"