diff --git a/README.md b/README.md index 0e7766f..fb831c3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Note: we have a short guide on [Building your first Ruby Project](https://github.com/bazelruby/rules_ruby/wiki/Build-your-ruby-project) on the Wiki. We encourage you to check it out. -## Table of Contents +## Table of Contents - [Ruby Rules® for Bazel Build System](#ruby-rules-for-bazelhttpsbazelbuild-build-system) - [Build Status & Activity](#build-status-activity) @@ -99,6 +99,12 @@ load( ruby_bundle( name = "bundle", + # Specify additional paths to be loaded from the gems at runtime, if any. + # Since spec.require_paths in Gem specifications are auto-included, directory paths + # in spec.require_paths do not need to be listed in includes hash. + includes = { + "grpc": ["etc"], + }, excludes = { "mini_portile": ["test/**/*"], }, @@ -200,7 +206,7 @@ ruby_gem( "rubocop": "", }, srcs = [ - glob("{bin,exe,lib,spec}/**/*.rb") + glob("{bin,exe,lib,spec}/**/*.rb") ], deps = [ "//lib:example_gem", @@ -224,7 +230,7 @@ You will have to be sure to export the `ASDF_DATA_DIR` in your profile since it' ### Rule Dependency Diagram -> NOTE: this diagram is slightly outdated. +> NOTE: this diagram is somewhat outdated. The following diagram attempts to capture the implementation behind `ruby_library` that depends on the result of `bundle install`, and a `ruby_binary` that depends on both: @@ -447,7 +453,7 @@ ruby_test( size, timeout, flaky, - local, + local, shard_count ) ``` @@ -540,7 +546,8 @@ ruby_bundle( gemfile, gemfile_lock, bundler_version = "2.1.4", - excludes = [], + includes = {}, + excludes = {}, vendor_cache = False, ruby_sdk = "@org_ruby_lang_ruby_toolchain", ruby_interpreter = "@org_ruby_lang_ruby_toolchain//:ruby", @@ -598,6 +605,29 @@ ruby_bundle(

NOTE: This rule never updates the Gemfile.lock. It is your responsibility to generate/update Gemfile.lock

+ + includes + + Dictionary of key-value-pairs (key: string, value: list of strings), optional +

+ List of glob patterns per gem to be additionally loaded from the library. + Keys are the names of the gems which require some file/directory paths not listed in the require_paths attribute of the gemspecs to be also added to $LOAD_PATH at runtime. + Values are lists of blob path patterns, which are relative to the root directories of the gems. +

+ + + + excludes + + Dictionary of key-value-pairs (key: string, value: list of strings), optional +

+ List of glob patterns per gem to be excluded from the library. + Keys are the names of the gems. + Values are lists of blob path patterns, which are relative to the root directories of the gems. + The default value is ["**/* *.*", "**/* */*"] +

+ + @@ -857,17 +887,17 @@ ruby_gem( gem_description - String, required + String, required

Single-line, paragraph-sized description text for the gem.

gem_homepage - String, optional + String, optional

Homepage URL of the gem.

- + gem_authors @@ -886,7 +916,7 @@ ruby_gem( List of email addresses of the authors.

- + srcs @@ -917,7 +947,7 @@ ruby_gem( Typically this value is just `lib` (which is also the default).

- + gem_runtime_dependencies @@ -938,8 +968,8 @@ ruby_gem( testing gems, linters, code coverage and more.

- - + + @@ -972,13 +1002,14 @@ After that, cd into the top level folder and run the setup script in your Termin This runs a complete setup, shouldn't take too long. You can explore various script options with the `help` command: ```bash -❯ bin/setup help +❯ bin/setup -h + USAGE # without any arguments runs a complete setup. bin/setup # alternatively, a sub-setup function name can be passed: - bin/setup [ gems | git-hook | help | os-specific | main | remove-git-hook ] + bin/setup [ gems | git-hook | help | main | os-specific | rbenv | remove-git-hook ] DESCRIPTION: Runs full setup without any arguments. @@ -988,7 +1019,13 @@ DESCRIPTION: This action removes the git commit hook installed by the setup. EXAMPLES: - bin/setup — runs the entire setup. + bin/setup + + Or, to run only one of the sub-functions (actions), pass + it as an argument: + + bin/setup help + bin/setup remove-git-hook ``` #### OS-Specific Setup diff --git a/WORKSPACE b/WORKSPACE index 20e7927..0ebc4dd 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -19,6 +19,11 @@ local_repository( path = "ruby/tests/testdata/another_workspace", ) +local_repository( + name = "bazelruby_rules_ruby_ruby_tests_testdata_bundle_includes_workspace", + path = "ruby/tests/testdata/bundle_includes_workspace", +) + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") diff --git a/ruby/private/bundle/create_bundle_build_file.rb b/ruby/private/bundle/create_bundle_build_file.rb index 96779f0..db40418 100755 --- a/ruby/private/bundle/create_bundle_build_file.rb +++ b/ruby/private/bundle/create_bundle_build_file.rb @@ -33,14 +33,14 @@ srcs = glob( include = [ ".bundle/config", - "{gem_lib_files}", - "lib/ruby/{ruby_version}/specifications/{name}-{version}.gemspec", + {gem_lib_files}, + "{gem_spec}", {gem_binaries} ], exclude = {exclude}, ), deps = {deps}, - includes = ["lib/ruby/{ruby_version}/gems/{name}-{version}/lib"], + includes = [{gem_lib_paths}], ) GEM_TEMPLATE @@ -59,7 +59,11 @@ ALL_GEMS GEM_PATH = ->(ruby_version, gem_name, gem_version) do - "lib/ruby/#{ruby_version}/gems/#{gem_name}-#{gem_version}" + Dir.glob("lib/ruby/#{ruby_version}/gems/#{gem_name}-#{gem_version}*").first +end + +SPEC_PATH = ->(ruby_version, gem_name, gem_version) do + Dir.glob("lib/ruby/#{ruby_version}/specifications/#{gem_name}-#{gem_version}*.gemspec").first end require 'bundler' @@ -147,6 +151,7 @@ class BundleBuildFileGenerator :repo_name, :build_file, :gemfile_lock, + :includes, :excludes, :ruby_version @@ -158,11 +163,14 @@ def initialize(workspace_name:, repo_name:, build_file: 'BUILD.bazel', gemfile_lock: 'Gemfile.lock', - excludes: nil) + includes: nil, + excludes: nil, + additional_require_paths: nil) @workspace_name = workspace_name @repo_name = repo_name @build_file = build_file @gemfile_lock = gemfile_lock + @includes = includes @excludes = excludes # This attribute returns 0 as the third minor version number, which happens to be # what Ruby uses in the PATH to gems, eg. ruby 2.6.5 would have a folder called @@ -226,7 +234,16 @@ def remove_bundler_version! def register_gem(spec, template_out, bundle_lib_paths, bundle_binaries) gem_path = GEM_PATH[ruby_version, spec.name, spec.version] - bundle_lib_paths << gem_lib_path = gem_path + '/lib' + spec_path = SPEC_PATH[ruby_version, spec.name, spec.version] + base_dir = "lib/ruby/#{ruby_version}" + + # paths to register to $LOAD_PATH + require_paths = Gem::StubSpecification.gemspec_stub(spec_path, base_dir, "#{base_dir}/gems").require_paths + # Usually, registering the directory paths listed in the `require_paths` of gemspecs is sufficient, but + # some gems also require additional paths to be included in the load paths. + require_paths += include_array(spec.name) + gem_lib_paths = require_paths.map { |require_path| File.join(gem_path, require_path) } + bundle_lib_paths.push(*gem_lib_paths) # paths to search for executables gem_binaries = find_bundle_binaries(gem_path) @@ -237,8 +254,9 @@ def register_gem(spec, template_out, bundle_lib_paths, bundle_binaries) warn("registering gem #{spec.name} with binaries: #{gem_binaries}") if bundle_binaries.key?(spec.name) template_out.puts GEM_TEMPLATE - .gsub('{gem_lib_path}', gem_lib_path) - .gsub('{gem_lib_files}', gem_lib_path + '/**/*') + .gsub('{gem_lib_paths}', to_flat_string(gem_lib_paths)) + .gsub('{gem_lib_files}', to_flat_string(gem_lib_paths.map { |p| "#{p}/**/*" })) + .gsub('{gem_spec}', spec_path) .gsub('{gem_binaries}', to_flat_string(gem_binaries)) .gsub('{exclude}', exclude_array(spec.name).to_s) .gsub('{name}', spec.name) @@ -265,6 +283,10 @@ def find_bundle_binaries(gem_path) .map { |binary| 'bin/' + binary } end + def include_array(gem_name) + (includes[gem_name] || []) + end + def exclude_array(gem_name) (excludes[gem_name] || []) + DEFAULT_EXCLUDES end @@ -274,18 +296,19 @@ def to_flat_string(array) end end -# ruby ./create_bundle_build_file.rb "BUILD.bazel" "Gemfile.lock" "repo_name" "[]" "wsp_name" +# ruby ./create_bundle_build_file.rb "BUILD.bazel" "Gemfile.lock" "repo_name" "{}" "{}" "wsp_name" if $0 == __FILE__ - if ARGV.length != 5 - warn("USAGE: #{$0} BUILD.bazel Gemfile.lock repo-name [excludes-json] workspace-name".orange) + if ARGV.length != 6 + warn("USAGE: #{$0} BUILD.bazel Gemfile.lock repo-name {includes-json} {excludes-json} workspace-name".orange) exit(1) end - build_file, gemfile_lock, repo_name, excludes, workspace_name, * = *ARGV + build_file, gemfile_lock, repo_name, includes, excludes, workspace_name, * = *ARGV BundleBuildFileGenerator.new(build_file: build_file, gemfile_lock: gemfile_lock, repo_name: repo_name, + includes: JSON.parse(includes), excludes: JSON.parse(excludes), workspace_name: workspace_name).generate! diff --git a/ruby/private/bundle/def.bzl b/ruby/private/bundle/def.bzl index c3fd334..1e1243b 100644 --- a/ruby/private/bundle/def.bzl +++ b/ruby/private/bundle/def.bzl @@ -144,6 +144,7 @@ def generate_bundle_build_file(runtime_ctx, previous_result): "BUILD.bazel", # Bazel build file (can be empty) "Gemfile.lock", # Gemfile.lock where we list all direct and transitive dependencies runtime_ctx.ctx.name, # Name of the target + repr(runtime_ctx.ctx.attr.includes), repr(runtime_ctx.ctx.attr.excludes), RULES_RUBY_WORKSPACE_NAME, ] diff --git a/ruby/private/constants.bzl b/ruby/private/constants.bzl index 2730717..d615ea6 100644 --- a/ruby/private/constants.bzl +++ b/ruby/private/constants.bzl @@ -85,6 +85,9 @@ BUNDLE_ATTRS = { "bundler_version": attr.string( default = DEFAULT_BUNDLER_VERSION, ), + "includes": attr.string_list_dict( + doc = "List of glob patterns per gem to be additionally loaded from the library", + ), "excludes": attr.string_list_dict( doc = "List of glob patterns per gem to be excluded from the library", ), diff --git a/ruby/tests/testdata/bundle_includes_workspace/BUILD.bazel b/ruby/tests/testdata/bundle_includes_workspace/BUILD.bazel new file mode 100644 index 0000000..5df776a --- /dev/null +++ b/ruby/tests/testdata/bundle_includes_workspace/BUILD.bazel @@ -0,0 +1,15 @@ +load( + "@bazelruby_rules_ruby//ruby:defs.bzl", + "ruby_binary", +) + +package(default_visibility = ["//:__subpackages__"]) + +ruby_binary( + name = "script", + srcs = ["script.rb"], + main = "script.rb", + deps = [ + "@gems//:grpc", + ], +) diff --git a/ruby/tests/testdata/bundle_includes_workspace/Gemfile b/ruby/tests/testdata/bundle_includes_workspace/Gemfile new file mode 100644 index 0000000..9595a03 --- /dev/null +++ b/ruby/tests/testdata/bundle_includes_workspace/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'grpc' diff --git a/ruby/tests/testdata/bundle_includes_workspace/Gemfile.lock b/ruby/tests/testdata/bundle_includes_workspace/Gemfile.lock new file mode 100644 index 0000000..0417ff0 --- /dev/null +++ b/ruby/tests/testdata/bundle_includes_workspace/Gemfile.lock @@ -0,0 +1,18 @@ +GEM + remote: https://rubygems.org/ + specs: + google-protobuf (3.17.3) + googleapis-common-protos-types (1.1.0) + google-protobuf (~> 3.14) + grpc (1.38.0) + google-protobuf (~> 3.15) + googleapis-common-protos-types (~> 1.0) + +PLATFORMS + ruby + +DEPENDENCIES + grpc + +BUNDLED WITH + 2.2.22 diff --git a/ruby/tests/testdata/bundle_includes_workspace/WORKSPACE b/ruby/tests/testdata/bundle_includes_workspace/WORKSPACE new file mode 100644 index 0000000..c97411c --- /dev/null +++ b/ruby/tests/testdata/bundle_includes_workspace/WORKSPACE @@ -0,0 +1,35 @@ +workspace(name = "bazelruby_rules_ruby_ruby_tests_testdata_bundle_includes_workspace") + +local_repository( + name = "bazelruby_rules_ruby", + path = "../../../..", +) + +load( + "@bazelruby_rules_ruby//ruby:deps.bzl", + "rules_ruby_dependencies", + "rules_ruby_select_sdk", +) + +rules_ruby_dependencies() + +rules_ruby_select_sdk(version = "3.0.1") + +load("@bazelruby_rules_ruby//ruby:defs.bzl", "ruby_bundle") + +ruby_bundle( + name = "gems", + bundler_version = "2.2.21", + gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", + includes = { + # The gemspec of grpc gem lists ['src/ruby/bin', 'src/ruby/lib', 'src/ruby/pb'] as the `require_paths`. When installing + # pre-built versions of the gem using a package downloaded from rubygems.org, these paths are sufficient since the file + # `src/ruby/lib/grpc.rb` in the downloaded gem package does not `require` any file outside these directories. + # However, when installing grpc gem from source using Bundler, `src/ruby/lib/grpc.rb` in the source package does + # `require` 'etc/roots.pem', so the directory containing this `require`-d file also needs to be present in the `$LOAD_PATH`. + # Thus users have to manually add the 'etc' directory to the `$LOAD_PATH` using the `includes` option of `ruby_bundle` rule. + # The `includes` option of `ruby_bundle` rule is a means of workaround for such a peculiar situation. + "grpc": ["etc"], + }, +) diff --git a/ruby/tests/testdata/bundle_includes_workspace/script.rb b/ruby/tests/testdata/bundle_includes_workspace/script.rb new file mode 100644 index 0000000..c29eab5 --- /dev/null +++ b/ruby/tests/testdata/bundle_includes_workspace/script.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +expected_gem_require_paths = [ + 'etc', + 'src/ruby/bin', + 'src/ruby/lib', + 'src/ruby/pb' +] + +gem_require_paths = $LOAD_PATH.map do |load_path| + %r{.+script.runfiles/(?:gems|bundle)/lib/ruby/3.0.0/gems/grpc-.+?/(.+)}.match(load_path).to_a[1] +end.compact + +(expected_gem_require_paths - gem_require_paths).each do |missing_require_path| + raise "Expected requir_path '#{missing_require_path}' is missing in $LOAD_PATH." +end + +begin + require 'grpc' +rescue LoadError + $stderr.puts 'Failed to load grpc gem' + raise +end + +puts GRPC::RpcServer.new