diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index cf7bf4f64..8507e76bb 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -38,6 +38,9 @@ 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`. 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(), @@ -99,6 +102,9 @@ def _ruby_module_extension(module_ctx): name = bundle_fetch.name, srcs = bundle_fetch.srcs, 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 942c80e8a..ccbf1192e 100644 --- a/ruby/private/bundle_fetch.bzl +++ b/ruby/private/bundle_fetch.bzl @@ -247,6 +247,9 @@ 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), + "{data}": _join_and_indent(repository_ctx.attr.data), + "{binstubs}": repr(repository_ctx.attr.binstubs), "{ruby}": ruby_toolchain_attr, }, ) @@ -258,6 +261,9 @@ 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, + "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, @@ -299,7 +305,23 @@ 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`.", ), "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 a0f192b0d..9537cf292 100644 --- a/ruby/private/bundle_fetch/BUILD.tpl +++ b/ruby/private/bundle_fetch/BUILD.tpl @@ -7,7 +7,10 @@ package(default_visibility = ["//visibility:public"]) rb_bundle_install( name = "{name}", srcs = {srcs}, + 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 898410310..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,12 +135,17 @@ 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 + ]), }, ) 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})", @@ -137,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), @@ -183,7 +216,28 @@ 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. " + + "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.", diff --git a/ruby/private/bundle_install/bundle_install.cmd.tpl b/ruby/private/bundle_install/bundle_install.cmd.tpl index f42c753fb..789a05319 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} binstubs --all +{ruby_path} {bundler_exe} install --standalone --local {extra_args} +{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 738a03786..78e060bde 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} binstubs --all +{ruby_path} {bundler_exe} install --standalone --local {extra_args} +{binstubs_cmd} # vim: ft=bash diff --git a/ruby/private/download/BUILD.tpl b/ruby/private/download/BUILD.tpl index c08bcdb2e..082539750 100644 --- a/ruby/private/download/BUILD.tpl +++ b/ruby/private/download/BUILD.tpl @@ -13,6 +13,30 @@ 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, + ), +) + +# 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 d7c1c0613..3973c31db 100644 --- a/ruby/private/toolchain/hub.bzl +++ b/ruby/private/toolchain/hub.bzl @@ -41,6 +41,8 @@ _STATIC_ALIASES = [ "toolchain", "headers", "jars", + "dist_files", + "rbconfig", ] _CONFIG_SETTING_TPL = """