diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b26882e0..f3a6f456 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,9 +83,10 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: '3.0.6' - - run: bazel test ... + - run: bazel build ... - run: bazel run lib/gem:add-numbers 2 - run: bazel run lib/gem:print-version + - run: bazel test ... - if: failure() && runner.debug == '1' uses: mxschmitt/action-tmate@v3 diff --git a/docs/rules.md b/docs/rules.md index 6cd24841..e6659cf1 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -151,7 +151,7 @@ rake, version 10.5.0 | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | -| data | List of non-Ruby source files used to build the library. | List of labels | optional | [] | +| data | List of runtime dependencies needed by a program that depends on this library. | List of labels | optional | [] | | deps | List of other Ruby libraries the target depends on. | List of labels | optional | [] | | env | Environment variables to use during execution. | Dictionary: String -> String | optional | {} | | env_inherit | List of environment variable names to be inherited by the test runner. | List of strings | optional | [] | @@ -258,7 +258,7 @@ INFO: Build completed successfully, 2 total actions | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | | bundle_env | List of bundle environment variables to set when building the library. | Dictionary: String -> String | optional | {} | -| data | List of non-Ruby source files used to build the library. | List of labels | optional | [] | +| data | List of runtime dependencies needed by a program that depends on this library. | List of labels | optional | [] | | deps | List of other Ruby libraries the target depends on. | List of labels | optional | [] | | gemspec | Gemspec file to use for gem building. | Label | required | | | srcs | List of Ruby source files used to build the library. | List of labels | optional | [] | @@ -336,7 +336,7 @@ Successfully registered gem: example (0.1.0) | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | | bundle_env | List of bundle environment variables to set when building the library. | Dictionary: String -> String | optional | {} | -| data | List of non-Ruby source files used to build the library. | List of labels | optional | [] | +| data | List of runtime dependencies needed by a program that depends on this library. | List of labels | optional | [] | | deps | List of other Ruby libraries the target depends on. | List of labels | optional | [] | | env | Environment variables to use during execution. | Dictionary: String -> String | optional | {} | | env_inherit | List of environment variable names to be inherited by the test runner. | List of strings | optional | [] | @@ -437,7 +437,7 @@ using other rules. | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | | bundle_env | List of bundle environment variables to set when building the library. | Dictionary: String -> String | optional | {} | -| data | List of non-Ruby source files used to build the library. | List of labels | optional | [] | +| data | List of runtime dependencies needed by a program that depends on this library. | List of labels | optional | [] | | deps | List of other Ruby libraries the target depends on. | List of labels | optional | [] | | srcs | List of Ruby source files used to build the library. | List of labels | optional | [] | @@ -597,7 +597,7 @@ root = File.expand_path(__dir__) | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | -| data | List of non-Ruby source files used to build the library. | List of labels | optional | [] | +| data | List of runtime dependencies needed by a program that depends on this library. | List of labels | optional | [] | | deps | List of other Ruby libraries the target depends on. | List of labels | optional | [] | | env | Environment variables to use during execution. | Dictionary: String -> String | optional | {} | | env_inherit | List of environment variable names to be inherited by the test runner. | List of strings | optional | [] | diff --git a/examples/gem/lib/gem/BUILD b/examples/gem/lib/gem/BUILD index 0a83aab3..8f21943f 100644 --- a/examples/gem/lib/gem/BUILD +++ b/examples/gem/lib/gem/BUILD @@ -1,3 +1,4 @@ +load("@bazel_skylib//rules:run_binary.bzl", "run_binary") load("@rules_ruby//ruby:defs.bzl", "rb_binary", "rb_library") package(default_visibility = ["//:__subpackages__"]) @@ -32,3 +33,15 @@ rb_binary( args = ["lib/gem/version.rb"], deps = [":version"], ) + +run_binary( + name = "perform-addition", + srcs = [":add"], + outs = ["addition-result"], + args = [ + "1", + "2", + "$(location :addition-result)", + ], + tool = ":add-numbers", +) diff --git a/examples/gem/lib/gem/add.rb b/examples/gem/lib/gem/add.rb index 65defc09..94615c7e 100755 --- a/examples/gem/lib/gem/add.rb +++ b/examples/gem/lib/gem/add.rb @@ -17,7 +17,13 @@ def result end if __FILE__ == $PROGRAM_NAME - raise "Pass two numbers to sum: #{ARGV}" unless ARGV.size == 2 + raise "Pass two numbers to sum: #{ARGV}" if ARGV.size < 2 - puts GEM::Add.new(*ARGV.map(&:to_i)).result + one, two, output = *ARGV + result = GEM::Add.new(Integer(one), Integer(two)).result + if output + File.write(output, result) + else + puts result + end end diff --git a/ruby/private/BUILD b/ruby/private/BUILD index fe40bda6..bc32b1ec 100644 --- a/ruby/private/BUILD +++ b/ruby/private/BUILD @@ -1,12 +1,6 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -exports_files([ - "binary/binary.cmd.tpl", - "binary/binary.sh.tpl", - "gem_build/gem_builder.rb.tpl", -]) - -exports_files(glob(["**/*.bzl"])) +exports_files(["gem_build/gem_builder.rb.tpl"]) bzl_library( name = "binary", @@ -15,6 +9,7 @@ bzl_library( deps = [ ":library", ":providers", + "//ruby/private/binary:rlocation", ], ) diff --git a/ruby/private/binary.bzl b/ruby/private/binary.bzl index 8b9529b2..4cd69dca 100644 --- a/ruby/private/binary.bzl +++ b/ruby/private/binary.bzl @@ -7,8 +7,10 @@ load( "get_bundle_env", "get_transitive_data", "get_transitive_deps", + "get_transitive_runfiles", "get_transitive_srcs", ) +load("//ruby/private/binary:rlocation.bzl", "BASH_RLOCATION_FUNCTION", "BATCH_RLOCATION_FUNCTION") ATTRS = { "main": attr.label( @@ -30,19 +32,27 @@ Use a built-in `args` attribute to pass extra arguments to the script. ), "_binary_cmd_tpl": attr.label( allow_single_file = True, - default = "@rules_ruby//ruby/private:binary/binary.cmd.tpl", + default = "@rules_ruby//ruby/private/binary:binary.cmd.tpl", ), "_binary_sh_tpl": attr.label( allow_single_file = True, - default = "@rules_ruby//ruby/private:binary/binary.sh.tpl", + default = "@rules_ruby//ruby/private/binary:binary.sh.tpl", + ), + "_runfiles_library": attr.label( + allow_single_file = True, + default = "@bazel_tools//tools/bash/runfiles", ), "_windows_constraint": attr.label( default = "@platforms//os:windows", ), } +_EXPORT_ENV_VAR_COMMAND = "{command} {name}={value}" +_EXPORT_BATCH_COMMAND = "set" +_EXPORT_BASH_COMMAND = "export" + # buildifier: disable=function-docstring -def generate_rb_binary_script(ctx, binary, bundler = False, args = []): +def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, java_bin = ""): windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo] is_windows = ctx.target_platform_has_constraint(windows_constraint) toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] @@ -55,10 +65,14 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = []): if is_windows: binary_path = binary_path.replace("/", "\\") - toolchain_bindir = toolchain_bindir.replace("/", "\\") + export_command = _EXPORT_BATCH_COMMAND + rlocation_function = BATCH_RLOCATION_FUNCTION script = ctx.actions.declare_file("{}.rb.cmd".format(ctx.label.name)) + toolchain_bindir = toolchain_bindir.replace("/", "\\") template = ctx.file._binary_cmd_tpl else: + export_command = _EXPORT_BASH_COMMAND + rlocation_function = BASH_RLOCATION_FUNCTION script = ctx.actions.declare_file("{}.rb.sh".format(ctx.label.name)) template = ctx.file._binary_sh_tpl @@ -70,6 +84,11 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = []): args = " ".join(args) args = ctx.expand_location(args) + environment = [] + for (name, value) in env.items(): + command = _EXPORT_ENV_VAR_COMMAND.format(command = export_command, name = name, value = value) + environment.append(command) + ctx.actions.expand_template( template = template, output = script, @@ -78,8 +97,11 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = []): "{args}": args, "{binary}": binary_path, "{toolchain_bindir}": toolchain_bindir, + "{env}": "\n".join(environment), "{bundler_command}": bundler_command, "{ruby_binary_name}": toolchain.ruby.basename, + "{java_bin}": java_bin, + "{rlocation_function}": rlocation_function, }, ) @@ -89,36 +111,46 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = []): def rb_binary_impl(ctx): bundler = False env = {} + java_bin = "" + + # TODO: avoid expanding the depset to a list, it may be expensive in a large graph transitive_data = get_transitive_data(ctx.files.data, ctx.attr.deps).to_list() transitive_deps = get_transitive_deps(ctx.attr.deps).to_list() transitive_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps).to_list() - tools = [] - java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"] - ruby_toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] - if not ctx.attr.main: - tools.append(ruby_toolchain.ruby) + ruby_toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] + tools = [ruby_toolchain.ruby, ruby_toolchain.bundle, ruby_toolchain.gem] if ruby_toolchain.version.startswith("jruby"): - env["JAVA_HOME"] = java_toolchain.java_runtime.java_home_runfiles_path + java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"] + tools.extend(ctx.files._runfiles_library) tools.extend(java_toolchain.java_runtime.files.to_list()) + java_bin = java_toolchain.java_runtime.java_executable_runfiles_path[3:] for dep in transitive_deps: # TODO: Do not depend on workspace name to determine bundle if dep.label.workspace_name.endswith("bundle"): bundler = True - runfiles = ctx.runfiles(transitive_data + transitive_srcs + tools) - bundle_env = get_bundle_env(ctx.attr.env, ctx.attr.deps) env.update(bundle_env) env.update(ctx.attr.env) - script = generate_rb_binary_script(ctx, ctx.executable.main, bundler) + script = generate_rb_binary_script( + ctx, + ctx.executable.main, + bundler = bundler, + env = env, + java_bin = java_bin, + ) + + runfiles = ctx.runfiles(transitive_srcs + transitive_data + tools) + runfiles = get_transitive_runfiles(runfiles, ctx.attr.srcs, ctx.attr.deps, ctx.attr.data) return [ DefaultInfo( executable = script, + files = depset(transitive_srcs + transitive_data + tools), runfiles = runfiles, ), RubyFilesInfo( diff --git a/ruby/private/binary/BUILD b/ruby/private/binary/BUILD new file mode 100644 index 00000000..4f09aefc --- /dev/null +++ b/ruby/private/binary/BUILD @@ -0,0 +1,9 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +exports_files(glob(["*.tpl"])) + +bzl_library( + name = "rlocation", + srcs = ["rlocation.bzl"], + visibility = ["//ruby:__subpackages__"], +) diff --git a/ruby/private/binary/binary.cmd.tpl b/ruby/private/binary/binary.cmd.tpl index 757686b1..98a7f4c8 100644 --- a/ruby/private/binary/binary.cmd.tpl +++ b/ruby/private/binary/binary.cmd.tpl @@ -1,2 +1,16 @@ -@set PATH={toolchain_bindir};%PATH% -@{bundler_command} {ruby_binary_name} {binary} {args} %* +@echo off +setlocal enableextensions enabledelayedexpansion + +:: Find location of JAVA_HOME in runfiles. +if "{java_bin}" neq "" ( + {rlocation_function} + set RUNFILES_MANIFEST_ONLY=1 + call :rlocation {java_bin} java_bin + for %%a in ("%java_bin%\..\..") do set JAVA_HOME=%%~fa +) + +:: Set environment variables. +set PATH={toolchain_bindir};%PATH% +{env} + +{bundler_command} {ruby_binary_name} {binary} {args} %* diff --git a/ruby/private/binary/binary.sh.tpl b/ruby/private/binary/binary.sh.tpl index 4603f973..ec323110 100644 --- a/ruby/private/binary/binary.sh.tpl +++ b/ruby/private/binary/binary.sh.tpl @@ -1,4 +1,13 @@ #!/usr/bin/env bash +# Find location of JAVA_HOME in runfiles. +if [ -n "{java_bin}" ]; then + {rlocation_function} + export JAVA_HOME=$(dirname $(dirname $(rlocation "{java_bin}"))) +fi + +# Set environment variables. export PATH={toolchain_bindir}:$PATH +{env} + {bundler_command} {ruby_binary_name} {binary} {args} $@ diff --git a/ruby/private/binary/rlocation.bzl b/ruby/private/binary/rlocation.bzl new file mode 100644 index 00000000..1e06c37d --- /dev/null +++ b/ruby/private/binary/rlocation.bzl @@ -0,0 +1,61 @@ +"Helpers to get location of files in runfiles. Vendored from aspect_bazel_lib" + +# https://github.com/aspect-build/bazel-lib/blob/ddac9c46c3bff4cf8d0118a164c75390dbec2da9/lib/paths.bzl +BASH_RLOCATION_FUNCTION = r""" +# --- begin runfiles.bash initialization v2 --- +set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ +source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ +source "$0.runfiles/$f" 2>/dev/null || \ +source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ +source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ +{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v2 --- +""" + +# https://github.com/aspect-build/bazel-lib/blob/ddac9c46c3bff4cf8d0118a164c75390dbec2da9/lib/windows_utils.bzl +BATCH_RLOCATION_FUNCTION = r""" +rem Usage of rlocation function: +rem call :rlocation +rem The rlocation function maps the given to its absolute +rem path and stores the result in a variable named . +rem This function fails if the doesn't exist in mainifest +rem file. +:: Start of rlocation +goto :rlocation_end +:rlocation +if "%~2" equ "" ( + echo>&2 ERROR: Expected two arguments for rlocation function. + exit 1 +) +if "%RUNFILES_MANIFEST_ONLY%" neq "1" ( + set %~2=%~1 + exit /b 0 +) +if exist "%RUNFILES_DIR%" ( + set RUNFILES_MANIFEST_FILE=%RUNFILES_DIR%_manifest +) +if "%RUNFILES_MANIFEST_FILE%" equ "" ( + set RUNFILES_MANIFEST_FILE=%~f0.runfiles\MANIFEST +) +if not exist "%RUNFILES_MANIFEST_FILE%" ( + set RUNFILES_MANIFEST_FILE=%~f0.runfiles_manifest +) +set MF=%RUNFILES_MANIFEST_FILE:/=\% +if not exist "%MF%" ( + echo>&2 ERROR: Manifest file %MF% does not exist. + exit 1 +) +set runfile_path=%~1 +for /F "tokens=2* usebackq" %%i in (`%SYSTEMROOT%\system32\findstr.exe /l /c:"!runfile_path! " "%MF%"`) do ( + set abs_path=%%i +) +if "!abs_path!" equ "" ( + echo>&2 ERROR: !runfile_path! not found in runfiles manifest + exit 1 +) +set %~2=!abs_path! +exit /b 0 +:rlocation_end +:: End of rlocation +""" diff --git a/ruby/private/library.bzl b/ruby/private/library.bzl index 671ad45d..82f8a6a6 100644 --- a/ruby/private/library.bzl +++ b/ruby/private/library.bzl @@ -6,6 +6,7 @@ load( "get_bundle_env", "get_transitive_data", "get_transitive_deps", + "get_transitive_runfiles", "get_transitive_srcs", ) @@ -19,7 +20,7 @@ ATTRS = { ), "data": attr.label_list( allow_files = True, - doc = "List of non-Ruby source files used to build the library.", + doc = "List of runtime dependencies needed by a program that depends on this library.", ), "bundle_env": attr.string_dict( default = {}, @@ -28,11 +29,23 @@ ATTRS = { } def _rb_library_impl(ctx): + # TODO: avoid expanding the depset to a list, it may be expensive in a large graph + transitive_data = get_transitive_data(ctx.files.data, ctx.attr.deps).to_list() + transitive_deps = get_transitive_deps(ctx.attr.deps).to_list() + transitive_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps).to_list() + + runfiles = ctx.runfiles(transitive_srcs + transitive_data) + runfiles = get_transitive_runfiles(runfiles, ctx.attr.srcs, ctx.attr.deps, ctx.attr.data) + return [ + DefaultInfo( + files = depset(transitive_srcs + transitive_data), + runfiles = runfiles, + ), RubyFilesInfo( - transitive_data = get_transitive_data(ctx.files.data, ctx.attr.deps), - transitive_deps = get_transitive_deps(ctx.attr.deps), - transitive_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps), + transitive_data = depset(transitive_data), + transitive_deps = depset(transitive_deps), + transitive_srcs = depset(transitive_srcs), bundle_env = get_bundle_env(ctx.attr.bundle_env, ctx.attr.deps), ), ] diff --git a/ruby/private/providers.bzl b/ruby/private/providers.bzl index 38fa96fd..e4e75df0 100644 --- a/ruby/private/providers.bzl +++ b/ruby/private/providers.bzl @@ -47,6 +47,24 @@ def get_transitive_deps(deps): transitive = [dep[RubyFilesInfo].transitive_deps for dep in deps], ) +# https://bazel.build/extending/rules#runfiles +def get_transitive_runfiles(runfiles, srcs, data, deps): + """Obtain the runfiles for a target, its transitive data files and dependencies. + + Args: + runfiles: the runfiles + srcs: a list of source files + data: a list of data files + deps: a list of targets that are direct dependencies + Returns: + the runfiles + """ + transitive_runfiles = [] + for targets in (srcs, data, deps): + for target in targets: + transitive_runfiles.append(target[DefaultInfo].default_runfiles) + return runfiles.merge_all(transitive_runfiles) + def get_bundle_env(envs, deps): """Obtain the BUNDLE_* environment variables for a target and its transitive dependencies.