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

Bundler versions 2.4.22 and 2.5.6 are now available for Ruby Applications #1428

Merged
merged 2 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
100 changes: 67 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,38 @@ 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 Gem::Version.new(key).segments.first == 1
hash["1"]
elsif Gem::Version::new(key).segments.first == 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)
if version_match
major = version_match[:major]
minor = version_match[:minor]
BLESSED_BUNDLER_VERSIONS["#{major}.#{minor}"]
else
BLESSED_BUNDLER_VERSIONS["1"]
end
end

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

class GemfileParseError < BuildpackError
def initialize(error)
Expand All @@ -49,8 +79,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 +103,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 +158,7 @@ def version
end

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

def ruby_version
Expand All @@ -143,7 +175,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(<<~WARNING, inline: true)
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 +257,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