@@ -23,15 +23,15 @@ defmodule Mix.Compilers.Elixir do
23
23
Compiles stale Elixir files.
24
24
25
25
It expects a `manifest` file, the source directories, the destination
26
- directory, an option to know if compilation is being forced or not, and a
27
- list of any additional compiler options .
26
+ directory, the cache key based on compiler configuration, external
27
+ manifests, and external modules, followed by opts .
28
28
29
29
The `manifest` is written down with information including dependencies
30
30
between modules, which helps it recompile only the modules that
31
31
have changed at runtime.
32
32
"""
33
33
def compile ( manifest , srcs , dest , new_cache_key , new_parent_manifests , new_parents , opts ) do
34
- manifest_last_modified = Mix.Utils . last_modified ( manifest )
34
+ modified = Mix.Utils . last_modified ( manifest )
35
35
new_parents = :ordsets . from_list ( new_parents )
36
36
37
37
# We fetch the time from before we read files so any future
@@ -47,22 +47,22 @@ defmodule Mix.Compilers.Elixir do
47
47
# we need to recompile all references to old and new modules.
48
48
stale =
49
49
if old_parents != new_parents or
50
- Mix.Utils . stale? ( new_parent_manifests , [ manifest_last_modified ] ) do
50
+ Mix.Utils . stale? ( new_parent_manifests , [ modified ] ) do
51
51
:ordsets . union ( old_parents , new_parents )
52
52
else
53
53
[ ]
54
54
end
55
55
56
56
# If mix.exs has changed, recompile anything that calls Mix.Project.
57
57
stale =
58
- if Mix.Utils . stale? ( [ Mix.Project . project_file ( ) ] , [ manifest_last_modified ] ) ,
58
+ if Mix.Utils . stale? ( [ Mix.Project . project_file ( ) ] , [ modified ] ) ,
59
59
do: [ Mix.Project | stale ] ,
60
60
else: stale
61
61
62
62
# If the dependencies have changed, we need to traverse lock/config files.
63
- deps_changed? = Mix.Utils . stale? ( [ Mix.Project . config_mtime ( ) ] , [ manifest_last_modified ] )
63
+ deps_changed? = Mix.Utils . stale? ( [ Mix.Project . config_mtime ( ) ] , [ modified ] )
64
64
65
- # The app tracker will return information about apps before this compilation.
65
+ # The app tracer will return information about apps before this compilation.
66
66
app_tracer = Mix.Compilers.ApplicationTracer . init ( )
67
67
68
68
{ force? , stale , new_lock , new_config } =
@@ -102,8 +102,6 @@ defmodule Mix.Compilers.Elixir do
102
102
{ false , stale , old_lock , old_config }
103
103
end
104
104
105
- modified = Mix.Utils . last_modified ( manifest )
106
-
107
105
{ stale_local_mods , stale_local_exports , all_local_exports } =
108
106
stale_local_deps ( manifest , stale , modified , all_local_exports )
109
107
@@ -116,11 +114,11 @@ defmodule Mix.Compilers.Elixir do
116
114
compiler_info_from_force ( manifest , all_paths , all_modules , dest )
117
115
else
118
116
compiler_info_from_updated (
117
+ manifest ,
119
118
modified ,
120
- all_paths ,
119
+ all_paths -- prev_paths ,
121
120
all_modules ,
122
121
all_sources ,
123
- prev_paths ,
124
122
removed ,
125
123
stale_local_mods ,
126
124
Map . merge ( stale_local_exports , removed_modules ) ,
@@ -182,9 +180,9 @@ defmodule Mix.Compilers.Elixir do
182
180
delete_compiler_info ( )
183
181
end
184
182
else
185
- # We need to return ok if deps_changed? or stale_local_mods changed
186
- # because we want to propagate the changed status to compile.protocols.
187
- # This will be the case whenever:
183
+ # We need to return ok if deps_changed? or stale_local_mods changed,
184
+ # even if no code was compiled, because we need to propagate the changed
185
+ # status to compile.protocols. This will be the case whenever:
188
186
#
189
187
# * the lock file or a config changes
190
188
# * any module in a path dependency changes
@@ -272,47 +270,59 @@ defmodule Mix.Compilers.Elixir do
272
270
{ [ ] , % { } , all_paths , sources_stats }
273
271
end
274
272
275
- # Assume that either all .beam files are missing, or none of them are
273
+ # If any .beam file is missing, the first one will the first to miss,
274
+ # so we always check that. If there are no modules, then we can rely
275
+ # purely on digests.
276
276
defp missing_beam_file? ( dest , [ mod | _ ] ) , do: not File . exists? ( beam_path ( dest , mod ) )
277
277
defp missing_beam_file? ( _dest , [ ] ) , do: false
278
278
279
279
defp compiler_info_from_updated (
280
+ manifest ,
280
281
modified ,
281
- all_paths ,
282
+ new_paths ,
282
283
all_modules ,
283
284
all_sources ,
284
- prev_paths ,
285
285
removed ,
286
286
stale_local_mods ,
287
287
stale_local_exports ,
288
288
dest
289
289
) do
290
- # Otherwise let's start with the new sources
291
- new_paths = all_paths -- prev_paths
290
+ # TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
291
+ modules_to_recompile =
292
+ for module ( module: module , recompile?: true ) <- all_modules ,
293
+ recompile_module? ( module ) ,
294
+ into: % { } ,
295
+ do: { module , [ ] }
296
+
297
+ { checkpoint_stale , checkpoint_modules } = parse_checkpoint ( manifest )
298
+ modules_to_recompile = Map . merge ( checkpoint_modules , modules_to_recompile )
299
+ stale_local_mods = Map . merge ( checkpoint_stale , stale_local_mods )
300
+
301
+ if map_size ( stale_local_mods ) != map_size ( checkpoint_stale ) or
302
+ map_size ( modules_to_recompile ) != map_size ( checkpoint_modules ) do
303
+ write_checkpoint ( manifest , stale_local_mods , modules_to_recompile )
304
+ end
292
305
293
306
sources_stats =
294
307
for path <- new_paths ,
295
308
into: mtimes_and_sizes ( all_sources ) ,
296
309
do: { path , Mix.Utils . last_modified_and_size ( path ) }
297
310
298
- modules_to_recompile =
299
- for module ( module: module , recompile?: true ) <- all_modules ,
300
- recompile_module? ( module ) ,
301
- into: % { } ,
302
- do: { module , true }
303
-
304
311
# Sources that have changed on disk or
305
312
# any modules associated with them need to be recompiled
306
313
changed =
307
314
for source ( source: source , external: external , size: size , digest: digest , modules: modules ) <-
308
315
all_sources ,
309
316
{ last_mtime , last_size } = Map . fetch! ( sources_stats , source ) ,
310
- Enum . any? ( modules , & Map . has_key? ( modules_to_recompile , & 1 ) ) or
317
+ # If the user does a change, compilation fails, and then they revert
318
+ # the change, the mtime will have changed but the .beam files will
319
+ # be missing and the digest is the same, so we need to check if .beam
320
+ # files are available.
321
+ size != last_size or
322
+ Enum . any? ( modules , & Map . has_key? ( modules_to_recompile , & 1 ) ) or
311
323
Enum . any? ( external , & stale_external? ( & 1 , modified , sources_stats ) ) or
312
- ( size != last_size or
313
- ( last_mtime > modified and
314
- ( missing_beam_file? ( dest , modules ) or
315
- digest != digest ( source ) ) ) ) ,
324
+ ( last_mtime > modified and
325
+ ( missing_beam_file? ( dest , modules ) or digest != digest ( source ) ) ) ,
316
326
do: source
317
327
318
328
changed = new_paths ++ changed
@@ -436,11 +446,12 @@ defmodule Mix.Compilers.Elixir do
436
446
end
437
447
438
448
defp dependent_runtime_modules ( sources , all_modules , pending_modules ) do
449
+ # TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
439
450
changed_modules =
440
451
for module ( module: module ) = entry <- all_modules ,
441
452
entry not in pending_modules ,
442
453
into: % { } ,
443
- do: { module , true }
454
+ do: { module , [ ] }
444
455
445
456
fixpoint_runtime_modules ( sources , changed_modules , % { } , pending_modules )
446
457
end
@@ -649,7 +660,8 @@ defmodule Mix.Compilers.Elixir do
649
660
end
650
661
651
662
defp update_stale_entries ( modules , sources , changed , stale_mods , stale_exports , compile_path ) do
652
- changed = Enum . into ( changed , % { } , & { & 1 , true } )
663
+ # TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
664
+ changed = Enum . into ( changed , % { } , & { & 1 , [ ] } )
653
665
reducer = & remove_stale_entry ( & 1 , & 2 , sources , stale_exports , compile_path )
654
666
remove_stale_entries ( modules , % { } , changed , stale_mods , reducer )
655
667
end
@@ -709,7 +721,7 @@ defmodule Mix.Compilers.Elixir do
709
721
base = Path . basename ( manifest )
710
722
711
723
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
712
- stale_modules = for module <- stale_modules , do: { module , true } , into: % { }
724
+ stale_modules = for module <- stale_modules , do: { module , [ ] } , into: % { }
713
725
714
726
for % { scm: scm , opts: opts } = dep <- Mix.Dep . cached ( ) ,
715
727
not scm . fetchable? ,
@@ -721,7 +733,7 @@ defmodule Mix.Compilers.Elixir do
721
733
{ modules , exports , new_exports } ->
722
734
module = beam |> Path . basename ( ) |> Path . rootname ( ) |> String . to_atom ( )
723
735
export = exports_md5 ( module , false )
724
- modules = Map . put ( modules , module , true )
736
+ modules = Map . put ( modules , module , [ ] )
725
737
726
738
# If the exports are the same, then the API did not change,
727
739
# so we do not mark the export as stale. Note this has to
@@ -731,7 +743,7 @@ defmodule Mix.Compilers.Elixir do
731
743
exports =
732
744
if export && old_exports [ module ] == export ,
733
745
do: exports ,
734
- else: Map . put ( exports , module , true )
746
+ else: Map . put ( exports , module , [ ] )
735
747
736
748
# In any case, we always store it as the most update export
737
749
# that we have, otherwise we delete it.
@@ -834,7 +846,7 @@ defmodule Mix.Compilers.Elixir do
834
846
835
847
defp deps_on ( apps ) do
836
848
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
837
- apps = for app <- apps , do: { app , true } , into: % { }
849
+ apps = for app <- apps , do: { app , [ ] } , into: % { }
838
850
deps_on ( Mix.Dep . cached ( ) , apps , [ ] , false )
839
851
end
840
852
@@ -846,7 +858,7 @@ defmodule Mix.Compilers.Elixir do
846
858
847
859
# It depends on one of the apps, store it
848
860
Enum . any? ( deps , & Map . has_key? ( apps , & 1 . app ) ) ->
849
- deps_on ( cached_deps , Map . put ( apps , app , true ) , acc , true )
861
+ deps_on ( cached_deps , Map . put ( apps , app , [ ] ) , acc , true )
850
862
851
863
# Otherwise we will check it later
852
864
true ->
@@ -936,6 +948,7 @@ defmodule Mix.Compilers.Elixir do
936
948
manifest_data = :erlang . term_to_binary ( term , [ :compressed ] )
937
949
File . write! ( manifest , manifest_data )
938
950
File . touch! ( manifest , timestamp )
951
+ delete_checkpoint ( manifest )
939
952
940
953
# Since Elixir is a dependency itself, we need to touch the lock
941
954
# so the current Elixir version, used to compile the files above,
@@ -946,4 +959,41 @@ defmodule Mix.Compilers.Elixir do
946
959
defp beam_path ( compile_path , module ) do
947
960
Path . join ( compile_path , Atom . to_string ( module ) <> ".beam" )
948
961
end
962
+
963
+ # Once we added semantic recompilation, the following can happen:
964
+ #
965
+ # 1. The user changes config/mix.exs/__mix_recompile__?
966
+ # 2. We detect the change, remove .beam files and start recompilation
967
+ # 3. Recompilation fails
968
+ # 4. The user reverts the change
969
+ # 5. The compiler no longer recompiles and the .beam files are missing
970
+ #
971
+ # Therefore, it is important for us to checkpoint any state that may
972
+ # have lead to a compilation and which can now be reverted.
973
+
974
+ defp parse_checkpoint ( manifest ) do
975
+ try do
976
+ ( manifest <> ".checkpoint" ) |> File . read! ( ) |> :erlang . binary_to_term ( )
977
+ rescue
978
+ _ ->
979
+ { % { } , % { } }
980
+ else
981
+ { @ manifest_vsn , stale , recompile_modules } ->
982
+ { stale , recompile_modules }
983
+
984
+ _ ->
985
+ { % { } , % { } }
986
+ end
987
+ end
988
+
989
+ defp write_checkpoint ( manifest , stale , recompile_modules ) do
990
+ File . mkdir_p! ( Path . dirname ( manifest ) )
991
+ term = { @ manifest_vsn , stale , recompile_modules }
992
+ checkpoint_data = :erlang . term_to_binary ( term , [ :compressed ] )
993
+ File . write! ( manifest <> ".checkpoint" , checkpoint_data )
994
+ end
995
+
996
+ defp delete_checkpoint ( manifest ) do
997
+ File . rm ( manifest <> ".checkpoint" )
998
+ end
949
999
end
0 commit comments