diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cd6ad49..ec9d271e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,6 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true -env: - JAVA_OPTS: -Djdk.io.File.enableADS=true - jobs: ruleset: name: Ruleset @@ -48,6 +45,11 @@ jobs: - bzlmod - WORKSPACE exclude: + # JRuby with bzlmod fails with long path issues on Windows. + # See #64 + - os: windows + ruby: jruby-9.4.5.0 + mode: bzlmod # TruffleRuby doesn't work on Windows. - os: windows ruby: truffleruby-23.1.1 diff --git a/examples/gem/MODULE.bazel.lock b/MODULE.bazel.lock similarity index 96% rename from examples/gem/MODULE.bazel.lock rename to MODULE.bazel.lock index 46af70c5..120e7cc0 100644 --- a/examples/gem/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,6 +1,6 @@ { "lockFileVersion": 3, - "moduleFileHash": "927e5313256a133827ed4814eb4dacff4884c3f2b77a6047917ab5e542e95d23", + "moduleFileHash": "505c065918d99a227d7f5250dd0e3d27bbc3d6409f8623f35e32583718bd7a7d", "flags": { "cmdRegistries": [ "https://bcr.bazel.build/" @@ -13,87 +13,28 @@ "compatibilityMode": "ERROR" }, "localOverrideHashes": { - "bazel_tools": "922ea6752dc9105de5af957f7a99a6933c0a6a712d23df6aad16a9c399f7e787", - "rules_ruby": "505c065918d99a227d7f5250dd0e3d27bbc3d6409f8623f35e32583718bd7a7d" + "bazel_tools": "922ea6752dc9105de5af957f7a99a6933c0a6a712d23df6aad16a9c399f7e787" }, "moduleDepGraph": { "": { - "name": "", - "version": "", + "name": "rules_ruby", + "version": "0.0.0", "key": "", - "repoName": "", + "repoName": "rules_ruby", "executionPlatformsToRegister": [], - "toolchainsToRegister": [ - "@ruby_toolchains//:all" - ], - "extensionUsages": [ - { - "extensionBzlFile": "@rules_ruby//ruby:extensions.bzl", - "extensionName": "ruby", - "usingModule": "", - "location": { - "file": "@@//:MODULE.bazel", - "line": 10, - "column": 21 - }, - "imports": { - "ruby": "ruby", - "bundle": "bundle", - "ruby_toolchains": "ruby_toolchains" - }, - "devImports": [], - "tags": [ - { - "tagName": "toolchain", - "attributeValues": { - "name": "ruby", - "version_file": "//:.ruby-version" - }, - "devDependency": false, - "location": { - "file": "@@//:MODULE.bazel", - "line": 11, - "column": 15 - } - }, - { - "tagName": "bundle", - "attributeValues": { - "name": "bundle", - "srcs": [ - "//:Gemfile.lock", - "//:gem.gemspec", - "//:lib/gem/version.rb" - ], - "env": { - "BUNDLE_BUILD__FOO": "bar" - }, - "gemfile": "//:Gemfile", - "toolchain": "@ruby//:BUILD" - }, - "devDependency": false, - "location": { - "file": "@@//:MODULE.bazel", - "line": 16, - "column": 12 - } - } - ], - "hasDevUseExtension": false, - "hasNonDevUseExtension": true - } - ], + "toolchainsToRegister": [], + "extensionUsages": [], "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "rules_ruby": "rules_ruby@_", + "bazel_skylib": "bazel_skylib@1.3.0", + "platforms": "platforms@0.0.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" } }, - "bazel_skylib@1.5.0": { + "bazel_skylib@1.3.0": { "name": "bazel_skylib", - "version": "1.5.0", - "key": "bazel_skylib@1.5.0", + "version": "1.3.0", + "key": "bazel_skylib@1.3.0", "repoName": "bazel_skylib", "executionPlatformsToRegister": [], "toolchainsToRegister": [ @@ -110,30 +51,43 @@ "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "bazel_skylib~1.5.0", + "name": "bazel_skylib~1.3.0", "urls": [ - "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz" ], - "integrity": "sha256-zVWgYudjuTSZIfD124w5MyiNyLpPdt2UFqrGis7jy5Q=", + "integrity": "sha256-dNVE2W9KW7Yw1GXKi7z+Ix41lOWq5X4e2/F6brPKJQY=", "strip_prefix": "", "remote_patches": {}, "remote_patch_strip": 0 } } }, - "rules_ruby@_": { - "name": "rules_ruby", - "version": "0.0.0", - "key": "rules_ruby@_", - "repoName": "rules_ruby", + "platforms@0.0.7": { + "name": "platforms", + "version": "0.0.7", + "key": "platforms@0.0.7", + "repoName": "platforms", "executionPlatformsToRegister": [], "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", - "platforms": "platforms@0.0.7", + "rules_license": "rules_license@0.0.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" + }, + "repoSpec": { + "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "name": "platforms", + "urls": [ + "https://github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz" + ], + "integrity": "sha256-OlYcmee9vpFzqmU/1Xn+hJ8djWc5V4CrR3Cx84FDHVE=", + "strip_prefix": "", + "remote_patches": {}, + "remote_patch_strip": 0 + } } }, "bazel_tools@_": { @@ -283,16 +237,15 @@ "bazel_tools": "bazel_tools@_" } }, - "platforms@0.0.7": { - "name": "platforms", + "rules_license@0.0.7": { + "name": "rules_license", "version": "0.0.7", - "key": "platforms@0.0.7", - "repoName": "platforms", + "key": "rules_license@0.0.7", + "repoName": "rules_license", "executionPlatformsToRegister": [], "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "rules_license": "rules_license@0.0.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" }, @@ -300,11 +253,11 @@ "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", "ruleClassName": "http_archive", "attributes": { - "name": "platforms", + "name": "rules_license~0.0.7", "urls": [ - "https://github.com/bazelbuild/platforms/releases/download/0.0.7/platforms-0.0.7.tar.gz" + "https://github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz" ], - "integrity": "sha256-OlYcmee9vpFzqmU/1Xn+hJ8djWc5V4CrR3Cx84FDHVE=", + "integrity": "sha256-RTHezLkTY5ww5cdRKgVNXYdWmNrrddjPkPKEN1/nw2A=", "strip_prefix": "", "remote_patches": {}, "remote_patch_strip": 0 @@ -441,7 +394,7 @@ "deps": { "platforms": "platforms@0.0.7", "rules_cc": "rules_cc@0.0.9", - "bazel_skylib": "bazel_skylib@1.5.0", + "bazel_skylib": "bazel_skylib@1.3.0", "rules_proto": "rules_proto@4.0.0", "rules_license": "rules_license@0.0.7", "bazel_tools": "bazel_tools@_", @@ -462,33 +415,6 @@ } } }, - "rules_license@0.0.7": { - "name": "rules_license", - "version": "0.0.7", - "key": "rules_license@0.0.7", - "repoName": "rules_license", - "executionPlatformsToRegister": [], - "toolchainsToRegister": [], - "extensionUsages": [], - "deps": { - "bazel_tools": "bazel_tools@_", - "local_config_platform": "local_config_platform@_" - }, - "repoSpec": { - "bzlFile": "@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "name": "rules_license~0.0.7", - "urls": [ - "https://github.com/bazelbuild/rules_license/releases/download/0.0.7/rules_license-0.0.7.tar.gz" - ], - "integrity": "sha256-RTHezLkTY5ww5cdRKgVNXYdWmNrrddjPkPKEN1/nw2A=", - "strip_prefix": "", - "remote_patches": {}, - "remote_patch_strip": 0 - } - } - }, "rules_proto@4.0.0": { "name": "rules_proto", "version": "4.0.0", @@ -498,7 +424,7 @@ "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", + "bazel_skylib": "bazel_skylib@1.3.0", "rules_cc": "rules_cc@0.0.9", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" @@ -584,7 +510,7 @@ "toolchainsToRegister": [], "extensionUsages": [], "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", + "bazel_skylib": "bazel_skylib@1.3.0", "zlib": "zlib@1.3", "rules_python": "rules_python@0.4.0", "rules_cc": "rules_cc@0.0.9", @@ -675,7 +601,7 @@ } ], "deps": { - "bazel_skylib": "bazel_skylib@1.5.0", + "bazel_skylib": "bazel_skylib@1.3.0", "platforms": "platforms@0.0.7", "bazel_tools": "bazel_tools@_", "local_config_platform": "local_config_platform@_" @@ -743,6 +669,24 @@ } } }, + "@@bazel_tools//tools/osx:xcode_configure.bzl%xcode_configure_extension": { + "general": { + "bzlTransitiveDigest": "Qh2bWTU6QW6wkrd87qrU4YeY+SG37Nvw3A0PR4Y0L2Y=", + "accumulatedFileDigests": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_xcode": { + "bzlFile": "@@bazel_tools//tools/osx:xcode_configure.bzl", + "ruleClassName": "xcode_autoconf", + "attributes": { + "name": "bazel_tools~xcode_configure_extension~local_config_xcode", + "xcode_locator": "@bazel_tools//tools/osx:xcode_locator.m", + "remote_xcode": "" + } + } + } + } + }, "@@bazel_tools//tools/sh:sh_configure.bzl%sh_configure_extension": { "general": { "bzlTransitiveDigest": "hp4NgmNjEg5+xgvzfh6L83bt9/aiiWETuNpwNuF1MSU=", @@ -1298,50 +1242,6 @@ } } } - }, - "@@rules_ruby~override//ruby:extensions.bzl%ruby": { - "general": { - "bzlTransitiveDigest": "SQxPoUdQVlY1E52xJKuldVUzDBzhDAK9Le3Dihl5TZo=", - "accumulatedFileDigests": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "ruby_toolchains": { - "bzlFile": "@@rules_ruby~override//ruby/private/toolchain:repository_proxy.bzl", - "ruleClassName": "rb_toolchain_repository_proxy", - "attributes": { - "name": "rules_ruby~override~ruby~ruby_toolchains", - "toolchain": "@ruby//:toolchain", - "toolchain_type": "@rules_ruby//ruby:toolchain_type" - } - }, - "bundle": { - "bzlFile": "@@rules_ruby~override//ruby/private:bundle.bzl", - "ruleClassName": "rb_bundle", - "attributes": { - "toolchain": "@@rules_ruby~override~ruby~ruby//:BUILD", - "name": "rules_ruby~override~ruby~bundle", - "srcs": [ - "@@//:Gemfile.lock", - "@@//:gem.gemspec", - "@@//:lib/gem/version.rb" - ], - "env": { - "BUNDLE_BUILD__FOO": "bar" - }, - "gemfile": "@@//:Gemfile" - } - }, - "ruby": { - "bzlFile": "@@rules_ruby~override//ruby/private:download.bzl", - "ruleClassName": "rb_download", - "attributes": { - "name": "rules_ruby~override~ruby~ruby", - "version": "", - "version_file": "@@//:.ruby-version" - } - } - } - } } } } diff --git a/README.md b/README.md index fa63a0e8..a46161f3 100755 --- a/README.md +++ b/README.md @@ -32,12 +32,12 @@ rb_register_toolchains( ```bazel # WORKSPACE -load("@rules_ruby//ruby:deps.bzl", "rb_bundle") +load("@rules_ruby//ruby:deps.bzl", "rb_bundle_fetch") -rb_bundle( +rb_bundle_fetch( name = "bundle", - srcs = ["//:Gemfile.lock"], gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", ) ``` @@ -64,11 +64,10 @@ use_repo(ruby, "ruby") ```bazel # MODULE.bazel -ruby.bundle( +ruby.bundle_fetch( name = "bundle", - srcs = ["//:Gemfile.lock"], gemfile = "//:Gemfile", - toolchain = "@ruby//:BUILD", + gemfile_lock = "//:Gemfile.lock", ) use_repo(ruby, "bundle", "ruby_toolchains") ``` @@ -137,11 +136,10 @@ However, some are known not to work or work only partially (e.g. mRuby has no bu ## Known Issues * JRuby/TruffleRuby might need `HOME` variable exposed. - See [`eamples/gem/.bazelrc`][7] to learn how to do that. + See [`examples/gem/.bazelrc`][7] to learn how to do that. This is to be fixed in [`jruby/jruby#5661`][9] and [`oracle/truffleruby#2784`][10]. * JRuby might fail with `Errno::EACCES: Permission denied - NUL` error on Windows. You need to configure JDK to allow proper access. - See [`examples/gem/.bazelrc`][7] to learn how to do that. This is described in [`jruby/jruby#7182`][11]. * RuboCop < 1.55 crashes with `LoadError` on Windows. This is fixed in [`rubocop/rubocop#12062`][12]. diff --git a/docs/repository_rules.md b/docs/repository_rules.md index 21b0a314..b34eae2d 100644 --- a/docs/repository_rules.md +++ b/docs/repository_rules.md @@ -2,6 +2,65 @@ Public API for repository rules + + +## rb_bundle_fetch + +
+rb_bundle_fetch(name, env, gemfile, gemfile_lock, repo_mapping, srcs)
+
+ + +Fetches Bundler dependencies to be automatically installed by other targets. + +Currently doesn't support installing gems from Git repositories, +see https://github.com/bazel-contrib/rules_ruby/issues/62. + +`WORKSPACE`: +```bazel +load("@rules_ruby//ruby:deps.bzl", "rb_bundle_fetch") + +rb_bundle_fetch( + name = "bundle", + gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", + srcs = [ + "//:gem.gemspec", + "//:lib/gem/version.rb", + ] +) +``` + +All the installed gems can be accessed using `@bundle` target and additionally +gems binary files can also be used: + +`BUILD`: +```bazel +load("@rules_ruby//ruby:defs.bzl", "rb_test") + +package(default_visibility = ["//:__subpackages__"]) + +rb_test( + name = "rubocop", + main = "@bundle//bin:rubocop", + deps = ["@bundle"], +) +``` + + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this repository. | Name | required | | +| env | Environment variables to use during installation. | Dictionary: String -> String | optional | {} | +| gemfile | Gemfile to install dependencies from. | Label | required | | +| gemfile_lock | Gemfile.lock to install dependencies from. | Label | required | | +| repo_mapping | A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<p>For example, an entry "@foo": "@bar" declares that, for any time this repository depends on @foo (such as a dependency on @foo//some:target, it should actually resolve that dependency within globally-declared @bar (@bar//some:target). | Dictionary: String -> String | required | | +| srcs | List of Ruby source files necessary during installation. | List of labels | optional | [] | + + ## rb_bundle_rule @@ -11,6 +70,8 @@ rb_bundle_rule(name, toolchain, name, version, version_file, register, kwargs) +rb_register_toolchains(name, version, version_file, msys2_packages, register, kwargs) Register a Ruby toolchain and lazily download the Ruby Interpreter. @@ -91,7 +152,7 @@ rb_register_toolchains(name, name | base name of resulting repositories, by default "rules_ruby" | "ruby" | | version | a semver version of MRI, or a string like [interpreter type]-[version], or "system" | None | | version_file | .ruby-version or .tool-versions file to read version from | None | +| msys2_packages | extra MSYS2 packages to install | ["libyaml"] | | register | whether to register the resulting toolchains, should be False under bzlmod | True | | kwargs | additional parameters to the downloader for this interpreter type | none | diff --git a/docs/rules.md b/docs/rules.md index e6659cf1..29d0bc8a 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -123,7 +123,7 @@ package(default_visibility = ["//:__subpackages__"]) rb_binary( name = "rake", - main = "@bundle//:bin/rake", + main = "@bundle//bin:rake", deps = [ "//lib:gem", "@bundle", @@ -159,6 +159,58 @@ rake, version 10.5.0 | srcs | List of Ruby source files used to build the library. | List of labels | optional | [] | + + +## rb_bundle_install + +
+rb_bundle_install(name, env, gemfile, gemfile_lock, gems, srcs)
+
+ + +Installs Bundler dependencies from cached gems. + +You normally don't need to call this rule directly as it's an internal one +used by `rb_bundle_fetch()`. + + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| env | Environment variables to use during installation. | Dictionary: String -> String | 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 | | +| srcs | List of Ruby source files used to build the library. | List of labels | optional | [] | + + + + +## rb_gem + +
+rb_gem(name, gem)
+
+ + +Exposes a Ruby gem file. + +You normally don't need to call this rule directly as it's an internal one +used by `rb_bundle_fetch()`. + + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| gem | Gem file. | Label | required | | + + ## rb_gem_build @@ -264,6 +316,74 @@ INFO: Build completed successfully, 2 total actions | srcs | List of Ruby source files used to build the library. | List of labels | optional | [] | + + +## rb_gem_install + +
+rb_gem_install(name, gem)
+
+ + +Installs a built Ruby gem. + +Suppose you have the following Ruby gem, where `rb_library()` is used +in `BUILD` files to define the packages for the gem and `rb_gem_build()` is used +to build a Ruby gem package from the sources. + +```output +|-- BUILD +|-- Gemfile +|-- WORKSPACE +|-- gem.gemspec +`-- lib + |-- BUILD + |-- gem + | |-- BUILD + | |-- add.rb + | |-- subtract.rb + | `-- version.rb + `-- gem.rb +``` + +You can now install the built `.gem` file by defining a target: + +`BUILD`: +```bazel +load("@rules_ruby//ruby:defs.bzl", "rb_gem_build", "rb_gem_install") + +package(default_visibility = ["//:__subpackages__"]) + +rb_gem_build( + name = "gem-build", + gemspec = "gem.gemspec", + deps = ["//lib:gem"], +) + +rb_gem_install( + name = "gem-install", + gem = ":gem-build", +) +``` + +```output +$ bazel build :gem-install +INFO: Analyzed target //:gem-install (4 packages loaded, 82 targets configured). +INFO: From Installing bazel-out/darwin_arm64-fastbuild/bin/gem-build.gem (//:gem-install): +Successfully installed example-0.1.0 +1 gem installed +``` + + +**ATTRIBUTES** + + +| Name | Description | Type | Mandatory | Default | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| gem | Gem file to install. | Label | required | | + + ## rb_gem_push @@ -491,7 +611,7 @@ rb_test( name = "add", srcs = ["add_spec.rb"], args = ["spec/add_spec.rb"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [ ":spec_helper", "@bundle", @@ -502,7 +622,7 @@ rb_test( name = "subtract", srcs = ["subtract_spec.rb"], args = ["spec/subtract_spec.rb"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [ ":spec_helper", "@bundle", @@ -536,7 +656,7 @@ package(default_visibility = ["//:__subpackages__"]) rb_test( name = "rubocop", args = ["lib/"], - main = "@bundle//:bin/rubocop", + main = "@bundle//bin:rubocop", tags = ["no-sandbox"], deps = [ "//lib:gem", diff --git a/examples/gem/.bazelrc b/examples/gem/.bazelrc index 3d204603..0b8abb3f 100644 --- a/examples/gem/.bazelrc +++ b/examples/gem/.bazelrc @@ -198,10 +198,8 @@ test --test_verbose_timeout_warnings=false build --action_env=HOME test --test_env=HOME -# JRuby might fail with "Errno::EACCES: Permission denied - NUL" on Windows: -# https://github.com/jruby/jruby/issues/7182#issuecomment-1112953015. -build --action_env=JAVA_OPTS="-Djdk.io.File.enableADS=true" -test --test_env=JAVA_OPTS="-Djdk.io.File.enableADS=true" +# Not ready for the MODULE.lock file yet, as of Bazel 7.0.0 there are still some stability issues. +common --lockfile_mode=off # Allows to run tests with rdbg: # 1. Add breakpoint with `binding.break`. diff --git a/examples/gem/BUILD b/examples/gem/BUILD index 0ede04dd..4877deca 100644 --- a/examples/gem/BUILD +++ b/examples/gem/BUILD @@ -2,6 +2,7 @@ load( "@rules_ruby//ruby:defs.bzl", "rb_binary", "rb_gem_build", + "rb_gem_install", "rb_gem_push", "rb_test", ) @@ -10,7 +11,7 @@ package(default_visibility = ["//:__subpackages__"]) rb_binary( name = "rake", - main = "@bundle//:bin/rake", + main = "@bundle//bin:rake", deps = [ "//lib:gem", "@bundle", @@ -22,7 +23,7 @@ rb_test( size = "small", timeout = "moderate", # JRuby startup can be slow data = [".rubocop.yml"], - main = "@bundle//:bin/rubocop", + main = "@bundle//bin:rubocop", tags = ["no-sandbox"], deps = [ "//lib:gem", @@ -37,10 +38,12 @@ rb_test( rb_gem_build( name = "gem-build", gemspec = "gem.gemspec", - deps = [ - "//lib:gem", - "@bundle", - ], + deps = ["//lib:gem"], +) + +rb_gem_install( + name = "gem-install", + gem = ":gem-build", ) rb_gem_push( diff --git a/examples/gem/Gemfile.lock b/examples/gem/Gemfile.lock index e73785af..c58f5d87 100644 --- a/examples/gem/Gemfile.lock +++ b/examples/gem/Gemfile.lock @@ -86,4 +86,4 @@ DEPENDENCIES rubocop (~> 1.10, >= 1.55) BUNDLED WITH - 2.1.4 + 2.2.19 diff --git a/examples/gem/MODULE.bazel b/examples/gem/MODULE.bazel index 82faf805..02fdf11a 100644 --- a/examples/gem/MODULE.bazel +++ b/examples/gem/MODULE.bazel @@ -12,11 +12,9 @@ ruby.toolchain( name = "ruby", version_file = "//:.ruby-version", ) -use_repo(ruby, "ruby") -ruby.bundle( +ruby.bundle_fetch( name = "bundle", srcs = [ - "//:Gemfile.lock", "//:gem.gemspec", "//:lib/gem/version.rb", ], @@ -24,7 +22,7 @@ ruby.bundle( "BUNDLE_BUILD__FOO": "bar", }, gemfile = "//:Gemfile", - toolchain = "@ruby//:BUILD", + gemfile_lock = "//:Gemfile.lock", ) use_repo(ruby, "bundle", "ruby_toolchains") diff --git a/examples/gem/WORKSPACE b/examples/gem/WORKSPACE index 12e19385..f3e59301 100644 --- a/examples/gem/WORKSPACE +++ b/examples/gem/WORKSPACE @@ -13,16 +13,15 @@ http_archive( ], ) -load("@rules_ruby//ruby:deps.bzl", "rb_bundle", "rb_register_toolchains") +load("@rules_ruby//ruby:deps.bzl", "rb_bundle_fetch", "rb_register_toolchains") rb_register_toolchains( version_file = "//:.ruby-version", ) -rb_bundle( +rb_bundle_fetch( name = "bundle", srcs = [ - "//:Gemfile.lock", "//:gem.gemspec", "//:lib/gem/version.rb", ], @@ -30,4 +29,5 @@ rb_bundle( "BUNDLE_BUILD__FOO": "bar", }, gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", ) diff --git a/examples/gem/spec/BUILD b/examples/gem/spec/BUILD index 78b0614a..c09c9f38 100644 --- a/examples/gem/spec/BUILD +++ b/examples/gem/spec/BUILD @@ -17,7 +17,7 @@ rb_test( timeout = "moderate", # JRuby startup can be slow srcs = ["add_spec.rb"], args = ["spec/add_spec.rb"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [":spec_helper"], ) @@ -32,7 +32,7 @@ rb_test( "USER", # POSIX "USERNAME", # Windows ], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [":spec_helper"], ) @@ -43,7 +43,7 @@ rb_test( srcs = ["file_spec.rb"], args = ["spec/file_spec.rb"], data = ["support/file.txt"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [":spec_helper"], ) @@ -53,6 +53,6 @@ rb_test( timeout = "moderate", # JRuby startup can be slow srcs = ["subtract_spec.rb"], args = ["spec/subtract_spec.rb"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [":spec_helper"], ) diff --git a/examples/gem/spec/env_spec.rb b/examples/gem/spec/env_spec.rb index add838f9..442f8b10 100644 --- a/examples/gem/spec/env_spec.rb +++ b/examples/gem/spec/env_spec.rb @@ -10,4 +10,8 @@ specify do expect(ENV).to have_key('USER').or have_key('USERNAME') end + + specify do + expect(ENV.fetch('BUNDLE_BUILD__FOO')).to eq('bar') + end end diff --git a/ruby/BUILD b/ruby/BUILD index 2b0d9ea1..606e1897 100644 --- a/ruby/BUILD +++ b/ruby/BUILD @@ -14,7 +14,10 @@ bzl_library( srcs = ["defs.bzl"], deps = [ "//ruby/private:binary", + "//ruby/private:bundle_install", + "//ruby/private:gem", "//ruby/private:gem_build", + "//ruby/private:gem_install", "//ruby/private:gem_push", "//ruby/private:library", "//ruby/private:test", @@ -26,6 +29,7 @@ bzl_library( srcs = ["deps.bzl"], deps = [ "//ruby/private:bundle", + "//ruby/private:bundle_fetch", "//ruby/private:toolchain", ], ) diff --git a/ruby/defs.bzl b/ruby/defs.bzl index 28ac9c75..35515542 100644 --- a/ruby/defs.bzl +++ b/ruby/defs.bzl @@ -1,13 +1,19 @@ "Public API for rules" load("//ruby/private:binary.bzl", _rb_binary = "rb_binary") +load("//ruby/private:bundle_install.bzl", _rb_bundle_install = "rb_bundle_install") +load("//ruby/private:gem.bzl", _rb_gem = "rb_gem") load("//ruby/private:gem_build.bzl", _rb_gem_build = "rb_gem_build") +load("//ruby/private:gem_install.bzl", _rb_gem_install = "rb_gem_install") load("//ruby/private:gem_push.bzl", _rb_gem_push = "rb_gem_push") load("//ruby/private:library.bzl", _rb_library = "rb_library") load("//ruby/private:test.bzl", _rb_test = "rb_test") rb_binary = _rb_binary +rb_bundle_install = _rb_bundle_install +rb_gem = _rb_gem rb_gem_build = _rb_gem_build +rb_gem_install = _rb_gem_install rb_gem_push = _rb_gem_push rb_library = _rb_library rb_test = _rb_test diff --git a/ruby/deps.bzl b/ruby/deps.bzl index c48ac209..7b80bdef 100644 --- a/ruby/deps.bzl +++ b/ruby/deps.bzl @@ -1,6 +1,7 @@ "Public API for repository rules" load("//ruby/private:bundle.bzl", _rb_bundle = "rb_bundle") +load("//ruby/private:bundle_fetch.bzl", _rb_bundle_fetch = "rb_bundle_fetch") load("//ruby/private:toolchain.bzl", _rb_register_toolchains = "rb_register_toolchains") def rb_bundle(toolchain = "@ruby//:BUILD", **kwargs): @@ -17,4 +18,5 @@ def rb_bundle(toolchain = "@ruby//:BUILD", **kwargs): ) rb_register_toolchains = _rb_register_toolchains +rb_bundle_fetch = _rb_bundle_fetch rb_bundle_rule = _rb_bundle diff --git a/ruby/extensions.bzl b/ruby/extensions.bzl index 95ce26e8..9f63d6c7 100644 --- a/ruby/extensions.bzl +++ b/ruby/extensions.bzl @@ -1,7 +1,7 @@ "Module extensions used by bzlmod" load("//ruby/private:toolchain.bzl", "DEFAULT_RUBY_REPOSITORY") -load(":deps.bzl", "rb_bundle", "rb_register_toolchains") +load(":deps.bzl", "rb_bundle", "rb_bundle_fetch", "rb_register_toolchains") ruby_bundle = tag_class(attrs = { "name": attr.string(doc = "Resulting repository name for the bundle"), @@ -11,10 +11,19 @@ ruby_bundle = tag_class(attrs = { "toolchain": attr.label(), }) +ruby_bundle_fetch = tag_class(attrs = { + "name": attr.string(doc = "Resulting repository name for the bundle"), + "srcs": attr.label_list(), + "env": attr.string_dict(), + "gemfile": attr.label(), + "gemfile_lock": attr.label(), +}) + ruby_toolchain = tag_class(attrs = { "name": attr.string(doc = "Base name for generated repositories, allowing multiple to be registered."), "version": attr.string(doc = "Explicit version of ruby."), "version_file": attr.label(doc = "File to read Ruby version from."), + "msys2_packages": attr.string_list(doc = "Extra MSYS2 packages to install.", default = ["libyaml"]), }) def _ruby_module_extension(module_ctx): @@ -29,6 +38,15 @@ def _ruby_module_extension(module_ctx): toolchain = bundle.toolchain, ) + for bundle_fetch in mod.tags.bundle_fetch: + rb_bundle_fetch( + name = bundle_fetch.name, + srcs = bundle_fetch.srcs, + env = bundle_fetch.env, + gemfile = bundle_fetch.gemfile, + gemfile_lock = bundle_fetch.gemfile_lock, + ) + for toolchain in mod.tags.toolchain: # Prevent a users dependencies creating conflicting toolchain names if toolchain.name != DEFAULT_RUBY_REPOSITORY and not mod.is_root: @@ -38,20 +56,21 @@ def _ruby_module_extension(module_ctx): if toolchain.version == registrations[toolchain.name]: # No problem to register a matching toolchain twice continue - fail("Multiple conflicting toolchains declared for name {} ({} and {}".format( + fail("Multiple conflicting toolchains declared for name {} ({}, {}) and {}".format( toolchain.name, toolchain.version, toolchain.version_file, registrations[toolchain.name], )) else: - registrations[toolchain.name] = (toolchain.version, toolchain.version_file) + registrations[toolchain.name] = (toolchain.version, toolchain.version_file, toolchain.msys2_packages) - for name, (version, version_file) in registrations.items(): + for name, (version, version_file, msys2_packages) in registrations.items(): rb_register_toolchains( name = name, version = version, version_file = version_file, + msys2_packages = msys2_packages, register = False, ) @@ -59,6 +78,7 @@ ruby = module_extension( implementation = _ruby_module_extension, tag_classes = { "bundle": ruby_bundle, + "bundle_fetch": ruby_bundle_fetch, "toolchain": ruby_toolchain, }, ) diff --git a/ruby/private/BUILD b/ruby/private/BUILD index bc32b1ec..5dd6b254 100644 --- a/ruby/private/BUILD +++ b/ruby/private/BUILD @@ -1,7 +1,5 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -exports_files(["gem_build/gem_builder.rb.tpl"]) - bzl_library( name = "binary", srcs = ["binary.bzl"], @@ -9,7 +7,7 @@ bzl_library( deps = [ ":library", ":providers", - "//ruby/private/binary:rlocation", + ":utils", ], ) @@ -20,6 +18,7 @@ bzl_library( deps = [ ":library", ":providers", + ":utils", ], ) @@ -60,6 +59,44 @@ bzl_library( ], ) +bzl_library( + name = "bundle_fetch", + srcs = ["bundle_fetch.bzl"], + visibility = ["//ruby:__subpackages__"], + deps = [ + ":utils", + "//ruby/private/bundle_fetch:gemfile_lock_parser", + ], +) + +bzl_library( + name = "bundle_install", + srcs = ["bundle_install.bzl"], + visibility = ["//ruby:__subpackages__"], + deps = [ + ":gem_install", + ":providers", + ":utils", + ], +) + +bzl_library( + name = "gem", + srcs = ["gem.bzl"], + visibility = ["//ruby:__subpackages__"], + deps = [":providers"], +) + +bzl_library( + name = "gem_install", + srcs = ["gem_install.bzl"], + visibility = ["//ruby:__subpackages__"], + deps = [ + ":providers", + ":utils", + ], +) + bzl_library( name = "bundle", srcs = ["bundle.bzl"], @@ -77,3 +114,9 @@ bzl_library( srcs = ["providers.bzl"], visibility = ["//ruby:__subpackages__"], ) + +bzl_library( + name = "utils", + srcs = ["utils.bzl"], + visibility = ["//ruby:__subpackages__"], +) diff --git a/ruby/private/binary.bzl b/ruby/private/binary.bzl index 4cd69dca..640ef8a7 100644 --- a/ruby/private/binary.bzl +++ b/ruby/private/binary.bzl @@ -3,6 +3,7 @@ load("//ruby/private:library.bzl", LIBRARY_ATTRS = "ATTRS") load( "//ruby/private:providers.bzl", + "BundlerInfo", "RubyFilesInfo", "get_bundle_env", "get_transitive_data", @@ -10,7 +11,14 @@ load( "get_transitive_runfiles", "get_transitive_srcs", ) -load("//ruby/private/binary:rlocation.bzl", "BASH_RLOCATION_FUNCTION", "BATCH_RLOCATION_FUNCTION") +load( + "//ruby/private:utils.bzl", + "BASH_RLOCATION_FUNCTION", + "BATCH_RLOCATION_FUNCTION", + _convert_env_to_script = "convert_env_to_script", + _is_windows = "is_windows", + _normalize_path = "normalize_path", +) ATTRS = { "main": attr.label( @@ -47,34 +55,27 @@ Use a built-in `args` attribute to pass extra arguments to the script. ), } -_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 = [], 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"] - toolchain_bindir = toolchain.bindir if binary: binary_path = binary.short_path else: binary_path = "" - if is_windows: - binary_path = binary_path.replace("/", "\\") - export_command = _EXPORT_BATCH_COMMAND + environment = {} + environment.update(env) + if _is_windows(ctx): 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 + environment.update({"PATH": _normalize_path(ctx, toolchain.bindir) + ";%PATH%"}) 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 + environment.update({"PATH": "%s:$PATH" % toolchain.bindir}) if bundler: bundler_command = "bundle exec" @@ -84,20 +85,14 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, 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, is_executable = True, substitutions = { "{args}": args, - "{binary}": binary_path, - "{toolchain_bindir}": toolchain_bindir, - "{env}": "\n".join(environment), + "{binary}": _normalize_path(ctx, binary_path), + "{env}": _convert_env_to_script(ctx, environment), "{bundler_command}": bundler_command, "{ruby_binary_name}": toolchain.ruby.basename, "{java_bin}": java_bin, @@ -119,23 +114,37 @@ def rb_binary_impl(ctx): transitive_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps).to_list() ruby_toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] - tools = [ruby_toolchain.ruby, ruby_toolchain.bundle, ruby_toolchain.gem] + tools = [ruby_toolchain.ruby, ruby_toolchain.bundle, ruby_toolchain.gem, ctx.file._runfiles_library] if ruby_toolchain.version.startswith("jruby"): 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 + # TODO: Remove workspace name check together with `rb_bundle()` if dep.label.workspace_name.endswith("bundle"): bundler = True + if BundlerInfo in dep: + info = dep[BundlerInfo] + transitive_srcs.extend([info.gemfile, info.bin, info.path]) + bundler = True + + # See https://bundler.io/v2.5/man/bundle-config.1.html for confiugration keys. + env.update({ + "BUNDLE_GEMFILE": info.gemfile.short_path.removeprefix("../"), + "BUNDLE_PATH": info.path.short_path.removeprefix("../"), + }) + bundle_env = get_bundle_env(ctx.attr.env, ctx.attr.deps) env.update(bundle_env) + env.update(ruby_toolchain.env) env.update(ctx.attr.env) + runfiles = ctx.runfiles(transitive_srcs + transitive_data + tools) + runfiles = get_transitive_runfiles(runfiles, ctx.attr.srcs, ctx.attr.deps, ctx.attr.data) + script = generate_rb_binary_script( ctx, ctx.executable.main, @@ -144,9 +153,6 @@ def rb_binary_impl(ctx): 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, @@ -291,7 +297,7 @@ package(default_visibility = ["//:__subpackages__"]) rb_binary( name = "rake", - main = "@bundle//:bin/rake", + main = "@bundle//bin:rake", deps = [ "//lib:gem", "@bundle", diff --git a/ruby/private/binary/BUILD b/ruby/private/binary/BUILD index 4f09aefc..0bb1cb2e 100644 --- a/ruby/private/binary/BUILD +++ b/ruby/private/binary/BUILD @@ -1,9 +1,4 @@ -load("@bazel_skylib//:bzl_library.bzl", "bzl_library") - -exports_files(glob(["*.tpl"])) - -bzl_library( - name = "rlocation", - srcs = ["rlocation.bzl"], - visibility = ["//ruby:__subpackages__"], -) +exports_files([ + "binary.cmd.tpl", + "binary.sh.tpl", +]) diff --git a/ruby/private/binary/binary.cmd.tpl b/ruby/private/binary/binary.cmd.tpl index 98a7f4c8..a4c4b41d 100644 --- a/ruby/private/binary/binary.cmd.tpl +++ b/ruby/private/binary/binary.cmd.tpl @@ -1,16 +1,23 @@ @echo off setlocal enableextensions enabledelayedexpansion +set RUNFILES_MANIFEST_ONLY=1 +{rlocation_function} + :: 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 + for %%a in ("!java_bin!\..\..") do set JAVA_HOME=%%~fa ) :: Set environment variables. -set PATH={toolchain_bindir};%PATH% {env} +if "{bundler_command}" neq "" ( + call :rlocation "!BUNDLE_GEMFILE!" BUNDLE_GEMFILE + call :rlocation "!BUNDLE_PATH!" BUNDLE_PATH +) + {bundler_command} {ruby_binary_name} {binary} {args} %* + +:: vim: ft=dosbatch diff --git a/ruby/private/binary/binary.sh.tpl b/ruby/private/binary/binary.sh.tpl index ec323110..25f1cd64 100644 --- a/ruby/private/binary/binary.sh.tpl +++ b/ruby/private/binary/binary.sh.tpl @@ -1,13 +1,36 @@ #!/usr/bin/env bash +{rlocation_function} + +# Provide a realpath implementation for macOS. +realpath() ( + OURPWD=$PWD + cd "$(dirname "$1")" + LINK=$(readlink "$(basename "$1")") + while [ "$LINK" ]; do + cd "$(dirname "$LINK")" + LINK=$(readlink "$(basename "$1")") + done + REALPATH="$PWD/$(basename "$1")" + cd "$OURPWD" + echo "$REALPATH" +) + +export RUNFILES_DIR="$(realpath "${RUNFILES_DIR:-$0.runfiles}")" + # 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} $@ +if [ -n "{bundler_command}" ]; then + export BUNDLE_GEMFILE=$(rlocation $BUNDLE_GEMFILE) + export BUNDLE_PATH=$(rlocation $BUNDLE_PATH) +fi + +{bundler_command} {ruby_binary_name} {binary} {args} "$@" + +# vim: ft=bash diff --git a/ruby/private/bundle.bzl b/ruby/private/bundle.bzl index 5d65fb68..af119b52 100644 --- a/ruby/private/bundle.bzl +++ b/ruby/private/bundle.bzl @@ -6,7 +6,16 @@ _BINSTUB_CMD = """@ruby -x "%~f0" %* {} """ +_DEPRECATED_MESSAGE = """ + +rb_bundle(...) is deprecated and will be removed in the future versions. +rb_bundle_fetch(...) should be used instead. + +""" + def _rb_bundle_impl(repository_ctx): + print(_DEPRECATED_MESSAGE) # buildifier: disable=print + binstubs_path = repository_ctx.path("bin") bundle_path = repository_ctx.path(".") gemfile_path = repository_ctx.path(repository_ctx.attr.gemfile) @@ -96,6 +105,8 @@ Gemfile to install dependencies from. ), }, doc = """ +(Deprecated) Use `rb_bundle_fetch()` instead. + Installs Bundler dependencies and registers an external repository that can be used by other targets. diff --git a/ruby/private/bundle/BUILD.tpl b/ruby/private/bundle/BUILD.tpl index 7f3c5a1d..b1ccc575 100644 --- a/ruby/private/bundle/BUILD.tpl +++ b/ruby/private/bundle/BUILD.tpl @@ -1,4 +1,5 @@ """@generated by @rules_ruby//:ruby/private/binary.bzl""" + load("@rules_ruby//ruby:defs.bzl", "rb_library") load("//:defs.bzl", "BUNDLE_ENV") @@ -6,6 +7,8 @@ package(default_visibility = ["//visibility:public"]) rb_library( name = "bundle", - data = glob(["**/*"]), bundle_env = BUNDLE_ENV, + data = glob(["**/*"]), ) + +# vim: ft=bzl diff --git a/ruby/private/bundle_fetch.bzl b/ruby/private/bundle_fetch.bzl new file mode 100644 index 00000000..3f7b40b8 --- /dev/null +++ b/ruby/private/bundle_fetch.bzl @@ -0,0 +1,228 @@ +"Implementation details for fetch the bundler" + +load( + "//ruby/private:utils.bzl", + _join_and_indent = "join_and_indent", + _normalize_bzlmod_repository_name = "normalize_bzlmod_repository_name", +) +load("//ruby/private/bundle_fetch:gemfile_lock_parser.bzl", "parse_gemfile_lock") + +_GEM_BUILD_FRAGMENT = """ +rb_gem( + name = "{name}", + gem = "{cache_path}/{gem}", +) +""" + +_GEM_INSTALL_BUILD_FRAGMENT = """ +rb_gem_install( + name = "{name}", + gem = "{cache_path}/{gem}", +) +""" + +_GIT_UNSUPPORTED_ERROR = """ + +rb_bundle_fetch(...) does not support gems installed from Git yet. +See https://github.com/bazel-contrib/rules_ruby/issues/62 for more details. + +""" + +_OUTDATED_BUNDLER_ERROR = """ + +rb_bundle_fetch(...) requires Bundler 2.2.19 or later in Gemfile.lock. +Please update Bundler version and try again. +See https://github.com/rubygems/rubygems/issues/4620 for more details. + +""" + +def _is_outdated_bundler(version): + """Checks that Bundler version is 2.2.19+. + + Older versions don't work with cached gems properly. + See https://github.com/rubygems/rubygems/issues/4620 for more details. + """ + major, minor, patch = version.split(".") + return (int(major) < 2 or int(minor) < 2 or int(patch) < 19) + +def _download_gem(repository_ctx, gem, cache_path): + """Downloads gem into a predefined vendor/cache location.""" + url = "{remote}gems/{filename}".format(remote = gem.remote, filename = gem.filename) + repository_ctx.download(url = url, output = "%s/%s" % (cache_path, gem.filename)) + +def _get_gem_executables(repository_ctx, gem, cache_path): + """Unpacks downloaded gem and returns its executables. + + Ideally, we would read the list of executables from gem metadata.gz, + which is a compressed YAML file. It has a separate `executables` field + containing the exact list of files. However, Bazel cannot decompress `.gz` + files, so we would need to use an external tool such as `gzcat` or `busybox`. + The tool should also work on all OSes. For now, a simpler path is taken + where we unpack the gem completely and then try to determin executables + by looking into its `bin` and `exe` locations. This is accurate enough so far, + so some exotic gems might not work correctly. + """ + executables = [] + repository_ctx.symlink(cache_path + "/" + gem.filename, gem.filename + ".tar") + repository_ctx.extract(gem.filename + ".tar", output = gem.full_name) + data = "/".join([gem.full_name, "data"]) + repository_ctx.extract("/".join([gem.full_name, "data.tar.gz"]), output = data) + gem_contents = repository_ctx.path(data) + + executable_dirnames = ["bin", "exe"] + for executable_dirname in executable_dirnames: + if gem_contents.get_child(executable_dirname).exists: + for executable in gem_contents.get_child(executable_dirname).readdir(): + executables.append(executable.basename) + + _cleanup_downloads(repository_ctx, gem) + return executables + +def _cleanup_downloads(repository_ctx, gem): + """Removes unnecessary downloaded/unpacked files.""" + repository_ctx.delete(gem.full_name) + repository_ctx.delete(gem.filename + ".tar") + +def _rb_bundle_fetch_impl(repository_ctx): + # Define vendor/cache relative to the location of Gemfile. + # This is expected by Bundler to operate correctly. + gemfile_dir = repository_ctx.attr.gemfile.name.rpartition("/")[0] + cache_path = ("%s/vendor/cache" % gemfile_dir).removeprefix("/") + + # Copy all necessary inputs to the repository. + gemfile_path = repository_ctx.path(repository_ctx.attr.gemfile) + gemfile_lock_path = repository_ctx.path(repository_ctx.attr.gemfile_lock) + repository_ctx.file(repository_ctx.attr.gemfile.name, repository_ctx.read(gemfile_path)) + repository_ctx.file(repository_ctx.attr.gemfile_lock.name, repository_ctx.read(gemfile_lock_path)) + srcs = [] + for src in repository_ctx.attr.srcs: + srcs.append(src.name) + repository_ctx.file(src.name, repository_ctx.read(src)) + + gemfile_lock = parse_gemfile_lock(repository_ctx.read(gemfile_lock_path)) + if _is_outdated_bundler(gemfile_lock.bundler.version): + fail(_OUTDATED_BUNDLER_ERROR) + + if len(gemfile_lock.git_packages) > 0: + fail(_GIT_UNSUPPORTED_ERROR) + + executables = [] + gem_full_names = [] + gem_fragments = [] + gem_install_fragments = [] + + # Fetch gems and expose them as `rb_gem()` targets. + for gem in gemfile_lock.remote_packages: + _download_gem(repository_ctx, gem, cache_path) + executables.extend(_get_gem_executables(repository_ctx, gem, cache_path)) + gem_full_names.append(":%s" % gem.full_name) + gem_fragments.append(_GEM_BUILD_FRAGMENT.format(name = gem.full_name, gem = gem.filename, cache_path = cache_path)) + + # Fetch Bundler and define an `rb_gem_install()` target for it. + _download_gem(repository_ctx, gemfile_lock.bundler, cache_path) + executables.extend(_get_gem_executables(repository_ctx, gemfile_lock.bundler, cache_path)) + gem_full_names.append(":%s" % gemfile_lock.bundler.full_name) + gem_install_fragments.append( + _GEM_INSTALL_BUILD_FRAGMENT.format( + name = gemfile_lock.bundler.full_name, + gem = gemfile_lock.bundler.filename, + cache_path = cache_path, + ), + ) + + repository_ctx.template( + "BUILD", + repository_ctx.attr._build_tpl, + executable = False, + substitutions = { + "{name}": _normalize_bzlmod_repository_name(repository_ctx.name), + "{srcs}": _join_and_indent(srcs), + "{gemfile_path}": repository_ctx.attr.gemfile.name, + "{gemfile_lock_path}": repository_ctx.attr.gemfile_lock.name, + "{gems}": _join_and_indent(gem_full_names), + "{gem_fragments}": "".join(gem_fragments), + "{gem_install_fragments}": "".join(gem_install_fragments), + "{env}": repr(repository_ctx.attr.env), + }, + ) + + # Create `bin` package with shims for gem executables. + # This allows targets to depend on `@bundle//bin:rake`. + repository_ctx.template( + "bin/BUILD", + repository_ctx.attr._bin_build_tpl, + executable = False, + substitutions = { + "{name}": _normalize_bzlmod_repository_name(repository_ctx.name), + }, + ) + for executable in executables: + repository_ctx.file("bin/%s" % executable) + +rb_bundle_fetch = repository_rule( + implementation = _rb_bundle_fetch_impl, + attrs = { + "gemfile": attr.label( + allow_single_file = ["Gemfile"], + mandatory = True, + doc = "Gemfile to install dependencies from.", + ), + "gemfile_lock": attr.label( + allow_single_file = ["Gemfile.lock"], + mandatory = True, + doc = "Gemfile.lock to install dependencies from.", + ), + "srcs": attr.label_list( + allow_files = True, + doc = "List of Ruby source files necessary during installation.", + ), + "env": attr.string_dict( + doc = "Environment variables to use during installation.", + ), + "_build_tpl": attr.label( + allow_single_file = True, + default = "@rules_ruby//:ruby/private/bundle_fetch/BUILD.tpl", + ), + "_bin_build_tpl": attr.label( + allow_single_file = True, + default = "@rules_ruby//:ruby/private/bundle_fetch/bin/BUILD.tpl", + ), + }, + doc = """ +Fetches Bundler dependencies to be automatically installed by other targets. + +Currently doesn't support installing gems from Git repositories, +see https://github.com/bazel-contrib/rules_ruby/issues/62. + +`WORKSPACE`: +```bazel +load("@rules_ruby//ruby:deps.bzl", "rb_bundle_fetch") + +rb_bundle_fetch( + name = "bundle", + gemfile = "//:Gemfile", + gemfile_lock = "//:Gemfile.lock", + srcs = [ + "//:gem.gemspec", + "//:lib/gem/version.rb", + ] +) +``` + +All the installed gems can be accessed using `@bundle` target and additionally +gems binary files can also be used: + +`BUILD`: +```bazel +load("@rules_ruby//ruby:defs.bzl", "rb_test") + +package(default_visibility = ["//:__subpackages__"]) + +rb_test( + name = "rubocop", + main = "@bundle//bin:rubocop", + deps = ["@bundle"], +) +``` + """, +) diff --git a/ruby/private/bundle_fetch/BUILD b/ruby/private/bundle_fetch/BUILD new file mode 100644 index 00000000..38556333 --- /dev/null +++ b/ruby/private/bundle_fetch/BUILD @@ -0,0 +1,7 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "gemfile_lock_parser", + srcs = ["gemfile_lock_parser.bzl"], + visibility = ["//ruby:__subpackages__"], +) diff --git a/ruby/private/bundle_fetch/BUILD.tpl b/ruby/private/bundle_fetch/BUILD.tpl new file mode 100644 index 00000000..9ff35853 --- /dev/null +++ b/ruby/private/bundle_fetch/BUILD.tpl @@ -0,0 +1,20 @@ +"""@generated by @rules_ruby//:ruby/private/bundle_fetch.bzl""" + +load("@rules_ruby//ruby:defs.bzl", "rb_bundle_install", "rb_gem", "rb_gem_install") + +package(default_visibility = ["//visibility:public"]) + +rb_bundle_install( + name = "{name}", + srcs = {srcs}, + env = {env}, + gemfile = "{gemfile_path}", + gemfile_lock = "{gemfile_lock_path}", + gems = {gems}, +) + +{gem_fragments} + +{gem_install_fragments} + +# vim: ft=bzl diff --git a/ruby/private/bundle_fetch/bin/BUILD.tpl b/ruby/private/bundle_fetch/bin/BUILD.tpl new file mode 100644 index 00000000..cc21f406 --- /dev/null +++ b/ruby/private/bundle_fetch/bin/BUILD.tpl @@ -0,0 +1,13 @@ +"""@generated by @rules_ruby//:ruby/private/bundle_fetch.bzl""" + +load("@rules_ruby//ruby:defs.bzl", "rb_library") + +package(default_visibility = ["//visibility:public"]) + +rb_library( + name = "bin", + data = glob(["*"]), + deps = ["//:{name}"], +) + +# vim: ft=bzl diff --git a/ruby/private/bundle_fetch/gemfile_lock_parser.bzl b/ruby/private/bundle_fetch/gemfile_lock_parser.bzl new file mode 100644 index 00000000..7a17ef85 --- /dev/null +++ b/ruby/private/bundle_fetch/gemfile_lock_parser.bzl @@ -0,0 +1,191 @@ +""" +Parses a Gemfile.lock purely in Starlark. + +Largely based on https://github.com/sushain97/rules_ruby/blob/master/tools/ruby/gemfile_parser.bzl (private). +Modifications include: + - Support for parsing out the gem remote URL. + - Usage of structs with extra fields as return values. +""" + +def _parse_package(line, remote): + """Parses an exact package specification from a single line of a Gemfile.lock. + + The Gemfile.lock format uses two spaces for each level of indentation. The + lines that we're interested in are nested underneath the `GEM.specs` + section (the concrete gems required and their exact versions). + + The lines that are parsed out of the 'GEM.specs' section will have four + leading spaces, and consist of the package name and exact version needed + in parenthesis. + + > gem-name (gem-version) + + What's returned is a struct that has the following fields in this case: + + > struct( + > name = "gem-name", + > version = "gem-version", + > full_name = "gem-name-gem-version", + > filename = "gem-name-gem-version.gem", + > remote = "https://rubygems.org" + > ) + + If the line does not match that format, `None` is returned. + """ + + prefix = line[0:4] + if not prefix.isspace(): + return None + + suffix = line[4:] + if suffix[0].isspace(): + return None + + version_start = suffix.find(" (") + if version_start < 0: + return None + + package = suffix[0:version_start] + version = suffix[version_start + 2:-1] + + return struct( + name = package, + version = version, + filename = "%s-%s.gem" % (package, version), + full_name = "%s-%s" % (package, version), + remote = remote, + ) + +def _parse_top_section(line): + """Parse a top-level section name. + + Returns a top-level section name ("PATH", "GEM", "PLATFORMS", + "DEPENDENCIES", etc.), or `None` if the line is empty or contains leading + space. + """ + + if line == "" or line[0].isspace(): + return None + + return line + +def _parse_remote(line): + """Parse a remote URL for packages. + + An example line is: + > remote: https://rubygems.org/ + """ + prefix = " remote: " + if line.startswith(prefix): + return line.removeprefix(prefix).strip() + + return None + +def _parse_git_package(lines): + """Parse a Git specification from several lines of a Gemfile.lock. + + The relevant lines begin with either `remote` or `revision`. + + > remote: path:to/remote.git + > revision: rev + + What's returned is a dict that will have two fields: + + > { "revision": "rev", "remote": "path:to/remote.git" } + + in this case. + + If the line does not match that format, an error is raised. + """ + remote = None + revision = None + + for line in lines: + if "remote: " in line: + remote = line.split(":", 1)[1].strip() + elif "revision: " in line: + revision = line.split(":", 1)[1].strip() + + if revision == None or remote == None: + fail("Unable to parse git package from gemfile: {}. Found remote={}, revision={}.".format(lines, remote, revision)) + + return {"revision": revision, "remote": remote} + +def parse_gemfile_lock(content): + """Parses a Gemfile.lock. + + Find lines in the content of a Gemfile.lock that look like package + constraints. + + Args: + content: Gemfile.lock contents + + Returns: + struct with parsed Gemfile.lock + """ + + remote_packages = [] + git_packages = [] + bundler = None + remote = None + + inside_gem = False + inside_git = False + inside_bundled_with = False + + git_lines = [] + + for line in content.splitlines(): + top_section = _parse_top_section(line) + if top_section != None: + # Toggle gem specification parsing. + if top_section == "GEM": + inside_gem = True + remote = None + + # Toggle bundler version parsing. Skip to the next line which + # has the actual version. + inside_bundled_with = (top_section == "BUNDLED WITH") + if inside_bundled_with: + continue + + # Toggle git specification parsing. + inside_git = (top_section == "GIT") + + # Only parse gem specifications from the GEM section. + if inside_gem: + if remote: + info = _parse_package(line, remote) + if info != None: + remote_packages.append(info) + else: + remote = _parse_remote(line) + + # Only parse git specifications from the GIT section. + if inside_git: + if line == "": + # The git section is complete, parse its information. + git_packages.append(_parse_git_package(git_lines)) + git_lines = [] + inside_git = False + else: + # Buffer up the git section. + git_lines.append(line) + + # Only parse bundler version from the BUNDLED_WITH section. + if inside_bundled_with: + version = line.strip() + bundler = struct( + name = "bundler", + version = version, + filename = "bundler-%s.gem" % version, + full_name = "bundler-%s" % version, + remote = "https://rubygems.org/", + ) + inside_bundled_with = False + + return struct( + bundler = bundler, + git_packages = git_packages, + remote_packages = remote_packages, + ) diff --git a/ruby/private/bundle_install.bzl b/ruby/private/bundle_install.bzl new file mode 100644 index 00000000..5dbaf120 --- /dev/null +++ b/ruby/private/bundle_install.bzl @@ -0,0 +1,158 @@ +"Implementation details for rb_bundle_install" + +load("//ruby/private:providers.bzl", "BundlerInfo", "GemInfo", "RubyFilesInfo") +load( + "//ruby/private:utils.bzl", + _convert_env_to_script = "convert_env_to_script", + _is_windows = "is_windows", + _normalize_path = "normalize_path", +) + +def _rb_bundle_install_impl(ctx): + toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] + + tools = [toolchain.ruby, toolchain.bundle] + bundler_exe = toolchain.bundle.path + + for gem in ctx.attr.gems: + if gem[GemInfo].name == "bundler": + # Use Bundler version defined in Gemfile.lock. + full_name = "%s-%s" % (gem[GemInfo].name, gem[GemInfo].version) + bundler_exe = gem.files.to_list()[-1].path + "/gems/" + full_name + "/exe/bundle" + tools.extend(gem.files.to_list()) + + binstubs = ctx.actions.declare_directory("bin") + bundle_path = ctx.actions.declare_directory("vendor/bundle") + + env = {} + env.update(toolchain.env) + env.update(ctx.attr.env) + if toolchain.version.startswith("jruby"): + java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"] + tools.extend(java_toolchain.java_runtime.files.to_list()) + env.update({"JAVA_HOME": java_toolchain.java_runtime.java_home}) + + if _is_windows(ctx): + script = ctx.actions.declare_file("bundle_install_{}.cmd".format(ctx.label.name)) + template = ctx.file._bundle_install_cmd_tpl + env.update({"PATH": _normalize_path(ctx, toolchain.bindir) + ";%PATH%"}) + else: + script = ctx.actions.declare_file("bundle_install_{}.sh".format(ctx.label.name)) + template = ctx.file._bundle_install_sh_tpl + env.update({"PATH": "%s:$PATH" % toolchain.bindir}) + + # Calculate relative location between BUNDLE_GEMFILE and BUNDLE_PATH. + relative_dir = "../../" + for _ in ctx.file.gemfile.short_path.split("/")[2:-1]: + relative_dir += "../" + + # See https://bundler.io/v2.5/man/bundle-config.1.html for confiugration keys. + env.update({ + "BUNDLE_BIN": "/".join([relative_dir, binstubs.path]), + "BUNDLE_DEPLOYMENT": "1", + "BUNDLE_DISABLE_SHARED_GEMS": "1", + "BUNDLE_DISABLE_VERSION_CHECK": "1", + "BUNDLE_GEMFILE": _normalize_path(ctx, ctx.file.gemfile.path), + "BUNDLE_IGNORE_CONFIG": "1", + "BUNDLE_PATH": _normalize_path(ctx, "/".join([relative_dir, bundle_path.path])), + "BUNDLE_SHEBANG": _normalize_path(ctx, toolchain.ruby.path), + }) + + ctx.actions.expand_template( + template = template, + output = script, + substitutions = { + "{env}": _convert_env_to_script(ctx, env), + "{bundler_exe}": _normalize_path(ctx, bundler_exe), + "{ruby_path}": _normalize_path(ctx, toolchain.ruby.path), + }, + ) + + ctx.actions.run( + executable = script, + inputs = depset([ctx.file.gemfile, ctx.file.gemfile_lock] + ctx.files.srcs + ctx.files.gems), + outputs = [binstubs, bundle_path], + mnemonic = "BundleInstall", + progress_message = "Running bundle install (%{label})", + tools = tools, + use_default_shell_env = True, + ) + + files = [ + ctx.file.gemfile, + ctx.file.gemfile_lock, + binstubs, + bundle_path, + ] + ctx.files.srcs + + return [ + DefaultInfo( + files = depset(files), + runfiles = ctx.runfiles(files), + ), + RubyFilesInfo( + transitive_srcs = depset([ctx.file.gemfile, ctx.file.gemfile_lock] + ctx.files.srcs), + transitive_deps = depset(), + transitive_data = depset(), + bundle_env = {}, + ), + BundlerInfo( + bin = binstubs, + env = ctx.attr.env, + gemfile = ctx.file.gemfile, + path = bundle_path, + ), + ] + +rb_bundle_install = rule( + _rb_bundle_install_impl, + attrs = { + "gemfile": attr.label( + allow_single_file = ["Gemfile"], + mandatory = True, + doc = "Gemfile to install dependencies from.", + ), + "gemfile_lock": attr.label( + allow_single_file = ["Gemfile.lock"], + mandatory = True, + doc = "Gemfile.lock to install dependencies from.", + ), + "gems": attr.label_list( + allow_files = [".gem"], + mandatory = True, + doc = "List of gems in vendor/cache that are used to install dependencies from.", + ), + "srcs": attr.label_list( + allow_files = True, + doc = "List of Ruby source files used to build the library.", + ), + "env": attr.string_dict( + doc = "Environment variables to use during installation.", + ), + "_runfiles_library": attr.label( + allow_single_file = True, + default = "@bazel_tools//tools/bash/runfiles", + ), + "_bundle_install_sh_tpl": attr.label( + allow_single_file = True, + default = "@rules_ruby//ruby/private/bundle_install:bundle_install.sh.tpl", + ), + "_bundle_install_cmd_tpl": attr.label( + allow_single_file = True, + default = "@rules_ruby//ruby/private/bundle_install:bundle_install.cmd.tpl", + ), + "_windows_constraint": attr.label( + default = "@platforms//os:windows", + ), + }, + toolchains = [ + "@rules_ruby//ruby:toolchain_type", + "@bazel_tools//tools/jdk:runtime_toolchain_type", + ], + doc = """ +Installs Bundler dependencies from cached gems. + +You normally don't need to call this rule directly as it's an internal one +used by `rb_bundle_fetch()`. + """, +) diff --git a/ruby/private/bundle_install/BUILD b/ruby/private/bundle_install/BUILD new file mode 100644 index 00000000..61e222e4 --- /dev/null +++ b/ruby/private/bundle_install/BUILD @@ -0,0 +1,4 @@ +exports_files([ + "bundle_install.cmd.tpl", + "bundle_install.sh.tpl", +]) diff --git a/ruby/private/bundle_install/bundle_install.cmd.tpl b/ruby/private/bundle_install/bundle_install.cmd.tpl new file mode 100644 index 00000000..4439f72b --- /dev/null +++ b/ruby/private/bundle_install/bundle_install.cmd.tpl @@ -0,0 +1,7 @@ +@echo off + +{env} + +{ruby_path} {bundler_exe} install --local + +:: vim: ft=dosbatch diff --git a/ruby/private/bundle_install/bundle_install.sh.tpl b/ruby/private/bundle_install/bundle_install.sh.tpl new file mode 100644 index 00000000..a4290576 --- /dev/null +++ b/ruby/private/bundle_install/bundle_install.sh.tpl @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +{env} + +{ruby_path} {bundler_exe} install --local + +# vim: ft=bash diff --git a/ruby/private/download.bzl b/ruby/private/download.bzl index f23e03a1..76509df5 100644 --- a/ruby/private/download.bzl +++ b/ruby/private/download.bzl @@ -30,6 +30,16 @@ def _rb_download_impl(repository_ctx): ruby_binary_name = "ruby" gem_binary_name = "gem" + env = {} + if version.startswith("jruby"): + # JRuby might fail with "Errno::EACCES: Permission denied - NUL" on Windows: + # https://github.com/jruby/jruby/issues/7182#issuecomment-1112953015 + env.update({"JAVA_OPTS": "-Djdk.io.File.enableADS=true"}) + elif version.startswith("truffleruby"): + # TruffleRuby needs explicit locale + # https://www.graalvm.org/dev/reference-manual/ruby/UTF8Locale/ + env.update({"LANG": "en_US.UTF-8"}) + repository_ctx.template( "BUILD", repository_ctx.attr._build_tpl, @@ -39,6 +49,7 @@ def _rb_download_impl(repository_ctx): "{version}": version, "{ruby_binary_name}": ruby_binary_name, "{gem_binary_name}": gem_binary_name, + "{env}": repr(env), }, ) @@ -67,6 +78,7 @@ def _install_jruby(repository_ctx, version): if repository_ctx.os.name.startswith("windows"): repository_ctx.symlink("dist/bin/bundle.bat", "dist/bin/bundle.cmd") + repository_ctx.symlink("dist/bin/jgem.bat", "dist/bin/jgem.cmd") # https://github.com/oneclick/rubyinstaller2/wiki/FAQ#q-how-do-i-perform-a-silentunattended-install-with-the-rubyinstaller def _install_via_rubyinstaller(repository_ctx, version): @@ -89,10 +101,28 @@ def _install_via_rubyinstaller(repository_ctx, version): if result.return_code != 0: fail("%s\n%s" % (result.stdout, result.stderr)) - result = repository_ctx.execute(["./dist/bin/ridk.cmd", "install", "1"]) + result = repository_ctx.execute(["./dist/bin/ridk.cmd", "install", "1", "3"]) if result.return_code != 0: fail("%s\n%s" % (result.stdout, result.stderr)) + if len(repository_ctx.attr.msys2_packages) > 0: + mingw_package_prefix = None + result = repository_ctx.execute([ + "./dist/bin/ruby.exe", + "-rruby_installer", + "-e", + "puts RubyInstaller::Runtime::Msys2Installation.new.mingw_package_prefix", + ]) + if result.return_code != 0: + fail("%s\n%s" % (result.stdout, result.stderr)) + else: + mingw_package_prefix = result.stdout.strip() + + packages = ["%s-%s" % (mingw_package_prefix, package) for package in repository_ctx.attr.msys2_packages] + result = repository_ctx.execute(["./dist/bin/ridk.cmd", "exec", "pacman", "--sync", "--noconfirm"] + packages) + if result.return_code != 0: + fail("%s\n%s" % (result.stdout, result.stderr)) + binpath = repository_ctx.path("dist/bin") if not binpath.get_child("bundle.cmd").exists: repository_ctx.symlink( @@ -136,6 +166,14 @@ rb_download = repository_rule( allow_single_file = [".ruby-version"], doc = "File to read Ruby version from.", ), + "msys2_packages": attr.string_list( + default = ["libyaml"], + doc = """ +Extra MSYS2 packages to install. + +By default, contains `libyaml` (dependency of a `psych` gem). +""", + ), "ruby_build_version": attr.string( default = "20231225", doc = """ diff --git a/ruby/private/download/BUILD.tpl b/ruby/private/download/BUILD.tpl index d43abf69..eefaa786 100644 --- a/ruby/private/download/BUILD.tpl +++ b/ruby/private/download/BUILD.tpl @@ -20,14 +20,20 @@ filegroup( filegroup( name = "gem", - srcs = ["dist/bin/{gem_binary_name}"], + srcs = select({ + "@platforms//os:windows": ["dist/bin/{gem_binary_name}.cmd"], + "//conditions:default": ["dist/bin/{gem_binary_name}"], + }), ) rb_toolchain( name = "toolchain", - ruby = ":ruby", + bindir = "{bindir}", bundle = ":bundle", + env = {env}, gem = ":gem", - bindir = "{bindir}", + ruby = ":ruby", version = "{version}", ) + +# vim: ft=bzl diff --git a/ruby/private/gem.bzl b/ruby/private/gem.bzl new file mode 100644 index 00000000..5f580ccd --- /dev/null +++ b/ruby/private/gem.bzl @@ -0,0 +1,32 @@ +"Implementation details for rb_gem" + +load("//ruby/private:providers.bzl", "GemInfo") + +def _rb_gem_impl(ctx): + gem = ctx.file.gem + name, _, version = ctx.attr.name.rpartition("-") + + return [ + DefaultInfo(files = depset([gem])), + GemInfo( + name = name, + version = version, + ), + ] + +rb_gem = rule( + _rb_gem_impl, + attrs = { + "gem": attr.label( + allow_single_file = [".gem"], + mandatory = True, + doc = "Gem file.", + ), + }, + doc = """ +Exposes a Ruby gem file. + +You normally don't need to call this rule directly as it's an internal one +used by `rb_bundle_fetch()`. + """, +) diff --git a/ruby/private/gem_build.bzl b/ruby/private/gem_build.bzl index 4af88111..f5d8cd05 100644 --- a/ruby/private/gem_build.bzl +++ b/ruby/private/gem_build.bzl @@ -3,31 +3,33 @@ load("//ruby/private:library.bzl", LIBRARY_ATTRS = "ATTRS") load( "//ruby/private:providers.bzl", + "BundlerInfo", "RubyFilesInfo", "get_bundle_env", "get_transitive_data", "get_transitive_deps", "get_transitive_srcs", ) +load("//ruby/private:utils.bzl", _is_windows = "is_windows") def _rb_gem_build_impl(ctx): - env = {} - windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo] - is_windows = ctx.target_platform_has_constraint(windows_constraint) tools = depset([]) gem_builder = ctx.actions.declare_file("{}_gem_builder.rb".format(ctx.label.name)) transitive_data = get_transitive_data(ctx.files.data, ctx.attr.deps).to_list() - transitive_deps = get_transitive_deps(ctx.attr.deps) + transitive_deps = get_transitive_deps(ctx.attr.deps).to_list() transitive_srcs = get_transitive_srcs(ctx.files.srcs, ctx.attr.deps).to_list() bundle_env = get_bundle_env({}, ctx.attr.deps) java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"] ruby_toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] + env = {} + env.update(ruby_toolchain.env) + if ruby_toolchain.version.startswith("jruby"): env["JAVA_HOME"] = java_toolchain.java_runtime.java_home tools = java_toolchain.java_runtime.files - if is_windows: + if _is_windows(ctx): env["PATH"] = ruby_toolchain.ruby.dirname # Inputs manifest is a dictionary where: @@ -58,23 +60,38 @@ def _rb_gem_build_impl(ctx): args = ctx.actions.args() args.add(gem_builder) ctx.actions.run( - inputs = depset(inputs), executable = ruby_toolchain.ruby, - arguments = [args], + inputs = depset(inputs), outputs = [ctx.outputs.gem], + arguments = [args], env = env, - use_default_shell_env = not is_windows, + mnemonic = "GemBuild", tools = tools, + use_default_shell_env = not _is_windows(ctx), ) - return [ + providers = [] + runfiles = ctx.runfiles(transitive_srcs + transitive_data) + for dep in transitive_deps: + if BundlerInfo in dep: + providers.append(dep[BundlerInfo]) + runfiles.merge(ctx.runfiles([dep[BundlerInfo].gemfile, dep[BundlerInfo].path])) + break + + providers.extend([ + DefaultInfo( + files = depset([ctx.outputs.gem]), + runfiles = runfiles, + ), RubyFilesInfo( transitive_data = depset(transitive_data), - transitive_deps = transitive_deps, + transitive_deps = depset(transitive_deps), transitive_srcs = depset(transitive_srcs), bundle_env = bundle_env, ), - ] + ]) + + return providers rb_gem_build = rule( _rb_gem_build_impl, @@ -87,7 +104,7 @@ rb_gem_build = rule( ), _gem_builder_tpl = attr.label( allow_single_file = True, - default = "@rules_ruby//ruby/private:gem_build/gem_builder.rb.tpl", + default = "@rules_ruby//ruby/private/gem_build:gem_builder.rb.tpl", ), _windows_constraint = attr.label( default = "@platforms//os:windows", diff --git a/ruby/private/gem_build/BUILD b/ruby/private/gem_build/BUILD new file mode 100644 index 00000000..350ec67a --- /dev/null +++ b/ruby/private/gem_build/BUILD @@ -0,0 +1 @@ +exports_files(["gem_builder.rb.tpl"]) diff --git a/ruby/private/gem_build/gem_builder.rb.tpl b/ruby/private/gem_build/gem_builder.rb.tpl index 3d2a7a49..883ecd11 100644 --- a/ruby/private/gem_build/gem_builder.rb.tpl +++ b/ruby/private/gem_build/gem_builder.rb.tpl @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fileutils' require 'json' require 'rubygems/package' @@ -34,7 +36,7 @@ Dir.mktmpdir do |tmpdir| gemspec_code = File.read(gemspec_path) Dir.chdir(gemspec_dir) do - spec = binding.eval(gemspec_code, gemspec_file, __LINE__) + spec = binding.eval(gemspec_code, gemspec_file, __LINE__) # rubocop:disable Security/Eval file = Gem::Package.build(spec) FileUtils.mv(file, packaged_gem_path) end diff --git a/ruby/private/gem_install.bzl b/ruby/private/gem_install.bzl new file mode 100644 index 00000000..3a7afa94 --- /dev/null +++ b/ruby/private/gem_install.bzl @@ -0,0 +1,137 @@ +"Implementation details for rb_gem_install" + +load("//ruby/private:providers.bzl", "GemInfo") +load( + "//ruby/private:utils.bzl", + _convert_env_to_script = "convert_env_to_script", + _is_windows = "is_windows", + _normalize_path = "normalize_path", +) + +def _rb_gem_install_impl(ctx): + gem = ctx.file.gem + install_dir = ctx.actions.declare_directory(gem.basename[:-4]) + toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] + + env = {} + env.update(toolchain.env) + tools = [toolchain.gem] + if toolchain.version.startswith("jruby"): + java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"] + tools.extend(java_toolchain.java_runtime.files.to_list()) + env.update({"JAVA_HOME": java_toolchain.java_runtime.java_home}) + + if _is_windows(ctx): + gem_install = ctx.actions.declare_file("gem_install_{}.cmd".format(ctx.label.name)) + template = ctx.file._gem_install_cmd_tpl + env.update({"PATH": _normalize_path(ctx, toolchain.bindir) + ";%PATH%"}) + else: + gem_install = ctx.actions.declare_file("gem_install_{}.sh".format(ctx.label.name)) + template = ctx.file._gem_install_sh_tpl + env.update({"PATH": "%s:$PATH" % toolchain.bindir}) + + ctx.actions.expand_template( + template = template, + output = gem_install, + substitutions = { + "{env}": _convert_env_to_script(ctx, env), + "{gem_binary}": _normalize_path(ctx, toolchain.gem.path), + "{gem}": gem.path, + "{install_dir}": install_dir.path, + }, + ) + + name, _, version = ctx.attr.name.rpartition("-") + ctx.actions.run( + executable = gem_install, + inputs = depset([gem, gem_install]), + outputs = [install_dir], + mnemonic = "GemInstall", + progress_message = "Installing %{input} (%{label})", + tools = tools, + use_default_shell_env = True, + ) + + return [ + DefaultInfo(files = depset([gem, install_dir])), + GemInfo( + name = name, + version = version, + ), + ] + +rb_gem_install = rule( + _rb_gem_install_impl, + attrs = { + "gem": attr.label( + allow_single_file = [".gem"], + mandatory = True, + doc = "Gem file to install.", + ), + "_gem_install_cmd_tpl": attr.label( + allow_single_file = True, + default = "@rules_ruby//ruby/private/gem_install:gem_install.cmd.tpl", + ), + "_gem_install_sh_tpl": attr.label( + allow_single_file = True, + default = "@rules_ruby//ruby/private/gem_install:gem_install.sh.tpl", + ), + "_windows_constraint": attr.label( + default = "@platforms//os:windows", + ), + }, + toolchains = [ + "@rules_ruby//ruby:toolchain_type", + "@bazel_tools//tools/jdk:runtime_toolchain_type", + ], + doc = """ +Installs a built Ruby gem. + +Suppose you have the following Ruby gem, where `rb_library()` is used +in `BUILD` files to define the packages for the gem and `rb_gem_build()` is used +to build a Ruby gem package from the sources. + +```output +|-- BUILD +|-- Gemfile +|-- WORKSPACE +|-- gem.gemspec +`-- lib + |-- BUILD + |-- gem + | |-- BUILD + | |-- add.rb + | |-- subtract.rb + | `-- version.rb + `-- gem.rb +``` + +You can now install the built `.gem` file by defining a target: + +`BUILD`: +```bazel +load("@rules_ruby//ruby:defs.bzl", "rb_gem_build", "rb_gem_install") + +package(default_visibility = ["//:__subpackages__"]) + +rb_gem_build( + name = "gem-build", + gemspec = "gem.gemspec", + deps = ["//lib:gem"], +) + +rb_gem_install( + name = "gem-install", + gem = ":gem-build", +) +``` + +```output +$ bazel build :gem-install +INFO: Analyzed target //:gem-install (4 packages loaded, 82 targets configured). +INFO: From Installing bazel-out/darwin_arm64-fastbuild/bin/gem-build.gem (//:gem-install): +Successfully installed example-0.1.0 +1 gem installed +``` + """, +) diff --git a/ruby/private/gem_install/BUILD b/ruby/private/gem_install/BUILD new file mode 100644 index 00000000..f7f1ec9e --- /dev/null +++ b/ruby/private/gem_install/BUILD @@ -0,0 +1,4 @@ +exports_files([ + "gem_install.cmd.tpl", + "gem_install.sh.tpl", +]) diff --git a/ruby/private/gem_install/gem_install.cmd.tpl b/ruby/private/gem_install/gem_install.cmd.tpl new file mode 100644 index 00000000..50c392aa --- /dev/null +++ b/ruby/private/gem_install/gem_install.cmd.tpl @@ -0,0 +1,16 @@ +@echo off + +{env} + +{gem_binary} ^ + install ^ + {gem} ^ + --wrappers ^ + --ignore-dependencies ^ + --local ^ + --no-document ^ + --no-env-shebang ^ + --install-dir {install_dir} ^ + --bindir {install_dir}/bin + +:: vim: ft=dosbatch diff --git a/ruby/private/gem_install/gem_install.sh.tpl b/ruby/private/gem_install/gem_install.sh.tpl new file mode 100644 index 00000000..89e8f059 --- /dev/null +++ b/ruby/private/gem_install/gem_install.sh.tpl @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +{env} + +{gem_binary} \ + install \ + {gem} \ + --wrappers \ + --ignore-dependencies \ + --local \ + --no-document \ + --no-env-shebang \ + --install-dir {install_dir} \ + --bindir {install_dir}/bin + +# vim: ft=bash diff --git a/ruby/private/gem_push.bzl b/ruby/private/gem_push.bzl index daf2cc58..3939e7aa 100644 --- a/ruby/private/gem_push.bzl +++ b/ruby/private/gem_push.bzl @@ -1,4 +1,4 @@ -"Implementation details for gem_push" +"Implementation details for rb_gem_push" load("//ruby/private:binary.bzl", "generate_rb_binary_script", BINARY_ATTRS = "ATTRS") load("//ruby/private:library.bzl", LIBRARY_ATTRS = "ATTRS") @@ -8,7 +8,7 @@ def _rb_gem_push_impl(ctx): java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"] ruby_toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"] srcs = [ctx.file.gem] - tools = [ruby_toolchain.gem] + tools = [ruby_toolchain.gem, ctx.file._runfiles_library] if ruby_toolchain.version.startswith("jruby"): env["JAVA_HOME"] = java_toolchain.java_runtime.java_home @@ -52,6 +52,7 @@ Gem file to push to RubyGems. You would usually use an output of `rb_gem_build() _binary_cmd_tpl = BINARY_ATTRS["_binary_cmd_tpl"], _binary_sh_tpl = BINARY_ATTRS["_binary_sh_tpl"], _windows_constraint = BINARY_ATTRS["_windows_constraint"], + _runfiles_library = BINARY_ATTRS["_runfiles_library"], ), toolchains = [ "@rules_ruby//ruby:toolchain_type", diff --git a/ruby/private/library.bzl b/ruby/private/library.bzl index 82f8a6a6..e245f174 100644 --- a/ruby/private/library.bzl +++ b/ruby/private/library.bzl @@ -2,6 +2,7 @@ load( "//ruby/private:providers.bzl", + "BundlerInfo", "RubyFilesInfo", "get_bundle_env", "get_transitive_data", @@ -37,7 +38,7 @@ def _rb_library_impl(ctx): runfiles = ctx.runfiles(transitive_srcs + transitive_data) runfiles = get_transitive_runfiles(runfiles, ctx.attr.srcs, ctx.attr.deps, ctx.attr.data) - return [ + providers = [ DefaultInfo( files = depset(transitive_srcs + transitive_data), runfiles = runfiles, @@ -50,6 +51,13 @@ def _rb_library_impl(ctx): ), ] + for dep in transitive_deps: + if BundlerInfo in dep: + providers.append(dep[BundlerInfo]) + break + + return providers + rb_library = rule( implementation = _rb_library_impl, attrs = ATTRS, diff --git a/ruby/private/providers.bzl b/ruby/private/providers.bzl index e4e75df0..9eca0ab9 100644 --- a/ruby/private/providers.bzl +++ b/ruby/private/providers.bzl @@ -1,9 +1,20 @@ -"Providers for Interoperability between rules" +"Providers for interoperability between rules" + RubyFilesInfo = provider( "Provider for Ruby files", fields = ["transitive_data", "transitive_deps", "transitive_srcs", "bundle_env"], ) +BundlerInfo = provider( + "Provider for Bundler installation", + fields = ["bin", "gemfile", "path", "env"], +) + +GemInfo = provider( + "Provider for a packed Ruby gem", + fields = ["name", "version"], +) + # https://bazel.build/rules/depsets def get_transitive_srcs(srcs, deps): @@ -79,9 +90,10 @@ def get_bundle_env(envs, deps): transitive_deps = get_transitive_deps(deps).to_list() for dep in transitive_deps: bundle_env.update(dep[RubyFilesInfo].bundle_env) + if BundlerInfo in dep: + bundle_env.update(dep[BundlerInfo].env) for env in envs: if env.startswith("BUNDLE_"): bundle_env[env] = envs[env] - return bundle_env diff --git a/ruby/private/test.bzl b/ruby/private/test.bzl index 48ee67a4..551be267 100644 --- a/ruby/private/test.bzl +++ b/ruby/private/test.bzl @@ -58,7 +58,7 @@ rb_test( name = "add", srcs = ["add_spec.rb"], args = ["spec/add_spec.rb"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [ ":spec_helper", "@bundle", @@ -69,7 +69,7 @@ rb_test( name = "subtract", srcs = ["subtract_spec.rb"], args = ["spec/subtract_spec.rb"], - main = "@bundle//:bin/rspec", + main = "@bundle//bin:rspec", deps = [ ":spec_helper", "@bundle", @@ -103,7 +103,7 @@ package(default_visibility = ["//:__subpackages__"]) rb_test( name = "rubocop", args = ["lib/"], - main = "@bundle//:bin/rubocop", + main = "@bundle//bin:rubocop", tags = ["no-sandbox"], deps = [ "//lib:gem", diff --git a/ruby/private/toolchain.bzl b/ruby/private/toolchain.bzl index 832da8d0..0040a0fc 100644 --- a/ruby/private/toolchain.bzl +++ b/ruby/private/toolchain.bzl @@ -5,7 +5,13 @@ load("//ruby/private/toolchain:repository_proxy.bzl", _rb_toolchain_repository_p DEFAULT_RUBY_REPOSITORY = "ruby" -def rb_register_toolchains(name = DEFAULT_RUBY_REPOSITORY, version = None, version_file = None, register = True, **kwargs): +def rb_register_toolchains( + name = DEFAULT_RUBY_REPOSITORY, + version = None, + version_file = None, + msys2_packages = ["libyaml"], + register = True, + **kwargs): """ Register a Ruby toolchain and lazily download the Ruby Interpreter. @@ -13,7 +19,7 @@ def rb_register_toolchains(name = DEFAULT_RUBY_REPOSITORY, version = None, versi * _(For MRI on Windows)_ Installed using [RubyInstaller](https://rubyinstaller.org). * _(For JRuby on any OS)_ Downloaded and installed directly from [official website](https://www.jruby.org). * _(For TruffleRuby on Linux and macOS)_ Installed using [ruby-build](https://github.com/rbenv/ruby-build). - * _(For "system") Ruby found on the PATH is used. Please note that builds are not hermetic in this case. + * _(For "system")_ Ruby found on the PATH is used. Please note that builds are not hermetic in this case. `WORKSPACE`: ```bazel @@ -28,6 +34,7 @@ def rb_register_toolchains(name = DEFAULT_RUBY_REPOSITORY, version = None, versi name: base name of resulting repositories, by default "rules_ruby" version: a semver version of MRI, or a string like [interpreter type]-[version], or "system" version_file: .ruby-version or .tool-versions file to read version from + msys2_packages: extra MSYS2 packages to install register: whether to register the resulting toolchains, should be False under bzlmod **kwargs: additional parameters to the downloader for this interpreter type """ @@ -37,6 +44,7 @@ def rb_register_toolchains(name = DEFAULT_RUBY_REPOSITORY, version = None, versi name = name, version = version, version_file = version_file, + msys2_packages = msys2_packages, **kwargs ) _rb_toolchain_repository_proxy( diff --git a/ruby/private/binary/rlocation.bzl b/ruby/private/utils.bzl similarity index 50% rename from ruby/private/binary/rlocation.bzl rename to ruby/private/utils.bzl index 1e06c37d..39f8fed5 100644 --- a/ruby/private/binary/rlocation.bzl +++ b/ruby/private/utils.bzl @@ -59,3 +59,83 @@ exit /b 0 :rlocation_end :: End of rlocation """ + +def is_windows(ctx): + windows_constraint = ctx.attr._windows_constraint[platform_common.ConstraintValueInfo] + return ctx.target_platform_has_constraint(windows_constraint) + +def convert_env_to_script(ctx, env): + """Converts an env dictionary to a string of batch/shell commands. + + Args: + ctx: rule context + env: dictionary of environment variables + + Returns: + a string with export environment variables commands. + """ + environment = [] + if is_windows(ctx): + export_command = "set" + else: + export_command = "export" + + for (name, value) in env.items(): + command = "{command} {name}={value}".format(command = export_command, name = name, value = value) + environment.append(command) + + return "\n".join(environment) + +def normalize_path(ctx, path): + """Converts path to an OS-specific equivalent. + + Args: + ctx: rule context + path: filepath string + + Returns: + an OS-specific path. + """ + if is_windows(ctx): + return path.replace("/", "\\") + else: + return path.replace("\\", "/") + +def join_and_indent(names, indentation_level = 2): + """Convers a list of strings to a pretty indented BUILD variant. + + Args: + names: list of strings + indentation_level: how many 4 spaces to indent with + + Returns: + indented string + """ + indentation = "" + for _ in range(0, indentation_level): + indentation += " " + + string = "[" + for name in names: + string += '\n%s"%s",' % (indentation, name) + string += "\n%s]" % indentation[:-4] + + return string + +def normalize_bzlmod_repository_name(name): + """Converts Bzlmod repostory to its private name. + + This is needed to define a target that is called the same as the repository. + For example, given a canonical name "rules_ruby~override~ruby~bundle", + the function would return "bundle" as the name. + + This is a hacky workaround and will be fixed upstream. + See https://github.com/bazelbuild/bazel/issues/20486. + + Args: + name: canonical repository name + + Returns: + repository name + """ + return name.rpartition("~")[-1] diff --git a/ruby/toolchain.bzl b/ruby/toolchain.bzl index 20743f53..96d766af 100644 --- a/ruby/toolchain.bzl +++ b/ruby/toolchain.bzl @@ -7,6 +7,7 @@ def _rb_toolchain_impl(ctx): gem = ctx.executable.gem, bindir = ctx.attr.bindir, version = ctx.attr.version, + env = ctx.attr.env, ) rb_toolchain = rule( @@ -36,5 +37,8 @@ rb_toolchain = rule( "version": attr.string( doc = "Ruby version", ), + "env": attr.string_dict( + doc = "Environment variables required by an interpreter", + ), }, )