Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
bazelrc: common --announce_rc --color=yes
repository-cache: true
- run: bazel run :buildifier.check
- run: bazel test ...
- run: bazel test --verbose_failures --test_output=errors --test_summary=detailed ...
- if: failure() && runner.debug == '1'
uses: mxschmitt/action-tmate@v3

Expand Down
4 changes: 3 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ module(
# the versions resolved in users repositories.
bazel_dep(name = "bazel_features", version = "1.9.0")
bazel_dep(name = "bazel_skylib", version = "1.3.0")
bazel_dep(name = "cgrindel_bazel_starlib", version = "0.27.0")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it have to be a runtime dependency?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used some shell libraries from my repo in the tools. We can copy those utilities somewhere, but then you would need to maintain them here, as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to use the dependency then.

bazel_dep(name = "platforms", version = "0.0.5")
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "rules_java", version = "7.2.0")
bazel_dep(name = "rules_shell", version = "0.4.1")
bazel_dep(name = "buildifier_prebuilt", version = "8.2.1.1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it have to be a runtime dependency?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatley, yes. The tools use buildozer to update the MODULE.bazel.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for explanation.


# Ruleset development dependencies.
bazel_dep(name = "aspect_bazel_lib", version = "2.22.0", dev_dependency = True)
bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.9.0", dev_dependency = True)
bazel_dep(name = "buildifier_prebuilt", version = "8.2.1.1", dev_dependency = True)
bazel_dep(name = "gazelle", version = "0.47.0", dev_dependency = True)
bazel_dep(name = "rules_go", version = "0.59.0", dev_dependency = True)
bazel_dep(name = "stardoc", version = "0.8.0", dev_dependency = True)
Expand Down
71 changes: 44 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,23 @@ For faster MRI installation on Linux and macOS, you can use prebuilt Ruby
binaries from [rv-ruby][19] instead of compiling from source. This significantly
reduces installation time and ensures consistent, portable Ruby environments.

**WORKSPACE:**
**Configure rv-ruby Downloads**

```bazel
load("@rules_ruby//ruby:deps.bzl", "rb_register_toolchains")
To securely download and properly cache the Ruby binaries, the `ruby.toolchain`
declaration must be updated with the `rv_version` and `rv_checksums` attributes.

rb_register_toolchains(
version = "3.4.8",
rv_version = "20251225",
rv_checksums = {
"linux-x86_64": "f36cef10365d370e0867f0c3ac36e457a26ab04f3cfbbd7edb227a18e6e9b3c3",
"linux-arm64": "0c08c35a99f10817643d548f98012268c5433ae25a737ab4d6751336108a941d",
"macos-x86_64": "e9da39082d1dd8502d322c850924d929bc45b7a1e35da593a5606c00673218d4",
"macos-arm64": "cd9d7a1428076bfcc6c2ca3c0eb69b8e671e9b48afb4c351fa4a84927841ffef",
},
)
We have provided the `generate_rv_checksums` utility to add/update these
attributes for you. The utility needs to know the `rv-ruby` version to use
(https://github.com/spinel-coop/rv-ruby/releases) and the version of Ruby to
download. By default, it will use the Ruby version specified in the
`.ruby-version` file.

```bash
bazel run @rules_ruby//tools/generate_rv_checksums -- 20251225
```

**Bzlmod:**
After running the utility, the toolchain declaration in your `MODULE.bazel`
should look something like the following:

```bazel
ruby = use_extension("@rules_ruby//ruby:extensions.bzl", "ruby")
Expand All @@ -156,26 +155,39 @@ ruby.toolchain(
version_file = "//:.ruby-version",
rv_version = "20251225",
rv_checksums = {
"linux-x86_64": "f36cef10365d370e0867f0c3ac36e457a26ab04f3cfbbd7edb227a18e6e9b3c3",
"linux-arm64": "0c08c35a99f10817643d548f98012268c5433ae25a737ab4d6751336108a941d",
"macos-x86_64": "e9da39082d1dd8502d322c850924d929bc45b7a1e35da593a5606c00673218d4",
"linux-x86_64": "f36cef10365d370e0867f0c3ac36e457a26ab04f3cfbbd7edb227a18e6e9b3c3",
"macos-arm64": "cd9d7a1428076bfcc6c2ca3c0eb69b8e671e9b48afb4c351fa4a84927841ffef",
"macos-x86_64": "e9da39082d1dd8502d322c850924d929bc45b7a1e35da593a5606c00673218d4",
},
)
```

**Important:** When using rv-ruby, you must exclude default gems with C extensions
from `rb_bundle_fetch` as these are pre-compiled in the rv-ruby binary:
**Configure Excluded Gems**

When using `rv-ruby`, you must exclude _default_ gems with C extensions from
`bundle_fetch` as these are pre-compiled in the `rv-ruby` binary. You may see
compilation errors if you do not exclude these gems.

We have provided the `generate_excluded_gems` utility to update the declaration
for you.

```bash
bazel run @rules_ruby//tools/generate_excluded_gems
```

The utility reads the Ruby version being used and checks
https://raw.githubusercontent.com/janlelis/stdgems/main/default_gems.json to
determine which gems should be excluded. The utility adds/updates the
`excluded_gems` attribute with the correct list of gems. The `bundle_fetch`
declaration will look something like the following:

```bazel
rb_bundle_fetch(
ruby.bundle_fetch(
name = "bundle",
gemfile = "//:Gemfile",
gemfile_lock = "//:Gemfile.lock",
excluded_gems = [
# Default gems with C extensions from https://stdgems.org/3.4.8
# These are pre-compiled in rv-ruby with portable dependencies.
# IMPORTANT: These gems must also be pinned in your Gemfile.
"date", "digest", "etc", "fcntl", "fiddle",
"io-console", "io-nonblock", "io-wait", "json",
"openssl", "pathname", "prism", "psych",
Expand All @@ -184,15 +196,20 @@ rb_bundle_fetch(
)
```

Find the list of default gems for your Ruby version at https://stdgems.org/\<version\>
(e.g., https://stdgems.org/3.4.8 for Ruby 3.4.8). Only exclude gems with C
extensions. Bundled gems should NOT be excluded.
> [!NOTE]
> You can find an HTML-rendered list of the default gems for a Ruby version at
> https://stdgems.org/\<version\> (e.g., https://stdgems.org/3.4.8 for Ruby
> 3.4.8).

**Notes:**

- rv-ruby is only supported on Linux and macOS (x86_64 and arm64).
- `rv-ruby` is only supported on Linux and macOS (x86_64 and arm64).
- On Windows, the toolchain automatically falls back to RubyInstaller.
- Find available rv-ruby releases at https://github.com/spinel-coop/rv-ruby/releases
- Find available `rv-ruby` releases at
https://github.com/spinel-coop/rv-ruby/releases
- The utilities support `--name` to target specific toolchains/bundles and
`--module-bazel` to specify a custom MODULE.bazel path.
- Run utilities with `--dry-run` to preview changes without modifying files.

### JRuby

Expand Down
48 changes: 48 additions & 0 deletions tools/generate_excluded_gems/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("@rules_shell//shell:sh_test.bzl", "sh_test")

sh_binary(
name = "generate_excluded_gems",
srcs = ["generate_excluded_gems.sh"],
data = [
"@buildifier_prebuilt//buildozer",
],
visibility = ["//visibility:public"],
deps = [
"@cgrindel_bazel_starlib//shlib/lib:fail",
"@rules_shell//shell/runfiles",
],
)

sh_test(
name = "generate_excluded_gems_test",
srcs = ["generate_excluded_gems_test.sh"],
data = [
"testdata/default_gems.json",
"testdata/expected_excluded_gems_output.txt",
":generate_excluded_gems",
],
deps = [
"@cgrindel_bazel_starlib//shlib/lib:assertions",
"@rules_shell//shell/runfiles",
],
)

sh_test(
name = "generate_excluded_gems_integration_test",
srcs = ["generate_excluded_gems_integration_test.sh"],
data = [
"testdata/default_gems.json",
":generate_excluded_gems",
],
deps = [
"@cgrindel_bazel_starlib//shlib/lib:assertions",
"@rules_shell//shell/runfiles",
],
)

filegroup(
name = "all_files",
srcs = glob(["**/*"]),
visibility = ["//:__subpackages__"],
)
171 changes: 171 additions & 0 deletions tools/generate_excluded_gems/generate_excluded_gems.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env bash

# Generates excluded_gems for ruby.bundle_fetch() and updates MODULE.bazel.

# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail
set +e
f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/${f}" 2>/dev/null \
|| source "$(grep -sm1 "^${f} " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null \
|| source "$0.runfiles/${f}" 2>/dev/null \
|| source "$(grep -sm1 "^${f} " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null \
|| source "$(grep -sm1 "^${f} " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null \
|| {
echo >&2 "ERROR: ${BASH_SOURCE[0]} cannot find ${f}"
exit 1
}
f=
set -e
# --- end runfiles.bash initialization v3 ---

# MARK - Dependencies

fail_sh_location=cgrindel_bazel_starlib/shlib/lib/fail.sh
fail_sh="$(rlocation "${fail_sh_location}")" \
|| (echo >&2 "Failed to locate ${fail_sh_location}" && exit 1)
# shellcheck disable=SC1090
source "${fail_sh}"

# Locate buildozer via runfiles
buildozer_location=buildifier_prebuilt/buildozer/buildozer
buildozer="$(rlocation "${buildozer_location}")" \
|| (echo >&2 "Failed to locate ${buildozer_location}" && exit 1)

# MARK - Default Values

# Default values
dry_run=false
name="bundle"
module_bazel="${BUILD_WORKSPACE_DIRECTORY:-.}/MODULE.bazel"
ruby_version=""

# MARK - Functions

# Read .ruby-version file
read_ruby_version() {
local version_file="${BUILD_WORKSPACE_DIRECTORY}/.ruby-version"
if [[ ! -f ${version_file} ]]; then
fail "Error: .ruby-version not found and --ruby-version not specified"
fi
tr -d '[:space:]' <"${version_file}"
}

# Extract minor version from a Ruby version string (e.g., 3.4.8 -> 3.4)
# Ruby uses MAJOR.MINOR.PATCH versioning, and stdgems data is organized by
# minor version since default gems are typically consistent within a minor
# release series.
get_minor_version() {
local version="$1"
echo "${version}" | cut -d. -f1,2
}

# MARK - Argument Handling

# Parse arguments
while (("$#")); do
case "${1}" in
--dry-run)
dry_run=true
shift
;;
--ruby-version)
ruby_version="${2}"
shift 2
;;
--name)
name="${2}"
shift 2
;;
--module-bazel)
module_bazel="${2}"
shift 2
;;
-*)
fail "Error: Unknown option: ${1}"
;;
*)
fail "Error: Unexpected argument: ${1}"
;;
esac
done

# Get Ruby version
if [[ -z ${ruby_version} ]]; then
ruby_version="$(read_ruby_version)"
fi

# Get minor version
minor_version=$(get_minor_version "${ruby_version}")

# MARK - Retrieve stdgems data

# Fetch stdgems data
stdgems_url="${STDGEMS_URL:-https://raw.githubusercontent.com/janlelis/stdgems/main/default_gems.json}"
response=$(curl -sL --max-time 30 "${stdgems_url}")

# Filter for native gems that exist for this Ruby version
# The jq query:
# 1. Select gems where native == true
# 2. Select gems where versions contains the minor version key
# 3. Extract the gem name
# 4. Sort
excluded_gems=$(echo "${response}" | jq -r --arg version "${minor_version}" \
'.gems[] | select(.native == true) | select(.versions | has($version)) | .gem' \
| sort)

# Check if we found any gems
if [[ -z ${excluded_gems} ]]; then
fail <<-EOT
Error: No native gems found for Ruby ${ruby_version} (${minor_version})
This Ruby version may not be supported in stdgems data
EOT
fi

# MARK - Update MODULE.bazel

# Generate output for dry-run or display
output="excluded_gems = [\n"
while IFS= read -r gem; do
output+=" \"${gem}\",\n"
done <<<"${excluded_gems}"
output+="],"

if [[ ${dry_run} == "true" ]]; then
# Dry-run: just output the excluded gems
echo -e "${output}"
exit 0
fi

# Construct list of gems for buildozer 'add' command
# Convert newline-separated list to space-separated
gem_list=""
while IFS= read -r gem; do
gem_list+=" ${gem}"
done <<<"${excluded_gems}"

# Update MODULE.bazel using buildozer
buildozer_cmd=(
"${buildozer}"
-types ruby.bundle_fetch
"remove excluded_gems"
"add excluded_gems${gem_list}"
"${module_bazel}:${name}"
)
if ! "${buildozer_cmd[@]}" 2>/dev/null; then
fail <<-EOT
Failed to update ${module_bazel}

Buildozer command failed. This could mean:
- The file doesn't exist at ${module_bazel}
- No ruby.bundle_fetch() call was found with name="${name}"
- The file has syntax errors

You can use --dry-run to see what would be updated:
$(echo -e "${output}")
EOT
fi

echo "Successfully updated excluded_gems in ${module_bazel}"
Loading
Loading