/
compile.elixir.ex
171 lines (138 loc) · 5.21 KB
/
compile.elixir.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
defmodule Mix.Tasks.Compile.Elixir do
use Mix.Task.Compiler
@recursive true
@manifest "compile.elixir"
@moduledoc """
Compiles Elixir source files.
Elixir is smart enough to recompile only files that have changed
and their dependencies. This means if `lib/a.ex` is invoking
a function defined over `lib/b.ex`, whenever `lib/b.ex` changes,
`lib/a.ex` is also recompiled.
Note it is important to recompile a file's dependencies as
there are often compile time dependencies between them.
## `__mix_recompile__?/0`
A module may export a `__mix_recompile__?/0` function that can
cause the module to be recompiled using custom rules. For example,
`@external_resource` already adds a compile-time dependency on an
external file, however to depend on a _dynamic_ list of files we
can do:
defmodule MyModule do
paths = Path.wildcard("*.txt")
@paths_hash :erlang.md5(paths)
for path <- paths do
@external_resource path
end
def __mix_recompile__?() do
Path.wildcard("*.txt") |> :erlang.md5() != @paths_hash
end
end
Compiler calls `__mix_recompile__?/0` for every module being
compiled (or previously compiled) and thus it is very important
to do there as little work as possible to not slow down the
compilation.
If module has `@compile {:autoload, false}`, `__mix_recompile__?/0` will
not be used.
## Command line options
* `--verbose` - prints each file being compiled
* `--force` - forces compilation regardless of modification times
* `--docs` (`--no-docs`) - attaches (or not) documentation to compiled modules
* `--debug-info` (`--no-debug-info`) - attaches (or not) debug info to compiled modules
* `--ignore-module-conflict` - does not emit warnings if a module was previously defined
* `--warnings-as-errors` - treats warnings in the current project as errors and
return a non-zero exit status
* `--long-compilation-threshold N` - sets the "long compilation" threshold
(in seconds) to `N` (see the docs for `Kernel.ParallelCompiler.compile/2`)
* `--profile` - if set to `time`, outputs timing information of compilation steps
* `--tracer` - adds a compiler tracer in addition to any specified in the `mix.exs` file
## Configuration
* `:elixirc_paths` - directories to find source files.
Defaults to `["lib"]`.
* `:elixirc_options` - compilation options that apply to Elixir's compiler.
See `Code.put_compiler_option/2` for a complete list of options. These
options are often overridable from the command line using the switches
above.
* `[xref: [exclude: ...]]` - a list of `module` or `{module, function, arity}`
that should not be warned on in case on undefined modules or undefined
application warnings.
"""
@switches [
force: :boolean,
docs: :boolean,
warnings_as_errors: :boolean,
ignore_module_conflict: :boolean,
debug_info: :boolean,
verbose: :boolean,
long_compilation_threshold: :integer,
profile: :string,
all_warnings: :boolean,
tracer: :keep
]
@impl true
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: @switches)
{tracers, opts} = pop_tracers(opts)
project = Mix.Project.config()
dest = Mix.Project.compile_path(project)
srcs = project[:elixirc_paths]
unless is_list(srcs) do
Mix.raise(":elixirc_paths should be a list of paths, got: #{inspect(srcs)}")
end
manifest = manifest()
base = xref_exclude_opts(project[:elixirc_options] || [], project)
cache_key = {base, srcs, "--no-optional-deps" in args}
opts =
base
|> Keyword.merge(opts)
|> tracers_opts(tracers)
|> profile_opts()
# The Elixir compiler relies on global state in the application tracer.
# However, even without it, having compilations racing with other is most
# likely undesired, so we wrap the compiler in a lock.
Mix.State.lock(__MODULE__, fn ->
Mix.Compilers.Elixir.compile(
manifest,
srcs,
dest,
cache_key,
Mix.Tasks.Compile.Erlang.manifests(),
Mix.Tasks.Compile.Erlang.modules(),
opts
)
end)
end
@impl true
def manifests, do: [manifest()]
defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest)
@impl true
def clean do
dest = Mix.Project.compile_path()
Mix.Compilers.Elixir.clean(manifest(), dest)
end
defp xref_exclude_opts(opts, project) do
exclude = List.wrap(project[:xref][:exclude])
if exclude == [] do
opts
else
Keyword.update(opts, :no_warn_undefined, exclude, &(List.wrap(&1) ++ exclude))
end
end
defp pop_tracers(opts) do
case Keyword.pop_values(opts, :tracer) do
{[], opts} ->
{[], opts}
{tracers, opts} ->
{Enum.map(tracers, &Module.concat([&1])), opts}
end
end
defp tracers_opts(opts, tracers) do
tracers = tracers ++ Code.get_compiler_option(:tracers)
Keyword.update(opts, :tracers, tracers, &(tracers ++ &1))
end
defp profile_opts(opts) do
case Keyword.fetch(opts, :profile) do
{:ok, "time"} -> Keyword.put(opts, :profile, :time)
{:ok, _} -> Keyword.delete(opts, :profile)
:error -> opts
end
end
end