Skip to content

Commit 264200a

Browse files
committed
Avoid unecessary umbrella recompilation when a path dependency's manifest is newer but unchanged, closes #15454
1 parent 39acd23 commit 264200a

2 files changed

Lines changed: 73 additions & 39 deletions

File tree

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

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,20 @@ defmodule Mix.Compilers.Elixir do
658658
reduce: {stale_modules, stale_modules, deps_exports, protocols_and_impls()} do
659659
{modules, exports, deps_exports, protocols_and_impls} ->
660660
{manifest_modules, manifest_sources} = read_manifest(manifest)
661+
all_modules = Enum.map(manifest_modules, fn {mod, _} -> mod end)
662+
dep_exports = Map.get(deps_exports, app, %{})
663+
664+
# Update modules, exports, and dep exports based on removed modules
665+
{modules, exports, dep_exports} =
666+
Enum.reduce(dep_exports, {modules, exports, dep_exports}, fn
667+
{mod, _}, {modules, exports, dep_exports} ->
668+
if mod in all_modules do
669+
{modules, exports, dep_exports}
670+
else
671+
{Map.put(modules, mod, true), Map.put(exports, mod, true),
672+
Map.delete(dep_exports, mod)}
673+
end
674+
end)
661675

662676
dep_modules =
663677
for {module, module(timestamp: timestamp)} <- manifest_modules,
@@ -671,47 +685,32 @@ defmodule Mix.Compilers.Elixir do
671685
dep_modules =
672686
fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
673687

674-
old_exports = Map.get(deps_exports, app, %{})
675-
676-
# Update exports
677-
{exports, new_exports} =
678-
for {module, _} <- dep_modules, reduce: {exports, []} do
679-
{exports, new_exports} ->
680-
export =
681-
if Code.ensure_loaded?(module) and function_exported?(module, :__info__, 1) do
682-
module.__info__(:exports_md5)
683-
end
684-
685-
# If the exports are the same, then the API did not change,
686-
# so we do not mark the export as stale. Note this has to
687-
# be very conservative. If the module is not loaded or if
688-
# the exports were not there, we need to consider it a stale
689-
# export.
690-
exports =
691-
if export && old_exports[module] == export,
692-
do: exports,
693-
else: Map.put(exports, module, true)
694-
695-
# Then we store the new export if any
696-
new_exports =
697-
if export,
698-
do: [{module, export} | new_exports],
699-
else: new_exports
700-
701-
{exports, new_exports}
702-
end
703-
704-
new_exports = Map.new(new_exports)
688+
{exports, dep_exports} =
689+
Enum.reduce(dep_modules, {exports, dep_exports}, fn {mod, _}, {exports, dep_exports} ->
690+
export =
691+
if Code.ensure_loaded?(mod) and function_exported?(mod, :__info__, 1) do
692+
mod.__info__(:exports_md5)
693+
end
705694

706-
removed =
707-
for {module, _} <- old_exports,
708-
not is_map_key(new_exports, module),
709-
do: {module, true},
710-
into: %{}
695+
# If the exports are the same, then the API did not change,
696+
# so we do not mark the export as stale. Note this has to
697+
# be very conservative. If the module is not loaded or if
698+
# the exports were not there, we need to consider it a stale export.
699+
case dep_exports do
700+
# They still match, nothing to change
701+
%{^mod => ^export} ->
702+
{exports, dep_exports}
703+
704+
# Now, it either matched and they are different, which means there was
705+
# an export at some point OR it didn't exist before, which means adding
706+
# it as an export is likely a no-op anyway
707+
_ ->
708+
{Map.put(exports, mod, true), Map.put(dep_exports, mod, export)}
709+
end
710+
end)
711711

712-
modules = modules |> Map.merge(dep_modules) |> Map.merge(removed)
713-
exports = Map.merge(exports, removed)
714-
deps_exports = Map.put(deps_exports, app, new_exports)
712+
modules = Map.merge(modules, dep_modules)
713+
deps_exports = Map.put(deps_exports, app, dep_exports)
715714

716715
protocols_and_impls =
717716
protocols_and_impls_from_modules(manifest_modules, protocols_and_impls)

lib/mix/test/mix/umbrella_test.exs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,41 @@ defmodule Mix.UmbrellaTest do
479479
end)
480480
end
481481

482+
test "does not recompile after unchanged struct dependency and touched project files" do
483+
in_fixture("umbrella_dep/deps/umbrella", fn ->
484+
Mix.Project.in_project(:umbrella, ".", fn _ ->
485+
File.mkdir_p!("config")
486+
File.write!("config/config.exs", "import Config\n")
487+
File.write!("apps/foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
488+
489+
File.write!("apps/bar/lib/bar.ex", """
490+
defmodule Bar do
491+
def foo_bar(), do: %Foo{bar: true}
492+
end
493+
""")
494+
495+
Mix.Task.run("compile", ["--verbose"])
496+
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
497+
498+
File.write!("apps/foo/lib/foo.ex", File.read!("apps/foo/lib/foo.ex") <> "\n")
499+
ensure_touched("apps/foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir")
500+
501+
Mix.Task.clear()
502+
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
503+
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
504+
505+
ensure_touched("mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
506+
ensure_touched("apps/foo/mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
507+
ensure_touched("apps/bar/mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
508+
ensure_touched("config/config.exs", "_build/dev/lib/bar/.mix/compile.elixir")
509+
510+
Mix.Task.clear()
511+
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
512+
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
513+
end)
514+
end)
515+
end
516+
482517
test "recompiles after compile through runtime path dependency changes" do
483518
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
484519
Mix.Project.in_project(:bar, "bar", fn _ ->

0 commit comments

Comments
 (0)