Skip to content

Commit a6d7232

Browse files
authored
Rely on mtime to recompile app (#14012)
1 parent 0c0f53c commit a6d7232

File tree

4 files changed

+76
-67
lines changed

4 files changed

+76
-67
lines changed

lib/mix/lib/mix/release.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -882,9 +882,7 @@ defmodule Mix.Release do
882882

883883
defp process_app_file(source_file, target_file) do
884884
with {:ok, [{:application, app, info}]} <- :file.consult(source_file) do
885-
# Remove :config_mtime so that .app files are deterministic between builds
886-
new_info = Keyword.delete(info, :config_mtime)
887-
File.write!(target_file, :io_lib.format("~tp.~n", [{:application, app, new_info}]))
885+
File.write!(target_file, :io_lib.format("~tp.~n", [{:application, app, info}]))
888886
else
889887
_ -> File.cp!(source_file, target_file)
890888
end

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

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ defmodule Mix.Tasks.Compile.App do
9292
* `--compile-path` - where to find `.beam` files and write the
9393
resulting `.app` file, defaults to `Mix.Project.compile_path/0`
9494
95+
## Configuration
96+
97+
* `:reliable_dir_mtime` - this task relies on the operating system
98+
changing the mtime on a directory whenever a file is added or removed.
99+
You can set this option to false if your system does not provide
100+
reliable mtimes. Defaults to false on Windows.
101+
95102
## Phases
96103
97104
Applications provide a start phases mechanism which will be called,
@@ -130,50 +137,58 @@ defmodule Mix.Tasks.Compile.App do
130137
@impl true
131138
def run(args) do
132139
{opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, compile_path: :string])
133-
134140
project = Mix.Project.get!()
135141
config = Mix.Project.config()
136142

137143
app = Keyword.get(config, :app)
138144
version = Keyword.get(config, :version)
145+
validate_app!(app)
146+
validate_version!(version)
139147

140-
validate_app(app)
141-
validate_version(version)
148+
compile_path = Keyword.get_lazy(opts, :compile_path, &Mix.Project.compile_path/0)
149+
target = Path.join(compile_path, "#{app}.app")
142150

143-
path = Keyword.get_lazy(opts, :compile_path, &Mix.Project.compile_path/0)
144-
modules = modules_from(path) |> Enum.sort()
145-
146-
target = Path.join(path, "#{app}.app")
147-
148-
# We mostly depend on the project_file through the def application function,
149-
# but it doesn't hurt to also include config_mtime.
151+
# If configurations changed, we may have changed compile_env.
152+
# If compile_path changed, we may have added or removed files.
153+
# If the project changed, we may have changed other properties.
150154
new_mtime =
151-
max(Mix.Project.config_mtime(), Mix.Utils.last_modified(Mix.Project.project_file()))
155+
Mix.Project.config_mtime()
156+
|> max(Mix.Utils.last_modified(Mix.Project.project_file()))
157+
|> max(Mix.Utils.last_modified(compile_path))
152158

153159
current_properties = current_app_properties(target)
154-
compile_env = load_compile_env(current_properties)
155-
old_mtime = Keyword.get(current_properties, :config_mtime, 0)
156160

157-
if opts[:force] || new_mtime > old_mtime ||
158-
app_changed?(current_properties, modules, compile_env) do
161+
{changed?, modules} =
162+
cond do
163+
opts[:force] || new_mtime > Mix.Utils.last_modified(target) ->
164+
{true, nil}
165+
166+
Keyword.get(config, :reliable_dir_mtime, fn -> not match?({:win32, _}, :os.type()) end) ->
167+
{false, nil}
168+
169+
true ->
170+
modules = modules_from(compile_path)
171+
{modules != Keyword.get(current_properties, :modules, []), modules}
172+
end
173+
174+
if changed? do
159175
properties =
160176
[
161177
description: to_charlist(config[:description] || app),
162-
modules: modules,
163178
registered: [],
164179
vsn: to_charlist(version)
165180
]
166181
|> merge_project_application(project)
167-
|> validate_properties!()
168182
|> handle_extra_applications(config)
169-
|> add_compile_env(compile_env)
183+
|> add_compile_env(current_properties)
184+
|> add_modules(modules, compile_path)
170185

