Skip to content

Publish precompiled native gems on tag (x86_64-linux, aarch64-linux, arm64-darwin) - #109

Merged
xrl merged 2 commits into
mainfrom
precompiled-native-gems
Jun 18, 2026
Merged

xrl merged 2 commits into
mainfrom
precompiled-native-gems

Conversation

@xrl

@xrl xrl commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What & why

red-candle has only ever published a source-only gem — all 30 versions (incl. 1.7.1) are platform ruby. So every consumer compiles the full Candle/Rust extension at bundle install: rb_sys bootstraps a toolchain via curl https://sh.rustup.rs | sh and builds from scratch on any cache miss. That's slow, network-fragile, and an unpinned curl|sh on the build path.

This wires up the cross-compile scaffolding the Rakefile already declares (but release.yml never invoked) so a git tag publishes precompiled fat gems and consumers install with no Rust toolchain:

Gem Runner Accel Notes
red-candle-<v>.gem (ruby) ubuntu-24.04 — source fallback (GPU-from-source, unsupported ABIs)
…-x86_64-linux.gem ubuntu-24.04 CPU oxidize-rb cross-gem
…-aarch64-linux.gem ubuntu-24.04-arm CPU native arm runner (no QEMU)
…-arm64-darwin.gem macos-26 Metal + Accelerate native per-ABI compile → merged fat gem

Each fat gem carries one binary per Ruby ABI: 3.1, 3.2, 3.3, 3.4, 4.0.

How it works

  • lib/candle.rb — loads the extension from the ABI-versioned subdir (lib/candle/<major.minor>/candle.{so,bundle}) first, falling back to the flat path. Without this, a fat gem LoadErrors. spec/require_spec.rb updated (the existing substring guards still hold).
  • candle.gemspec — RED_CANDLE_PLATFORM_GEM gate: for the manually-assembled darwin fat gem it sets the platform, clears spec.extensions (else RubyGems recompiles on install) and packs the per-ABI bundles. The linux gems are assembled by rake-compiler/rb_sys, which clears extensions itself; the source gem path is unchanged.
  • release.yml — on: push: tags:; a prepare job guards that the tag matches version.rb. Pushes are per-platform and idempotent so a darwin flake can't block the linux gems and the source gem always ships.

Why arm64-darwin is a native job (and a win, not a regression)

A darwin gem cross-compiled from Linux is CPU-only (Metal frameworks can't link under osxcross) — that would silently strip Metal from Mac devs. Built natively on macos-26, extconf.rb auto-enables metal + accelerate, and since candle 0.9.2 compiles its Metal shaders at runtime (newLibraryWithSource; no build.rs, no metallib), the build needs only base CLT — no Metal Toolchain download, no full-Xcode select. Metal/Accelerate are system frameworks, so the binary is portable across Apple Silicon Macs (unlike CUDA, which pins a driver). We publish the generic arm64-darwin platform because RubyGems doesn't fall back across darwin majors. CI has no GPU, so the release gate is an otool -L link check, not a runtime Metal test.

Versions

All actions SHA-pinned to current latest (June 2026): checkout v6.0.3, upload-artifact v7.0.1, download-artifact v8.0.1, setup-ruby v1.313.0, oxidize-rb/actions/* v1.4.4. Rust stable (1.96). Also bumps build.yml off the deprecated checkout@v3.

Release procedure

The tag (not CI) drives it — a CI-created tag wouldn't re-trigger:

# bump lib/candle/version.rb, then:
git commit -am "Release vX.Y.Z" && git tag X.Y.Z && git push origin HEAD --tags

Verification

Local: ruby -c, gemspec evaluates correctly in both modes (platform=ruby/extensions set vs platform=arm64-darwin/extensions cleared + bundles packed), require_spec assertions pass, both workflows actionlint-clean. The cross-compile feasibility, gem size (~17 MB stripped/ABI, far under the ~500 MB cap), and Metal build-vs-runtime questions were each verified with adversarial review; the heavy aarch64/darwin compiles themselves are first exercised by this workflow on the first tagged release.

Follow-ups (not in this PR)

  • Consume in rx: bump red-candle to the first tag built here, then bundle lock --add-platform aarch64-linux (the Candle::Reranker API is byte-identical 1.2.2→1.7.x).
  • Optional: musl gems if an Alpine consumer appears; migrate benchmate off its git-fork pin.

🤖 Generated with Claude Code

xrl and others added 2 commits June 18, 2026 00:38
…arm64-darwin)

red-candle has only ever shipped a source ("ruby" platform) gem, so every
consumer compiles the full Candle/Rust extension at `bundle install` time:
rb_sys bootstraps rustup via `curl | sh` and builds from scratch on any cache
miss. This adds a tag-triggered release pipeline that publishes precompiled fat
gems so consumers install with no Rust toolchain.

- lib/candle.rb: load the extension from a Ruby-ABI-versioned subdir
  (lib/candle/<minor>/candle.{so,bundle}) first, falling back to the flat path,
  so a fat gem can carry one binary per Ruby version. require_spec updated to
  assert the versioned-first-with-fallback form.
- candle.gemspec: RED_CANDLE_PLATFORM_GEM gate that, for the manually-assembled
  darwin fat gem, sets the platform, clears spec.extensions (no recompile on
  install) and packs the per-ABI bundles.
- .github/workflows/release.yml: on a version tag, build + push the source gem
  plus x86_64-linux & aarch64-linux (oxidize-rb cross-gem; aarch64 native on
  ubuntu-24.04-arm, no QEMU) and a native Metal+Accelerate arm64-darwin fat gem
  (matrix compile on macos-26 -> merge). ABIs 3.1-4.0. Actions SHA-pinned latest.
- build.yml: bump deprecated actions/checkout@v3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allow X.Y.Z.preN / .rcN tags (RubyGems dot form) to drive the pipeline so the
full tag -> rubygems.org flow can be exercised with e.g. 1.8.0.pre1 — which
normal requirements (rx's `~> 1`, plain `gem install`) ignore. The prepare job
now derives a prerelease flag (Gem::Version#prerelease?) and the GitHub Release
is marked --prerelease accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xrl
xrl merged commit 3388f62 into main Jun 18, 2026
2 checks passed
@xrl
xrl deleted the precompiled-native-gems branch June 18, 2026 04:57
xrl added a commit that referenced this pull request Jun 18, 2026
End-to-end test of the precompiled-native-gem release pipeline (#109) all the
way to rubygems.org. Prereleases are ignored by normal requirements (rx's
`~> 1`, plain `gem install`), so this is non-disruptive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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