@@ -1,200 +1,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Build and (on main) publish the runner binaries for all four targets.
#
# The cross-compilation toolchain — zig, cargo-zigbuild, the macOS SDK, and the
# rustup targets — is NOT installed here. It is baked into a cached prepared
# image by the `prepare:` block on the build-runner step in .anvil.yml, so it is
# not reinstalled on every run (fangorn/anvil-cli#43, fangorn/anvil#354). This
# script assumes that image: zig on PATH under /opt/zig, cargo-zigbuild in
# CARGO_HOME, the targets added, and the SDK unpacked under /opt.
#
# IMPORTANT: the toolchain *version pins* live in .anvil.yml's prepare commands,
# because the prepared-image cache key hashes those command strings, not this
# script. Bumping a version here would not rebuild the image. Keep pins there.
# The CI runner bind-mounts the workspace owned by a non-root host user
# into a root-run container. Git's safe-directory check rejects that,
# exiting 128 when we later try to read log history for the changelog.
git config --global --add safe.directory /workspace
# Fail fast if we're about to publish a release but the secret is missing —
# don't waste minutes on a build that will error at the end.
PUBLISH=0
if [ "${ANVIL_BRANCH:-}" = "main" ] || [ "${ANVIL_BRANCH:-}" = "refs/heads/main" ]; then
if [ -z "${ANVIL_TOKEN:-}" ]; then
echo "ERROR: ANVIL_TOKEN secret not set. Run:" >&2
echo " anvil ci set-secret --name ANVIL_TOKEN --value <pat> --repo fangorn/anvil-cli" >&2
exit 1
fi
PUBLISH=1
fi
# Compute the CalVer version and tag HEAD locally so that build.rs can
# read it via 'git describe --tags'. The tag is local-only — it never
# gets pushed. The server-side tag is created by 'anvil release create'.
if [ "$PUBLISH" = "1" ]; then
VERSION=$(bash ci/release.sh)
if [ -z "$VERSION" ]; then
echo "ERROR: ci/release.sh returned empty version" >&2
exit 1
fi
echo "==> Tagging HEAD as $VERSION (local only)"
git tag -f "$VERSION"
fi
# ── Toolchain env (binaries come from the prepared image) ────────────────
#
# The prepare block installs the toolchain but a `docker commit` snapshots the
# filesystem, not exported env vars — so set PATH/SDKROOT here at build time.
export PATH="/opt/zig:$PATH"
# macOS targets: no glibc floor (Apple libSystem). zig bundles glibc/musl but
# NOT Apple's non-redistributable libSystem + frameworks, so cross-linking
# against Darwin needs a macOS SDK, found via SDKROOT. Glob the SDK dir so this
# script carries no SDK version (the version pin lives in .anvil.yml).
SDKROOT=$(echo /opt/MacOSX*.sdk)
if [ ! -d "$SDKROOT" ]; then
echo "ERROR: macOS SDK not found under /opt (expected the prepared image to unpack it)" >&2
exit 1
fi
export SDKROOT
# Link against an older Darwin so the binaries run on macOS 11+ (Big Sur).
export MACOSX_DEPLOYMENT_TARGET=11.0
# ── Pinned glibc floor (anvil-cli#20) ───────────────────────────────────
#
# Building natively on the trixie image bakes a GLIBC_2.39 requirement into the
# binary, which then fails to load on older runtimes. cargo-zigbuild links
# against an older glibc supplied by zig, so the binaries run on
# glibc >= GLIBC_FLOOR. This floor is a build/verify constant (not an install
# version), so it stays here.
GLIBC_FLOOR=2.31
echo "==> Building linux arm64 (glibc $GLIBC_FLOOR floor)..."
cargo zigbuild --release --target "aarch64-unknown-linux-gnu.${GLIBC_FLOOR}" 2>&1
echo "==> Building linux amd64 (glibc $GLIBC_FLOOR floor)..."
cargo zigbuild --release --target "x86_64-unknown-linux-gnu.${GLIBC_FLOOR}" 2>&1
echo "==> Building macos arm64..."
cargo zigbuild --release --target aarch64-apple-darwin 2>&1
echo "==> Building macos amd64..."
cargo zigbuild --release --target x86_64-apple-darwin 2>&1
ARM64_BIN="target/aarch64-unknown-linux-gnu/release/anvil"
AMD64_BIN="target/x86_64-unknown-linux-gnu/release/anvil"
MACOS_ARM64_BIN="target/aarch64-apple-darwin/release/anvil"
MACOS_AMD64_BIN="target/x86_64-apple-darwin/release/anvil"
# Hard-gate the glibc floor: fail the build if either linux binary references a
# GLIBC symbol newer than the floor. Without the gate a future dep or toolchain
# bump silently raises it again. The version tags live as ASCII in the dynamic
# string table, so grep finds them without binutils.
assert_glibc_floor() {
local bin="$1" max
max=$(grep -aoE 'GLIBC_[0-9]+\.[0-9]+' "$bin" | sort -V | tail -1)
if [ -z "$max" ]; then
echo "ERROR: no GLIBC version symbols found in $bin (unexpected)" >&2
exit 1
fi
if [ "$(printf '%s\nGLIBC_%s\n' "$max" "$GLIBC_FLOOR" | sort -V | tail -1)" != "GLIBC_${GLIBC_FLOOR}" ]; then
echo "ERROR: $bin requires $max, above the GLIBC_${GLIBC_FLOOR} floor" >&2
exit 1
fi
echo " OK: $bin max $max (<= GLIBC_${GLIBC_FLOOR})"
}
echo "==> Verifying glibc floor ($GLIBC_FLOOR)..."
assert_glibc_floor "$ARM64_BIN"
assert_glibc_floor "$AMD64_BIN"
# Stage for CI artifact upload (unversioned names)
mkdir -p runner-dist
cp "$ARM64_BIN" runner-dist/anvil_linux_arm64
cp "$AMD64_BIN" runner-dist/anvil_linux_amd64
cp "$MACOS_ARM64_BIN" runner-dist/anvil_macos_arm64
cp "$MACOS_AMD64_BIN" runner-dist/anvil_macos_amd64
if [ "$PUBLISH" != "1" ]; then
exit 0
fi
# Use the binary matching THIS runner's architecture to drive `release
# list`/`release create` — the publish job can land on either an arm64 (carl)
# or amd64 (xps) runner. Hardcoding one made publishing fail with "Exec format
# error" whenever the job ran on the other runner.
case "$(uname -m)" in
aarch64 | arm64) ANVIL_CLI="$PWD/$ARM64_BIN" ;;
x86_64 | amd64) ANVIL_CLI="$PWD/$AMD64_BIN" ;;
*)
echo "ERROR: unsupported runner architecture: $(uname -m)" >&2
exit 1
;;
esac
echo "==> Publishing release $VERSION"
cp "$ARM64_BIN" "runner-dist/anvil_linux_arm64_${VERSION}"
cp "$AMD64_BIN" "runner-dist/anvil_linux_amd64_${VERSION}"
cp "$MACOS_ARM64_BIN" "runner-dist/anvil_macos_arm64_${VERSION}"
cp "$MACOS_AMD64_BIN" "runner-dist/anvil_macos_amd64_${VERSION}"
# Compute SHA256 checksums for every versioned asset — published as
# SHA256SUMS_${VERSION} alongside the binaries so install scripts can verify
# what they downloaded. Format matches `sha256sum` output: `<hash> <filename>`.
echo "==> Computing SHA256 checksums..."
(
cd runner-dist
ASSETS="anvil_linux_arm64_${VERSION} anvil_linux_amd64_${VERSION} anvil_macos_arm64_${VERSION} anvil_macos_amd64_${VERSION}"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum $ASSETS > "SHA256SUMS_${VERSION}"
else
shasum -a 256 $ASSETS > "SHA256SUMS_${VERSION}"
fi
)
cat "runner-dist/SHA256SUMS_${VERSION}"
# Generate changelog body from commits since the previous CalVer tag.
PREV_TAG=$("$ANVIL_CLI" release list --format json fangorn/anvil-cli \
| jq -r '[.[] | select(.tag_name | test("^[0-9]{4}\\.[0-9]{2}\\.[0-9]+$"))]
| sort_by(.tag_name | split(".") | map(tonumber))
| .[-1].tag_name // empty')
if [ -n "$PREV_TAG" ] && git rev-parse --verify "$PREV_TAG" >/dev/null 2>&1; then
CHANGELOG=$(git log --oneline "${PREV_TAG}..HEAD" || echo "(no commits since $PREV_TAG)")
else
CHANGELOG=$(git log --oneline -n 20)
fi
BODY=$(printf 'Runner binaries for linux/{amd64,arm64} and macos/{amd64,arm64}.\n\n## Changes\n\n%s\n' "$CHANGELOG")
# Publish atomically: create the release as a DRAFT, upload every asset, and
# only publish once they are all present. A failure mid-upload leaves a draft
# (invisible to `anvil update` and to the version bump), never a half-populated
# public release — the failure mode that shipped 2026.07.6 broken (#40).
"$ANVIL_CLI" release create \
--tag "$VERSION" \
--title "anvil-cli $VERSION" \
--body "$BODY" \
--draft \
--repo fangorn/anvil-cli
# Roll back the draft if any subsequent step fails — don't leave orphaned
# drafts lying around. A SIGKILL (e.g. step timeout) can't run this trap, but
# a killed run leaves only an unpublished draft, which is harmless.
cleanup_release() {
echo "==> Publish failed — rolling back release $VERSION" >&2
"$ANVIL_CLI" release delete "$VERSION" --repo fangorn/anvil-cli >&2 || true
}
trap cleanup_release ERR
for asset in \
"anvil_linux_arm64_${VERSION}" \
"anvil_linux_amd64_${VERSION}" \
"anvil_macos_arm64_${VERSION}" \
"anvil_macos_amd64_${VERSION}" \
"SHA256SUMS_${VERSION}"; do
"$ANVIL_CLI" release upload "$VERSION" "runner-dist/${asset}" --repo fangorn/anvil-cli
done
# Everything is present — flip the draft to published in one step.
"$ANVIL_CLI" release publish "$VERSION" --repo fangorn/anvil-cli
trap - ERR
echo "==> Published release $VERSION"