2.6. Gems and Bundler: Library Management in Ruby

Libraries. The Ruby standard library includes a large number of useful classes covering file and network input/output, time and date manipulation, manipulating strings and collections, and more.

An external library is packaged as a Ruby gem, a collection of classes with well-defined interfaces. Gems can be as simple as augmenting existing classes with a few utility functions, or as complex as an entire framework: Rails itself is distributed as a gem that depends on several other gems. Similar to Python import, require makes a gem’s classes and functions available within a file of Ruby code, as Figure 2.12 shows.

Where Ruby really shines, though, is in managing dependencies among gems. GitLab, a popular open-source application written in Rails, relies on around 400 gems. Since some of those rely in turn on other gems, all in all GitLab depends on over 800 gems, many of which are constantly evolving. It’s therefore critical to specify which version(s) of libraries an app has been developed and tested with, so that when the app is deployed or distributed, it behaves the same way in every environment in which it’s run.

To manage complex dependencies, we need a dependency manager or package manager, such as pip for Python, npm for Node.js, or Apache Maven for Java. Ruby’s package manager, Bundler, is itself a gem. Once Bundler is installed with gem install bundler, you should allow it to do your dependency management.

2.13
Figure 2.13: Frequently used commands for working with gems and Bundler. We will learn about Bundler “environments” in Section 4.1.

To use Bundler, a Ruby project should have a file called Gemfile in its top-level directory that records the dependencies of the app on particular libraries. Bundler reads this file and tries to compute a set of library version(s) that respects all the constraints in the file. For example, if the app depends on version ≥ 3.0 and ≤ 4.0 for library X, but the app also depends on library Y which requires version 3.5 of library X, then version 3.5 of library X will be installed. Bundler can also detect when it’s impossible to satisfy all the constraints. In general, when you start a new Ruby project you immediately create a Gemfile for it, and when you download someone else’s Ruby project to work on,you first run bundle install in the project’s main directory to allow Bundler to locate and download all the necessary libraries.

Bundler then arranges to install all needed gems with their proper versions, and records the results in Gemfile.lock. Both Gemfile and Gemfile.lock should be stored as part of the codebase, since the latter records which versions of which libraries were actually used in development, whereas Gemfile just specifies constraints on which versions could be compatibly used.

Increasingly, library version numbers follow semantic versioning, not just for Ruby gems but in other languages as well. The usual arrangement is for a version number to be formatted as major.minor.patch, where each field is an integer, such as 2.3.1. Changes in the value of patch are usually minor, backwards-compatible bug fixes, including security patches, that do not change the semantics or functionality of the gem. Changes in minor usually indicate that functionality has been added in a backward-compatible manner. Changes in major signal that the Application Programming Interface (API)—the way you call the library’s functions—has changed in a way that may break compatibility with previous versions.

Since breaking compatibility is a major decision that may affect thousands of apps using a library, a common practice is for such changes to first appear as deprecation warnings in a new minor version or patch version. Such warnings typically manifest as messages emitted at build time or run time to the effect of “Warning: this feature will work differently [or be dropped] in the next major release of this library.” As a general rule, deprecation warnings become errors when the major version changes. The prudent developer faced with a deprecation warning will therefore read the documentation and determine if there is a way to change the current code so that it uses the soon-to-be-new version of the feature or of the feature’s API.

Indeed, when upgrading to a new major version, a best practice is to first incrementally upgrade so that you can identify and address deprecations before the major version change. For example, suppose your app uses version 2.7.3 of the Foobaz gem, but the latest version is 3.1.5, and you haven’t been keeping up:

  1. First, update to the latest whose major version is still 2—let’s say that turns out to be 2.8.1.

  2. Identify and address any deprecation warnings generated by that upgrade.

  3. Now upgrade to the first release whose major version is 3. In all likelihood this is 3.0.0, but might be different. Ensure all works well with the new major version.

  4. Next, upgrade to the latest minor version—in our example, probably 3.1.0. Ensure all works well.

  5. Finally, upgrade to 3.1.5, the latest patch release.

Of course, in an ideal world, the developer has been gradually updating the gem over time, so it is not necessary to do all these steps at once.

In Chapter 8, we will have a lot to say about “ensuring all works well,” as this is one of the key roles of having a solid test suite.