From 71f205f73ea09a0285ccfe53adee3b23e59b68b6 Mon Sep 17 00:00:00 2001 From: Nacer Laradji Date: Wed, 15 Apr 2026 17:25:39 +0100 Subject: [PATCH] feat-distribution-appimage-linux-nnp --- .github/actions/build-appimage/action.yml | 156 ++++++++++++++++++++++ .github/assets/deadzone.png | Bin 0 -> 4189 bytes .github/workflows/release.yml | 86 ++++++++++-- README.md | 19 ++- 4 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 .github/actions/build-appimage/action.yml create mode 100644 .github/assets/deadzone.png diff --git a/.github/actions/build-appimage/action.yml b/.github/actions/build-appimage/action.yml new file mode 100644 index 0000000..37fc9e0 --- /dev/null +++ b/.github/actions/build-appimage/action.yml @@ -0,0 +1,156 @@ +name: Build AppImage +description: | + Repackage a per-platform deadzone tarball into an AppImage. + Reads the tarball from the working directory, produces + `deadzone__linux_.AppImage` as an output. + + `AppImage/appimagetool` release `1.9.1` is pinned here instead of + the `continuous` tag; bumping it is a conscious step (same philosophy + as TOKENIZERS_VERSION in release.yml). The older `AppImage/AppImageKit` + repo is archived and all its release-13 assets have been renamed + `obsolete-*` — don't use them. + +inputs: + tarball: + description: Path (or glob) of the input tarball produced by the build job. + required: true + arch: + description: "`amd64` or `arm64` — matches the Linux build matrix." + required: true + icon: + description: Path to the 256x256 placeholder icon checked into the repo. + required: false + default: .github/assets/deadzone.png + +outputs: + appimage: + description: Path to the produced AppImage file. + value: ${{ steps.build.outputs.appimage }} + +runs: + using: composite + steps: + - name: Install appimagetool + shell: bash + env: + APPIMAGE_ARCH: ${{ inputs.arch }} + run: | + set -euo pipefail + # AppImage/appimagetool release 1.9.1 is the active successor + # to the archived AppImageKit repo (whose release-13 assets are + # now prefixed `obsolete-*`). Both x86_64 and aarch64 AppImages + # are published per release. Pinning (vs the `continuous` tag) + # means a bump is an explicit commit. + case "${APPIMAGE_ARCH}" in + amd64) tool_arch="x86_64" ;; + arm64) tool_arch="aarch64" ;; + *) echo "unsupported arch: ${APPIMAGE_ARCH}" >&2; exit 1 ;; + esac + url="https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${tool_arch}.AppImage" + curl -fL -o /tmp/appimagetool "${url}" + chmod +x /tmp/appimagetool + + - name: Install libfuse2 + shell: bash + run: | + set -euo pipefail + # appimagetool is itself an AppImage and needs FUSE v2 to + # self-mount. Ubuntu 24.04 runners don't ship libfuse2 by + # default (FUSE v3 is the new default), so install it here. + # --appimage-extract-and-run would also work but costs an extra + # extract step per invocation. + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends libfuse2 + + - name: Assemble AppDir and build AppImage + id: build + shell: bash + env: + TARBALL_GLOB: ${{ inputs.tarball }} + APPIMAGE_ARCH: ${{ inputs.arch }} + ICON_PATH: ${{ inputs.icon }} + run: | + set -euo pipefail + # Resolve the tarball glob (download-artifact lands one file). + shopt -s nullglob + matches=( ${TARBALL_GLOB} ) + if [[ ${#matches[@]} -ne 1 ]]; then + echo "::error::expected exactly 1 tarball matching '${TARBALL_GLOB}', got ${#matches[@]}" + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + fi + tarball="${matches[0]}" + + # Tarball naming: deadzone__linux_.tar.gz — the + # version segment is everything between `deadzone_` and + # `_linux_.tar.gz`, so peel both sides to recover it. + base="$(basename "${tarball}" .tar.gz)" + version="${base#deadzone_}" + version="${version%_linux_${APPIMAGE_ARCH}}" + if [[ -z "${version}" || "${version}" == "${base}" ]]; then + echo "::error::could not parse version from tarball name: ${base}" >&2 + exit 1 + fi + + workdir="$(mktemp -d)" + tar -xzf "${tarball}" -C "${workdir}" + extracted="${workdir}/${base}" + if [[ ! -x "${extracted}/deadzone" ]]; then + echo "::error::extracted tarball missing executable deadzone binary" >&2 + ls -la "${extracted}" >&2 || true + exit 1 + fi + + appdir="${workdir}/deadzone.AppDir" + mkdir -p "${appdir}/usr/bin" + cp "${extracted}/deadzone" "${appdir}/usr/bin/deadzone" + cp "${extracted}/LICENSE" "${extracted}/NOTICE" "${extracted}/README.md" "${appdir}/" + + # AppRun wrapper. Explicitly steers DEADZONE_ORT_CACHE at the + # user's XDG cache dir — the AppImage payload is read-only at + # runtime (SquashFS mount), so letting the Go default land + # somewhere inside the mount would be a footgun. This matches + # internal/ort/ort.go's existing default, just lifted up so the + # contract is documented at the package boundary too. + cat > "${appdir}/AppRun" <<'APPRUN' + #!/bin/bash + set -e + HERE="$(dirname "$(readlink -f "$0")")" + export DEADZONE_ORT_CACHE="${DEADZONE_ORT_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/deadzone/ort}" + exec "${HERE}/usr/bin/deadzone" "$@" + APPRUN + chmod +x "${appdir}/AppRun" + + # Minimal CLI .desktop file. Terminal=true tells launchers to + # spawn a terminal, but in practice deadzone is invoked from + # the shell or by an MCP client (stdio), so this is just spec + # compliance — not a desktop-integration feature. + cat > "${appdir}/deadzone.desktop" <<'DESKTOP' + [Desktop Entry] + Name=deadzone + Exec=deadzone + Icon=deadzone + Type=Application + Categories=Utility;Development; + Terminal=true + DESKTOP + + # appimagetool looks for .png (or .svg) at the AppDir + # root; a 256x256 placeholder is enough to satisfy validation. + cp "${GITHUB_WORKSPACE}/${ICON_PATH}" "${appdir}/deadzone.png" + + output="deadzone_${version}_linux_${APPIMAGE_ARCH}.AppImage" + # ARCH env var is how appimagetool picks the embedded runtime; + # without it the tool guesses from the host, which is correct + # on the matrix but explicit is cheaper than debugging later. + case "${APPIMAGE_ARCH}" in + amd64) export ARCH="x86_64" ;; + arm64) export ARCH="aarch64" ;; + esac + # -n skips the GPG signing prompt (we don't sign yet; see #124 + # out-of-scope). --no-appstream because this is a CLI with no + # AppStream metadata by design. + /tmp/appimagetool -n --no-appstream "${appdir}" "${output}" + + ls -la "${output}" + echo "appimage=${output}" >> "$GITHUB_OUTPUT" diff --git a/.github/assets/deadzone.png b/.github/assets/deadzone.png new file mode 100644 index 0000000000000000000000000000000000000000..e4de3ddf90be5daa0b741e735adbd9d3790c74b3 GIT binary patch literal 4189 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 "deadzone_${VERSION}_checksums.txt" + # Single checksums.txt covers every per-platform tarball and + # AppImage — users who want to verify hit one file, one + # command. The consolidated `deadzone.db` is uploaded + # separately by `deadzone dbrelease` from the operator's + # laptop after this job finishes (see #101 §H); its sha256 + # lives in a sibling `deadzone.db.sha256` asset written by + # that subcommand. + sha256sum deadzone_*.tar.gz deadzone_*.AppImage \ + > "deadzone_${VERSION}_checksums.txt" cat "deadzone_${VERSION}_checksums.txt" - name: Create GitHub release @@ -249,4 +318,5 @@ jobs: --title "$VERSION" \ --generate-notes \ dist/deadzone_*.tar.gz \ + dist/deadzone_*.AppImage \ "dist/deadzone_${VERSION}_checksums.txt" diff --git a/README.md b/README.md index 302c093..682b95a 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Documentation is fetched by the `deadzone scrape` subcommand, embedded into vect Pre-built binaries for **macOS Apple Silicon**, **Linux amd64**, and **Linux arm64** are published on the [Releases page](https://github.com/laradji/deadzone/releases). Windows is blocked upstream (no `libtokenizers.a`). If you want to build from source instead — most useful if you're contributing or running on an unsupported platform — skip to [Build from source](#build-from-source). -macOS Apple Silicon users can also install via [Homebrew](#homebrew-macos-apple-silicon) below — it's a one-liner and skips the quarantine workaround. +macOS Apple Silicon users can also install via [Homebrew](#homebrew-macos-apple-silicon) below — it's a one-liner and skips the quarantine workaround. Linux users have a one-liner too: the [AppImage](#appimage-linux) bundles the binary and its assets into a single self-mounting file. ### Homebrew (macOS Apple Silicon) @@ -68,6 +68,23 @@ Apple Silicon only — Intel Macs aren't built by the release pipeline. If you n Homebrew installs into a non-quarantined location, so the [quarantine workaround](#macos-clear-the-quarantine-attribute) below doesn't apply. +### AppImage (Linux) + +```bash +VERSION=v0.1.0 +ARCH=amd64 # or arm64 + +curl -L -O "https://github.com/laradji/deadzone/releases/download/${VERSION}/deadzone_${VERSION}_linux_${ARCH}.AppImage" +chmod +x "deadzone_${VERSION}_linux_${ARCH}.AppImage" +"./deadzone_${VERSION}_linux_${ARCH}.AppImage" -version +``` + +The AppImage self-mounts its payload via FUSE v2. Most desktop distros have `libfuse2` preinstalled; minimal server or container images often don't. If you hit `dlopen(): error loading libfuse.so.2`, either install the FUSE v2 package (`apt-get install libfuse2` on Debian/Ubuntu, `dnf install fuse-libs` on Fedora) or pass `--appimage-extract-and-run`, which bypasses FUSE entirely by extracting the payload to a temp dir per invocation: + +```bash +"./deadzone_${VERSION}_linux_${ARCH}.AppImage" --appimage-extract-and-run -version +``` + ### Quick install Pick the archive for your platform and extract it into the directory you want to run deadzone from: