Skip to content

Commit a89f8a9

Browse files
committed
Track removed modules and exports across local deps, closes #12707
1 parent 93bfbdf commit a89f8a9

File tree

3 files changed

+94
-81
lines changed

3 files changed

+94
-81
lines changed

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

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ defmodule Mix.Compilers.Elixir do
5656
[]
5757
end
5858

59-
local_deps = Enum.reject(Mix.Dep.cached(), & &1.scm.fetchable?)
59+
local_deps = Enum.reject(Mix.Dep.cached(), & &1.scm.fetchable?())
6060

6161
# If mix.exs has changed, recompile anything that calls Mix.Project.
6262
stale =
@@ -569,53 +569,65 @@ defmodule Mix.Compilers.Elixir do
569569
# as any export from a dependency needs to be recompiled.
570570
stale_modules = Map.from_keys(stale_modules, true)
571571

572-
for %{opts: opts} <- local_deps,
573-
manifest = Path.join([opts[:build], ".mix", base]),
574-
Mix.Utils.last_modified(manifest) > modified,
575-
reduce: {stale_modules, stale_modules, old_exports} do
576-
{modules, exports, new_exports} ->
577-
{manifest_modules, manifest_sources} = read_manifest(manifest)
578-
579-
dep_modules =
580-
for module(module: module, timestamp: timestamp) <- manifest_modules,
581-
timestamp > modified,
582-
do: module
583-
584-
# If any module has a compile time dependency on a changed module
585-
# within the dependency, they will be recompiled. However, export
586-
# and runtime dependencies won't have recompiled so we need to
587-
# propagate them to the parent app.
588-
{dep_modules, _, _} =
589-
fixpoint_runtime_modules(manifest_sources, Map.from_keys(dep_modules, true))
590-
591-
# Update exports
592-
{exports, new_exports} =
593-
for {module, _} <- dep_modules, reduce: {exports, new_exports} do
594-
{exports, new_exports} ->
595-
export = exports_md5(module, false)
596-
597-
# If the exports are the same, then the API did not change,
598-
# so we do not mark the export as stale. Note this has to
599-
# be very conservative. If the module is not loaded or if
600-
# the exports were not there, we need to consider it a stale
601-
# export.
602-
exports =
603-
if export && old_exports[module] == export,
604-
do: exports,
605-
else: Map.put(exports, module, true)
606-
607-
# In any case, we always store it as the most update export
608-
# that we have, otherwise we delete it.
609-
new_exports =
610-
if export,
611-
do: Map.put(new_exports, module, export),
612-
else: Map.delete(new_exports, module)
613-
614-
{exports, new_exports}
615-
end
572+
{stale_modules, stale_exports, new_exports} =
573+
for %{opts: opts} <- local_deps,
574+
manifest = Path.join([opts[:build], ".mix", base]),
575+
Mix.Utils.last_modified(manifest) > modified,
576+
reduce: {stale_modules, stale_modules, []} do
577+
{modules, exports, new_exports} ->
578+
{manifest_modules, manifest_sources} = read_manifest(manifest)
579+
580+
dep_modules =
581+
for module(module: module, timestamp: timestamp) <- manifest_modules,
582+
timestamp > modified,
583+
do: module
584+
585+
# If any module has a compile time dependency on a changed module
586+
# within the dependency, they will be recompiled. However, export
587+
# and runtime dependencies won't have recompiled so we need to
588+
# propagate them to the parent app.
589+
{dep_modules, _, _} =
590+
fixpoint_runtime_modules(manifest_sources, Map.from_keys(dep_modules, true))
591+
592+
# Update exports
593+
{exports, new_exports} =
594+
for {module, _} <- dep_modules, reduce: {exports, new_exports} do
595+
{exports, new_exports} ->
596+
export = exports_md5(module, false)
597+
598+
# If the exports are the same, then the API did not change,
599+
# so we do not mark the export as stale. Note this has to
600+
# be very conservative. If the module is not loaded or if
601+
# the exports were not there, we need to consider it a stale
602+
# export.
603+
exports =
604+
if export && old_exports[module] == export,
605+
do: exports,
606+
else: Map.put(exports, module, true)
607+
608+
# Then we store the new export if any
609+
new_exports =
610+
if export,
611+
do: [{module, export} | new_exports],
612+
else: new_exports
613+
614+
{exports, new_exports}
615+
end
616+
617+
{Map.merge(modules, dep_modules), exports, new_exports}
618+
end
616619

617-
{Map.merge(modules, dep_modules), exports, new_exports}
618-
end
620+
# Any dependency in old export but not in new export
621+
# was removed so we need to mark them as stale too.
622+
new_exports = Map.new(new_exports)
623+
624+
removed =
625+
for {module, _} <- old_exports,
626+
not is_map_key(new_exports, module),
627+
do: {module, true},
628+
into: %{}
629+
630+
{Map.merge(stale_modules, removed), Map.merge(stale_exports, removed), new_exports}
619631
end
620632

