diff --git a/.github/scripts/publish/freebsd-prepare.sh b/.github/scripts/publish/freebsd-prepare.sh new file mode 100755 index 0000000..b2b8699 --- /dev/null +++ b/.github/scripts/publish/freebsd-prepare.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Substitute the release version into the FreeBSD port's Makefile and +# regenerate its distinfo (per-arch SHA256 + SIZE) from the release's +# published assets — the `.sha256` companion gives the digest and the +# release asset metadata gives the byte size. This avoids running +# `make makesum` on a host that has no ports tree (and no need to download +# the multi-MB tarballs just to size them). +# +# Usage: freebsd-prepare.sh +# Requires: GH_TOKEN, GITHUB_REPOSITORY (provided by Actions). +set -euo pipefail + +version="${1:?usage: freebsd-prepare.sh }" +port_dir="freebsd/runner" +makefile="${port_dir}/Makefile" +distinfo="${port_dir}/distinfo" + +# Reject anything that isn't strict semver (with optional prerelease) before +# touching files. The downstream `sed -i` substitution would otherwise be at +# the mercy of `&`, `/`, `\`, and newlines in whatever the workflow handed +# us. Keeping the alphabet to [0-9A-Za-z.-] guarantees a byte-for-byte +# literal substitution. +if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "error: version '${version}' does not match semver (X.Y.Z or X.Y.Z-prerelease)" >&2 + exit 1 +fi + +for f in "${makefile}" "${distinfo}"; do + if [[ ! -f "${f}" ]]; then + echo "error: ${f} not found" >&2 + exit 1 + fi +done + +# Fresh upstream version → DISTVERSION bump. PORTREVISION (if any) is reset +# by simply not carrying one in the checked-in Makefile. The literal tab +# keeps the value-column alignment used throughout the Makefile. +tab="$(printf '\t')" +sed -i -E "s/^DISTVERSION=.*/DISTVERSION=${tab}${version}/" "${makefile}" + +# CARCH -> Rust triple. Keep in lockstep with RUST_TARGET_* in the Makefile +# and the freebsd-* entries in npm/targets.json. amd64 is the tier-2 +# release-blocking build and is mandatory here; aarch64 is the tier-3 +# `experimental` build (continue-on-error in release.yml), so its asset may +# legitimately be missing for a given release — we skip it rather than abort, +# matching how the npm publish treats experimental packages as optional. +declare -A triples=( + [amd64]='x86_64-unknown-freebsd' + [aarch64]='aarch64-unknown-freebsd' +) +declare -A required=([amd64]=1 [aarch64]=0) + +# distinfo entry for one arch, emitted to stdout. Returns non-zero (without +# printing) when the arch's assets are absent. +emit_arch() { + local carch="$1" triple="$2" distfile sum size + distfile="runner-v${version}-${triple}.tar.gz" + + # `.sha256` asset is " "; field 1 is the digest. + if ! sum="$(gh release download "v${version}" \ + --repo "${GITHUB_REPOSITORY}" \ + --pattern "runner-v${version}-${triple}.sha256" \ + --output - 2>/dev/null | awk 'NR==1{print $1}')" || [[ -z "${sum}" ]]; then + return 1 + fi + if [[ ! "${sum}" =~ ^[0-9a-f]{64}$ ]]; then + echo "error: bad sha256 for ${distfile}: '${sum}'" >&2 + exit 1 + fi + + # Byte size from the release asset metadata (no tarball download). + size="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/v${version}" \ + --jq ".assets[] | select(.name == \"${distfile}\") | .size")" + if [[ ! "${size}" =~ ^[0-9]+$ ]]; then + echo "error: bad size for ${distfile}: '${size}'" >&2 + exit 1 + fi + + echo "SHA256 (${distfile}) = ${sum}" + echo "SIZE (${distfile}) = ${size}" +} + +# Regenerate distinfo from scratch so a dropped arch can never leave a stale +# entry behind. TIMESTAMP mirrors what `make makesum` would stamp. +{ + echo "TIMESTAMP = $(date +%s)" + for carch in amd64 aarch64; do + if ! emit_arch "${carch}" "${triples[$carch]}"; then + if [[ "${required[$carch]}" == 1 ]]; then + echo "error: required ${carch} freebsd asset missing for v${version}" >&2 + exit 1 + fi + echo "note: skipping ${carch} — no freebsd asset for v${version}" >&2 + fi + done +} >"${distinfo}.tmp" +mv "${distinfo}.tmp" "${distinfo}" + +echo "--- prepared ${makefile} ---" +grep -E '^(PORTNAME|DISTVERSION|PKGNAMESUFFIX)=' "${makefile}" +echo "--- prepared ${distinfo} ---" +cat "${distinfo}" diff --git a/.github/workflows/freebsd-release.yml b/.github/workflows/freebsd-release.yml new file mode 100644 index 0000000..46ba9ac --- /dev/null +++ b/.github/workflows/freebsd-release.yml @@ -0,0 +1,91 @@ +name: freebsd-release +run-name: >- + ${{ format('freebsd publish {0}', case(github.event_name == 'release', + github.event.release.tag_name, + inputs.tag)) }} + +on: + release: { types: [published] } + workflow_dispatch: + inputs: + tag: + description: Release tag to publish (e.g. v0.12.0) + required: true + type: string + dry-run: + description: Prepare + build the port in the FreeBSD VM but do not upload + required: true + default: false + type: boolean + +permissions: { contents: read } + +jobs: + publish: + name: build + attach freebsd pkg + runs-on: ubuntu-latest + # Serialize so a manual dispatch and a release-published run can't race + # the same release's asset upload. + concurrency: { group: freebsd-publish, cancel-in-progress: false } + permissions: { contents: write } + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ case(github.event_name == 'release', github.event.release.tag_name, inputs.tag) }} + DRY_RUN: ${{ inputs.dry-run || 'false' }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: { persist-credentials: false } + + # Bump DISTVERSION + regenerate distinfo from the published release + # assets, so the VM build below fetches and checksums the tarballs for + # exactly this tag (not the checked-in reference snapshot). + - name: prepare port (version + distinfo) + run: | + set -euo pipefail + version="${RELEASE_TAG#v}" + bash "${GITHUB_WORKSPACE}/.github/scripts/publish/freebsd-prepare.sh" "${version}" + + # Build the port the canonical way — `make package` runs the staging + # QA + plist checks, so a drifted PLIST_FILES fails here instead of + # shipping a broken package. We only need the ports `Mk/` infra, so a + # blobless, sparse clone keeps the checkout tiny. + - name: build pkg in FreeBSD VM + uses: vmactions/freebsd-vm@a6de9343ef5747433d9c25784c90e84998b9d69a # v1.4.6 + with: + usesh: true + prepare: | + pkg install -y git + run: | + set -eu + ws="$(pwd)" + git clone --filter=blob:none --depth 1 --sparse \ + https://github.com/freebsd/freebsd-ports /usr/ports + cd /usr/ports + git sparse-checkout set Mk Keywords Templates + mkdir -p /usr/ports/devel + cp -R "${ws}/freebsd/runner" /usr/ports/devel/runner + cd /usr/ports/devel/runner + # check-plist fails on drift in either direction (a listed file + # that wasn't staged, or a staged file that isn't listed). + make BATCH=yes stage check-plist package + pkgfile="$(make -V PKGFILE)" + echo "built ${pkgfile}" + mkdir -p "${ws}/freebsd/pkgout" + cp "${pkgfile}" "${ws}/freebsd/pkgout/runner-freebsd-amd64.pkg" + + - name: checksum + run: | + set -euo pipefail + cd freebsd/pkgout + sha256sum runner-freebsd-amd64.pkg > runner-freebsd-amd64.pkg.sha256 + ls -l + cat runner-freebsd-amd64.pkg.sha256 + + - name: upload pkg to release + if: env.DRY_RUN != 'true' + run: | + set -euo pipefail + gh release upload "${RELEASE_TAG}" \ + freebsd/pkgout/runner-freebsd-amd64.pkg \ + freebsd/pkgout/runner-freebsd-amd64.pkg.sha256 \ + --repo "${GITHUB_REPOSITORY}" --clobber diff --git a/CHANGELOG.md b/CHANGELOG.md index 948318d..fe220f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,33 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic ### Added +- FreeBSD distribution channel. A prebuilt port (`freebsd/runner`, + package name `runner-bin`) installs the `runner` + `run` binaries from + the GitHub release `*-unknown-freebsd` tarballs the `release.yml` matrix + already publishes — no recompile — for `amd64` and `aarch64`. Since + FreeBSD has no AUR-style push-to-git remote, the channel ships an + installable amd64 `.pkg` attached to each release: + `pkg add https://github.com/kjanat/runner/releases/latest/download/runner-freebsd-amd64.pkg`. +- FreeBSD completions shipped by the port and auto-loaded from the + canonical `${LOCALBASE}` dirs: bash at + `share/bash-completion/completions/{runner,run}`, zsh at + `share/zsh/site-functions/{_runner,_run}`, fish at + `share/fish/vendor_completions.d/{runner,run}.fish`. PowerShell has no + autoload convention, so the pwsh script is installed at + `share/runner/runner.ps1` for users to dot-source from their `$PROFILE`. +- `.github/workflows/freebsd-release.yml` builds and attaches the `.pkg` + on every `release: published` event (with manual `workflow_dispatch` + + `dry-run` for validation). It runs the real ports build (`make package`) + inside a FreeBSD VM (`vmactions/freebsd-vm`) against a blobless, sparse + checkout of the ports `Mk/` infrastructure, so the staging QA and plist + checks gate every release. Third-party `uses:` pinned to commit SHAs. +- `.github/scripts/publish/freebsd-prepare.sh` rewrites `DISTVERSION` and + regenerates `distinfo` (per-arch `SHA256` + `SIZE`) from the release's + published `.sha256` companion assets and asset metadata. The mandatory + `amd64` entry aborts on a missing asset; the `experimental` `aarch64` + build is skipped with a warning when absent. Strict semver regex on the + version input refuses anything containing `&`, `/`, `\`, or newlines + before any `sed` runs. - AUR distribution channel. Two packages on the Arch User Repository: `runner-run-bin` (prebuilt binaries for `x86_64`, `aarch64`, `armv7h`) and `runner-run` (source build for `x86_64`, `aarch64`). `-bin` diff --git a/README.md b/README.md index 6ea2fce..0dc363c 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,12 @@ Or on Arch Linux: yay -S runner-run-bin ``` +Or on FreeBSD: + +```sh +pkg add https://github.com/kjanat/runner/releases/latest/download/runner-freebsd-amd64.pkg +``` +
Other install methods @@ -197,6 +203,12 @@ cargo install --path . yay -S runner-run ``` +```sh +# FreeBSD/aarch64: build the prebuilt port locally (distinfo already +# carries the aarch64 distfile checksum), or fetch the amd64 .pkg above. +cd /usr/ports/devel/runner && make package +``` + ```sh curl -fsSLO https://raw.githubusercontent.com/kjanat/runner/master/install.sh bash install.sh diff --git a/freebsd/README.md b/freebsd/README.md new file mode 100644 index 0000000..e4a2b76 --- /dev/null +++ b/freebsd/README.md @@ -0,0 +1,86 @@ +# FreeBSD packaging + +A single prebuilt [FreeBSD port][ports] under `runner/`: + +| Port | Installs | Arches | +| ------------ | --------------------------------- | -------------- | +| `runner-bin` | prebuilt from GitHub release tars | amd64, aarch64 | + +It mirrors the AUR `runner-run-bin` package: no compile, just the upstream +release binaries (`runner` + `run`) plus bash, zsh and fish completions. +The release tarballs are the same `*-unknown-freebsd` assets that the main +`release.yml` build matrix already publishes (see the `freebsd-x64` / +`freebsd-arm64` entries in `npm/targets.json`), so this port reuses them +rather than recompiling. + +There is no FreeBSD equivalent of the AUR's push-to-git remote — the +official Ports Collection lands through Bugzilla review. So the +self-serviceable channel is a binary `.pkg` attached to each GitHub +release, installable with `pkg add`: + +```sh +pkg add https://github.com/kjanat/runner/releases/latest/download/runner-freebsd-amd64.pkg +``` + +Completions auto-load from the canonical `${LOCALBASE}` dirs (no `eval` +line needed in a user's rc): + +- bash: `${LOCALBASE}/share/bash-completion/completions/{runner,run}` +- zsh: `${LOCALBASE}/share/zsh/site-functions/{_runner,_run}` +- fish: `${LOCALBASE}/share/fish/vendor_completions.d/{runner,run}.fish` + +PowerShell has no system autoload dir, so the pwsh script is installed at +`${LOCALBASE}/share/runner/runner.ps1` for users to dot-source from their +`$PROFILE`: + +```powershell +if (Test-Path /usr/local/share/runner/runner.ps1) { . /usr/local/share/runner/runner.ps1 } +``` + +Completions are clap-dynamic (the shell shells out to the binary for +candidates), so tab-completing in a project picks up the *current* task +list from `package.json` / `turbo.json` / `Justfile` / etc., not a static +snapshot. + +## Automation + +`.github/workflows/freebsd-release.yml` builds and attaches the `.pkg` on +every `release: published` event (and via manual `workflow_dispatch` with +a `tag` input). Per release it: + +1. Rewrites `DISTVERSION` in the `Makefile` and regenerates `distinfo` + (per-arch `SHA256` + `SIZE`) from the release's published `.sha256` + companion assets and asset metadata + (`.github/scripts/publish/freebsd-prepare.sh`). +2. Inside a FreeBSD VM ([`vmactions/freebsd-vm`]) builds the port through + the standard ports framework (`make package`) against a sparse checkout + of the ports `Mk/` infrastructure. `make package` runs the staging QA + checks and the plist check, so a drifted `PLIST_FILES` fails the build + loudly instead of shipping a broken package. +3. Uploads the resulting amd64 `.pkg` to the GitHub release as + `runner-freebsd-amd64.pkg` (+ a `.sha256` companion). + +The checked-in `Makefile` / `distinfo` values are a reference snapshot; CI +overwrites them before building, so they need not be bumped by hand. + +aarch64 users build the port locally — `distinfo` already carries the +aarch64 distfile checksum, so `make package` on an aarch64 host produces +the matching `.pkg` with no edits. + +## Validation + +Cut a release as usual, or dry-run first: + +- **Validate without uploading**: Actions → `freebsd-release` → Run + workflow, set `tag` and tick `dry-run`. This prepares the port and runs + the full `make package` build in the FreeBSD VM, but skips the release + upload. +- **Local build** (on a FreeBSD box, with the port dropped into a ports + tree at `devel/runner`): + + ```sh + cd /usr/ports/devel/runner && make stage check-plist package + ``` + +[ports]: https://docs.freebsd.org/en/books/porters-handbook/ +[`vmactions/freebsd-vm`]: https://github.com/vmactions/freebsd-vm diff --git a/freebsd/runner/Makefile b/freebsd/runner/Makefile new file mode 100644 index 0000000..9612f95 --- /dev/null +++ b/freebsd/runner/Makefile @@ -0,0 +1,113 @@ +# Maintainer: Kaj Kowalski +# +# Prebuilt port: installs the `runner` + `run` binaries straight from the +# GitHub release `.tar.gz` assets (no compile), mirroring the AUR +# `runner-run-bin` package. `DISTVERSION` and the `distinfo` checksums are +# rewritten by CI (.github/workflows/freebsd-release.yml) on every release; +# the values below are only a checked-in reference snapshot. + +PORTNAME= runner +DISTVERSIONPREFIX= v +DISTVERSION= 0.12.0 +CATEGORIES= devel +MASTER_SITES= https://github.com/kjanat/runner/releases/download/${DISTVERSIONPREFIX}${DISTVERSION}/ +PKGNAMESUFFIX= -bin + +MAINTAINER= info@kajkowalski.nl +COMMENT= Universal project task runner (prebuilt binary) +WWW= https://github.com/kjanat/runner + +LICENSE= MIT +LICENSE_FILE= ${WRKSRC}/LICENSE + +ONLY_FOR_ARCHS= amd64 aarch64 +ONLY_FOR_ARCHS_REASON= prebuilt binaries are published for amd64 and aarch64 only + +# Each prebuilt release tarball carries its Rust target triple in the +# basename, so pick the one matching the build host's architecture. Keep +# these in lockstep with the freebsd-* entries in npm/targets.json. +RUST_TARGET_amd64= x86_64-unknown-freebsd +RUST_TARGET_aarch64= aarch64-unknown-freebsd +RUST_TARGET= ${RUST_TARGET_${ARCH}} + +DISTNAME= ${PORTNAME}-${DISTVERSIONFULL}-${RUST_TARGET} + +# The archive is flat — runner, run, README.md and LICENSE all sit at the +# root — so there is no wrapper directory to descend into and nothing to +# compile. +NO_WRKSUBDIR= yes +NO_BUILD= yes + +OPTIONS_DEFINE= DOCS + +PLIST_FILES= bin/run \ + bin/runner \ + share/bash-completion/completions/run \ + share/bash-completion/completions/runner \ + share/fish/vendor_completions.d/run.fish \ + share/fish/vendor_completions.d/runner.fish \ + share/runner/runner.ps1 \ + share/zsh/site-functions/_run \ + share/zsh/site-functions/_runner \ + %%PORTDOCS%%%%DOCSDIR%%/README.md +PORTDOCS= README.md + +do-install: + ${INSTALL_PROGRAM} ${WRKSRC}/runner ${STAGEDIR}${PREFIX}/bin/runner + ${INSTALL_PROGRAM} ${WRKSRC}/run ${STAGEDIR}${PREFIX}/bin/run + +# Shell completions. `runner completions ` is the only generator (a +# `run completions` subcommand does not exist) and emits a single stream +# covering BOTH `runner` and `run`, with the path of the invoking `runner` +# binary baked in via `current_exe()`. Strategy: +# 1. Generate the combined stream for each shell (the freshly staged +# binary runs natively on the build host). +# 2. Rewrite the baked ${WRKSRC} paths to ${PREFIX}/bin/{runner,run}. +# Longer match first — `${WRKSRC}/run` is a prefix of `${WRKSRC}/runner`. +# 3. Split bash + zsh on their start-of-line boundaries so each command +# gets its own autoload file. Fish stays as one file. + @${MKDIR} ${WRKDIR}/_compl + ${WRKSRC}/runner completions bash >${WRKDIR}/_compl/bash.combined + ${WRKSRC}/runner completions zsh >${WRKDIR}/_compl/zsh.combined + ${WRKSRC}/runner completions fish >${WRKDIR}/_compl/fish.combined + ${WRKSRC}/runner completions pwsh >${WRKDIR}/_compl/runner.ps1 + ${REINPLACE_CMD} -e 's|${WRKSRC}/runner|${PREFIX}/bin/runner|g' \ + -e 's|${WRKSRC}/run|${PREFIX}/bin/run|g' \ + ${WRKDIR}/_compl/bash.combined ${WRKDIR}/_compl/zsh.combined \ + ${WRKDIR}/_compl/fish.combined ${WRKDIR}/_compl/runner.ps1 + ${AWK} -v r=${WRKDIR}/_compl/runner.bash -v n=${WRKDIR}/_compl/run.bash \ + '/^_clap_complete_run\(\) \{$$/ {o=n} {print > (o?o:r)}' \ + ${WRKDIR}/_compl/bash.combined + ${AWK} -v r=${WRKDIR}/_compl/_runner -v n=${WRKDIR}/_compl/_run \ + '/^#compdef run$$/ {o=n} {print > (o?o:r)}' \ + ${WRKDIR}/_compl/zsh.combined + @${MKDIR} ${STAGEDIR}${PREFIX}/share/bash-completion/completions + ${INSTALL_DATA} ${WRKDIR}/_compl/runner.bash \ + ${STAGEDIR}${PREFIX}/share/bash-completion/completions/runner + ${INSTALL_DATA} ${WRKDIR}/_compl/run.bash \ + ${STAGEDIR}${PREFIX}/share/bash-completion/completions/run + @${MKDIR} ${STAGEDIR}${PREFIX}/share/zsh/site-functions + ${INSTALL_DATA} ${WRKDIR}/_compl/_runner \ + ${STAGEDIR}${PREFIX}/share/zsh/site-functions/_runner + ${INSTALL_DATA} ${WRKDIR}/_compl/_run \ + ${STAGEDIR}${PREFIX}/share/zsh/site-functions/_run +# Fish autoloads completion files by command basename — `runner.fish` is +# sourced on `runner` but never on `run`. Install the (identical) +# combined stream under both names so each command's first tab works in a +# fresh shell, without depending on session order. + @${MKDIR} ${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d + ${INSTALL_DATA} ${WRKDIR}/_compl/fish.combined \ + ${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d/runner.fish + ${INSTALL_DATA} ${WRKDIR}/_compl/fish.combined \ + ${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d/run.fish +# PowerShell has no system autoload dir — pwsh users dot-source this file +# from their `$PROFILE`: . ${PREFIX}/share/runner/runner.ps1 + @${MKDIR} ${STAGEDIR}${PREFIX}/share/runner + ${INSTALL_DATA} ${WRKDIR}/_compl/runner.ps1 \ + ${STAGEDIR}${PREFIX}/share/runner/runner.ps1 + +post-install-DOCS-on: + @${MKDIR} ${STAGEDIR}${DOCSDIR} + ${INSTALL_DATA} ${WRKSRC}/README.md ${STAGEDIR}${DOCSDIR} + +.include diff --git a/freebsd/runner/distinfo b/freebsd/runner/distinfo new file mode 100644 index 0000000..8a2c9e2 --- /dev/null +++ b/freebsd/runner/distinfo @@ -0,0 +1,5 @@ +TIMESTAMP = 1780337058 +SHA256 (runner-v0.12.0-x86_64-unknown-freebsd.tar.gz) = 578d21585769670d47798cbed4f292cf3eb294e868ca190910d8408ad131f685 +SIZE (runner-v0.12.0-x86_64-unknown-freebsd.tar.gz) = 2281230 +SHA256 (runner-v0.12.0-aarch64-unknown-freebsd.tar.gz) = 17b74daaf9cbb61f388e2736c27f58091377533b2b91940dcf12ede449454084 +SIZE (runner-v0.12.0-aarch64-unknown-freebsd.tar.gz) = 2059418 diff --git a/freebsd/runner/pkg-descr b/freebsd/runner/pkg-descr new file mode 100644 index 0000000..6c6e0ec --- /dev/null +++ b/freebsd/runner/pkg-descr @@ -0,0 +1,11 @@ +runner is for people who bounce between codebases and refuse to memorize +each repo's private little task-running religion. + +Instead of guessing whether a project wants npm run, pnpm exec, bunx, +cargo, uv run, deno task, turbo, make, just, etc., you type `run ` +and runner detects the package manager / task runner in use and dispatches +to it. Shell completion picks up the current task list from package.json, +turbo.json, Justfile and friends, not a static snapshot. + +This package installs the prebuilt `runner` and `run` binaries from the +upstream GitHub release, together with bash, zsh and fish completions.