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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant