From 3de6c8182ae077067edffc4aa32d933338de3c3d Mon Sep 17 00:00:00 2001 From: dhash Date: Wed, 5 Aug 2026 14:03:01 -0700 Subject: [PATCH 1/8] feat: add extra_args passthrough to bundle install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads extra command-line arguments to `bundle install` from the `ruby.bundle_fetch` bzlmod tag / `rb_bundle_fetch` repo rule / `rb_bundle_install` rule down onto the install command line. Primary use case: cross-platform bundles. With bundler's `--target-rbconfig` (RubyGems 3.4+ / Gem::TargetRbConfig), the host ruby can install a DIFFERENT platform's precompiled gems — e.g. assembling an x86_64-linux vendor/bundle on an arm64-darwin host for a container image layer, without a linux executor: ruby.bundle_fetch( name = "bundle", gemfile = "//:Gemfile", gemfile_lock = "//:Gemfile.lock", extra_args = ["--target-rbconfig", "/path/to/linux/rbconfig.rb"], ) Covers both the bzlmod and WORKSPACE paths (shared rb_bundle_fetch repo rule) and the standalone rb_bundle_install rule. --- ruby/extensions.bzl | 2 ++ ruby/private/bundle_fetch.bzl | 6 ++++++ ruby/private/bundle_fetch/BUILD.tpl | 1 + ruby/private/bundle_install.bzl | 6 ++++++ ruby/private/bundle_install/bundle_install.cmd.tpl | 2 +- ruby/private/bundle_install/bundle_install.sh.tpl | 2 +- 6 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index cf7bf4f64..0e58d8253 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -38,6 +38,7 @@ ruby_bundle_fetch = tag_class(attrs = { "name": attr.string(doc = "Resulting repository name for the bundle"), "srcs": attr.label_list(), "env": attr.string_dict(), + "extra_args": attr.string_list(doc = "Extra arguments appended to `bundle install`."), "gemfile": attr.label(), "gemfile_lock": attr.label(), "gem_checksums": attr.string_dict(), @@ -99,6 +100,7 @@ def _ruby_module_extension(module_ctx): name = bundle_fetch.name, srcs = bundle_fetch.srcs, env = bundle_fetch.env, + extra_args = bundle_fetch.extra_args, gemfile = bundle_fetch.gemfile, gemfile_lock = bundle_fetch.gemfile_lock, gem_checksums = bundle_fetch.gem_checksums, diff --git a/ruby/private/bundle_fetch.bzl b/ruby/private/bundle_fetch.bzl index 942c80e8a..1f0a68183 100644 --- a/ruby/private/bundle_fetch.bzl +++ b/ruby/private/bundle_fetch.bzl @@ -247,6 +247,7 @@ def _rb_bundle_fetch_impl(repository_ctx): "{gem_fragments}": "".join(gem_fragments), "{gem_install_fragments}": "".join(gem_install_fragments), "{env}": repr(repository_ctx.attr.env), + "{extra_args}": repr(repository_ctx.attr.extra_args), "{ruby}": ruby_toolchain_attr, }, ) @@ -258,6 +259,7 @@ def _rb_bundle_fetch_impl(repository_ctx): "gemfile_lock": repository_ctx.attr.gemfile_lock, "srcs": repository_ctx.attr.srcs, "env": repository_ctx.attr.env, + "extra_args": repository_ctx.attr.extra_args, "bundler_remote": repository_ctx.attr.bundler_remote, "bundler_checksums": repository_ctx.attr.bundler_checksums, "gem_checksums": gem_checksums, @@ -301,6 +303,10 @@ rb_bundle_fetch = repository_rule( "env": attr.string_dict( doc = "Environment variables to use during installation.", ), + "extra_args": attr.string_list( + doc = "Extra arguments appended to the `bundle install` command line " + + "run by the generated rb_bundle_install target.", + ), "bundler_remote": attr.string( default = "https://rubygems.org/", doc = "Remote to fetch the bundler gem from.", diff --git a/ruby/private/bundle_fetch/BUILD.tpl b/ruby/private/bundle_fetch/BUILD.tpl index a0f192b0d..7d52ca5b8 100644 --- a/ruby/private/bundle_fetch/BUILD.tpl +++ b/ruby/private/bundle_fetch/BUILD.tpl @@ -8,6 +8,7 @@ rb_bundle_install( name = "{name}", srcs = {srcs}, env = {env}, + extra_args = {extra_args}, gemfile = "{gemfile_path}", gemfile_lock = "{gemfile_lock_path}", jars = glob(["{jars_path}/**/*.jar"], allow_empty = True), diff --git a/ruby/private/bundle_install.bzl b/ruby/private/bundle_install.bzl index 898410310..33e2ca586 100644 --- a/ruby/private/bundle_install.bzl +++ b/ruby/private/bundle_install.bzl @@ -112,6 +112,7 @@ def _rb_bundle_install_impl(ctx): "{env}": _convert_env_to_script(ctx, env), "{bundler_exe}": _normalize_path(ctx, bundler_exe), "{ruby_path}": _normalize_path(ctx, toolchain.ruby.path), + "{extra_args}": " ".join(ctx.attr.extra_args), }, ) @@ -185,6 +186,11 @@ rb_bundle_install = rule( "env": attr.string_dict( doc = "Environment variables to use during installation.", ), + "extra_args": attr.string_list( + doc = "Extra arguments appended to the `bundle install` command line. " + + "For example `[\"--target-rbconfig\", \"$(location //path:rbconfig.rb)\"]` " + + "to install a different platform's precompiled gems (cross-platform bundle).", + ), "ruby": attr.label( doc = "Override Ruby toolchain to use when installing the gem.", providers = [platform_common.ToolchainInfo], diff --git a/ruby/private/bundle_install/bundle_install.cmd.tpl b/ruby/private/bundle_install/bundle_install.cmd.tpl index f42c753fb..96cc07eb8 100644 --- a/ruby/private/bundle_install/bundle_install.cmd.tpl +++ b/ruby/private/bundle_install/bundle_install.cmd.tpl @@ -2,7 +2,7 @@ {env} -{ruby_path} {bundler_exe} install --standalone --local +{ruby_path} {bundler_exe} install --standalone --local {extra_args} {ruby_path} {bundler_exe} binstubs --all :: vim: ft=dosbatch diff --git a/ruby/private/bundle_install/bundle_install.sh.tpl b/ruby/private/bundle_install/bundle_install.sh.tpl index 738a03786..4b9b705a9 100644 --- a/ruby/private/bundle_install/bundle_install.sh.tpl +++ b/ruby/private/bundle_install/bundle_install.sh.tpl @@ -2,7 +2,7 @@ {env} -{ruby_path} {bundler_exe} install --standalone --local +{ruby_path} {bundler_exe} install --standalone --local {extra_args} {ruby_path} {bundler_exe} binstubs --all # vim: ft=bash From f4cce4e22dc5b76dbc2426585acfe8dee66f70f3 Mon Sep 17 00:00:00 2001 From: dhash Date: Wed, 5 Aug 2026 15:00:24 -0700 Subject: [PATCH 2/8] feat: expand $(location) in extra_args via new data attr extra_args now supports $(location)/$(rootpath)/$(execpath) make-variable expansion (built-in ctx.expand_location) against a new `data` label_list, which is also threaded as inputs to the bundle install action. This lets a file-valued flag reference a target instead of a raw path: ruby.bundle_fetch( name = "bundle_linux_amd64", gemfile = "//:Gemfile", gemfile_lock = "//:Gemfile.lock", data = ["//image:x86_64-linux-rbconfig.rb"], extra_args = ["--target-rbconfig", "$(location //image:x86_64-linux-rbconfig.rb)"], ) Threaded through the bzlmod tag, repo rule, generated BUILD, and install rule. --- ruby/extensions.bzl | 4 +++- ruby/private/bundle_fetch.bzl | 9 ++++++++- ruby/private/bundle_fetch/BUILD.tpl | 1 + ruby/private/bundle_install.bzl | 16 +++++++++++++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index 0e58d8253..bac459a4c 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -38,7 +38,8 @@ ruby_bundle_fetch = tag_class(attrs = { "name": attr.string(doc = "Resulting repository name for the bundle"), "srcs": attr.label_list(), "env": attr.string_dict(), - "extra_args": attr.string_list(doc = "Extra arguments appended to `bundle install`."), + "extra_args": attr.string_list(doc = "Extra arguments appended to `bundle install`. Supports `$(location ...)` against `data`."), + "data": attr.label_list(doc = "Files referenced from `extra_args` via `$(location ...)`."), "gemfile": attr.label(), "gemfile_lock": attr.label(), "gem_checksums": attr.string_dict(), @@ -101,6 +102,7 @@ def _ruby_module_extension(module_ctx): srcs = bundle_fetch.srcs, env = bundle_fetch.env, extra_args = bundle_fetch.extra_args, + data = [str(label) for label in bundle_fetch.data], gemfile = bundle_fetch.gemfile, gemfile_lock = bundle_fetch.gemfile_lock, gem_checksums = bundle_fetch.gem_checksums, diff --git a/ruby/private/bundle_fetch.bzl b/ruby/private/bundle_fetch.bzl index 1f0a68183..0d1d99e6e 100644 --- a/ruby/private/bundle_fetch.bzl +++ b/ruby/private/bundle_fetch.bzl @@ -248,6 +248,7 @@ def _rb_bundle_fetch_impl(repository_ctx): "{gem_install_fragments}": "".join(gem_install_fragments), "{env}": repr(repository_ctx.attr.env), "{extra_args}": repr(repository_ctx.attr.extra_args), + "{data}": _join_and_indent(repository_ctx.attr.data), "{ruby}": ruby_toolchain_attr, }, ) @@ -260,6 +261,7 @@ def _rb_bundle_fetch_impl(repository_ctx): "srcs": repository_ctx.attr.srcs, "env": repository_ctx.attr.env, "extra_args": repository_ctx.attr.extra_args, + "data": repository_ctx.attr.data, "bundler_remote": repository_ctx.attr.bundler_remote, "bundler_checksums": repository_ctx.attr.bundler_checksums, "gem_checksums": gem_checksums, @@ -305,7 +307,12 @@ rb_bundle_fetch = repository_rule( ), "extra_args": attr.string_list( doc = "Extra arguments appended to the `bundle install` command line " + - "run by the generated rb_bundle_install target.", + "run by the generated rb_bundle_install target. Supports " + + "`$(location ...)` expansion against `data`.", + ), + "data": attr.string_list( + doc = "Labels (as canonical strings) referenced from `extra_args` via " + + "`$(location ...)`; forwarded to the generated rb_bundle_install `data`.", ), "bundler_remote": attr.string( default = "https://rubygems.org/", diff --git a/ruby/private/bundle_fetch/BUILD.tpl b/ruby/private/bundle_fetch/BUILD.tpl index 7d52ca5b8..f2ff595a7 100644 --- a/ruby/private/bundle_fetch/BUILD.tpl +++ b/ruby/private/bundle_fetch/BUILD.tpl @@ -7,6 +7,7 @@ package(default_visibility = ["//visibility:public"]) rb_bundle_install( name = "{name}", srcs = {srcs}, + data = {data}, env = {env}, extra_args = {extra_args}, gemfile = "{gemfile_path}", diff --git a/ruby/private/bundle_install.bzl b/ruby/private/bundle_install.bzl index 33e2ca586..8f514cd18 100644 --- a/ruby/private/bundle_install.bzl +++ b/ruby/private/bundle_install.bzl @@ -112,13 +112,16 @@ def _rb_bundle_install_impl(ctx): "{env}": _convert_env_to_script(ctx, env), "{bundler_exe}": _normalize_path(ctx, bundler_exe), "{ruby_path}": _normalize_path(ctx, toolchain.ruby.path), - "{extra_args}": " ".join(ctx.attr.extra_args), + "{extra_args}": " ".join([ + ctx.expand_location(arg, ctx.attr.data) + for arg in ctx.attr.extra_args + ]), }, ) ctx.actions.run( executable = script, - inputs = depset([ctx.file.gemfile, ctx.file.gemfile_lock] + ctx.files.srcs + ctx.files.gems + jar_files), + inputs = depset([ctx.file.gemfile, ctx.file.gemfile_lock] + ctx.files.srcs + ctx.files.data + ctx.files.gems + jar_files), outputs = [binstubs, bundle_path], mnemonic = "BundleInstall", progress_message = "Running bundle install (%{label})", @@ -188,9 +191,16 @@ rb_bundle_install = rule( ), "extra_args": attr.string_list( doc = "Extra arguments appended to the `bundle install` command line. " + - "For example `[\"--target-rbconfig\", \"$(location //path:rbconfig.rb)\"]` " + + "Supports `$(location ...)`/`$(rootpath ...)`/`$(execpath ...)` make-variable " + + "expansion against `data`. For example " + + "`[\"--target-rbconfig\", \"$(location //path:rbconfig.rb)\"]` " + "to install a different platform's precompiled gems (cross-platform bundle).", ), + "data": attr.label_list( + allow_files = True, + doc = "Files referenced from `extra_args` via `$(location ...)` expansion. " + + "They are also added as inputs to the `bundle install` action.", + ), "ruby": attr.label( doc = "Override Ruby toolchain to use when installing the gem.", providers = [platform_common.ToolchainInfo], From 0d5701a7f5809de6e85e8ca2117e650b7306ddd4 Mon Sep 17 00:00:00 2001 From: dhash Date: Wed, 5 Aug 2026 15:32:24 -0700 Subject: [PATCH 3/8] feat: expose dist_files filegroup for the ruby install tree Adds a public dist_files filegroup (glob dist/**/*) to each per-platform ruby repo, aliased on the @ruby hub. Lets you package the interpreter into a container image (portable-ruby is relocatable), e.g.: pkg_tar(name = "ruby_runtime", srcs = ["@ruby//:dist_files"], package_dir = "/usr/local", strip_prefix = "dist") --- ruby/private/download/BUILD.tpl | 11 +++++++++++ ruby/private/toolchain/hub.bzl | 1 + 2 files changed, 12 insertions(+) diff --git a/ruby/private/download/BUILD.tpl b/ruby/private/download/BUILD.tpl index c08bcdb2e..acdffbba3 100644 --- a/ruby/private/download/BUILD.tpl +++ b/ruby/private/download/BUILD.tpl @@ -13,6 +13,17 @@ filegroup( }), ) +# The complete Ruby install tree (bin/, lib/, include/, ...). Useful for +# packaging the interpreter into a container image (portable-ruby is relocatable +# via relative rpaths, so this tars cleanly to e.g. /usr/local). +filegroup( + name = "dist_files", + srcs = glob( + ["dist/**/*"], + allow_empty = True, + ), +) + rb_binary( name = "ruby", main = ":ruby_file", diff --git a/ruby/private/toolchain/hub.bzl b/ruby/private/toolchain/hub.bzl index d7c1c0613..1fe37a855 100644 --- a/ruby/private/toolchain/hub.bzl +++ b/ruby/private/toolchain/hub.bzl @@ -41,6 +41,7 @@ _STATIC_ALIASES = [ "toolchain", "headers", "jars", + "dist_files", ] _CONFIG_SETTING_TPL = """ From adce1b28e993f4923e492bb1f3c50c3ed22c31ad Mon Sep 17 00:00:00 2001 From: dhash Date: Wed, 5 Aug 2026 17:55:35 -0700 Subject: [PATCH 4/8] feat(bundle_install): cross-platform / container bundle support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the pieces needed to install a bundle for a FOREIGN platform (e.g. build a linux gems layer from a macOS host via --target-rbconfig) and package it into a container image: * env values now support $(location ...)/$(execpath ...) expansion against `data` (mirrors extra_args) — lets an env var reference a build artifact, e.g. prepend a generated cross-compiler wrapper dir onto PATH. * new `rbconfig` filegroup on the ruby dist (+ hub alias): the interpreter's own relocatable rbconfig.rb, for `gem install --target-rbconfig` cross-builds. * `binstubs` attr (default True): skip `bundle binstubs --all` for cross-platform bundles, where the host ruby can't validate the target's native extensions; the (empty) binstubs dir is still materialized. * `gems` output group: just the vendor/bundle tree (no Gemfile/binstubs), so consumers can lay the gems into a container BUNDLE_PATH with a clean strip_prefix. --- ruby/extensions.bzl | 2 + ruby/private/bundle_fetch.bzl | 11 ++++- ruby/private/bundle_fetch/BUILD.tpl | 1 + ruby/private/bundle_install.bzl | 48 +++++++++++++++++-- .../bundle_install/bundle_install.cmd.tpl | 2 +- .../bundle_install/bundle_install.sh.tpl | 2 +- ruby/private/download/BUILD.tpl | 13 +++++ ruby/private/toolchain/hub.bzl | 1 + 8 files changed, 72 insertions(+), 8 deletions(-) diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index bac459a4c..8507e76bb 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -40,6 +40,7 @@ ruby_bundle_fetch = tag_class(attrs = { "env": attr.string_dict(), "extra_args": attr.string_list(doc = "Extra arguments appended to `bundle install`. Supports `$(location ...)` against `data`."), "data": attr.label_list(doc = "Files referenced from `extra_args` via `$(location ...)`."), + "binstubs": attr.bool(default = True, doc = "Run `bundle binstubs --all` after install. Set False for cross-platform bundles."), "gemfile": attr.label(), "gemfile_lock": attr.label(), "gem_checksums": attr.string_dict(), @@ -103,6 +104,7 @@ def _ruby_module_extension(module_ctx): env = bundle_fetch.env, extra_args = bundle_fetch.extra_args, data = [str(label) for label in bundle_fetch.data], + binstubs = bundle_fetch.binstubs, gemfile = bundle_fetch.gemfile, gemfile_lock = bundle_fetch.gemfile_lock, gem_checksums = bundle_fetch.gem_checksums, diff --git a/ruby/private/bundle_fetch.bzl b/ruby/private/bundle_fetch.bzl index 0d1d99e6e..ccbf1192e 100644 --- a/ruby/private/bundle_fetch.bzl +++ b/ruby/private/bundle_fetch.bzl @@ -249,6 +249,7 @@ def _rb_bundle_fetch_impl(repository_ctx): "{env}": repr(repository_ctx.attr.env), "{extra_args}": repr(repository_ctx.attr.extra_args), "{data}": _join_and_indent(repository_ctx.attr.data), + "{binstubs}": repr(repository_ctx.attr.binstubs), "{ruby}": ruby_toolchain_attr, }, ) @@ -262,6 +263,7 @@ def _rb_bundle_fetch_impl(repository_ctx): "env": repository_ctx.attr.env, "extra_args": repository_ctx.attr.extra_args, "data": repository_ctx.attr.data, + "binstubs": repository_ctx.attr.binstubs, "bundler_remote": repository_ctx.attr.bundler_remote, "bundler_checksums": repository_ctx.attr.bundler_checksums, "gem_checksums": gem_checksums, @@ -303,13 +305,20 @@ rb_bundle_fetch = repository_rule( doc = "List of Ruby source files necessary during installation.", ), "env": attr.string_dict( - doc = "Environment variables to use during installation.", + doc = "Environment variables to use during installation. Values support " + + "`$(location ...)` expansion against `data` (forwarded to the " + + "generated rb_bundle_install `env`).", ), "extra_args": attr.string_list( doc = "Extra arguments appended to the `bundle install` command line " + "run by the generated rb_bundle_install target. Supports " + "`$(location ...)` expansion against `data`.", ), + "binstubs": attr.bool( + default = True, + doc = "Forwarded to the generated rb_bundle_install `binstubs` (set False " + + "to skip `bundle binstubs --all` for cross-platform bundles).", + ), "data": attr.string_list( doc = "Labels (as canonical strings) referenced from `extra_args` via " + "`$(location ...)`; forwarded to the generated rb_bundle_install `data`.", diff --git a/ruby/private/bundle_fetch/BUILD.tpl b/ruby/private/bundle_fetch/BUILD.tpl index f2ff595a7..9537cf292 100644 --- a/ruby/private/bundle_fetch/BUILD.tpl +++ b/ruby/private/bundle_fetch/BUILD.tpl @@ -10,6 +10,7 @@ rb_bundle_install( data = {data}, env = {env}, extra_args = {extra_args}, + binstubs = {binstubs}, gemfile = "{gemfile_path}", gemfile_lock = "{gemfile_lock_path}", jars = glob(["{jars_path}/**/*.jar"], allow_empty = True), diff --git a/ruby/private/bundle_install.bzl b/ruby/private/bundle_install.bzl index 8f514cd18..03ff41d02 100644 --- a/ruby/private/bundle_install.bzl +++ b/ruby/private/bundle_install.bzl @@ -57,12 +57,20 @@ def _rb_bundle_install_impl(ctx): jar_files = ctx.files.jars if ctx.attr.jars else [] + # Expand `$(location ...)`/`$(execpath ...)` in env values (against `data`), + # mirroring `extra_args`. Lets an env var reference a build artifact by label + # — e.g. prepending a generated cross-compiler wrapper dir onto PATH. + attr_env = { + key: ctx.expand_location(value, ctx.attr.data) + for key, value in ctx.attr.env.items() + } + env = {} env.update(toolchain.env) - env.update(ctx.attr.env) + env.update(attr_env) bundler_env = {} - bundler_env.update(ctx.attr.env) + bundler_env.update(attr_env) jars_home_strip_suffix = "" if toolchain.version.startswith("jruby"): @@ -80,12 +88,12 @@ def _rb_bundle_install_impl(ctx): if _is_windows(ctx): script = ctx.actions.declare_file("bundle_install_{}.cmd".format(ctx.label.name)) template = ctx.file._bundle_install_cmd_tpl - path = ctx.attr.env.get("PATH", "%PATH%") + path = attr_env.get("PATH", "%PATH%") env.update({"PATH": _normalize_path(ctx, toolchain.ruby.dirname) + ";" + path}) else: script = ctx.actions.declare_file("bundle_install_{}.sh".format(ctx.label.name)) template = ctx.file._bundle_install_sh_tpl - path = ctx.attr.env.get("PATH", "$PATH") + path = attr_env.get("PATH", "$PATH") env.update({"PATH": toolchain.ruby.dirname + ":" + path}) # Calculate relative location between BUNDLE_GEMFILE and BUNDLE_PATH. @@ -105,6 +113,21 @@ def _rb_bundle_install_impl(ctx): "BUNDLE_SHEBANG": _normalize_path(ctx, toolchain.ruby.short_path), }) + # Binstubs generation runs with the HOST ruby, which validates gems against + # the running platform. For a cross-platform bundle (e.g. installed with + # --target-rbconfig for another OS/arch) the host can't see those gems' + # native extensions and `binstubs --all` fails. `binstubs = False` skips it, + # just materializing the (empty) declared binstubs dir instead. + if ctx.attr.binstubs: + binstubs_cmd = "{} {} binstubs --all".format( + _normalize_path(ctx, toolchain.ruby.path), + _normalize_path(ctx, bundler_exe), + ) + elif _is_windows(ctx): + binstubs_cmd = 'if not exist "{p}" mkdir "{p}"'.format(p = _normalize_path(ctx, binstubs.path)) + else: + binstubs_cmd = 'mkdir -p "{}"'.format(binstubs.path) + ctx.actions.expand_template( template = template, output = script, @@ -112,6 +135,7 @@ def _rb_bundle_install_impl(ctx): "{env}": _convert_env_to_script(ctx, env), "{bundler_exe}": _normalize_path(ctx, bundler_exe), "{ruby_path}": _normalize_path(ctx, toolchain.ruby.path), + "{binstubs_cmd}": binstubs_cmd, "{extra_args}": " ".join([ ctx.expand_location(arg, ctx.attr.data) for arg in ctx.attr.extra_args @@ -141,6 +165,11 @@ def _rb_bundle_install_impl(ctx): files = depset(files), runfiles = ctx.runfiles(files), ), + # `gems` exposes JUST the installed vendor/bundle tree (no Gemfile/ + # binstubs), so consumers can package it cleanly — e.g. + # `filegroup(output_group = "gems")` + pkg_files strip_prefix to lay the + # gems into a container's BUNDLE_PATH without the surrounding files. + OutputGroupInfo(gems = depset([bundle_path])), RubyFilesInfo( binary = None, transitive_srcs = depset([ctx.file.gemfile, ctx.file.gemfile_lock] + ctx.files.srcs), @@ -187,7 +216,16 @@ rb_bundle_install = rule( doc = "List of Ruby source files used to build the library.", ), "env": attr.string_dict( - doc = "Environment variables to use during installation.", + doc = "Environment variables to use during installation. Values support " + + "`$(location ...)`/`$(execpath ...)` make-variable expansion against " + + "`data` (e.g. prepend a generated cross-compiler wrapper dir onto PATH).", + ), + "binstubs": attr.bool( + default = True, + doc = "Whether to run `bundle binstubs --all` after install. Set False for " + + "cross-platform bundles (installed with a foreign --target-rbconfig): the " + + "host ruby can't validate the target's native extensions, so binstubs " + + "generation fails. When False the (empty) binstubs dir is still created.", ), "extra_args": attr.string_list( doc = "Extra arguments appended to the `bundle install` command line. " + diff --git a/ruby/private/bundle_install/bundle_install.cmd.tpl b/ruby/private/bundle_install/bundle_install.cmd.tpl index 96cc07eb8..789a05319 100644 --- a/ruby/private/bundle_install/bundle_install.cmd.tpl +++ b/ruby/private/bundle_install/bundle_install.cmd.tpl @@ -3,6 +3,6 @@ {env} {ruby_path} {bundler_exe} install --standalone --local {extra_args} -{ruby_path} {bundler_exe} binstubs --all +{binstubs_cmd} :: vim: ft=dosbatch diff --git a/ruby/private/bundle_install/bundle_install.sh.tpl b/ruby/private/bundle_install/bundle_install.sh.tpl index 4b9b705a9..78e060bde 100644 --- a/ruby/private/bundle_install/bundle_install.sh.tpl +++ b/ruby/private/bundle_install/bundle_install.sh.tpl @@ -3,6 +3,6 @@ {env} {ruby_path} {bundler_exe} install --standalone --local {extra_args} -{ruby_path} {bundler_exe} binstubs --all +{binstubs_cmd} # vim: ft=bash diff --git a/ruby/private/download/BUILD.tpl b/ruby/private/download/BUILD.tpl index acdffbba3..082539750 100644 --- a/ruby/private/download/BUILD.tpl +++ b/ruby/private/download/BUILD.tpl @@ -24,6 +24,19 @@ filegroup( ), ) +# The interpreter's own rbconfig.rb (a single file). Because portable-ruby is +# relocatable (rbconfig computes TOPDIR from __FILE__), passing this unmodified +# to `gem install --target-rbconfig` cross-compiles source gems for THIS Ruby's +# platform — its rubyhdrdir/libdir auto-resolve to the staged `dist` tree. Under +# a platform transition it resolves to the target arch's Ruby. +filegroup( + name = "rbconfig", + srcs = glob( + ["dist/lib/ruby/*/*/rbconfig.rb"], + allow_empty = True, + ), +) + rb_binary( name = "ruby", main = ":ruby_file", diff --git a/ruby/private/toolchain/hub.bzl b/ruby/private/toolchain/hub.bzl index 1fe37a855..3973c31db 100644 --- a/ruby/private/toolchain/hub.bzl +++ b/ruby/private/toolchain/hub.bzl @@ -42,6 +42,7 @@ _STATIC_ALIASES = [ "headers", "jars", "dist_files", + "rbconfig", ] _CONFIG_SETTING_TPL = """ From beea040dccde05bb3b9b6666526a7e69049447f6 Mon Sep 17 00:00:00 2001 From: dhash Date: Wed, 12 Aug 2026 18:32:13 -0700 Subject: [PATCH 5/8] test(examples/gem): exercise extra_args end-to-end in the bundle_fetch example Addresses review feedback on #394 (comment 5223558709): "It would be great to have a sample test so that it doesn't break in the future," in reply to the offer to add an example/e2e test for the new extra_args attribute. Adds extra_args = ["--quiet"] to the existing bundle_fetch() used by examples/gem (both the bzlmod MODULE.bazel and legacy WORKSPACE entry points), so the extra_args -> rb_bundle_fetch -> rb_bundle_install plumbing is exercised by every existing examples/gem CI job. If that threading ever regresses (e.g. the flag gets dropped or the bundle install command line gets mangled), the existing `bazel build ...` step fails instead of the regression going unnoticed. Verified locally: built @bundle//bin:rake with this change (bzlmod mode) and confirmed the generated install script contains `bundle install --standalone --local --quiet`. --- examples/gem/MODULE.bazel | 4 ++++ examples/gem/WORKSPACE | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/examples/gem/MODULE.bazel b/examples/gem/MODULE.bazel index fe9f5c064..ff63b90a3 100644 --- a/examples/gem/MODULE.bazel +++ b/examples/gem/MODULE.bazel @@ -27,6 +27,10 @@ ruby.bundle_fetch( env = { "BUNDLE_BUILD__FOO": "bar", }, + # Exercises `extra_args` end-to-end, so a regression in that plumbing + # (e.g. the flag getting dropped or the command line getting mangled) + # fails the build instead of going unnoticed. + extra_args = ["--quiet"], gem_checksums = { "ast-2.4.2": "1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12", "concurrent-ruby-1.3.5": "813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6", diff --git a/examples/gem/WORKSPACE b/examples/gem/WORKSPACE index c5cb95e20..3243c0419 100644 --- a/examples/gem/WORKSPACE +++ b/examples/gem/WORKSPACE @@ -29,6 +29,10 @@ rb_bundle_fetch( env = { "BUNDLE_BUILD__FOO": "bar", }, + # Exercises `extra_args` end-to-end, so a regression in that plumbing + # (e.g. the flag getting dropped or the command line getting mangled) + # fails the build instead of going unnoticed. + extra_args = ["--quiet"], gem_checksums = { "ast-2.4.2": "1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12", "concurrent-ruby-1.3.5": "813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6", From deab2b34d1d1b9dea047e7cdcb549a83cb90ca19 Mon Sep 17 00:00:00 2001 From: dhash Date: Wed, 12 Aug 2026 18:45:00 -0700 Subject: [PATCH 6/8] docs: regenerate rules.md/repository_rules.md for new bundle attrs Fixes the failing docs:update_0_test/docs:update_1_test (stardoc diff tests) in the Ruleset CI job. The rb_bundle_install and rb_bundle_fetch attrs added earlier in this PR (extra_args, data, binstubs) changed the .bzl docstrings but docs/*.md were never regenerated to match. Ran `bazel run //docs:update` and committed the resulting diff verbatim. --- docs/repository_rules.md | 10 +++++++--- docs/rules.md | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/repository_rules.md b/docs/repository_rules.md index 0626905eb..610e3338b 100644 --- a/docs/repository_rules.md +++ b/docs/repository_rules.md @@ -117,8 +117,9 @@ rb_library(
 load("@rules_ruby//ruby:deps.bzl", "rb_bundle_fetch")
 
-rb_bundle_fetch(name, srcs, auth_patterns, bundler_checksums, bundler_remote, env, gem_checksums,
-                gemfile, gemfile_lock, jar_checksums, netrc, repo_mapping, ruby)
+rb_bundle_fetch(name, srcs, data, auth_patterns, binstubs, bundler_checksums, bundler_remote, env,
+                extra_args, gem_checksums, gemfile, gemfile_lock, jar_checksums, netrc, repo_mapping,
+                ruby)
 
Fetches Bundler dependencies to be automatically installed by other targets. @@ -180,10 +181,13 @@ rb_test( | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this repository. | Name | required | | | srcs | List of Ruby source files necessary during installation. | List of labels | optional | `[]` | +| data | Labels (as canonical strings) referenced from `extra_args` via `$(location ...)`; forwarded to the generated rb_bundle_install `data`. | List of strings | optional | `[]` | | auth_patterns | A list of patterns to match against urls for which the auth object should be used. | Dictionary: String -> String | optional | `{}` | +| binstubs | Forwarded to the generated rb_bundle_install `binstubs` (set False to skip `bundle binstubs --all` for cross-platform bundles). | Boolean | optional | `True` | | bundler_checksums | Custom map from Bundler version to its SHA-256 checksum. | Dictionary: String -> String | optional | `{}` | | bundler_remote | Remote to fetch the bundler gem from. | String | optional | `"https://rubygems.org/"` | -| env | Environment variables to use during installation. | Dictionary: String -> String | optional | `{}` | +| env | Environment variables to use during installation. Values support `$(location ...)` expansion against `data` (forwarded to the generated rb_bundle_install `env`). | Dictionary: String -> String | optional | `{}` | +| extra_args | Extra arguments appended to the `bundle install` command line run by the generated rb_bundle_install target. Supports `$(location ...)` expansion against `data`. | List of strings | optional | `[]` | | gem_checksums | SHA-256 checksums for remote gems. Keys are gem names (e.g. foobar-1.2.3), values are SHA-256 checksums. | Dictionary: String -> String | optional | `{}` | | gemfile | Gemfile to install dependencies from. | Label | required | | | gemfile_lock | Gemfile.lock to install dependencies from. | Label | required | | diff --git a/docs/rules.md b/docs/rules.md index 9e6c402b6..8fa2a9967 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -147,7 +147,8 @@ rake, version 13.1.0
 load("@rules_ruby//ruby:defs.bzl", "rb_bundle_install")
 
-rb_bundle_install(name, srcs, env, gemfile, gemfile_lock, gems, jars, jars_path, ruby)
+rb_bundle_install(name, srcs, data, binstubs, env, extra_args, gemfile, gemfile_lock, gems, jars,
+                  jars_path, ruby)
 
Installs Bundler dependencies from cached gems. @@ -162,7 +163,10 @@ used by `rb_bundle_fetch()`. | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | | srcs | List of Ruby source files used to build the library. | List of labels | optional | `[]` | -| env | Environment variables to use during installation. | Dictionary: String -> String | optional | `{}` | +| data | Files referenced from `extra_args` via `$(location ...)` expansion. They are also added as inputs to the `bundle install` action. | List of labels | optional | `[]` | +| binstubs | Whether to run `bundle binstubs --all` after install. Set False for cross-platform bundles (installed with a foreign --target-rbconfig): the host ruby can't validate the target's native extensions, so binstubs generation fails. When False the (empty) binstubs dir is still created. | Boolean | optional | `True` | +| env | Environment variables to use during installation. Values support `$(location ...)`/`$(execpath ...)` make-variable expansion against `data` (e.g. prepend a generated cross-compiler wrapper dir onto PATH). | Dictionary: String -> String | optional | `{}` | +| extra_args | Extra arguments appended to the `bundle install` command line. Supports `$(location ...)`/`$(rootpath ...)`/`$(execpath ...)` make-variable expansion against `data`. For example `["--target-rbconfig", "$(location //path:rbconfig.rb)"]` to install a different platform's precompiled gems (cross-platform bundle). | List of strings | optional | `[]` | | gemfile | Gemfile to install dependencies from. | Label | required | | | gemfile_lock | Gemfile.lock to install dependencies from. | Label | required | | | gems | List of gems in vendor/cache that are used to install dependencies from. | List of labels | required | | From bd75b7088debcf76e052edbc98b6596e001b5bbd Mon Sep 17 00:00:00 2001 From: dhash Date: Fri, 14 Aug 2026 13:35:44 -0700 Subject: [PATCH 7/8] fix: use rb_library for pure-Ruby target, rename dist_files to files, drop trivial comment Addresses review feedback on the extra_args/target-rbconfig PR: - rbconfig glob only matches .rb source, so use rb_library instead of filegroup - rename dist_files -> files to match the pre-existing `files` attribute naming - drop a comment that just restated what the code already said Co-Authored-By: Claude Sonnet 5 --- ruby/private/bundle_install.bzl | 3 --- ruby/private/download/BUILD.tpl | 6 +++--- ruby/private/toolchain/hub.bzl | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ruby/private/bundle_install.bzl b/ruby/private/bundle_install.bzl index 03ff41d02..121b07404 100644 --- a/ruby/private/bundle_install.bzl +++ b/ruby/private/bundle_install.bzl @@ -57,9 +57,6 @@ def _rb_bundle_install_impl(ctx): jar_files = ctx.files.jars if ctx.attr.jars else [] - # Expand `$(location ...)`/`$(execpath ...)` in env values (against `data`), - # mirroring `extra_args`. Lets an env var reference a build artifact by label - # — e.g. prepending a generated cross-compiler wrapper dir onto PATH. attr_env = { key: ctx.expand_location(value, ctx.attr.data) for key, value in ctx.attr.env.items() diff --git a/ruby/private/download/BUILD.tpl b/ruby/private/download/BUILD.tpl index 082539750..6fc237b9d 100644 --- a/ruby/private/download/BUILD.tpl +++ b/ruby/private/download/BUILD.tpl @@ -1,6 +1,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") load("@rules_java//java:defs.bzl", "java_import") -load("@rules_ruby//ruby:defs.bzl", "rb_binary") +load("@rules_ruby//ruby:defs.bzl", "rb_binary", "rb_library") load("@rules_ruby//ruby:toolchain.bzl", "rb_toolchain") package(default_visibility = ["//visibility:public"]) @@ -17,7 +17,7 @@ filegroup( # packaging the interpreter into a container image (portable-ruby is relocatable # via relative rpaths, so this tars cleanly to e.g. /usr/local). filegroup( - name = "dist_files", + name = "files", srcs = glob( ["dist/**/*"], allow_empty = True, @@ -29,7 +29,7 @@ filegroup( # to `gem install --target-rbconfig` cross-compiles source gems for THIS Ruby's # platform — its rubyhdrdir/libdir auto-resolve to the staged `dist` tree. Under # a platform transition it resolves to the target arch's Ruby. -filegroup( +rb_library( name = "rbconfig", srcs = glob( ["dist/lib/ruby/*/*/rbconfig.rb"], diff --git a/ruby/private/toolchain/hub.bzl b/ruby/private/toolchain/hub.bzl index 3973c31db..97662e4ab 100644 --- a/ruby/private/toolchain/hub.bzl +++ b/ruby/private/toolchain/hub.bzl @@ -41,7 +41,7 @@ _STATIC_ALIASES = [ "toolchain", "headers", "jars", - "dist_files", + "files", "rbconfig", ] From 51e3def7f02bc4b2cf9e8dcbc620b7524e54d1f7 Mon Sep 17 00:00:00 2001 From: dhash Date: Fri, 14 Aug 2026 19:53:47 -0700 Subject: [PATCH 8/8] refactor: have rb_toolchain consume :files instead of re-globbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review: the toolchain already had a `files` attribute holding glob(["dist/**/*"]), so the new filegroup was globbing the same tree a second time. Now the filegroup is the single definition and rb_toolchain's `files` points at it, so the two cannot drift. The target itself stays, because the attribute alone does not cover the use case. rb_toolchain surfaces it as ToolchainInfo.files, which is reachable only from a rule implementation that resolves the Ruby toolchain; a BUILD file that wants the install tree — to package the interpreter into a container image, say — has no label to depend on. Hence :files, plus its hub alias, so @ruby//:files resolves. --- ruby/private/download/BUILD.tpl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ruby/private/download/BUILD.tpl b/ruby/private/download/BUILD.tpl index 6fc237b9d..14939ef0f 100644 --- a/ruby/private/download/BUILD.tpl +++ b/ruby/private/download/BUILD.tpl @@ -13,9 +13,16 @@ filegroup( }), ) -# The complete Ruby install tree (bin/, lib/, include/, ...). Useful for -# packaging the interpreter into a container image (portable-ruby is relocatable -# via relative rpaths, so this tars cleanly to e.g. /usr/local). +# The complete Ruby install tree (bin/, lib/, include/, ...). This is the single +# definition of that tree: rb_toolchain's `files` attribute below consumes it +# rather than re-globbing, so the toolchain and anything depending on this +# target can never drift apart. +# +# Exposed as a target because ToolchainInfo.files is only reachable from a rule +# implementation that resolves the toolchain — a BUILD file that wants the tree +# (e.g. to package the interpreter into a container image) has no way to ask for +# it otherwise. portable-ruby is relocatable via relative rpaths, so this tars +# cleanly to e.g. /usr/local. filegroup( name = "files", srcs = glob( @@ -67,7 +74,7 @@ rb_toolchain( "//conditions:default": "dist/bin/bundle", }), env = {env}, - files = glob(["dist/**/*"]), + files = [":files"], gem = select({ "@platforms//os:windows": "dist/bin/{gem_binary_name}.cmd", "//conditions:default": "dist/bin/{gem_binary_name}",