diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8fde9e88..cfced1785 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
-## 1.2.8 (UNRELEASED)
+## 1.2.8
+- Improve isolation and bundler version/CLI boot issues with more opinionated boot process (#461)
- Fix possible infinite loop in Response#isClientAbortException (#449, #450)
- Fix startup logging of captured errors when config properties cannot be dumped
- Update (bundled) rack to 2.2.24
diff --git a/pom.xml b/pom.xml
index 070350d60..60b34dbb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
org.jruby.rack
jruby-rack
- 1.2.7
+ 1.2.8
JRuby-Rack
https://github.com/jruby/jruby-rack/
diff --git a/src/main/ruby/jruby/rack/booter.rb b/src/main/ruby/jruby/rack/booter.rb
index 958deee42..1524b42bc 100644
--- a/src/main/ruby/jruby/rack/booter.rb
+++ b/src/main/ruby/jruby/rack/booter.rb
@@ -96,6 +96,7 @@ def boot!
change_working_directory
export_global_settings
load_settings_from_init_rb
+ prepare_bundler_env
set_relative_url_root
run_boot_hooks
self
@@ -184,6 +185,26 @@ def load_settings_from_init_rb
end
end
+ # Default Bundler settings for bundled applications (Rails or plain Rack), guarded on a Gemfile being present. These
+ # are *defaults* only: values already present in the environment (passed through from the hosting process) or set by
+ # the application's *init.rb* (evaluated just before this) always take precedence, as does a `.bundle/config`
+ # shipped with the application (e.g. as generated by Warbler).
+ def prepare_bundler_env
+ if ! ENV['BUNDLE_GEMFILE'] && app_path
+ gemfile = expand_path('Gemfile')
+ ENV['BUNDLE_GEMFILE'] = gemfile if gemfile && File.exist?(gemfile)
+ end
+ return unless ENV['BUNDLE_GEMFILE'] # not a bundled application
+
+ # never "auto-switch" to Gemfile.lock's BUNDLED WITH bundler version - Bundler restarts the process with
+ # `Kernel.exec` to do so, which cannot work embedded in a JVM.
+ ENV['BUNDLE_VERSION'] ||= 'system'
+ # fail fast with a descriptive error on Gemfile vs Gemfile.lock drift instead of attempting a runtime
+ # re-resolution that reports a misleading GemNotFound. (Deliberately not BUNDLE_DEPLOYMENT, which forces the
+ # vendor/bundle path.)
+ ENV['BUNDLE_FROZEN'] ||= 'true' if File.exist?("#{ENV['BUNDLE_GEMFILE']}.lock")
+ end
+
# @deprecated no longer used, replaced with {#run_boot_hooks}
def load_extensions
run_boot_hooks
diff --git a/src/main/ruby/jruby/rack/rails/environment.rb b/src/main/ruby/jruby/rack/rails/environment.rb
index 3d4764ac6..8f23cf381 100644
--- a/src/main/ruby/jruby/rack/rails/environment.rb
+++ b/src/main/ruby/jruby/rack/rails/environment.rb
@@ -20,7 +20,9 @@ def to_app
# Loads the Rails environment (*config/environment.rb*).
def load_environment
- require expand_path('config/boot.rb')
+ user_boot = expand_path('config/boot.rb')
+ prepare_bundler(user_boot)
+ require user_boot
require 'jruby/rack/rails/railtie'
require expand_path('config/environment.rb')
require 'jruby/rack/rails/extensions'
@@ -33,4 +35,26 @@ def set_public_root
# no-op here
end
+ private
+
+ # For a default (unmodified) Rails *config/boot.rb*, runs `Bundler.setup` up-front. `bundler/setup` can swallow setup
+ # errors and end up calling `exit` when it believes stdout is a tty (which is frequently mis-detected under a servlet
+ # container); pre-booting makes failures raise instead, so the container logs the actual error.
+ def prepare_bundler(boot_rb_path)
+ return unless ENV['BUNDLE_GEMFILE'] # not a bundled application
+
+ if rails_has_default_bundler_boot?(boot_rb_path)
+ # pre-boot bundler with groups respecting BUNDLE_WITHOUT from the environment
+ require 'bundler'
+ Bundler.ui.silence { Bundler.setup }
+ end
+ end
+
+ def rails_has_default_bundler_boot?(boot_rb_path)
+ boot_rb_content = File.read(boot_rb_path) if File.readable?(boot_rb_path)
+ return false unless boot_rb_content
+ # Assume default if there is a `require 'bundler/setup'` and no `BUNDLE_WITHOUT` in the boot.rb file.
+ %r{^\s*require\s+["']bundler/setup["']} =~ boot_rb_content && %r{BUNDLE_WITHOUT} !~ boot_rb_content
+ end
+
end
\ No newline at end of file
diff --git a/src/main/ruby/jruby/rack/version.rb b/src/main/ruby/jruby/rack/version.rb
index a95c08a22..92fe09445 100644
--- a/src/main/ruby/jruby/rack/version.rb
+++ b/src/main/ruby/jruby/rack/version.rb
@@ -8,6 +8,6 @@
module JRuby
module Rack
- VERSION = '1.2.7'
+ VERSION = '1.2.8'
end
end
diff --git a/src/spec/ruby/jruby/rack/booter_spec.rb b/src/spec/ruby/jruby/rack/booter_spec.rb
index 759b0579c..a6e283bba 100644
--- a/src/spec/ruby/jruby/rack/booter_spec.rb
+++ b/src/spec/ruby/jruby/rack/booter_spec.rb
@@ -363,4 +363,69 @@
end
+ describe "#prepare_bundler_env" do
+ require 'tmpdir'; require 'fileutils'
+
+ before :each do
+ @original_pwd = Dir.pwd
+ @original_bundle_env = {}
+ %w(BUNDLE_GEMFILE BUNDLE_VERSION BUNDLE_FROZEN).each { |k| @original_bundle_env[k] = ENV.delete(k) }
+ end
+
+ after :each do
+ Dir.chdir(@original_pwd)
+ @original_bundle_env.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
+ FileUtils.rm_rf @app_dir if @app_dir
+ end
+
+ def boot_app(gemfile = "source 'https://rubygems.org'\n", lockfile = "GEM\n")
+ @app_dir = File.realpath(Dir.mktmpdir('rack-app')) # macOS: /var -> /private/var
+ File.write(File.join(@app_dir, 'Gemfile'), gemfile) if gemfile
+ File.write(File.join(@app_dir, 'Gemfile.lock'), lockfile) if gemfile && lockfile
+ booter.layout_class = JRuby::Rack::FileSystemLayout
+ booter.app_path = @app_dir
+ booter.boot!
+ end
+
+ it "defaults BUNDLE_GEMFILE, BUNDLE_VERSION and BUNDLE_FROZEN for a bundled (plain Rack) application" do
+ boot_app
+ expect( ENV['BUNDLE_GEMFILE'] ).to eq File.join(@app_dir, 'Gemfile')
+ expect( ENV['BUNDLE_VERSION'] ).to eq 'system'
+ expect( ENV['BUNDLE_FROZEN'] ).to eq 'true'
+ end
+
+ it "does not default BUNDLE_FROZEN without a Gemfile.lock" do
+ boot_app "source 'https://rubygems.org'\n", nil
+ expect( ENV['BUNDLE_GEMFILE'] ).to eq File.join(@app_dir, 'Gemfile')
+ expect( ENV['BUNDLE_VERSION'] ).to eq 'system'
+ expect( ENV['BUNDLE_FROZEN'] ).to be nil
+ end
+
+
+ it "does not touch the environment for a non-bundled application (no Gemfile)" do
+ boot_app nil
+ expect( ENV['BUNDLE_GEMFILE'] ).to be nil
+ expect( ENV['BUNDLE_VERSION'] ).to be nil
+ expect( ENV['BUNDLE_FROZEN'] ).to be nil
+ end
+
+ it "tolerates (custom) layouts without a real app path" do
+ booter.layout = double('layout', :app_path => nil)
+ expect { booter.send :prepare_bundler_env }.to_not raise_error
+ expect( ENV['BUNDLE_GEMFILE'] ).to be nil
+ expect( ENV['BUNDLE_VERSION'] ).to be nil
+ end
+
+ it "respects values from the environment (or set by init.rb, evaluated before)" do
+ ENV['BUNDLE_GEMFILE'] = gemfile = File.join(Dir.pwd, 'Gemfile')
+ ENV['BUNDLE_VERSION'] = 'lockfile'
+ ENV['BUNDLE_FROZEN'] = 'false'
+ boot_app
+ expect( ENV['BUNDLE_GEMFILE'] ).to eq gemfile
+ expect( ENV['BUNDLE_VERSION'] ).to eq 'lockfile'
+ expect( ENV['BUNDLE_FROZEN'] ).to eq 'false'
+ end
+
+ end
+
end
diff --git a/src/spec/ruby/jruby/rack/rails_booter_spec.rb b/src/spec/ruby/jruby/rack/rails_booter_spec.rb
index a49f5cec9..6da89962a 100644
--- a/src/spec/ruby/jruby/rack/rails_booter_spec.rb
+++ b/src/spec/ruby/jruby/rack/rails_booter_spec.rb
@@ -106,6 +106,70 @@
booter.logger.info 'hello-there'
end
+ describe "#prepare_bundler (default rails boot.rb pre-boot)" do
+ require 'tmpdir'; require 'fileutils'
+
+ DEFAULT_RAILS_BOOT_RB = <<-BOOT # Rails 5.x - 8.x default config/boot.rb
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+ require "bundler/setup" # Set up gems listed in the Gemfile.
+ BOOT
+
+ before :each do
+ @original_pwd = Dir.pwd
+ @original_bundle_env = {}
+ %w(BUNDLE_GEMFILE BUNDLE_VERSION BUNDLE_FROZEN).each { |k| @original_bundle_env[k] = ENV.delete(k) }
+ require 'bundler'
+ end
+
+ after :each do
+ Dir.chdir(@original_pwd)
+ @original_bundle_env.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
+ FileUtils.rm_rf @app_dir if @app_dir
+ end
+
+ def booted_with_boot_rb(boot_rb, gemfile = "source 'https://rubygems.org'\n")
+ @app_dir = File.realpath(Dir.mktmpdir('rails-app')) # macOS: /var -> /private/var
+ FileUtils.mkdir_p File.join(@app_dir, 'config')
+ File.write File.join(@app_dir, 'config', 'boot.rb'), boot_rb
+ File.write File.join(@app_dir, 'Gemfile'), gemfile if gemfile
+ rails_booter.layout_class = JRuby::Rack::FileSystemLayout
+ rails_booter.app_path = @app_dir
+ rails_booter.boot!
+ rails_booter
+ end
+
+ def prepare_bundler!(booted)
+ booted.send :prepare_bundler, File.join(@app_dir, 'config', 'boot.rb')
+ end
+
+ it "pre-boots bundler for a default rails boot.rb" do
+ booted = booted_with_boot_rb DEFAULT_RAILS_BOOT_RB
+ allow(Bundler.ui).to receive(:silence).and_yield
+ expect(Bundler).to receive(:setup)
+ prepare_bundler! booted
+ end
+
+ it "does not pre-boot when boot.rb manages BUNDLE_WITHOUT itself" do
+ booted = booted_with_boot_rb %Q{ENV['BUNDLE_WITHOUT'] = 'test'\nrequire "bundler/setup"\n}
+ expect(Bundler).to_not receive(:setup)
+ prepare_bundler! booted
+ end
+
+ it "does not pre-boot when the require is commented out" do
+ booted = booted_with_boot_rb %Q{# require "bundler/setup"\n}
+ expect(Bundler).to_not receive(:setup)
+ prepare_bundler! booted
+ end
+
+ it "does not pre-boot a non-bundled application (no Gemfile)" do
+ booted = booted_with_boot_rb DEFAULT_RAILS_BOOT_RB, nil
+ expect(Bundler).to_not receive(:setup)
+ prepare_bundler! booted
+ end
+
+ end
+
RAILS_ROOT_DIR = File.expand_path("../../../rails3x", __FILE__)
# NOTE: specs currently only test with a stubbed Rails::Railtie