Skip to content

Commit 81c9f38

Browse files
authored
Build improvements (#859)
* return all warnings from compilation make diagnostics do not disappear in umbrella disable protoco consolidation * unload MixProject modules from deps and apps reduce number of warnings emitted on recompilation * debug unexpected responses * add TODO fix typo * we no longer need to purge protocols as we are not consolidating * Do not emit protocol consolidation diagnostics * since consolidation is disabled we don't need to dialize consolidated beams revert to dialysing non consolidated protocol beams from the build dir * add comment * ignore_already_consolidated is available on elixir 1.14+ * fix dialyzer * return 0 exit code on restart * do not emit undefined warnings from mix.exs compile * make debugger behave more similar to mix 1.14 * add todo * remove dbg * add comment * correctly append params * fix crash
1 parent 06e1dc0 commit 81c9f38

File tree

9 files changed

+172
-89
lines changed

9 files changed

+172
-89
lines changed

apps/elixir_ls_debugger/lib/debugger/server.ex

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ defmodule ElixirLS.Debugger.Server do
2727
}
2828

2929
alias ElixirLS.Debugger.Stacktrace.Frame
30-
alias ElixirLS.Utils.MixfileHelpers
3130
use GenServer
3231
use Protocol
3332

@@ -926,27 +925,23 @@ defmodule ElixirLS.Debugger.Server do
926925
end
927926

928927
defp initialize(%{"projectDir" => project_dir} = config) do
929-
prev_env = Mix.env()
930928
task = config["task"]
931-
task_args = config["taskArgs"]
929+
task_args = config["taskArgs"] || []
932930
auto_interpret_files? = Map.get(config, "debugAutoInterpretAllModules", true)
933931

934932
set_stack_trace_mode(config["stackTraceMode"])
935933
set_env_vars(config["env"])
936934

937935
File.cd!(project_dir)
938936

939-
# Mixfile may already be loaded depending on cwd when launching debugger task
940-
mixfile = Path.absname(MixfileHelpers.mix_exs())
937+
# the startup sequence here is taken from
938+
# https://github.com/elixir-lang/elixir/blob/v1.14.4/lib/mix/lib/mix/cli.ex#L158
939+
# we assume that mix is already started and has archives and tasks loaded
940+
ElixirLS.Utils.Launch.load_mix_exs()
941+
{task, task_args} = ElixirLS.Utils.Launch.get_task(List.wrap(task) ++ task_args)
942+
ElixirLS.Utils.Launch.maybe_change_env_and_target(task)
941943

942-
# FIXME: Private API
943-
unless match?(%{file: ^mixfile}, Mix.ProjectStack.peek()) do
944-
Code.compile_file(MixfileHelpers.mix_exs())
945-
end
946-
947-
task = task || Mix.Project.config()[:default_task]
948-
env = task_env(task)
949-
if env != prev_env, do: change_env(env)
944+
Output.debugger_console("Running with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}\n")
950945

951946
Mix.Task.run("loadconfig")
952947

@@ -1068,32 +1063,8 @@ defmodule ElixirLS.Debugger.Server do
10681063
end
10691064
end
10701065

1071-
defp change_env(env) do
1072-
Mix.env(env)
1073-
1074-
# FIXME: Private API
1075-
if project = Mix.Project.pop() do
1076-
%{name: name, file: file} = project
1077-
:code.purge(name)
1078-
:code.delete(name)
1079-
# It's important to use `compile_file` here instead of `require_file`
1080-
# because we are recompiling this file to reload the mix project back onto
1081-
# the project stack.
1082-
Code.compile_file(file)
1083-
end
1084-
end
1085-
1086-
defp task_env(task) do
1087-
if System.get_env("MIX_ENV") do
1088-
String.to_atom(System.get_env("MIX_ENV"))
1089-
else
1090-
task = String.to_atom(task)
1091-
Mix.Project.config()[:preferred_cli_env][task] || Mix.Task.preferred_cli_env(task) || :dev
1092-
end
1093-
end
1094-
10951066
defp launch_task(task, args) do
1096-
# This fixes a race condition in the tests and likely improves reliability when using the
1067+
# This fixes a race condition in the tests and likely improves reliability when using the
10971068
# debugger as well.
10981069
Process.sleep(100)
10991070