621633
defp fixpoint_runtime_modules(sources, modules) when modules != %{} do

lib/mix/lib/mix/tasks/compile.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ defmodule Mix.Tasks.Compile do
146146
config = Mix.Project.config()
147147

148148
# If we are in an umbrella project, now load paths from all children.
149-
if Mix.Project.umbrella?(config) do
149+
if apps_paths = Mix.Project.apps_paths(config) do
150150
loaded_paths =
151-
Mix.Project.apps_paths(config)
151+
apps_paths
152152
|> Map.keys()
153153
|> Mix.AppLoader.load_apps(Mix.Dep.cached(), config, [], fn
154154
{_app, path}, acc -> if path, do: [path | acc], else: acc

lib/mix/test/mix/umbrella_test.exs

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,6 @@ defmodule Mix.UmbrellaTest do
252252
end)
253253
end
254254

255-
## Umbrellas as a dependency
256-
257255
test "list deps for umbrella as dependency" do
258256
in_fixture("umbrella_dep", fn ->
259257
Mix.Project.in_project(:umbrella_dep, ".", fn _ ->
@@ -373,7 +371,7 @@ defmodule Mix.UmbrellaTest do
373371
end)
374372
end
375373

376-
test "recompiles after compile time path dependency changes" do
374+
test "recompiles when path dependencies change" do
377375
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
378376
Mix.Project.in_project(:bar, "bar", fn _ ->
379377
Mix.Task.run("compile", [])
@@ -385,30 +383,13 @@ defmodule Mix.UmbrellaTest do
385383
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
386384
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
387385

388-
# Emulate local recompilation
386+
# Compile-time dependencies are recompiled
389387
File.write!("../foo/lib/foo.ex", File.read!("../foo/lib/foo.ex") <> "\n")
390-
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
391-
ensure_touched("../foo/lib/foo.ex", mtime)
392-
393-
Mix.Task.clear()
394-
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
395-
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
396-
397-
# But exports dependencies are not recompiled
398-
File.write!("lib/bar.ex", "defmodule Bar, do: (require Foo)")
388+
ensure_touched("../foo/lib/foo.ex", "_build/dev/lib/foo/.mix/compile.elixir")
399389

400390
Mix.Task.clear()
401391
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
402392
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
403-
404-
# Touch to emulate local recompilation
405-
File.write!("../foo/lib/foo.ex", File.read!("../foo/lib/foo.ex") <> "\n")
406-
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
407-
ensure_touched("../foo/lib/foo.ex", mtime)
408-
409-
Mix.Task.clear()
410-
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []}
411-
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
412393
end)
413394

414395
# Now let's add a new file to foo
@@ -446,18 +427,39 @@ defmodule Mix.UmbrellaTest do
446427
File.write!("../foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
447428

448429
# Add struct dependency
449-
File.write!("lib/bar.ex", "defmodule Bar, do: %Foo{bar: true}")
430+
File.write!("lib/bar.ex", """
431+
defmodule Bar do
432+
def is_foo_bar(%Foo{bar: true}), do: true
433+
end
434+
""")
435+
450436
Mix.Task.run("compile", ["--verbose"])
451-
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
437+
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
452438

453-
# Recompiles for struct dependencies
439+
# Does not recompiles if export dependency does not change
454440
File.write!("../foo/lib/foo.ex", File.read!("../foo/lib/foo.ex") <> "\n")
455-
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
456-
ensure_touched("../foo/lib/foo.ex", mtime)
441+
ensure_touched("../foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir")
457442

458443
Mix.Task.clear()
459444
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
460-
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
445+
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
446+
447+
# Recompiles if export dependency changes
448+
File.write!("../foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar, :baz]")
449+
ensure_touched("../foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir")
450+
451+
Mix.Task.clear()
452+
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
453+
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
454+
455+
# Recompiles if export dependency is removed
456+
File.rm!("../foo/lib/foo.ex")
457+
Mix.Task.clear()
458+
459+
ExUnit.CaptureIO.capture_io(:stderr, fn ->
460+
Process.flag(:trap_exit, true)
461+
catch_exit(Mix.Task.run("compile", ["--verbose"]))
462+
end)
461463
end)
462464
end)
463465
end
@@ -480,16 +482,15 @@ defmodule Mix.UmbrellaTest do
480482
# Add compile time to Foo.Bar
481483
File.write!("lib/bar.ex", "defmodule Bar, do: Foo.Bar.hello()")
482484
Mix.Task.run("compile", ["--verbose"])
483-
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
485+
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
484486

485487
# Recompiles for due to compile dependency via runtime dependencies
486488
File.write!("../foo/lib/foo.baz.ex", File.read!("../foo/lib/foo.baz.ex") <> "\n")
487-
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
488-
ensure_touched("../foo/lib/foo.ex", mtime)
489+
ensure_touched("../foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir")
489490

490491
Mix.Task.clear()
491492
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
492-
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
493+
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
493494
end)
494495
end)
495496
end

0 commit comments

Comments
 (0)