/
compile.ex
245 lines (193 loc) · 7.7 KB
/
compile.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
defmodule Mix.Tasks.Compile do
use Mix.Task.Compiler
@shortdoc "Compiles source files"
@moduledoc """
The main entry point to compile source files.
It simply runs the compilers registered in your project and returns
a tuple with the compilation status and a list of diagnostics.
Before compiling code, it performs a series of checks to ensure all
dependencies are compiled and the project is up to date. Then the
code path of your Elixir system is pruned to only contain the dependencies
and applications that you have explicitly listed in your `mix.exs`.
## Configuration
* `:build_embedded` - this option was used to copy all code and
priv content to the `_build` directory. However, this option no
longer has an effect as Elixir will now copy those at release time
* `:compilers` - compilers to run, defaults to `Mix.compilers/0`,
which are `[:yecc, :leex, :erlang, :elixir, :app]`.
* `:consolidate_protocols` - when `true`, runs protocol
consolidation via the `mix compile.protocols` task. The default
value is `true`.
* `:build_path` - the directory where build artifacts
should be written to. This option is intended only for
child apps within a larger umbrella application so that
each child app can use the common `_build` directory of
the parent umbrella. In a non-umbrella context, configuring
this has undesirable side-effects (such as skipping some
compiler checks) and should be avoided.
## Compilers
To see documentation for each specific compiler, you must
invoke `help` directly for the compiler command:
$ mix help compile.elixir
$ mix help compile.erlang
You can get a list of all compilers by running:
$ mix compile --list
## Command line options
* `--erl-config` - path to an Erlang term file that will be loaded as Mix config
* `--force` - forces compilation
* `--list` - lists all enabled compilers
* `--no-all-warnings` - prints only warnings from files currently compiled (instead of all)
* `--no-app-loading` - does not load .app resource file after compilation
* `--no-archives-check` - skips checking of archives
* `--no-compile` - does not actually compile, only loads code and perform checks
* `--no-deps-check` - skips checking of dependencies
* `--no-elixir-version-check` - does not check Elixir version
* `--no-optional-deps` - does not compile or load optional deps. Useful for testing
if a library still successfully compiles without optional dependencies (which is the
default case with dependencies)
* `--no-prune-code-paths` - do not prune code paths before compilation, this keeps
the entirety of Erlang/OTP available on the project starts
* `--no-protocol-consolidation` - skips protocol consolidation
* `--no-validate-compile-env` - does not validate the application compile environment
* `--return-errors` - returns error status and diagnostics instead of exiting on error
* `--warnings-as-errors` - exit with non-zero status if compilation has one or more warnings
"""
@doc """
Returns all compilers.
"""
def compilers(config \\ Mix.Project.config()) do
compilers = config[:compilers] || Mix.compilers()
if :xref in compilers do
IO.warn(
"the :xref compiler is deprecated, please remove it from your mix.exs :compilers options"
)
List.delete(compilers, :xref)
else
compilers
end
end
@impl true
def run(["--list"]) do
# Loadpaths without checks because compilers may be defined in deps.
args = ["--no-elixir-version-check", "--no-deps-check", "--no-archives-check"]
Mix.Task.run("loadpaths", args)
Mix.Task.reenable("loadpaths")
Mix.Task.reenable("deps.loadpaths")
# Compilers are tasks, so load all tasks available.
_ = Mix.Task.load_all()
shell = Mix.shell()
modules = Mix.Task.all_modules()
docs =
for module <- modules,
task = Mix.Task.task_name(module),
match?("compile." <> _, task),
doc = Mix.Task.moduledoc(module) do
{task, first_line(doc)}
end
max =
Enum.reduce(docs, 0, fn {task, _}, acc ->
max(byte_size(task), acc)
end)
sorted = Enum.sort(docs)
Enum.each(sorted, fn {task, doc} ->
shell.info(format(~c"mix ~-#{max}s # ~ts", [task, doc]))
end)
consolidate_protocols? = Mix.Project.config()[:consolidate_protocols]
compilers = compilers() ++ if(consolidate_protocols?, do: [:protocols], else: [])
shell.info("\nEnabled compilers: #{Enum.join(compilers, ", ")}")
:ok
end
@impl true
def run(args) do
Mix.Project.get!()
# We run loadpaths to perform checks but we don't bother setting
# up the load paths because compile.all will manage them anyway.
Mix.Task.run("loadpaths", ["--no-deps-loading" | args])
{opts, _, _} = OptionParser.parse(args, switches: [erl_config: :string])
load_erl_config(opts)
{res, diagnostics} =
Mix.Task.run("compile.all", args)
|> List.wrap()
|> Enum.map(&Mix.Task.Compiler.normalize(&1, :all))
|> Enum.reduce({:noop, []}, &merge_diagnostics/2)
config = Mix.Project.config()
# If we are in an umbrella project, now load paths from all children.
if apps_paths = Mix.Project.apps_paths(config) do
loaded_paths =
apps_paths
|> Map.keys()
|> Mix.AppLoader.load_apps(Mix.Dep.cached(), config, [], fn
{_app, path}, acc -> if path, do: [path | acc], else: acc
end)
# We don't cache umbrella paths as we may write to them
Code.prepend_paths(loaded_paths -- :code.get_path())
end
consolidate_protocols? =
config[:consolidate_protocols] and "--no-protocol-consolidation" not in args
res =
cond do
"--no-compile" in args ->
Mix.Task.reenable("compile")
:noop
consolidate_protocols? and reconsolidate_protocols?(res) ->
Mix.Task.run("compile.protocols", args)
:ok
true ->
res
end
with true <- consolidate_protocols?,
path = Mix.Project.consolidation_path(config),
{:ok, protocols} <- File.ls(path) do
# We don't cache consolidation path as we may write to it
Code.prepend_path(path)
Enum.each(protocols, &load_protocol/1)
end
{res, diagnostics}
end
defp format(expression, args) do
:io_lib.format(expression, args) |> IO.iodata_to_binary()
end
defp first_line(doc) do
String.split(doc, "\n", parts: 2) |> hd |> String.trim() |> String.trim_trailing(".")
end
defp merge_diagnostics({status1, diagnostics1}, {status2, diagnostics2}) do
new_status =
cond do
status1 == :error or status2 == :error -> :error
status1 == :ok or status2 == :ok -> :ok
true -> :noop
end
{new_status, diagnostics1 ++ diagnostics2}
end
defp load_erl_config(opts) do
if path = opts[:erl_config] do
{:ok, terms} = :file.consult(path)
Application.put_all_env(terms, persistent: true)
end
end
@impl true
def manifests do
Enum.flat_map(compilers(), fn compiler ->
module = Mix.Task.get("compile.#{compiler}")
if module && function_exported?(module, :manifests, 0) do
module.manifests
else
[]
end
end)
end
## Consolidation handling
defp reconsolidate_protocols?(:ok), do: true
defp reconsolidate_protocols?(:noop), do: not Mix.Tasks.Compile.Protocols.consolidated?()
defp reconsolidate_protocols?(:error), do: false
defp load_protocol(file) do
case file do
"Elixir." <> _ ->
module = file |> Path.rootname() |> String.to_atom()
:code.purge(module)
:code.delete(module)
_ ->
:ok
end
end
end