diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ae9282b..ac80f23fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/changelogs/unreleased/bundler_major_minor.md b/changelogs/unreleased/bundler_major_minor.md new file mode 100644 index 000000000..614a9c068 --- /dev/null +++ b/changelogs/unreleased/bundler_major_minor.md @@ -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. diff --git a/changelogs/unreleased/ruby_gemfile_lock.md b/changelogs/unreleased/ruby_gemfile_lock.md new file mode 100644 index 000000000..1d9e44101 --- /dev/null +++ b/changelogs/unreleased/ruby_gemfile_lock.md @@ -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. diff --git a/lib/language_pack/helpers/bundler_wrapper.rb b/lib/language_pack/helpers/bundler_wrapper.rb index 552427858..6caa5eca8 100644 --- a/lib/language_pack/helpers/bundler_wrapper.rb +++ b/lib/language_pack/helpers/bundler_wrapper.rb @@ -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) (?\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) (?\d+)\.(?\d+)\.\d+/m class GemfileParseError < BuildpackError def initialize(error) @@ -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" @@ -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 @@ -126,7 +158,7 @@ def version end def dir_name - "bundler-#{version}" + @dir_name end def ruby_version @@ -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) @@ -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 diff --git a/spec/helpers/bundler_wrapper_spec.rb b/spec/helpers/bundler_wrapper_spec.rb index 64a82f324..c49abf8ad 100644 --- a/spec/helpers/bundler_wrapper_spec.rb +++ b/spec/helpers/bundler_wrapper_spec.rb @@ -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']