@@ -14,23 +14,38 @@ defmodule ElixirLS.LanguageServer.Build do
14
14
:timer . tc ( fn ->
15
15
Logger . info ( "Starting build with MIX_ENV: #{ Mix . env ( ) } MIX_TARGET: #{ Mix . target ( ) } " )
16
16
17
+ # read cache before cleaning up mix state in reload_project
18
+ cached_deps = read_cached_deps ( )
19
+
17
20
case reload_project ( ) do
18
21
{ :ok , mixfile_diagnostics } ->
19
22
# FIXME: Private API
20
- if Keyword . get ( opts , :fetch_deps? ) and
21
- Mix.Dep . load_on_environment ( [ ] ) != cached_deps ( ) do
22
- # NOTE: Clear deps cache when deps in mix.exs has change to prevent
23
- # formatter crash from clearing deps during build.
24
- :ok = Mix.Project . clear_deps_cache ( )
25
- fetch_deps ( )
26
- end
27
23
28
- # if we won't do it elixir >= 1.11 warns that protocols have already been consolidated
29
- purge_consolidated_protocols ( )
30
- { status , diagnostics } = run_mix_compile ( )
24
+ try do
25
+ # this call can raise
26
+ current_deps = Mix.Dep . load_on_environment ( [ ] )
27
+
28
+ purge_changed_deps ( current_deps , cached_deps )
29
+
30
+ if Keyword . get ( opts , :fetch_deps? ) and current_deps != cached_deps do
31
+ fetch_deps ( current_deps )
32
+ end
33
+
34
+ # if we won't do it elixir >= 1.11 warns that protocols have already been consolidated
35
+ purge_consolidated_protocols ( )
36
+ { status , diagnostics } = run_mix_compile ( )
37
+
38
+ diagnostics = Diagnostics . normalize ( diagnostics , root_path )
39
+ Server . build_finished ( parent , { status , mixfile_diagnostics ++ diagnostics } )
40
+ rescue
41
+ e ->
42
+ Logger . warn (
43
+ "Mix.Dep.load_on_environment([]) failed: #{ inspect ( e . __struct__ ) } #{ Exception . message ( e ) } "
44
+ )
31
45
32
- diagnostics = Diagnostics . normalize ( diagnostics , root_path )
33
- Server . build_finished ( parent , { status , mixfile_diagnostics ++ diagnostics } )
46
+ # TODO pass diagnostic
47
+ Server . build_finished ( parent , { :error , [ ] } )
48
+ end
34
49
35
50
{ :error , mixfile_diagnostics } ->
36
51
Server . build_finished ( parent , { :error , mixfile_diagnostics } )
@@ -73,8 +88,8 @@ defmodule ElixirLS.LanguageServer.Build do
73
88
# see https://github.com/elixir-lsp/elixir-ls/issues/120
74
89
# originally reported in https://github.com/JakeBecker/elixir-ls/issues/71
75
90
# Note that `Mix.State.clear_cache()` is not enough (at least on elixir 1.14)
76
- # FIXME: Private API
77
- Mix.Dep . clear_cached ( )
91
+ Mix.Project . clear_deps_cache ( )
92
+ Mix.State . clear_cache ( )
78
93
79
94
Mix.Task . clear ( )
80
95
@@ -179,24 +194,104 @@ defmodule ElixirLS.LanguageServer.Build do
179
194
:code . delete ( module )
180
195
end
181
196
182
- defp cached_deps do
183
- try do
184
- # FIXME: Private API
185
- Mix.Dep . cached ( )
186
- rescue
187
- _ ->
188
- [ ]
197
+ defp purge_app ( app ) do
198
+ # TODO use hack with ets
199
+ modules =
200
+ case :application . get_key ( app , :modules ) do
201
+ { :ok , modules } -> modules
202
+ _ -> [ ]
203
+ end
204
+
205
+ if modules != [ ] do
206
+ Logger . debug ( "Purging #{ length ( modules ) } modules from #{ app } " )
207
+ for module <- modules , do: purge_module ( module )
208
+ end
209
+
210
+ Logger . debug ( "Unloading #{ app } " )
211
+
212
+ case Application . stop ( app ) do
213
+ :ok -> :ok
214
+ { :error , :not_started } -> :ok
215
+ { :error , error } -> Logger . error ( "Application.stop failed for #{ app } : #{ inspect ( error ) } " )
216
+ end
217
+
218
+ case Application . unload ( app ) do
219
+ :ok -> :ok
220
+ { :error , error } -> Logger . error ( "Application.unload failed for #{ app } : #{ inspect ( error ) } " )
221
+ end
222
+
223
+ # Code.delete_path()
224
+ end
225
+
226
+ defp get_deps_by_app ( deps ) , do: get_deps_by_app ( deps , % { } )
227
+ defp get_deps_by_app ( [ ] , acc ) , do: acc
228
+
229
+ defp get_deps_by_app ( [ curr = % Mix.Dep { app: app , deps: deps } | rest ] , acc ) do
230
+ acc = get_deps_by_app ( deps , acc )
231
+
232
+ list =
233
+ case acc [ app ] do
234
+ nil -> [ curr ]
235
+ list -> [ curr | list ]
236
+ end
237
+
238
+ get_deps_by_app ( rest , acc |> Map . put ( app , list ) )
239
+ end
240
+
241
+ defp maybe_purge_dep ( % Mix.Dep { status: status , deps: deps } = dep ) do
242
+ for dep <- deps , do: maybe_purge_dep ( dep )
243
+
244
+ purge? =
245
+ case status do
246
+ { :nomatchvsn , _ } -> true
247
+ :lockoutdated -> true
248
+ { :lockmismatch , _ } -> true
249
+ _ -> false
250
+ end
251
+
252
+ if purge? do
253
+ purge_dep ( dep )
254
+ end
255
+ end
256
+
257
+ defp purge_dep ( % Mix.Dep { app: app } = dep ) do
258
+ for path <- Mix.Dep . load_paths ( dep ) do
259
+ Code . delete_path ( path )
260
+ end
261
+
262
+ purge_app ( app )
263
+ end
264
+
265
+ defp purge_changed_deps ( _current_deps , nil ) , do: :ok
266
+
267
+ defp purge_changed_deps ( current_deps , cached_deps ) do
268
+ current_deps_by_app = get_deps_by_app ( current_deps )
269
+ cached_deps_by_app = get_deps_by_app ( cached_deps )
270
+ removed_apps = Map . keys ( cached_deps_by_app ) -- Map . keys ( current_deps_by_app )
271
+
272
+ removed_deps = cached_deps_by_app |> Map . take ( removed_apps )
273
+
274
+ for { _app , deps } <- removed_deps ,
275
+ dep <- deps do
276
+ purge_dep ( dep )
277
+ end
278
+
279
+ for dep <- current_deps do
280
+ maybe_purge_dep ( dep )
189
281
end
190
282
end
191
283
192
- defp fetch_deps do
193
- # FIXME: Private API and struct
284
+ defp fetch_deps ( current_deps ) do
285
+ # FIXME: private struct
194
286
missing_deps =
195
- Mix.Dep . load_on_environment ( [ ] )
196
- |> Enum . filter ( fn % Mix.Dep { status: status } ->
287
+ current_deps
288
+ |> Enum . filter ( fn % Mix.Dep { status: status , scm: scm } ->
197
289
case status do
198
- { :unavailable , _ } -> true
290
+ { :unavailable , _ } -> scm . fetchable? ( )
199
291
{ :nomatchvsn , _ } -> true
292
+ :nolock -> true
293
+ :lockoutdated -> true
294
+ { :lockmismatch , _ } -> true
200
295
_ -> false
201
296
end
202
297
end )
@@ -215,6 +310,8 @@ defmodule ElixirLS.LanguageServer.Build do
215
310
:info ,
216
311
"Done fetching deps"
217
312
)
313
+ else
314
+ Logger . debug ( "All deps are up to date" )
218
315
end
219
316
220
317
:ok
@@ -237,4 +334,17 @@ defmodule ElixirLS.LanguageServer.Build do
237
334
238
335
Code . compiler_options ( options )
239
336
end
337
+
338
+ defp read_cached_deps ( ) do
339
+ # FIXME: Private api
340
+ # we cannot use Mix.Dep.cached() here as it tries to load deps
341
+ if project = Mix.Project . get ( ) do
342
+ env_target = { Mix . env ( ) , Mix . target ( ) }
343
+
344
+ case Mix.State . read_cache ( { :cached_deps , project } ) do
345
+ { ^ env_target , deps } -> deps
346
+ _ -> nil
347
+ end
348
+ end
349
+ end
240
350
end
0 commit comments