Skip to content

build: ship cgo-enabled xurl chat in release binaries - #90

Merged
santiagomed merged 1 commit into
mainfrom
cgo-release-binaries
Jul 21, 2026
Merged

build: ship cgo-enabled xurl chat in release binaries#90
santiagomed merged 1 commit into
mainfrom
cgo-release-binaries

Conversation

@santiagomed

Copy link
Copy Markdown
Collaborator

Problem

Release binaries are built with CGO_ENABLED=0 in a single Linux cross-compile, so every distributed binary (npm @xdevplatform/xurl, Homebrew cask, GitHub release tarballs) ships the build-tagged xurl chat stub — even on macOS and linux/amd64 where chat is fully supported.

Approach: one GoReleaser run on a macOS arm64 runner

Rather than a multi-runner build matrix with an artifact-assembly job (GoReleaser OSS has no split/merge), the whole release still comes from one GoReleaser invocationcut-release.yml just moves from ubuntu-latest to macos-latest:

Target Build Chat
darwin/arm64 native cgo (Apple clang), ad-hoc signed ✅ real client
darwin/amd64 cgo cross-arch (-arch x86_64), ad-hoc signed ✅ real client
linux/amd64 cgo via zig cc -target x86_64-linux-musl, -static -lunwind, -tags musl ✅ real client
linux/arm64, linux/386 CGO_ENABLED=0 graceful stub
windows/amd64, arm64, 386 CGO_ENABLED=0 graceful stub

This preserves every existing property with a small diff: one release, no duplicate/racing GoReleaser runs (release.yml stays npm-only, dispatch-only), Homebrew cask updated by the same run, identical asset names — so npm/install.js needs no changes.

Why zig/musl for linux/amd64: chat-xdk ships a linux_amd64_musl static lib selected by a musl build tag. Linking it statically with zig's bundled musl + LLVM libunwind yields a dependency-free static ELF, verified to run (real chat client, no stub string) on both Debian bookworm (glibc) and Alpine (musl) amd64 containers. This answers the "musl variant" question: the single xurl_Linux_x86_64.tar.gz asset is the musl-static build and covers both libc families — no new asset name or npm slot needed. (-lunwind is required because the Rust-built archive references _Unwind_* symbols.)

Build-tag verification: cli/chat.go is gated on cgo && ((darwin && (amd64 || arm64)) || (linux && amd64)); CGO_ENABLED=1 + a working C toolchain for the target is sufficient to flip it, and each cgo target links the matching libs/<os>_<arch> archive (checked in the module cache for chat-xdk v0.4.1: darwin_amd64, darwin_arm64, linux_amd64, linux_amd64_musl — nothing for windows or linux/arm64, so those remain stubs by necessity).

Signing determination (empirical)

  • npm: node's https.get download sets no com.apple.quarantine (only com.apple.provenance); the v1.3.0 npm binary runs directly on macOS arm64. Gatekeeper never triggers, so no notarization is needed.
  • Homebrew: the cask post-install hook already strips quarantine; unchanged and sufficient.
  • The native darwin/arm64 cgo build is linker-signed (ad-hoc) automatically; the cross-built darwin/amd64 binary comes out unsigned, so a codesign -s - --force post-build hook ad-hoc signs both darwin binaries. No Apple credentials required or added.

Changes

  • .goreleaser.yaml: three build groups (darwin-cgo / linux-cgo / stub) replacing the single CGO=0 build; archives/cask/release config untouched; stale comment rewritten.
  • .github/workflows/cut-release.yml: runs-on: macos-latest + pinned mlugg/setup-zig@v2 (zig 0.16.0). go test ./... now also exercises the real chat code path (cgo on macOS).
  • .github/workflows/go.yml: new release-build job compiles all 8 release targets via goreleaser build --snapshot on macOS and fails if the darwin binary prints the stub; the ubuntu job now also cross-compiles the CGO=0 stub targets.
  • cli/chat_unsupported.go: stub message updated — it now only ships on platforms chat can never support, so it no longer suggests that a release binary is missing something a source build would fix.
  • Docs: README "Encrypted Chat" note, SKILL.md availability line, CHANGELOG entry.

Validation

  • Local: make build, make test, go vet ./..., gofmt -l, and CGO=0 stub cross-builds (GOOS=windows, GOOS=linux GOARCH=arm64) all pass.
  • goreleaser build --snapshot --clean on macOS arm64 builds all 8 targets; each binary checked by hand: real chat client on darwin/arm64 (native), darwin/amd64 (under Rosetta), linux/amd64 (Debian + Alpine amd64 containers); stub verified on linux/arm64 (Alpine arm64 container).
  • Release-cut validation on real published artifacts is recorded in the PR comments after the v1.3.x cut.
Release binaries were built with CGO_ENABLED=0 on a Linux runner, so the
npm/Homebrew/GitHub-release binaries always shipped the xurl chat stub.

Cut Release now runs GoReleaser on a macOS arm64 runner with three build
groups: native cgo builds for darwin/arm64 and darwin/amd64 (ad-hoc signed),
a zig-cross-compiled musl-static cgo build for linux/amd64 (runs on both
glibc and musl distros), and CGO_ENABLED=0 stubs for windows/* and
linux/{arm64,386} where chat-xdk has no library. Asset names, the Homebrew
cask, the npm launcher, and the release.yml dispatch dedup are unchanged.

CI gains a release-build job that compiles all release targets and asserts
the darwin binary carries the real chat client, plus stub cross-compile
checks on the ubuntu job.
@santiagomed
santiagomed merged commit 95853a1 into main Jul 21, 2026
4 of 5 checks passed
@santiagomed

Copy link
Copy Markdown
Collaborator Author

v1.3.1 release-cut validation (real published artifacts)

Cut Release run: https://github.com/xdevplatform/xurl/actions/runs/29862081307 (one GoReleaser run; the dispatched Release run published npm — no duplicate/racing runs).

All 9 assets published under the existing names; shasum -a 256 -c xurl_1.3.1_checksums.txt passes for all.

Artifact / channel Environment xurl chat result
npm i -g @xdevplatform/xurl@1.3.1 macOS arm64 ✅ real client — chat conversations authenticated and listed as the live user; binary has no com.apple.quarantine, ad-hoc signed
brew install --cask xdevplatform/tap/xurl (cask auto-updated to 1.3.1) macOS arm64 ✅ real client — chat conversations authenticated as the live user
xurl_Darwin_arm64.tar.gz macOS arm64 ✅ real client, Signature=adhoc
xurl_Darwin_x86_64.tar.gz macOS arm64 (Rosetta) ✅ real client, Signature=adhoc
xurl_Linux_x86_64.tar.gz Debian bookworm and Alpine amd64 containers ✅ real client on both (musl-static); chat conversations reaches the auth layer
xurl_Linux_arm64.tar.gz Alpine arm64 container ✅ graceful stub, xurl version works
xurl_Windows_{x86_64,arm64}.zip go version -m inspection CGO_ENABLED=0 → stub by build constraint (cannot execute Windows locally)
xurl_Linux_i386.tar.gz linux/386 QEMU container crashes under QEMU-on-arm64 emulation — the v1.3.0 binary fails identically in the same container and both are byte-equivalent in build config (CGO_ENABLED=0, GOOS=linux, GOARCH=386), so this is a pre-existing emulation artifact, not a regression

Non-chat functionality spot-check: xurl /2/users/me through the brew-installed 1.3.1 returns the authenticated user.

Signing determination confirmed on the released artifacts: npm's programmatic download sets no quarantine attribute and the binary runs directly; the Homebrew cask post-install strips quarantine. No notarization required; no Apple credentials involved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant