Skip to content

Commit bf9cd77

Browse files
committed
Add native gem precompilation support
Package precompiled native gems for Ruby 3.3, 3.4, and 4.0 across 10 platforms: aarch64-linux-gnu, aarch64-linux-musl, aarch64-mingw-ucrt, arm-linux-gnu, arm-linux-musl, arm64-darwin, x64-mingw-ucrt, x86_64-darwin, x86_64-linux-gnu, and x86_64-linux-musl. - Add Ruby-version-aware extension loader in lib/prism.rb with GLIBC error message for musl/glibc mismatches - Add cross-compilation support to Rakefile using rake-compiler-dock - Add a reusable `build-gems.yml` workflow called by both the CI workflow (`native-gem-precompilation.yml`) and the publish workflow, building source and native gems across all platforms and Ruby versions - Add bin/ scripts for building, installing, testing, and verifying gems Update `publish-gem.yml` to handle multi-gem publishing: `rubygems/release-gem` only supports a single gem, so this uses `rubygems/configure-rubygems-credentials` and loops `gem push` over each built `.gem`. Add a `workflow_dispatch` trigger alongside the existing tag-push trigger, and create/update a GitHub release for the tag with all gems plus a `CHECKSUMS.txt` manifest. Pin all GitHub Actions to commit SHAs and apply zizmor-style hardening across the new and modified workflows.
1 parent 3ed2888 commit bf9cd77

12 files changed

Lines changed: 830 additions & 17 deletions

.github/workflows/build-gems.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: Build gems
2+
on:
3+
workflow_call:
4+
inputs:
5+
release:
6+
description: "Pass --release to bin/test-gem-build (skip timestamp version)"
7+
type: boolean
8+
default: false
9+
artifact_prefix:
10+
description: "Prefix for artifact names (e.g., 'release-v1.10.0-')"
11+
type: string
12+
default: ""
13+
ref:
14+
description: "Git ref to check out (e.g., a version tag). Defaults to the caller's ref."
15+
type: string
16+
default: ""
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
native_setup:
23+
name: "Setup"
24+
runs-on: ubuntu-latest
25+
outputs:
26+
rcd_image_version: ${{ steps.rcd_image_version.outputs.rcd_image_version }}
27+
steps:
28+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
with:
30+
ref: ${{ inputs.ref || github.ref }}
31+
persist-credentials: false
32+
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
33+
with:
34+
ruby-version: "3.4"
35+
bundler-cache: true
36+
- id: rcd_image_version
37+
run: bundle exec ruby -e 'require "rake_compiler_dock"; puts "rcd_image_version=#{RakeCompilerDock::IMAGE_VERSION}"' >> "$GITHUB_OUTPUT"
38+
39+
build_source_gem:
40+
name: "build source"
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
44+
with:
45+
ref: ${{ inputs.ref || github.ref }}
46+
persist-credentials: false
47+
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
48+
with:
49+
ruby-version: "3.4"
50+
bundler-cache: true
51+
- run: |
52+
# shellcheck disable=SC2086 # RELEASE_FLAG intentionally word-splits (empty or "--release")
53+
./bin/test-gem-build ${RELEASE_FLAG} gems ruby
54+
env:
55+
RELEASE_FLAG: ${{ inputs.release && '--release' || '' }}
56+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
57+
with:
58+
name: "${{ inputs.artifact_prefix }}source-gem"
59+
path: gems
60+
retention-days: 1
61+
62+
build_native_gem:
63+
needs: native_setup
64+
name: "build native ${{ matrix.platform }}"
65+
strategy:
66+
# CI runs want to surface every platform's failure (fail-fast: false);
67+
# release runs should stop on first failure to avoid pushing a partial
68+
# set of gems.
69+
fail-fast: ${{ inputs.release }}
70+
matrix:
71+
platform:
72+
- aarch64-linux-gnu
73+
- aarch64-linux-musl
74+
- aarch64-mingw-ucrt
75+
- arm-linux-gnu
76+
- arm-linux-musl
77+
- arm64-darwin
78+
- x64-mingw-ucrt
79+
- x86_64-darwin
80+
- x86_64-linux-gnu
81+
- x86_64-linux-musl
82+
runs-on: ubuntu-latest
83+
steps:
84+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
85+
with:
86+
ref: ${{ inputs.ref || github.ref }}
87+
persist-credentials: false
88+
- run: |
89+
# shellcheck disable=SC2086 # RELEASE_FLAG intentionally word-splits (empty or "--release")
90+
docker run --rm -v "$PWD:/work" -w /work \
91+
"ghcr.io/rake-compiler/rake-compiler-dock-image:${RCD_IMAGE_VERSION}-mri-${PLATFORM}" \
92+
./bin/test-gem-build ${RELEASE_FLAG} gems "${PLATFORM}"
93+
env:
94+
RCD_IMAGE_VERSION: ${{ needs.native_setup.outputs.rcd_image_version }}
95+
RELEASE_FLAG: ${{ inputs.release && '--release' || '' }}
96+
PLATFORM: ${{ matrix.platform }}
97+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
98+
with:
99+
name: "${{ inputs.artifact_prefix }}cruby-${{ matrix.platform }}-gem"
100+
path: gems
101+
retention-days: 1
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: Native gem precompilation
2+
concurrency:
3+
group: "${{ github.workflow }}-${{ github.ref }}"
4+
cancel-in-progress: true
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
tags:
11+
- v*
12+
pull_request:
13+
paths:
14+
- Rakefile
15+
- Gemfile
16+
- Gemfile.lock
17+
- prism.gemspec
18+
- ext/prism/**
19+
- lib/prism.rb
20+
- bin/test-gem-*
21+
- bin/build-gems
22+
- .github/workflows/build-gems.yml
23+
- .github/workflows/native-gem-precompilation.yml
24+
workflow_dispatch:
25+
26+
permissions:
27+
contents: read
28+
29+
jobs:
30+
build:
31+
uses: ./.github/workflows/build-gems.yml
32+
33+
install_source_gem:
34+
needs: build
35+
name: "test source ${{ matrix.os }} ${{ matrix.ruby }}"
36+
strategy:
37+
fail-fast: false
38+
matrix:
39+
os: [ubuntu, macos, windows]
40+
ruby: ["3.3", "3.4", "4.0"]
41+
runs-on: ${{ matrix.os }}-latest
42+
steps:
43+
- if: matrix.os == 'windows'
44+
name: configure git crlf
45+
run: |
46+
git config --system core.autocrlf false
47+
git config --system core.eol lf
48+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
49+
with:
50+
persist-credentials: false
51+
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
52+
with:
53+
ruby-version: ${{ matrix.ruby }}
54+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
55+
with:
56+
name: source-gem
57+
path: gems
58+
- run: ./bin/test-gem-install gems
59+
shell: sh
60+
61+
test_linux_native:
62+
name: "${{ matrix.platform }} ${{ matrix.ruby }}"
63+
needs: build
64+
strategy:
65+
fail-fast: false
66+
matrix:
67+
platform:
68+
- aarch64-linux-gnu
69+
- aarch64-linux-musl
70+
- arm-linux-gnu
71+
- arm-linux-musl
72+
- x86_64-linux-gnu
73+
- x86_64-linux-musl
74+
ruby: ["3.3", "3.4", "4.0"]
75+
include:
76+
# musl platforms need alpine image and build-base
77+
- { platform: aarch64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base linux-headers yaml-dev &&" }
78+
- { platform: arm-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base linux-headers yaml-dev &&" }
79+
- { platform: x86_64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base linux-headers yaml-dev &&" }
80+
# docker platform for each platform
81+
- { platform: aarch64-linux-gnu, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm64" }
82+
- { platform: aarch64-linux-musl, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm64" }
83+
- { platform: arm-linux-gnu, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm/v7" }
84+
- { platform: arm-linux-musl, runner: ubuntu-24.04-arm, docker_platform: "--platform=linux/arm/v7" }
85+
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
86+
steps:
87+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
88+
with:
89+
persist-credentials: false
90+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
91+
with:
92+
name: cruby-${{ matrix.platform }}-gem
93+
path: gems
94+
- env:
95+
DOCKER_PLATFORM: ${{ matrix.docker_platform }}
96+
RUBY_VERSION: ${{ matrix.ruby }}
97+
DOCKER_TAG: ${{ matrix.docker_tag }}
98+
BOOTSTRAP: ${{ matrix.bootstrap }}
99+
run: |
100+
# shellcheck disable=SC2086 # DOCKER_PLATFORM intentionally word-splits (empty or "--platform=...")
101+
docker run --rm -v "$PWD:/work" -w /work \
102+
${DOCKER_PLATFORM} "ruby:${RUBY_VERSION}${DOCKER_TAG}" \
103+
sh -c "
104+
${BOOTSTRAP}
105+
./bin/test-gem-install ./gems
106+
"
107+
108+
test_darwin_native:
109+
name: "${{ matrix.platform }} ${{ matrix.ruby }}"
110+
needs: build
111+
strategy:
112+
fail-fast: false
113+
matrix:
114+
os: [macos-15, macos-15-intel]
115+
ruby: ["3.3", "3.4", "4.0"]
116+
include:
117+
- os: macos-15
118+
platform: arm64-darwin
119+
- os: macos-15-intel
120+
platform: x86_64-darwin
121+
runs-on: ${{ matrix.os }}
122+
steps:
123+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
124+
with:
125+
persist-credentials: false
126+
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
127+
with:
128+
ruby-version: "${{ matrix.ruby }}"
129+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
130+
with:
131+
name: cruby-${{ matrix.platform }}-gem
132+
path: gems
133+
- run: ./bin/test-gem-install gems
134+
135+
test_windows_native:
136+
name: "${{ matrix.platform }} ${{ matrix.ruby }}"
137+
needs: build
138+
strategy:
139+
fail-fast: false
140+
matrix:
141+
platform: [x64-mingw-ucrt, aarch64-mingw-ucrt]
142+
ruby: ["3.3", "3.4", "4.0"]
143+
include:
144+
- { platform: x64-mingw-ucrt, os: windows-latest }
145+
- { platform: aarch64-mingw-ucrt, os: windows-11-arm }
146+
exclude:
147+
- { platform: aarch64-mingw-ucrt, ruby: "3.3" }
148+
runs-on: ${{ matrix.os }}
149+
steps:
150+
- name: configure git crlf
151+
run: |
152+
git config --system core.autocrlf false
153+
git config --system core.eol lf
154+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
155+
with:
156+
persist-credentials: false
157+
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
158+
with:
159+
ruby-version: "${{ matrix.ruby }}"
160+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
161+
with:
162+
name: cruby-${{ matrix.platform }}-gem
163+
path: gems
164+
- run: ./bin/test-gem-install gems
165+
shell: sh

.github/workflows/publish-gem.yml

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,87 @@
11
name: Publish gem to rubygems.org
2+
concurrency:
3+
group: "release-${{ inputs.version_tag || github.ref }}"
4+
cancel-in-progress: false
25

36
on:
47
push:
58
tags:
69
- 'v*'
10+
workflow_dispatch:
11+
inputs:
12+
version_tag:
13+
description: "Version tag to release (e.g., v1.10.0)"
14+
required: true
15+
type: string
716

817
permissions:
918
contents: read
1019

1120
jobs:
21+
build:
22+
uses: ./.github/workflows/build-gems.yml
23+
with:
24+
release: true
25+
artifact_prefix: "release-${{ inputs.version_tag || github.ref_name }}-"
26+
ref: ${{ inputs.version_tag || github.ref }}
27+
1228
push:
29+
name: "Push gems and create GitHub release"
30+
needs: build
1331
if: github.repository == 'ruby/prism'
1432
runs-on: ubuntu-latest
15-
1633
environment:
1734
name: rubygems.org
1835
url: https://rubygems.org/gems/prism
19-
2036
permissions:
21-
contents: write
22-
id-token: write
23-
37+
contents: write # create/update GitHub releases and upload .gem assets
38+
id-token: write # OIDC token for rubygems.org trusted publishing
2439
steps:
2540
- name: Harden Runner
26-
uses: step-security/harden-runner@v2
41+
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
2742
with:
2843
egress-policy: audit
2944

30-
- uses: actions/checkout@v6
45+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
46+
with:
47+
ref: ${{ inputs.version_tag || github.ref }}
48+
persist-credentials: false
3149

3250
- name: Set up Ruby
33-
uses: ruby/setup-ruby@v1
51+
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0 # zizmor: ignore[cache-poisoning] -- cache is for gem-push tooling only; release artifacts come from prior job's uploaded artifacts, not this Ruby env
3452
with:
3553
ruby-version: "3.4"
3654
bundler-cache: true
3755

38-
- name: Publish to RubyGems
39-
uses: rubygems/release-gem@v1
56+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
57+
with:
58+
path: gems
59+
pattern: "release-${{ inputs.version_tag || github.ref_name }}-*"
60+
merge-multiple: true
61+
62+
- name: Print and record checksums
63+
run: |
64+
cd gems
65+
sha256sum ./*.gem | tee CHECKSUMS.txt
66+
67+
- uses: rubygems/configure-rubygems-credentials@762a4b77c3300434bb57c7ce80b20e36231927aa # v2.0.0
68+
69+
# TODO: once RubyGems >= 4.1 is in setup-ruby, `gem push` will auto-attest (ruby/rubygems#9325).
70+
- name: Push gems to RubyGems.org
71+
run: |
72+
for gem in gems/*.gem; do
73+
echo "Pushing ${gem} ..."
74+
gem push "${gem}" || echo "WARNING: Failed to push ${gem} (may already exist)"
75+
done
76+
77+
- name: Create or update GitHub release
78+
env:
79+
GH_TOKEN: ${{ github.token }}
80+
TAG: ${{ inputs.version_tag || github.ref_name }}
81+
REPO: ${{ github.repository }}
82+
run: |
83+
if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1 ; then
84+
gh release upload "$TAG" --repo "$REPO" --clobber gems/*.gem gems/CHECKSUMS.txt
85+
else
86+
gh release create "$TAG" --repo "$REPO" --title "$TAG" --generate-notes gems/*.gem gems/CHECKSUMS.txt
87+
fi

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ gem "benchmark-ips"
88
gem "parser"
99
gem "rake"
1010
gem "rake-compiler"
11+
gem "rake-compiler-dock", "~> 1.12.0"
1112
gem "ruby_parser"
1213
gem "test-unit"
1314

0 commit comments

Comments
 (0)