Skip to content

Commit 3798dff

Browse files
author
José Valim
committed
Only recompile empty files if they changed
Prior to this commit, an Elixir file that did not generate any modules would always be regenerated whenever mix compile was invoked. This commit addresses this issue by keeping source files on its own rows in the Elixir manifest. Signed-off-by: José Valim <[email protected]>
1 parent 17e8dbb commit 3798dff

File tree

2 files changed

+70
-38
lines changed

2 files changed

+70
-38
lines changed

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

Lines changed: 58 additions & 38 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 :v2
4+
@manifest_vsn :v3
55

66
@doc """
77
Compiles stale Elixir files.
@@ -18,10 +18,10 @@ defmodule Mix.Compilers.Elixir do
1818
def compile(manifest, srcs, skip, exts, dest, force, on_start) do
1919
keep = srcs -- skip
2020
all = Mix.Utils.extract_files(keep, exts)
21-
{all_entries, skip_entries} = parse_manifest(manifest, keep)
21+
{all_entries, skip_entries, all_sources, skip_sources} = parse_manifest(manifest, keep)
2222

2323
removed =
24-
for {_b, _m, _k, source, _cd, _rd, _f, _bin} <- all_entries,
24+
for {source, _files} <- all_sources,
2525
not(source in all),
2626
do: source
2727

@@ -32,43 +32,43 @@ defmodule Mix.Compilers.Elixir do
3232
all
3333
else
3434
modified = Mix.Utils.last_modified(manifest)
35-
all_mtimes = mtimes(all_entries)
35+
all_mtimes = mtimes(all_sources)
3636

3737
# Otherwise let's start with the new ones
3838
# plus the ones that have changed
3939
for(source <- all,
40-
not Enum.any?(all_entries, fn {_b, _m, _k, s, _cd, _rd, _f, _bin} -> s == source end),
40+
not Map.has_key?(all_sources, source),
4141
do: source)
4242
++
43-
for({_b, _m, _k, source, _cd, _rd, files, _bin} <- all_entries,
43+
for({source, files} <- all_sources,
4444
times = Enum.map([source|files], &Map.fetch!(all_mtimes, &1)),
4545
Mix.Utils.stale?(times, [modified]),
4646
do: source)
4747
end
4848

49+
sources = update_stale_sources(all_sources, removed, changed)
4950
{entries, changed} = remove_stale_entries(all_entries, removed ++ changed)
5051
stale = changed -- removed
5152

53+
new_entries = entries ++ skip_entries
54+
new_sources = Map.merge(sources, skip_sources)
55+
5256
cond do
5357
stale != [] ->
54-
compile_manifest(manifest, entries ++ skip_entries, stale, dest, on_start)
58+
compile_manifest(manifest, new_entries, new_sources, stale, dest, on_start)
5559
:ok
5660
removed != [] ->
57-
write_manifest(manifest, entries ++ skip_entries)
61+
write_manifest(manifest, new_entries, new_sources)
5862
:ok
5963
true ->
6064
:noop
6165
end
6266
end
6367

64-
defp mtimes(entries) do
65-
Enum.reduce(entries, %{}, fn {_b, _m, _k, source, _cd, _rd, files, _bin}, dict ->
66-
Enum.reduce([source|files], dict, fn file, dict ->
67-
if Map.has_key?(dict, file) do
68-
dict
69-
else
70-
Map.put(dict, file, Mix.Utils.last_modified(file))
71-
end
68+
defp mtimes(sources) do
69+
Enum.reduce(sources, %{}, fn {source, files}, map ->
70+
Enum.reduce([source|files], map, fn file, map ->
71+
Map.put_new_lazy(map, file, fn -> Mix.Utils.last_modified(file) end)
7272
end)
7373
end)
7474
end
@@ -77,22 +77,24 @@ defmodule Mix.Compilers.Elixir do
7777
Removes compiled files.
7878
"""
7979
def clean(manifest) do
80-
Enum.map read_manifest(manifest), fn {beam, _, _, _, _, _, _, _} ->
81-
File.rm(beam)
80+
Enum.each read_manifest(manifest), fn
81+
{beam, _, _, _, _, _, _} ->
82+
File.rm(beam)
83+
{_, _} ->
84+
:ok
8285
end
83-
:ok
8486
end
8587

8688
@doc """
8789
Returns protocols and implementations for the given manifest.
8890
"""
8991
def protocols_and_impls(manifest) do
90-
for {_, module, kind, _, _, _, _, _} <- read_manifest(manifest),
92+
for {_, module, kind, _, _, _, _} <- read_manifest(manifest),
9193
match?(:protocol, kind) or match?({:impl, _}, kind),
9294
do: {module, kind}
9395
end
9496

95-
defp compile_manifest(manifest, entries, stale, dest, on_start) do
97+
defp compile_manifest(manifest, entries, sources, stale, dest, on_start) do
9698
Mix.Project.ensure_structure()
9799
true = Code.prepend_path(dest)
98100

@@ -101,16 +103,16 @@ defmodule Mix.Compilers.Elixir do
101103

102104
# Starts a server responsible for keeping track which files
103105
# were compiled and the dependencies between them.
104-
{:ok, pid} = Agent.start_link(fn -> entries end)
106+
{:ok, pid} = Agent.start_link(fn -> {entries, sources} end)
105107

