Skip to content

Commit e4a9fae

Browse files
author
José Valim
committed
Remove dependency on manifest files
1 parent 0030b28 commit e4a9fae

File tree

9 files changed

+421
-391
lines changed

9 files changed

+421
-391
lines changed

lib/mix/lib/mix/compilers/elixir.ex

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
defmodule Mix.Compilers.Elixir do
2+
@moduledoc false
3+
4+
@doc """
5+
Compiles stale Elixir files.
6+
7+
It expects a manifest file, the source directories, the extensions
8+
to read in sources, the destination directory, a flag to know if
9+
compilation is being forced or not and a callback to be invoked
10+
once (and only if) compilation starts.
11+
12+
The manifest is written down with information including dependencies
13+
in between modules, which helps it recompile only the modules that
14+
have changed at runtime.
15+
"""
16+
def compile(manifest, srcs, exts, dest, force, on_start) do
17+
all = Mix.Utils.extract_files(srcs, exts)
18+
all_entries = read_manifest(manifest)
19+
20+
removed =
21+
for {_b, _m, source, _d, _f} <- all_entries, not(source in all), do: source
22+
23+
changed =
24+
if force do
25+
# A config, path dependency or manifest has
26+
# changed, let's just compile everything
27+
all
28+
else
29+
modified = Mix.Utils.last_modified(manifest)
30+
31+
# Otherwise let's start with the new ones
32+
# plus the ones that have changed
33+
for(source <- all,
34+
not Enum.any?(all_entries, fn {_b, _m, s, _d, _f} -> s == source end),
35+
do: source)
36+
++
37+
for({_b, _m, source, _d, files} <- all_entries,
38+
Mix.Utils.stale?([source|files], [modified]),
39+
do: source)
40+
end
41+
42+
{entries, changed} = remove_stale_entries(all_entries, removed ++ changed, [], [])
43+
stale = changed -- removed
44+
45+
cond do
46+
stale != [] ->
47+
do_compile(manifest, entries, stale, dest, on_start)
48+
:ok
49+
removed != [] ->
50+
:ok
51+
true ->
52+
:noop
53+
end
54+
end
55+
56+
@doc """
57+
Removes compiled files.
58+
"""
59+
def clean(manifest) do
60+
case File.read(manifest) do
61+
{:ok, contents} ->
62+
contents
63+
|> String.split("\n")
64+
|> Enum.each(& &1 |> String.split("\t") |> hd |> File.rm)
65+
File.rm(manifest)
66+
{:error, _} ->
67+
:ok
68+
end
69+
end
70+
71+
defp do_compile(manifest, entries, stale, dest, on_start) do
72+
Mix.Project.build_structure
73+
on_start.()
74+
cwd = File.cwd!
75+
76+
# Starts a server responsible for keeping track which files
77+
# were compiled and the dependencies in between them.
78+
{:ok, pid} = Agent.start_link(fn ->
79+
Enum.map(entries, &Tuple.insert_at(&1, 5, nil))
80+
end)
81+
82+
try do
83+
Kernel.ParallelCompiler.files :lists.usort(stale),
84+
each_module: &each_module(pid, dest, cwd, &1, &2, &3),
85+
each_file: &each_file(&1)
86+
Agent.cast pid, fn entries ->
87+
write_manifest(manifest, entries)
88+
entries
89+
end
90+
after
91+
Agent.stop pid
92+
end
93+
94+
:ok
95+
end
96+
97+
defp each_module(pid, dest, cwd, source, module, binary) do
98+
source = Path.relative_to(source, cwd)
99+
bin = Atom.to_string(module)
100+
beam = dest
101+
|> Path.join(bin <> ".beam")
102+
|> Path.relative_to(cwd)
103+
104+
deps = Kernel.LexicalTracker.remotes(module)
105+
|> List.delete(module)
106+
|> :lists.usort
107+
|> Enum.map(&Atom.to_string(&1))
108+
|> Enum.reject(&match?("elixir_" <> _, &1))
109+
110+
files = get_beam_files(binary, cwd)
111+
|> List.delete(source)
112+
|> Enum.filter(&(Path.type(&1) == :relative))
113+
114+
Agent.cast pid, &:lists.keystore(beam, 1, &1, {beam, bin, source, deps, files, binary})
115+
end
116+
117+
defp get_beam_files(binary, cwd) do
118+
case :beam_lib.chunks(binary, [:abstract_code]) do
119+
{:ok, {_, [abstract_code: {:raw_abstract_v1, code}]}} ->
120+
for {:attribute, _, :file, {file, _}} <- code,
121+
File.exists?(file) do
122+
Path.relative_to(file, cwd)
123+
end
124+
_ ->
125+
[]
126+
end
127+
end
128+
129+
defp each_file(file) do
130+
Mix.shell.info "Compiled #{file}"
131+
end
132+
133+
## Resolution
134+
135+
# This function receives the manifest entries and some source
136+
# files that have changed. It then, recursively, figures out
137+
# all the files that changed (thanks to the dependencies) and
138+
# return their sources as the remaining entries.
139+
defp remove_stale_entries(all, []) do
140+
{all, []}
141+
end
142+
143+
defp remove_stale_entries(all, changed) do
144+
remove_stale_entries(all, :lists.usort(changed), [], [])
145+
end
146+
147+
defp remove_stale_entries([{beam, module, source, _d, _f} = entry|t], changed, removed, acc) do
148+
if source in changed do
149+
File.rm(beam)
150+
remove_stale_entries(t, changed, [module|removed], acc)
151+
else
152+
remove_stale_entries(t, changed, removed, [entry|acc])
153+
end
154+
end
155+
156+
defp remove_stale_entries([], changed, removed, acc) do
157+
# If any of the dependencies for the remaining entries
158+
# were removed, get its source so we can remove them.
159+
next_changed = for {_b, _m, source, deps, _f} <- acc,
160+
Enum.any?(deps, &(&1 in removed)),
161+
do: source
162+
163+
{acc, next} = remove_stale_entries(Enum.reverse(acc), next_changed)
164+
{acc, next ++ changed}
165+
end
166+
167+
## Manifest handling
168+
169+
# Reads the manifest returning the results as tuples.
170+
# The beam files are read, removed and stored in memory.
171+
defp read_manifest(manifest) do
172+
case File.read(manifest) do
173+
{:ok, contents} ->
174+
Enum.reduce String.split(contents, "\n"), [], fn x, acc ->
175+
case String.split(x, "\t") do
176+
[beam, module, source|deps] ->
177+
{deps, files} =
178+
case Enum.split_while(deps, &(&1 != "Elixir")) do
179+
{deps, ["Elixir"|files]} -> {deps, files}
180+
{deps, _} -> {deps, []}
181+
end
182+
[{beam, module, source, deps, files}|acc]
183+
_ ->
184+
acc
185+
end
186+
end
187+
{:error, _} ->
188+
[]
189+
end
190+
end
191+
192+
# Writes the manifest separating entries by tabs.
193+
defp write_manifest(_manifest, []) do
194+
:ok
195+
end
196+
197+
defp write_manifest(manifest, entries) do
198+
lines = Enum.map(entries, fn
199+
{beam, module, source, deps, files, binary} ->
200+
if binary, do: File.write!(beam, binary)
201+
tail = deps ++ ["Elixir"] ++ files
202+
[beam, module, source | tail] |> Enum.join("\t")
203+
end)
204+
205+
File.mkdir_p!(Path.dirname(manifest))
206+
File.write!(manifest, Enum.join(lines, "\n"))
207+
end
208+
end

