Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically install ruby-lsp-rails as part of custom bundle #1381

Merged
merged 1 commit into from
Feb 16, 2024
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
52 changes: 34 additions & 18 deletions lib/ruby_lsp/setup_bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,18 @@ def initialize(project_path, **options)
def setup!
raise BundleNotLocked if @gemfile&.exist? && !@lockfile&.exist?

# Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile
if @dependencies["ruby-lsp"] && @dependencies["debug"]
# Do not set up a custom bundle if LSP dependencies are already in the Gemfile
if @dependencies["ruby-lsp"] &&
@dependencies["debug"] &&
(@dependencies["rails"] ? @dependencies["ruby-lsp-rails"] : true)
$stderr.puts(
"Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in #{@gemfile}",
"Ruby LSP> Skipping custom bundle setup since LSP dependencies are already in #{@gemfile}",
)

# If the user decided to add the `ruby-lsp` and `debug` to their Gemfile after having already run the Ruby LSP,
# then we need to remove the `.ruby-lsp` folder, otherwise we will run `bundle install` for the top level and
# try to execute the Ruby LSP using the custom bundle, which will fail since the gems are not installed there
# If the user decided to add `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) to their Gemfile after
# having already run the Ruby LSP, then we need to remove the `.ruby-lsp` folder, otherwise we will run `bundle
# install` for the top level and try to execute the Ruby LSP using the custom bundle, which will fail since the
# gems are not installed there
@custom_dir.rmtree if @custom_dir.exist?
return run_bundle_install
end
Expand Down Expand Up @@ -143,6 +146,10 @@ def write_custom_gemfile
parts << 'gem "debug", require: false, group: :development, platforms: :mri'
end

if @dependencies["rails"] && !@dependencies["ruby-lsp-rails"]
parts << 'gem "ruby-lsp-rails", require: false, group: :development'
end

content = parts.join("\n")
@custom_gemfile.write(content) unless @custom_gemfile.exist? && @custom_gemfile.read == content
end
Expand Down Expand Up @@ -183,22 +190,24 @@ def run_bundle_install(bundle_gemfile = @gemfile)
local_config_path = File.join(Dir.pwd, ".bundle")
env["BUNDLE_APP_CONFIG"] = local_config_path if Dir.exist?(local_config_path)

# If both `ruby-lsp` and `debug` are already in the Gemfile, then we shouldn't try to upgrade them or else we'll
# produce undesired source control changes. If the custom bundle was just created and either `ruby-lsp` or `debug`
# weren't a part of the Gemfile, then we need to run `bundle install` for the first time to generate the
# Gemfile.lock with them included or else Bundler will complain that they're missing. We can only update if the
# custom `.ruby-lsp/Gemfile.lock` already exists and includes both gems
# If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
# to upgrade them or else we'll produce undesired source control changes. If the custom bundle was just created
# and any of `ruby-lsp`, `ruby-lsp-rails` or `debug` weren't a part of the Gemfile, then we need to run `bundle
# install` for the first time to generate the Gemfile.lock with them included or else Bundler will complain that
# they're missing. We can only update if the custom `.ruby-lsp/Gemfile.lock` already exists and includes all gems

# When not updating, we run `(bundle check || bundle install)`
# When updating, we run `((bundle check && bundle update ruby-lsp debug) || bundle install)`
command = +"(bundle check"

if should_bundle_update?
# If ruby-lsp or debug are not in the Gemfile, try to update them to the latest version
# If any of `ruby-lsp`, `ruby-lsp-rails` or `debug` are not in the Gemfile, try to update them to the latest
# version
command.prepend("(")
command << " && bundle update "
command << "ruby-lsp " unless @dependencies["ruby-lsp"]
command << "debug " unless @dependencies["debug"]
command << "ruby-lsp-rails " if @dependencies["rails"] && !@dependencies["ruby-lsp-rails"]
command << "--pre" if @experimental
command.delete_suffix!(" ")
command << ")"
Expand All @@ -221,13 +230,20 @@ def run_bundle_install(bundle_gemfile = @gemfile)

sig { returns(T::Boolean) }
def should_bundle_update?
# If both `ruby-lsp` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it will produce
# version control changes
return false if @dependencies["ruby-lsp"] && @dependencies["debug"]
# If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it
# will produce version control changes
if @dependencies["rails"]
return false if @dependencies.values_at("ruby-lsp", "ruby-lsp-rails", "debug").all?

# If the custom lockfile doesn't include `ruby-lsp`, `ruby-lsp-rails` or `debug`, we need to run bundle install
# before updating
return false if custom_bundle_dependencies.values_at("ruby-lsp", "debug", "ruby-lsp-rails").any?(&:nil?)
else
return false if @dependencies.values_at("ruby-lsp", "debug").all?