apps/elixir_ls_debugger/mix.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ defmodule ElixirLS.Debugger.Mixfile do
1313
build_embedded: false,
1414
start_permanent: true,
1515
build_per_environment: false,
16+
# if we consolidate here debugged code will not work correctly
17+
# and debugged protocol implementation will not be available
1618
consolidate_protocols: false,
1719
deps: deps(),
1820
xref: [exclude: [:int, :dbg_iserver]]

apps/elixir_ls_utils/lib/launch.ex

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,26 @@ defmodule ElixirLS.Utils.Launch do
33
@compiled_otp_version System.otp_release()
44

55
def start_mix do
6+
# reset env and target if it is set
7+
Mix.env(:dev)
8+
Mix.target(:host)
9+
System.put_env([{"MIX_ENV", nil}, {"MIX_TARGET", nil}])
10+
611
Mix.start()
12+
Mix.Local.append_archives()
13+
Mix.Local.append_paths()
14+
true = Mix.Hex.ensure_installed?(false)
715
# when running via mix install script mix starts and stops hex
816
# we need to make sure it's started
917
if function_exported?(Hex, :start, 0) do
1018
Hex.start()
1119
end
1220

13-
# FIXME: Private API
14-
Mix.Local.append_archives()
15-
# FIXME: Private API
16-
Mix.Local.append_paths()
1721
load_dot_config()
22+
23+
# as of 1.14 mix supports two environment variables MIX_QUIET and MIX_DEBUG
24+
# that are not important for our use cases
25+
1826
:ok
1927
end
2028

@@ -53,11 +61,103 @@ defmodule ElixirLS.Utils.Launch do
5361
end
5462

5563
defp load_dot_config do
56-
# FIXME: Private API
5764
path = Path.join(Mix.Utils.mix_home(), "config.exs")
5865

5966
if File.regular?(path) do
6067
Mix.Task.run("loadconfig", [path])
6168
end
6269
end
70+
71+
def load_mix_exs() do
72+
file = ElixirLS.Utils.MixfileHelpers.mix_exs()
73+
74+
if File.regular?(file) do
75+
# TODO elixir 1.15 calls
76+
# Mix.ProjectStack.post_config(state_loader: {:cli, List.first(args)})
77+
# added in https://github.com/elixir-lang/elixir/commit/9e07da862784ac7d18a1884141c49ab049e61691
78+
# def cli
79+
# do we need that?
80+
old_undefined = Code.get_compiler_option(:no_warn_undefined)
81+
Code.put_compiler_option(:no_warn_undefined, :all)
82+
Code.compile_file(file)
83+
Code.put_compiler_option(:no_warn_undefined, old_undefined)
84+
end
85+
end
86+
87+
# TODO add support for def cli
88+
def get_task(["-" <> _ | _]) do
89+
task = "mix #{Mix.Project.config()[:default_task]}"
90+
91+
Mix.shell().error(
92+
"** (Mix) Mix only recognizes the options --help and --version.\n" <>
93+
"You may have wanted to invoke a task instead, such as #{inspect(task)}"
94+
)
95+
96+
display_usage()
97+
exit({:shutdown, 1})
98+
end
99+
100+
def get_task([h | t]) do
101+
{h, t}
102+
end
103+
104+
def get_task([]) do
105+
case Mix.Project.get() do
106+
nil ->
107+
Mix.shell().error(
108+
"** (Mix) \"mix\" with no arguments must be executed in a directory with a mix.exs file"
109+
)
110+
111+
display_usage()
112+
exit({:shutdown, 1})
113+
114+
_ ->
115+
{Mix.Project.config()[:default_task], []}
116+
end
117+
end
118+
119+
def maybe_change_env_and_target(task) do
120+
task = String.to_atom(task)
121+
config = Mix.Project.config()
122+
123+
env = preferred_cli_env(task, config)
124+
target = preferred_cli_target(task, config)
125+
env && Mix.env(env)
126+
target && Mix.target(target)
127+
128+
if env || target do
129+
reload_project()
130+
end
131+
end
132+
133+
defp reload_project() do
134+
if project = Mix.Project.pop() do
135+
%{name: name, file: file} = project
136+
Mix.Project.push(name, file)
137+
end
138+
end
139+
140+
defp preferred_cli_env(task, config) do
141+
if System.get_env("MIX_ENV") do
142+
nil
143+
else
144+
config[:preferred_cli_env][task] || Mix.Task.preferred_cli_env(task)
145+
end
146+
end
147+
148+
defp preferred_cli_target(task, config) do
149+
config[:preferred_cli_target][task]
150+
end
151+
152+
defp display_usage do
153+
Mix.shell().info("""
154+
Usage: mix [task]
155+
Examples:
156+
mix - Invokes the default task (mix run) in a project
157+
mix new PATH - Creates a new Elixir project at the given path
158+
mix help - Lists all available tasks
159+
mix help TASK - Prints documentation for a given task
160+
The --help and --version options can be given instead of a task for usage and versioning information.
161+
""")
162+
end
63163
end

