Skip to content
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ Breaking configuration capability changes
- Drop deprecated `jruby.rack.ignore.env` property, replaced long ago by `jruby.runtime.env` and optional `jruby.runtime.env.rubyopt`
- Drop deprecated `jruby.rack.filter.*` properties, replaced long ago by init parameters `addsHtmlToPathInfo` and `verifiesHtmlResource`

## 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
Expand Down
21 changes: 21 additions & 0 deletions src/main/ruby/jruby/rack/booter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,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
Expand Down Expand Up @@ -178,6 +179,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

def relative_url_root(init_param = 'rack.relative_url_append')
relative_url_root = @rack_context.getContextPath || ''
if relative_url_append = @rack_context.getInitParameter(init_param)
Expand Down
24 changes: 23 additions & 1 deletion src/main/ruby/jruby/rack/rails_booter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,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'
Expand All @@ -65,6 +67,26 @@ def run_boot_hooks

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

class << self

# @see #RailsRackApplicationFactory
Expand Down
65 changes: 65 additions & 0 deletions src/spec/ruby/jruby/rack/booter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -358,4 +358,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
64 changes: 64 additions & 0 deletions src/spec/ruby/jruby/rack/rails_booter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,70 @@
expect(booter.public_path).to eq "."
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
booter.layout_class = JRuby::Rack::FileSystemLayout
booter.app_path = @app_dir
booter.boot!
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("../../../rails", __FILE__)

describe "Rails (stubbed)", :lib => :stub do
Expand Down
Loading