#!/usr/bin/env bash # # Cross-compiles the atfs CLI (./cmd/atfs) for every platform named on the # command line, or for every platform atfs ships CLI binaries for when none # are named. # # The target list is the SAME four GOOS/GOARCH pairs as flake.nix's # `systems` — that list is atfs's own commitment to which platforms the CLI # supports, so a release that shipped a different set of binaries would be # publishing artifacts for platforms the flake doesn't (or vice versa, # silently dropping one it does). Restated here rather than read out of # flake.nix because there's no nix on the machines that call this script # (see CLAUDE.md's "flake.nix's vendorHash and flake.lock are derived, # never authored" — the same reasoning: no nix on the dev machine, so # nothing here can shell out to it to ask). # # This is the CLI-binary counterpart to hack/build-sbc-images.sh: same # structure, same --targets/--boards introspection flag, same VERSION # convention. hack/publish-release-images.sh is the caller — it uploads # whatever this script builds, exactly as it already does for # hack/build-sbc-images.sh's board images (see that script's header). # # CGO_ENABLED=0 matches the CGO-free ground rule (CLAUDE.md) and the flake's # own `env.CGO_ENABLED = 0`; the ldflags match both the flake and # .tangled/workflows/publish-sbc-images.yml's existing single-platform # "build the atfs CLI" step, so `atfs --version` reports the same thing # whichever build produced the binary. # # VERSION in the environment feeds -X main.version; it defaults to a git # describe, so a shallow or tagless checkout falls back to a short SHA, # which is fine and honest. A release passes its validated tag explicitly. # # The two darwin-* binaries are additionally code-signed and notarized # (ATFS-e3fz) when APPLE_CERT_P12_BASE64, APPLE_CERT_PASSWORD and # APPLE_API_KEY_JSON_BASE64 are ALL set — see # .tangled/workflows/publish-sbc-images.yml's header for what each one is # and where to get it. Any other subset (one or two of the three) is # refused outright: a half-configured signing setup is far more likely a # mistake than a deliberate choice, and shipping an unsigned binary while # believing it's signed is worse than failing loudly. With none set at all, # darwin binaries build exactly as they always have — this is what keeps # local dev builds and any release cut before the Apple credentials exist # working unchanged. # # Signing uses rcodesign (the `apple-codesign` Rust crate) rather than # Xcode's codesign/notarytool, since Tangled's CI has no macOS runner (see # CLAUDE.md) — rcodesign speaks Apple's signing and Notary API from Linux. # A standalone executable (not an .app/.dmg/.pkg) can be signed directly, # but Apple's notary service only accepts a zip/dmg/pkg submission and a # notarization ticket can only be STAPLED to one of those, never to a bare # Mach-O binary — so the signed binary itself is what ships, a throwaway # zip of it is what gets submitted for notarization, and Gatekeeper # performs its usual online check against Apple's servers the first time a # downloaded (quarantined) copy is run. That's the same mechanism the # downloads page's now-conditional quarantine notice describes. # # Usage: hack/build-cli-binaries.sh [TARGET...] # hack/build-cli-binaries.sh --targets # print the target list, build nothing set -euo pipefail # Every platform atfs ships CLI binaries for — kept identical to flake.nix's # `systems` list by hand (see above for why this can't just read that file). # Target id is $GOOS-$GOARCH, the same plain lowercase-hyphenated shape # hack/build-sbc-images.sh uses for board ids. ALL_TARGETS=(linux-amd64 linux-arm64 darwin-amd64 darwin-arm64) if [ "${1:-}" = "--targets" ]; then echo "${ALL_TARGETS[*]}" exit 0 fi TARGETS=("$@") [ "${#TARGETS[@]}" -gt 0 ] || TARGETS=("${ALL_TARGETS[@]}") VERSION="${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo unknown)}" # SIGN_APPLE decides once, up front, whether any darwin-* target this run # builds gets signed — never decided per-target, so both mac binaries in a # release always come out the same way. See the header comment above for # what "some but not all" refusal is guarding against. n_apple_vars=0 for var in APPLE_CERT_P12_BASE64 APPLE_CERT_PASSWORD APPLE_API_KEY_JSON_BASE64; do [ -n "${!var:-}" ] && n_apple_vars=$((n_apple_vars + 1)) done case "$n_apple_vars" in 0) SIGN_APPLE=0 ;; 3) SIGN_APPLE=1 ;; *) echo "APPLE_CERT_P12_BASE64, APPLE_CERT_PASSWORD and APPLE_API_KEY_JSON_BASE64 must be set together or not at all (got $n_apple_vars of 3) — see .tangled/workflows/publish-sbc-images.yml's header" >&2 exit 1 ;; esac # Decoded once, however many darwin-* targets this run builds, into a temp # dir removed on exit whatever happens — these are secrets, not build # output, and never belong on disk longer than the sign/notarize calls # that need them. APPLE_SECRETS_DIR="" if [ "$SIGN_APPLE" -eq 1 ]; then APPLE_SECRETS_DIR="$(mktemp -d)" trap 'rm -rf "$APPLE_SECRETS_DIR"' EXIT base64 -d <<<"$APPLE_CERT_P12_BASE64" >"$APPLE_SECRETS_DIR/cert.p12" printf '%s' "$APPLE_CERT_PASSWORD" >"$APPLE_SECRETS_DIR/cert.password" base64 -d <<<"$APPLE_API_KEY_JSON_BASE64" >"$APPLE_SECRETS_DIR/api-key.json" chmod 600 "$APPLE_SECRETS_DIR"/* fi # sign_and_notarize code-signs $1 in place with the hardened runtime flag # notarization requires, zips it into a throwaway submission, and blocks # (--wait) on Apple's notary service accepting it. A rejection or any # transport failure exits non-zero, which — same as every other step in # this script — takes the whole build down: a release is not the place to # discover a lapsed certificate or a rejected binary by shipping it anyway. sign_and_notarize() { local bin="$1" zip="$1.zip" echo " signing + notarizing…" rcodesign sign \ --p12-file "$APPLE_SECRETS_DIR/cert.p12" \ --p12-password-file "$APPLE_SECRETS_DIR/cert.password" \ --code-signature-flags runtime \ "$bin" rm -f "$zip" zip -q "$zip" "$bin" rcodesign notary-submit \ --api-key-file "$APPLE_SECRETS_DIR/api-key.json" \ --wait \ "$zip" rm -f "$zip" # A marker, not a modification to $bin itself: hack/publish-release-images.sh # reads this to record `signed: true` in the release manifest per CLI # target (ATFS-e3fz), which is how the downloads page knows to stop # telling visitors to clear the quarantine flag by hand. touch "$bin.signed" } for target in "${TARGETS[@]}"; do case "$target" in linux-amd64) goos=linux; goarch=amd64 ;; linux-arm64) goos=linux; goarch=arm64 ;; darwin-amd64) goos=darwin; goarch=amd64 ;; darwin-arm64) goos=darwin; goarch=arm64 ;; *) echo "unknown target '$target' — valid targets: ${ALL_TARGETS[*]}" >&2 exit 1 ;; esac echo "=== go build: $target ($VERSION) ===" out="atfs-$target" # Removed first, so a build that dies partway can't leave a previous run's # binary (or a stale .signed marker claiming a fresh unsigned rebuild is # signed) sitting there for a later step to upload as this one's (same # reasoning as hack/build-sbc-images.sh's rm -f before each gosd build). rm -f "$out" "$out.signed" GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 go build \ -ldflags "-X main.version=$VERSION" \ -o "$out" ./cmd/atfs if [ "$SIGN_APPLE" -eq 1 ] && [[ "$target" == darwin-* ]]; then sign_and_notarize "$out" fi done