apps/language_server/lib/language_server/build.ex

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ defmodule ElixirLS.LanguageServer.Build do
3131
fetch_deps(current_deps)
3232
end
3333

34-
# if we won't do it elixir >= 1.11 warns that protocols have already been consolidated
35-
purge_consolidated_protocols()
3634
{status, diagnostics} = run_mix_compile()
3735

3836
diagnostics = Diagnostics.normalize(diagnostics, root_path)
@@ -78,6 +76,19 @@ defmodule ElixirLS.LanguageServer.Build do
7876

7977
if File.exists?(mixfile) do
8078
if module = Mix.Project.get() do
79+
build_path = Mix.Project.config()[:build_path]
80+
81+
for {app, path} <- Mix.Project.deps_paths() do
82+
child_module =
83+
Mix.Project.in_project(app, path, [build_path: build_path], fn mix_project ->
84+
mix_project
85+
end)
86+
87+
if child_module do
88+
purge_module(child_module)
89+
end
90+
end
91+
8192
# FIXME: Private API
8293
Mix.Project.pop()
8394
purge_module(module)
@@ -102,6 +113,18 @@ defmodule ElixirLS.LanguageServer.Build do
102113
# FIXME: Private API
103114
Mix.ProjectStack.post_config(build_path: ".elixir_ls/build")
104115

116+
# TODO elixir 1.15 calls
117+
# Mix.ProjectStack.post_config(state_loader: {:cli, List.first(args)})
118+
# added in https://github.com/elixir-lang/elixir/commit/9e07da862784ac7d18a1884141c49ab049e61691
119+
# def cli
120+
# do we need that?
121+
122+
# since elixir 1.10 mix disables undefined warnings for mix.exs
123+
# see discussion in https://github.com/elixir-lang/elixir/issues/9676
124+
# https://github.com/elixir-lang/elixir/blob/6f96693b355a9b670f2630fd8e6217b69e325c5a/lib/mix/lib/mix/cli.ex#L41
125+
old_undefined = Code.get_compiler_option(:no_warn_undefined)
126+
Code.put_compiler_option(:no_warn_undefined, :all)
127+
105128
# We can get diagnostics if Mixfile fails to load
106129
{status, diagnostics} =
107130
case Kernel.ParallelCompiler.compile([mixfile]) do
@@ -116,6 +139,9 @@ defmodule ElixirLS.LanguageServer.Build do
116139
}
117140
end
118141

142+
# restore warnings
143+
Code.put_compiler_option(:no_warn_undefined, old_undefined)
144+
119145
if status == :ok do
120146
# The project may override our logger config, so we reset it after loading their config
121147
logger_config = Application.get_all_env(:logger)
@@ -136,15 +162,21 @@ defmodule ElixirLS.LanguageServer.Build do
136162
end
137163

