diff --git a/examples/proto/app/BUILD b/examples/proto/app/BUILD index c52b592f..d8e62402 100644 --- a/examples/proto/app/BUILD +++ b/examples/proto/app/BUILD @@ -15,7 +15,7 @@ rb_test( srcs = ["hello_test.rb"], main = "hello_test.rb", deps = [ - "//proto:foo_proto", + "//proto:all_protos", "@bundle", ], ) diff --git a/examples/proto/app/hello.rb b/examples/proto/app/hello.rb index e27eba86..62a855ab 100644 --- a/examples/proto/app/hello.rb +++ b/examples/proto/app/hello.rb @@ -1,10 +1,3 @@ -# TODO: is there a ruby runfiles helper? -runfiles_dir = ENV["RUNFILES_DIR"] -if runfiles_dir - main_root = File.join(runfiles_dir, "_main") - $LOAD_PATH.unshift(main_root) if Dir.exist?(main_root) && !$LOAD_PATH.include?(main_root) -end - require "grpc" require "proto/foo_pb" require "proto/foo_services_pb" diff --git a/examples/proto/app/hello_test.rb b/examples/proto/app/hello_test.rb index 11f8d7e6..260d5f51 100644 --- a/examples/proto/app/hello_test.rb +++ b/examples/proto/app/hello_test.rb @@ -1,14 +1,6 @@ #!/usr/bin/env ruby # Test that the gRPC app starts and can process a GetFoo RPC. -runfiles_dir = ENV["RUNFILES_DIR"] -if runfiles_dir - main_root = File.join(runfiles_dir, "_main") - $LOAD_PATH.unshift(main_root) if Dir.exist?(main_root) && !$LOAD_PATH.include?(main_root) - proto_root = File.join(main_root, "proto") - $LOAD_PATH.unshift(proto_root) if Dir.exist?(proto_root) && !$LOAD_PATH.include?(proto_root) -end - require "grpc" require "proto/foo_pb" require "proto/foo_services_pb" diff --git a/examples/proto/proto/BUILD b/examples/proto/proto/BUILD index 60b9eeb3..436b47f0 100644 --- a/examples/proto/proto/BUILD +++ b/examples/proto/proto/BUILD @@ -16,3 +16,14 @@ proto_library( visibility = ["//visibility:public"], deps = ["@protobuf//:timestamp_proto"], ) + +# Bundles the other protos without compiling any sources of its own, +# like the `all_protos` style targets in envoy_api. +proto_library( + name = "all_protos", + visibility = ["//visibility:public"], + deps = [ + ":foo_proto", + ":subdir_proto", + ], +) diff --git a/ruby/private/binary.bzl b/ruby/private/binary.bzl index 571032c5..5704c4d1 100644 --- a/ruby/private/binary.bzl +++ b/ruby/private/binary.bzl @@ -8,6 +8,7 @@ load( "get_bundle_env", "get_transitive_data", "get_transitive_deps", + "get_transitive_load_paths", "get_transitive_runfiles", "get_transitive_srcs", ) @@ -75,7 +76,7 @@ Supports `$(location)` expansion for targets from `srcs`, `data` and `deps`. } # buildifier: disable=function-docstring -def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, java_bin = "", jars_home_strip_suffix = "", coverage_helper = "", runfiles_helper = "", is_jruby = False): +def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, java_bin = "", jars_home_strip_suffix = "", coverage_helper = "", runfiles_helper = "", is_jruby = False, load_paths = []): toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] if ctx.attr.ruby != None: toolchain = ctx.attr.ruby[platform_common.ToolchainInfo] @@ -140,6 +141,7 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, "{locate_binary_in_runfiles}": locate_binary_in_runfiles, "{coverage_helper}": coverage_helper, "{runfiles_helper}": runfiles_helper, + "{load_paths}": " ".join(['"%s"' % path for path in load_paths]), "{is_jruby}": "true" if is_jruby else "", }, ) @@ -157,6 +159,7 @@ def rb_binary_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) transitive_deps = get_transitive_deps(ctx.attr.deps) + transitive_load_paths = get_transitive_load_paths([], ctx.attr.deps) transitive_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps) ruby_toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] @@ -229,6 +232,7 @@ def rb_binary_impl(ctx): coverage_helper = _to_rlocation_path(ctx, ctx.file._coverage_helper), runfiles_helper = _to_rlocation_path(ctx, ctx.file._runfiles_helper), is_jruby = ruby_toolchain.version.startswith("jruby"), + load_paths = transitive_load_paths.to_list(), ) return [ @@ -241,6 +245,7 @@ def rb_binary_impl(ctx): binary = executable, transitive_data = depset(tools, transitive = [transitive_data]), transitive_deps = transitive_deps, + transitive_load_paths = transitive_load_paths, transitive_srcs = transitive_srcs, ), RunEnvironmentInfo( diff --git a/ruby/private/binary/binary.sh.tpl b/ruby/private/binary/binary.sh.tpl index c14bdd24..6aaccc80 100644 --- a/ruby/private/binary/binary.sh.tpl +++ b/ruby/private/binary/binary.sh.tpl @@ -39,6 +39,12 @@ export RUNFILES_DIR="$(realpath "${RUNFILES_DIR:-$0.runfiles}")" # Set RUBYLIB to include the runfiles library. export RUBYLIB="$(dirname "$(dirname "$(rlocation "{runfiles_helper}")")")${RUBYLIB:+:${RUBYLIB}}" +# Set RUBYLIB to include load paths of dependencies (e.g. generated protobuf code). +# These are directories, which rlocation can't resolve, so join them to RUNFILES_DIR. +for load_path in {load_paths}; do + RUBYLIB="${RUNFILES_DIR}/${load_path}:${RUBYLIB}" +done + # Find location of Ruby in runfiles. export PATH=$(dirname $(rlocation {ruby})):$PATH diff --git a/ruby/private/library.bzl b/ruby/private/library.bzl index df87e2a7..b5fccb74 100644 --- a/ruby/private/library.bzl +++ b/ruby/private/library.bzl @@ -7,6 +7,7 @@ load( "RubyFilesInfo", "get_transitive_data", "get_transitive_deps", + "get_transitive_load_paths", "get_transitive_runfiles", "get_transitive_srcs", ) @@ -45,6 +46,7 @@ def _rb_library_impl(ctx): binary = None, transitive_data = depset(transitive_data), transitive_deps = depset(transitive_deps), + transitive_load_paths = get_transitive_load_paths([], ctx.attr.deps), transitive_srcs = depset(transitive_srcs), ), ] diff --git a/ruby/private/proto.bzl b/ruby/private/proto.bzl index cc56b326..cdddbe3a 100644 --- a/ruby/private/proto.bzl +++ b/ruby/private/proto.bzl @@ -5,7 +5,7 @@ This file is loaded by `rules_ruby` (see its `rb_library` implementation) using """ load("@protobuf//bazel/common:proto_info.bzl", "ProtoInfo") -load("//ruby/private:providers.bzl", "RubyFilesInfo") +load("//ruby/private:providers.bzl", "RubyFilesInfo", "get_transitive_load_paths") load(":proto_common.bzl", "proto_common") GRPC_PLUGIN_TOOLCHAIN = "@rules_ruby//ruby:ruby_grpc_protoc_plugin.toolchain_type" @@ -13,11 +13,43 @@ PROTO_TOOLCHAIN = "@protobuf//bazel/private:proto_toolchain_type" RUBY_OUTPUT_FILE = "{proto_file_basename}_pb.rb" RUBY_SERVICE_OUTPUT_FILE = "{proto_file_basename}_services_pb.rb" +# The google-protobuf gem already ships Ruby code for these, and generating our +# own copies would shadow the gem's on the load path. +PROTOBUF_WELL_KNOWN_TYPES_REPO = Label("@protobuf//:BUILD").repo_name + +def _ruby_files_info(ctx, srcs = [], load_paths = []): + deps = ctx.rule.attr.deps + return RubyFilesInfo( + transitive_srcs = depset(srcs, transitive = [ + dep[RubyFilesInfo].transitive_srcs + for dep in deps + if RubyFilesInfo in dep + ]), + transitive_load_paths = get_transitive_load_paths(load_paths, deps), + transitive_deps = depset(), + transitive_data = depset(), + ) + +def _runfiles_load_path(ctx, output_root, proto_outdir): + """Converts a protoc output directory into a path relative to the runfiles root.""" + path = proto_outdir.removeprefix(output_root.path).removeprefix("/") + if path.startswith("external/"): + return path.removeprefix("external/") + if path.startswith("../"): + return path.removeprefix("../") + return ctx.workspace_name + "/" + path if path else ctx.workspace_name + def _ruby_proto_aspect_impl(target, ctx): if not ProtoInfo in target: return [] proto_info = target[ProtoInfo] + + # Aggregator proto_library targets (deps only, no srcs) have nothing to compile. + is_aggregator = not proto_info.direct_sources + if is_aggregator or target.label.repo_name == PROTOBUF_WELL_KNOWN_TYPES_REPO: + return [_ruby_files_info(ctx)] + protoc_info = ctx.exec_groups["proto"].toolchains[PROTO_TOOLCHAIN].proto grpc_info = ctx.exec_groups["proto"].toolchains[GRPC_PLUGIN_TOOLCHAIN].proto msg_outputs = proto_common.declare_generated_files(ctx.actions, proto_info, "_pb.rb") @@ -64,14 +96,10 @@ def _ruby_proto_aspect_impl(target, ctx): ) return [ - RubyFilesInfo( - transitive_srcs = depset(msg_outputs + service_outputs, transitive = [ - dep[RubyFilesInfo].transitive_srcs - for dep in ctx.rule.attr.deps - if RubyFilesInfo in dep - ]), - transitive_deps = depset(), - transitive_data = depset(), + _ruby_files_info( + ctx, + srcs = msg_outputs + service_outputs, + load_paths = [_runfiles_load_path(ctx, msg_outputs[0].root, proto_outdir)], ), ] diff --git a/ruby/private/providers.bzl b/ruby/private/providers.bzl index ecc0569b..a95d03e6 100644 --- a/ruby/private/providers.bzl +++ b/ruby/private/providers.bzl @@ -6,6 +6,7 @@ RubyFilesInfo = provider( "binary": "Main Ruby script.", "transitive_data": "Transitive data files to add to runfiles.", "transitive_deps": "Transitive dependencies to get files from.", + "transitive_load_paths": "Transitive runfiles-relative directories to add to the Ruby load path.", "transitive_srcs": "Transitive Ruby files.", }, ) @@ -59,6 +60,25 @@ def get_transitive_data(data, deps): transitive = [dep[RubyFilesInfo].transitive_data for dep in deps], ) +def get_transitive_load_paths(load_paths, deps): + """Obtain the load path directories for a target and its transitive dependencies. + + Args: + load_paths: a list of runfiles-relative directories + deps: a list of targets that are direct dependencies + Returns: + a collection of the transitive load path directories + """ + return depset( + load_paths, + transitive = [ + # Not every RubyFilesInfo producer sets this field. + getattr(dep[RubyFilesInfo], "transitive_load_paths", None) or depset() + for dep in deps + if RubyFilesInfo in dep + ], + ) + def get_transitive_deps(deps): """Obtain the dependencies for a target and its transitive dependencies.