4
4
require "sorbet-runtime"
5
5
require "bundler"
6
6
require "fileutils"
7
+ require "pathname"
7
8
8
9
# This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
9
10
# the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
@@ -13,107 +14,133 @@ module RubyLsp
13
14
class SetupBundler
14
15
extend T ::Sig
15
16
17
+ class BundleNotLocked < StandardError ; end
18
+
16
19
sig { params ( project_path : String ) . void }
17
20
def initialize ( project_path )
18
21
@project_path = project_path
19
- @dependencies = T . let ( load_dependencies , T ::Hash [ String , T . untyped ] )
20
- @custom_bundle_dependencies = T . let (
21
- if File . exist? ( ".ruby-lsp/Gemfile.lock" )
22
- Bundler ::LockfileParser . new ( Bundler . read_file ( ".ruby-lsp/Gemfile.lock" ) ) . dependencies
23
- else
24
- { }
22
+
23
+ # Custom bundle paths
24
+ @custom_dir = T . let ( Pathname . new ( ".ruby-lsp" ) . expand_path ( Dir . pwd ) , Pathname )
25
+ @custom_gemfile = T . let ( @custom_dir + "Gemfile" , Pathname )
26
+ @custom_lockfile = T . let ( @custom_dir + "Gemfile.lock" , Pathname )
27
+
28
+ # Regular bundle paths
29
+ @gemfile = T . let (
30
+ begin
31
+ Bundler . default_gemfile
32
+ rescue Bundler ::GemfileNotFound
33
+ nil
25
34
end ,
26
- T :: Hash [ String , T . untyped ] ,
35
+ T . nilable ( Pathname ) ,
27
36
)
37
+ @lockfile = T . let ( @gemfile ? Bundler . default_lockfile : nil , T . nilable ( Pathname ) )
38
+
39
+ @dependencies = T . let ( load_dependencies , T ::Hash [ String , T . untyped ] )
40
+ @custom_bundle_dependencies = T . let ( custom_bundle_dependencies , T ::Hash [ String , T . untyped ] )
28
41
end
29
42
30
- sig { void }
43
+ # Setups up the custom bundle and returns the `BUNDLE_GEMFILE` and `BUNDLE_PATH` that should be used for running the
44
+ # server
45
+ sig { returns ( [ String , T . nilable ( String ) ] ) }
31
46
def setup!
32
- # Do not setup a custom bundle if we're working on the Ruby LSP, since it's already included by default
33
- if File . basename ( @project_path ) == "ruby-lsp"
34
- warn ( "Ruby LSP> Skipping custom bundle setup since we're working on the Ruby LSP itself" )
35
- run_bundle_install
36
- return
37
- end
47
+ raise BundleNotLocked if @gemfile &.exist? && !@lockfile &.exist?
38
48
39
49
# Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile
40
50
if @dependencies [ "ruby-lsp" ] && @dependencies [ "debug" ]
41
- warn ( "Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in the Gemfile " )
51
+ warn ( "Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in #{ @gemfile } " )
42
52
43
53
# If the user decided to add the `ruby-lsp` and `debug` to their Gemfile after having already run the Ruby LSP,
44
54
# then we need to remove the `.ruby-lsp` folder, otherwise we will run `bundle install` for the top level and
45
55
# try to execute the Ruby LSP using the custom bundle, which will fail since the gems are not installed there
46
- FileUtils . rm_r ( ".ruby-lsp" ) if Dir . exist? ( ".ruby-lsp" )
47
- run_bundle_install
48
- return
56
+ @custom_dir . rmtree if @custom_dir . exist?
57
+ return run_bundle_install
49
58
end
50
59
51
60
# Automatically create and ignore the .ruby-lsp folder for users
52
- FileUtils . mkdir ( ".ruby-lsp" ) unless Dir . exist? ( ".ruby-lsp" )
53
- File . write ( ".ruby-lsp/.gitignore" , "*" ) unless File . exist? ( ".ruby-lsp/.gitignore" )
61
+ @custom_dir . mkpath unless @custom_dir . exist?
62
+ ignore_file = @custom_dir + ".gitignore"
63
+ ignore_file . write ( "*" ) unless ignore_file . exist?
54
64
55
- # Write the custom `.ruby-lsp/Gemfile` if it doesn't exist or if the content doesn't match
56
- content = custom_gemfile_content
65
+ write_custom_gemfile
57
66
58
- unless File . exist? ( ".ruby-lsp/Gemfile" ) && File . read ( ".ruby-lsp/Gemfile" ) == content
59
- File . write ( ".ruby-lsp/Gemfile" , content )
67
+ unless @gemfile &.exist? && @lockfile &.exist?
68
+ warn ( "Ruby LSP> Skipping lockfile copies because there's no top level bundle" )
69
+ return run_bundle_install ( @custom_gemfile )
60
70
end
61
71
62
72
# If .ruby-lsp/Gemfile.lock already exists and the top level Gemfile.lock hasn't been modified since it was last
63
73
# updated, then we're ready to boot the server
64
- if File . exist? ( ".ruby-lsp/Gemfile.lock" ) &&
65
- File . stat ( ".ruby-lsp/Gemfile.lock" ) . mtime > File . stat ( "Gemfile.lock" ) . mtime
66
- warn ( "Ruby LSP> Skipping custom bundle setup since .ruby-lsp/Gemfile.lock already exists and is up to date" )
67
- run_bundle_install ( ".ruby-lsp/Gemfile" )
68
- return
74
+ if @custom_lockfile . exist? && @custom_lockfile . stat . mtime > @lockfile . stat . mtime
75
+ warn ( "Ruby LSP> Skipping custom bundle setup since #{ @custom_lockfile } already exists and is up to date" )
76
+ return run_bundle_install ( @custom_gemfile )
69
77
end
70
78
71
- FileUtils . cp ( "Gemfile.lock" , ".ruby-lsp/Gemfile.lock" )
72
- run_bundle_install ( ".ruby-lsp/Gemfile" )
79
+ FileUtils . cp ( @lockfile . to_s , @custom_lockfile . to_s )
80
+ run_bundle_install ( @custom_gemfile )
73
81
end
74
82
75
83
private
76
84
77
- sig { returns ( String ) }
78
- def custom_gemfile_content
85
+ sig { returns ( T ::Hash [ String , T . untyped ] ) }
86
+ def custom_bundle_dependencies
87
+ return { } unless @custom_lockfile . exist?
88
+
89
+ ENV [ "BUNDLE_GEMFILE" ] = @custom_gemfile . to_s
90
+ Bundler ::LockfileParser . new ( @custom_lockfile . read ) . dependencies
91
+ ensure
92
+ ENV . delete ( "BUNDLE_GEMFILE" )
93
+ end
94
+
95
+ sig { void }
96
+ def write_custom_gemfile
79
97
parts = [
80
98
"# This custom gemfile is automatically generated by the Ruby LSP." ,
81
99
"# It should be automatically git ignored, but in any case: do not commit it to your repository." ,
82
100
"" ,
83
- "eval_gemfile(File.expand_path(\" ../Gemfile\" , __dir__))" ,
84
101
]
85
102
103
+ # If there's a top level Gemfile, we want to evaluate from the custom bundle. We get the source from the top level
104
+ # Gemfile, so if there isn't one we need to add a default source
105
+ if @gemfile &.exist?
106
+ parts << "eval_gemfile(File.expand_path(\" ../Gemfile\" , __dir__))"
107
+ else
108
+ parts . unshift ( 'source "https://rubygems.org"' )
109
+ end
110
+
86
111
unless @dependencies [ "ruby-lsp" ]
87
- parts << 'gem "ruby-lsp", require: false, group: :development, source: "https://rubygems.org" '
112
+ parts << 'gem "ruby-lsp", require: false, group: :development'
88
113
end
89
114
90
115
unless @dependencies [ "debug" ]
91
- parts << 'gem "debug", require: false, group: :development, platforms: :mri, source: "https://rubygems.org" '
116
+ parts << 'gem "debug", require: false, group: :development, platforms: :mri'
92
117
end
93
118
94
- parts . join ( "\n " )
119
+ content = parts . join ( "\n " )
120
+ @custom_gemfile . write ( content ) unless @custom_gemfile . exist? && @custom_gemfile . read == content
95
121
end
96
122
97
123
sig { returns ( T ::Hash [ String , T . untyped ] ) }
98
124
def load_dependencies
125
+ return { } unless @lockfile &.exist?
126
+
99
127
# We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more
100
128
# convenient, we may end up with issues when the globally installed `ruby-lsp` version mismatches the one included
101
129
# in the `Gemfile`
102
- dependencies = Bundler ::LockfileParser . new ( Bundler . read_file ( "Gemfile.lock" ) ) . dependencies
130
+ dependencies = Bundler ::LockfileParser . new ( @lockfile . read ) . dependencies
103
131
104
132
# When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we
105
- # check those as well or else we may get version mismatch errors
106
- gemspec_path = Dir . glob ( "*.gemspec" ) . first
107
- if gemspec_path
108
- gemspec_dependencies = Bundler . load_gemspec ( gemspec_path ) . dependencies . to_h { |dep | [ dep . name , dep ] }
109
- dependencies . merge! ( gemspec_dependencies )
133
+ # check those as well or else we may get version mismatch errors. Notice that bundler allows more than one
134
+ # gemspec, so we need to make sure we go through all of them
135
+ Dir . glob ( "{,*}.gemspec" ) . each do |path |
136
+ dependencies . merge! ( Bundler . load_gemspec ( path ) . dependencies . to_h { |dep | [ dep . name , dep ] } )
110
137
end
111
138
112
139
dependencies
113
140
end
114
141
115
- sig { params ( bundle_gemfile : T . untyped ) . void }
116
- def run_bundle_install ( bundle_gemfile = nil )
142
+ sig { params ( bundle_gemfile : T . nilable ( Pathname ) ) . returns ( [ String , T . nilable ( String ) ] ) }
143
+ def run_bundle_install ( bundle_gemfile = @gemfile )
117
144
# If the user has a custom bundle path configured, we need to ensure that we will use the absolute and not
118
145
# relative version of it when running `bundle install`. This is necessary to avoid installing the gems under the
119
146
# `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
@@ -122,7 +149,7 @@ def run_bundle_install(bundle_gemfile = nil)
122
149
123
150
# Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
124
151
env = { }
125
- env [ "BUNDLE_GEMFILE" ] = bundle_gemfile if bundle_gemfile
152
+ env [ "BUNDLE_GEMFILE" ] = bundle_gemfile . to_s
126
153
env [ "BUNDLE_PATH" ] = File . expand_path ( path , Dir . pwd ) if path
127
154
128
155
# If both `ruby-lsp` and `debug` are already in the Gemfile, then we shouldn't try to upgrade them or else we'll
@@ -150,6 +177,7 @@ def run_bundle_install(bundle_gemfile = nil)
150
177
# Add bundle update
151
178
warn ( "Ruby LSP> Running bundle install for the custom bundle. This may take a while..." )
152
179
system ( env , command )
180
+ [ bundle_gemfile . to_s , path ]
153
181
end
154
182
end
155
183
end
0 commit comments