Skip to content

Commit 27848b7

Browse files
authored
Install Ruby before bundler (#1684)
* Revert "Revert v334 (#1682)" This reverts commit 57462aa. * Changelog * Inject environment name The modification in #1676 accidentally saw "test" replaced with "production" as the inheritance-based logic was no longer being invoked. This change threads the value ("production" or "test") used through the lifecycle of the buildpack. * Don't export bootstrap Ruby on PATH (And Fix "herokuish" CI test case) Previously the Ruby buildpack worked like this: - Install a "bootstrap" version of Ruby - Use bootstrap ruby to execute the buildpack code (written in ruby) - Download customer's bundler version - Run that bundler version with bootstrap ruby to determine customer's ruby version `bundle platform --ruby` - ... With that flow, it's required that the Ruby program must be able to re-exec itself since it must have a Ruby version to run and a ruby version to run `bundle platform --ruby`. The code was changed such that it now looks like this: - Install a "bootstrap" version of Ruby - Use bootstrap ruby to execute the buildpack code (written in ruby) - Statically determine Ruby version from the Gemfile.lock - Install customer's Ruby version - Install customer's bundler version - ... With this flow, we do not ever need to re-exec the bootstrap ruby version. This lets us remove exporting it to the PATH which has caused a LOT of problems over the years. As mentioned it also fixes the "herokuish" bug that was introduced in v334 (then rolled back). I was able to reproduce the bug via a test added in #1687. The test uses a different version of Ruby than the "bootstrap" version. The problem was this error: ``` Running: which -a ruby /app/bin/ruby /tmp/tmp.Dx6ifBJLoP/bin//ruby /app/bin/ruby Running: which -a bundle /tmp/tmp.Dx6ifBJLoP/bin//bundle /app/vendor/bundle/bin/bundle /app/vendor/bundle/ruby/3.1.0/bin/bundle Running: bundle list /tmp/buildpacks/5332b9a13007e20408c130cf3c47b025a8417ef0/lib/language_pack/helpers/bundle_list.rb:56:in `call': Error detecting dependencies (LanguagePack::Helpers::BundleList::CmdError) The Ruby buildpack requires information about your application???s dependencies to complete the build. Without this information, the Ruby buildpack cannot continue. Command failed: `bundle list` Could not find sinatra-4.2.1, puma-7.1.0, rack-3.2.4, rake-13.3.1, rackup-2.3.1, rack-test-2.2.0, test-unit-3.7.3, logger-1.7.0, mustermann-3.0.4, rack-protection-4.2.1, rack-session-2.1.1, tilt-2.6.1, nio4r-2.7.4, power_assert-3.0.1, base64-0.3.0 in locally installed gems Install missing gems with `bundle install`. ``` (Debugging output added for later explanation) This comes from a Heroku CI run which works by executing `bin/test-compile` (installing ruby and bundler etc.) and then calling `bin/test`. The `bin/test` already has a full ruby + bundler environment setup and ready to go, but we still download a bootstrap version of ruby so that the code only ever has to support one Ruby version (rather than N versions across the lifecycle of M stacks). What's happening in the output is that we see that `ruby` being used is `/app/bin/ruby` (local `./bin` binstub) because we forced it onto the path via: ``` LanguagePack::ShellHelpers.user_env_hash["PATH"] = "#{app_path.join("bin")}:#{ENV["PATH"]}" ``` However the `bundle` executable is pointing to `/tmp/tmp.Dx6ifBJLoP/bin//bundle` instead of the correct `/app/vendor/bundle/bin/bundle` because we don't symlink that to the binstub directory (people expect to check in their `./bin/bundle` binstub and not have it overwritten). And because the PATH has the bootstrap ruby directory `/tmp/tmp.Dx6ifBJLoP/bin/` and that also includes a `bundle` executable, it uses that to call `gem list`. Why weren't all of the tests failing? Well, the gems are installed to `./vendor/bundle/ruby/<major>.<minor>.0/`. The bundler that ships with ruby doesn't use PATH but instead is hardcoded to use the version of ruby it was shipped with: ``` $ chruby 3.3.9 $ cat $HOME/.rubies/ruby-3.3.9/bin/bundle #!/Users/rschneeman/.rubies/ruby-3.3.9/bin/ruby ``` Or ``` $ curl https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com/heroku-24/arm64/ruby-4.0.0.preview3-bb0a79e.tgz -O $ ... $ cat ruby-4.0.0.preview3-bb0a79e/bin/bundle #!/bin/sh # -*- ruby -*- # This file was generated by RubyGems. # # The application 'bundler' is installed as part of a gem, and # this file is here to facilitate running it. # _=_\ =begin bindir="${0%/*}" exec "$bindir/ruby" "-x" "$0" "$@" =end #!/usr/bin/env ruby ``` Now, bundler is smart enough to look in `./vendor/bundle/ruby/<major>.<minor>.0/` when BUNDLE_DEPLOYMENT is set, BUT since it's being executed with the bootstrap ruby version, it will always be looking in `./vendor/bundle/ruby/3.3.0/` (bootstrap ruby version is 3.3.9 now). The test fails because it's using Ruby 3.1.3 which has gems installed to `./vendor/bundle/ruby/3.1.0/` which doesn't exist, so `gem list` fails to find any of the gems (because it's looking at the wrong directory because it was accidentally executed with the wrong ruby version. Previously the fix was to exploit the "user provided environment variables" for a purpose it was never intended for, to put the customer's PATH first (in front of the bootstrap ruby version): ``` # - Add bundler's bin directory to the PATH # - Always make sure `$HOME/bin` is first on the path user_env_hash["PATH"] = "#{build_dir}/bin:#{bundler.bundler_path}/bin:#{user_env_hash["PATH"]}" user_env_hash["GEM_PATH"] = LanguagePack::Ruby.slug_vendor_base ``` This approach requires knowing the bundler_path which is now derived like this: ``` # ... class RubyVersion # Ruby versioned bundler directory # # When installing gems via `BUNDLE_DEPLOYMENT=1 bundle install`, they're installed into a versioned directory based on the ruby version. # # This becomes the location of GEM_PATH on disk https://www.schneems.com/2014/04/15/gem-path.html. # - Executables are at bundler_directory.join("bin") # - Gems are at bundler_directory.join("gems") # # For example: # # - Ruby 3.4.7 would be "vendor/bundle/ruby/3.4.0" # - JRuby 9.4.14.0 would be "vendor/bundle/jruby/3.1.0" (As it implements Ruby 3.1.7 spec) def bundler_directory "vendor/bundle/#{engine}/#{major}.#{minor}.0" end ``` OR previously it was the same as `slug_vendor_base` which is derived by shelling out: ``` # For example "vendor/bundle/ruby/2.6.0" def self.slug_vendor_base @slug_vendor_base ||= begin command = %q(ruby -e "require 'rbconfig';puts \"vendor/bundle/#{RUBY_ENGINE}/#{RbConfig::CONFIG['ruby_version']}\"") out = run_no_pipe(command, user_env: true).strip error "Problem detecting bundler vendor directory: #{out}" unless $?.success? out end end ``` A better approach such as un-shifting the first value of path, or passing the bootstrap ruby version wasn't really feasible due to the architecture of the program. The refactoring that happened in v334 (that is reverted and re-applied in this PR) enabled this seemingly small, but very impactful simplification (removing re-exporting of the bootstrap PATH). * Modify CI test to exercise `bin/test` code Previously this test worked by using the `app.json` to define a test script. When that happens, the `bin/test` code is completely bypassed, so it's not under test. This new modification instead uses the `bin/test` logic to call `bin/rake test` and that file prints the same information using the same script. While the assertions in the test on this branch didn't need to change (except for asserting `bin/rake` is now in the `bin` directory), they will fail on `main` due to the way that `bin/test` is implemented, it resolves path order differently which can cause different execution behavior than if you define a test via `app.json` and different than the normally deployed Heroku app (in production). * CHANGELOG * Fix CI failure With the new fix for PATH for `bin/test` Heroku CI users, this was accidentally working. This is the change in that test app: ``` $ git diff HEAD~1 diff --git a/Gemfile.lock b/Gemfile.lock index 89b041a..b8f5c7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,10 @@ GEM remote: https://rubygems.org/ specs: - minitest (5.11.3) - rake (12.3.1) + minitest (6.0.0) + prism (~> 1.5) + prism (1.7.0) + rake (13.2.1) PLATFORMS ruby ``` When the path was fixed it started failing with an error: ``` -----> Running test: rake test rake aborted! NoMethodError: undefined method `=~' for an instance of Proc /app/vendor/bundle/ruby/3.3.0/gems/rake-12.3.1/exe/rake:27:in `<top (required)>' (See full trace by running task with --trace) -----> Ruby buildpack tests failed with exit status 1 # ./vendor/bundle/ruby/3.3.0/gems/heroku_hatchet-8.0.6/lib/hatchet/test_run.rb:135:in `block in wait!' # /opt/hostedtoolcache/Ruby/3.3.9/x64/lib/ruby/3.3.0/timeout.rb:186:in `block in timeout' # /opt/hostedtoolcache/Ruby/3.3.9/x64/lib/ruby/3.3.0/timeout.rb:41:in `handle_timeout' # /opt/hostedtoolcache/Ruby/3.3.9/x64/lib/ruby/3.3.0/timeout.rb:195:in `timeout' # ./vendor/bundle/ruby/3.3.0/gems/heroku_hatchet-8.0.6/lib/hatchet/test_run.rb:127:in `wait!' ``` It was working before because previously the path was different: ``` $ which -a rake vendor/bundle/ruby/3.3.0/bin/rake /tmp/tmp.V7sALmG2lV/bin//rake /app/vendor/bundle/bin/rake /app/vendor/bundle/ruby/3.3.0/bin/rake ``` This called `/app/vendor/bundle/ruby/3.3.0/bin/rake` (expanded from relative path) this is a RubyGems binstub and not a Bundler binstub. That means it does NOT use the Gemfile.lock to load gems. This is the binstub: ``` ~ $ cat vendor/bundle/ruby/3.3.0/bin/rake #!/usr/bin/env ruby # # This file was generated by RubyGems. # # The application 'rake' is installed as part of a gem, and # this file is here to facilitate running it. # require 'rubygems' Gem.use_gemdeps version = ">= 0.a" str = ARGV.first if str str = str.b[/\A_(.*)_\z/, 1] if str and Gem::Version.correct?(str) version = str ARGV.shift end end if Gem.respond_to?(:activate_bin_path) load Gem.activate_bin_path('rake', 'rake', version) else gem "rake", version load Gem.bin_path("rake", "rake", version) end ``` This code will execute this path: ``` load Gem.activate_bin_path('rake', 'rake', version) ``` With a version of `>= 0.a` and produce a load path of `/app/vendor/ruby-3.3.9/lib/ruby/gems/3.3.0/gems/rake-13.1.0/exe/rake` this path is the default gem that ships with Ruby 3.3.0. But now: ``` $ which -a rake /app/vendor/bundle/bin/rake /app/vendor/bundle/ruby/3.3.0/bin/rake ``` This will call `/app/vendor/bundle/bin` which loads bundler and is effectively the same as calling `bundle exec rake`. ``` ~ $ cat vendor/bundle/bin/rake #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'rake' is installed as part of a gem, and # this file is here to facilitate running it. # ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("rake", "rake") ``` This will use the bundled version of rake such that `Gem.bin_path` returns `/app/vendor/bundle/ruby/3.3.0/gems/rake-12.3.1/exe/rake` and Rake 12.3.1 is used. Rake 12.3.1 is incompatible with Ruby 3.3 so it fails. Notably it SHOULD fail as this is what happens when you `git push heroku` on the same code: ``` ~ $ rake -v rake aborted! NoMethodError: undefined method `=~' for an instance of Proc /app/vendor/bundle/ruby/3.3.0/gems/rake-12.3.1/exe/rake:27:in `<top (required)>' ``` So the problem is in the app, which needs to be updated.
1 parent 8a44458 commit 27848b7

39 files changed

+644
-536
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
## [Unreleased]
44

5+
- Ruby is now installed before bundler. Previously, Bundler was used to detect the Ruby version by calling
6+
`bundle platform --ruby`. Now that the Ruby version is detected directly from the `Gemfile.lock`, the
7+
order of installation can be changed such that Ruby is installed before Bundler.
8+
9+
This change should be a refactor (no observed change in build behavior), but involved substantial
10+
internal changes. If your app can build with `https://github.com/heroku/heroku-buildpack-ruby#v335`
11+
but not with this version, please open a support ticket https://help.heroku.com/. (https://github.com/heroku/heroku-buildpack-ruby/pull/1684)
12+
- The `PATH` order on Heroku CI relying on `bin/test` interface has changed for applications using the `heroku/ruby`
13+
buildpack. It now starts with: `/app/bin:/app/vendor/bundle/bin:/app/vendor/bundle/ruby/3.3.0/bin` which matches
14+
the behavior of regular `git push heroku` and customers who specify tests via `app.json`.
15+
(https://github.com/heroku/heroku-buildpack-ruby/pull/1684)
16+
517

618
## [v338] - 2025-12-25
719

@@ -24,7 +36,7 @@
2436

2537
## [v334] - 2025-12-12
2638

27-
- Rolled back
39+
- Rolled back due to https://github.com/heroku/heroku-buildpack-ruby/issues/1681
2840

2941
## [v333] - 2025-12-03
3042

@@ -1845,6 +1857,7 @@ Bugfixes:
18451857
[v337]: https://github.com/heroku/heroku-buildpack-ruby/compare/v336...v337
18461858
[v336]: https://github.com/heroku/heroku-buildpack-ruby/compare/v335...v336
18471859
[v335]: https://github.com/heroku/heroku-buildpack-ruby/compare/v334...v335
1860+
[v334]: https://github.com/heroku/heroku-buildpack-ruby/compare/v333...v334
18481861
[v333]: https://github.com/heroku/heroku-buildpack-ruby/compare/v332...v333
18491862
[v332]: https://github.com/heroku/heroku-buildpack-ruby/compare/v331...v332
18501863
[v331]: https://github.com/heroku/heroku-buildpack-ruby/compare/v330...v331

bin/compile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ else
2727
fi
2828
trap 'rm -rf "$bootstrap_ruby_dir"' EXIT
2929

30-
export PATH="$bootstrap_ruby_dir/bin/:$PATH"
31-
unset GEM_PATH
32-
3330
if detect_needs_java "$BUILD_DIR"; then
3431
cat <<EOM
3532

bin/support/ruby_compile.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121
Dir.chdir(app_path)
2222

2323
LanguagePack::ShellHelpers.initialize_env(ARGV[2])
24-
if pack = LanguagePack.detect(
24+
LanguagePack.call(
2525
app_path: app_path,
2626
cache_path: cache_path,
27-
gemfile_lock: gemfile_lock
27+
gemfile_lock: gemfile_lock,
28+
bundle_default_without: "development:test",
2829
)
29-
pack.topic("Compiling #{pack.name}")
30-
pack.compile
31-
end
3230
rescue Exception => e
3331
LanguagePack::ShellHelpers.display_error_and_exit(e)
3432
end

bin/support/ruby_test-compile.rb

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@
2424
gemfile_lock = LanguagePack.gemfile_lock(app_path: app_path)
2525
Dir.chdir(app_path)
2626

27-
if pack = LanguagePack.detect(
28-
app_path: app_path,
29-
cache_path: cache_path,
30-
gemfile_lock: gemfile_lock
31-
)
32-
LanguagePack::ShellHelpers.initialize_env(ARGV[2])
33-
pack.topic("Setting up Test for #{pack.name}")
34-
pack.compile
35-
end
27+
LanguagePack::ShellHelpers.initialize_env(ARGV[2])
28+
LanguagePack.call(
29+
app_path: app_path,
30+
cache_path: cache_path,
31+
gemfile_lock: gemfile_lock,
32+
bundle_default_without: "development",
33+
environment_name: "test",
34+
)
3635
rescue Exception => e
3736
LanguagePack::ShellHelpers.display_error_and_exit(e)
3837
end

bin/support/ruby_test.rb

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,44 +29,23 @@ def execute_command(command)
2929
pipe(command, :user_env => true, :output_object => Kernel)
3030
end
3131

32-
def user_env_hash
33-
LanguagePack::ShellHelpers.user_env_hash
34-
end
35-
36-
# $ bin/test BUILD_DIR ENV_DIR ARTIFACT_DIR
37-
build_dir, env_dir, _ = ARGV
32+
# $ bin/test app_path ENV_DIR ARTIFACT_DIR
33+
app_path, env_dir, _ = ARGV.map { |arg| Pathname(arg).expand_path }
3834
LanguagePack::ShellHelpers.initialize_env(env_dir)
39-
Dir.chdir(build_dir)
40-
41-
# The `ruby_test-compile` program installs a version of Ruby for the
42-
# user's application. It needs the propper `PATH`, where ever Ruby is installed
43-
# otherwise we end up using the buildpack's version of Ruby
44-
#
45-
# This is needed here because LanguagePack::Ruby.slug_vendor_base shells out to the user's ruby binary
46-
user_env_hash["PATH"] = "#{build_dir}/bin:#{ENV["PATH"]}"
47-
48-
bundler = LanguagePack::Helpers::BundlerWrapper.new(
49-
gemfile_path: "#{build_dir}/Gemfile",
50-
bundler_path: LanguagePack::Ruby.slug_vendor_base # This was previously installed by bin/support/ruby_test-compile
51-
)
52-
53-
# - Add bundler's bin directory to the PATH
54-
# - Always make sure `$HOME/bin` is first on the path
55-
user_env_hash["PATH"] = "#{build_dir}/bin:#{bundler.bundler_path}/bin:#{user_env_hash["PATH"]}"
56-
user_env_hash["GEM_PATH"] = LanguagePack::Ruby.slug_vendor_base
35+
Dir.chdir(app_path)
5736

58-
# - Sets BUNDLE_GEMFILE
59-
# - Loads bundler's internal Gemfile.lock parser so we can use `bundler.has_gem?`
60-
bundler.install
37+
gems_list = LanguagePack::Helpers::BundleList::HumanCommand.new(
38+
stream_to_user: false,
39+
).call
6140

6241
execute_test(
63-
if bundler.has_gem?("rspec-core")
42+
if gems_list.has_gem?("rspec-core")
6443
if File.exist?("bin/rspec")
6544
"bin/rspec"
6645
else
6746
"bundle exec rspec"
6847
end
69-
elsif File.exist?("bin/rails") && bundler.has_gem?("railties") && bundler.gem_version("railties") >= Gem::Version.new("5.x")
48+
elsif File.exist?("bin/rails") && gems_list.has_gem?("railties") && gems_list.gem_version("railties") >= Gem::Version.new("5.x")
7049
"bin/rails test"
7150
elsif File.exist?("bin/rake")
7251
"bin/rake test"

bin/test

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,4 @@ bootstrap_ruby_dir=$(mktemp -d)
2020
"$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir"
2121
trap 'rm -rf "$bootstrap_ruby_dir"' EXIT
2222

23-
export PATH="$bootstrap_ruby_dir/bin/:$PATH"
24-
unset GEM_PATH
25-
2623
"$bootstrap_ruby_dir"/bin/ruby "$BIN_DIR/support/ruby_test.rb" "$@"

bin/test-compile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ bootstrap_ruby_dir=$(mktemp -d)
2323
"$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir"
2424
trap 'rm -rf "$bootstrap_ruby_dir"' EXIT
2525

26-
export PATH="$bootstrap_ruby_dir/bin/:$PATH"
27-
unset GEM_PATH
28-
2926
if detect_needs_java "$BUILD_DIR"; then
3027
cat <<EOM
3128
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
## Ruby applications using Heroku CI have a different PATH load order
2+
3+
The `PATH` order on Heroku CI relying on `bin/test` interface has changed for applications using the `heroku/ruby` buildpack.
4+
5+
It now starts with:
6+
7+
- `/app/bin:/app/vendor/bundle/bin:/app/vendor/bundle/ruby/<major>.<minor>.0/bin`
8+
9+
> Note `<major>.<minor>` is for the Ruby version so Ruby 3.3.10 would show `/app/vendor/bundle/ruby/3.3.0/bin` on the path .
10+
11+
This matches the behavior of regular `$ git push heroku` deploys and applications specifying a test command via `app.json`.
12+
13+
Previously it started with:
14+
15+
- `/app/bin:vendor/bundle/ruby/<major>.<minor>.0/bin:<bootstrap ruby>/bin:/app/vendor/bundle/ruby/<major>.<minor>.0/bin:/app/vendor/bundle/bin`
16+
17+
This discrepancy between has resulted in zero reported issues or tickets, so the fix is not expected to be disruptive. However, it's still a change, and if your application is affected it helps to understand each of the parts of those path to help with debugging.
18+
19+
## Heroku CI `bin/test`
20+
21+
Only applications that do not specify a test command in their `app.json` will trigger calling `bin/test` of the
22+
buildpack. This `bin/test` runs a Ruby script to determine what test command should be called (such as `bin/rake test`).
23+
Applications relying on this behavior will now get a different `PATH` order.
24+
25+
## What is the `PATH`?
26+
27+
When you type in `$ rspec` the operating system will look for the executable `rspec` by breaking the `PATH` environment variable into parts with a colon (`:`) separator in order from back to front. That means that it will now look for the `rspec` executable in this order:
28+
29+
- `/app/bin/rspec`
30+
- `/app/vendor/bundle/bin/rspec`
31+
- `/app/vendor/bundle/ruby/<major>.<minor>.0/bin/rspec`
32+
33+
By changing the contents or the ordering of the `PATH` you'll possibly change which executable is run. You can see the executable order by using the `which` tool.
34+
35+
```
36+
$ heroku run bash
37+
~ $ which -a rake
38+
/app/bin/rake
39+
/app/vendor/bundle/bin/rake
40+
/app/vendor/bundle/ruby/3.3.0/bin/rake
41+
```
42+
43+
The `-a` flag tells `which` to list all found executables, not just the first. But when you run `$ rake` it will effectively be the same as calling the full path `$ /app/bin/rake` directly.
44+
45+
## Path parts
46+
47+
The following describes the parts that `heroku/ruby` places on the `PATH` both before and after the change.
48+
49+
### App binstubs `/app/bin`
50+
51+
This is the local `./bin` "binstubs" directory that all recent Rails applications have.
52+
In addition, the Ruby buildpack also places a symlink to the `ruby` executable we install there, as
53+
well as other default gems.
54+
55+
This path is first for the current and prior `PATH`.
56+
57+
### Bundler binstubs `/app/vendor/bundle/bin`
58+
59+
The location of binstubs installed by `bundle install`. So if you have `rake` in your `Gemfile` you would get a `/app/vendor/bundle/bin/rake` executable file. Notably, these executables load `bundler/setup`, so if you call `$ /app/vendor/bundle/bin/rake` it's similar to calling `$ bundle exec /app/vendor/bundle/bin/rake`.
60+
61+
Unlike on a local machine, the difference between activating `$ rspec` and `$ bundle exec rspec` is very small, because Heroku cleans unused gem versions. The only time there are multiple versions of a gem on the system is due to default gems or multiple Ruby installations (due to conflicting "bootstrap" Ruby).
62+
63+
This is now second on the `PATH`, previously it was last (as installed by `heroku/ruby`).
64+
65+
> Note that this is bundler version dependent Bundler 2.6 places files here Bundler 2.7+ does not
66+
67+
### RubyGems binstubs`/app/vendor/bundle/ruby/<major>.<minor>.0/bin`
68+
69+
The location of binstubs installed by RubyGems (`gem`). When you `bundle install`, it also installs "binstubs" to this directory. These executables do NOT load bundler, so on Ruby 3.3.10 `$ bundle exec /app/vendor/bundle/ruby/3.3.0/bin/rake` and `$ /app/vendor/bundle/ruby/3.3.0/bin` would possibly produce different results on a system where there are many versions of a default gem installed.
70+
71+
This is now third on the `PATH`, previously it was second as a relative path and again later as an absolute path.
72+
73+
When a relative path is on the `PATH` as the application changes working directories it changes the effective value of the path. For example, `Dir.chdir("tmp")` would trigger path lookups in `tmp/vendor/bundle/ruby/3.3.0/bin` (for Ruby 3.3.10). It's unlikely this affected many people, but the difference is worth noting.
74+
75+
### Bootstrap Ruby `<bootstrap ruby>/bin`
76+
77+
The Ruby buildpack uses a "bootstrap" version of Ruby to execute itself.
78+
79+
Because `/app/bin` is on the path first, the correct version of Ruby will always be used (since that is where we symlink Ruby). However, if you were trying to call a default gem binstub, it's possible that prior to this change, you could have activated the "bootstrap" Ruby's copy instead of the Ruby version you requested.
80+
81+
This is no longer on the path as the implementation was refactored, so it's no longer needed. Previously, it was on the path by necessity of the implementation of `bin/test`.

hatchet.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- - "./repos/ci/heroku-ci-json-example"
99
- 728cc99c8e80290cc07441d61a8bcd4596e696fb
1010
- - "./repos/ci/ruby_no_rails_test"
11-
- c5925ab061f65433ec5dcbc890975f580e74c5ce
11+
- 8f6b08138db00ef7f3b5a21b1eb42611b83e363b
1212
- - "./repos/heroku/ruby-getting-started"
1313
- main
1414
- - "./repos/node/minimal_webpacker"

lib/language_pack.rb

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,106 @@ def self.gemfile_lock(app_path: )
2020
end
2121
end
2222

23+
def self.call(app_path:, cache_path:, gemfile_lock:, bundle_default_without:, environment_name: "production")
24+
arch = LanguagePack::Base.get_arch
25+
stack = ENV.fetch("STACK")
26+
cache = LanguagePack::Cache.new(cache_path)
27+
warn_io = LanguagePack::ShellHelpers::WarnIO.new
28+
user_env_hash = LanguagePack::ShellHelpers.user_env_hash
29+
bundler_cache = LanguagePack::BundlerCache.new(cache, stack)
30+
bundler_version = LanguagePack::Helpers::BundlerWrapper.detect_bundler_version(contents: gemfile_lock.contents)
31+
32+
metadata = LanguagePack::Metadata.new(cache_path: cache_path)
33+
new_app = metadata.empty?
34+
35+
ruby_version = Ruby.get_ruby_version(
36+
report: HerokuBuildReport::GLOBAL,
37+
metadata: metadata,
38+
gemfile_lock: gemfile_lock
39+
)
40+
41+
Ruby.remove_vendor_bundle(app_path: app_path)
42+
Ruby.warn_bundler_upgrade(metadata: metadata, bundler_version: bundler_version)
43+
Ruby.warn_bad_binstubs(app_path: app_path, warn_object: warn_io)
44+
Ruby.install_ruby(
45+
app_path: app_path,
46+
ruby_version: ruby_version,
47+
stack: stack,
48+
arch: arch,
49+
metadata: metadata,
50+
io: warn_io
51+
)
52+
53+
bundler = Helpers::BundlerWrapper.new.install
54+
default_config_vars = Ruby.default_config_vars(metadata: metadata, ruby_version: ruby_version, bundler: bundler, environment_name: environment_name)
55+
Ruby.setup_language_pack_environment(
56+
app_path: app_path.expand_path,
57+
ruby_version: ruby_version,
58+
user_env_hash: user_env_hash,
59+
bundle_default_without: bundle_default_without,
60+
default_config_vars: default_config_vars
61+
)
62+
Ruby.install_bundler_in_app(bundler_src_dir: bundler.bundler_path, app_bundler_dir: ruby_version.bundler_directory)
63+
Ruby.load_bundler_cache(
64+
ruby_version: ruby_version,
65+
new_app: new_app,
66+
cache: cache,
67+
metadata: metadata,
68+
stack: stack,
69+
bundler_cache: bundler_cache,
70+
bundler_version: bundler_version,
71+
bundler: bundler,
72+
io: warn_io
73+
)
74+
75+
bundler_output = String.new # buffer
76+
Ruby.build_bundler(
77+
ruby_version: ruby_version,
78+
app_path: app_path,
79+
io: warn_io,
80+
bundler_cache: bundler_cache,
81+
bundler_version: bundler_version,
82+
bundler_output: bundler_output,
83+
)
84+
85+
gems = Ruby.bundle_list(
86+
io: warn_io,
87+
stream_to_user: !bundler_output.match?(/Installing|Fetching|Using/)
88+
)
89+
90+
if pack = LanguagePack.detect(
91+
arch: arch,
92+
new_app: new_app,
93+
warn_io: warn_io,
94+
bundler: bundler,
95+
app_path: app_path,
96+
cache_path: cache_path,
97+
ruby_version: ruby_version,
98+
gemfile_lock: gemfile_lock,
99+
environment_name: environment_name
100+
)
101+
pack.topic("Compiling #{pack.name}")
102+
pack.compile
103+
end
104+
end
105+
23106
# detects which language pack to use
24-
def self.detect(app_path:, cache_path:, gemfile_lock: )
107+
def self.detect(arch:, app_path:, cache_path:, environment_name:, gemfile_lock:, new_app:, ruby_version:, warn_io:, bundler:)
25108
pack_klass = [ Rails8, Rails7, Rails6, Rails5, Rails4, Rails3, Rails2, Rack, Ruby ].detect do |klass|
26-
klass.use?
109+
klass.use?(bundler: bundler)
27110
end
28111

29112
if pack_klass
30113
pack_klass.new(
114+
arch: arch,
115+
bundler: bundler,
116+
new_app: new_app,
117+
warn_io: warn_io,
31118
app_path: app_path,
32119
cache_path: cache_path,
33-
gemfile_lock: gemfile_lock
120+
environment_name: environment_name,
121+
gemfile_lock: gemfile_lock,
122+
ruby_version: ruby_version,
34123
)
35124
else
36125
nil

0 commit comments

Comments
 (0)