Skip to content

Commit 5e29e0f

Browse files
committed
Turn custom bundler setup script into a class
1 parent 71d3fcf commit 5e29e0f

File tree

3 files changed

+150
-86
lines changed

3 files changed

+150
-86
lines changed

exe/ruby-lsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# the application's bundle
77
if ENV["BUNDLE_GEMFILE"].nil? && File.exist?("Gemfile.lock")
88
require_relative "../lib/ruby_lsp/setup_bundler"
9+
RubyLsp::SetupBundler.new(Dir.pwd).setup!
910

1011
# In some cases, like when the `ruby-lsp` is already a part of the bundle, we don't generate `.ruby-lsp/Gemfile`.
1112
# However, we still want to run the server with `bundle exec`. We need to make sure we're pointing to the right

lib/ruby_lsp/setup_bundler.rb

Lines changed: 122 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,131 @@
33

44
require "bundler"
55
require "fileutils"
6-
require "pathname"
76

87
# This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
98
# the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
109
# exact locked versions of dependencies.
1110

12-
# Do not setup a custom bundle if we're working on the Ruby LSP, since it's already included by default
13-
if Pathname.new(Dir.pwd).basename == "ruby-lsp"
14-
warn("Ruby LSP> Skipping custom bundle setup since we're working on the Ruby LSP itself")
15-
return
11+
module RubyLsp
12+
class SetupBundler
13+
extend T::Sig
14+
15+
sig { params(project_path: String).void }
16+
def initialize(project_path)
17+
@project_path = project_path
18+
@dependencies = T.let(load_dependencies, T::Hash[String, T.untyped])
19+
end
20+
21+
sig { void }
22+
def setup!
23+
# Do not setup a custom bundle if we're working on the Ruby LSP, since it's already included by default
24+
if File.basename(@project_path) == "ruby-lsp"
25+
warn("Ruby LSP> Skipping custom bundle setup since we're working on the Ruby LSP itself")
26+
run_bundle_install
27+
return
28+
end
29+
30+
# Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile
31+
if @dependencies["ruby-lsp"] && @dependencies["debug"]
32+
warn("Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in the Gemfile")
33+
run_bundle_install
34+
return
35+
end
36+
37+
# Automatically create and ignore the .ruby-lsp folder for users
38+
FileUtils.mkdir(".ruby-lsp") unless Dir.exist?(".ruby-lsp")
39+
File.write(".ruby-lsp/.gitignore", "*") unless File.exist?(".ruby-lsp/.gitignore")
40+
41+
# Write the custom `.ruby-lsp/Gemfile` if it doesn't exist or if the content doesn't match
42+
content = custom_gemfile_content
43+
44+
unless File.exist?(".ruby-lsp/Gemfile") && File.read(".ruby-lsp/Gemfile") == content
45+
File.write(".ruby-lsp/Gemfile", content)
46+
end
47+
48+
# If .ruby-lsp/Gemfile.lock already exists and the top level Gemfile.lock hasn't been modified since it was last
49+
# updated, then we're ready to boot the server
50+
if File.exist?(".ruby-lsp/Gemfile.lock") &&
51+
File.stat(".ruby-lsp/Gemfile.lock").mtime > File.stat("Gemfile.lock").mtime
52+
warn("Ruby LSP> Skipping custom bundle setup since .ruby-lsp/Gemfile.lock already exists and is up to date")
53+
run_bundle_install(".ruby-lsp/Gemfile")
54+
return
55+
end
56+
57+
FileUtils.cp("Gemfile.lock", ".ruby-lsp/Gemfile.lock")
58+
run_bundle_install(".ruby-lsp/Gemfile")
59+
end
60+
61+
private
62+
63+
sig { returns(String) }
64+
def custom_gemfile_content
65+
parts = [
66+
"# This custom gemfile is automatically generated by the Ruby LSP.",
67+
"# It should be automatically git ignored, but in any case: do not commit it to your repository.",
68+
"",
69+
"eval_gemfile(File.expand_path(\"../Gemfile\", __dir__))",
70+
]
71+
72+
unless @dependencies["ruby-lsp"]
73+
parts << 'gem "ruby-lsp", require: false, group: :development, source: "https://rubygems.org"'
74+
end
75+
76+
unless @dependencies["debug"]
77+
parts << 'gem "debug", require: false, group: :development, platforms: :mri, source: "https://rubygems.org"'
78+
end
79+
80+
parts.join("\n")
81+
end
82+
83+
sig { returns(T::Hash[String, T.untyped]) }
84+
def load_dependencies
85+
# We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more
86+
# convenient, we may end up with issues when the globally installed `ruby-lsp` version mismatches the one included
87+
# in the `Gemfile`
88+
dependencies = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")).dependencies
89+
90+
# When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we
91+
# check those as well or else we may get version mismatch errors
92+
gemspec_path = Dir.glob("*.gemspec").first
93+
if gemspec_path
94+
gemspec_dependencies = Bundler.load_gemspec(gemspec_path).dependencies.to_h { |dep| [dep.name, dep] }
95+
dependencies.merge!(gemspec_dependencies)
96+
end
97+
98+
dependencies
99+
end
100+
101+
sig { params(bundle_gemfile: T.untyped).void }
102+
def run_bundle_install(bundle_gemfile = nil)
103+
# If the user has a custom bundle path configured, we need to ensure that we will use the absolute and not
104+
# relative version of it when running `bundle install`. This is necessary to avoid installing the gems under the
105+
# `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
106+
# want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
107+
path = Bundler.settings["path"]
108+
109+
command = +""
110+
# Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
111+
command << "BUNDLE_PATH=#{File.expand_path(path, Dir.pwd)} " if path
112+
command << "BUNDLE_GEMFILE=#{bundle_gemfile} " if bundle_gemfile
113+
114+
if @dependencies["ruby-lsp"] && @dependencies["debug"]
115+
# Install gems using the custom bundle
116+
command << "bundle install "
117+
else
118+
# If ruby-lsp or debug are not in the Gemfile, try to update them to the latest version
119+
command << "bundle update "
120+
command << "ruby-lsp " unless @dependencies["ruby-lsp"]
121+
command << "debug " unless @dependencies["debug"]
122+
end
123+
124+
# Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
125+
# responses
126+
command << "1>&2"
127+
128+
# Add bundle update
129+
warn("Ruby LSP> Running bundle install for the custom bundle. This may take a while...")
130+
system(command)
131+
end
132+
end
16133
end
17-
18-
# We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more convenient, we
19-
# may end up with issues when the globally installed `ruby-lsp` version mismatches the one included in the `Gemfile`
20-
dependencies = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")).dependencies
21-
22-
# When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we check
23-
# those as well or else we may get version mismatch errors
24-
gemspec_path = Dir.glob("*.gemspec").first
25-
if gemspec_path
26-
gemspec_dependencies = Bundler.load_gemspec(gemspec_path).dependencies.to_h { |dep| [dep.name, dep] }
27-
dependencies.merge!(gemspec_dependencies)
28-
end
29-
30-
# Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile
31-
if dependencies["ruby-lsp"] && dependencies["debug"]
32-
warn("Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in the Gemfile")
33-
return
34-
end
35-
36-
# Automatically create and ignore the .ruby-lsp folder for users
37-
FileUtils.mkdir(".ruby-lsp") unless Dir.exist?(".ruby-lsp")
38-
File.write(".ruby-lsp/.gitignore", "*") unless File.exist?(".ruby-lsp/.gitignore")
39-
40-
parts = [
41-
"# This custom gemfile is automatically generated by the Ruby LSP.",
42-
"# It should be automatically git ignored, but in any case: do not commit it to your repository.",
43-
"",
44-
"eval_gemfile(File.expand_path(\"../Gemfile\", __dir__))",
45-
]
46-
47-
unless dependencies["ruby-lsp"]
48-
parts << 'gem "ruby-lsp", require: false, group: :development, source: "https://rubygems.org"'
49-
end
50-
51-
unless dependencies["debug"]
52-
parts << 'gem "debug", require: false, group: :development, platforms: :mri, source: "https://rubygems.org"'
53-
end
54-
55-
gemfile_content = parts.join("\n")
56-
57-
unless File.exist?(".ruby-lsp/Gemfile") && File.read(".ruby-lsp/Gemfile") == gemfile_content
58-
File.write(".ruby-lsp/Gemfile", gemfile_content)
59-
end
60-
61-
# If .ruby-lsp/Gemfile.lock already exists and the top level Gemfile.lock hasn't been modified since it was last
62-
# updated, then we're ready to boot the server
63-
if File.exist?(".ruby-lsp/Gemfile.lock") && File.stat(".ruby-lsp/Gemfile.lock").mtime > File.stat("Gemfile.lock").mtime
64-
warn("Ruby LSP> Skipping custom bundle setup since .ruby-lsp/Gemfile.lock already exists and is up to date")
65-
return
66-
end
67-
68-
FileUtils.cp("Gemfile.lock", ".ruby-lsp/Gemfile.lock")
69-
70-
# If the user has a custom bundle path configured, we need to ensure that we will use the absolute and not relative
71-
# version of it when running bundle install. This is necessary to avoid installing the gems under the `.ruby-lsp`
72-
# folder, which is not the user's intention. For example, if path is configured as `vendor`, we want to install it in
73-
# the top level `vendor` and not `.ruby-lsp/vendor`
74-
path = Bundler.settings["path"]
75-
76-
command = +""
77-
# Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
78-
command << "BUNDLE_PATH=#{File.expand_path(path, Dir.pwd)} " if path
79-
# Install gems using the custom bundle
80-
command << "BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle install "
81-
# Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
82-
# responses
83-
command << "1>&2"
84-
85-
warn("Ruby LSP> Running bundle install for the custom bundle. This may take a while...")
86-
system(command)

