@@ -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 , [ ] )
0 commit comments