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