diff --git a/lib/ruby_lsp/setup_bundler.rb b/lib/ruby_lsp/setup_bundler.rb index 846341ab8..51c3ab6c8 100644 --- a/lib/ruby_lsp/setup_bundler.rb +++ b/lib/ruby_lsp/setup_bundler.rb @@ -60,7 +60,7 @@ def initialize(project_path, **options) @custom_dir = Pathname.new(".ruby-lsp").expand_path(@project_path) #: Pathname @custom_gemfile = @custom_dir + @gemfile_name #: Pathname @custom_lockfile = @custom_dir + (@lockfile&.basename || "Gemfile.lock") #: Pathname - @lockfile_hash_path = @custom_dir + "main_lockfile_hash" #: Pathname + @freshness_hash_path = @custom_dir + "freshness_hash" #: Pathname @last_updated_path = @custom_dir + "last_updated" #: Pathname @error_path = @custom_dir + "install_error" #: Pathname @already_composed_path = @custom_dir + "bundle_is_composed" #: Pathname @@ -119,8 +119,14 @@ def setup! return run_bundle_install(@custom_gemfile) end - if @lockfile_hash && @custom_lockfile.exist? && @lockfile_hash_path.exist? && - @lockfile_hash_path.read == @lockfile_hash + # Our freshness hash determines if we need to copy the lockfile from the main app again and run bundle install + # from scratch. We use a combination of the main app's lockfile and the composed Gemfile. The goal is to + # automatically account for CLI arguments which can change the Gemfile we compose. If the CLI arguments or the + # main lockfile change, we need to make sure we're re-composing. + freshness_digest = Digest::SHA256.hexdigest("#{@lockfile_hash}#{@custom_gemfile.read}") + + if @lockfile_hash && @custom_lockfile.exist? && @freshness_hash_path.exist? && + @freshness_hash_path.read == freshness_digest $stderr.puts( "Ruby LSP> Skipping composed bundle setup since #{@custom_lockfile} already exists and is up to date", ) @@ -130,7 +136,7 @@ def setup! @needs_update_path.delete if @needs_update_path.exist? FileUtils.cp(@lockfile.to_s, @custom_lockfile.to_s) correct_relative_remote_paths - @lockfile_hash_path.write(@lockfile_hash) + @freshness_hash_path.write(freshness_digest) run_bundle_install(@custom_gemfile) end diff --git a/test/setup_bundler_test.rb b/test/setup_bundler_test.rb index f36a88a2b..89aed5bdf 100644 --- a/test/setup_bundler_test.rb +++ b/test/setup_bundler_test.rb @@ -74,7 +74,7 @@ def test_creates_composed_bundle assert_path_exists(".ruby-lsp") assert_path_exists(".ruby-lsp/Gemfile") assert_path_exists(".ruby-lsp/Gemfile.lock") - assert_path_exists(".ruby-lsp/main_lockfile_hash") + assert_path_exists(".ruby-lsp/freshness_hash") assert_match("ruby-lsp", File.read(".ruby-lsp/Gemfile")) assert_match("debug", File.read(".ruby-lsp/Gemfile")) refute_match("ruby-lsp-rails", File.read(".ruby-lsp/Gemfile")) @@ -101,7 +101,7 @@ def test_creates_composed_bundle_for_a_rails_app assert_path_exists(".ruby-lsp") assert_path_exists(".ruby-lsp/Gemfile") assert_path_exists(".ruby-lsp/Gemfile.lock") - assert_path_exists(".ruby-lsp/main_lockfile_hash") + assert_path_exists(".ruby-lsp/freshness_hash") assert_match("ruby-lsp", File.read(".ruby-lsp/Gemfile")) assert_match("debug", File.read(".ruby-lsp/Gemfile")) assert_match("ruby-lsp-rails", File.read(".ruby-lsp/Gemfile")) @@ -538,42 +538,24 @@ def test_ruby_lsp_rails_detection_handles_lang_from_environment def test_recovers_from_stale_lockfiles in_temp_dir do |dir| - custom_dir = File.join(dir, ".ruby-lsp") - FileUtils.mkdir_p(custom_dir) - - # Write the main Gemfile and lockfile with valid versions File.write(File.join(dir, "Gemfile"), <<~GEMFILE) source "https://rubygems.org" gem "stringio" GEMFILE - lockfile_contents = <<~LOCKFILE - GEM - remote: https://rubygems.org/ - specs: - stringio (3.1.0) - - PLATFORMS - arm64-darwin-23 - ruby - - DEPENDENCIES - stringio - - BUNDLED WITH - 2.5.7 - LOCKFILE - File.write(File.join(dir, "Gemfile.lock"), lockfile_contents) + capture_subprocess_io do + Bundler.with_unbundled_env do + system("bundle install") - # Write the lockfile hash based on the valid file - File.write(File.join(custom_dir, "main_lockfile_hash"), Digest::SHA256.hexdigest(lockfile_contents)) + # First run to set up the composed bundle correctly, including the freshness hash + run_script(dir) + end + end - # Write the composed bundle's lockfile using a fake version that doesn't exist to force bundle install to fail - File.write(File.join(custom_dir, "Gemfile"), <<~GEMFILE) - source "https://rubygems.org" - gem "stringio" - GEMFILE - File.write(File.join(custom_dir, "Gemfile.lock"), <<~LOCKFILE) + # Tamper with the composed lockfile using a fake version that doesn't exist to force bundle install to fail. + # The freshness hash from the first run still matches (main lockfile and composed Gemfile are unchanged), so + # the stale lockfile will NOT be re-copied and bundle install will encounter the bad version. + File.write(File.join(dir, ".ruby-lsp", "Gemfile.lock"), <<~LOCKFILE) GEM remote: https://rubygems.org/ specs: @@ -590,8 +572,10 @@ def test_recovers_from_stale_lockfiles 2.5.7 LOCKFILE - Bundler.with_unbundled_env do - run_script(dir) + capture_subprocess_io do + Bundler.with_unbundled_env do + run_script(dir) + end end # Verify that the script recovered and re-generated the composed bundle from scratch @@ -1145,6 +1129,43 @@ def test_beta_has_no_effect_when_ruby_lsp_is_in_the_bundle end end + def test_composed_lockfile_is_recopied_when_cli_options_change + in_temp_dir do |dir| + File.write(File.join(dir, "Gemfile"), <<~GEMFILE) + source "https://rubygems.org" + gem "rdoc" + GEMFILE + + capture_subprocess_io do + Bundler.with_unbundled_env do + system("bundle install") + + # First run with --beta creates composed bundle + run_script(dir, beta: true) + end + end + + assert_match(/>= 0\.a/, File.read(".ruby-lsp/Gemfile")) + assert_valid_gemfile(File.join(dir, ".ruby-lsp", "Gemfile")) + + # Second run without --beta. The main lockfile hasn't changed, but the composed Gemfile will change. The freshness + # check should detect this and re-copy the lockfile from main, NOT skip it. + _stdout, stderr = capture_subprocess_io do + Bundler.with_unbundled_env do + RubyLsp::SetupBundler.new(File.realpath(dir)).setup! + end + end + + refute_match(/>= 0\.a/, File.read(".ruby-lsp/Gemfile")) + assert_valid_gemfile(File.join(dir, ".ruby-lsp", "Gemfile")) + refute_match( + /Skipping composed bundle setup/, + stderr, + "Composed lockfile was not refreshed when CLI options changed", + ) + end + end + private def assert_valid_gemfile(gemfile_path)