diff --git a/lib/mix/lib/mix/tasks/clean.ex b/lib/mix/lib/mix/tasks/clean.ex index eb634e9669..d1f2900385 100644 --- a/lib/mix/lib/mix/tasks/clean.ex +++ b/lib/mix/lib/mix/tasks/clean.ex @@ -19,7 +19,8 @@ defmodule Mix.Tasks.Clean do manifests = Mix.Tasks.Compile.manifests Enum.each(manifests, fn(manifest) -> - Mix.Utils.read_manifest(manifest) |> Enum.each(File.rm(&1)) + Enum.each Mix.Utils.read_manifest(manifest), + &1 |> String.split("\t") |> hd |> File.rm File.rm(manifest) end) diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 344ee52f87..9dc7ec4dd5 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -110,6 +110,7 @@ defmodule Mix.Tasks.Compile.Elixir do Path.join(Mix.project[:compile_path], @manifest) end + defp compile_files(true, project, compile_path, to_compile, stale, opts) do set_compiler_opts(project, opts, ignore_module_conflict: true) to_compile = lc f inlist stale, f in to_compile, do: f @@ -117,16 +118,151 @@ defmodule Mix.Tasks.Compile.Elixir do File.touch! Path.join(compile_path, @manifest) end - defp compile_files(false, project, compile_path, to_compile, _stale, opts) do - manifest = Path.join(compile_path, @manifest) + defmodule ManifestCompiler do + @moduledoc false + use GenServer.Behaviour - Mix.Utils.watch_manifest(manifest, compile_path, fn -> - set_compiler_opts(project, opts, []) - compiled = compile_files to_compile, compile_path - lc { mod, _ } inlist compiled do - Path.join(compile_path, atom_to_binary(mod) <> ".beam") + def files_to_path(manifest, stale, all, compile_path) do + entries = read_manifest(manifest) + + # Each entry that is not in all was removed and must be pruned + entries = prune_entries(entries, fn(x) -> not(x in all) end) + + # Filter stale to be a subset of all + stale = lc i inlist stale, i in all, do: i + + # Each entry in all that's not in the manifest is also stale + stale = stale ++ lc i inlist all, + not Enum.any?(entries, fn { _b, _m, s, _d } -> s == i end), + do: i + + { :ok, pid } = :gen_server.start_link(__MODULE__, entries, []) + + try do + do_files_to_path(pid, stale, compile_path, File.cwd!) + :gen_server.cast(pid, :merge) + after + case :gen_server.call(pid, :stop) do + { :ok, entries } -> write_manifest(manifest, entries) + { :error, beams } -> Enum.each(beams, File.rm(&1)) + end end - end) + end + + defp do_files_to_path(_pid, [], _compile_path, _cwd), do: :ok + defp do_files_to_path(pid, files, compile_path, cwd) do + Kernel.ParallelCompiler.files_to_path :lists.usort(files), compile_path, + each_module: each_module(pid, compile_path, cwd, &1, &2, &3), + each_file: each_file(&1) + + do_files_to_path(pid, :gen_server.call(pid, :next), compile_path, cwd) + end + + defp each_module(pid, compile_path, cwd, source, module, _binary) do + bin = atom_to_binary(module) + beam = Path.join(compile_path, bin <> ".beam") + + deps = Module.DispatchTracker.remotes(module) ++ + Module.DispatchTracker.imports(module) + deps = deps |> :lists.usort |> Enum.map(atom_to_binary(&1)) + + :gen_server.cast(pid, { :store, beam, bin, Path.relative_to(source, cwd), deps }) + end + + defp each_file(file) do + Mix.shell.info "Compiled #{file}" + end + + # Reads the manifest returning the results as tuples. + defp read_manifest(manifest) do + Enum.reduce Mix.Utils.read_manifest(manifest), [], fn x, acc -> + case String.split(x, "\t") do + [beam, module, source | deps] -> [{ beam, module, source, deps }|acc] + _ -> acc + end + end + end + + # Writes the manifest separating entries by tabs. + defp write_manifest(manifest, entries) do + lines = Enum.map(entries, fn + { beam, module, source, deps } -> + [beam, module, source | deps] |> Enum.join("\t") + end) + + Mix.Utils.write_manifest(manifest, lines) + end + + # If the source is no longer available OR the source is stale, + # we remove its artifacts and prune the entry from the list. + defp prune_entries([{ beam, _, source, _ } = entry|entries], callback) do + if callback.(source) do + File.rm(beam) + prune_entries(entries, callback) + else + [entry|prune_entries(entries, callback)] + end + end + + defp prune_entries([], _callback), do: [] + + # Callbacks + + def init(old) do + { :ok, { old, [] } } + end + + def handle_call(:stop, _from, { old, [] }) do + modules = lc { _b, module, _s, _d } inlist old, do: module + + filtered = lc { b, m, s, deps } inlist old do + { b, m, s, Enum.filter(deps, &1 in modules) } + end + + { :stop, :normal, { :ok, filtered }, { filtered, [] } } + end + + def handle_call(:stop, _from, { _old, new } = state) do + beams = lc { beam, _m, _s, _d } inlist new, do: beam + { :stop, :normal, { :error, beams }, state } + end + + def handle_call(:next, _from, { old, new }) do + modules = lc { _b, module, _s, _d } inlist new, do: module + + # For each previous entry in the manifest that + # had its dependency changed and it was not yet + # compiled, get its source as next + next = lc { beam, module, source, deps } inlist old, + Enum.any?(modules, &1 in deps), + not(module in modules), + do: File.rm(beam) && source + + { :reply, next, { old, new } } + end + + def handle_call(msg, from, state) do + super(msg, from, state) + end + + def handle_cast(:merge, { old, new }) do + old = :lists.keymerge(1, :lists.sort(new), :lists.sort(old)) + { :noreply, { old, [] } } + end + + def handle_cast({ :store, beam, module, source, deps }, { old, new }) do + { :noreply, { old, [{ beam, module, source, deps }|new] } } + end + + def handle_cast(msg, state) do + super(msg ,state) + end + end + + defp compile_files(false, project, compile_path, to_compile, stale, opts) do + manifest = Path.join(compile_path, @manifest) + set_compiler_opts(project, opts, []) + ManifestCompiler.files_to_path(manifest, stale, to_compile, compile_path) end defp set_compiler_opts(project, opts, extra) do @@ -136,10 +272,10 @@ defmodule Mix.Tasks.Compile.Elixir do end defp compile_files(files, to) do - Kernel.ParallelCompiler.files_to_path files, to, each_file: fn(x) -> - Mix.shell.info "Compiled #{x}" - x - end + Kernel.ParallelCompiler.files_to_path files, to, + each_file: fn(file) -> + Mix.shell.info "Compiled #{file}" + end end defp path_deps_changed?(manifest) do diff --git a/lib/mix/lib/mix/tasks/compile.erlang.ex b/lib/mix/lib/mix/tasks/compile.erlang.ex index eefb2a73f4..f3bcedff0a 100644 --- a/lib/mix/lib/mix/tasks/compile.erlang.ex +++ b/lib/mix/lib/mix/tasks/compile.erlang.ex @@ -179,7 +179,7 @@ defmodule Mix.Tasks.Compile.Erlang do Path.join(compile_path, "#{mod}.beam") end - Mix.Utils.update_manifest(manifest_path, compiled) + Mix.Utils.write_manifest(manifest_path, compiled) if Enum.any?(results, &1 == :error), do: raise CompileError end diff --git a/lib/mix/lib/mix/tasks/compile.leex.ex b/lib/mix/lib/mix/tasks/compile.leex.ex index ef22f02e51..d0c6edd182 100644 --- a/lib/mix/lib/mix/tasks/compile.leex.ex +++ b/lib/mix/lib/mix/tasks/compile.leex.ex @@ -68,8 +68,8 @@ defmodule Mix.Tasks.Compile.Leex do end outputs = Enum.map(files, elem(&1, 1)) - Mix.Utils.update_manifest(manifest_path, outputs) + Mix.Utils.write_manifest(manifest_path, outputs) if Enum.any?(results, &1 == :error), do: raise CompileError end end diff --git a/lib/mix/lib/mix/tasks/compile.yecc.ex b/lib/mix/lib/mix/tasks/compile.yecc.ex index 7c023ae27b..ea0e302f91 100644 --- a/lib/mix/lib/mix/tasks/compile.yecc.ex +++ b/lib/mix/lib/mix/tasks/compile.yecc.ex @@ -68,8 +68,8 @@ defmodule Mix.Tasks.Compile.Yecc do end outputs = Enum.map(files, elem(&1, 1)) - Mix.Utils.update_manifest(manifest_path, outputs) + Mix.Utils.write_manifest(manifest_path, outputs) if Enum.any?(results, &1 == :error), do: raise CompileError end end diff --git a/lib/mix/lib/mix/utils.ex b/lib/mix/lib/mix/utils.ex index 2758744d8f..39cd63f3c6 100644 --- a/lib/mix/lib/mix/utils.ex +++ b/lib/mix/lib/mix/utils.ex @@ -127,41 +127,11 @@ defmodule Mix.Utils do function is compared to the manifest in order do detect the files removed from the manifest file. """ - def update_manifest(file, new) do + def write_manifest(file, new) do Path.dirname(file) |> File.mkdir_p! File.write!(file, Enum.join(new, "\n")) end - @doc """ - Automatically maintains the manifest in three steps: - - 1. Deletes previous entries - 2. Stores existing entries in the directory - 3. Executes the given function - 4. Updates the manifest file with the function results - - In case an exception is raised while executing the function, - we will delete any new entries created after step 2. - """ - def watch_manifest(manifest, directory, function) do - read_manifest(manifest) |> Enum.each(File.rm(&1)) - current = File.ls!(directory) - - try do - function.() - rescue - e -> - stacktrace = System.stacktrace - Enum.each(File.ls!(directory) -- current, fn(x) -> - File.rm(Path.join(directory, x)) - end) - raise e, [], stacktrace - else - res -> - update_manifest(manifest, res) - end - end - @doc """ Extract files from a list of paths.