Skip to content

Commit

Permalink
## Bundler versions 2.4.22 and 2.5.6 are now available for Ruby Appli…
Browse files Browse the repository at this point in the history
…cations

The [Ruby Buildpack](https://devcenter.heroku.com/articles/ruby-support#libraries) now installs a version of bundler based on the major and minor version listed in the `Gemfile.lock` under the `BUNDLED WITH` key. Previously, it only used the major version. Now, this logic will be used:

- `BUNDLED WITH` 1.x will receive bundler `1.17.3`
- `BUNDLED WITH` 2.0.x to 2.3.x will receive bundler `2.3.25`
- `BUNDLED WITH` 2.4.x will receive bundler `2.4.22`
- `BUNDLED WITH` 2.5.x and above will receive bundler `2.5.6`

It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git:

```
$ bundle update --ruby
$ git add Gemfile.lock
$ git commit -m "Update Gemfile.lock"
```

Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change.
  • Loading branch information
schneems committed Feb 22, 2024
1 parent a5f827b commit 698e845
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 34 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Bundler version installation is now based on both major and minor version (https://github.com/heroku/heroku-buildpack-ruby/pull/1428)
- Applications using bundler 2.4+ must now specify a ruby version in the Gemfile.lock or they will receive the default Ruby version (https://github.com/heroku/heroku-buildpack-ruby/pull/1428)

## [v266] - 2024-02-20

Expand Down
18 changes: 18 additions & 0 deletions changelogs/unreleased/bundler_major_minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Bundler versions 2.4.22 and 2.5.6 are now available for Ruby Applications

The [Ruby Buildpack](https://devcenter.heroku.com/articles/ruby-support#libraries) now installs a version of bundler based on the major and minor version listed in the `Gemfile.lock` under the `BUNDLED WITH` key. Previously, it only used the major version. Now, this logic will be used:

- `BUNDLED WITH` 1.x will receive bundler `1.17.3`
- `BUNDLED WITH` 2.0.x to 2.3.x will receive bundler `2.3.25`
- `BUNDLED WITH` 2.4.x will receive bundler `2.4.22`
- `BUNDLED WITH` 2.5.x and above will receive bundler `2.5.6`

It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git:

```
$ bundle update --ruby
$ git add Gemfile.lock
$ git commit -m "Update Gemfile.lock"
```

Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change.
15 changes: 15 additions & 0 deletions changelogs/unreleased/ruby_gemfile_lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Ruby applications without a `RUBY VERSION` in the Gemfile.lock may receive a default Ruby version

Previously, it was possible to specify a full version of Ruby in the `Gemfile` even if it was not present in the `Gemfile.lock`. The Ruby directive in the `Gemfile` was parsed by bundler and emitted via the command `bundle --platform ruby`. This behavior has changed with bundler `2.4+`, so only ruby versions listed in the `RUBY VERSION` key of the `Gemfile.lock` will be returned. If your application uses bundler 2.4+ and does not have a `RUBY VERSION` specified in the `Gemfile.lock`, it will receive a default version of Ruby.

It is strongly recommended that you have both a `RUBY VERSION` and `BUNDLED WITH` version listed in your `Gemfile.lock`. If you do not have those values, you can generate them and commit them to git:

```
$ bundle update --ruby
$ git add Gemfile.lock
$ git commit -m "Update Gemfile.lock"
```

Applications without these values specified in the `Gemfile.lock` may break unexpectedly when the defaults change.

If your app relies on specifying the ruby version in the `Gemfile` but not the `Gemfile.lock` and it is not yet using Bundler 2.4+, you may preserve this behavior by not upgrading the bundler version in your `Gemfile.lock`, however, this behavior is deprecated. It will be removed at a future date. It is recommended you lock your Ruby version now to avoid an unexpected breakage in the future.
96 changes: 63 additions & 33 deletions lib/language_pack/helpers/bundler_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,34 @@ class LanguagePack::Helpers::BundlerWrapper

BLESSED_BUNDLER_VERSIONS = {}
BLESSED_BUNDLER_VERSIONS["1"] = "1.17.3"
BLESSED_BUNDLER_VERSIONS["2"] = "2.3.25"
BUNDLED_WITH_REGEX = /^BUNDLED WITH$(\r?\n) (?<major>\d+)\.\d+\.\d+/m
# Heroku-20's oldest Ruby verison is 2.5.x which doesn't work with bundler 2.4
BLESSED_BUNDLER_VERSIONS["2.3"] = "2.3.25"
BLESSED_BUNDLER_VERSIONS["2.4"] = "2.4.22"
BLESSED_BUNDLER_VERSIONS["2.5"] = "2.5.6"
BLESSED_BUNDLER_VERSIONS.default_proc = Proc.new do |hash, key|
if key.start_with?("1")
hash["1"]
elsif key.start_with?("2")
if Gem::Version.new(key) > Gem::Version.new("2.5")
hash["2.5"]
elsif Gem::Version.new(key) < Gem::Version.new("2.3")
hash["2.3"]
else
raise UnsupportedBundlerVersion.new(hash, key)
end
else
raise UnsupportedBundlerVersion.new(hash, key)
end
end

def self.detect_bundler_version(contents: )
version_match = contents.match(BUNDLED_WITH_REGEX)
major = version_match[:major]
minor = version_match[:minor]
BLESSED_BUNDLER_VERSIONS["#{major}.#{minor}"]
end

BUNDLED_WITH_REGEX = /^BUNDLED WITH$(\r?\n) (?<major>\d+)\.(?<minor>\d+)\.\d+/m

class GemfileParseError < BuildpackError
def initialize(error)
Expand All @@ -49,8 +75,8 @@ def initialize(error)
end

class UnsupportedBundlerVersion < BuildpackError
def initialize(version_hash, major)
msg = String.new("Your Gemfile.lock indicates you need bundler `#{major}.x`\n")
def initialize(version_hash, major_minor)
msg = String.new("Your Gemfile.lock indicates you need bundler `#{major_minor}.x`\n")
msg << "which is not currently supported. You can deploy with bundler version:\n"
version_hash.keys.each do |v|
msg << " - `#{v}.x`\n"
Expand All @@ -73,12 +99,14 @@ def initialize(options = {})
@fetcher = options[:fetcher] || LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL) # coupling
@gemfile_path = options[:gemfile_path] || Pathname.new("./Gemfile")
@gemfile_lock_path = Pathname.new("#{@gemfile_path}.lock")
detect_bundler_version_and_dir_name!

@bundler_path = options[:bundler_path] || @bundler_tmp.join(dir_name)
@bundler_tar = options[:bundler_tar] || "bundler/#{dir_name}.tgz"
@version = self.class.detect_bundler_version(contents: @gemfile_lock_path.read(mode: "rt"))
@dir_name = "bundler-#{@version}"

@bundler_path = options[:bundler_path] || @bundler_tmp.join(@dir_name)
@bundler_tar = options[:bundler_tar] || "bundler/#{@dir_name}.tgz"
@orig_bundle_gemfile = ENV['BUNDLE_GEMFILE']
@path = Pathname.new("#{@bundler_path}/gems/#{dir_name}/lib")
@path = Pathname.new("#{@bundler_path}/gems/#{@dir_name}/lib")
end

def install
Expand Down Expand Up @@ -126,7 +154,7 @@ def version
end

def dir_name
"bundler-#{version}"
@dir_name
end

def ruby_version
Expand All @@ -143,7 +171,32 @@ def ruby_version
# If there's a gem in the Gemfile (i.e. syntax error) emit error
raise GemfileParseError.new(run("bundle check", user_env: true, env: env)) unless $?.success?

self.class.platform_to_version(output)
ruby_version = self.class.platform_to_version(output)
if ruby_version.nil? || ruby_version.empty?
if Gem::Version.new(self.version) > Gem::Version.new("2.3")
warn(inline: true, <<~WARNING)
No ruby version specified in the Gemfile.lock
We could not determine the version of Ruby from your Gemfile.lock.
$ bundle platform --ruby
#{output}
$ bundle -v
#{run("bundle -v", user_env: true, env: env)}
Ensure the above command outputs the version of Ruby you expect. If you have a ruby version specified in your Gemfile, you can update the Gemfile.lock by running the following command:
$ bundle update --ruby
Make sure you commit the results to git before attempting to deploy again:
$ git add Gemfile.lock
$ git commit -m "update ruby version"
WARNING
end
end
ruby_version
end

def self.platform_to_version(bundle_platform_output)
Expand Down Expand Up @@ -200,27 +253,4 @@ def parse_gemfile_lock
Bundler::LockfileParser.new(gemfile_contents)
end

def major_bundler_version
# https://rubular.com/r/jt9yj0aY7fU3hD
bundler_version_match = @gemfile_lock_path.read(mode: "rt").match(BUNDLED_WITH_REGEX)

if bundler_version_match
bundler_version_match[:major]
else
"1"
end
end

# You cannot use Bundler 2.x with a Gemfile.lock that points to a 1.x bundler
# version. The solution here is to read in the value set in the Gemfile.lock
# and download the "blessed" version with the same major version.
def detect_bundler_version_and_dir_name!
major = major_bundler_version
if BLESSED_BUNDLER_VERSIONS.key?(major)
@version = BLESSED_BUNDLER_VERSIONS[major]
else
raise UnsupportedBundlerVersion.new(BLESSED_BUNDLER_VERSIONS, major)
end
end

end
35 changes: 34 additions & 1 deletion spec/helpers/bundler_wrapper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,40 @@
end
end

describe "BundlerWrapper" do
describe "Bundler version detection" do
it "supports minor versions" do
wrapper_klass = LanguagePack::Helpers::BundlerWrapper
version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 1.17.3")
expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("1")).to be_truthy
expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["1"])

version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 2.2.7")
expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.3")).to be_truthy
expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.3"])

version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 2.3.7")
expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.3")).to be_truthy
expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.3"])

version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 2.4.7")
expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.4")).to be_truthy
expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.4"])

version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 2.5.7")
expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.5")).to be_truthy
expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.5"])

version = wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 2.6.7")
expect(wrapper_klass::BLESSED_BUNDLER_VERSIONS.key?("2.5")).to be_truthy
expect(version).to eq(wrapper_klass::BLESSED_BUNDLER_VERSIONS["2.5"])

expect {
wrapper_klass.detect_bundler_version(contents: "BUNDLED WITH\n 3.6.7")
}.to raise_error(wrapper_klass::UnsupportedBundlerVersion)
end
end

describe "BundlerWrapper mutates rubyopt" do
before(:each) do
if ENV['RUBYOPT']
@original_rubyopt = ENV['RUBYOPT']
Expand Down

0 comments on commit 698e845

Please sign in to comment.