test/setup_bundler_test.rb

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22
# frozen_string_literal: true
33

44
require "test_helper"
5+
require "ruby_lsp/setup_bundler"
56

67
class SetupBundlerTest < Minitest::Test
78
def test_does_nothing_when_running_in_the_ruby_lsp
8-
run_script
9+
Object.any_instance.expects(:system).with(bundle_install_command(update: false))
10+
run_script("/some/path/ruby-lsp")
911
refute_path_exists(".ruby-lsp")
1012
end
1113

1214
def test_does_nothing_if_both_ruby_lsp_and_debug_are_in_the_bundle
15+
Object.any_instance.expects(:system).with(bundle_install_command(update: false))
1316
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "ruby-lsp" => true, "debug" => true })
1417
run_script
1518
refute_path_exists(".ruby-lsp")
1619
end
1720

1821
def test_creates_custom_bundle
19-
Object.any_instance.expects(:system).with(bundle_install_command)
22+
Object.any_instance.expects(:system).with(bundle_install_command(".ruby-lsp/Gemfile"))
2023
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({})
2124
run_script
2225

@@ -30,7 +33,7 @@ def test_creates_custom_bundle
3033
end
3134

3235
def test_copies_gemfile_lock_when_modified
33-
Object.any_instance.expects(:system).with(bundle_install_command)
36+
Object.any_instance.expects(:system).with(bundle_install_command(".ruby-lsp/Gemfile"))
3437
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({})
3538
FileUtils.mkdir(".ruby-lsp")
3639
FileUtils.touch(".ruby-lsp/Gemfile.lock")
@@ -45,9 +48,20 @@ def test_copies_gemfile_lock_when_modified
4548
FileUtils.rm_r(".ruby-lsp")
4649
end
4750

