-
Notifications
You must be signed in to change notification settings - Fork 3
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
Remove bundler version in Gemfile.lock #928
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is the 1st commit message: Failing test for Bundler 2 and Ruby 2.5.5 When you deploy an app with Bundler 2.0.1 in the Gemfile.lock with Bundler 2.0.2 on the system, it works the first time, but on the second deploy it fails: ``` 1) Bundler deploys with version 2.x Failure/Error: app.push! Hatchet::App::FailedDeploy: Could not deploy 'hatchet-t-31aafb8954' (default_ruby) using 'Hatchet::GitApp' at path: './repos/rack/default_ruby' if this was expected add `allow_failure: true` to your deploy hash. output: Buildpack: nil Repo: https://git.heroku.com/hatchet-t-31aafb8954.git remote: Compressing source files... done. remote: Building source: remote: remote: -----> Ruby app detected remote: -----> Compiling Ruby/Rack remote: -----> Using Ruby version: ruby-2.5.5 remote: -----> Installing dependencies using bundler 2.0.2 remote: Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment remote: Activating bundler (2.0.1) failed: remote: Could not find 'bundler' (2.0.1) required by your /tmp/build_a9c801af0c0fc42d984cfda6569e532c/Gemfile.lock. remote: To update to the latest version installed on your system, run `bundle update --bundler`. remote: To install the missing version, run `gem install bundler:2.0.1` remote: Checked in 'GEM_PATH=vendor/bundle/ruby/2.5.0', execute `gem env` for more information remote: remote: To install the version of bundler this project requires, run `gem install bundler -v '2.0.1'` remote: Bundler Output: Activating bundler (2.0.1) failed: remote: Could not find 'bundler' (2.0.1) required by your /tmp/build_a9c801af0c0fc42d984cfda6569e532c/Gemfile.lock. remote: To update to the latest version installed on your system, run `bundle update --bundler`. remote: To install the missing version, run `gem install bundler:2.0.1` remote: Checked in 'GEM_PATH=vendor/bundle/ruby/2.5.0', execute `gem env` for more information remote: remote: To install the version of bundler this project requires, run `gem install bundler -v '2.0.1'` remote: remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed remote: Verifying deploy... remote: remote: ! Push rejected to hatchet-t-31aafb8954. remote: To https://git.heroku.com/hatchet-t-31aafb8954.git ! [remote rejected] master -> master (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/hatchet-t-31aafb8954.git' ``` The first deploy succeeds because there is no cache. The second deploy fails because the cache exists and `which bundler` points to a file generated by this bundler template (in `vendor/bundle/bin/bundle`): https://github.com/bundler/bundler/blob/905dce42705d0e92fa5c74ce4b9133c7d77a6fb1/lib/bundler/templates/Executable.bundler It fails due to this code which is run: ``` def activate_bundler(bundler_version) if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") bundler_version = "< 2" end gem_error = activation_error_handling do gem "bundler", bundler_version end ``` The `bundler_version` here will be 2.0.1 but it sees that it's own version is 2.0.2. This can potentially be mitigated by manually setting `BUNDLER_VERSION=2.0.2`, however I'm not in love with that solution as it doesn't seem like it should be required. Another solution is to not call this `vendor/bundle/bin/bundle` executable directly and instead go through a shim such as: ``` #!/usr/bin/env ruby require 'rubygems' version = "#{bundler.version}" if ARGV.first str = ARGV.first str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then version = $1 ARGV.shift end end if Gem.respond_to?(:activate_bin_path) load Gem.activate_bin_path('bundler', 'bundle', version) else gem "bundler", version load Gem.bin_path("bundler", "bundle", version) end ``` This is similar (if not identical) to the shim that rubygems would install and use on a "normal" system. On Heroku we do not use this system since we do not actually install bundler through rubygems, but rather we pre-package it as a tarball, put it on S3 and then download it. We invoke bundler, not through rubygems, but by placing it's executable on the PATH and then letting the OS find it directly. There are two fixes listed here: - Use BUNDLE_VERSION env var - Pros: Fixes the problem. A relatively small change. - Cons: Requires us to propagate this env var into the dyno and/or remember to set it any time we invoke bundler. This will be very hard to catch incorrect uses as failure to include it does not always make things fail right away, but they may fail in the future. There's also no guarantee that this will continue to work into the future. I.e. this might be a fix for today - Write our own shim file and pretend to be rubygems - Pros: Fixes the problem - Cons: We're effectively forking Rubygems shim logic with this method, any bugfixes or upstream changes to Rubygems would need to be mirrored in the buildpack, not great. Possibly brittle. A large change. I'm interested in looking for a third solution, one where Heroku will behave more like a "normal" install of Ruby w/ Bundler. Ideally we would find a way for all `bundle` calls when the client's codebase does not include a `bin/bundle` binstub, to go through whatever version of Rubygems is on the system for all invocations of `bundle` as this is how it will eventually be executed on the Dyno. - Pros: Fixes the problem. No need to worry about following upstream fixes or changes to Rubygems shim logic. - Cons: There might be additional Rubygems bugs that we're successfully avoiding today due to our current (accidental?) logic. This would be the largest change. # This is the commit message #2: Update to 2.5.7
schneems
force-pushed
the
schneems/bust-bundler
branch
from
November 4, 2019 19:50
65642b2
to
21bb1a9
Compare
danielleadams
approved these changes
Nov 5, 2019
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Backstory: Bundler introduced the BUNDLED WITH directive into the Gemfile.lock to record the version of bundler that was used to generate the Gemfile.lock. For a long time this information was only a FYI.
In Bundler 2.x originally there were plans for new behavior. Rubygems became "bundler aware" and when you tried to work in a project that had a Gemfile.lock with a
BUNDLED WITH
then it would automatically try to find that version and use it. For exampleIn this scenario, RubyGems would try to find bundler 1.15.2 and use it even if there was a more recent version available. Otherwise, it would fail.
This is "bounce" behavior was problematic for a few reasons, when the version switching happened it was opaque to the user, it's not guaranteed that everyone on the team and every environment (such as production/CI/etc.) will have that EXACT version of bundler on the system, and the errors were pre-emptive. What I mean by this is that there was no guarantee that a project generated with bundler 1.15.2 could not run with bundler 1.15.3.
This plan for "bounce" behavior was decided to be removed from the bundler codebase. Instead, it was decided to only look at the major version and then find a major version that satisfies these criteria:
When executed with bundler 2.x would look on the system for any valid 1.x version of bundler. Such as 1.17.3 or 1.0.0. Any 1.x version would be valid. If multiple are present then the latest is used (which is similar to how rubygems selects bundler to be loaded prior to these changes).
If the Gemfile.lock contains:
Then when executed with bundler 2.x then the system looks for any version of bundler 2.x. If it can't find one then it would error.
This is somewhat better behavior than the originally proposed "bounce" because it preserves existing 1.x interactions with rubygems (choosing the latest version of bundler on the platform) and it allows for a wide range of versions to execute the Gemfile.lock (making the "platform" problem i.e. heroku/CI/etc. better). But it still has two flaws: The silent version switching code is still present (though more granular) and the error behavior is still present.
Originally Bundler 2.x was intended to be backwards incompatible with 1.x. Eventually many of these incompatibilities were dropped. To my knowledge, the only one that stayed in was officially dropping support for REALLY old versions of Ruby. Even with this incompatibility if you can install a version of Bundler on your system, then the behavior between 1.x and 2.x should be identical.
What this means is that 100% of the time that bundler raises an error because it could not find a "valid version of bundler" on the platform, it would still be executable with ANY (1.x or 2.x) version of bundler. Granted there are still bugs on individual versions of bundler released, but that's a different story.
So now we've got this error behavior in bundler that is also coupled to RubyGems that is not needed.
Now is where it starts to go bad
Because Rubygems has been integrating with this bundler logic for a long time, the logic still lives inside of older versions of Rubygems. If there's a bug, it's not enough to rev the bundler version you also have to get a newer rubygems version.
All of this code was behind a flag that only triggers when bundler >= 2.0.0 is on a system so it was not widely tested or used until bundler 2.0.0 was officially released. After that time many bugs were found. Bugs continue to be found.
So we're in a situation where there is a matrix of failure modes (bundler versions on one axis, rubygems versions on another axis, and
BUNDLED WITH
declarations on the third axis). This surface area has exploded the number of cases that have to be handled. Another axis is that with Ruby 2.6 bundler became a default gem that cannot be uninstalled.Heroku Bundler support
Heroku originally worked with the bundler team to understand behavior of Bundler 2.0 on release and we decided that we would inspect the Gemfile.lock and install an appropriate "blessed" version of bundler 1.x or 2.x based on the version. This would have been a fine approach except older versions of RubyGems still contain bugs built in for Bundler 2.0.
Our version of RubyGems is tightly coupled to the Ruby version. Each version of Ruby ships with a Ruby Gems version. Since bundler 2.0 came out, we've gone so far as to apply backports of patches to older rubygems versions. This is a difficult process and could result in introducing incompatibilities for people's applications that were otherwise running. There are also cases where there are no patches to apply or version updates that will fix issues.
To the Future
This PR deletes the information in the
BUNDLED WITH
section after a bundler version is detected and installed. This means if you're deploying with1.17.3
you'll get our blessed version of bundler1.x
. If you're deploying with bundler2.0.1
you'll get our blessed version of bundler2.x
. After this, we discard theBUNDLED WITH
information so that no version of RubyGems or Bundler can use this information against your application to falsely raise an exception.While this might sound drastic, we are still honoring the intent of the value by using it to inform the bundler version that you get. Also this is not much different than if the feature had been shipped with some kind of an "escape valve" to disable the errors when we are in a context we know should be valid. For example if they had switched this error behavior off when an environment variable was detected
BUNDLER_DISABLE_VERSION_ERRORS=1
then we would have enabled that escape valve months ago.For fun
Twitter poll: https://twitter.com/schneems/status/1179413794583392257