Skip to content

Commit 74f9a29

Browse files
marcandrejosevalim
authored andcommitted
Rely on modification time and hash to determine modified sources (#11080)
1 parent 2ce9d25 commit 74f9a29

File tree

4 files changed

+59
-16
lines changed

4 files changed

+59
-16
lines changed

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

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

4-
@manifest_vsn 8
4+
@manifest_vsn 9
55

66
import Record
77

@@ -10,6 +10,7 @@ defmodule Mix.Compilers.Elixir do
1010
defrecord :source,
1111
source: nil,
1212
size: 0,
13+
digest: nil,
1314
compile_references: [],
1415
export_references: [],
1516
runtime_references: [],
@@ -206,12 +207,13 @@ defmodule Mix.Compilers.Elixir do
206207
# Sources that have changed on disk or
207208
# any modules associated with them need to be recompiled
208209
changed =
209-
for source(source: source, external: external, size: size, modules: modules) <-
210+
for source(source: source, external: external, size: size, digest: digest, modules: modules) <-
210211
all_sources,
211212
{last_mtime, last_size} = Map.fetch!(sources_stats, source),
212213
times = Enum.map(external, &(sources_stats |> Map.fetch!(&1) |> elem(0))),
213-
size != last_size or Mix.Utils.stale?([last_mtime | times], [modified]) or
214-
Enum.any?(modules, &Map.has_key?(modules_to_recompile, &1)),
214+
Enum.any?(modules, &Map.has_key?(modules_to_recompile, &1)) or
215+
Mix.Utils.stale?(times, [modified]) or
216+
(size != last_size or (last_mtime > modified and digest != digest(source))),
215217
do: source
216218

217219
changed = new_paths ++ changed
@@ -246,6 +248,12 @@ defmodule Mix.Compilers.Elixir do
246248
end)
247249
end
248250

251+
defp digest(file) do
252+
file
253+
|> File.read!()
254+
|> :erlang.md5()
255+
end
256+
249257
defp compile_path(stale, dest, timestamp, opts) do
250258
cwd = File.cwd!()
251259
long_compilation_threshold = opts[:long_compilation_threshold] || 10
@@ -460,6 +468,8 @@ defmodule Mix.Compilers.Elixir do
460468
source =
461469
source(
462470
source,
471+
# We preserve the digest if the file is recompiled but not changed
472+
digest: source(source, :digest) || digest(file),
463473
compile_references: compile_references,
464474
export_references: export_references,
465475
runtime_references: runtime_references,
@@ -509,11 +519,11 @@ defmodule Mix.Compilers.Elixir do
509519
# to be recompiled (but were not changed on disk)
510520
defp update_stale_sources(sources, changed) do
511521
Enum.reduce(changed, {sources, %{}}, fn file, {acc_sources, acc_modules} ->
512-
{source(size: size, modules: modules), acc_sources} =
522+
{source(size: size, digest: digest, modules: modules), acc_sources} =
513523
List.keytake(acc_sources, file, source(:source))
514524

515525
acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true))
516-
{[source(source: file, size: size) | acc_sources], acc_modules}
526+
{[source(source: file, size: size, digest: digest) | acc_sources], acc_modules}
517527
end)
518528
end
519529

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

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
195195
end)
196196
end
197197

198-
test "compiles mtime changed files" do
198+
test "compiles mtime changed files if content changed but not length" do
199199
in_fixture("no_mixfile", fn ->
200200
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
201201
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -204,6 +204,8 @@ defmodule Mix.Tasks.Compile.ElixirTest do
204204
Mix.shell().flush
205205
purge([A, B])
206206

207+
same_length_content = "lib/a.ex" |> File.read!() |> String.replace("A", "Z")
208+
File.write!("lib/a.ex", same_length_content)
207209
future = {{2038, 1, 1}, {0, 0, 0}}
208210
File.touch!("lib/a.ex", future)
209211
Mix.Tasks.Compile.Elixir.run(["--verbose"])
@@ -225,6 +227,36 @@ defmodule Mix.Tasks.Compile.ElixirTest do
225227
end)
226228
end
227229

230+
test "does not recompile mtime changed but identical files" do
231+
in_fixture("no_mixfile", fn ->
232+
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
233+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
234+
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
235+
236+
Mix.shell().flush
237+
purge([A, B])
238+
239+
future = {{2038, 1, 1}, {0, 0, 0}}
240+
File.touch!("lib/a.ex", future)
241+
Mix.Tasks.Compile.Elixir.run(["--verbose"])
242+
243+
message =
244+
"warning: mtime (modified time) for \"lib/a.ex\" was set to the future, resetting to now"
245+
246+
assert_received {:mix_shell, :error, [^message]}
247+
248+
message =
249+
"warning: mtime (modified time) for \"lib/b.ex\" was set to the future, resetting to now"
250+
251+
refute_received {:mix_shell, :error, [^message]}
252+
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
253+
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
254+
255+
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", future)
256+
assert Mix.Tasks.Compile.Elixir.run([]) == {:noop, []}
257+
end)
258+
end
259+
228260
test "compiles size changed files" do
229261
in_fixture("no_mixfile", fn ->
230262
past = {{2010, 1, 1}, {0, 0, 0}}
@@ -257,8 +289,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
257289
Mix.shell().flush
258290
purge([A, B])
259291

260-
future = {{2038, 1, 1}, {0, 0, 0}}
261-
File.touch!("lib/b.ex", future)
292+
force_recompilation("lib/b.ex")
262293
Mix.Tasks.Compile.Elixir.run(["--verbose"])
263294

264295
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -283,7 +314,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
283314

284315
Code.put_compiler_option(:ignore_module_conflict, true)
285316
Code.compile_file("lib/b.ex")
286-
File.touch!("lib/a.ex", {{2038, 1, 1}, {0, 0, 0}})
317+
force_recompilation("lib/a.ex")
287318

288319
Mix.Tasks.Compile.Elixir.run(["--verbose"])
289320
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -472,8 +503,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
472503
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
473504
purge([A, B])
474505

475-
future = {{2038, 1, 1}, {0, 0, 0}}
476-
File.touch!("lib/a.ex", future)
506+
force_recompilation("lib/a.ex")
477507

478508
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
479509
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -615,8 +645,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
615645
Mix.shell().flush
616646
purge([A, B])
617647

618-
future = {{2038, 1, 1}, {0, 0, 0}}
619-
File.touch!("lib/a.ex", future)
648+
force_recompilation("lib/a.ex")
620649
Mix.Tasks.Compile.Elixir.run(["--verbose"])
621650

622651
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}

lib/mix/test/mix/tasks/test_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ defmodule Mix.Tasks.TestTest do
6868
assert_stale_run_output("2 tests, 0 failures")
6969

7070
set_all_mtimes()
71-
File.touch!("lib/b.ex")
71+
force_recompilation("lib/b.ex")
7272

7373
assert_stale_run_output("1 test, 0 failures")
7474

7575
set_all_mtimes()
76-
File.touch!("lib/a.ex")
76+
force_recompilation("lib/a.ex")
7777

7878
assert_stale_run_output("2 tests, 0 failures")
7979
end)

lib/mix/test/test_helper.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ defmodule MixTest.Case do
179179
])
180180
end
181181

182+
def force_recompilation(file) do
183+
File.write!(file, File.read!(file) <> "\n")
184+
end
185+
182186
defp mix_executable do
183187
Path.expand("../../../bin/mix", __DIR__)
184188
end

0 commit comments

Comments
 (0)