Skip to content

Building Ruby with Bazel

Konstantin Gredeskoul edited this page Jan 4, 2020 · 1 revision

Here we document the various ways that we bridge Bazel and RulesRuby together.

Building Your Ruby Project with Bazel

Repository Context

In your WORKSPACE file bring Ruby Toolchain with:

# file: //WORKSPACE
workspace(name = "my_awesome_workspace")

# We need this to load ruby_rules
load(
    "@bazel_tools//tools/build_defs/repo:git.bzl",
    "git_repository",
)

git_repository(
    name = "bazelruby_ruby_rules",
    branch = "develop",  # for now is the primary branch
    remote = "https://github.com/bazelruby/rules_ruby.git",
)

load(
    "@bazelruby_ruby_rules//ruby:deps.bzl",
    "ruby_register_toolchains",
    "ruby_rules_dependencies",
)

Registering Toolchain and Choosing Ruby SDK

Next, in the WORKSPACE file we must call the following two functions:

ruby_rules_dependencies()

# version here can ONLY be one of: "host", "2.6.5" and "2.6.3"
ruby_register_toolchains(version = "2.6.5")

Note that this is where you'd declare your Ruby version, which is very important:

  • If you selected a numeric version, AND your host Ruby interpreter matches that version, then the toolchain switches to the "host" mode, using your pre-installed Ruby. This is a performance compromise, because this allows you to skip building Ruby interpreter which takes a long time.

  • If the versions are not a match, or if there is no Ruby installed, the toolchain downloads and compiles Ruby Interpreter (but only one of the two mentioned versions for now), and uses that SDK moving forward, with a couple of caveats:

    1. When you wipe Bazel's Cache you will, most likely, have to wait for Bazel to rebuild Ruby Interpreter again.

    2. Currently only two versions of Ruby are supported: 2.6.3 and 2.6.5.

Using repository rule ruby_bundle

Third party dependencies can only be loaded by a repository rule according to Bazel design, thus exists ruby_bundle repository rule that is made to do just that.

Ideally, in a large Ruby mono-repo, all ruby projects will be sharing one single top-level Gemfile and a corresponding Gemfile.lock, as well as the .ruby-version. Check those in at the top level, and then you can reference individual gems or the entire gem set from your projects.

The ruby_bundle rule can only be used in WORKSPACE file, but it can be specified more than once — to create several non-overlapping bundles of gems. This can be useful to ease the migration path, but ultimately you wont benefit from Bazel's speed and caching as much if each project relies on its own bundle, as compared to sharing a single Gem bundle defined up top.

Here is the rule that registers that:

load("@bazelruby_ruby_rules//ruby:defs.bzl", "ruby_bundle")

ruby_bundle(
    name = "main_bundle",
    gemfile = "//:Gemfile",
    gemfile_lock = "//:Gemfile.lock",
    bundler_version = "2.1.2",
    visibility = ["//visibility:public"],
)

This will install Bundler version 2.1.2, and then install all of the gems in that bundle.

Your individual Ruby projects won't need every single dependency from this file, so you would register a dependency on an external gem in your BUILD file by referencing this top-level bundle label, and then selecting a subset of gems under it.

We'll look into it in the next section.

Package Context

Bazel package is any directory with a BUILD file in it. A Ruby BUILD file may look something like this:

# file: //foo/BUILD
package(default_visibility = ["//:__subpackages__"])

load(
    "@bazelruby_ruby_rules//ruby:defs.bzl",
    "ruby_binary",
    "ruby_library",
    "ruby_rspec",
)

ruby_library(
    name = "lib",
    srcs = glob(["lib/**/*.rb", "bin/foo"]),
    includes = ["lib"],             # this is the directory added to $LOAD_PATH, but it must be
                                    # declared as absolute in relation to the top level workspace.
    deps = [
        "@main_bundle//:awesome_print",  # these gems must be present in the top level Gemfile
        "@main_bundle//:tty-ui",         # but then you can include them here like so.
        "@main_bundle//:foo-bar",
    ],
)

ruby_binary(
    name = "bin",
    srcs = ["bin/foo"],
    main = "bin/foo",
    args = [
      "-f",
      "config.ru"
      "--logging=enabled"
    ],
    env = {
       "MALLOC_ARENA_MAX": 2        # set the environment 
    },
    deps = [
        "//foo:lib",            # we depend on the library created above
        "@main_bundle//:thor",           # and some gems, some of which may be different.
        "@main_bundle//:awesome_print",  
        "@main_bundle//:tty-ui",         
        "@main_bundle//:foo-bar",
    ],
)

So effectively each build target has to declare explicit dependencies on all sources, executables as well as the bundled gems one by one.

This may get automated or auto-generated soon, but for now this mimics closely to how Bazel NPM rules function.

Running RSpec

In addition to the standard ruby_test rule (which works just like the ruby_binary rule, but is interpreted by Bazel as a test result), there is a specialized macro ruby_rspec which instantiates ruby_rspec_test rule. This macro allows you to run rspecs on a folder or a set of files with minimal number of lines of code.

**Make sure you have rspec and rspec-its gem defined in your Gemfile.lock.

# BUILD.bazel
load(
    "@bazelruby_ruby_rules//ruby:defs.bzl",
    "ruby_binary",
    "ruby_library",
    "ruby_rspec",
    "ruby_test",
)

filegroup(
    name = "sources",
    srcs = glob([
        "lib/**/*.rb",
        "app/**/*.rb",
    ]),
    data = glob([
        "config/**/*",
    ]),
)

filegroup(
    name = "specs",
    srcs = glob([
        "spec/**/*.rb",
    ]),
    data = glob([
        "spec/fixtures/**/*.yml"
    )],
)

ruby_rspec(
    name = "rspec",
    srcs = [
        ":sources",
        ":specs",
    ],
    rspec_args = {       
        "--format": "progress",  # this is how we can override rspec's default documentation format
    },
    specs = ["spec"], # this will run rspec on the entire folder.
    deps = [
        "@bundle//:awesome_print",
        "@bundle//:colored2",  # NOTE: `rspec` and `rspec-its` gems are automatically added to the dependency list.
    ],
)

What if things break?

Yes, today many things may break. You might encounter issues with:

  • gems requiring native extensions compilation
  • gems loading stuff from other gem's sources
  • Ruby programs that use __dir__ instead of __FILE__ for requiring source files.

More updates will be added here as they available. Thanks!

What's Coming?

We are still ironing out the kinks on how to deal with the gems in general and in particular bundled gems.

There may be breaking changes in the future.