Skip to content

Commit 3a8d7f4

Browse files
committed
Track transitive runtime dependencies coming from local deps
1 parent 1dc6e71 commit 3a8d7f4

File tree

2 files changed

+100
-30
lines changed

2 files changed

+100
-30
lines changed

lib/mix/lib/mix/compilers/elixir.ex

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -725,37 +725,79 @@ defmodule Mix.Compilers.Elixir do
725725

726726
for %{scm: scm, opts: opts} = dep <- Mix.Dep.cached(),
727727
not scm.fetchable?,
728-
Mix.Utils.last_modified(Path.join([opts[:build], ".mix", base])) > modified,
729-
path <- Mix.Dep.load_paths(dep),
730-
beam <- Path.wildcard(Path.join(path, "*.beam")),
731-
Mix.Utils.last_modified(beam) > modified,
728+
manifest = Path.join([opts[:build], ".mix", base]),
729+
Mix.Utils.last_modified(manifest) > modified,
732730
reduce: {stale_modules, %{}, old_exports} do
733731
{modules, exports, new_exports} ->
734-
module = beam |> Path.basename() |> Path.rootname() |> String.to_atom()
735-
export = exports_md5(module, false)
736-
modules = Map.put(modules, module, [])
737-
738-
# If the exports are the same, then the API did not change,
739-
# so we do not mark the export as stale. Note this has to
740-
# be very conservative. If the module is not loaded or if
741-
# the exports were not there, we need to consider it a stale
742-
# export.
743-
exports =
744-
if export && old_exports[module] == export,
745-
do: exports,
746-
else: Map.put(exports, module, [])
747-
748-
# In any case, we always store it as the most update export
749-
# that we have, otherwise we delete it.
750-
new_exports =
751-
if export,
752-
do: Map.put(new_exports, module, export),
753-
else: Map.delete(new_exports, module)
754-
755-
{modules, exports, new_exports}
732+
{_manifest_modules, dep_sources} = read_manifest(manifest)
733+
734+
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
735+
dep_modules =
736+
for path <- Mix.Dep.load_paths(dep),
737+
beam <- Path.wildcard(Path.join(path, "*.beam")),
738+
Mix.Utils.last_modified(beam) > modified,
739+
do: {beam |> Path.basename() |> Path.rootname() |> String.to_atom(), []},
740+
into: %{}
741+
742+
# If any module has a compile time dependency on a changed module
743+
# within the dependnecy, they will be recompiled. However, export
744+
# and runtime dependencies won't have recompiled so we need to
745+
# propagate them to the parent app.
746+
dep_modules = fixpoint_dep_modules(dep_sources, dep_modules, false, [])
747+
748+
# Update exports
749+
{exports, new_exports} =
750+
for {module, _} <- dep_modules, reduce: {exports, new_exports} do
751+
{exports, new_exports} ->
752+
export = exports_md5(module, false)
753+
754+
# If the exports are the same, then the API did not change,
755+
# so we do not mark the export as stale. Note this has to
756+
# be very conservative. If the module is not loaded or if
757+
# the exports were not there, we need to consider it a stale
758+
# export.
759+
exports =
760+
if export && old_exports[module] == export,
761+
do: exports,
762+
else: Map.put(exports, module, [])
763+
764+
# In any case, we always store it as the most update export
765+
# that we have, otherwise we delete it.
766+
new_exports =
767+
if export,
768+
do: Map.put(new_exports, module, export),
769+
else: Map.delete(new_exports, module)
770+
771+
{exports, new_exports}
772+
end
773+
774+
{Map.merge(modules, dep_modules), exports, new_exports}
775+
end
776+
end
777+
778+
defp fixpoint_dep_modules([source | sources], modules, new_modules?, acc_sources) do
779+
source(export_references: export_refs, runtime_references: runtime_refs) = source
780+
781+
if has_any_key?(modules, compile_refs) or has_any_key?(modules, export_refs) or
782+
has_any_key?(modules, runtime_refs) do
783+
new_modules = Enum.reject(source(source, :modules), &Map.has_key?(modules, &1))
784+
new_modules? = new_modules? or new_modules != []
785+
modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, []))
786+
fixpoint_dep_modules(sources, modules, new_modules?, acc_sources)
787+
else
788+
fixpoint_dep_modules(sources, modules, new_modules?, [source | acc_sources])
756789
end
757790
end
758791

792+
defp fixpoint_dep_modules([], modules, false, _),
793+
do: modules
794+
795+
defp fixpoint_dep_modules([], modules, true, []),
796+
do: modules
797+
798+
defp fixpoint_dep_modules([], modules, true, sources),
799+
do: fixpoint_dep_modules(sources, modules, false, [])
800+
759801
defp exports_md5(module, use_attributes?) do
760802
cond do
761803
function_exported?(module, :__info__, 1) ->

lib/mix/test/mix/umbrella_test.exs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,9 @@ defmodule Mix.UmbrellaTest do
439439
Mix.Project.in_project(:bar, "bar", fn _ ->
440440
File.write!("../foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
441441

442-
Mix.Task.run("compile", ["--verbose"])
443-
444442
# Add struct dependency
445443
File.write!("lib/bar.ex", "defmodule Bar, do: %Foo{bar: true}")
446-
447-
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
444+
Mix.Task.run("compile", ["--verbose"])
448445
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
449446

450447
# Recompiles for struct dependencies
@@ -458,6 +455,37 @@ defmodule Mix.UmbrellaTest do
458455
end)
459456
end
460457

458+
test "recompiles after compile through runtime path dependency changes" do
459+
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
460+
Mix.Project.in_project(:bar, "bar", fn _ ->
461+
File.write!("../foo/lib/foo.bar.ex", """
462+
defmodule Foo.Bar do
463+
def hello, do: Foo.Baz.hello()
464+
end
465+
""")
466+
467+
File.write!("../foo/lib/foo.baz.ex", """
468+
defmodule Foo.Baz do
469+
def hello, do: "from bar"
470+
end
471+
""")
472+
473+
# Add compile time to Foo.Bar
474+
File.write!("lib/bar.ex", "defmodule Bar, do: Foo.Bar.hello()")
475+
Mix.Task.run("compile", ["--verbose"])
476+
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
477+
478+
# Recompiles for due to compile dependency via runtime dependencies
479+
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
480+
ensure_touched("_build/dev/lib/foo/ebin/Elixir.Foo.Baz.beam", mtime)
481+
ensure_touched("_build/dev/lib/foo/.mix/compile.elixir", mtime)
482+
483+
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
484+
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
485+
end)
486+
end)
487+
end
488+
461489
test "reloads app in app tracer if .app changes" do
462490
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
463491
deps = [{:foo, in_umbrella: true}]

0 commit comments

Comments
 (0)