# If the custom lockfile doesn't include either the `ruby-lsp` or `debug`, we need to run bundle install before
# updating
return false if custom_bundle_dependencies["ruby-lsp"].nil? || custom_bundle_dependencies["debug"].nil?
# If the custom lockfile doesn't include `ruby-lsp` or `debug`, we need to run bundle install before updating
return false if custom_bundle_dependencies.values_at("ruby-lsp", "debug").any?(&:nil?)
end

# If the last updated file doesn't exist or was updated more than 4 hours ago, we should update
!@last_updated_path.exist? || Time.parse(@last_updated_path.read) < (Time.now - FOUR_HOURS)
Expand Down
81 changes: 80 additions & 1 deletion test/setup_bundler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ def test_does_nothing_if_both_ruby_lsp_and_debug_are_in_the_bundle
refute_path_exists(".ruby-lsp")
end

def test_does_nothing_if_both_ruby_lsp_and_debug_are_in_the_bundle2
Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2")
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({
"ruby-lsp" => true,
"rails" => true,
"ruby-lsp-rails" => true,
"debug" => true,
})
run_script
refute_path_exists(".ruby-lsp")
end

def test_removes_ruby_lsp_folder_if_both_gems_were_added_to_the_bundle
Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2")
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "ruby-lsp" => true, "debug" => true })
Expand All @@ -22,6 +34,27 @@ def test_removes_ruby_lsp_folder_if_both_gems_were_added_to_the_bundle
FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp")
end

def test_in_a_rails_app_does_nothing_if_ruby_lsp_and_ruby_lsp_rails_and_debug_are_in_the_bundle
Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2")
Bundler::LockfileParser.any_instance.expects(:dependencies)
.returns({ "ruby-lsp" => true, "ruby-lsp-rails" => true, "debug" => true })
run_script
refute_path_exists(".ruby-lsp")
ensure
FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp")
end

def test_in_a_rails_app_removes_ruby_lsp_folder_if_all_gems_were_added_to_the_bundle
Object.any_instance.expects(:system).with(bundle_env, "(bundle check || bundle install) 1>&2")
Bundler::LockfileParser.any_instance.expects(:dependencies)
.returns({ "ruby-lsp" => true, "ruby-lsp-rails" => true, "debug" => true })
FileUtils.mkdir(".ruby-lsp")
run_script
refute_path_exists(".ruby-lsp")
ensure
FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp")
end

def test_creates_custom_bundle
Object.any_instance.expects(:system).with(bundle_env(".ruby-lsp/Gemfile"), "(bundle check || bundle install) 1>&2")
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({}).at_least_once
Expand All @@ -32,9 +65,26 @@ def test_creates_custom_bundle
assert_path_exists(".ruby-lsp/Gemfile.lock")
assert_path_exists(".ruby-lsp/main_lockfile_hash")
assert_match("ruby-lsp", File.read(".ruby-lsp/Gemfile"))
refute_match("ruby-lsp-rails", File.read(".ruby-lsp/Gemfile"))
assert_match("debug", File.read(".ruby-lsp/Gemfile"))
ensure
FileUtils.rm_r(".ruby-lsp")
FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp")
end

def test_creates_custom_bundle_for_a_rails_app
Object.any_instance.expects(:system).with(bundle_env(".ruby-lsp/Gemfile"), "(bundle check || bundle install) 1>&2")
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "rails" => true }).at_least_once
run_script

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_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"))
ensure
FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp")
end

def test_changing_lockfile_causes_custom_bundle_to_be_rebuilt
Expand Down Expand Up @@ -422,6 +472,35 @@ def test_ensures_lockfile_remotes_are_relative_to_default_gemfile
end
end

def test_ruby_lsp_rails_is_automatically_included_in_rails_apps
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
File.write(File.join(dir, "Gemfile"), <<~GEMFILE)
source "https://rubygems.org"
gem "rails"
GEMFILE

capture_subprocess_io do
Bundler.with_unbundled_env do
# Run bundle install to generate the lockfile
system("bundle install")
end
end

Object.any_instance.expects(:system).with(
bundle_env(".ruby-lsp/Gemfile"),
"(bundle check || bundle install) 1>&2",
)
Bundler.with_unbundled_env do
run_script
end

assert_path_exists(".ruby-lsp/Gemfile")
assert_match('gem "ruby-lsp-rails"', File.read(".ruby-lsp/Gemfile"))
end
end
end

private

# This method runs the script and then immediately unloads it. This allows us to make assertions against the effects
Expand Down
Loading