Skip to content

Commit fc0c487

Browse files
authoredFeb 29, 2024
Support recompiling local Mix.install/2 dependencies (#13375)
1 parent 7e5ccce commit fc0c487

File tree

3 files changed

+122
-39
lines changed

3 files changed

+122
-39
lines changed
 

Diff for: ‎lib/iex/lib/iex/helpers.ex

+38-22
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,22 @@ defmodule IEx.Helpers do
6969
import IEx, only: [dont_display_result: 0]
7070

7171
@doc """
72-
Recompiles the current Mix project.
72+
Recompiles the current Mix project or Mix install
73+
dependencies.
7374
74-
This helper only works when IEx is started with a Mix
75-
project, for example, `iex -S mix`. Note this function
76-
simply recompiles Elixir modules, without reloading
77-
configuration, recompiling dependencies, or restarting
78-
applications.
75+
This helper requires either `Mix.install/2` to have been
76+
called within the current IEx session or for IEx to be
77+
started alongside, for example, `iex -S mix`.
7978
80-
Therefore, any long running process may crash on recompilation,
81-
as changed modules will be temporarily removed and recompiled,
82-
without going through the proper code change callback.
79+
In the `Mix.install/1` case, it will recompile any outdated
80+
path dependency declared during install. Within a project,
81+
it will recompile any outdated module.
82+
83+
Note this function simply recompiles Elixir modules, without
84+
reloading configuration or restarting applications. This means
85+
any long running process may crash on recompilation, as changed
86+
modules will be temporarily removed and recompiled, without
87+
going through the proper code change callback.
8388
8489
If you want to reload a single module, consider using
8590
`r(ModuleName)` instead.
@@ -93,19 +98,30 @@ defmodule IEx.Helpers do
9398
9499
"""
95100
def recompile(options \\ []) do
96-
if mix_started?() do
97-
project = Mix.Project.get()
98-
99-
if is_nil(project) or
100-
project.__info__(:compile)[:source] == String.to_charlist(Path.absname("mix.exs")) do
101-
do_recompile(options)
102-
else
103-
message = "Cannot recompile because the current working directory changed"
104-
IO.puts(IEx.color(:eval_error, message))
105-
end
106-
else
107-
IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix"))
108-
:error
101+
cond do
102+
not mix_started?() ->
103+
IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix"))
104+
:error
105+
106+
Mix.installed?() ->
107+
Mix.in_install_project(fn ->
108+
do_recompile(options)
109+
# Just as with Mix.install/2 we clear all task invocations,
110+
# so that we can recompile the dependencies again next time
111+
Mix.Task.clear()
112+
:ok
113+
end)
114+
115+
true ->
116+
project = Mix.Project.get()
117+
118+
if is_nil(project) or
119+
project.__info__(:compile)[:source] == String.to_charlist(Path.absname("mix.exs")) do
120+
do_recompile(options)
121+
else
122+
message = "Cannot recompile because the current working directory changed"
123+
IO.puts(IEx.color(:eval_error, message))
124+
end
109125
end
110126
end
111127

Diff for: ‎lib/mix/lib/mix.ex

+50-15
Original file line numberDiff line numberDiff line change
@@ -855,23 +855,14 @@ defmodule Mix do
855855
File.rm_rf!(install_dir)
856856
end
857857

858-
config = [
859-
version: "0.1.0",
860-
build_embedded: false,
861-
build_per_environment: true,
862-
build_path: "_build",
863-
lockfile: "mix.lock",
864-
deps_path: "deps",
858+
dynamic_config = [
865859
deps: deps,
866-
app: :mix_install,
867-
erlc_paths: [],
868-
elixirc_paths: [],
869-
compilers: [],
870860
consolidate_protocols: consolidate_protocols?,
871-
config_path: config_path,
872-
prune_code_paths: false
861+
config_path: config_path
873862
]
874863

864+
config = install_project_config(dynamic_config)
865+
875866
started_apps = Application.started_applications()
876867
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
877868
build_dir = Path.join(install_dir, "_build")
@@ -937,13 +928,18 @@ defmodule Mix do
937928
end
938929
end
939930

940-
Mix.State.put(:installed, id)
931+
Mix.State.put(:installed, {id, dynamic_config})
941932
:ok
942933
after
943934
Mix.ProjectStack.pop()
935+
# Clear all tasks invoked during installation, since there
936+
# is no reason to keep this in memory. Additionally this
937+
# allows us to rerun tasks for the dependencies later on,
938+
# such as recompilation
939+
Mix.Task.clear()
944940
end
945941

946-
^id when not force? ->
942+
{^id, _dynamic_config} when not force? ->
947943
:ok
948944

949945
_ ->
@@ -978,6 +974,45 @@ defmodule Mix do
978974
Path.join([install_root, version, cache_id])
979975
end
980976

977+
defp install_project_config(dynamic_config) do
978+
[
979+
version: "0.1.0",
980+
build_embedded: false,
981+
build_per_environment: true,
982+
build_path: "_build",
983+
lockfile: "mix.lock",
984+
deps_path: "deps",
985+
app: :mix_install,
986+
erlc_paths: [],
987+
elixirc_paths: [],
988+
compilers: [],
989+
prune_code_paths: false
990+
] ++ dynamic_config
991+
end
992+
993+
@doc false
994+
def in_install_project(fun) do
995+
case Mix.State.get(:installed) do
996+
{id, dynamic_config} ->
997+
config = install_project_config(dynamic_config)
998+
999+
install_dir = install_dir(id)
1000+
1001+
File.cd!(install_dir, fn ->
1002+
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
1003+
1004+
try do
1005+
fun.()
1006+
after
1007+
Mix.ProjectStack.pop()
1008+
end
1009+
end)
1010+
1011+
nil ->
1012+
Mix.raise("trying to call Mix.in_install_project/1, but Mix.install/2 was never called")
1013+
end
1014+
end
1015+
9811016
@doc """
9821017
Returns whether `Mix.install/2` was called in the current node.
9831018
"""

Diff for: ‎lib/mix/test/mix_test.exs

+34-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule MixTest do
3939
assert Protocol.consolidated?(InstallTest.Protocol)
4040

4141
assert_received {:mix_shell, :info, ["==> install_test"]}
42-
assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]}
42+
assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]}
4343
assert_received {:mix_shell, :info, ["Generated install_test app"]}
4444
refute_received _
4545

