Skip to content

Commit f02c97a

Browse files
authored
Purge changed deps (#777)
* update readme * run formatter * Unload changed deps Purge all modules from changed and removed deps, remove from code paths Fixes #120 Fixes #688 * run ci on ubuntu 20.04 22.04 is OTP >= 24 * fix warnings
1 parent 2984b66 commit f02c97a

File tree

6 files changed

+146
-34
lines changed

6 files changed

+146
-34
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
jobs:
1212
mix_test:
1313
name: mix test (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}})
14-
runs-on: ubuntu-latest
14+
runs-on: ubuntu-20.04
1515
strategy:
1616
fail-fast: false
1717
matrix:
@@ -53,7 +53,7 @@ jobs:
5353

5454
static_analysis:
5555
name: static analysis (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}})
56-
runs-on: ubuntu-latest
56+
runs-on: ubuntu-20.04
5757
strategy:
5858
matrix:
5959
include:

.github/workflows/docsite.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
jobs:
99
build:
1010
name: Build Mkdocs website
11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-20.04
1212
strategy:
1313
max-parallel: 1
1414
container:
@@ -27,7 +27,7 @@ jobs:
2727
publish:
2828
needs: build
2929
name: Publish Mkdocs website to GH Pages
30-
runs-on: ubuntu-latest
30+
runs-on: ubuntu-20.04
3131
strategy:
3232
max-parallel: 1
3333
steps:

.github/workflows/release-asset.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
jobs:
99
release:
1010
name: Create draft release
11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-20.04
1212
outputs:
1313
upload_url: ${{steps.create_release.outputs.upload_url}}
1414

@@ -26,7 +26,7 @@ jobs:
2626

2727
build:
2828
name: Build and publish release asset
29-
runs-on: ubuntu-latest
29+
runs-on: ubuntu-20.04
3030
needs: release
3131

3232
strategy:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ For VSCode install the extension: https://marketplace.visualstudio.com/items?ite
8888

8989
Elixir:
9090

91-
- 1.11.0 minimum
91+
- 1.12.3 minimum
9292

9393
Erlang:
9494

apps/language_server/lib/language_server/build.ex

Lines changed: 136 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,38 @@ defmodule ElixirLS.LanguageServer.Build do
1414
:timer.tc(fn ->
1515
Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}")
1616

17+
# read cache before cleaning up mix state in reload_project
18+
cached_deps = read_cached_deps()
19+
1720
case reload_project() do
1821
{:ok, mixfile_diagnostics} ->
1922
# 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
2723

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+
)
3145

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
3449

3550
{:error, mixfile_diagnostics} ->
3651
Server.build_finished(parent, {:error, mixfile_diagnostics})
@@ -73,8 +88,8 @@ defmodule ElixirLS.LanguageServer.Build do
7388
# see https://github.com/elixir-lsp/elixir-ls/issues/120
7489
# originally reported in https://github.com/JakeBecker/elixir-ls/issues/71
7590
# 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()
7893

7994
Mix.Task.clear()
8095

@@ -179,24 +194,104 @@ defmodule ElixirLS.LanguageServer.Build do
179194
:code.delete(module)
180195
end
181196

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)
189281
end
190282
end
191283

192-
defp fetch_deps do
193-
# FIXME: Private API and struct
284+
defp fetch_deps(current_deps) do
285+
# FIXME: private struct
194286
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} ->
197289
case status do
198-
{:unavailable, _} -> true
290+
{:unavailable, _} -> scm.fetchable?()
199291
{:nomatchvsn, _} -> true
292+
:nolock -> true
293+
:lockoutdated -> true
294+
{:lockmismatch, _} -> true
200295
_ -> false
201296
end
202297
end)
@@ -215,6 +310,8 @@ defmodule ElixirLS.LanguageServer.Build do
215310
:info,
216311
"Done fetching deps"
217312
)
313+
else
314+
Logger.debug("All deps are up to date")
218315
end
219316

220317
:ok
@@ -237,4 +334,17 @@ defmodule ElixirLS.LanguageServer.Build do
237334

238335
Code.compiler_options(options)
239336
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
240350
end

apps/language_server/test/providers/completion_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
141141

142142
{line, char} = {3, 17}
143143
TestUtils.assert_has_cursor_char(text, line, char)
144-
{:ok, %{"items" => [first_suggestion | _tail]}} = Completion.completion(text, line, char, @supports)
144+
145+
{:ok, %{"items" => [first_suggestion | _tail]}} =
146+
Completion.completion(text, line, char, @supports)
145147

146148
assert first_suggestion["label"] === "fn"
147149
end

0 commit comments

Comments
 (0)