51+
def test_does_not_copy_gemfile_lock_when_not_modified
52+
Object.any_instance.expects(:system).with(bundle_install_command(".ruby-lsp/Gemfile"))
53+
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({})
54+
FileUtils.mkdir(".ruby-lsp")
55+
FileUtils.cp("Gemfile.lock", ".ruby-lsp/Gemfile.lock")
56+
57+
run_script
58+
ensure
59+
FileUtils.rm_r(".ruby-lsp")
60+
end
61+
4862
def test_uses_absolute_bundle_path_for_bundle_install
4963
Bundler.settings.set_global("path", "vendor/bundle")
50-
Object.any_instance.expects(:system).with(bundle_install_command)
64+
Object.any_instance.expects(:system).with(bundle_install_command(".ruby-lsp/Gemfile"))
5165
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({})
5266
run_script
5367
ensure
@@ -60,19 +74,21 @@ def test_uses_absolute_bundle_path_for_bundle_install
6074

6175
# This method runs the script and then immediately unloads it. This allows us to make assertions against the effects
6276
# of running the script multiple times
63-
def run_script
64-
path = File.expand_path("../lib/ruby_lsp/setup_bundler.rb", __dir__)
65-
require path
66-
ensure
67-
$LOADED_FEATURES.delete(path)
77+
def run_script(path = "/fake/project/path")
78+
RubyLsp::SetupBundler.new(path).setup!
6879
end
6980

70-
def bundle_install_command
81+
def bundle_install_command(bundle_gemfile = nil, update: true)
7182
path = Bundler.settings["path"]
7283

7384
command = +""
7485
command << "BUNDLE_PATH=#{File.expand_path(path, Dir.pwd)} " if path
75-
command << "BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle install "
86+
command << "BUNDLE_GEMFILE=#{bundle_gemfile} " if bundle_gemfile
87+
command << if update
88+
"bundle update ruby-lsp debug "
89+
else
90+
"bundle install "
91+
end
7692
command << "1>&2"
7793
end
7894
end

0 commit comments

Comments
 (0)