lib/mix/lib/mix/compilers/erlang.ex

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
defmodule Mix.Compilers.Erlang do
2+
@moduledoc false
3+
4+
@doc """
5+
Compiles the files in `src_dirs` with given extensions into
6+
the destination, automatically invoking the callback for each
7+
stale input and output pair (or for all if `force` is true) and
8+
removing files that no longer have a source, while keeping the
9+
manifest up to date.
10+
11+
## Examples
12+
13+
For example, a simple compiler for Lisp Flavored Erlang
14+
would be implemented like:
15+
16+
manifest = Path.join Mix.Project.manifest_path, ".compile.lfe"
17+
dest = Mix.Project.compile_path
18+
19+
compile manifest, [{"src", dest}], :lfe, :beam, opts[:force], fn
20+
input, output ->
21+
:lfe_comp.file(to_erl_file(input),
22+
[output_dir: Path.dirname(output)])
23+
end
24+
25+
The command above will:
26+
27+
1. Look for files ending with the `lfe` extension in `src`
28+
and their `beam` counterpart in `ebin`;
29+
2. For each stale file (or for all if `force` is true),
30+
invoke the callback passing the calculated input
31+
and output;
32+
3. Update the manifest with the newly compiled outputs;
33+
4. Remove any output in the manifest that that does not
34+
have an equivalent source;
35+
36+
The callback must return `{:ok, mod}` or `:error` in case
37+
of error. An error is raised at the end if any of the
38+
files failed to compile.
39+
"""
40+
def compile(manifest, mappings, src_ext, dest_ext, force, callback) do
41+
files = for {src, dest} <- mappings do
42+
extract_targets(src, src_ext, dest, dest_ext, force)
43+
end |> Enum.concat
44+
compile(manifest, files, callback)
45+
end
46+
47+
@doc """
48+
Compiles the given src/dest tuples.
49+
50+
A manifest file and a callback to be invoked for each src/dest pair
51+
must be given. A src/dest pair where destination is nil is considered
52+
to be up to date and won't be (re-)compiled.
53+
"""
54+
def compile(manifest, tuples, callback) do
55+
stale = for {:stale, src, dest} <- tuples, do: {src, dest}
56+
57+
# Get the previous entries from the manifest
58+
entries = read_manifest(manifest)
59+
60+
# Files to remove are the ones in the manifest
61+
# but they no longer have a source
62+
removed = Enum.filter(entries, fn entry ->
63+
not Enum.any?(tuples, fn {_status, _src, dest} -> dest == entry end)
64+
end)
65+
66+
if stale == [] && removed == [] do
67+
:noop
68+
else
69+
# Build the project structure so we can write down compiled files.
70+
Mix.Project.build_structure
71+
72+
# Remove manifest entries with no source
73+
Enum.each(removed, &File.rm/1)
74+
75+
# Compile stale files and print the results
76+
results = for {input, output} <- stale do
77+
interpret_result(input, callback.(input, output))
78+
end
79+
80+
# Write final entries to manifest
81+
entries = (entries -- removed) ++ Enum.map(stale, &elem(&1, 1))
82+
write_manifest(manifest, :lists.usort(entries))
83+
84+
# Raise if any error, return :ok otherwise
85+
if :error in results, do: raise CompileError
86+
:ok
87+
end
88+
end
89+
90+
@doc """
91+
Removes compiled files.
92+
"""
93+
def clean(manifest) do
94+
Enum.each read_manifest(manifest), &File.rm/1
95+
File.rm manifest
96+
end
97+
98+
@doc """
99+
Converts the given file to a format accepted by
100+
the Erlang compilation tools.
101+
"""
102+
def to_erl_file(file) do
103+
to_char_list(file)
104+
end
105+
106+
defp extract_targets(src_dir, src_ext, dest_dir, dest_ext, force) do
107+
files = Mix.Utils.extract_files(List.wrap(src_dir), List.wrap(src_ext))
108+
109+
for file <- files do
110+
module = module_from_artifact(file)
111+
target = Path.join(dest_dir, module <> "." <> to_string(dest_ext))
112+
113+
if force || Mix.Utils.stale?([file], [target]) do
114+
{:stale, file, target}
115+
else
116+
{:ok, file, target}
117+
end
118+
end
119+
end
120+
121+
defp module_from_artifact(artifact) do
122+
artifact |> Path.basename |> Path.rootname
123+
end
124+
125+
defp interpret_result(file, result) do
126+
case result do
127+
{:ok, _} -> Mix.shell.info "Compiled #{file}"
128+
:error -> :error
129+
end
130+
result
131+
end
132+
133+
defp read_manifest(file) do
134+
case File.read(file) do
135+
{:ok, contents} -> String.split(contents, "\n")
136+
{:error, _} -> []
137+
end
138+
end
139+
140+
defp write_manifest(file, entries) do
141+
Path.dirname(file) |> File.mkdir_p!
142+
File.write!(file, Enum.join(entries, "\n"))
143+
end
144+
end

lib/mix/lib/mix/tasks/clean.ex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ defmodule Mix.Tasks.Clean do
2020
def run(args) do
2121
{opts, _, _} = OptionParser.parse(args)
2222

23-
manifests = Mix.Tasks.Compile.manifests
24-
Enum.each(manifests, fn(manifest) ->
25-
Enum.each Mix.Utils.read_manifest(manifest),
26-
&(&1 |> String.split("\t") |> hd |> File.rm)
27-
File.rm(manifest)
28-
end)
23+
for compiler <- Mix.Tasks.Compile.compilers() do
24+
module = Mix.Task.get!("compile.#{compiler}")
25+
if function_exported?(module, :clean, 0) do
26+
module.clean
27+
end
28+
end
2929

3030
if opts[:all] do
3131
Mix.Task.run("deps.clean", args)

0 commit comments

Comments
 (0)