summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2021-12-02 16:38:19 +0100
committerJosé Valim <jose.valim@dashbit.co>2021-12-02 16:38:19 +0100
commit5d7f272eb6967dff220c041e3a794fb9df4893fe (patch)
treecc3ea68644d9048b6440610f9af41c057e2774d6
parent2dede8ea2b82cf113c7a212c75ab3a299080502c (diff)
downloadelixir-5d7f272eb6967dff220c041e3a794fb9df4893fe.tar.gz
Track transitive runtime dependencies coming from local deps
-rw-r--r--lib/mix/lib/mix/compilers/elixir.ex98
-rw-r--r--lib/mix/test/mix/umbrella_test.exs36
2 files changed, 104 insertions, 30 deletions
diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex
index 644fafcde..12458ef26 100644
--- a/lib/mix/lib/mix/compilers/elixir.ex
+++ b/lib/mix/lib/mix/compilers/elixir.ex
@@ -725,37 +725,83 @@ defmodule Mix.Compilers.Elixir do
for %{scm: scm, opts: opts} = dep <- Mix.Dep.cached(),
not scm.fetchable?,
- Mix.Utils.last_modified(Path.join([opts[:build], ".mix", base])) > modified,
- path <- Mix.Dep.load_paths(dep),
- beam <- Path.wildcard(Path.join(path, "*.beam")),
- Mix.Utils.last_modified(beam) > modified,
+ manifest = Path.join([opts[:build], ".mix", base]),
+ Mix.Utils.last_modified(manifest) > modified,
reduce: {stale_modules, %{}, old_exports} do
{modules, exports, new_exports} ->
- module = beam |> Path.basename() |> Path.rootname() |> String.to_atom()
- export = exports_md5(module, false)
- modules = Map.put(modules, module, [])
-
- # If the exports are the same, then the API did not change,
- # so we do not mark the export as stale. Note this has to
- # be very conservative. If the module is not loaded or if
- # the exports were not there, we need to consider it a stale
- # export.
- exports =
- if export && old_exports[module] == export,
- do: exports,
- else: Map.put(exports, module, [])
-
- # In any case, we always store it as the most update export
- # that we have, otherwise we delete it.
- new_exports =
- if export,
- do: Map.put(new_exports, module, export),
- else: Map.delete(new_exports, module)
-
- {modules, exports, new_exports}
+ {_manifest_modules, dep_sources} = read_manifest(manifest)
+
+ # TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
+ dep_modules =
+ for path <- Mix.Dep.load_paths(dep),
+ beam <- Path.wildcard(Path.join(path, "*.beam")),
+ Mix.Utils.last_modified(beam) > modified,
+ do: {beam |> Path.basename() |> Path.rootname() |> String.to_atom(), []},
+ into: %{}
+
+ # If any module has a compile time dependency on a changed module
+ # within the dependnecy, they will be recompiled. However, export
+ # and runtime dependencies won't have recompiled so we need to
+ # propagate them to the parent app.
+ dep_modules = fixpoint_dep_modules(dep_sources, dep_modules, false, [])
+
+ # Update exports
+ {exports, new_exports} =
+ for {module, _} <- dep_modules, reduce: {exports, new_exports} do
+ {exports, new_exports} ->
+ export = exports_md5(module, false)
+
+ # If the exports are the same, then the API did not change,
+ # so we do not mark the export as stale. Note this has to
+ # be very conservative. If the module is not loaded or if
+ # the exports were not there, we need to consider it a stale
+ # export.
+ exports =
+ if export && old_exports[module] == export,
+ do: exports,
+ else: Map.put(exports, module, [])
+
+ # In any case, we always store it as the most update export
+ # that we have, otherwise we delete it.
+ new_exports =
+ if export,
+ do: Map.put(new_exports, module, export),
+ else: Map.delete(new_exports, module)
+
+ {exports, new_exports}
+ end
+
+ {Map.merge(modules, dep_modules), exports, new_exports}
end
end
+ defp fixpoint_dep_modules([source | sources], modules, new_modules?, acc_sources) do
+ source(
+ compile_references: compile_refs,
+ export_references: export_refs,
+ runtime_references: runtime_refs
+ ) = source
+
+ if has_any_key?(modules, compile_refs) or has_any_key?(modules, export_refs) or
+ has_any_key?(modules, runtime_refs) do
+ new_modules = Enum.reject(source(source, :modules), &Map.has_key?(modules, &1))
+ new_modules? = new_modules? or new_modules != []
+ modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, []))
+ fixpoint_dep_modules(sources, modules, new_modules?, acc_sources)
+ else
+ fixpoint_dep_modules(sources, modules, new_modules?, [source | acc_sources])
+ end
+ end
+
+ defp fixpoint_dep_modules([], modules, false, _),
+ do: modules
+
+ defp fixpoint_dep_modules([], modules, true, []),
+ do: modules
+
+ defp fixpoint_dep_modules([], modules, true, sources),
+ do: fixpoint_dep_modules(sources, modules, false, [])
+
defp exports_md5(module, use_attributes?) do
cond do
function_exported?(module, :__info__, 1) ->
diff --git a/lib/mix/test/mix/umbrella_test.exs b/lib/mix/test/mix/umbrella_test.exs
index 67488c94d..cc6bf3e0e 100644
--- a/lib/mix/test/mix/umbrella_test.exs
+++ b/lib/mix/test/mix/umbrella_test.exs
@@ -439,12 +439,9 @@ defmodule Mix.UmbrellaTest do
Mix.Project.in_project(:bar, "bar", fn _ ->
File.write!("../foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
- Mix.Task.run("compile", ["--verbose"])
-
# Add struct dependency
File.write!("lib/bar.ex", "defmodule Bar, do: %Foo{bar: true}")
-
- assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
+ Mix.Task.run("compile", ["--verbose"])
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
# Recompiles for struct dependencies
@@ -458,6 +455,37 @@ defmodule Mix.UmbrellaTest do
end)
end
+ test "recompiles after compile through runtime path dependency changes" do
+ in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
+ Mix.Project.in_project(:bar, "bar", fn _ ->
+ File.write!("../foo/lib/foo.bar.ex", """
+ defmodule Foo.Bar do
+ def hello, do: Foo.Baz.hello()
+ end
+ """)
+
+ File.write!("../foo/lib/foo.baz.ex", """
+ defmodule Foo.Baz do
+ def hello, do: "from bar"
+ end
+ """)
+
+ # Add compile time to Foo.Bar
+ File.write!("lib/bar.ex", "defmodule Bar, do: Foo.Bar.hello()")
+ Mix.Task.run("compile", ["--verbose"])
+ assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
+
+ # Recompiles for due to compile dependency via runtime dependencies
+ mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
+ ensure_touched("_build/dev/lib/foo/ebin/Elixir.Foo.Baz.beam", mtime)
+ ensure_touched("_build/dev/lib/foo/.mix/compile.elixir", mtime)
+
+ assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
+ assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
+ end)
+ end)
+ end
+
test "reloads app in app tracer if .app changes" do
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
deps = [{:foo, in_umbrella: true}]