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.