January 3, 2019·6 min read·engineeringrubybundlerdependencies

Bundler 2 makes the lockfile choose the tool generation

Bundler 2 drops obsolete Ruby support and adds automatic selection between major versions. The lockfile now coordinates the package-manager transition as well as dependency versions.

Installing a new package manager should not silently migrate every application on a machine. A developer may maintain several Ruby applications with different dependency histories, while a build host may install dependencies for projects owned by different teams. Bundler 2.0, released January 3, addresses that transition by selecting Bundler 1 or 2 from the application's lockfile.

The release removes support for Ruby versions below 2.3. Its original 2.0.0 gemspec also requires RubyGems 3.0.0 or newer. Those are requirements of this release of the tool itself. An application remaining on Bundler 1 does not become a Bundler 2 application simply because the newer generation has been installed alongside it.

I find Ruby approachable for larger object-oriented programs, particularly where its blocks and collection operations make the code clear. Dependency installation is a different concern. A readable application still needs a reproducible account of which packages and tooling make it runnable, so this migration policy matters independently of the language syntax.

The lockfile records more than package names

The application's Gemfile describes its requirements, while the lockfile records a resolved set. The Bundler 2 lockfile generator writes sources and resolved specifications, followed by platforms and dependency declarations. It can also record the locked Ruby version and writes a BUNDLED WITH section for Bundler's own version.

That last section gives the tooling transition a project-local reference. Installing a newer executable globally is a change to the machine. Changing the lockfile is a change to the application's declared installation context. Separating those scopes lets a shared machine support applications that have not all made the same migration decision.

The release announcement describes Bundler 1.17.3 and 2.0.0 coexisting, with the application using the major version indicated by its lockfile. This gives application maintainers control over when a major tooling change becomes part of their project. It also makes the relevant decision reviewable alongside the dependency state it affects.

A lockfile cannot describe every property of an execution environment. Native libraries and the interpreter installation still have their own requirements. Its role here is narrower and concrete. It preserves the package resolution and carries the Bundler-version information needed for the major-version selection described in the announcement.

This distinction prevents a common misunderstanding about reproducibility. Recording one part of the environment does not make all other inputs irrelevant. It does, however, remove one avoidable source of variation. Two developers should not select different package-manager generations merely because one happened to install a new gem yesterday.

Upgrading the application is an explicit operation

The announced migration command is short.

bundle update --bundler

The announcement instructs maintainers to commit the resulting lockfile. That makes the migration a project change that can reach other development machines and deployment environments. The command's purpose is to update the recorded Bundler choice. It should be distinguished from a general dependency update undertaken for unrelated package upgrades.

There are several independent prerequisites behind that small command. The machine needs a Ruby version that the new Bundler supports, a suitable RubyGems version, and the installed Bundler generation needed for the project. The gemspec makes the first two requirements explicit. A project's ability to run its own code under an interpreter does not establish that every version of its installation tooling supports that interpreter.

New applications use Bundler 2 when it is installed, according to the release announcement. Existing applications keep their previous generation until deliberately upgraded. That policy gives new work the current default while protecting established projects from a machine-wide change in behavior.

For a deployment pipeline, this changes what it means to prepare the environment. Installing a single latest tool is insufficient if the pipeline builds applications with different recorded requirements. The environment must be able to honor the project's choice. The lockfile communicates that choice, while the environment supplies the compatible tooling to execute it.

I prefer this arrangement because the migration belongs to the application maintainer. A change in a developer's shared tooling should not decide when an unrelated application crosses a major-version boundary. Project-local metadata provides a concrete place to make and review that decision.

Support requirements have a maintenance cost

Dropping old interpreters removes combinations that the package-manager maintainers must support. The announcement frames Bundler 2 primarily as the removal of support for Ruby and RubyGems versions that have reached the end of their supported life. It also states an intention to aim for one major Bundler release per year, so future support changes can follow the Ruby project's lifecycle more closely.

That is a plan for future releases, rather than a guarantee that the schedule will occur exactly as proposed. Its immediate implication is a clearer division between maintaining old applications and developing the current tooling. Keeping Bundler 1 usable for existing lockfiles allows the new generation to narrow its requirements without pretending the older applications have disappeared.

This has a practical relationship to maintenance effort. Every additional interpreter or RubyGems generation can introduce another behavior that a package-manager implementation has to accommodate. Removing a supported combination is therefore a compatibility change even if the commands used by a current application look familiar.

The gemspec provides an executable expression of the boundary. Its requirement is not merely prose in the release announcement. Installation tooling can inspect the declared minimum Ruby and RubyGems versions and reject an unsupported combination. This makes the tool's own dependencies subject to the same general idea of declared requirements that it applies to application gems.

Source selection and command output also change

Bundler 2 changes the default expansion of the GitHub source shorthand to HTTPS. That shorthand appears in Gemfiles as a repository name attached to a gem. The Gemfile documentation shipped with the release describes Git dependencies and the options for selecting a branch, tag, or reference.

The transport choice and the selected revision solve different problems. A branch or commit determines which source is requested. The transport determines how the request reaches the host. Making HTTPS the default for the shorthand improves the default retrieval path without requiring every Gemfile to spell out the full URL.

A project can also define its own Git source shorthand through a Ruby block. That is consistent with the Gemfile's role as Ruby code evaluated in a dependency-description context. It also means that a built-in default cannot explain every repository URL a particular project uses. The application's declaration still needs to be read when determining its actual source configuration.

The release moves errors and warnings to standard error. This affects callers that capture command output, because diagnostic text and ordinary output no longer necessarily arrive through the same stream. A wrapper that only stores standard output can therefore omit information required to explain a failed installation. Capturing the intended streams is part of the wrapper's interface with the command.

These changes are small relative to replacing the entire dependency-management model, but they are observable. A Gemfile using a built-in source shorthand can request a different transport. A script consuming output can receive diagnostics on a different stream. The lockfile-controlled major-version transition keeps those behavior changes tied to an application decision instead of making them an incidental consequence of another project's tooling installation.

Application requirements remain separate from tool requirements

The Gemfile can declare a Ruby requirement for the application, including an engine and engine version where appropriate. That statement concerns the program being installed. Bundler's own gemspec concerns the program performing the installation. Both need to be satisfied for the intended workflow to succeed.

An application with permissive dependency requirements may still rely on a particular resolved set in its lockfile. Updating the installer generation does not make those dependency requirements disappear. Conversely, retaining an old installer does not extend the support lifetime of the interpreter it runs on. The release gives maintainers a way to coordinate the package-manager change while retaining those distinctions.

The result is a migration with a defined unit of change. A machine can acquire Bundler 2, an application can keep its recorded Bundler 1 choice, and a later lockfile update can move that application deliberately. That gives deployment tooling a concrete project artifact to follow when several Ruby applications share the same installation environment.