@@ -67,7 +67,7 @@ defmodule MixTest do
6767

6868
assert File.dir?(Path.join(tmp_dir, "installs"))
6969
assert_received {:mix_shell, :info, ["==> install_test"]}
70-
assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]}
70+
assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]}
7171
assert_received {:mix_shell, :info, ["Generated install_test app"]}
7272
refute_received _
7373

@@ -345,6 +345,36 @@ defmodule MixTest do
345345
assert Mix.installed?()
346346
end
347347

348+
test "in_install_project", %{tmp_dir: tmp_dir} do
349+
Mix.install([
350+
{:install_test, path: Path.join(tmp_dir, "install_test")}
351+
])
352+
353+
Mix.in_install_project(fn ->
354+
config = Mix.Project.config()
355+
assert [{:install_test, [path: _]}] = config[:deps]
356+
end)
357+
end
358+
359+
test "in_install_project recompile", %{tmp_dir: tmp_dir} do
360+
Mix.install([
361+
{:install_test, path: Path.join(tmp_dir, "install_test")}
362+
])
363+
364+
File.write!("#{tmp_dir}/install_test/lib/install_test.ex", """
365+
defmodule InstallTest do
366+
def hello do
367+
:universe
368+
end
369+
end
370+
""")
371+
372+
Mix.in_install_project(fn ->
373+
Mix.Task.run("compile")
374+
assert apply(InstallTest, :hello, []) == :universe
375+
end)
376+
end
377+
348378
defp test_project(%{tmp_dir: tmp_dir}) do
349379
path = :code.get_path()
350380

@@ -384,7 +414,9 @@ defmodule MixTest do
384414
:world
385415
end
386416
end
417+
""")
387418

419+
File.write!("#{tmp_dir}/install_test/lib/install_test_protocol.ex", """
388420
defprotocol InstallTest.Protocol do
389421
def foo(x)
390422
end

0 commit comments

Comments
 (0)
Please sign in to comment.