171-
properties = [config_mtime: new_mtime] ++ properties
172186
contents = :io_lib.format("~p.~n", [{:application, app, properties}])
173187
:application.load({:application, app, properties})
174188

175189
Mix.Project.ensure_structure()
176190
File.write!(target, IO.chardata_to_string(contents))
191+
File.touch!(target, new_mtime)
177192
Mix.shell().info("Generated #{app} app")
178193
{:ok, []}
179194
else
@@ -189,27 +204,15 @@ defmodule Mix.Tasks.Compile.App do
189204
end
190205
end
191206

192-
defp load_compile_env(current_properties) do
193-
case Mix.ProjectStack.compile_env(nil) do
194-
nil -> Keyword.get(current_properties, :compile_env, [])
195-
list -> list
196-
end
197-
end
198-
199-
defp app_changed?(properties, mods, compile_env) do
200-
Keyword.get(properties, :modules, []) != mods or
201-
Keyword.get(properties, :compile_env, []) != compile_env
202-
end
207+
defp validate_app!(app) when is_atom(app), do: :ok
203208

204-
defp validate_app(app) when is_atom(app), do: :ok
205-
206-
defp validate_app(app) do
207-
ensure_present(:app, app)
209+
defp validate_app!(app) do
210+
ensure_present!(:app, app)
208211
Mix.raise("Expected :app to be an atom, got: #{inspect(app)}")
209212
end
210213

211-
defp validate_version(version) do
212-
ensure_present(:version, version)
214+
defp validate_version!(version) do
215+
ensure_present!(:version, version)
213216

214217
if not (is_binary(version) and match?({:ok, _}, Version.parse(version))) do
215218
Mix.raise(
@@ -218,18 +221,20 @@ defmodule Mix.Tasks.Compile.App do
218221
end
219222
end
220223

221-
defp ensure_present(name, nil) do
224+
defp ensure_present!(name, nil) do
222225
Mix.raise("Please ensure mix.exs file has the #{inspect(name)} in the project definition")
223226
end
224227

225-
defp ensure_present(_name, _val), do: :ok
228+
defp ensure_present!(_name, _val), do: :ok
226229

227230
defp modules_from(path) do
228231
case File.ls(path) do
229232
{:ok, entries} ->
230-
for entry <- entries,
231-
String.ends_with?(entry, ".beam"),
232-
do: entry |> binary_part(0, byte_size(entry) - 5) |> String.to_atom()
233+
Enum.sort(
234+
for entry <- entries,
235+
String.ends_with?(entry, ".beam"),
236+
do: entry |> binary_part(0, byte_size(entry) - 5) |> String.to_atom()
237+
)
233238

234239
{:error, _} ->
235240
[]
@@ -246,7 +251,7 @@ defmodule Mix.Tasks.Compile.App do
246251
)
247252
end
248253

249-
Keyword.merge(best_guess, project_application)
254+
Keyword.merge(best_guess, validate_properties!(project_application))
250255
else
251256
best_guess
252257
end
@@ -357,8 +362,21 @@ defmodule Mix.Tasks.Compile.App do
357362
defp typed_app?({app, type}) when is_atom(app) and type in [:required, :optional], do: true
358363
defp typed_app?(_), do: false
359364

360-
defp add_compile_env(properties, []), do: properties
361-
defp add_compile_env(properties, compile_env), do: [compile_env: compile_env] ++ properties
365+
defp add_compile_env(properties, current_properties) do
366+
# If someone calls compile.elixir and then compile.app across two
367+
# separate OS calls, then the compile_env won't be properly reflected.
368+
# This is ok because compile_env is not used for correctness. It is
369+
# simply to catch possible errors early.
370+
case Mix.ProjectStack.compile_env(nil) do
371+
nil -> Keyword.take(current_properties, [:compile_env]) ++ properties
372+
[] -> properties
373+
compile_env -> Keyword.put(properties, :compile_env, compile_env)
374+
end
375+
end
376+
377+
defp add_modules(properties, modules, compile_path) do
378+
Keyword.put_new_lazy(properties, :modules, fn -> modules || modules_from(compile_path) end)
379+
end
362380

