From 95755e8ccf5ba08d2fe251f10690cbb24b4f5b3c Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Fri, 29 May 2026 08:15:42 -0700 Subject: [PATCH 1/9] feat: multi-platform toolchains (jruby, portable-ruby) --- ruby/extensions.bzl | 24 +++ ruby/private/BUILD | 7 +- ruby/private/download.bzl | 91 ++++++++--- ruby/private/toolchain.bzl | 116 +++++++++++--- ruby/private/toolchain/BUILD | 14 ++ ruby/private/toolchain/hub.bzl | 149 ++++++++++++++++++ ruby/private/toolchain/platforms.bzl | 43 +++++ ruby/private/toolchain/repository_proxy.bzl | 55 +++++-- .../toolchain/repository_proxy/BUILD.tpl | 6 - 9 files changed, 437 insertions(+), 68 deletions(-) create mode 100644 ruby/private/toolchain/hub.bzl create mode 100644 ruby/private/toolchain/platforms.bzl delete mode 100644 ruby/private/toolchain/repository_proxy/BUILD.tpl diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index 7755c4f4..6ba0ecae 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -5,6 +5,26 @@ 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 for a toolchain tag. + + The hub repo and platform list (CRuby vs JRuby) are decided at extension + evaluation time, before any per-platform repository rule runs — so when the + user provides `version_file`, the extension must read it itself to know + which engine/platforms to register. + """ + 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(), @@ -107,6 +127,7 @@ def _ruby_module_extension(module_ctx): registrations[toolchain.name], )) else: + resolved_version = _resolve_version(module_ctx, toolchain) registrations[toolchain.name] = ( toolchain.version, toolchain.version_file, @@ -115,6 +136,7 @@ def _ruby_module_extension(module_ctx): toolchain.portable_ruby, toolchain.portable_ruby_release_suffix, toolchain.portable_ruby_checksums, + resolved_version, ) 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..744df235 100644 --- a/ruby/private/download.bzl +++ b/ruby/private/download.bzl @@ -1,6 +1,7 @@ "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", "PORTABLE_RUBY_ARTIFACT_KEY") RUBY_BUILD_VERSION = "20260512" @@ -89,6 +90,34 @@ load Gem.bin_path("bundler", "bundle", version) end """ +def _resolve_platform(repository_ctx): + """Return the canonical platform key (e.g. 'linux_x86_64'). + + 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 == "amd64" or arch == "x86_64": + arch_key = "x86_64" + elif arch in ["arm64", "aarch64"]: + arch_key = "arm64" + else: + arch_key = arch + + return "{}_{}".format(os_key, arch_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,8 +133,11 @@ def _rb_download_impl(repository_ctx): ruby_binary_name = "ruby" gem_binary_name = "gem" + platform = _resolve_platform(repository_ctx) + is_windows_target = platform.startswith("windows_") + if version.startswith("jruby"): - _install_jruby(repository_ctx, version) + _install_jruby(repository_ctx, version, platform) engine = "jruby" ruby_binary_name = "jruby" @@ -134,12 +166,16 @@ 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 and not repository_ctx.attr.platform: + # Windows host with implicit platform: use RubyInstaller. When `platform` attr + # is set explicitly we skip this branch — Windows portable Ruby is not supported, + # and an explicit non-JRuby Windows download would have no path that works here. _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, ) @@ -199,7 +235,7 @@ def _parse_version_from_tool_versions(file): return version return None -def _install_jruby(repository_ctx, version): +def _install_jruby(repository_ctx, version, platform): version = version.removeprefix("jruby-") repository_ctx.report_progress("Downloading JRuby %s" % version) @@ -219,7 +255,7 @@ 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"): + if platform.startswith("windows_"): repository_ctx.symlink("dist/bin/bundle.bat", "dist/bin/bundle.cmd") repository_ctx.symlink("dist/bin/jgem.bat", "dist/bin/jgem.cmd") @@ -283,35 +319,22 @@ 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., "linux_x86_64") + checksums: Dict mapping artifact names to SHA256 checksums """ + if platform not in PORTABLE_RUBY_ARTIFACT_KEY: + fail("portable Ruby is not available for platform {}; supported: {}".format( + platform, + sorted(PORTABLE_RUBY_ARTIFACT_KEY.keys()), + )) - # 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 + platform_key = PORTABLE_RUBY_ARTIFACT_KEY[platform] # Determine release suffix: explicit attr overrides built-in default suffix = repository_ctx.attr.portable_ruby_release_suffix @@ -422,6 +445,22 @@ 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. `linux_x86_64`). 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 = [ + "", + "linux_x86_64", + "linux_arm64", + "darwin_x86_64", + "darwin_arm64", + "windows_x86_64", + "windows_arm64", + ], + ), "_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..124932ba 100644 --- a/ruby/private/toolchain.bzl +++ b/ruby/private/toolchain.bzl @@ -1,10 +1,33 @@ "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", + "JRUBY_PLATFORMS", + "PORTABLE_RUBY_PLATFORMS", + "engine_from_version", +) 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 _is_multi_platform(version, portable_ruby): + if portable_ruby: + return True + if version and engine_from_version(version) == "jruby": + return True + return False + +def _platforms_for(version, portable_ruby): + if version and engine_from_version(version) == "jruby": + return JRUBY_PLATFORMS + if portable_ruby: + return PORTABLE_RUBY_PLATFORMS + return [] + def rb_register_toolchains( name = DEFAULT_RUBY_REPOSITORY, version = None, @@ -13,6 +36,7 @@ def rb_register_toolchains( portable_ruby = False, portable_ruby_release_suffix = "", portable_ruby_checksums = {}, + resolved_version = None, register = True, **kwargs): """ @@ -25,6 +49,18 @@ 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` (CRuby) or `version` starts with `jruby`, 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`. + + 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 +104,69 @@ 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 decide engine and platform set when + `version` itself is not provided. 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)) + + # Used for engine/platform decisions when the user passed `version_file` + # instead of an explicit `version` — the extension reads the file and + # forwards the resolved value via the (private) `resolved_version` arg. + effective_version = resolved_version if resolved_version != None else version + multi = _is_multi_platform(effective_version, portable_ruby) + + if multi: + platforms = _platforms_for(effective_version, portable_ruby) + entries = [] + for plat in 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 = platforms, + engine = engine_from_version(effective_version), + ) + 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..baa86eda --- /dev/null +++ b/ruby/private/toolchain/hub.bzl @@ -0,0 +1,149 @@ +"Hub repository rule for multi-platform Ruby toolchains." + +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", +] + +def _emit_config_settings(platforms): + blocks = [] + for plat in platforms: + cv = PLATFORM_CONSTRAINTS[plat] + blocks.append( + 'config_setting(name = "is_{p}", constraint_values = {cv})'.format( + p = plat, + cv = repr(cv), + ), + ) + return "\n".join(blocks) + +def _emit_alias(name, hub_name, platforms): + branches = [] + for plat in platforms: + branches.append( + ' ":is_{p}": "@{hub}_{p}//:{name}"'.format( + p = plat, + hub = hub_name, + name = name, + ), + ) + return ( + "alias(\n" + + ' name = "{name}",\n'.format(name = name) + + " actual = select({\n" + + ",\n".join(branches) + ",\n" + + " }),\n" + + ' visibility = ["//visibility:public"],\n' + + ")" + ) + +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"])', + _emit_config_settings(platforms), + ] + for n in alias_names: + parts.append(_emit_alias(n, hub_name, platforms)) + + repository_ctx.file("BUILD", "\n\n".join(parts) + "\n") + + 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..148bada9 --- /dev/null +++ b/ruby/private/toolchain/platforms.bzl @@ -0,0 +1,43 @@ +"Shared platform constants for multi-platform Ruby toolchains." + +PORTABLE_RUBY_PLATFORMS = [ + "linux_x86_64", + "linux_arm64", + "darwin_x86_64", + "darwin_arm64", +] + +JRUBY_PLATFORMS = PORTABLE_RUBY_PLATFORMS + ["windows_x86_64", "windows_arm64"] + +PLATFORM_CONSTRAINTS = { + "linux_x86_64": ["@platforms//os:linux", "@platforms//cpu:x86_64"], + "linux_arm64": ["@platforms//os:linux", "@platforms//cpu:arm64"], + "darwin_x86_64": ["@platforms//os:macos", "@platforms//cpu:x86_64"], + "darwin_arm64": ["@platforms//os:macos", "@platforms//cpu:arm64"], + "windows_x86_64": ["@platforms//os:windows", "@platforms//cpu:x86_64"], + "windows_arm64": ["@platforms//os:windows", "@platforms//cpu:arm64"], +} + +# Mapping from canonical platform key to portable-ruby artifact suffix. +PORTABLE_RUBY_ARTIFACT_KEY = { + "linux_x86_64": "x86_64_linux", + "linux_arm64": "arm64_linux", + "darwin_x86_64": "x86_64_darwin", + "darwin_arm64": "arm64_darwin", +} + +def engine_from_version(version): + """Infer the Ruby engine (`ruby`, `jruby`, `truffleruby`) from a version string. + + Args: + version: Version string such as `3.4.8`, `jruby-10.1.0.0`, or + `truffleruby-24.0.0`. May be None, in which case `ruby` is returned. + + Returns: + One of `ruby`, `jruby`, `truffleruby`. + """ + if version and version.startswith("jruby"): + return "jruby" + if version and version.startswith("truffleruby"): + return "truffleruby" + return "ruby" 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"], -) From d96446b2a9789c77b828cce9597b11d94caa84fe Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Mon, 1 Jun 2026 13:36:53 -0700 Subject: [PATCH 2/9] chore(ai): let claude use bazel --- .claude/settings.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .claude/settings.json 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 *)" + ] + } +} From d8f60b4ae9f6e8f5faadf37333bb1528e4fd74cc Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Tue, 2 Jun 2026 07:57:42 -0700 Subject: [PATCH 3/9] revert: unconstrain jruby toolchain --- ruby/extensions.bzl | 14 +++--- ruby/private/download.bzl | 12 +++--- ruby/private/toolchain.bzl | 64 +++++++++++----------------- ruby/private/toolchain/platforms.bzl | 20 --------- 4 files changed, 38 insertions(+), 72 deletions(-) diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index 6ba0ecae..cf7bf4f6 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -6,12 +6,13 @@ 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 for a toolchain tag. + """Resolve the Ruby version string from `version` or `version_file`. - The hub repo and platform list (CRuby vs JRuby) are decided at extension - evaluation time, before any per-platform repository rule runs — so when the - user provides `version_file`, the extension must read it itself to know - which engine/platforms to register. + `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 @@ -127,7 +128,6 @@ def _ruby_module_extension(module_ctx): registrations[toolchain.name], )) else: - resolved_version = _resolve_version(module_ctx, toolchain) registrations[toolchain.name] = ( toolchain.version, toolchain.version_file, @@ -136,7 +136,7 @@ def _ruby_module_extension(module_ctx): toolchain.portable_ruby, toolchain.portable_ruby_release_suffix, toolchain.portable_ruby_checksums, - resolved_version, + _resolve_version(module_ctx, toolchain), ) if module_ctx.is_dev_dependency(toolchain): direct_dev_dep_names.append(toolchain.name) diff --git a/ruby/private/download.bzl b/ruby/private/download.bzl index 744df235..80b39f22 100644 --- a/ruby/private/download.bzl +++ b/ruby/private/download.bzl @@ -137,7 +137,7 @@ def _rb_download_impl(repository_ctx): is_windows_target = platform.startswith("windows_") if version.startswith("jruby"): - _install_jruby(repository_ctx, version, platform) + _install_jruby(repository_ctx, version) engine = "jruby" ruby_binary_name = "jruby" @@ -235,7 +235,7 @@ def _parse_version_from_tool_versions(file): return version return None -def _install_jruby(repository_ctx, version, platform): +def _install_jruby(repository_ctx, version): version = version.removeprefix("jruby-") repository_ctx.report_progress("Downloading JRuby %s" % version) @@ -255,9 +255,11 @@ def _install_jruby(repository_ctx, version, platform): if sha256 != download.sha256: print(_JRUBY_INTEGRITY_MISSING.format(sha256 = download.sha256, version = version)) # buildifier: disable=print - if platform.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): diff --git a/ruby/private/toolchain.bzl b/ruby/private/toolchain.bzl index 124932ba..48a68c6c 100644 --- a/ruby/private/toolchain.bzl +++ b/ruby/private/toolchain.bzl @@ -2,32 +2,13 @@ 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", - "JRUBY_PLATFORMS", - "PORTABLE_RUBY_PLATFORMS", - "engine_from_version", -) +load("//ruby/private/toolchain:platforms.bzl", "PORTABLE_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 _is_multi_platform(version, portable_ruby): - if portable_ruby: - return True - if version and engine_from_version(version) == "jruby": - return True - return False - -def _platforms_for(version, portable_ruby): - if version and engine_from_version(version) == "jruby": - return JRUBY_PLATFORMS - if portable_ruby: - return PORTABLE_RUBY_PLATFORMS - return [] - def rb_register_toolchains( name = DEFAULT_RUBY_REPOSITORY, version = None, @@ -49,14 +30,17 @@ 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` (CRuby) or `version` starts with `jruby`, 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`. + 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. @@ -104,24 +88,24 @@ 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 decide engine and platform set when - `version` itself is not provided. + 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" - # Used for engine/platform decisions when the user passed `version_file` - # instead of an explicit `version` — the extension reads the file and - # forwards the resolved value via the (private) `resolved_version` arg. + # `portable_ruby` only meaningfully applies to CRuby. For JRuby the archive is + # platform-independent, so we use the single-platform path even when the user + # passes `portable_ruby = True` (matches the "has no effect on JRuby" docstring). effective_version = resolved_version if resolved_version != None else version - multi = _is_multi_platform(effective_version, portable_ruby) + is_jruby = effective_version != None and effective_version.startswith("jruby") + use_portable_ruby = portable_ruby and not is_jruby - if multi: - platforms = _platforms_for(effective_version, portable_ruby) + if use_portable_ruby: entries = [] - for plat in platforms: + for plat in PORTABLE_RUBY_PLATFORMS: per_repo = "{}_{}".format(name, plat) if per_repo not in native.existing_rules(): _rb_download( @@ -140,8 +124,8 @@ def rb_register_toolchains( _rb_hub_repository( name = name, apparent_name = name, - platforms = platforms, - engine = engine_from_version(effective_version), + platforms = PORTABLE_RUBY_PLATFORMS, + engine = "ruby", ) if proxy_repo_name not in native.existing_rules(): _rb_toolchain_repository_proxy( diff --git a/ruby/private/toolchain/platforms.bzl b/ruby/private/toolchain/platforms.bzl index 148bada9..58d51c2e 100644 --- a/ruby/private/toolchain/platforms.bzl +++ b/ruby/private/toolchain/platforms.bzl @@ -7,15 +7,11 @@ PORTABLE_RUBY_PLATFORMS = [ "darwin_arm64", ] -JRUBY_PLATFORMS = PORTABLE_RUBY_PLATFORMS + ["windows_x86_64", "windows_arm64"] - PLATFORM_CONSTRAINTS = { "linux_x86_64": ["@platforms//os:linux", "@platforms//cpu:x86_64"], "linux_arm64": ["@platforms//os:linux", "@platforms//cpu:arm64"], "darwin_x86_64": ["@platforms//os:macos", "@platforms//cpu:x86_64"], "darwin_arm64": ["@platforms//os:macos", "@platforms//cpu:arm64"], - "windows_x86_64": ["@platforms//os:windows", "@platforms//cpu:x86_64"], - "windows_arm64": ["@platforms//os:windows", "@platforms//cpu:arm64"], } # Mapping from canonical platform key to portable-ruby artifact suffix. @@ -25,19 +21,3 @@ PORTABLE_RUBY_ARTIFACT_KEY = { "darwin_x86_64": "x86_64_darwin", "darwin_arm64": "arm64_darwin", } - -def engine_from_version(version): - """Infer the Ruby engine (`ruby`, `jruby`, `truffleruby`) from a version string. - - Args: - version: Version string such as `3.4.8`, `jruby-10.1.0.0`, or - `truffleruby-24.0.0`. May be None, in which case `ruby` is returned. - - Returns: - One of `ruby`, `jruby`, `truffleruby`. - """ - if version and version.startswith("jruby"): - return "jruby" - if version and version.startswith("truffleruby"): - return "truffleruby" - return "ruby" From b9a636ef885622ee527777f33725eb363152eb6e Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Tue, 2 Jun 2026 08:31:00 -0700 Subject: [PATCH 4/9] refactor: use portable-ruby platform names --- ruby/private/download.bzl | 36 +++++++++++++--------------- ruby/private/toolchain/platforms.bzl | 27 +++++++++------------ 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/ruby/private/download.bzl b/ruby/private/download.bzl index 80b39f22..f7e03388 100644 --- a/ruby/private/download.bzl +++ b/ruby/private/download.bzl @@ -1,7 +1,7 @@ "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", "PORTABLE_RUBY_ARTIFACT_KEY") +load("//ruby/private/toolchain:platforms.bzl", "PORTABLE_RUBY_PLATFORMS") RUBY_BUILD_VERSION = "20260512" @@ -91,7 +91,7 @@ end """ def _resolve_platform(repository_ctx): - """Return the canonical platform key (e.g. 'linux_x86_64'). + """Return the canonical platform key (e.g. 'x86_64_linux'). Honors the explicit `platform` attr when set; otherwise infers from host. """ @@ -109,14 +109,14 @@ def _resolve_platform(repository_ctx): os_key = os_name arch = repository_ctx.os.arch - if arch == "amd64" or arch == "x86_64": + if arch in ["amd64", "x86_64"]: arch_key = "x86_64" elif arch in ["arm64", "aarch64"]: arch_key = "arm64" else: arch_key = arch - return "{}_{}".format(os_key, arch_key) + return "{}_{}".format(arch_key, os_key) def _rb_download_impl(repository_ctx): if repository_ctx.attr.version and not repository_ctx.attr.version_file: @@ -134,7 +134,7 @@ def _rb_download_impl(repository_ctx): gem_binary_name = "gem" platform = _resolve_platform(repository_ctx) - is_windows_target = platform.startswith("windows_") + is_windows_target = platform.endswith("_windows") if version.startswith("jruby"): _install_jruby(repository_ctx, version) @@ -327,17 +327,15 @@ def _install_portable_ruby(repository_ctx, ruby_version, platform, checksums): Args: repository_ctx: Repository context ruby_version: Ruby version (e.g., "3.4.8") - platform: Canonical platform key (e.g., "linux_x86_64") + platform: Canonical platform key (e.g., "x86_64_linux") checksums: Dict mapping artifact names to SHA256 checksums """ - if platform not in PORTABLE_RUBY_ARTIFACT_KEY: + if platform not in PORTABLE_RUBY_PLATFORMS: fail("portable Ruby is not available for platform {}; supported: {}".format( platform, - sorted(PORTABLE_RUBY_ARTIFACT_KEY.keys()), + sorted(PORTABLE_RUBY_PLATFORMS), )) - platform_key = PORTABLE_RUBY_ARTIFACT_KEY[platform] - # Determine release suffix: explicit attr overrides built-in default suffix = repository_ctx.attr.portable_ruby_release_suffix if not suffix: @@ -345,7 +343,7 @@ def _install_portable_ruby(repository_ctx, ruby_version, platform, 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) @@ -356,7 +354,7 @@ def _install_portable_ruby(repository_ctx, ruby_version, platform, 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( @@ -449,18 +447,18 @@ Values: SHA256 checksums for the corresponding platform. ), "platform": attr.string( doc = """ -Explicit canonical platform key (e.g. `linux_x86_64`). When set, overrides host +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 = [ "", - "linux_x86_64", - "linux_arm64", - "darwin_x86_64", - "darwin_arm64", - "windows_x86_64", - "windows_arm64", + "arm64_darwin", + "arm64_linux", + "arm64_windows", + "x86_64_darwin", + "x86_64_linux", + "x86_64_windows", ], ), "_build_tpl": attr.label( diff --git a/ruby/private/toolchain/platforms.bzl b/ruby/private/toolchain/platforms.bzl index 58d51c2e..2ed77df6 100644 --- a/ruby/private/toolchain/platforms.bzl +++ b/ruby/private/toolchain/platforms.bzl @@ -1,23 +1,18 @@ "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. PORTABLE_RUBY_PLATFORMS = [ - "linux_x86_64", - "linux_arm64", - "darwin_x86_64", - "darwin_arm64", + "arm64_darwin", + "arm64_linux", + "x86_64_darwin", + "x86_64_linux", ] PLATFORM_CONSTRAINTS = { - "linux_x86_64": ["@platforms//os:linux", "@platforms//cpu:x86_64"], - "linux_arm64": ["@platforms//os:linux", "@platforms//cpu:arm64"], - "darwin_x86_64": ["@platforms//os:macos", "@platforms//cpu:x86_64"], - "darwin_arm64": ["@platforms//os:macos", "@platforms//cpu:arm64"], -} - -# Mapping from canonical platform key to portable-ruby artifact suffix. -PORTABLE_RUBY_ARTIFACT_KEY = { - "linux_x86_64": "x86_64_linux", - "linux_arm64": "arm64_linux", - "darwin_x86_64": "x86_64_darwin", - "darwin_arm64": "arm64_darwin", + "arm64_darwin": ["@platforms//os:macos", "@platforms//cpu:arm64"], + "arm64_linux": ["@platforms//os:linux", "@platforms//cpu:arm64"], + "x86_64_darwin": ["@platforms//os:macos", "@platforms//cpu:x86_64"], + "x86_64_linux": ["@platforms//os:linux", "@platforms//cpu:x86_64"], } From fb300f11194a42a2f7786ee10533e5f376fcf02a Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Tue, 2 Jun 2026 08:51:39 -0700 Subject: [PATCH 5/9] fix(windows): ignore portable-ruby toolchains --- ruby/private/download.bzl | 25 +++++++++++-------------- ruby/private/toolchain.bzl | 27 +++++++++++++++++++-------- ruby/private/toolchain/platforms.bzl | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/ruby/private/download.bzl b/ruby/private/download.bzl index f7e03388..f05ccbe5 100644 --- a/ruby/private/download.bzl +++ b/ruby/private/download.bzl @@ -1,7 +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", "PORTABLE_RUBY_PLATFORMS") +load( + "//ruby/private/toolchain:platforms.bzl", + "MULTI_PLATFORM_RUBY_PLATFORMS", + "PORTABLE_RUBY_PLATFORMS", +) RUBY_BUILD_VERSION = "20260512" @@ -166,10 +170,11 @@ def _rb_download_impl(repository_ctx): env.update({"OPENSSL_PREFIX": openssl_prefix}) elif version == "system": engine = _symlink_system_ruby(repository_ctx) - elif is_windows_target and not repository_ctx.attr.platform: - # Windows host with implicit platform: use RubyInstaller. When `platform` attr - # is set explicitly we skip this branch — Windows portable Ruby is not supported, - # and an explicit non-JRuby Windows download would have no path that works here. + elif is_windows_target: + # Windows CRuby 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( @@ -451,15 +456,7 @@ 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 = [ - "", - "arm64_darwin", - "arm64_linux", - "arm64_windows", - "x86_64_darwin", - "x86_64_linux", - "x86_64_windows", - ], + values = [""] + MULTI_PLATFORM_RUBY_PLATFORMS, ), "_build_tpl": attr.label( allow_single_file = True, diff --git a/ruby/private/toolchain.bzl b/ruby/private/toolchain.bzl index 48a68c6c..bb7edaee 100644 --- a/ruby/private/toolchain.bzl +++ b/ruby/private/toolchain.bzl @@ -2,7 +2,7 @@ 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", "PORTABLE_RUBY_PLATFORMS") +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" @@ -96,16 +96,27 @@ def rb_register_toolchains( """ proxy_repo_name = name + "_toolchains" - # `portable_ruby` only meaningfully applies to CRuby. For JRuby the archive is - # platform-independent, so we use the single-platform path even when the user - # passes `portable_ruby = True` (matches the "has no effect on JRuby" docstring). + # Multi-platform mode is only meaningful for CRuby + portable_ruby. JRuby's + # archive is platform-independent, TruffleRuby and "system" can't cross- + # compile, and Windows CRuby 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") - use_portable_ruby = portable_ruby and not is_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_portable_ruby: + if use_multi_platform: entries = [] - for plat in PORTABLE_RUBY_PLATFORMS: + for plat in MULTI_PLATFORM_RUBY_PLATFORMS: per_repo = "{}_{}".format(name, plat) if per_repo not in native.existing_rules(): _rb_download( @@ -124,7 +135,7 @@ def rb_register_toolchains( _rb_hub_repository( name = name, apparent_name = name, - platforms = PORTABLE_RUBY_PLATFORMS, + platforms = MULTI_PLATFORM_RUBY_PLATFORMS, engine = "ruby", ) if proxy_repo_name not in native.existing_rules(): diff --git a/ruby/private/toolchain/platforms.bzl b/ruby/private/toolchain/platforms.bzl index 2ed77df6..da808879 100644 --- a/ruby/private/toolchain/platforms.bzl +++ b/ruby/private/toolchain/platforms.bzl @@ -3,6 +3,11 @@ # 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", @@ -10,9 +15,18 @@ PORTABLE_RUBY_PLATFORMS = [ "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"], } From 07c74c7a7179df0cbad547dfe97c0c6fbb178bfd Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Tue, 2 Jun 2026 08:52:23 -0700 Subject: [PATCH 6/9] chore: use MRI instead of CRuby --- ruby/private/download.bzl | 2 +- ruby/private/toolchain.bzl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ruby/private/download.bzl b/ruby/private/download.bzl index f05ccbe5..cf4433d4 100644 --- a/ruby/private/download.bzl +++ b/ruby/private/download.bzl @@ -171,7 +171,7 @@ def _rb_download_impl(repository_ctx): elif version == "system": engine = _symlink_system_ruby(repository_ctx) elif is_windows_target: - # Windows CRuby uses RubyInstaller — portable-ruby does not publish + # 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). diff --git a/ruby/private/toolchain.bzl b/ruby/private/toolchain.bzl index bb7edaee..7a5eed17 100644 --- a/ruby/private/toolchain.bzl +++ b/ruby/private/toolchain.bzl @@ -96,9 +96,9 @@ def rb_register_toolchains( """ proxy_repo_name = name + "_toolchains" - # Multi-platform mode is only meaningful for CRuby + portable_ruby. JRuby's + # 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 CRuby goes through RubyInstaller (handled per + # 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. From 40bb53fdc5a75a2f18fbb082c6483a23b5ae0592 Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Tue, 2 Jun 2026 09:31:14 -0700 Subject: [PATCH 7/9] docs: update for multi-platform toolchains --- docs/repository_rules.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 | From 9bfb936f692efa82185d8aa623ed092b2003203d Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Tue, 2 Jun 2026 10:22:17 -0700 Subject: [PATCH 8/9] docs: update README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From 42a372c27d2d7410c31c75588bf0c451a00b24d7 Mon Sep 17 00:00:00 2001 From: Alex Rodionov Date: Wed, 3 Jun 2026 09:54:28 -0700 Subject: [PATCH 9/9] refactor: simplify manual formatting --- ruby/private/toolchain/hub.bzl | 59 ++++++++++++++++++---------------- ruby/private/utils.bzl | 34 ++++++++++++-------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/ruby/private/toolchain/hub.bzl b/ruby/private/toolchain/hub.bzl index baa86eda..d7c1c061 100644 --- a/ruby/private/toolchain/hub.bzl +++ b/ruby/private/toolchain/hub.bzl @@ -1,5 +1,6 @@ "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. @@ -42,36 +43,38 @@ _STATIC_ALIASES = [ "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): - blocks = [] - for plat in platforms: - cv = PLATFORM_CONSTRAINTS[plat] - blocks.append( - 'config_setting(name = "is_{p}", constraint_values = {cv})'.format( - p = plat, - cv = repr(cv), - ), + return "".join([ + _CONFIG_SETTING_TPL.format( + platform = plat, + constraint_values = join_and_indent(PLATFORM_CONSTRAINTS[plat]), ) - return "\n".join(blocks) + for plat in platforms + ]) def _emit_alias(name, hub_name, platforms): - branches = [] - for plat in platforms: - branches.append( - ' ":is_{p}": "@{hub}_{p}//:{name}"'.format( - p = plat, - hub = hub_name, - name = name, - ), - ) - return ( - "alias(\n" + - ' name = "{name}",\n'.format(name = name) + - " actual = select({\n" + - ",\n".join(branches) + ",\n" + - " }),\n" + - ' visibility = ["//visibility:public"],\n' + - ")" + 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): @@ -99,13 +102,13 @@ def _rb_hub_repository_impl(repository_ctx): alias_names.append(n) parts = [ - 'package(default_visibility = ["//visibility:public"])', + '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", "\n\n".join(parts) + "\n") + repository_ctx.file("BUILD", "".join(parts)) repository_ctx.template( "engine/BUILD", 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.