1defmodule Mix.Tasks.Compile.All do 2 use Mix.Task.Compiler 3 4 @moduledoc false 5 @compile {:no_warn_undefined, Logger} 6 @recursive true 7 8 # This is an internal task used by "mix compile" which 9 # is meant to be recursive and be invoked for each child 10 # project. 11 12 @impl true 13 def run(args) do 14 Mix.Project.get!() 15 config = Mix.Project.config() 16 validate_compile_env? = "--no-validate-compile-env" not in args 17 lib_path = Path.join(Mix.Project.build_path(config), "lib") 18 19 # Make sure Mix.Dep is cached to avoid loading dependencies 20 # during compilation. It is likely this will be invoked anyway, 21 # as both Elixir and app compilers rely on it. 22 Mix.Dep.cached() 23 24 unless "--no-app-loading" in args do 25 load_apps(config, lib_path, validate_compile_env?) 26 end 27 28 result = 29 if "--no-compile" in args do 30 Mix.Task.reenable("compile.all") 31 {:noop, []} 32 else 33 # Build the project structure so we can write down compiled files. 34 Mix.Project.build_structure(config) 35 36 with_logger_app(config, fn -> 37 config 38 |> Mix.Tasks.Compile.compilers() 39 |> compile(args, :noop, []) 40 end) 41 end 42 43 app = config[:app] 44 _ = Code.prepend_path(Mix.Project.compile_path()) 45 load_app(app, lib_path, validate_compile_env?) 46 result 47 end 48 49 defp with_logger_app(config, fun) do 50 app = Keyword.fetch!(config, :app) 51 logger? = Process.whereis(Logger) 52 logger_config_app = Application.get_env(:logger, :compile_time_application) 53 54 try do 55 if logger? do 56 Logger.configure(compile_time_application: app) 57 end 58 59 fun.() 60 after 61 if logger? do 62 Logger.configure(compile_time_application: logger_config_app) 63 end 64 end 65 end 66 67 defp compile([], _, status, diagnostics) do 68 {status, diagnostics} 69 end 70 71 defp compile([compiler | rest], args, status, diagnostics) do 72 {new_status, new_diagnostics} = run_compiler(compiler, args) 73 diagnostics = diagnostics ++ new_diagnostics 74 75 case new_status do 76 :error -> 77 if "--return-errors" not in args do 78 exit({:shutdown, 1}) 79 end 80 81 {:error, diagnostics} 82 83 :ok -> 84 compile(rest, args, :ok, diagnostics) 85 86 :noop -> 87 compile(rest, args, status, diagnostics) 88 end 89 end 90 91 defp run_compiler(compiler, args) do 92 result = Mix.Task.Compiler.normalize(Mix.Task.run("compile.#{compiler}", args), compiler) 93 Enum.reduce(Mix.ProjectStack.pop_after_compiler(compiler), result, & &1.(&2)) 94 end 95 96 ## App loading helpers 97 98 defp load_apps(config, lib_path, validate_compile_env?) do 99 {runtime, optional} = Mix.Tasks.Compile.App.project_apps(config) 100 parent = self() 101 opts = [ordered: false, timeout: :infinity] 102 deps = for dep <- Mix.Dep.cached(), into: %{}, do: {dep.app, lib_path} 103 104 stream_apps(runtime ++ optional, deps) 105 |> Task.async_stream(&load_stream_app(&1, parent, validate_compile_env?), opts) 106 |> Stream.run() 107 end 108 109 defp load_stream_app({app, lib_path}, parent, validate_compile_env?) do 110 children = 111 case load_app(app, lib_path, validate_compile_env?) do 112 :ok -> 113 Application.spec(app, :applications) ++ Application.spec(app, :included_applications) 114 115 :error -> 116 [] 117 end 118 119 send(parent, {:done, app, children}) 120 :ok 121 end 122 123 defp stream_apps(initial, deps) do 124 Stream.unfold({initial, %{}, %{}, deps}, &stream_app/1) 125 end 126 127 # We already processed this app, skip it. 128 defp stream_app({[app | apps], seen, done, deps}) when is_map_key(seen, app) do 129 stream_app({apps, seen, done, deps}) 130 end 131 132 # We haven't processed this app, emit it. 133 defp stream_app({[app | apps], seen, done, deps}) do 134 {{app, deps[app]}, {apps, Map.put(seen, app, true), done, deps}} 135 end 136 137 # We have processed all apps and all seen have been done. 138 defp stream_app({[], seen, done, _deps}) when map_size(seen) == map_size(done) do 139 nil 140 end 141 142 # We have processed all apps but there is work being done. 143 defp stream_app({[], seen, done, deps}) do 144 receive do 145 {:done, app, children} -> stream_app({children, seen, Map.put(done, app, true), deps}) 146 end 147 end 148 149 defp load_app(app, lib_path, validate_compile_env?) do 150 if Application.spec(app, :vsn) do 151 :ok 152 else 153 with {:ok, bin} <- read_app(app, lib_path), 154 {:ok, {:application, _, properties} = application_data} <- consult_app_file(bin), 155 :ok <- :application.load(application_data) do 156 if compile_env = validate_compile_env? && properties[:compile_env] do 157 Config.Provider.validate_compile_env(compile_env, false) 158 end 159 160 :ok 161 else 162 _ -> :error 163 end 164 end 165 end 166 167 # The app didn't come from a dep, go through the slow path (code/erl_prim_loader) 168 defp read_app(app, nil) do 169 name = Atom.to_charlist(app) ++ '.app' 170 171 with [_ | _] = path <- :code.where_is_file(name), 172 {:ok, bin, _full_name} <- :erl_prim_loader.get_file(path), 173 do: {:ok, bin} 174 end 175 176 defp read_app(app, lib_path) do 177 File.read("#{lib_path}/#{app}/ebin/#{app}.app") 178 end 179 180 defp consult_app_file(bin) do 181 # The path could be located in an .ez archive, so we use the prim loader. 182 with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(bin)) do 183 :erl_parse.parse_term(tokens) 184 end 185 end 186end 187