138164
defp run_mix_compile do
139-
# TODO consider adding --no-compile
140-
case Mix.Task.run("compile", ["--return-errors", "--ignore-module-conflict"]) do
165+
# TODO --all-warnings not needed on 1.15
166+
case Mix.Task.run("compile", [
167+
"--return-errors",
168+
"--ignore-module-conflict",
169+
"--all-warnings",
170+
"--no-protocol-consolidation"
171+
]) do
141172
{status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) ->
142173
{status, diagnostics}
143174

144175
status when status in [:ok, :noop] ->
145176
{status, []}
146177

147-
_ ->
178+
other ->
179+
Logger.debug("mix compile returned #{inspect(other)}")
148180
{:ok, []}
149181
end
150182
end
@@ -169,26 +201,6 @@ defmodule ElixirLS.LanguageServer.Build do
169201
end
170202
end
171203

172-
defp purge_consolidated_protocols do
173-
config = Mix.Project.config()
174-
path = Mix.Project.consolidation_path(config)
175-
176-
with {:ok, beams} <- File.ls(path) do
177-
Enum.map(beams, &(&1 |> Path.rootname(".beam") |> String.to_atom() |> purge_module()))
178-
else
179-
{:error, :enoent} ->
180-
# consolidation_path does not exist
181-
:ok
182-
183-
{:error, reason} ->
184-
Logger.warn("Unable to purge consolidated protocols from #{path}: #{inspect(reason)}")
185-
end
186-
187-
# NOTE this implementation is based on https://github.com/phoenixframework/phoenix/commit/b5580e9
188-
# calling `Code.delete_path(path)` may be unnecessary in our case
189-
Code.delete_path(path)
190-
end
191-
192204
defp purge_module(module) do
193205
:code.purge(module)
194206
:code.delete(module)
@@ -334,19 +346,29 @@ defmodule ElixirLS.LanguageServer.Build do
334346

335347
def set_compiler_options(options \\ [], parser_options \\ []) do
336348
parser_options =
337-
parser_options
338-
|> Keyword.merge(
349+
Keyword.merge(parser_options,
339350
columns: true,
340351
token_metadata: true
341352
)
342353

343354
options =
344-
options
345-
|> Keyword.merge(
355+
Keyword.merge(options,
346356
tracers: [Tracer],
347357
parser_options: parser_options
348358
)
349359

360+
options =
361+
if Version.match?(System.version(), ">= 1.14.0") do
362+
Keyword.merge(options,
363+
# we are running the server with consolidated protocols
364+
# this disables warnings `X has already been consolidated`
365+
# when running `compile` task
366+
ignore_already_consolidated: true
367+
)
368+
else
369+
options
370+
end
371+
350372
Code.compiler_options(options)
351373
end
352374

apps/language_server/lib/language_server/cli.ex

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ defmodule ElixirLS.LanguageServer.CLI do
4848
Launch.limit_num_schedulers()
4949

5050
Mix.shell(ElixirLS.LanguageServer.MixShell)
51-
# FIXME: Private API
52-
true = Mix.Hex.ensure_installed?(false)
53-
true = Mix.Hex.ensure_updated?()
5451

5552
WireProtocol.stream_packets(&JsonRpc.receive_packet/1)
5653
end

apps/language_server/lib/language_server/dialyzer.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,9 @@ defmodule ElixirLS.LanguageServer.Dialyzer do
214214
defp update_stale(md5, removed_files, file_changes, timestamp) do
215215
prev_paths = Map.keys(md5) |> MapSet.new()
216216

217-
# FIXME: Private API
218-
consolidation_path = Mix.Project.consolidation_path()
219-
220-
consolidated_protocol_beams =
221-
for path <- Path.join(consolidation_path, "*.beam") |> Path.wildcard(),
222-
into: MapSet.new(),
223-
do: Path.basename(path)
224-
225217
# FIXME: Private API
226218
all_paths =
227219
for path <- Mix.Utils.extract_files([Mix.Project.build_path()], [:beam]),
228-
Path.basename(path) not in consolidated_protocol_beams or
229-
Path.dirname(path) == consolidation_path,
230220
into: MapSet.new(),
231221
do: Path.relative_to_cwd(path)
232222

0 commit comments

Comments
 (0)