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