Skip to content

Commit 079b92e

Browse files
jonatankloskojosevalim
authored andcommitted
Support recompiling local Mix.install/2 dependencies (#13375)
1 parent 325877a commit 079b92e

File tree

3 files changed

+126
-39
lines changed

3 files changed

+126
-39
lines changed

lib/iex/lib/iex/helpers.ex

Lines changed: 42 additions & 22 deletions
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,21 +98,36 @@ defmodule IEx.Helpers do
9398
9499
"""
95100
def recompile(options \\ []) do
96-
if mix_started?() do
97-
config = Mix.Project.config()
98-
consolidation = Mix.Project.consolidation_path(config)
99-
reenable_tasks(config)
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+
do_recompile(options)
117+
end
118+
end
100119

101-
force? = Keyword.get(options, :force, false)
102-
args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation]
103-
args = if force?, do: ["--force" | args], else: args
120+
defp do_recompile(options) do
121+
config = Mix.Project.config()
122+
consolidation = Mix.Project.consolidation_path(config)
123+
reenable_tasks(config)
104124

105-
{result, _} = Mix.Task.run("compile", args)
106-
result
107-
else
108-
IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix"))
109-
:error
110-
end
125+
force? = Keyword.get(options, :force, false)
126+
args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation]
127+
args = if force?, do: ["--force" | args], else: args
128+
129+
{result, _} = Mix.Task.run("compile", args)
130+
result
111131
end
112132

113133
defp mix_started? do

lib/mix/lib/mix.ex

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -858,23 +858,14 @@ defmodule Mix do
858858
File.rm_rf!(install_dir)
859859
end
860860

861-
config = [
862-
version: "0.1.0",
863-
build_embedded: false,
864-
build_per_environment: true,
865-
build_path: "_build",
866-
lockfile: "mix.lock",
867-
deps_path: "deps",
861+
dynamic_config = [
868862
deps: deps,
869-
app: :mix_install,
870-
erlc_paths: [],
871-
elixirc_paths: [],
872-
compilers: [],
873863
consolidate_protocols: consolidate_protocols?,
874-
config_path: config_path,
875-
prune_code_paths: false
864+
config_path: config_path
876865
]
877866

867+
config = install_project_config(dynamic_config)
868+
878869
started_apps = Application.started_applications()
879870
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
880871
build_dir = Path.join(install_dir, "_build")
@@ -940,13 +931,18 @@ defmodule Mix do
940931
end
941932
end
942933

943-
Mix.State.put(:installed, id)
934+
Mix.State.put(:installed, {id, dynamic_config})
944935
:ok
945936
after
946937
Mix.ProjectStack.pop()
938+
# Clear all tasks invoked during installation, since there
939+
# is no reason to keep this in memory. Additionally this
940+
# allows us to rerun tasks for the dependencies later on,
941+
# such as recompilation
942+
Mix.Task.clear()
947943
end
948944

949-
^id when not force? ->
945+
{^id, _dynamic_config} when not force? ->
950946
:ok
951947

952948
_ ->
@@ -981,6 +977,45 @@ defmodule Mix do
981977
Path.join([install_root, version, cache_id])
982978
end
983979

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

lib/mix/test/mix_test.exs

Lines changed: 34 additions & 2 deletions
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)