363381
defp handle_extra_applications(properties, config) do
364382
{extra, properties} = Keyword.pop(properties, :extra_applications, [])

lib/mix/test/mix/release_test.exs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -716,13 +716,6 @@ defmodule Mix.ReleaseTest do
716716

717717
assert mode!(source_so_path) == mode!(tmp_path("mix_release/libtest_nif.so"))
718718
end
719-
720-
test "removes config_mtime from app files" do
721-
assert copy_ebin(release([]), @eex_ebin, tmp_path("eex_ebin"))
722-
723-
{:ok, [{:application, :eex, info}]} = :file.consult(tmp_path("eex_ebin/eex.app"))
724-
refute Keyword.get(info, :config_mtime)
725-
end
726719
end
727720

728721
describe "copy_app/2" do
@@ -755,14 +748,6 @@ defmodule Mix.ReleaseTest do
755748
refute File.exists?(Path.join(@release_lib, "runtime_tools-#{@runtime_tools_version}/ebin"))
756749
refute File.exists?(Path.join(@release_lib, "runtime_tools-#{@runtime_tools_version}/priv"))
757750
end
758-
759-
test "removes config_mtime from app files" do
760-
assert copy_app(release(strip_beams: false, applications: [eex: :permanent]), :eex)
761-
762-
eex_app_path = Path.join(@release_lib, "eex-#{@elixir_version}/ebin/eex.app")
763-
{:ok, [{:application, :eex, info}]} = :file.consult(eex_app_path)
764-
refute Keyword.get(info, :config_mtime)
765-
end
766751
end
767752

768753
describe "strip_beam/1" do

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,32 @@ defmodule Mix.Tasks.Compile.AppTest do
8686
test "generates .app file with compile_env" do
8787
in_fixture("no_mixfile", fn ->
8888
Mix.Project.push(MixTest.Case.Sample)
89+
File.mkdir_p!("config")
90+
File.write!("config/config.exs", "[]")
91+
Mix.Task.run("loadconfig")
92+
93+
reset_config = fn ->
94+
Mix.ProjectStack.reset_config_mtime()
95+
ensure_touched("config/config.exs", "_build/dev/lib/sample/ebin/sample.app")
96+
end
8997

9098
Mix.ProjectStack.compile_env([{:app, :key, :error}])
9199
assert Mix.Tasks.Compile.App.run([]) == {:ok, []}
92100
assert parse_resource_file(:sample)[:compile_env] == [{:app, :key, :error}]
93101

94102
# No-op with untouched unset compile_env
95-
assert Mix.Tasks.Compile.App.run([]) == {:noop, []}
96-
97-
# No-op with same compile_env
98-
Mix.ProjectStack.compile_env([{:app, :key, :error}])
99-
assert Mix.Tasks.Compile.App.run([]) == {:noop, []}
103+
reset_config.()
104+
assert Mix.Tasks.Compile.App.run([]) == {:ok, []}
105+
assert parse_resource_file(:sample)[:compile_env] == [{:app, :key, :error}]
100106

101107
# Recompiles with new compile_env
108+
reset_config.()
102109
Mix.ProjectStack.compile_env([{:app, :another, :error}])
103110
assert Mix.Tasks.Compile.App.run([]) == {:ok, []}
104111
assert parse_resource_file(:sample)[:compile_env] == [{:app, :another, :error}]
105112

106113
# Keeps compile_env if forcing
114+
reset_config.()
107115
assert Mix.Tasks.Compile.App.run(["--force"]) == {:ok, []}
108116
assert parse_resource_file(:sample)[:compile_env] == [{:app, :another, :error}]
109117
end)

0 commit comments

Comments
 (0)