Files
Andrey Kumanyaev 9449485dcf release: fix the homebrew cask so it loads at all
The generated cask used `pre_install` / `post_install`. Those are formula
DSL methods; a cask has no such stanzas, so brew rejected the file at load
time with

  Error: Cask 'gortex' definition is invalid: undefined method 'pre_install'

Every brew command that touches the cask parses it, so `brew install` and
`brew upgrade` both failed for every macOS user on both arches, starting
with v0.63.7. Closes #639.

Two further defects made a straight rename insufficient:

  * The cask DSL's `system_command` is `SystemCommand.run!`, which raises on
    a non-zero exit. `gortex daemon status` exits 1 exactly when no daemon
    is reachable, so `next unless status.success?` could never run — brew
    would have aborted the install outright on any machine without a live
    daemon. The probe now passes `must_succeed: false`.

  * `preflight` was the wrong hook for stopping the daemon. On upgrade brew
    unlinks the old cask's binary (`start_upgrade` -> `uninstall_artifacts`)
    before installing the new cask's artifacts, so by the time a preflight
    block runs there is no gortex on disk to ask. The stop half could never
    fire on the path it was written for.

`postflight` handles both halves instead: after the new binary is linked,
probe for a daemon and, only if one answers, `daemon restart` — which stops
the old process (blocking until it exits, releasing the store lock) before
starting the new one, so a store migration still runs alone. A fresh install
and CI have no daemon answering and skip it.

The cask body moves out of the release.yml heredoc into
.github/homebrew/gortex.rb.tmpl, rendered by scripts/render-cask.sh. That
makes it reviewable as Ruby, removes the shell-expansion hazards of an
unquoted heredoc, and — the point — lets a real brew load it before
publication. The renderer also refuses a malformed sha256, a version with a
leading "v", or a placeholder that survived substitution. Rendered output is
byte-identical to the published cask apart from the hook block.

Nothing could have caught this: the file was valid Ruby, valid YAML, and the
tap has no CI, so the first machine to evaluate the cask was a user's.
scripts/validate-cask.sh renders the template with dummy values and loads it
through a real brew. It runs on every PR that touches the cask
(.github/workflows/homebrew-cask.yml) and again in build-darwin as the last
gate before the release job pushes to the tap. Both are macOS-only —
Homebrew on Linux cannot load casks. Verified by reintroducing `pre_install`
in the template: the validator fails with the exact error users reported.
2026-08-20 21:16:46 +02:00

78 lines
2.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Render .github/homebrew/gortex.rb.tmpl into a concrete Homebrew cask on
# stdout. The template lives in its own file (rather than a heredoc inside
# release.yml) so it can be reviewed as Ruby and, crucially, parsed by a real
# `brew` before a release pushes it to zzet/homebrew-tap — see
# scripts/validate-cask.sh.
#
# Usage:
# render-cask.sh <version> <sha_darwin_amd64> <sha_darwin_arm64> \
# <sha_linux_amd64> <sha_linux_arm64>
#
# `version` is the bare version with no leading "v" (e.g. 0.63.8).
set -euo pipefail
if [ "$#" -ne 5 ]; then
echo "usage: $0 <version> <sha_darwin_amd64> <sha_darwin_arm64> <sha_linux_amd64> <sha_linux_arm64>" >&2
exit 2
fi
version="$1"
sha_darwin_amd64="$2"
sha_darwin_arm64="$3"
sha_linux_amd64="$4"
sha_linux_arm64="$5"
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
tmpl="$repo_root/.github/homebrew/gortex.rb.tmpl"
[ -f "$tmpl" ] || {
echo "FATAL: cask template not found at $tmpl" >&2
exit 1
}
case "$version" in
"")
echo "FATAL: version is empty" >&2
exit 1
;;
v*)
echo "FATAL: version must not carry a leading 'v': '$version'" >&2
exit 1
;;
esac
# Every sha must be a full sha256. An empty or truncated value would publish a
# cask whose downloads all fail a checksum check, and the error the user sees
# ("SHA256 mismatch") points nowhere near the cause.
for pair in "darwin_amd64=$sha_darwin_amd64" "darwin_arm64=$sha_darwin_arm64" \
"linux_amd64=$sha_linux_amd64" "linux_arm64=$sha_linux_arm64"; do
sha="${pair#*=}"
if ! printf '%s' "$sha" | grep -Eq '^[0-9a-f]{64}$'; then
echo "FATAL: sha256 for ${pair%%=*} is not 64 lowercase hex chars: '$sha'" >&2
exit 1
fi
done
# The template also contains `#{version}` — Ruby interpolation brew evaluates
# at install time. It must survive verbatim, which is why substitution is
# keyed on @@…@@ placeholders rather than shell expansion.
rendered="$(sed \
-e "s|@@VERSION@@|$version|g" \
-e "s|@@SHA_DARWIN_AMD64@@|$sha_darwin_amd64|g" \
-e "s|@@SHA_DARWIN_ARM64@@|$sha_darwin_arm64|g" \
-e "s|@@SHA_LINUX_AMD64@@|$sha_linux_amd64|g" \
-e "s|@@SHA_LINUX_ARM64@@|$sha_linux_arm64|g" \
"$tmpl")"
# A placeholder that survived rendering means the template grew a field the
# renderer doesn't know about — publishing that would ship a literal @@…@@
# into the tap.
if leftover="$(printf '%s\n' "$rendered" | grep -n '@@[A-Z0-9_]*@@')"; then
echo "FATAL: unsubstituted placeholder left in the rendered cask:" >&2
printf '%s\n' "$leftover" >&2
exit 1
fi
printf '%s\n' "$rendered"