106108
try do
107109
_ = Kernel.ParallelCompiler.files :lists.usort(stale),
108110
each_module: &each_module(pid, dest, cwd, &1, &2, &3),
109111
each_file: &each_file(&1),
110112
dest: dest
111-
Agent.cast pid, fn entries ->
112-
write_manifest(manifest, entries)
113-
entries
113+
Agent.cast pid, fn {entries, sources} ->
114+
write_manifest(manifest, entries, sources)
115+
{entries, sources}
114116
end
115117
after
116118
Agent.stop(pid, :normal, :infinity)
@@ -140,8 +142,12 @@ defmodule Mix.Compilers.Elixir do
140142
kind = detect_kind(module)
141143
source = Path.relative_to(source, cwd)
142144
files = get_external_resources(module, cwd)
143-
tuple = {beam, module, kind, source, compile, runtime, files, binary}
144-
Agent.cast pid, &:lists.keystore(beam, 1, &1, tuple)
145+
146+
Agent.cast pid, fn {entries, sources} ->
147+
entries = List.keystore(entries, beam, 0, {beam, module, kind, source, compile, runtime, binary})
148+
sources = Map.update(sources, source, files, & files ++ &1)
149+
{entries, sources}
150+
end
145151
end
146152

147153
defp detect_kind(module) do
@@ -163,12 +169,16 @@ defmodule Mix.Compilers.Elixir do
163169
do: relative
164170
end
165171

166-
defp each_file(file) do
167-
Mix.shell.info "Compiled #{file}"
172+
defp each_file(source) do
173+
Mix.shell.info "Compiled #{source}"
168174
end
169175

170176
## Resolution
171177

178+
defp update_stale_sources(sources, removed, changed) do
179+
Enum.reduce changed, Map.drop(sources, removed), &Map.put(&2, &1, [])
180+
end
181+
172182
# This function receives the manifest entries and some source
173183
# files that have changed. It then, recursively, figures out
174184
# all the files that changed (via the module dependencies) and
@@ -193,7 +203,7 @@ defmodule Mix.Compilers.Elixir do
193203
end
194204
end
195205

196-
defp remove_stale_entry({beam, module, _kind, source, compile, runtime, _f, _bin} = entry,
206+
defp remove_stale_entry({beam, module, _kind, source, compile, runtime, _bin} = entry,
197207
{rest, stale, removed}) do
198208
cond do
199209
# If I changed in disk or have a compile time dependency
@@ -225,30 +235,40 @@ defmodule Mix.Compilers.Elixir do
225235
end
226236

227237
defp parse_manifest(manifest, keep_paths) do
228-
Enum.reduce read_manifest(manifest), {[], []}, fn
229-
{_, _, _, source, _, _, _, _} = entry, {keep, skip} ->
238+
Enum.reduce read_manifest(manifest), {[], [], %{}, %{}}, fn
239+
{_, _, _, source, _, _, _} = entry, {keep, skip, keep_sources, skip_sources} ->
230240
if String.starts_with?(source, keep_paths) do
231-
{[entry|keep], skip}
241+
{[entry|keep], skip, keep_sources, skip_sources}
232242
else
233-
{keep, [entry|skip]}
243+
{keep, [entry|skip], keep_sources, skip_sources}
244+
end
245+
{source, files}, {keep, skip, keep_sources, skip_sources} ->
246+
if String.starts_with?(source, keep_paths) do
247+
{keep, skip, Map.put(keep_sources, source, files), skip_sources}
248+
else
249+
{keep, skip, keep_sources, Map.put(skip_sources, source, files)}
234250
end
235251
end
236252
end
237253

238-
defp write_manifest(manifest, []) do
254+
defp write_manifest(manifest, [], sources) when sources == %{} do
239255
File.rm(manifest)
240256
:ok
241257
end
242258

243-
defp write_manifest(manifest, entries) do
259+
defp write_manifest(manifest, entries, sources) do
244260
File.mkdir_p!(Path.dirname(manifest))
245261

246262
File.open!(manifest, [:write], fn device ->
247263
:io.format(device, '~p.~n', [@manifest_vsn])
248264

249-
Enum.map entries, fn {beam, _, _, _, _, _, _, binary} = entry ->
265+
Enum.each entries, fn {beam, _, _, _, _, _, binary} = entry ->
250266
if binary, do: File.write!(beam, binary)
251-
:io.format(device, '~p.~n', [put_elem(entry, 7, nil)])
267+
:io.format(device, '~p.~n', [put_elem(entry, 6, nil)])
268+
end
269+
270+
Enum.each sources, fn {_, _} = entry ->
271+
:io.format(device, '~p.~n', [entry])
252272
end
253273

254274
:ok

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,18 @@ defmodule Mix.Tasks.Compile.ElixirTest do
192192
end
193193
end
194194

195+
test "does not recompile empty files" do
196+
in_fixture "no_mixfile", fn ->
197+
File.write!("lib/a.ex", "")
198+
199+
assert Mix.Tasks.Compile.Elixir.run([]) == :ok
200+
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
201+
202+
assert Mix.Tasks.Compile.Elixir.run([]) == :noop
203+
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
204+
end
205+
end
206+
195207
test "recompiles with --force" do
196208
in_fixture "no_mixfile", fn ->
197209
assert Mix.Tasks.Compile.Elixir.run([]) == :ok

0 commit comments

Comments
 (0)