[Talking-Ruby] RVM( or Rbenv), RubyGems and Bundler Work Together

RVM( or Rbenv), RubyGems and Bundler

RVM( or Rbenv), RubyGems, and Bundler work together to give us a lot of control over our code’s environment. If you know how they work, you’ll be better prepared to troubleshoot any problems you encounter.

Ruby Code Loading

By default, the Ruby language provides two major methods for loading code defined elsewhere: load & require.

1
2
3
load 'json.rb'
require 'json.rb'
require_relative 'json.rb'

Both loading methods accept both absolute and relative paths as arguments. However, there are two differentiating factors:

  • Multiple calls to load will re-execute the file, whereas multiple calls to require will not re-execute the file; instead, it will return false.

  • Calls to load resolves only to absolute and relative paths. Calls to require checks up on the $LOAD_PATH when the path doesn’t resolve to an absolute path.

  • A third variant is require_relative, which uses relative paths to require code relative to the current file’s location rather than the Ruby process’ working directory.

Ruby Version Management (RVM, Rbenv, chruby)

A version manager is a tool used to manage and easily switch between versions of our interpreter (in this case, Ruby) and specify the location to find respective gems for our project. Version managers are largely language agnostic tools, and various languages have their respective implementations, such as Nvm, n for Node.js, pyenv for Python, and Rbenv, rvm, and chruby for Ruby. Now, let’s take rbenv for a spin, shall we?

Shims and Rehashing

These two concepts need to be properly understood to be able to debug Ruby Version Management effectively.

Shims are lightweight bash scripts that exist in your PATH to intercept commands and route them to the appropriate version for execution. On a high level, every command (e.g., rspec) is translated into rvm exec rspec(or rbenv exec rspec). See the details below.

First, rvm or rbenv creates a shim for all commands (rspec, bundle, etc.) across all installed Ruby versions to intercept calls to the CLI regardless of the version.

Shims on your PATH must be prepended; this ensures they’re the first point of contact for your Ruby executables and can properly intercept. The best way I found to understand your PATH setup and know whether your shims are intercepting properly is as follows:

1
2
3
4
$ which -a bundle

/path/to/home/.rbenv/shims/bundle
/usr/bin/bundle

which -a bundle: this naively looks through your PATH and prints out in the order which it is found, locations where bundle can be found.

Rehashing is the process of creating shims. When you newly install a Ruby gem that provides an executable, such as rspec, you need to run rbenv rehash to create the shim so that subsequent calls to rspec can be intercepted by rbenv and passed on to the appropriate Ruby version.

RubyGems

Next is RubyGems. It is available from the official Ruby site. RubyGems is a Ruby packaging system designed to facilitate the creation, sharing, and installation of libraries; in some ways, it is a distribution packaging system similar to, say, apt-get, but targeted at Ruby software. RubyGems is the de-facto method for sharing gems. They are usually installed at ~/.rvm/gems/{ruby-number}[@{gemset-name}]/gems on rvm (or ~/.rbenv/versions/{version-number}/lib/ruby/gems/{minor-version}/ on rbenv), or its variant, depending on which version manager is used. Ruby’s default required method Kernel.require doesn’t provide any mechanism to load gems from the Gems installation directory. RubyGems monkey-patches Kernel.require to

  • First, search for gems in the $LOAD_PATH.

  • If not found, search for gems in the GEMS INSTALLATION DIRECTORY.

    • Once found, add the path to $LOAD_PATH.

This works “natively” because Ruby has come with RubyGems by default since version 1.9; previous Ruby versions required RubyGems to be installed manually. Although this works natively, it is also important to know this difference when debugging.

A gem is a bunch of related code used to solve a specific problem. Install a gem and get information about the gem environment as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ gem install gemname
$ gem env

RubyGems Environment:
- RUBYGEMS VERSION: 3.1.2
- RUBY VERSION: 2.7.1 (2020-03-31 patchlevel 83) [x86_64-darwin20]
- INSTALLATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0
- USER INSTALLATION DIRECTORY: /path/to/home/.gem/ruby/2.7.0
- RUBY EXECUTABLE: /path/to/home/.rbenv/versions/2.7.1/bin/ruby
- GIT EXECUTABLE: /usr/bin/git
- EXECUTABLE DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/bin
- SPEC CACHE DIRECTORY: /path/to/home/.gem/specs
- SYSTEM CONFIGURATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/etc
- RUBYGEMS PLATFORMS:
- ruby
- x86_64-darwin-20
- GEM PATHS:
- /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0
- /path/to/home/.gem/ruby/2.7.0
- GEM CONFIGURATION:
...
- REMOTE SOURCES:
- https://rubygems.org/
- SHELL PATH:
- /path/to/home/.rbenv/versions/2.7.1/bin

How does RubyGems solve this problem? It monkey patches the Kernel’s require system with its own require method. With this in-place, when require honeybadger is called, it searches through the gems folder for honeybadger.rb and activates the gem when found.

