From a66147c6d51678e98efb96168fe191ac2b44f235 Mon Sep 17 00:00:00 2001 From: Chris Lyle Date: Fri, 10 Jul 2026 15:32:20 -0700 Subject: [PATCH] feat: ship a Universal 2 (arm64 + x86_64) darwin release binary The release pipeline builds and ships only the maintainer's native arch (arm64), so Intel Macs have no signed/notarized binary to install and are stuck on unsigned `go install` builds. That blocks universal cookie delivery on Intel sinks, since the sink daemon needs a signed binary to read Chrome Safe Storage via teamid. Build one Universal 2 binary (arm64 + x86_64) with lipo, signed and notarized once, so a single asset runs natively on both architectures: - Makefile: add `build-universal` (cross-compiles each slice with CGO on, pinning the target arch via CC="clang -arch ..." so the build host is irrelevant, then lipo-fuses them). `release` now depends on it. - release-tarball.sh: detect the binary's archs via `lipo -archs` and name the asset darwin-universal (or darwin-arm64 / darwin-amd64 for a single-arch dev build). - install-beta.sh: prefer the darwin-universal asset, fall back to an arch-matched asset for older releases. - release.yml: upload the darwin-universal bundle. - docs: update download filenames and note Intel is supported. Signing/notarization scripts are unchanged and arch-agnostic; no new secrets are required. Verified locally on Apple Silicon: both slices cross-compile with CGO, lipo produces a working universal binary, and the tarball arch-detection resolves universal / arm64 / amd64 correctly. --- .github/RELEASE_NOTES_TEMPLATE.md | 6 +++--- .github/workflows/release.yml | 9 +++++---- Makefile | 32 ++++++++++++++++++++++++++----- docs/quickstart-beta.md | 6 +++--- scripts/install-beta.sh | 13 +++++++++++-- scripts/release-tarball.sh | 23 ++++++++++++++++++---- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/.github/RELEASE_NOTES_TEMPLATE.md b/.github/RELEASE_NOTES_TEMPLATE.md index a2fe748..4a00955 100644 --- a/.github/RELEASE_NOTES_TEMPLATE.md +++ b/.github/RELEASE_NOTES_TEMPLATE.md @@ -4,11 +4,11 @@ Closed-beta release. Invitation only. ## Install -Download `agentcookie-{{VERSION}}-darwin-arm64.tar.gz` from the assets below, then: +Download `agentcookie-{{VERSION}}-darwin-universal.tar.gz` from the assets below, then: ``` -tar -xzf agentcookie-{{VERSION}}-darwin-arm64.tar.gz -cd agentcookie-{{VERSION}}-darwin-arm64 +tar -xzf agentcookie-{{VERSION}}-darwin-universal.tar.gz +cd agentcookie-{{VERSION}}-darwin-universal ./install-beta.sh --as source # on your MacBook # or ./install-beta.sh --as sink # on your second Mac diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d81c445..abe3e66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,9 +71,10 @@ jobs: --team-id NM8VT393AR \ --password "${{ secrets.AC_NOTARY_PASSWORD }}" - # Build, sign, notarize the agentcookie binary. After this step - # bin/agentcookie is fully launchable on any Mac without - # Gatekeeper interactive approval. + # Build, sign, notarize the agentcookie binary. `make release` now + # builds a Universal 2 (arm64 + x86_64) binary, so after this step + # bin/agentcookie is fully launchable on any Intel or Apple Silicon + # Mac without Gatekeeper interactive approval. - name: make release run: make release @@ -102,7 +103,7 @@ jobs: - name: attach beta bundle to release run: | gh release upload "${{ github.ref_name }}" \ - dist/agentcookie-*-darwin-arm64.tar.gz \ + dist/agentcookie-*-darwin-universal.tar.gz \ --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 8f4fb6b..619a4d0 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,19 @@ # # Targets: # make - build and sign bin/agentcookie (default; not notarized) -# make build - go build ./cmd/agentcookie -> bin/agentcookie +# make build - go build ./cmd/agentcookie -> bin/agentcookie (native arch) +# make build-universal +# - build a Universal 2 (arm64 + x86_64) bin/agentcookie +# via lipo, so one signed binary runs on both Apple +# Silicon and Intel Macs # make install - go install ./cmd/agentcookie, then sign $(GOBIN)/agentcookie # make sign - sign bin/agentcookie with the Developer ID identity # make notarize - submit bin/agentcookie to Apple's notary service # (5-30 min; required before deploying to a Mac other # than the one this build ran on) -# make release - build + sign + notarize in one shot (a fully-portable -# binary that launches on any Mac without prompts) +# make release - build-universal + sign + notarize in one shot (a +# fully-portable Universal 2 binary that launches on any +# Intel or Apple Silicon Mac without prompts) # make verify - print the designated requirement of bin/agentcookie # make test - go test -race ./... # make vet - go vet ./... @@ -45,16 +50,33 @@ ifeq ($(GOBIN),) GOBIN := $(shell go env GOPATH)/bin endif -.PHONY: all build install sign notarize release verify test vet clean +.PHONY: all build build-universal install sign notarize release verify test vet clean all: build sign -release: build sign notarize +release: build-universal sign notarize build: @mkdir -p $(BIN_DIR) go build -ldflags "$(LDFLAGS)" -o $(BINARY) $(PKG) +# build-universal - Produce a Universal 2 (arm64 + x86_64) binary at +# bin/agentcookie via lipo so a single signed + notarized release runs +# natively on both Apple Silicon and Intel Macs. CGO must stay enabled +# (the codebase links C for SQLite + the macOS Keychain), so each slice is +# built separately with the target arch pinned through the C compiler +# (CC="clang -arch ..."). Pinning CC per slice makes the build host +# irrelevant: it works whether run on an arm64 or an x86_64 Mac. +build-universal: + @mkdir -p $(BIN_DIR) + CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 CC="clang -arch arm64" \ + go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/agentcookie-arm64 $(PKG) + CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 CC="clang -arch x86_64" \ + go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/agentcookie-amd64 $(PKG) + lipo -create $(BIN_DIR)/agentcookie-arm64 $(BIN_DIR)/agentcookie-amd64 -output $(BINARY) + @rm -f $(BIN_DIR)/agentcookie-arm64 $(BIN_DIR)/agentcookie-amd64 + @echo "make build-universal: wrote $(BINARY) (archs: $$(lipo -archs $(BINARY)))" + # Install to $(GOBIN)/agentcookie and sign in place so steady-state # `make install` produces a signed binary with the same designated # requirement as the local build. diff --git a/docs/quickstart-beta.md b/docs/quickstart-beta.md index e4dd524..2e46e89 100644 --- a/docs/quickstart-beta.md +++ b/docs/quickstart-beta.md @@ -23,7 +23,7 @@ with no `auth login`, no Keychain prompt, no copy-paste-the-cookie ritual. ## Prereqs -- Two Macs running macOS 14 or later. Apple silicon recommended. One you browse on (we'll call it source); one your agents run on (sink). Many people use a Mac mini for the sink. +- Two Macs running macOS 14 or later. Both Apple Silicon and Intel are supported (the release ships a Universal 2 binary). One you browse on (we'll call it source); one your agents run on (sink). Many people use a Mac mini for the sink. - Both Macs on the same Tailscale tailnet. Run `tailscale status` on each; both should appear in each other's list. If not, set up Tailscale first. - Google Chrome installed on the source. Sign in to whatever sites you want your agents to act on. - The release tarball (your invite includes a link or `gh release download` instructions). @@ -32,8 +32,8 @@ Optional: Go 1.22+ if you want to build from source. Not required when using the ## Install the source side (your MacBook) -1. Download `agentcookie-v0.12.0-beta.1-darwin-arm64.tar.gz` from the release link in your invite. -2. Extract: `tar -xzf agentcookie-v0.12.0-beta.1-darwin-arm64.tar.gz`. The bundle contains `agentcookie`, `install-beta.sh`, and this guide. +1. Download `agentcookie-v0.12.0-beta.1-darwin-universal.tar.gz` from the release link in your invite. +2. Extract: `tar -xzf agentcookie-v0.12.0-beta.1-darwin-universal.tar.gz`. The bundle contains `agentcookie`, `install-beta.sh`, and this guide. 3. Run the install script: `./install-beta.sh --as source`. It will: - Verify your binary is notarized (so macOS doesn't block it) - Place it at `/usr/local/bin/agentcookie` (or `~/bin/agentcookie` if you don't have admin) diff --git a/scripts/install-beta.sh b/scripts/install-beta.sh index 5e580cb..8f36b04 100755 --- a/scripts/install-beta.sh +++ b/scripts/install-beta.sh @@ -148,7 +148,15 @@ if [[ -z "$TARBALL" ]]; then fi step "downloading latest beta release from $REPO" TMP_DL="$(mktemp -d -t agentcookie-beta.XXXXXX)" - gh release download --repo "$REPO" --pattern '*darwin-arm64.tar.gz' --dir "$TMP_DL" --clobber + # Prefer the Universal 2 asset (one binary for both Apple Silicon and + # Intel). Fall back to an arch-specific asset for older releases that + # predate the universal binary; map uname -m (x86_64) to Go's amd64. + HOST_ARCH="$(uname -m)" + [[ "$HOST_ARCH" == "x86_64" ]] && HOST_ARCH="amd64" + if ! gh release download --repo "$REPO" --pattern '*darwin-universal.tar.gz' --dir "$TMP_DL" --clobber 2>/dev/null; then + step "no universal asset found; falling back to darwin-$HOST_ARCH" + gh release download --repo "$REPO" --pattern "*darwin-${HOST_ARCH}.tar.gz" --dir "$TMP_DL" --clobber + fi TARBALL="$(ls -1 "$TMP_DL"/*.tar.gz | head -n1)" if [[ -z "$TARBALL" || ! -f "$TARBALL" ]]; then die "release tarball not found after download (looked in $TMP_DL)" @@ -161,7 +169,8 @@ fi WORK="$(mktemp -d -t agentcookie-install.XXXXXX)" tar -xzf "$TARBALL" -C "$WORK" # The release tarball wraps everything in a versioned directory -# (agentcookie-${VERSION}-darwin-arm64/), so the binary is one level +# (agentcookie-${VERSION}-darwin-universal/, or -darwin-arm64/ / +# -darwin-amd64/ for single-arch builds), so the binary is one level # deep. find tolerates both shapes (wrapped + flat). NEW_BIN="$(find "$WORK" -name agentcookie -type f -perm -u+x 2>/dev/null | head -n1)" if [[ -z "$NEW_BIN" || ! -x "$NEW_BIN" ]]; then diff --git a/scripts/release-tarball.sh b/scripts/release-tarball.sh index 01648d4..d248b66 100755 --- a/scripts/release-tarball.sh +++ b/scripts/release-tarball.sh @@ -11,7 +11,11 @@ # Where matches the release tag (e.g. v0.12.0-beta.1). The # script produces: # -# dist/agentcookie--darwin-arm64.tar.gz +# dist/agentcookie--darwin-universal.tar.gz (arm64 + x86_64) +# +# When bin/agentcookie is a single-arch binary (e.g. a `make build` dev +# build rather than `make build-universal`), the tarball is named for that +# lone arch instead (darwin-arm64 / darwin-amd64). # # Prereqs: # 1. bin/agentcookie exists, signed and notarized (run `make release` @@ -54,11 +58,22 @@ if ! codesign -d -r- "$BIN" >/dev/null 2>&1; then exit 2 fi -ARCH="$(uname -m)" -if [[ "$ARCH" == "arm64" ]]; then +# Name the tarball for the binary's actual architecture(s). A Universal 2 +# binary (make build-universal) carries both slices and ships as +# "darwin-universal"; a single-arch dev build ships as darwin-amd64 / +# darwin-arm64. lipo -archs reports "x86_64" (Go's amd64) and/or "arm64". +BIN_ARCHS="$(lipo -archs "$BIN" 2>/dev/null || true)" +if [[ "$BIN_ARCHS" == *arm64* && "$BIN_ARCHS" == *x86_64* ]]; then + TARBALL_ARCH="darwin-universal" +elif [[ "$BIN_ARCHS" == *x86_64* ]]; then + TARBALL_ARCH="darwin-amd64" +elif [[ "$BIN_ARCHS" == *arm64* ]]; then TARBALL_ARCH="darwin-arm64" else - TARBALL_ARCH="darwin-$ARCH" + # lipo unavailable or silent; fall back to the build host arch. + HOST_ARCH="$(uname -m)" + [[ "$HOST_ARCH" == "x86_64" ]] && HOST_ARCH="amd64" + TARBALL_ARCH="darwin-$HOST_ARCH" fi OUT_NAME="agentcookie-${VERSION}-${TARBALL_ARCH}"