summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2023-04-22 20:45:10 +0200
committerJosé Valim <jose.valim@dashbit.co>2023-04-22 23:45:44 +0200
commit434ef73e91b60abaa3d06e7311df877220360d2b (patch)
tree3b06707ee62f9890b4f0d86be283d8bdf6b78f9b
parente1a6ac386498365b7c128a9a2f102a2fc1de0f64 (diff)
downloadelixir-434ef73e91b60abaa3d06e7311df877220360d2b.tar.gz
Track digests of @external_resources
Closes #12057
-rw-r--r--lib/mix/lib/mix/compilers/elixir.ex23
-rw-r--r--lib/mix/lib/mix/tasks/compile.elixir.ex22
-rw-r--r--lib/mix/test/mix/tasks/compile.elixir_test.exs6
3 files changed, 34 insertions, 17 deletions
diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex
index 004331018..8accc7cba 100644
--- a/lib/mix/lib/mix/compilers/elixir.ex
+++ b/lib/mix/lib/mix/compilers/elixir.ex
@@ -1,7 +1,7 @@
defmodule Mix.Compilers.Elixir do
@moduledoc false
- @manifest_vsn 17
+ @manifest_vsn 18
@checkpoint_vsn 2
import Record
@@ -393,7 +393,7 @@ defmodule Mix.Compilers.Elixir do
Enum.any?(modules, &Map.has_key?(modules_to_recompile, &1)) or
Enum.any?(external, &stale_external?(&1, modified, sources_stats)) or
(last_mtime > modified and
- (missing_beam_file?(dest, modules) or digest != digest(source))),
+ (missing_beam_file?(dest, modules) or digest != digest_file!(source))),
do: source
changed = new_paths ++ changed
@@ -420,10 +420,10 @@ defmodule Mix.Compilers.Elixir do
{modules, exports, changed, sources_stats}
end
- defp stale_external?({external, existed?}, modified, sources_stats) do
+ defp stale_external?({external, digest}, modified, sources_stats) do
case sources_stats do
- %{^external => {0, 0}} -> existed?
- %{^external => {mtime, _}} -> mtime > modified
+ %{^external => {0, 0}} -> digest != nil
+ %{^external => {mtime, _}} -> mtime > modified and digest != digest_file!(external)
end
end
@@ -437,9 +437,11 @@ defmodule Mix.Compilers.Elixir do
end)
end
- defp digest(file) do
- contents = File.read!(file)
+ defp digest_file!(file) do
+ file |> File.read!() |> digest_contents()
+ end
+ defp digest_contents(contents) do
case :erlang.system_info(:wordsize) do
8 -> :crypto.hash(:blake2b, contents)
_ -> :crypto.hash(:blake2s, contents)
@@ -1065,7 +1067,7 @@ defmodule Mix.Compilers.Elixir do
source(
source,
# We preserve the digest if the file is recompiled but not changed
- digest: source(source, :digest) || digest(file),
+ digest: source(source, :digest) || digest_file!(file),
compile_references: compile_references,
export_references: export_references,
runtime_references: runtime_references,
@@ -1149,7 +1151,10 @@ defmodule Mix.Compilers.Elixir do
defp process_external_resources(external, cwd) do
for file <- external do
- {Path.relative_to(file, cwd), File.exists?(file)}
+ case File.read(file) do
+ {:ok, binary} -> {Path.relative_to(file, cwd), digest_contents(binary)}
+ {:error, _} -> {Path.relative_to(file, cwd), nil}
+ end
end
end
end
diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex
index 87776af56..a8d1ac05e 100644
--- a/lib/mix/lib/mix/tasks/compile.elixir.ex
+++ b/lib/mix/lib/mix/tasks/compile.elixir.ex
@@ -9,19 +9,27 @@ defmodule Mix.Tasks.Compile.Elixir do
Elixir is smart enough to recompile only files that have changed
and their dependencies. This means if `lib/a.ex` is invoking
- a function defined over `lib/b.ex`, whenever `lib/b.ex` changes,
- `lib/a.ex` is also recompiled.
+ a function defined over `lib/b.ex` at compile time, whenever
+ `lib/b.ex` changes, `lib/a.ex` is also recompiled.
- Note it is important to recompile a file's dependencies as
- there are often compile time dependencies between them.
+ Note Elixir considers a file as changed if its source file has
+ changed on disk since the last compilation AND its contents are
+ no longer the same.
+
+ ## `@external_resource`
+
+ If a module depends on external files, those can be annotated
+ with the `@external_resource` module attribute. If these files
+ change, the Elixir module is automatically recompiled.
## `__mix_recompile__?/0`
A module may export a `__mix_recompile__?/0` function that can
cause the module to be recompiled using custom rules. For example,
- `@external_resource` already adds a compile-time dependency on an
- external file, however, to depend on a _dynamic_ list of files we
- can do:
+ to recompile whenever a file is changed in a given directory, you
+ can use a combination of `@external_resource` for existing files
+ and a `__mix_recompile__?/0` check to verify when new entries are
+ added to the directory itself:
defmodule MyModule do
paths = Path.wildcard("*.txt")
diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs
index c2e32f7d3..e18cefc00 100644
--- a/lib/mix/test/mix/tasks/compile.elixir_test.exs
+++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs
@@ -983,7 +983,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do
Mix.shell().flush
purge([A, B])
- # Update local existing resource
+ # Update local existing resource timestamp is not enough
+ File.touch!("lib/a.eex", {{2038, 1, 1}, {0, 0, 0}})
+ assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []}
+
+ File.write!("lib/a.eex", [File.read!("lib/a.eex"), ?\n])
File.touch!("lib/a.eex", {{2038, 1, 1}, {0, 0, 0}})
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}