|
| 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 |
0 commit comments