diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 00000000..226834e8
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,11 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(bazel build *)",
+ "Bash(bazel clean *)",
+ "Bash(bazel query *)",
+ "Bash(bazel run *)",
+ "Bash(bazel test *)"
+ ]
+ }
+}
diff --git a/README.md b/README.md
index 8d6fb851..cddd4e8f 100755
--- a/README.md
+++ b/README.md
@@ -180,16 +180,17 @@ ruby.toolchain(
**Notes:**
-- Portable Ruby is only supported on Linux (arm64, x86_64) and macOS (arm64).
+- Portable Ruby is only supported on Linux (arm64, x86_64) and macOS (arm64, x86_64).
- Setting `portable_ruby = True` has no effect on JRuby, TruffleRuby, or Windows.
- On Windows, the toolchain automatically falls back to RubyInstaller.
- Find available portable Ruby releases at https://github.com/bazel-contrib/portable-ruby/releases
+- Portable Ruby toolchains are multi-platform and can be used on for [Remote Build Execution][15].
### JRuby
On all operating systems, JRuby is downloaded manually.
It uses Bazel runtime Java toolchain as JDK.
-JRuby is currently the only toolchain that supports [Remote Build Execution][15].
+JRuby toolchain is cross-platform and fully supports [Remote Build Execution][15].
### TruffleRuby
diff --git a/docs/repository_rules.md b/docs/repository_rules.md
index c6130451..0626905e 100644
--- a/docs/repository_rules.md
+++ b/docs/repository_rules.md
@@ -31,7 +31,8 @@ Wraps `rb_bundle_rule()` providing default toolchain name.
load("@rules_ruby//ruby:deps.bzl", "rb_register_toolchains")
rb_register_toolchains(name, version, version_file, msys2_packages, portable_ruby,
- portable_ruby_release_suffix, portable_ruby_checksums, register, **kwargs)
+ portable_ruby_release_suffix, portable_ruby_checksums, resolved_version,
+ register, **kwargs)
Register a Ruby toolchain and lazily download the Ruby Interpreter.
@@ -43,6 +44,21 @@ Register a Ruby toolchain and lazily download the Ruby Interpreter.
* _(With portable_ruby)_ Portable Ruby downloaded from [bazel-contrib/portable-ruby](https://github.com/bazel-contrib/portable-ruby).
* _(For "system")_ Ruby found on the PATH is used. Please note that builds are not hermetic in this case.
+When `portable_ruby = True`, this function registers a Bazel toolchain per
+supported execution platform so that builds resolve to the right
+interpreter on remote execution and cross-platform setups. Per-platform
+repositories `@_` are created lazily — Bazel only fetches
+the one matching the resolved exec platform. A hub repository `@`
+aliases the canonical targets (`:bundle`, `:gem`, `:ruby`, `:headers`,
+`:jars`, etc.) via `select()`, preserving direct references like
+`@ruby//:bundle`.
+
+JRuby's archive is platform-independent (JVM-based), so it is registered
+as a single unconstrained toolchain — no per-platform repos needed.
+
+Other modes (ruby-build for MRI source compile, TruffleRuby, RubyInstaller,
+`system`) remain single-platform host-only.
+
`WORKSPACE`:
```bazel
load("@rules_ruby//ruby:deps.bzl", "rb_register_toolchains")
@@ -89,6 +105,7 @@ rb_library(
| portable_ruby | when True, downloads portable Ruby from bazel-contrib/portable-ruby instead of compiling via ruby-build. Has no effect on JRuby, TruffleRuby, or Windows. | `False` |
| portable_ruby_release_suffix | release suffix for portable Ruby (default "1", e.g. "2" downloads X.Y.Z-2). | `""` |
| portable_ruby_checksums | platform checksums for portable Ruby downloads, overriding built-in checksums. | `{}` |
+| resolved_version | the version string resolved from `version_file` by the module extension. Used to detect JRuby (which skips the multi-platform `portable_ruby` path since its archive is platform-independent). | `None` |
| register | whether to register the resulting toolchains, should be False under bzlmod | `True` |
| kwargs | additional parameters to the downloader for this interpreter type | none |
diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl
index 7755c4f4..cf7bf4f6 100644
--- a/ruby/extensions.bzl
+++ b/ruby/extensions.bzl
@@ -5,6 +5,27 @@ load("//ruby/private:download.bzl", "RUBY_BUILD_VERSION")
load("//ruby/private:toolchain.bzl", "DEFAULT_RUBY_REPOSITORY")
load(":deps.bzl", "rb_bundle", "rb_bundle_fetch", "rb_register_toolchains")
+def _resolve_version(module_ctx, toolchain):
+ """Resolve the Ruby version string from `version` or `version_file`.
+
+ `rb_register_toolchains` needs to know whether the requested engine is
+ JRuby to decide between the multi-platform `portable_ruby` path and the
+ single-platform path. When the user supplies `version_file` instead of an
+ explicit `version`, the extension reads the file here so the decision can
+ be made at extension-evaluation time, before the repo rules fire.
+ """
+ if toolchain.version:
+ return toolchain.version
+ if not toolchain.version_file:
+ return None
+ content = module_ctx.read(toolchain.version_file).strip("\r\n")
+ if toolchain.version_file.name == ".tool-versions":
+ for line in content.splitlines():
+ if line.startswith("ruby"):
+ return line.partition(" ")[-1]
+ return None
+ return content
+
ruby_bundle = tag_class(attrs = {
"name": attr.string(doc = "Resulting repository name for the bundle"),
"srcs": attr.label_list(),
@@ -115,6 +136,7 @@ def _ruby_module_extension(module_ctx):
toolchain.portable_ruby,
toolchain.portable_ruby_release_suffix,
toolchain.portable_ruby_checksums,
+ _resolve_version(module_ctx, toolchain),
)
if module_ctx.is_dev_dependency(toolchain):
direct_dev_dep_names.append(toolchain.name)
@@ -132,6 +154,7 @@ def _ruby_module_extension(module_ctx):
portable_ruby,
portable_ruby_release_suffix,
portable_ruby_checksums,
+ resolved_version,
) = config
rb_register_toolchains(
name = name,
@@ -142,6 +165,7 @@ def _ruby_module_extension(module_ctx):
portable_ruby = portable_ruby,
portable_ruby_release_suffix = portable_ruby_release_suffix,
portable_ruby_checksums = portable_ruby_checksums,
+ resolved_version = resolved_version,
register = False,
)
diff --git a/ruby/private/BUILD b/ruby/private/BUILD
index fdda52c2..b0cd9446 100644
--- a/ruby/private/BUILD
+++ b/ruby/private/BUILD
@@ -80,6 +80,8 @@ bzl_library(
visibility = ["//ruby:__subpackages__"],
deps = [
":download",
+ "//ruby/private/toolchain:hub",
+ "//ruby/private/toolchain:platforms",
"//ruby/private/toolchain:repository_proxy",
],
)
@@ -132,7 +134,10 @@ bzl_library(
name = "download",
srcs = ["download.bzl"],
visibility = ["//ruby:__subpackages__"],
- deps = [":portable_ruby_checksums"],
+ deps = [
+ ":portable_ruby_checksums",
+ "//ruby/private/toolchain:platforms",
+ ],
)
bzl_library(
diff --git a/ruby/private/download.bzl b/ruby/private/download.bzl
index 44fec788..cf4433d4 100644
--- a/ruby/private/download.bzl
+++ b/ruby/private/download.bzl
@@ -1,6 +1,11 @@
"Repository rule for fetching Ruby interpreters"
load("//ruby/private:portable_ruby_checksums.bzl", "PORTABLE_RUBY_CHECKSUMS", "PORTABLE_RUBY_DEFAULT_SUFFIXES")
+load(
+ "//ruby/private/toolchain:platforms.bzl",
+ "MULTI_PLATFORM_RUBY_PLATFORMS",
+ "PORTABLE_RUBY_PLATFORMS",
+)
RUBY_BUILD_VERSION = "20260512"
@@ -89,6 +94,34 @@ load Gem.bin_path("bundler", "bundle", version)
end
"""
+def _resolve_platform(repository_ctx):
+ """Return the canonical platform key (e.g. 'x86_64_linux').
+
+ Honors the explicit `platform` attr when set; otherwise infers from host.
+ """
+ if repository_ctx.attr.platform:
+ return repository_ctx.attr.platform
+
+ os_name = repository_ctx.os.name
+ if os_name.startswith("mac"):
+ os_key = "darwin"
+ elif os_name.startswith("linux"):
+ os_key = "linux"
+ elif os_name.startswith("windows"):
+ os_key = "windows"
+ else:
+ os_key = os_name
+
+ arch = repository_ctx.os.arch
+ if arch in ["amd64", "x86_64"]:
+ arch_key = "x86_64"
+ elif arch in ["arm64", "aarch64"]:
+ arch_key = "arm64"
+ else:
+ arch_key = arch
+
+ return "{}_{}".format(arch_key, os_key)
+
def _rb_download_impl(repository_ctx):
if repository_ctx.attr.version and not repository_ctx.attr.version_file:
version = repository_ctx.attr.version
@@ -104,6 +137,9 @@ def _rb_download_impl(repository_ctx):
ruby_binary_name = "ruby"
gem_binary_name = "gem"
+ platform = _resolve_platform(repository_ctx)
+ is_windows_target = platform.endswith("_windows")
+
if version.startswith("jruby"):
_install_jruby(repository_ctx, version)
@@ -134,12 +170,17 @@ def _rb_download_impl(repository_ctx):
env.update({"OPENSSL_PREFIX": openssl_prefix})
elif version == "system":
engine = _symlink_system_ruby(repository_ctx)
- elif repository_ctx.os.name.startswith("windows"):
+ elif is_windows_target:
+ # Windows MRI uses RubyInstaller — portable-ruby does not publish
+ # Windows tarballs. Applies whether the platform was inferred from the
+ # host (single-platform mode) or set explicitly via the `platform` attr
+ # (multi-platform mode picking the Windows per-platform repo).
_install_via_rubyinstaller(repository_ctx, version)
elif repository_ctx.attr.portable_ruby:
_install_portable_ruby(
repository_ctx,
version,
+ platform,
repository_ctx.attr.portable_ruby_checksums,
)
@@ -219,9 +260,11 @@ def _install_jruby(repository_ctx, version):
if sha256 != download.sha256:
print(_JRUBY_INTEGRITY_MISSING.format(sha256 = download.sha256, version = version)) # buildifier: disable=print
- if repository_ctx.os.name.startswith("windows"):
- repository_ctx.symlink("dist/bin/bundle.bat", "dist/bin/bundle.cmd")
- repository_ctx.symlink("dist/bin/jgem.bat", "dist/bin/jgem.cmd")
+ # Always create the Windows `.cmd` wrappers — JRuby's archive is the same
+ # across platforms, and the BUILD.tpl select picks `bundle.cmd` / `jgem.cmd`
+ # when consumed at a Windows target config. Symlinks are harmless on Unix.
+ repository_ctx.symlink("dist/bin/bundle.bat", "dist/bin/bundle.cmd")
+ repository_ctx.symlink("dist/bin/jgem.bat", "dist/bin/jgem.cmd")
# https://github.com/oneclick/rubyinstaller2/wiki/FAQ#q-how-do-i-perform-a-silentunattended-install-with-the-rubyinstaller
def _install_via_rubyinstaller(repository_ctx, version):
@@ -283,35 +326,20 @@ def _install_via_ruby_build(repository_ctx, version):
repository_ctx.delete("ruby-build")
-def _install_portable_ruby(repository_ctx, ruby_version, checksums):
+def _install_portable_ruby(repository_ctx, ruby_version, platform, checksums):
"""Install portable Ruby from bazel-contrib/portable-ruby project.
Args:
repository_ctx: Repository context
ruby_version: Ruby version (e.g., "3.4.8")
- checksums: Dict mapping platform keys to SHA256 checksums
+ platform: Canonical platform key (e.g., "x86_64_linux")
+ checksums: Dict mapping artifact names to SHA256 checksums
"""
-
- # Detect platform
- os_name = repository_ctx.os.name
- if os_name.startswith("mac"):
- os_key = "darwin"
- elif os_name.startswith("linux"):
- os_key = "linux"
- else:
- os_key = os_name
-
- # Detect architecture
- arch = repository_ctx.os.arch
- if arch == "amd64":
- arch_key = "x86_64"
- elif arch in ["arm64", "aarch64"]:
- arch_key = "arm64"
- else:
- arch_key = arch
-
- # Combine to form platform key
- platform_key = arch_key + "_" + os_key
+ if platform not in PORTABLE_RUBY_PLATFORMS:
+ fail("portable Ruby is not available for platform {}; supported: {}".format(
+ platform,
+ sorted(PORTABLE_RUBY_PLATFORMS),
+ ))
# Determine release suffix: explicit attr overrides built-in default
suffix = repository_ctx.attr.portable_ruby_release_suffix
@@ -320,7 +348,7 @@ def _install_portable_ruby(repository_ctx, ruby_version, checksums):
artifact_name = _PORTABLE_RUBY_NAME.format(
version = ruby_version,
- platform = platform_key,
+ platform = platform,
)
# Get checksum if provided (Bazel will warn if not provided)
@@ -331,7 +359,7 @@ def _install_portable_ruby(repository_ctx, ruby_version, checksums):
kwargs["sha256"] = PORTABLE_RUBY_CHECKSUMS[suffix][artifact_name]
repository_ctx.report_progress(
- "Downloading portable Ruby %s-%s for %s" % (ruby_version, suffix, platform_key),
+ "Downloading portable Ruby %s-%s for %s" % (ruby_version, suffix, platform),
)
repository_ctx.download_and_extract(
url = _PORTABLE_RUBY_URL.format(
@@ -422,6 +450,14 @@ Keys: artifact names (e.g., ruby-3.4.8.x86_64_linux.tar.gz, ruby-3.4.8.arm64_dar
Values: SHA256 checksums for the corresponding platform.
""",
),
+ "platform": attr.string(
+ doc = """
+Explicit canonical platform key (e.g. `x86_64_linux`). When set, overrides host
+auto-detection and is used to select the right portable Ruby download. Used by
+`rb_register_toolchains` when registering per-platform toolchains.
+""",
+ values = [""] + MULTI_PLATFORM_RUBY_PLATFORMS,
+ ),
"_build_tpl": attr.label(
allow_single_file = True,
default = "@rules_ruby//:ruby/private/download/BUILD.tpl",
diff --git a/ruby/private/toolchain.bzl b/ruby/private/toolchain.bzl
index fa271453..7a5eed17 100644
--- a/ruby/private/toolchain.bzl
+++ b/ruby/private/toolchain.bzl
@@ -1,10 +1,14 @@
"Repository rule for registering Ruby interpreters"
load("//ruby/private:download.bzl", _rb_download = "rb_download")
+load("//ruby/private/toolchain:hub.bzl", _rb_hub_repository = "rb_hub_repository")
+load("//ruby/private/toolchain:platforms.bzl", "MULTI_PLATFORM_RUBY_PLATFORMS")
load("//ruby/private/toolchain:repository_proxy.bzl", _rb_toolchain_repository_proxy = "rb_toolchain_repository_proxy")
DEFAULT_RUBY_REPOSITORY = "ruby"
+_TOOLCHAIN_TYPE = "@rules_ruby//ruby:toolchain_type"
+
def rb_register_toolchains(
name = DEFAULT_RUBY_REPOSITORY,
version = None,
@@ -13,6 +17,7 @@ def rb_register_toolchains(
portable_ruby = False,
portable_ruby_release_suffix = "",
portable_ruby_checksums = {},
+ resolved_version = None,
register = True,
**kwargs):
"""
@@ -25,6 +30,21 @@ def rb_register_toolchains(
* _(With portable_ruby)_ Portable Ruby downloaded from [bazel-contrib/portable-ruby](https://github.com/bazel-contrib/portable-ruby).
* _(For "system")_ Ruby found on the PATH is used. Please note that builds are not hermetic in this case.
+ When `portable_ruby = True`, this function registers a Bazel toolchain per
+ supported execution platform so that builds resolve to the right
+ interpreter on remote execution and cross-platform setups. Per-platform
+ repositories `@_` are created lazily — Bazel only fetches
+ the one matching the resolved exec platform. A hub repository `@`
+ aliases the canonical targets (`:bundle`, `:gem`, `:ruby`, `:headers`,
+ `:jars`, etc.) via `select()`, preserving direct references like
+ `@ruby//:bundle`.
+
+ JRuby's archive is platform-independent (JVM-based), so it is registered
+ as a single unconstrained toolchain — no per-platform repos needed.
+
+ Other modes (ruby-build for MRI source compile, TruffleRuby, RubyInstaller,
+ `system`) remain single-platform host-only.
+
`WORKSPACE`:
```bazel
load("@rules_ruby//ruby:deps.bzl", "rb_register_toolchains")
@@ -68,25 +88,80 @@ def rb_register_toolchains(
portable_ruby_release_suffix: release suffix for portable Ruby (default "1", e.g. "2" downloads X.Y.Z-2).
portable_ruby_checksums: platform checksums for portable Ruby downloads, overriding
built-in checksums.
+ resolved_version: the version string resolved from `version_file` by the module
+ extension. Used to detect JRuby (which skips the multi-platform `portable_ruby`
+ path since its archive is platform-independent).
register: whether to register the resulting toolchains, should be False under bzlmod
**kwargs: additional parameters to the downloader for this interpreter type
"""
proxy_repo_name = name + "_toolchains"
- if name not in native.existing_rules().values():
- _rb_download(
- name = name,
- version = version,
- version_file = version_file,
- msys2_packages = msys2_packages,
- portable_ruby = portable_ruby,
- portable_ruby_release_suffix = portable_ruby_release_suffix,
- portable_ruby_checksums = portable_ruby_checksums,
- **kwargs
- )
- _rb_toolchain_repository_proxy(
- name = proxy_repo_name,
- toolchain = "@{}//:toolchain".format(name),
- toolchain_type = "@rules_ruby//ruby:toolchain_type",
- )
- if register:
- native.register_toolchains("@{}//:all".format(proxy_repo_name))
+
+ # Multi-platform mode is only meaningful for MRI + portable_ruby. JRuby's
+ # archive is platform-independent, TruffleRuby and "system" can't cross-
+ # compile, and Windows MRI goes through RubyInstaller (handled per
+ # per-platform repo). When we can't determine the engine ahead of time
+ # (e.g. WORKSPACE mode with version_file but no module_ctx to read it), we
+ # fall back to single-platform host-only.
+ effective_version = resolved_version if resolved_version != None else version
+ is_jruby = effective_version != None and effective_version.startswith("jruby")
+ is_truffleruby = effective_version != None and effective_version.startswith("truffleruby")
+ is_system = effective_version == "system"
+ use_multi_platform = (
+ portable_ruby and
+ effective_version != None and
+ not is_jruby and
+ not is_truffleruby and
+ not is_system
+ )
+
+ if use_multi_platform:
+ entries = []
+ for plat in MULTI_PLATFORM_RUBY_PLATFORMS:
+ per_repo = "{}_{}".format(name, plat)
+ if per_repo not in native.existing_rules():
+ _rb_download(
+ name = per_repo,
+ version = version,
+ version_file = version_file,
+ msys2_packages = msys2_packages,
+ portable_ruby = portable_ruby,
+ portable_ruby_release_suffix = portable_ruby_release_suffix,
+ portable_ruby_checksums = portable_ruby_checksums,
+ platform = plat,
+ **kwargs
+ )
+ entries.append("{}|{}".format(per_repo, plat))
+ if name not in native.existing_rules():
+ _rb_hub_repository(
+ name = name,
+ apparent_name = name,
+ platforms = MULTI_PLATFORM_RUBY_PLATFORMS,
+ engine = "ruby",
+ )
+ if proxy_repo_name not in native.existing_rules():
+ _rb_toolchain_repository_proxy(
+ name = proxy_repo_name,
+ toolchains = entries,
+ toolchain_type = _TOOLCHAIN_TYPE,
+ )
+ else:
+ if name not in native.existing_rules():
+ _rb_download(
+ name = name,
+ version = version,
+ version_file = version_file,
+ msys2_packages = msys2_packages,
+ portable_ruby = portable_ruby,
+ portable_ruby_release_suffix = portable_ruby_release_suffix,
+ portable_ruby_checksums = portable_ruby_checksums,
+ **kwargs
+ )
+ if proxy_repo_name not in native.existing_rules():
+ _rb_toolchain_repository_proxy(
+ name = proxy_repo_name,
+ toolchains = ["{}|".format(name)],
+ toolchain_type = _TOOLCHAIN_TYPE,
+ )
+
+ if register:
+ native.register_toolchains("@{}//:all".format(proxy_repo_name))
diff --git a/ruby/private/toolchain/BUILD b/ruby/private/toolchain/BUILD
index cf57a0cc..9e1ee9d8 100644
--- a/ruby/private/toolchain/BUILD
+++ b/ruby/private/toolchain/BUILD
@@ -1,7 +1,21 @@
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+bzl_library(
+ name = "platforms",
+ srcs = ["platforms.bzl"],
+ visibility = ["//ruby:__subpackages__"],
+)
+
+bzl_library(
+ name = "hub",
+ srcs = ["hub.bzl"],
+ visibility = ["//ruby:__subpackages__"],
+ deps = [":platforms"],
+)
+
bzl_library(
name = "repository_proxy",
srcs = ["repository_proxy.bzl"],
visibility = ["//ruby:__subpackages__"],
+ deps = [":platforms"],
)
diff --git a/ruby/private/toolchain/hub.bzl b/ruby/private/toolchain/hub.bzl
new file mode 100644
index 00000000..d7c1c061
--- /dev/null
+++ b/ruby/private/toolchain/hub.bzl
@@ -0,0 +1,152 @@
+"Hub repository rule for multi-platform Ruby toolchains."
+
+load("//ruby/private:utils.bzl", "join_and_indent")
+load("//ruby/private/toolchain:platforms.bzl", "PLATFORM_CONSTRAINTS")
+
+# Canonical bin targets exposed by `download/BUILD.tpl` for an MRI Ruby install.
+_MRI_BINS = [
+ "bundle",
+ "bundler",
+ "erb",
+ "gem",
+ "irb",
+ "racc",
+ "rake",
+ "rbs",
+ "rdbg",
+ "rdoc",
+ "ri",
+ "ruby",
+]
+
+# Canonical bin targets exposed by `download/BUILD.tpl` for a JRuby install.
+_JRUBY_BINS = [
+ "jruby",
+ "jirb",
+ "jgem",
+ "jrake",
+ "bundle",
+ "bundler",
+ "gem",
+ "rake",
+ "irb",
+ "rdoc",
+ "ri",
+]
+
+# Targets that always exist in a per-platform repo regardless of engine.
+_STATIC_ALIASES = [
+ "ruby",
+ "ruby_file",
+ "toolchain",
+ "headers",
+ "jars",
+]
+
+_CONFIG_SETTING_TPL = """
+config_setting(
+ name = "is_{platform}",
+ constraint_values = {constraint_values},
+)
+"""
+
+_ALIAS_TPL = """
+alias(
+ name = "{name}",
+ actual = select({branches}),
+ visibility = ["//visibility:public"]
+)
+"""
+
+def _emit_config_settings(platforms):
+ return "".join([
+ _CONFIG_SETTING_TPL.format(
+ platform = plat,
+ constraint_values = join_and_indent(PLATFORM_CONSTRAINTS[plat]),
+ )
+ for plat in platforms
+ ])
+
+def _emit_alias(name, hub_name, platforms):
+ branches = {
+ ":is_{}".format(platform): "@{}_{}//:{}".format(hub_name, platform, name)
+ for platform in platforms
+ }
+ return _ALIAS_TPL.format(
+ name = name,
+ branches = join_and_indent(branches),
+ )
+
+def _rb_hub_repository_impl(repository_ctx):
+ # `repository_ctx.attr.name` returns the canonical name under bzlmod, which is
+ # not the apparent name the per-platform repos are siblings under. We use the
+ # explicit `apparent_name` attr so the generated BUILD references repos by their
+ # apparent names (visible to each other when created by the same extension).
+ hub_name = repository_ctx.attr.apparent_name
+ platforms = repository_ctx.attr.platforms
+ engine = repository_ctx.attr.engine
+
+ if engine == "jruby":
+ bins = _JRUBY_BINS
+ else:
+ bins = _MRI_BINS
+
+ # _STATIC_ALIASES contains "ruby", which is also in _MRI_BINS — deduplicate
+ # while preserving order so the generated BUILD is deterministic.
+ alias_names = []
+ seen = {}
+ for n in _STATIC_ALIASES + bins:
+ if n in seen:
+ continue
+ seen[n] = True
+ alias_names.append(n)
+
+ parts = [
+ 'package(default_visibility = ["//visibility:public"])\n',
+ _emit_config_settings(platforms),
+ ]
+ for n in alias_names:
+ parts.append(_emit_alias(n, hub_name, platforms))
+
+ repository_ctx.file("BUILD", "".join(parts))
+
+ repository_ctx.template(
+ "engine/BUILD",
+ repository_ctx.attr._engine_tpl,
+ executable = False,
+ substitutions = {
+ "{ruby_engine}": engine,
+ },
+ )
+
+rb_hub_repository = repository_rule(
+ implementation = _rb_hub_repository_impl,
+ attrs = {
+ "apparent_name": attr.string(
+ mandatory = True,
+ doc = """
+Apparent name of this hub (matches `name=` passed to `rb_register_toolchains`) used to construct sibling per-platform apparent labels in the generated BUILD.
+ """,
+ ),
+ "platforms": attr.string_list(
+ mandatory = True,
+ doc = "Canonical platform keys whose per-platform repos this hub aliases.",
+ ),
+ "engine": attr.string(
+ mandatory = True,
+ values = ["ruby", "jruby", "truffleruby"],
+ doc = "Ruby engine for `engine/BUILD` config_settings.",
+ ),
+ "_engine_tpl": attr.label(
+ allow_single_file = True,
+ default = "@rules_ruby//:ruby/private/download/BUILD.engine.tpl",
+ ),
+ },
+ doc = """
+A hub repository whose BUILD file contains `alias()` targets pointing to the
+matching per-platform `@_` repository via `select()` on
+@platforms constraints. This preserves backwards compatibility with direct
+references like `@ruby//:bundle` while the actual Ruby interpreter lives in a
+per-platform repo selected by Bazel's toolchain/platform resolution.
+ """,
+)
diff --git a/ruby/private/toolchain/platforms.bzl b/ruby/private/toolchain/platforms.bzl
new file mode 100644
index 00000000..da808879
--- /dev/null
+++ b/ruby/private/toolchain/platforms.bzl
@@ -0,0 +1,32 @@
+"Shared platform constants for multi-platform Ruby toolchains."
+
+# Canonical platform keys use the `{arch}_{os}` order to match the naming used
+# by portable-ruby's release artifacts (e.g. `ruby-X.Y.Z.x86_64_linux.tar.gz`),
+# avoiding any conversion when constructing the download URL.
+#
+# The Windows entries are NOT covered by portable-ruby (which only ships
+# Linux/Darwin tarballs). They exist here so multi-platform mode also
+# registers Windows-constrained toolchains; `_rb_download_impl` then routes
+# Windows downloads to RubyInstaller instead of the portable-ruby URL.
+PORTABLE_RUBY_PLATFORMS = [
+ "arm64_darwin",
+ "arm64_linux",
+ "x86_64_darwin",
+ "x86_64_linux",
+]
+
+WINDOWS_RUBY_PLATFORMS = [
+ "arm64_windows",
+ "x86_64_windows",
+]
+
+MULTI_PLATFORM_RUBY_PLATFORMS = PORTABLE_RUBY_PLATFORMS + WINDOWS_RUBY_PLATFORMS
+
+PLATFORM_CONSTRAINTS = {
+ "arm64_darwin": ["@platforms//os:macos", "@platforms//cpu:arm64"],
+ "arm64_linux": ["@platforms//os:linux", "@platforms//cpu:arm64"],
+ "arm64_windows": ["@platforms//os:windows", "@platforms//cpu:arm64"],
+ "x86_64_darwin": ["@platforms//os:macos", "@platforms//cpu:x86_64"],
+ "x86_64_linux": ["@platforms//os:linux", "@platforms//cpu:x86_64"],
+ "x86_64_windows": ["@platforms//os:windows", "@platforms//cpu:x86_64"],
+}
diff --git a/ruby/private/toolchain/repository_proxy.bzl b/ruby/private/toolchain/repository_proxy.bzl
index 35c1a20f..07bebcbf 100644
--- a/ruby/private/toolchain/repository_proxy.bzl
+++ b/ruby/private/toolchain/repository_proxy.bzl
@@ -1,29 +1,50 @@
"Repository rule for proxying registering Ruby interpreters"
+load("//ruby/private/toolchain:platforms.bzl", "PLATFORM_CONSTRAINTS")
+
+_TOOLCHAIN_TPL = """toolchain(
+ name = "toolchain_{suffix}",
+ toolchain = "{target}",
+ toolchain_type = "{toolchain_type}",
+ exec_compatible_with = {exec_compatible_with},
+ visibility = ["//visibility:public"],
+)
+"""
+
def _rb_toolchain_repository_proxy_impl(repository_ctx):
- repository_ctx.template(
- "BUILD",
- repository_ctx.attr._build_tpl,
- substitutions = {
- "{name}": repository_ctx.attr.name,
- "{toolchain}": repository_ctx.attr.toolchain,
- "{toolchain_type}": repository_ctx.attr.toolchain_type,
- },
- executable = False,
- )
+ blocks = []
+ for entry in repository_ctx.attr.toolchains:
+ repo, _, plat = entry.partition("|")
+ if plat:
+ if plat not in PLATFORM_CONSTRAINTS:
+ fail("Unknown platform key for toolchain proxy: {}".format(plat))
+ suffix = plat
+ exec_cw = repr(PLATFORM_CONSTRAINTS[plat])
+ else:
+ suffix = "default"
+ exec_cw = "[]"
+ blocks.append(_TOOLCHAIN_TPL.format(
+ suffix = suffix,
+ target = "@{}//:toolchain".format(repo),
+ toolchain_type = repository_ctx.attr.toolchain_type,
+ exec_compatible_with = exec_cw,
+ ))
+ repository_ctx.file("BUILD", "\n".join(blocks))
rb_toolchain_repository_proxy = repository_rule(
implementation = _rb_toolchain_repository_proxy_impl,
attrs = {
- "toolchain": attr.string(mandatory = True),
- "toolchain_type": attr.string(mandatory = True),
- "_build_tpl": attr.label(
- allow_single_file = True,
- default = "@rules_ruby//:ruby/private/toolchain/repository_proxy/BUILD.tpl",
+ "toolchains": attr.string_list(
+ mandatory = True,
+ doc = "List of `repo|platform` entries. An empty platform suffix " +
+ "registers an unconstrained toolchain (legacy single-platform mode).",
),
+ "toolchain_type": attr.string(mandatory = True),
},
doc = """
-A proxy repository that contains the toolchain declaration; this indirection
-allows the Ruby toolchain to be downloaded lazily.
+A proxy repository containing one or more `toolchain()` declarations that
+forward to per-platform Ruby repositories. Per-platform constraints come from
+`PLATFORM_CONSTRAINTS`. This indirection lets Bazel resolve a Ruby toolchain
+lazily (only the platform that matches gets materialized).
""",
)
diff --git a/ruby/private/toolchain/repository_proxy/BUILD.tpl b/ruby/private/toolchain/repository_proxy/BUILD.tpl
deleted file mode 100644
index 94620581..00000000
--- a/ruby/private/toolchain/repository_proxy/BUILD.tpl
+++ /dev/null
@@ -1,6 +0,0 @@
-toolchain(
- name = "{name}",
- toolchain = "{toolchain}",
- toolchain_type = "{toolchain_type}",
- visibility = ["//visibility:public"],
-)
diff --git a/ruby/private/utils.bzl b/ruby/private/utils.bzl
index bce6e9b1..bfc02aba 100644
--- a/ruby/private/utils.bzl
+++ b/ruby/private/utils.bzl
@@ -119,26 +119,34 @@ def normalize_path(ctx, path):
else:
return path.replace("\\", "/")
-def join_and_indent(names, indentation_level = 2):
- """Convers a list of strings to a pretty indented BUILD variant.
+def join_and_indent(items, indentation_level = 2):
+ """Converts a list or dict of strings into a pretty indented BUILD literal.
+
+ Lists produce `["a", "b", ...]`; dicts produce `{"k": "v", ...}` — both
+ multi-line with each entry on its own line.
Args:
- names: list of strings
+ items: list of strings, or dict of string -> string
indentation_level: how many 4 spaces to indent with
Returns:
indented string
"""
- indentation = ""
- for _ in range(0, indentation_level):
- indentation += " "
-
- string = "["
- for name in names:
- string += '\n%s"%s",' % (indentation, name)
- string += "\n%s]" % indentation[:-4]
-
- return string
+ indentation = " " * indentation_level
+ close_indentation = indentation[:-4]
+
+ if type(items) == "dict":
+ body = "".join([
+ '\n%s"%s": "%s",' % (indentation, k, v)
+ for k, v in items.items()
+ ])
+ return "{" + body + "\n" + close_indentation + "}"
+
+ body = "".join([
+ '\n%s"%s",' % (indentation, item)
+ for item in items
+ ])
+ return "[" + body + "\n" + close_indentation + "]"
def normalize_bzlmod_repository_name(name):
"""Converts Bzlmod repostory to its private name.