Skip to content

Commit 0f0d4c0

Browse files
committed
Track all stale modules from config/lock as exports too
1 parent 5ee492a commit 0f0d4c0

File tree

2 files changed

+112
-24
lines changed

2 files changed

+112
-24
lines changed

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

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Mix.Compilers.Elixir do
22
@moduledoc false
33

44
@manifest_vsn 13
5+
@checkpoint_vsn 2
56

67
import Record
78

@@ -102,7 +103,7 @@ defmodule Mix.Compilers.Elixir do
102103
{false, stale, old_lock, old_config}
103104
end
104105

105-
{stale_local_mods, stale_local_exports, all_local_exports} =
106+
{stale_modules, stale_exports, all_local_exports} =
106107
stale_local_deps(manifest, stale, modified, all_local_exports)
107108

108109
prev_paths = for source(source: source) <- all_sources, do: source
@@ -120,8 +121,8 @@ defmodule Mix.Compilers.Elixir do
120121
all_modules,
121122
all_sources,
122123
removed,
123-
stale_local_mods,
124-
Map.merge(stale_local_exports, removed_modules),
124+
Map.merge(stale_modules, removed_modules),
125+
Map.merge(stale_exports, removed_modules),
125126
dest
126127
)
127128
end
@@ -180,7 +181,7 @@ defmodule Mix.Compilers.Elixir do
180181
delete_compiler_info()
181182
end
182183
else
183-
# We need to return ok if deps_changed? or stale_local_mods changed,
184+
# We need to return ok if deps_changed? or stale_modules changed,
184185
# even if no code was compiled, because we need to propagate the changed
185186
# status to compile.protocols. This will be the case whenever:
186187
#
@@ -193,7 +194,7 @@ defmodule Mix.Compilers.Elixir do
193194
# will only compute the diff with current protocols. In fact, there is no
194195
# need to reconsolidate if an Erlang file changes and it doesn't trigger
195196
# any other change, but the diff check should be reasonably fast anyway.
196-
status = if removed != [] or deps_changed? or stale_local_mods != %{}, do: :ok, else: :noop
197+
status = if removed != [] or deps_changed? or stale_modules != %{}, do: :ok, else: :noop
197198

198199
# If nothing changed but there is one more recent mtime, bump the manifest
199200
if status != :noop or Enum.any?(Map.values(sources_stats), &(elem(&1, 0) > modified)) do
@@ -283,8 +284,8 @@ defmodule Mix.Compilers.Elixir do
283284
all_modules,
284285
all_sources,
285286
removed,
286-
stale_local_mods,
287-
stale_local_exports,
287+
stale_modules,
288+
stale_exports,
288289
dest
289290
) do
290291
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
@@ -294,13 +295,17 @@ defmodule Mix.Compilers.Elixir do
294295
into: %{},
295296
do: {module, []}
296297

297-
{checkpoint_stale, checkpoint_modules} = parse_checkpoint(manifest)
298+
{checkpoint_stale_modules, checkpoint_stale_exports, checkpoint_modules} =
299+
parse_checkpoint(manifest)
300+
298301
modules_to_recompile = Map.merge(checkpoint_modules, modules_to_recompile)
299-
stale_local_mods = Map.merge(checkpoint_stale, stale_local_mods)
302+
stale_modules = Map.merge(checkpoint_stale_modules, stale_modules)
303+
stale_exports = Map.merge(checkpoint_stale_exports, stale_exports)
300304

301-
if map_size(stale_local_mods) != map_size(checkpoint_stale) or
305+
if map_size(stale_modules) != map_size(checkpoint_stale_modules) or
306+
map_size(stale_exports) != map_size(checkpoint_stale_exports) or
302307
map_size(modules_to_recompile) != map_size(checkpoint_modules) do
303-
write_checkpoint(manifest, stale_local_mods, modules_to_recompile)
308+
write_checkpoint(manifest, stale_modules, stale_exports, modules_to_recompile)
304309
end
305310

306311
sources_stats =
@@ -332,8 +337,8 @@ defmodule Mix.Compilers.Elixir do
332337
all_modules,
333338
all_sources,
334339
removed ++ changed,
335-
stale_local_mods,
336-
stale_local_exports,
340+
stale_modules,
341+
stale_exports,
337342
dest
338343
)
339344

@@ -654,16 +659,16 @@ defmodule Mix.Compilers.Elixir do
654659
# files that have changed. Then it recursively figures out
655660
# all the files that changed (via the module dependencies) and
656661
# return the non-changed entries and the removed sources.
657-
defp update_stale_entries(modules, _sources, [], stale_mods, stale_exports, _compile_path)
658-
when stale_mods == %{} and stale_exports == %{} do
662+
defp update_stale_entries(modules, _sources, [], stale_modules, stale_exports, _compile_path)
663+
when stale_modules == %{} and stale_exports == %{} do
659664
{modules, %{}, []}
660665
end
661666

662-
defp update_stale_entries(modules, sources, changed, stale_mods, stale_exports, compile_path) do
667+
defp update_stale_entries(modules, sources, changed, stale_modules, stale_exports, compile_path) do
663668
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
664669
changed = Enum.into(changed, %{}, &{&1, []})
665670
reducer = &remove_stale_entry(&1, &2, sources, stale_exports, compile_path)
666-
remove_stale_entries(modules, %{}, changed, stale_mods, reducer)
671+
remove_stale_entries(modules, %{}, changed, stale_modules, reducer)
667672
end
668673

