Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Bundler Version Locking #29

Merged
merged 7 commits into from
Jan 24, 2022
193 changes: 193 additions & 0 deletions text/0000-bundler-version-locking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
- Feature Name: bundler_version_locking
- Start Date: 2020-09-01
- RFC PR: (leave this empty)
- Bundler Issue: (leave this empty)

# Summary

Bundler should respect the exact version specified for a project.

If there is a lockfile (typically `Gemfile.lock`) with a `BUNDLED WITH` statement,
it should install and use that Bundler version.

Otherwise, if there is a dependency on `bundler` (in `Gemfile` or equivalent),
it should install and use that Bundler version.

This should happen transparently during the normal `bundle install`/`bundle exec` workflow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would mention, either here or in the "# Motivation" section, that this is how bundler works for any other dependency.

EDIT: I noticed that you mentioned this in the end 👍.

# Motivation

There are many times where locking your Bundler version is useful.
The existence of `BundlerVersionFinder` shows that, but the initial attempt
caused innumerable problems. This is an attempt to resolve those problems.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's expand on the motivation section. Some draft with ideas from my side:

Bundler is used by many people in many different scenarios. Experience has proven that releasing changes in bundler can result if breaking behaviour for people for whom the current version is working just fine. Even if no backwards incompatibiltiy was intended.

These problems could be avoided by making sure the dependency on bundler for people is locked just like all the other dependencies. After all, one of the main purposes of bundler is to lock your dependencies so that all developers and environments of an application run the exact same code, thus avoiding "works on my machine" issues, and breakage because a third party dependency was released.

This proposal expands this philosophy to the bundler dependency itself. Bundler already records the bundler version that generated a given lockfile in the lockfile itself, so by always running that version we make sure that developers always get a deterministic working version when working on a project.

Developers can still upgrade bundler, and we will of course encourage them to do so, but we don't want everyone to silently and automatically be updated just because we released a new version. This is also a quality of life improvement for the bundler maintainers team, because we won't get a lot of issues at the same time right after we release, but instead issues will be reported on a more distributed manner as people choose to upgrade. This will reduce our chances to get burnt out and overwhelmed with work.


# Guide-level explanation

Pin the Bundler version for a project by either:

1. Commiting `Gemfile.lock`.
2. Adding `gem "bundler", some_version_constraint` to Gemfile.

If you take approach 1, you can upgrade the locked Bundler version by
running `bundle update --bundler`.

If you take approach 2, you can upgrade the locked Bundler version by
changing the version constraint and running `bundle install`.


## Example 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all the example, I think it will help make them clear to also list the contents of the Gemfile.lock if it exists.


For this example, assume Bundler 2.1.4 is installed but Bundler 2.0.2 is not.

Gemfile:

```
source "https://rubygems.org"

gem "bundler", "= 2.0.2"
```

Output, first run:

```
$ bundle install
Bundler 2.1.4 is being run, but "Gemfile" requires version "= 2.0.2".
Fetching bundler-2.0.2.gem
(... rest of output from installing Bundler 2.0.2 ...)
Using bundler 2.0.2
(... rest of output, as normal ...)
```

Output, second or later run:
```
$ bundle install
Using bundler 2.0.2
(... rest of output, as normal ...)
```

## Example 2

For this example, assume both Bundler 2.0.2 and Bundler 2.1.4 are installed.

Gemfile:

```
source "https://rubygems.org"

gem "bundler", "~> 2.0"
```

Output:

```
$ bundle install
Using bundler 2.0.2
(... rest of output, as normal ...)
```

## Example 3

For this example, assume both Bundler 2.0.2 and Bundler 2.1.4 are installed.

Gemfile:

```
source "https://rubygems.org"

gemspec
```

blah.gemspec:

```
<...>
spec.add_development_dependency "bundler", "~> 2.0"
<...>
```

Output, first run:

```
$ bundle install
Bundler 2.1.4 is being run, but "blah.gemspec" requires version "~> 2.0".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I'd say

Bundler 2.1.4 is being run, but your dependencies were resolved to bundler 2.0.2.

Using bundler 2.0.2
(... normal output ...)
```

Output, second or later run:

```
$ bundle install
Using bundler 2.0.2
(... normal output ...)
```

## Example 4

For this example, assume both Bundler 2.0.2 and Bundler 2.1.4 are installed.
Note that since the default behavior is to run the newest version installed, and that matches the requirement, it never needs to switch versions.

Gemfile:

```
source "https://rubygems.org"

gem "bundler", "~> 2.1"
```

Output:

```
$ bundle install
(... normal output ...)
```

# Reference-level explanation

When executing a Bundler command, it should do the following:

1. If the first argument is `_<bundler version>_`:
1. If the specified version is running, skip to step 5.
2. Install the specified version, if needed.
3. Re-execute Bundler using the specified version.
2. If there is a lockfile with a `BUNDLED WITH` statement:
1. If the specified version is running, skip to step 5.
2. Install the version specified, if needed.
3. Re-execute Bundler using the specified version.
3. Resolve dependencies.
4. If the resolved dependencies include `bundler`:
1. If the specified version is running, skip to step 5.
2. Install the Bundler version specified, if needed.
3. Re-execute Bundler using the specified version.
5. Run as normal.

# Drawbacks

This does add localized complexity to part of the codebase, either in the binstub or the `bundle install` process (depending on the implementation).

# Rationale and Alternatives

The approach in this RFC tries to ensure:

1. It is transparent about what is occurring.
2. The user stays in control:
- By respecting the `_<some version>_` feature, we provide a way for users to override the behavior if needed.
3. It builds on existing conventions:
- Locking the Bundler version is done in the same place and way as any other dependency.
- Changing the locked Bundler version is done the same way as any other dependency.

I am not aware of any alternatives that accomplish all of these.

# Unresolved questions

1. There are many quality-of-life things that could be added, like telling
users if they're relying on an outdated Bundler version, but these can be
added after the fact.
2. The exact implentation is still unclear &mdash; it could be part of
`bundle install`, or installing the right Bundler version could be handled
by the `bundle` binstub.
3. How do we [preserve the system environment](https://github.com/rubygems/rfcs/pull/29#issuecomment-735416819)?
```
$ MY_ENV=a ruby -e 'ENV["MY_ENV"]="b";Kernel.exec("ruby", "-e", "print ENV[\"MY_ENV\"]")'
b
```