diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 86c06613eef..30ad72c75df 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -122,6 +122,10 @@ defmodule Kernel.ParallelCompiler do ## Options + * `:after_compile` - invoked after all modules are compiled, but before + they are verified. If the files are being written to disk, such as in + `compile_to_path/3`, this will be invoked after the files are written + * `:each_file` - for each file compiled, invokes the callback passing the file @@ -258,13 +262,6 @@ defmodule Kernel.ParallelCompiler do {status, modules_or_errors, info} = try do spawn_workers(schedulers, cache, files, output, options) - else - {:ok, outcome, info} -> - beam_timestamp = Keyword.get(options, :beam_timestamp) - {:ok, write_module_binaries(outcome, output, beam_timestamp), info} - - {:error, errors, info} -> - {:error, errors, info} after Module.ParallelChecker.stop(cache) end @@ -288,7 +285,9 @@ defmodule Kernel.ParallelCompiler do {outcome, state} = spawn_workers(files, %{}, %{}, [], %{}, [], [], %{ + beam_timestamp: Keyword.get(options, :beam_timestamp), dest: Keyword.get(options, :dest), + after_compile: Keyword.get(options, :after_compile, fn -> :ok end), each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end), each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(), each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end), @@ -345,9 +344,11 @@ defmodule Kernel.ParallelCompiler do ## Verification defp verify_modules(result, compile_warnings, dependent_modules, state) do + modules = write_module_binaries(result, state.output, state.beam_timestamp) + _ = state.after_compile.() runtime_warnings = maybe_check_modules(result, dependent_modules, state) info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings} - {{:ok, result, info}, state} + {{:ok, modules, info}, state} end defp maybe_check_modules(result, runtime_modules, state) do diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index a06ccea05fc..0992ef74c2c 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -171,7 +171,6 @@ defmodule IEx.Helpers do defp reenable_tasks(config) do Mix.Task.reenable("compile") Mix.Task.reenable("compile.all") - Mix.Task.reenable("compile.protocols") compilers = config[:compilers] || Mix.compilers() Enum.each(compilers, &Mix.Task.reenable("compile.#{&1}")) end diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 97f018e6fce..5c7a6005b35 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -704,8 +704,7 @@ defmodule Mix do * `:verbose` - if `true`, prints additional debugging information (Default: `false`) - * `:consolidate_protocols` - if `true`, runs protocol - consolidation via the `mix compile.protocols` task (Default: `true`) + * `:consolidate_protocols` - if `true`, runs protocol consolidation (Default: `true`) * `:elixir` - if set, ensures the current Elixir version matches the given version requirement (Default: `nil`) @@ -1078,7 +1077,7 @@ defmodule Mix do app: @mix_install_app, erlc_paths: [], elixirc_paths: [], - compilers: [], + compilers: [:elixir], prune_code_paths: false ] ++ dynamic_config end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 629bc73cd04..e6634320687 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,7 +1,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 26 + @manifest_vsn 27 @checkpoint_vsn 2 import Record @@ -46,9 +46,8 @@ defmodule Mix.Compilers.Elixir do all_paths = Mix.Utils.extract_files(srcs, [:ex]) {all_modules, all_sources, all_local_exports, old_parents, old_cache_key, old_deps_config, - old_project_mtime, - old_config_mtime} = - parse_manifest(manifest, dest) + old_project_mtime, old_config_mtime, + old_protocols_and_impls} = parse_manifest(manifest, dest) # Prepend ourselves early because of __mix_recompile__? checks # and also that, in case of nothing compiled, we already need @@ -129,7 +128,7 @@ defmodule Mix.Compilers.Elixir do {false, stale, old_deps_config} end - {stale_modules, stale_exports, all_local_exports} = + {stale_modules, stale_exports, all_local_exports, protocols_and_impls} = stale_local_deps(local_deps, manifest, stale, modified, all_local_exports) prev_paths = Map.keys(all_sources) @@ -157,14 +156,25 @@ defmodule Mix.Compilers.Elixir do {sources, removed_modules} = update_stale_sources(sources, stale, removed_modules, sources_stats) - if stale != [] or stale_modules != %{} do + consolidation_status = + if Mix.Project.umbrella?() do + :off + else + Mix.Compilers.Protocol.status(config_mtime > old_config_mtime, opts) + end + + if stale != [] or stale_modules != %{} or removed != [] or deps_changed? or + consolidation_status == :force do path = opts[:purge_consolidation_path_if_stale] if is_binary(path) and Code.delete_path(path) do purge_modules_in_path(path) end - Mix.Utils.compiling_n(length(stale), :ex) + if stale != [] do + Mix.Utils.compiling_n(length(stale), :ex) + end + Mix.Project.ensure_structure() # We don't want to cache this path as we will write to it @@ -172,11 +182,13 @@ defmodule Mix.Compilers.Elixir do previous_opts = set_compiler_opts(opts) try do - state = {%{}, exports, sources, [], modules, removed_modules} + consolidation = {consolidation_status, old_protocols_and_impls, protocols_and_impls} + state = {%{}, exports, sources, [], modules, removed_modules, consolidation} compiler_loop(stale, stale_modules, dest, timestamp, opts, state) else {:ok, %{runtime_warnings: runtime_warnings, compile_warnings: compile_warnings}, state} -> - {modules, _exports, sources, _changed, pending_modules, _stale_exports} = state + {modules, _exports, sources, _changed, pending_modules, _stale_exports, + protocols_and_impls} = state previous_warnings = if Keyword.get(opts, :all_warnings, true), @@ -197,6 +209,7 @@ defmodule Mix.Compilers.Elixir do new_deps_config, project_mtime, config_mtime, + protocols_and_impls, timestamp ) @@ -217,7 +230,7 @@ defmodule Mix.Compilers.Elixir do else: {errors, r_warnings ++ c_warnings} # In case of errors, we show all previous warnings and all new ones. - {_, _, sources, _, _, _} = state + {_, _, sources, _, _, _, _} = state errors = Enum.map(errors, &diagnostic/1) warnings = Enum.map(warnings, &diagnostic/1) all_warnings = Keyword.get(opts, :all_warnings, errors == []) @@ -226,39 +239,9 @@ defmodule Mix.Compilers.Elixir do Code.compiler_options(previous_opts) end else - # We need to return ok if deps_changed? or stale_modules changed, - # even if no code was compiled, because we need to propagate the changed - # status to compile.protocols. This will be the case whenever: - # - # * the lock file or a config changes - # * any module in a path dependency changes - # * the mix.exs changes - # * the Erlang manifest updates (Erlang files are compiled) - # - # In the first case, we will consolidate from scratch. In the remaining, we - # will only compute the diff with current protocols. In fact, there is no - # need to reconsolidate if an Erlang file changes and it doesn't trigger - # any other change, but the diff check should be reasonably fast anyway. - status = if removed != [] or deps_changed? or stale_modules != %{}, do: :ok, else: :noop - - if status != :noop do - write_manifest( - manifest, - modules, - sources, - all_local_exports, - new_parents, - new_cache_key, - new_deps_config, - project_mtime, - config_mtime, - timestamp - ) - end - all_warnings = Keyword.get(opts, :all_warnings, true) previous_warnings = previous_warnings(sources, all_warnings) - unless_warnings_as_errors(opts, {status, previous_warnings}) + unless_warnings_as_errors(opts, {:noop, previous_warnings}) end end @@ -295,24 +278,6 @@ defmodule Mix.Compilers.Elixir do end) end - @doc """ - Returns protocols and implementations for the given `manifest`. - """ - def protocols_and_impls(paths) do - Enum.reduce(paths, {%{}, %{}}, fn path, acc -> - {modules, _} = read_manifest(Path.join(path, ".mix/compile.elixir")) - - Enum.reduce(modules, acc, fn - {module, module(kind: kind, timestamp: timestamp)}, {protocols, impls} -> - case kind do - :protocol -> {Map.put(protocols, module, timestamp), impls} - {:impl, protocol} -> {protocols, Map.put(impls, module, protocol)} - _ -> {protocols, impls} - end - end) - end) - end - @doc """ Reads the manifest for external consumption. """ @@ -322,7 +287,7 @@ defmodule Mix.Compilers.Elixir do rescue _ -> {[], []} else - {@manifest_vsn, modules, sources, _, _, _, _, _, _} -> {modules, sources} + {@manifest_vsn, modules, sources, _, _, _, _, _, _, _} -> {modules, sources} _ -> {[], []} end end @@ -330,9 +295,9 @@ defmodule Mix.Compilers.Elixir do @doc """ Retrieves all diagnostics from the given manifest. """ - def diagnostics(manifest, dest) do - {_, all_sources, _, _, _, _, _, _} = parse_manifest(manifest, dest) - previous_warnings(all_sources, false) + def diagnostics(manifest) do + {_, sources} = read_manifest(manifest) + previous_warnings(sources, false) end defp compiler_info_from_force(manifest, all_paths, all_modules, dest) do @@ -622,8 +587,8 @@ defmodule Mix.Compilers.Elixir do for %{app: app, opts: opts} <- local_deps, manifest = Path.join([opts[:build], ".mix", base]), Mix.Utils.last_modified(manifest) > modified, - reduce: {stale_modules, stale_modules, deps_exports} do - {modules, exports, deps_exports} -> + reduce: {stale_modules, stale_modules, deps_exports, protocols_and_impls()} do + {modules, exports, deps_exports, protocols_and_impls} -> {manifest_modules, manifest_sources} = read_manifest(manifest) dep_modules = @@ -676,7 +641,11 @@ defmodule Mix.Compilers.Elixir do modules = modules |> Map.merge(dep_modules) |> Map.merge(removed) exports = Map.merge(exports, removed) deps_exports = Map.put(deps_exports, app, new_exports) - {modules, exports, deps_exports} + + protocols_and_impls = + protocols_and_impls_from_modules(manifest_modules, protocols_and_impls) + + {modules, exports, deps_exports, protocols_and_impls} end end @@ -882,7 +851,7 @@ defmodule Mix.Compilers.Elixir do ## Manifest handling - @default_manifest {%{}, %{}, %{}, [], nil, nil, 0, 0} + @default_manifest {%{}, %{}, %{}, [], nil, nil, 0, 0, {%{}, %{}}} # Similar to read_manifest, but for internal consumption and with data migration support. defp parse_manifest(manifest, compile_path) do @@ -893,9 +862,9 @@ defmodule Mix.Compilers.Elixir do @default_manifest else {@manifest_vsn, modules, sources, local_exports, parent, cache_key, deps_config, - project_mtime, config_mtime} -> + project_mtime, config_mtime, protocols_and_impls} -> {modules, sources, local_exports, parent, cache_key, deps_config, project_mtime, - config_mtime} + config_mtime, protocols_and_impls} # {vsn, %{module => record}, sources, ...} v22-? # {vsn, [module_record], sources, ...} v5-v21 @@ -949,28 +918,24 @@ defmodule Mix.Compilers.Elixir do deps_config, project_mtime, config_mtime, + protocols_and_impls, timestamp ) do - if modules == %{} and sources == %{} do - File.rm(manifest) - else - File.mkdir_p!(Path.dirname(manifest)) + File.mkdir_p!(Path.dirname(manifest)) - term = - {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime, - config_mtime} + term = + {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime, + config_mtime, protocols_and_impls} - manifest_data = :erlang.term_to_binary(term, [:compressed]) - File.write!(manifest, manifest_data) - File.touch!(manifest, timestamp) - delete_checkpoint(manifest) - - # Since Elixir is a dependency itself, we need to touch the lock - # so the current Elixir version, used to compile the files above, - # is properly stored. - Mix.Dep.ElixirSCM.update() - end + manifest_data = :erlang.term_to_binary(term, [:compressed]) + File.write!(manifest, manifest_data) + File.touch!(manifest, timestamp) + delete_checkpoint(manifest) + # Since Elixir is a dependency itself, we need to touch the lock + # so the current Elixir version, used to compile the files above, + # is properly stored. + Mix.Dep.ElixirSCM.update() :ok end @@ -1056,6 +1021,9 @@ defmodule Mix.Compilers.Elixir do pid = spawn_link(fn -> compile_opts = [ + after_compile: fn -> + compiler_call(parent, ref, {:after_compile, opts}) + end, each_cycle: fn -> compiler_call(parent, ref, {:each_cycle, stale_modules, dest, timestamp}) end, @@ -1093,6 +1061,11 @@ defmodule Mix.Compilers.Elixir do defp compiler_loop(ref, pid, state, cwd) do receive do + {^ref, {:after_compile, opts}} -> + {response, state} = after_compile(state, opts) + send(pid, {ref, response}) + compiler_loop(ref, pid, state, cwd) + {^ref, {:each_cycle, stale_modules, dest, timestamp}} -> {response, state} = each_cycle(stale_modules, dest, timestamp, state) send(pid, {ref, response}) @@ -1122,8 +1095,18 @@ defmodule Mix.Compilers.Elixir do end end + defp after_compile(state, opts) do + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} = state + + state = + {modules, exports, sources, changed, pending_modules, stale_exports, + maybe_consolidate(consolidation, modules, pending_modules, opts)} + + {:ok, state} + end + defp each_cycle(stale_modules, compile_path, timestamp, state) do - {modules, _exports, sources, changed, pending_modules, stale_exports} = state + {modules, _exports, sources, changed, pending_modules, stale_exports, consolidation} = state {pending_modules, exports, changed} = update_stale_entries(pending_modules, sources, changed, %{}, stale_exports, compile_path) @@ -1166,7 +1149,7 @@ defmodule Mix.Compilers.Elixir do runtime_paths = Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")}) - state = {modules, exports, sources, [], pending_modules, stale_exports} + state = {modules, exports, sources, [], pending_modules, stale_exports, consolidation} {{:runtime, runtime_paths, []}, state} else Mix.Utils.compiling_n(length(changed), :ex) @@ -1186,14 +1169,14 @@ defmodule Mix.Compilers.Elixir do {Map.replace!(acc_sources, file, source(size: size, digest: digest)), acc_modules} end) - state = {modules, exports, sources, [], pending_modules, removed_modules} + state = {modules, exports, sources, [], pending_modules, removed_modules, consolidation} {{:compile, changed, []}, state} end end defp each_file(file, references, verbose, state, cwd) do {compile_references, export_references, runtime_references, compile_env} = references - {modules, exports, sources, changed, pending_modules, stale_exports} = state + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} = state file = Path.relative_to(file, cwd) @@ -1221,11 +1204,11 @@ defmodule Mix.Compilers.Elixir do ) sources = Map.replace!(sources, file, source) - {modules, exports, sources, changed, pending_modules, stale_exports} + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} end defp each_module(file, module, kind, external, new_export, state, timestamp, cwd) do - {modules, exports, sources, changed, pending_modules, stale_exports} = state + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} = state file = Path.relative_to(file, cwd) external = process_external_resources(external, cwd) @@ -1278,7 +1261,7 @@ defmodule Mix.Compilers.Elixir do %{} -> changed end - {modules, exports, sources, changed, pending_modules, stale_exports} + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} end defp detect_kind(module) do @@ -1307,4 +1290,52 @@ defmodule Mix.Compilers.Elixir do {Path.relative_to(file, cwd), Mix.Utils.last_modified_and_size(file), digest} end end + + ## Consolidation + + @doc """ + Returns protocols and implementations for the given `manifest`. + """ + def protocols_and_impls_from_paths(paths) do + Enum.reduce(paths, protocols_and_impls(), fn path, acc -> + {modules, _} = read_manifest(Path.join(path, ".mix/compile.elixir")) + protocols_and_impls_from_modules(modules, acc) + end) + end + + defp protocols_and_impls_from_modules(modules, protocols_and_impls) do + Enum.reduce(modules, protocols_and_impls, fn + {module, module(kind: kind, timestamp: timestamp)}, {protocols, impls} -> + case kind do + :protocol -> {Map.put(protocols, module, timestamp), impls} + {:impl, protocol} -> {protocols, Map.put(impls, module, protocol)} + _ -> {protocols, impls} + end + end) + end + + defp protocols_and_impls(), do: {%{}, %{}} + + defp maybe_consolidate({:off, _, _}, _, _, _) do + protocols_and_impls() + end + + defp maybe_consolidate( + {on_or_force, old_protocols_and_impls, protocols_and_impls}, + modules, + pending_modules, + opts + ) do + protocols_and_impls = protocols_and_impls_from_modules(modules, protocols_and_impls) + protocols_and_impls = protocols_and_impls_from_modules(pending_modules, protocols_and_impls) + + Mix.Compilers.Protocol.compile( + on_or_force == :force, + old_protocols_and_impls, + protocols_and_impls, + opts + ) + + protocols_and_impls + end end diff --git a/lib/mix/lib/mix/compilers/protocol.ex b/lib/mix/lib/mix/compilers/protocol.ex new file mode 100644 index 00000000000..69c5bd748bf --- /dev/null +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -0,0 +1,235 @@ +defmodule Mix.Compilers.Protocol do + @moduledoc false + @manifest "compile.protocols" + @manifest_vsn 3 + + ## Umbrella handling + + @switches [force: :boolean, verbose: :boolean, consolidate_protocols: :boolean] + + def umbrella(args, res) do + config = Mix.Project.config() + {opts, _, _} = OptionParser.parse(args, switches: @switches) + + opts = + if "--no-protocol-consolidation" in args do + # TODO: Deprecate me on Elixir v1.23 + Keyword.put(opts, :consolidate_protocols, false) + else + opts ++ Keyword.take(config, [:consolidate_protocols]) + end + + manifest = manifest() + config_mtime = Mix.Project.config_mtime() + {old_config_mtime, old_protocols_and_impls} = read_manifest(manifest) + + case status(config_mtime > old_config_mtime, opts) do + :off -> + res + + :on when res == :noop -> + :noop + + on_or_force -> + deps_paths = + for %{scm: scm, opts: opts} <- Mix.Dep.cached(), + not scm.fetchable?(), + do: opts[:build] + + protocols_and_impls = Mix.Compilers.Elixir.protocols_and_impls_from_paths(deps_paths) + + case compile(on_or_force == :force, old_protocols_and_impls, protocols_and_impls, opts) do + :ok -> + write_manifest(manifest, {config_mtime, protocols_and_impls}) + :ok + + :noop -> + res + end + end + end + + defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest) + + defp read_manifest(manifest) do + try do + [@manifest_vsn | metadata] = manifest |> File.read!() |> :erlang.binary_to_term() + metadata + rescue + _ -> + # If there is no manifest or it is out of date, remove old files + clean_consolidated() + {0, {%{}, %{}}} + end + end + + defp write_manifest(manifest, metadata) do + File.mkdir_p!(Path.dirname(manifest)) + manifest_data = :erlang.term_to_binary([@manifest_vsn | metadata], [:compressed]) + File.write!(manifest, manifest_data) + end + + def clean do + File.rm(manifest()) + clean_consolidated() + end + + ## General handling + + def status(mtime_changed?, opts) do + consolidation_path = Mix.Project.consolidation_path() + + cond do + not Keyword.get(opts, :consolidate_protocols, true) -> + clean_consolidated() + :off + + # We need to reconsolidate all protocols whenever the dependency changes + # because we only track protocols from the current app and from local deps. + # We are only interested in the compile.lock from config_mtime (which is + # a build artifact). + not File.exists?(consolidation_path) or mtime_changed? -> + :force + + true -> + :on + end + end + + def compile(force?, old_protocols_and_impls, protocols_and_impls, opts) do + output = Mix.Project.consolidation_path() + + res = + if opts[:force] || force? do + clean_consolidated() + paths = consolidation_paths() + + paths + |> Protocol.extract_protocols() + |> consolidate(paths, output, opts) + else + protocols_and_impls + |> diff_manifest(old_protocols_and_impls, output) + |> consolidate(consolidation_paths(), output, opts) + end + + Code.prepend_path(output) + res + end + + defp clean_consolidated do + File.rm_rf(Mix.Project.consolidation_path()) + end + + defp consolidation_paths do + filter_otp(:code.get_path(), :code.lib_dir()) + end + + defp filter_otp(paths, otp) do + Enum.filter(paths, &(not :lists.prefix(otp, &1))) + end + + defp consolidate([], _paths, output, _opts) do + File.mkdir_p!(output) + + :noop + end + + defp consolidate(protocols, paths, output, opts) do + File.mkdir_p!(output) + + protocols + |> Enum.uniq() + |> Enum.map(&Task.async(fn -> consolidate_each(&1, paths, output, opts) end)) + |> Enum.map(&Task.await(&1, :infinity)) + + :ok + end + + defp consolidate_each(protocol, paths, output, opts) do + impls = Protocol.extract_impls(protocol, paths) + reload(protocol) + + case Protocol.consolidate(protocol, impls) do + {:ok, binary} -> + File.write!(Path.join(output, "#{Atom.to_string(protocol)}.beam"), binary) + + if opts[:verbose] do + Mix.shell().info("Consolidated #{inspect_protocol(protocol)}") + end + + # If we remove a dependency and we have implemented one of its + # protocols locally, we will mark the protocol as needing to be + # reconsolidated when the implementation is removed even though + # the protocol no longer exists. Although most times removing a + # dependency will trigger a full recompilation, such won't happen + # in umbrella apps with shared build. + {:error, :no_beam_info} -> + remove_consolidated(protocol, output) + + if opts[:verbose] do + Mix.shell().info("Unavailable #{inspect_protocol(protocol)}") + end + end + end + + # We cannot use the inspect protocol while consolidating + # since inspect may not be available. + defp inspect_protocol(protocol) do + Macro.inspect_atom(:literal, protocol) + end + + defp reload(module) do + :code.purge(module) + :code.delete(module) + end + + defp diff_manifest({new_protocols, new_impls}, {old_protocols, old_impls}, output) do + protocols = + new_protocols + |> Enum.filter(fn {protocol, new_timestamp} -> + case old_protocols do + # There is a new version, removed the consolidated + %{^protocol => old_timestamp} when new_timestamp > old_timestamp -> + remove_consolidated(protocol, output) + true + + # Nothing changed + %{^protocol => _} -> + false + + # New protocol + %{} -> + true + end + end) + |> Map.new() + + protocols = + for {impl, protocol} <- new_impls, + not is_map_key(old_impls, impl), + do: {protocol, true}, + into: protocols + + removed_protocols = + for {protocol, _timestamp} <- old_protocols, + not is_map_key(new_protocols, protocol), + do: remove_consolidated(protocol, output) + + removed_protocols = Map.from_keys(removed_protocols, true) + + protocols = + for {impl, protocol} <- old_impls, + not is_map_key(new_impls, impl), + not is_map_key(removed_protocols, protocol), + do: {protocol, true}, + into: protocols + + Map.keys(protocols) + end + + defp remove_consolidated(protocol, output) do + File.rm(Path.join(output, "#{Atom.to_string(protocol)}.beam")) + protocol + end +end diff --git a/lib/mix/lib/mix/tasks/archive.build.ex b/lib/mix/lib/mix/tasks/archive.build.ex index ba6951f9543..f618e873b3e 100644 --- a/lib/mix/lib/mix/tasks/archive.build.ex +++ b/lib/mix/lib/mix/tasks/archive.build.ex @@ -63,7 +63,7 @@ defmodule Mix.Tasks.Archive.Build do if project && Keyword.get(opts, :compile, true) do # We only care about the archive ebin, so we disable protocol # consolidation to avoid further changes to the environment. - Mix.Task.run(:compile, ["--no-protocol-consolidation" | args]) + Mix.Task.run(:compile, ["--no-consolidate-protocols" | args]) end source = diff --git a/lib/mix/lib/mix/tasks/clean.ex b/lib/mix/lib/mix/tasks/clean.ex index 7c0964f97d7..138b623ecc7 100644 --- a/lib/mix/lib/mix/tasks/clean.ex +++ b/lib/mix/lib/mix/tasks/clean.ex @@ -39,11 +39,12 @@ defmodule Mix.Tasks.Clean do # First, we get the tasks. After that, we clean them. # This is to avoid a task cleaning a compiler module. tasks = - for compiler <- [:protocols] ++ Mix.Task.Compiler.compilers(), + for compiler <- Mix.Task.Compiler.compilers(), module = Mix.Task.get("compile.#{compiler}"), function_exported?(module, :clean, 0), do: module + Mix.Compilers.Protocol.clean() Enum.each(tasks, & &1.clean()) build = diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 7c3894f5992..7f542b15ab9 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -90,6 +90,7 @@ defmodule Mix.Tasks.Compile.Elixir do @switches [ force: :boolean, docs: :boolean, + consolidate_protocols: :boolean, warnings_as_errors: :boolean, ignore_module_conflict: :boolean, debug_info: :boolean, @@ -124,6 +125,14 @@ defmodule Mix.Tasks.Compile.Elixir do |> tracers_opts(tracers) |> profile_opts() + opts = + if "--no-protocol-consolidation" in args do + # TODO: Deprecate me on Elixir v1.23 + Keyword.put(opts, :consolidate_protocols, false) + else + opts ++ Keyword.take(project, [:consolidate_protocols]) + end + # Having compilations racing with other is most undesired, # so we wrap the compiler in a lock. @@ -148,8 +157,7 @@ defmodule Mix.Tasks.Compile.Elixir do @impl true def diagnostics do - dest = Mix.Project.compile_path() - Mix.Compilers.Elixir.diagnostics(manifest(), dest) + Mix.Compilers.Elixir.diagnostics(manifest()) end @impl true diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index e573e62b784..02ab8966038 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -24,8 +24,7 @@ defmodule Mix.Tasks.Compile do which are `[:erlang, :elixir, :app]`. * `:consolidate_protocols` - when `true`, runs protocol - consolidation via the `mix compile.protocols` task. The default - value is `true`. + consolidation after compiling Elixir. The default value is `true`. * `:build_path` - the directory where build artifacts should be written to. This option is intended only for @@ -151,26 +150,22 @@ defmodule Mix.Tasks.Compile do 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 + Mix.Project.umbrella?(config) -> + Mix.Compilers.Protocol.umbrella(args, res) true -> res end - with true <- consolidate_protocols?, - path = Mix.Project.consolidation_path(config), - {:ok, protocols} <- File.ls(path) do + path = Mix.Project.consolidation_path(config) + + with {: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) @@ -208,12 +203,6 @@ defmodule Mix.Tasks.Compile do @deprecated "Use Mix.Task.Compiler.manifests/0 instead" defdelegate manifests, to: Mix.Task.Compiler - ## 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." <> _ -> diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex index 6734f67dd18..26ba910fbca 100644 --- a/lib/mix/lib/mix/tasks/compile.protocols.ex +++ b/lib/mix/lib/mix/tasks/compile.protocols.ex @@ -1,243 +1,9 @@ defmodule Mix.Tasks.Compile.Protocols do - use Mix.Task.Compiler + @moduledoc false + use Mix.Task - @manifest "compile.protocols" - @manifest_vsn 3 - - @moduledoc ~S""" - Consolidates all protocols in all paths. - - This task is automatically invoked unless the project - disables the `:consolidate_protocols` option in their - configuration. - - ## Consolidation - - Protocol consolidation is useful in production when no - dynamic code loading will happen, effectively optimizing - protocol dispatches by not accounting for code loading. - - This task consolidates all protocols in the code path - and outputs the new binary files to the given directory. - Defaults to "_build/MIX_ENV/lib/YOUR_APP/consolidated" - for regular apps and "_build/MIX_ENV/consolidated" in - umbrella projects. - - In case you are manually compiling protocols or building - releases, you need to take the generated protocols into - account. This can be done with: - - $ elixir -pa _build/MIX_ENV/lib/YOUR_APP/consolidated -S mix run - - Or in umbrellas: - - $ elixir -pa _build/MIX_ENV/consolidated -S mix run - - You can verify a protocol is consolidated by checking - its attributes: - - iex> Protocol.consolidated?(Enumerable) - true - - """ - - @impl true - def run(args) do - config = Mix.Project.config() - Mix.Task.run("compile") - {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, verbose: :boolean]) - - manifest = manifest() - output = Mix.Project.consolidation_path(config) - config_mtime = Mix.Project.config_mtime() - protocols_and_impls = protocols_and_impls(config) - metadata = {config_mtime, protocols_and_impls} - {old_config_mtime, old_protocols_and_impls} = read_manifest(manifest, output) - - cond do - # We need to reconsolidate all protocols whenever the dependency changes - # because we only track protocols from the current app and from local deps. - # - # We are only interested in the compile.lock from config_mtime (which is - # a build artifact), so it would be fine to compare it directly against - # the manifest, but let's follow best practices anyway. - opts[:force] || config_mtime > old_config_mtime -> - clean() - paths = consolidation_paths() - - paths - |> Protocol.extract_protocols() - |> consolidate(paths, output, manifest, metadata, opts) - - protocols_and_impls -> - protocols_and_impls - |> diff_manifest(old_protocols_and_impls, output) - |> consolidate(consolidation_paths(), output, manifest, metadata, opts) - - true -> - :noop - end - end - - @impl true - def clean do - File.rm(manifest()) - File.rm_rf(Mix.Project.consolidation_path()) - end - - @impl true - def manifests, do: [manifest()] - - defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest) - - @doc """ - Returns if protocols have been consolidated at least once. - """ - def consolidated? do - File.regular?(manifest()) - end - - defp protocols_and_impls(config) do - deps = for %{scm: scm, opts: opts} <- Mix.Dep.cached(), not scm.fetchable?(), do: opts[:build] - - paths = - if Mix.Project.umbrella?(config) do - deps - else - [Mix.Project.app_path(config) | deps] - end - - Mix.Compilers.Elixir.protocols_and_impls(paths) - end - - defp consolidation_paths do - filter_otp(:code.get_path(), :code.lib_dir()) - end - - defp filter_otp(paths, otp) do - Enum.filter(paths, &(not :lists.prefix(otp, &1))) - end - - defp consolidate([], _paths, output, manifest, metadata, _opts) do - File.mkdir_p!(output) - write_manifest(manifest, metadata) + # TODO: Deprecate me on Elixir v1.23 + def run(_args) do :noop end - - defp consolidate(protocols, paths, output, manifest, metadata, opts) do - File.mkdir_p!(output) - - protocols - |> Enum.uniq() - |> Enum.map(&Task.async(fn -> consolidate(&1, paths, output, opts) end)) - |> Enum.map(&Task.await(&1, :infinity)) - - write_manifest(manifest, metadata) - :ok - end - - defp consolidate(protocol, paths, output, opts) do - impls = Protocol.extract_impls(protocol, paths) - reload(protocol) - - case Protocol.consolidate(protocol, impls) do - {:ok, binary} -> - File.write!(Path.join(output, "#{Atom.to_string(protocol)}.beam"), binary) - - if opts[:verbose] do - Mix.shell().info("Consolidated #{inspect_protocol(protocol)}") - end - - # If we remove a dependency and we have implemented one of its - # protocols locally, we will mark the protocol as needing to be - # reconsolidated when the implementation is removed even though - # the protocol no longer exists. Although most times removing a - # dependency will trigger a full recompilation, such won't happen - # in umbrella apps with shared build. - {:error, :no_beam_info} -> - remove_consolidated(protocol, output) - - if opts[:verbose] do - Mix.shell().info("Unavailable #{inspect_protocol(protocol)}") - end - end - end - - # We cannot use the inspect protocol while consolidating - # since inspect may not be available. - defp inspect_protocol(protocol) do - Macro.inspect_atom(:literal, protocol) - end - - defp reload(module) do - :code.purge(module) - :code.delete(module) - end - - defp read_manifest(manifest, output) do - try do - [@manifest_vsn | metadata] = manifest |> File.read!() |> :erlang.binary_to_term() - metadata - rescue - _ -> - # If there is no manifest or it is out of date, remove old files - File.rm_rf(output) - {0, {%{}, %{}}} - end - end - - defp write_manifest(manifest, metadata) do - File.mkdir_p!(Path.dirname(manifest)) - manifest_data = :erlang.term_to_binary([@manifest_vsn | metadata], [:compressed]) - File.write!(manifest, manifest_data) - end - - defp diff_manifest({new_protocols, new_impls}, {old_protocols, old_impls}, output) do - protocols = - new_protocols - |> Enum.filter(fn {protocol, new_timestamp} -> - case old_protocols do - # There is a new version, removed the consolidated - %{^protocol => old_timestamp} when new_timestamp > old_timestamp -> - remove_consolidated(protocol, output) - true - - # Nothing changed - %{^protocol => _} -> - false - - # New protocol - %{} -> - true - end - end) - |> Map.new() - - protocols = - for {impl, protocol} <- new_impls, - not is_map_key(old_impls, impl), - do: {protocol, true}, - into: protocols - - removed_protocols = - for {protocol, _timestamp} <- old_protocols, - not is_map_key(new_protocols, protocol), - do: remove_consolidated(protocol, output) - - removed_protocols = Map.from_keys(removed_protocols, true) - - protocols = - for {impl, protocol} <- old_impls, - not is_map_key(new_impls, impl), - not is_map_key(removed_protocols, protocol), - do: {protocol, true}, - into: protocols - - Map.keys(protocols) - end - - defp remove_consolidated(protocol, output) do - File.rm(Path.join(output, "#{Atom.to_string(protocol)}.beam")) - protocol - end end diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 8b130b74f02..86d12c4c6b9 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -6,13 +6,29 @@ defmodule Mix.Tasks.Compile.ElixirTest do import ExUnit.CaptureIO alias Mix.Task.Compiler.Diagnostic + @old_time {{2010, 1, 1}, {0, 0, 0}} + @elixir_otp_version {System.version(), :erlang.system_info(:otp_release)} + def trace(event, env) do send(__MODULE__, {event, env}) :ok end - @old_time {{2010, 1, 1}, {0, 0, 0}} - @elixir_otp_version {System.version(), :erlang.system_info(:otp_release)} + defp mtime(path) do + File.stat!(path).mtime + end + + defp mark_as_old!(path) do + mtime = mtime(path) + File.touch!(path, @old_time) + mtime + end + + defp purge_protocol(module) do + :code.del_path(:filename.absname(~c"_build/dev/lib/sample/consolidated")) + :code.purge(module) + :code.delete(module) + end test "compiles a project without per environment build" do Mix.ProjectStack.post_config(build_per_environment: false) @@ -128,7 +144,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time + assert mtime("_build/dev/lib/sample/.mix/compile.elixir") > @old_time ensure_touched(__ENV__.file, "_build/dev/lib/sample/.mix/compile.elixir") assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} @@ -245,7 +261,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time + assert mtime("_build/dev/lib/sample/.mix/compile.elixir") > @old_time end) after Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []}) @@ -482,7 +498,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time + assert mtime("_build/dev/lib/sample/.mix/compile.elixir") > @old_time # Changing lock recompiles File.write!("mix.lock", """ @@ -494,7 +510,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time + assert mtime("_build/dev/lib/sample/.mix/compile.elixir") > @old_time # Removing a lock recompiles File.write!("mix.lock", """ @@ -506,7 +522,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time + assert mtime("_build/dev/lib/sample/.mix/compile.elixir") > @old_time # Adding an unknown dependency returns :ok but does not recompile File.write!("mix.lock", """ @@ -584,9 +600,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do Mix.Tasks.Compile.run([]) assert Mix.Dep.ElixirSCM.read() == {:ok, @elixir_otp_version, Mix.SCM.Path} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir_scm").mtime > - @old_time - + assert mtime("_build/dev/lib/sample/.mix/compile.elixir_scm") > @old_time refute File.exists?("_build/dev/lib/sample/consolidated/.to_be_removed") end) end @@ -608,8 +622,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do Mix.Tasks.Compile.run([]) assert Mix.Dep.ElixirSCM.read() == {:ok, @elixir_otp_version, Mix.SCM.Path} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir_scm").mtime > - @old_time + assert mtime("_build/dev/lib/sample/.mix/compile.elixir_scm") > @old_time end) end @@ -720,38 +733,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do end) end - test "purges consolidation path if asked" do - in_fixture("no_mixfile", fn -> - File.write!("lib/a.ex", """ - defmodule A do - defstruct [] - end - - defimpl Inspect, for: A do - def inspect(_, _), do: "sample" - end - """) - - Mix.Project.push(MixTest.Case.Sample) - assert Mix.Tasks.Compile.run([]) == {:ok, []} - assert inspect(struct(A, [])) == "sample" - - purge([A, B, Inspect.A]) - Mix.Task.clear() - - assert capture_io(:stderr, fn -> - {:ok, [_]} = Mix.Tasks.Compile.run(["--force"]) - end) =~ - "the Inspect protocol has already been consolidated" - - purge([A, B, Inspect.A]) - Mix.Task.clear() - consolidation = Mix.Project.consolidation_path() - args = ["--force", "--purge-consolidation-path-if-stale", consolidation] - assert Mix.Tasks.Compile.run(args) == {:ok, []} - end) - end - test "recompiles mtime changed files if content changed but not length" do in_fixture("no_mixfile", fn -> Mix.Project.push(MixTest.Case.Sample) @@ -1315,6 +1296,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} + Mix.shell().flush() File.rm!("lib/a.ex") assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} @@ -1343,7 +1325,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []} - assert File.stat!("lib/a.ex").mtime == @old_time + assert mtime("lib/a.ex") == @old_time Agent.update(:mix_recompile_raise, fn _ -> true end) @@ -1728,4 +1710,180 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert Mix.Tasks.Compile.Elixir.run(["--no-optional-deps"]) == {:ok, []} end) end + + describe "consolidation protocols" do + test "with local protocols", context do + in_tmp(context.test, fn -> + Mix.Project.push(MixTest.Case.Sample) + + File.mkdir_p!("lib") + assert Mix.Task.run("compile") + + # Define a local protocol + File.write!("lib/protocol.ex", """ + defprotocol Compile.Protocol do + def foo(a, b) + end + """) + + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") + + # Implement a local protocol + File.write!("lib/impl.ex", """ + defimpl Compile.Protocol, for: Integer do + def foo(a, b), do: a + b + end + """) + + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + + assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") != + @old_time + + # Delete a local implementation + File.rm!("lib/impl.ex") + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + + assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") != + @old_time + + # Delete a local protocol + File.rm!("lib/protocol.ex") + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") + end) + end + + test "compiles after converting a protocol into a standard module", context do + in_tmp(context.test, fn -> + Mix.Project.push(MixTest.Case.Sample) + + File.mkdir_p!("lib") + Mix.Task.run("compile") + purge_protocol(Compile.Protocol) + + # Define a local protocol + File.write!("lib/protocol.ex", """ + defprotocol Compile.Protocol do + def foo(a) + end + + defimpl Compile.Protocol, for: Integer do + def foo(a), do: a + end + """) + + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") + File.rm!("lib/protocol.ex") + + # Define a standard module + File.write!("lib/protocol.ex", """ + defmodule Compile.Protocol do + end + """) + + purge_protocol(Compile.Protocol) + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + + # Delete a local protocol + File.rm!("lib/protocol.ex") + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") + end) + end + + test "with deps protocols", context do + in_tmp(context.test, fn -> + Mix.Project.push(MixTest.Case.Sample) + + File.mkdir_p!("lib") + Mix.Task.run("compile") + purge_protocol(String.Chars) + mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") + + assert Mix.Tasks.Compile.Elixir.run([]) == {:noop, []} + assert mtime("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") == @old_time + + # Implement a deps protocol + File.write!("lib/struct.ex", """ + defmodule Compile.Protocol.Struct do + defstruct a: nil + defimpl String.Chars do + def to_string(_), do: "ok" + end + end + """) + + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + + assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != + @old_time + + # Delete the local implementation + File.rm!("lib/struct.ex") + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} + + assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != + @old_time + end) + end + + test "keep relative path to their source" do + in_fixture("no_mixfile", fn -> + Mix.Project.push(MixTest.Case.Sample) + Mix.Task.run("compile") + + # Load consolidated + :code.add_patha(~c"_build/dev/lib/sample/consolidated") + :code.purge(Enumerable) + :code.delete(Enumerable) + + try do + Enumerable.impl_for!(:oops) + rescue + Protocol.UndefinedError -> + assert [{_, _, _, [file: ~c"lib/enum.ex"] ++ _} | _] = __STACKTRACE__ + else + _ -> + flunk("Enumerable.impl_for!/1 should have failed") + after + purge_protocol(Enumerable) + end + end) + end + + test "purges consolidation path if asked" do + in_fixture("no_mixfile", fn -> + File.write!("lib/a.ex", """ + defmodule A do + defstruct [] + end + + defimpl Inspect, for: A do + def inspect(_, _), do: "sample" + end + """) + + Mix.Project.push(MixTest.Case.Sample) + assert Mix.Tasks.Compile.run([]) == {:ok, []} + assert inspect(struct(A, [])) == "sample" + + purge([A, B, Inspect.A]) + Mix.Task.clear() + + assert capture_io(:stderr, fn -> + {:ok, [_]} = Mix.Tasks.Compile.run(["--force"]) + end) =~ + "the Inspect protocol has already been consolidated" + + purge([A, B, Inspect.A]) + Mix.Task.clear() + consolidation = Mix.Project.consolidation_path() + args = ["--force", "--purge-consolidation-path-if-stale", consolidation] + assert Mix.Tasks.Compile.run(args) == {:ok, []} + end) + end + end end diff --git a/lib/mix/test/mix/tasks/compile.protocols_test.exs b/lib/mix/test/mix/tasks/compile.protocols_test.exs deleted file mode 100644 index 6e7db79ad29..00000000000 --- a/lib/mix/test/mix/tasks/compile.protocols_test.exs +++ /dev/null @@ -1,165 +0,0 @@ -Code.require_file("../../test_helper.exs", __DIR__) - -defmodule Mix.Tasks.Compile.ProtocolsTest do - use MixTest.Case - - @old {{2010, 1, 1}, {0, 0, 0}} - - test "compiles and consolidates local protocols", context do - in_tmp(context.test, fn -> - Mix.Project.push(MixTest.Case.Sample) - - File.mkdir_p!("lib") - assert Mix.Task.run("compile") - - # Define a local protocol - File.write!("lib/protocol.ex", """ - defprotocol Compile.Protocol do - def foo(a, b) - end - """) - - assert compile_elixir_and_protocols() == :ok - mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") - - # Implement a local protocol - File.write!("lib/impl.ex", """ - defimpl Compile.Protocol, for: Integer do - def foo(a, b), do: a + b - end - """) - - assert compile_elixir_and_protocols() == :ok - - assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") != - @old - - # Delete a local implementation - File.rm!("lib/impl.ex") - assert compile_elixir_and_protocols() == :ok - - assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") != - @old - - # Delete a local protocol - File.rm!("lib/protocol.ex") - assert compile_elixir_and_protocols() == :noop - refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") - end) - end - - test "compiles after converting a protocol into a standard module", context do - in_tmp(context.test, fn -> - Mix.Project.push(MixTest.Case.Sample) - - File.mkdir_p!("lib") - Mix.Task.run("compile") - purge_protocol(Compile.Protocol) - - # Define a local protocol - File.write!("lib/protocol.ex", """ - defprotocol Compile.Protocol do - def foo(a) - end - - defimpl Compile.Protocol, for: Integer do - def foo(a), do: a - end - """) - - assert compile_elixir_and_protocols() == :ok - mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") - File.rm!("lib/protocol.ex") - - # Define a standard module - File.write!("lib/protocol.ex", """ - defmodule Compile.Protocol do - end - """) - - assert compile_elixir_and_protocols() == :noop - - # Delete a local protocol - File.rm!("lib/protocol.ex") - assert compile_elixir_and_protocols() == :noop - refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") - end) - end - - test "compiles and consolidates deps protocols", context do - in_tmp(context.test, fn -> - Mix.Project.push(MixTest.Case.Sample) - - File.mkdir_p!("lib") - Mix.Task.run("compile") - purge_protocol(String.Chars) - mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") - - assert compile_elixir_and_protocols() == :noop - assert mtime("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") == @old - - # Implement a deps protocol - File.write!("lib/struct.ex", """ - defmodule Compile.Protocol.Struct do - defstruct a: nil - defimpl String.Chars do - def to_string(_), do: "ok" - end - end - """) - - assert compile_elixir_and_protocols() == :ok - assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old - - # Delete the local implementation - File.rm!("lib/struct.ex") - assert compile_elixir_and_protocols() == :ok - assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old - end) - end - - test "consolidated protocols keep relative path to their source" do - in_fixture("no_mixfile", fn -> - Mix.Project.push(MixTest.Case.Sample) - compile_elixir_and_protocols() - - # Load consolidated - :code.add_patha(~c"_build/dev/lib/sample/consolidated") - :code.purge(Enumerable) - :code.delete(Enumerable) - - try do - Enumerable.impl_for!(:oops) - rescue - Protocol.UndefinedError -> - assert [{_, _, _, [file: ~c"lib/enum.ex"] ++ _} | _] = __STACKTRACE__ - else - _ -> - flunk("Enumerable.impl_for!/1 should have failed") - after - purge_protocol(Enumerable) - end - end) - end - - defp compile_elixir_and_protocols do - Mix.Tasks.Compile.Elixir.run([]) - Mix.Tasks.Compile.Protocols.run([]) - end - - defp mtime(path) do - File.stat!(path).mtime - end - - defp mark_as_old!(path) do - mtime = mtime(path) - File.touch!(path, @old) - mtime - end - - defp purge_protocol(module) do - :code.del_path(:filename.absname(~c"_build/dev/lib/sample/consolidated")) - :code.purge(module) - :code.delete(module) - end -end diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 2ecf29cbeb7..df2bbb5b5e5 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -29,7 +29,7 @@ defmodule Mix.Tasks.CompileTest do msg = "\nEnabled compilers: yecc, leex, erlang, elixir, app, protocols" assert_received {:mix_shell, :info, [^msg]} - assert_received {:mix_shell, :info, ["mix compile.elixir # " <> _]} + assert_received {:mix_shell, :info, ["mix compile.elixir # " <> _]} end @tag project: [compilers: [:elixir, :app, :custom]] @@ -55,22 +55,9 @@ defmodule Mix.Tasks.CompileTest do assert_received {:mix_shell, :info, ["Generated sample app"]} assert File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam") - # Noop Mix.Task.clear() assert Mix.Task.run("compile", ["--verbose"]) == {:noop, []} refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} - - # Consolidates protocols if manifest is out of date - File.rm("_build/dev/lib/sample/.mix/compile.protocols") - Mix.Task.clear() - assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []} - refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} - assert File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam") - - # Purge so consolidated is picked up - purge([Enumerable]) - assert Mix.Tasks.App.Start.run(["--verbose"]) == :ok - assert Protocol.consolidated?(Enumerable) end) end diff --git a/lib/mix/test/mix/umbrella_test.exs b/lib/mix/test/mix/umbrella_test.exs index a2c66962210..b7a92b7d2bb 100644 --- a/lib/mix/test/mix/umbrella_test.exs +++ b/lib/mix/test/mix/umbrella_test.exs @@ -569,7 +569,7 @@ defmodule Mix.UmbrellaTest do Mix.Task.run("compile") assert File.regular?("_build/dev/lib/bar/consolidated/Elixir.Foo.beam") - assert Mix.Tasks.Compile.Protocols.run([]) == :noop + assert Mix.Tasks.Compile.Elixir.run([]) == {:noop, []} # Mark protocol as outdated File.touch!("_build/dev/lib/bar/consolidated/Elixir.Foo.beam", {{2010, 1, 1}, {0, 0, 0}})