Skip to content
Open
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
14 changes: 10 additions & 4 deletions lib/ruby_lsp/setup_bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
Expand All @@ -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

Expand Down
87 changes: 54 additions & 33 deletions test/setup_bundler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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"))
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down