669674
defp remove_stale_entries(modules, exports, old_changed, old_stale, reducer) do
@@ -720,14 +725,16 @@ defmodule Mix.Compilers.Elixir do
720725
defp stale_local_deps(manifest, stale_modules, modified, old_exports) do
721726
base = Path.basename(manifest)
722727

728+
# The stale modules so far will become both stale_modules and stale_exports,
729+
# as any export from a dependency needs to be recompiled.
723730
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
724731
stale_modules = for module <- stale_modules, do: {module, []}, into: %{}
725732

726733
for %{scm: scm, opts: opts} = dep <- Mix.Dep.cached(),
727734
not scm.fetchable?,
728735
manifest = Path.join([opts[:build], ".mix", base]),
729736
Mix.Utils.last_modified(manifest) > modified,
730-
reduce: {stale_modules, %{}, old_exports} do
737+
reduce: {stale_modules, stale_modules, old_exports} do
731738
{modules, exports, new_exports} ->
732739
{_manifest_modules, dep_sources} = read_manifest(manifest)
733740

@@ -1017,19 +1024,19 @@ defmodule Mix.Compilers.Elixir do
10171024
(manifest <> ".checkpoint") |> File.read!() |> :erlang.binary_to_term()
10181025
rescue
10191026
_ ->
1020-
{%{}, %{}}
1027+
{%{}, %{}, %{}}
10211028
else
1022-
{@manifest_vsn, stale, recompile_modules} ->
1023-
{stale, recompile_modules}
1029+
{@checkpoint_vsn, stale_modules, stale_exports, recompile_modules} ->
1030+
{stale_modules, stale_exports, recompile_modules}
10241031

10251032
_ ->
1026-
{%{}, %{}}
1033+
{%{}, %{}, %{}}
10271034
end
10281035
end
10291036

1030-
defp write_checkpoint(manifest, stale, recompile_modules) do
1037+
defp write_checkpoint(manifest, stale_modules, stale_exports, recompile_modules) do
10311038
File.mkdir_p!(Path.dirname(manifest))
1032-
term = {@manifest_vsn, stale, recompile_modules}
1039+
term = {@checkpoint_vsn, stale_modules, stale_exports, recompile_modules}
10331040
checkpoint_data = :erlang.term_to_binary(term, [:compressed])
10341041
File.write!(manifest <> ".checkpoint", checkpoint_data)
10351042
end

lib/mix/test/mix/tasks/compile.elixir_test.exs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do
182182
Process.put({MixTest.Case.Sample, :application}, extra_applications: [:logger])
183183
File.mkdir_p!("config")
184184

185+
File.write!("config/config.exs", """
186+
import Config
187+
config :logger, :level, :debug
188+
""")
189+
185190
File.write!("lib/a.ex", """
186191
defmodule A do
187192
_ = Logger.metadata()
@@ -234,6 +239,12 @@ defmodule Mix.Tasks.Compile.ElixirTest do
234239
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
235240
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
236241

242+
# No-op does not recompile
243+
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
244+
assert recompile.() == {:ok, []}
245+
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
246+
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
247+
237248
# Changing self fully recompiles
238249
File.write!("config/config.exs", """
239250
import Config
@@ -263,6 +274,76 @@ defmodule Mix.Tasks.Compile.ElixirTest do
263274
Application.delete_env(:sample, :foo, persistent: true)
264275
end
265276

277+
test "recompiles files when config changes export dependencies" do
278+
in_fixture("no_mixfile", fn ->
279+
Mix.Project.push(MixTest.Case.Sample)
280+
Process.put({MixTest.Case.Sample, :application}, extra_applications: [:ex_unit])
281+
File.mkdir_p!("config")
282+
283+
File.write!("lib/a.ex", """
284+
defmodule A do
285+
def test_struct do
286+
%ExUnit.Test{}
287+
end
288+
end
289+
""")
290+
291+
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
292+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
293+
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
294+
295+
recompile = fn ->
296+
Mix.ProjectStack.pop()
297+
Mix.Project.push(MixTest.Case.Sample)
298+
Mix.Tasks.Loadconfig.load_compile("config/config.exs")
299+
Mix.Tasks.Compile.Elixir.run(["--verbose"])
300+
end
301+
302+
# Adding config recompiles
303+
File.write!("config/config.exs", """
304+
import Config
305+
config :ex_unit, :some, :config
306+
""")
307+
308+
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
309+
assert recompile.() == {:ok, []}
310+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
311+
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
312+
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
313+
314+
# Changing config recompiles
315+
File.write!("config/config.exs", """
316+
import Config
317+
config :ex_unit, :some, :another
318+
""")
319+
320+
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
321+
assert recompile.() == {:ok, []}
322+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
323+
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
324+
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
325+
326+
# Removing config recompiles
327+
File.write!("config/config.exs", """
328+
import Config
329+
""")
330+
331+
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
332+
assert recompile.() == {:ok, []}
333+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
334+
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
335+
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
336+
337+
# No-op does not recompile
338+
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
339+
assert recompile.() == {:ok, []}
340+
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
341+
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
342+
end)
343+
after
344+
Application.delete_env(:ex_unit, :some, persistent: true)
345+
end
346+
266347
test "recompiles files when config changes with crashes" do
267348
in_fixture("no_mixfile", fn ->
268349
Mix.Project.push(MixTest.Case.Sample)

0 commit comments

Comments
 (0)