For example, require 'honeybadger' produces something similar to the following:

1
2
spec = Gem::Specification.find_by_path('honeybadger')
spec.activate

Activating a gem simply means putting it in the $LOAD_PATH. RubyGems also helps download all of a gem’s dependencies before downloading the gem itself.

Also, Rubygems ships with a nice feature that enables you to open the associated gem’s directory with gem open <gem-name>.

This allows us to easily find/trace the specific version of the gem our app is referencing.

Bundle

At this layer, Bundler helps us easily specify all our project dependencies and optionally specify a version for each. Then, it resolves our gems, as well as installs it and its dependencies. Building real-world applications pre-bundler came with a myriad of challenges, such as the following:

  • Our applications exist with numerous dependencies, and these dependencies have various other dependencies and their respective versions. Installing the wrong version of one gem will easily break our app, and fixing this problem involved lots of tears.

  • Also, two(2) of our dependencies can refer to the same third-level dependency. Finding compatibility was an issue, and if there was any, it was a problem.

  • Where we have multiple applications on the same machine, with various dependencies, our application can access any gems installed on the machine, which goes against the principle of least privilege and exposes our application to all gems installed on the machine, regardless of whether they’re malicious.

Bundler solves all three problems and gives us a sane way to manage our app dependencies by doing the following.

Bundler resolves dependencies and generates a lockfile Gemfile.lock:

Given a Gemfile.

1
2
3

# Gemfile
gem 'httparty'

If we run bundle or bundle install, it will generate the lockfile Gemfile.lock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Gemfile.lock

GEM
specs:
httparty (0.18.1)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104)
multi_xml (0.6.0)

PLATFORMS
ruby

DEPENDENCIES
httparty

BUNDLED WITH
2.1.4

From the above, the bundler generates the version of httparty to be installed, as well as its own dependencies in the Gemfile.lock. This file is the blueprint of our app dependencies and should be checked into version control. It ensures that our project dependencies are consistent across environments (development, staging, or production).

Bundler resolves compatibility among dependencies

It resolves the dependencies for httparty by finding a suitable version for its dependencies and specifying them. Bundler also tries to resolve dependencies between gems. For example,

1
2
3
4
# Gemfile

gem 'httparty' # That relies on gem 'mime-types', '>= 3.0.1, < 4.0.1'
gem 'rest-client' # That relies on gem 'mime-types', '>= 2.0.1, < 3.0'

The example above is arbitrary and will result in an error, such as the following:

1
2
3
4
5
6
7
Bundler could not find compatible versions for gem "mime-types":
In Gemfile:
httparty was resolved to 0.18.1, which depends on
mime-types ('>= 3.0.1, < 4.0.1')

rest-client was resolved to 2.0.4, which depends on
mime-types ('>= 2.0.1, < 3.0')

This is because two gems have dependencies that are not compatible and cannot be automatically resolved.

Bundler restricts access to gems installed but not specified in our Gemfile

In a sample gemfile like the following,

1
2
3
# Gemfile

gem 'httparty'
# irb
require 'rest-client'

# raises
LoadError (cannot load such file -- rest-client)
it ensures that only the dependencies specified in our Gemfile can be required by our project.

### Bundle exec

When you run `rspec` in a project directory, there is a possibility of running a different version other than what was specified in the `Gemfile`. This is because the most recent version will be selected to run versus the version specified in the `Gemfile`. 

`bundle exec rspec` ensures rspec is run in the context of that project (i.e., The gems specified in the `Gemfile`).

### Bundle binstubs

Often, we read articles where we run commands like `./bin/rails`; this command is similar to `bundle exec rails`. Binstubs are wrappers around Ruby executables to ease the usage of `bundle exec`.

To generate a binstub run, use `bundle binstubs gem-name`. This creates a binstub in the `./bin` folder but can be configured with the `--path` directory if set.

## References

[1] [Understanding How Rbenv, RubyGems And Bundler Work Together - Honeybadger Developer Blog - https://www.honeybadger.io/blog/rbenv-rubygems-bundler-path/](https://www.honeybadger.io/blog/rbenv-rubygems-bundler-path/)

[2] [RubyGems.org | your community gem host - https://rubygems.org/](https://rubygems.org/)

[3] [RVM: Ruby Version Manager - RVM Ruby Version Manager - Documentation - https://rvm.io/](https://rvm.io/)

[4] [rbenv/rbenv: Groom your app’s Ruby environment - https://github.com/rbenv/rbenv](https://github.com/rbenv/rbenv)

[5] [Bundler: The best way to manage a Ruby application's gems - https://bundler.io/](https://bundler.io/)

[6] [How do gems work? - Justin Weiss - https://www.justinweiss.com/articles/how-do-gems-work/](https://www.justinweiss.com/articles/how-do-gems-work/)

[7] [postmodern/chruby: Changes the current Ruby - https://github.com/postmodern/chruby](https://github.com/postmodern/chruby)