From 64dd1d91d2628575d0eba0bcc48591b159845db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 15:41:45 +0100 Subject: [PATCH 01/14] Merge protocol consolidation into compile.elixir --- lib/mix/lib/mix.ex | 3 +- lib/mix/lib/mix/compilers/elixir.ex | 172 ++++++++------- lib/mix/lib/mix/compilers/protocol.ex | 218 +++++++++++++++++++ lib/mix/lib/mix/tasks/archive.build.ex | 2 +- lib/mix/lib/mix/tasks/clean.ex | 3 +- lib/mix/lib/mix/tasks/compile.elixir.ex | 1 + lib/mix/lib/mix/tasks/compile.ex | 21 +- lib/mix/lib/mix/tasks/compile.protocols.ex | 242 +-------------------- lib/mix/test/mix/tasks/compile_test.exs | 6 +- 9 files changed, 331 insertions(+), 337 deletions(-) create mode 100644 lib/mix/lib/mix/compilers/protocol.ex diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 97f018e6fce..6c3bbf5cdab 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`) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 629bc73cd04..6d7d2fb97e2 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,26 +156,41 @@ 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 true = Code.prepend_path(dest) previous_opts = set_compiler_opts(opts) + # Group consolidation information to pass to compiler + 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 +211,7 @@ defmodule Mix.Compilers.Elixir do new_deps_config, project_mtime, config_mtime, + protocols_and_impls, timestamp ) @@ -217,7 +232,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 +241,13 @@ 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) +<<<<<<< HEAD unless_warnings_as_errors(opts, {status, previous_warnings}) +======= + unless_previous_warnings_as_errors(previous_warnings, opts, {:noop, previous_warnings}) +>>>>>>> 737162c52 (Merge protocol consolidation into compile.elixir) end end @@ -295,24 +284,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. """ @@ -331,7 +302,7 @@ defmodule Mix.Compilers.Elixir do Retrieves all diagnostics from the given manifest. """ def diagnostics(manifest, dest) do - {_, all_sources, _, _, _, _, _, _} = parse_manifest(manifest, dest) + {_, all_sources, _, _, _, _, _, _, _} = parse_manifest(manifest, dest) previous_warnings(all_sources, false) end @@ -622,8 +593,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 +647,8 @@ 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(modules, protocols_and_impls) + {modules, exports, deps_exports, protocols_and_impls} end end @@ -882,7 +854,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 +865,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,6 +921,7 @@ defmodule Mix.Compilers.Elixir do deps_config, project_mtime, config_mtime, + protocols_and_impls, timestamp ) do if modules == %{} and sources == %{} do @@ -958,7 +931,7 @@ defmodule Mix.Compilers.Elixir do term = {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime, - config_mtime} + config_mtime, protocols_and_impls} manifest_data = :erlang.term_to_binary(term, [:compressed]) File.write!(manifest, manifest_data) @@ -1057,7 +1030,7 @@ defmodule Mix.Compilers.Elixir do spawn_link(fn -> compile_opts = [ each_cycle: fn -> - compiler_call(parent, ref, {:each_cycle, stale_modules, dest, timestamp}) + compiler_call(parent, ref, {:each_cycle, stale_modules, dest, timestamp, opts}) end, each_file: fn file, lexical -> compiler_call(parent, ref, {:each_file, file, lexical, verbose}) @@ -1093,8 +1066,8 @@ defmodule Mix.Compilers.Elixir do defp compiler_loop(ref, pid, state, cwd) do receive do - {^ref, {:each_cycle, stale_modules, dest, timestamp}} -> - {response, state} = each_cycle(stale_modules, dest, timestamp, state) + {^ref, {:each_cycle, stale_modules, dest, timestamp, opts}} -> + {response, state} = each_cycle(stale_modules, dest, timestamp, state, opts) send(pid, {ref, response}) compiler_loop(ref, pid, state, cwd) @@ -1122,8 +1095,8 @@ defmodule Mix.Compilers.Elixir do end end - defp each_cycle(stale_modules, compile_path, timestamp, state) do - {modules, _exports, sources, changed, pending_modules, stale_exports} = state + defp each_cycle(stale_modules, compile_path, timestamp, state, opts) do + {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 +1139,8 @@ 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} + protocols_and_impls = maybe_consolidate(consolidation, modules, opts) + state = {modules, exports, sources, [], pending_modules, stale_exports, protocols_and_impls} {{:runtime, runtime_paths, []}, state} else Mix.Utils.compiling_n(length(changed), :ex) @@ -1193,7 +1167,7 @@ defmodule Mix.Compilers.Elixir do 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, consolidate} = state file = Path.relative_to(file, cwd) @@ -1221,11 +1195,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, consolidate} 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, consolidate} = state file = Path.relative_to(file, cwd) external = process_external_resources(external, cwd) @@ -1278,7 +1252,7 @@ defmodule Mix.Compilers.Elixir do %{} -> changed end - {modules, exports, sources, changed, pending_modules, stale_exports} + {modules, exports, sources, changed, pending_modules, stale_exports, consolidate} end defp detect_kind(module) do @@ -1307,4 +1281,50 @@ 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, + opts + ) do + protocols_and_impls = protocols_and_impls_from_modules(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..2ffb36bf07f --- /dev/null +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -0,0 +1,218 @@ +defmodule Mix.Compilers.Protocol do + @moduledoc false + @manifest "compile.protocols" + @manifest_vsn 3 + + ## Umbrella handling + + def umbrella(args, res) do + {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, verbose: :boolean]) + 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() + + 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 + 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..5b655caa6a8 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, diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index e573e62b784..d7fcaa1c40d 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 @@ -72,7 +71,7 @@ defmodule Mix.Tasks.Compile do default case with dependencies) * `--no-prune-code-paths` - do not prune code paths before compilation, this keeps the entirety of Erlang/OTP available when the project starts - * `--no-protocol-consolidation` - skips protocol consolidation + * `--no-consolidate-protocols` - skips protocol consolidation * `--no-validate-compile-env` - does not validate the application compile environment * `--return-errors` - returns error status and diagnostics instead of exiting on error * `--warnings-as-errors` - exit with non-zero status if compilation has one or more @@ -151,24 +150,20 @@ 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?, + with true <- config[:consolidate_protocols] and "--no-consolidate-protocols" not in args, path = Mix.Project.consolidation_path(config), {:ok, protocols} <- File.ls(path) do # We don't cache consolidation path as we may write to it @@ -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..c84bd5fd778 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: Warn 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_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 2ecf29cbeb7..1a382440a36 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -224,10 +224,10 @@ defmodule Mix.Tasks.CompileTest do end) end - test "skip protocol consolidation when --no-protocol-consolidation" do + test "skip protocol consolidation when --no-consolidate-protocols" do in_fixture("no_mixfile", fn -> File.rm("_build/dev/lib/sample/.mix/compile.protocols") - assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:ok, []} + assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:ok, []} assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam") refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam") end) @@ -283,7 +283,7 @@ defmodule Mix.Tasks.CompileTest do Mix.Project.push(WrongPath) ExUnit.CaptureIO.capture_io(fn -> - assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:noop, []} + assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:noop, []} end) end From 7ca6b219ecf5887633824146037047b9bfc24863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 15:46:54 +0100 Subject: [PATCH 02/14] Prepend consolidated after running --- lib/mix/lib/mix/compilers/protocol.ex | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/mix/lib/mix/compilers/protocol.ex b/lib/mix/lib/mix/compilers/protocol.ex index 2ffb36bf07f..47bf661b712 100644 --- a/lib/mix/lib/mix/compilers/protocol.ex +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -86,6 +86,8 @@ defmodule Mix.Compilers.Protocol do def compile(force?, old_protocols_and_impls, protocols_and_impls, opts) do output = Mix.Project.consolidation_path() + File.mkdir_p!(output) + Code.prepend_path(output) if opts[:force] || force? do clean_consolidated() @@ -113,14 +115,11 @@ defmodule Mix.Compilers.Protocol do Enum.filter(paths, &(not :lists.prefix(otp, &1))) end - defp consolidate([], _paths, output, _opts) do - File.mkdir_p!(output) + defp consolidate([], _paths, _output, _opts) do :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)) From 6f270b306e86483ae04653a82f6c390603c6af9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 16:13:18 +0100 Subject: [PATCH 03/14] Skip consolidation flag check, as the path won't exist --- lib/mix/lib/mix/tasks/compile.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index d7fcaa1c40d..6e5579c8dfb 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -163,8 +163,7 @@ defmodule Mix.Tasks.Compile do res end - with true <- config[:consolidate_protocols] and "--no-consolidate-protocols" not in args, - path = Mix.Project.consolidation_path(config), + with path = Mix.Project.consolidation_path(config), {:ok, protocols} <- File.ls(path) do # We don't cache consolidation path as we may write to it Code.prepend_path(path) From 6f057947ab7d836967c911a2ddc0e5f155fda701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 17:53:32 +0100 Subject: [PATCH 04/14] Fixes --- lib/iex/lib/iex/helpers.ex | 1 - lib/mix/lib/mix/compilers/elixir.ex | 13 +++++---- lib/mix/lib/mix/compilers/protocol.ex | 36 ++++++++++++++----------- lib/mix/test/mix/tasks/compile_test.exs | 2 +- 4 files changed, 30 insertions(+), 22 deletions(-) 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/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 6d7d2fb97e2..d9f01b7e751 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -293,7 +293,7 @@ defmodule Mix.Compilers.Elixir do rescue _ -> {[], []} else - {@manifest_vsn, modules, sources, _, _, _, _, _, _} -> {modules, sources} + {@manifest_vsn, modules, sources, _, _, _, _, _, _, _} -> {modules, sources} _ -> {[], []} end end @@ -302,8 +302,8 @@ defmodule Mix.Compilers.Elixir do Retrieves all diagnostics from the given manifest. """ def diagnostics(manifest, dest) do - {_, all_sources, _, _, _, _, _, _, _} = parse_manifest(manifest, dest) - previous_warnings(all_sources, false) + {_, sources} = read_manifest(manifest, dest) + previous_warnings(sources, false) end defp compiler_info_from_force(manifest, all_paths, all_modules, dest) do @@ -647,7 +647,10 @@ 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) - protocols_and_impls = protocols_and_impls_from_modules(modules, protocols_and_impls) + + protocols_and_impls = + protocols_and_impls_from_modules(manifest_modules, protocols_and_impls) + {modules, exports, deps_exports, protocols_and_impls} end end @@ -1160,7 +1163,7 @@ 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, consolidate} {{:compile, changed, []}, state} end end diff --git a/lib/mix/lib/mix/compilers/protocol.ex b/lib/mix/lib/mix/compilers/protocol.ex index 47bf661b712..329d2f6997b 100644 --- a/lib/mix/lib/mix/compilers/protocol.ex +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -86,21 +86,23 @@ defmodule Mix.Compilers.Protocol do def compile(force?, old_protocols_and_impls, protocols_and_impls, opts) do output = Mix.Project.consolidation_path() - File.mkdir_p!(output) - Code.prepend_path(output) - 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 + 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 @@ -115,11 +117,15 @@ defmodule Mix.Compilers.Protocol do Enum.filter(paths, &(not :lists.prefix(otp, &1))) end - defp consolidate([], _paths, _output, _opts) do + 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)) diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 1a382440a36..bc7c3f14eef 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]] From a8b32ecd8e6cceb985b0c7ac7c73cbdcb128f7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:15:19 +0100 Subject: [PATCH 05/14] More work --- lib/mix/lib/mix/compilers/elixir.ex | 16 +++++++--------- lib/mix/lib/mix/tasks/compile.elixir.ex | 3 +-- lib/mix/lib/mix/tasks/compile.ex | 5 +++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index d9f01b7e751..c5941fef671 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -181,8 +181,6 @@ defmodule Mix.Compilers.Elixir do true = Code.prepend_path(dest) previous_opts = set_compiler_opts(opts) - # Group consolidation information to pass to compiler - try do consolidation = {consolidation_status, old_protocols_and_impls, protocols_and_impls} state = {%{}, exports, sources, [], modules, removed_modules, consolidation} @@ -301,8 +299,8 @@ defmodule Mix.Compilers.Elixir do @doc """ Retrieves all diagnostics from the given manifest. """ - def diagnostics(manifest, dest) do - {_, sources} = read_manifest(manifest, dest) + def diagnostics(manifest) do + {_, sources} = read_manifest(manifest) previous_warnings(sources, false) end @@ -1163,14 +1161,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, consolidate} + 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, consolidate} = state + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} = state file = Path.relative_to(file, cwd) @@ -1198,11 +1196,11 @@ defmodule Mix.Compilers.Elixir do ) sources = Map.replace!(sources, file, source) - {modules, exports, sources, changed, pending_modules, stale_exports, consolidate} + {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, consolidate} = state + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} = state file = Path.relative_to(file, cwd) external = process_external_resources(external, cwd) @@ -1255,7 +1253,7 @@ defmodule Mix.Compilers.Elixir do %{} -> changed end - {modules, exports, sources, changed, pending_modules, stale_exports, consolidate} + {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} end defp detect_kind(module) do diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 5b655caa6a8..5c283e77fde 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -149,8 +149,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 6e5579c8dfb..860f5bcaff8 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -163,8 +163,9 @@ defmodule Mix.Tasks.Compile do res end - with 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) From f7d8b29713c03d2b661164e49e5e3a7c3614ee1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:29:09 +0100 Subject: [PATCH 06/14] Merge result --- lib/mix/lib/mix/compilers/elixir.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index c5941fef671..c5f91e1930a 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -241,11 +241,7 @@ defmodule Mix.Compilers.Elixir do else all_warnings = Keyword.get(opts, :all_warnings, true) previous_warnings = previous_warnings(sources, all_warnings) -<<<<<<< HEAD - unless_warnings_as_errors(opts, {status, previous_warnings}) -======= - unless_previous_warnings_as_errors(previous_warnings, opts, {:noop, previous_warnings}) ->>>>>>> 737162c52 (Merge protocol consolidation into compile.elixir) + unless_warnings_as_errors(opts, {:noop, previous_warnings}) end end From 6e455eafd396cb238d502ae0caacc00834be1c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:40:05 +0100 Subject: [PATCH 07/14] More fixes --- lib/elixir/lib/kernel/parallel_compiler.ex | 15 ++++++----- lib/mix/lib/mix/compilers/elixir.ex | 29 +++++++++++++++++----- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 86c06613eef..06c43e8ea8f 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -139,6 +139,8 @@ defmodule Kernel.ParallelCompiler do * `{:runtime, modules, warnings}` - to stop compilation and verify the list of modules because dependent modules have changed + * `:after_persistence` - ... + * `:long_compilation_threshold` - the timeout (in seconds) to check for modules taking too long to compile. For each file that exceeds the threshold, the `:each_long_compilation` callback is invoked. From Elixir v1.11, only the time @@ -258,13 +260,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 +283,9 @@ defmodule Kernel.ParallelCompiler do {outcome, state} = spawn_workers(files, %{}, %{}, [], %{}, [], [], %{ + beam_timestamp: Keyword.get(options, :beam_timestamp), dest: Keyword.get(options, :dest), + after_persistence: Keyword.get(options, :after_persistence, 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 +342,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_persistence.() 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/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index c5f91e1930a..7e7a68d116f 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1026,8 +1026,11 @@ defmodule Mix.Compilers.Elixir do pid = spawn_link(fn -> compile_opts = [ + after_persistence: fn -> + compiler_call(parent, ref, {:after_persistence, opts}) + end, each_cycle: fn -> - compiler_call(parent, ref, {:each_cycle, stale_modules, dest, timestamp, opts}) + compiler_call(parent, ref, {:each_cycle, stale_modules, dest, timestamp}) end, each_file: fn file, lexical -> compiler_call(parent, ref, {:each_file, file, lexical, verbose}) @@ -1063,8 +1066,13 @@ defmodule Mix.Compilers.Elixir do defp compiler_loop(ref, pid, state, cwd) do receive do - {^ref, {:each_cycle, stale_modules, dest, timestamp, opts}} -> - {response, state} = each_cycle(stale_modules, dest, timestamp, state, opts) + {^ref, {:after_persistence, opts}} -> + {response, state} = after_persistence(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}) compiler_loop(ref, pid, state, cwd) @@ -1092,7 +1100,17 @@ defmodule Mix.Compilers.Elixir do end end - defp each_cycle(stale_modules, compile_path, timestamp, state, opts) do + defp after_persistence(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, opts)} + + {:ok, state} + end + + defp each_cycle(stale_modules, compile_path, timestamp, state) do {modules, _exports, sources, changed, pending_modules, stale_exports, consolidation} = state {pending_modules, exports, changed} = @@ -1136,8 +1154,7 @@ defmodule Mix.Compilers.Elixir do runtime_paths = Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")}) - protocols_and_impls = maybe_consolidate(consolidation, modules, opts) - state = {modules, exports, sources, [], pending_modules, stale_exports, protocols_and_impls} + state = {modules, exports, sources, [], pending_modules, stale_exports, consolidation} {{:runtime, runtime_paths, []}, state} else Mix.Utils.compiling_n(length(changed), :ex) From 33629c1871a23a2b723554d17f08be0ae5575c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:49:08 +0100 Subject: [PATCH 08/14] Preserve compatibility --- lib/mix/lib/mix/compilers/protocol.ex | 11 ++++++++++- lib/mix/lib/mix/tasks/archive.build.ex | 2 +- lib/mix/lib/mix/tasks/compile.elixir.ex | 8 +++++++- lib/mix/lib/mix/tasks/compile.ex | 2 +- lib/mix/test/mix/tasks/compile_test.exs | 6 +++--- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/compilers/protocol.ex b/lib/mix/lib/mix/compilers/protocol.ex index 329d2f6997b..45ae3502e1d 100644 --- a/lib/mix/lib/mix/compilers/protocol.ex +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -6,9 +6,18 @@ defmodule Mix.Compilers.Protocol do ## Umbrella handling def umbrella(args, res) do + config = Mix.Project.config() + config_mtime = Mix.Project.config_mtime(config) {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, verbose: :boolean]) + + opts = + if "--no-protocol-consolidation" in args do + Keyword.put(opts, :consolidate_protocols, false) + else + Keyword.take(config, [:consolidate_protocols]) ++ opts + 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 diff --git a/lib/mix/lib/mix/tasks/archive.build.ex b/lib/mix/lib/mix/tasks/archive.build.ex index f618e873b3e..ba6951f9543 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-consolidate-protocols" | args]) + Mix.Task.run(:compile, ["--no-protocol-consolidation" | args]) end source = diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 5c283e77fde..4ef3e7df9c8 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -90,7 +90,6 @@ 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, @@ -125,6 +124,13 @@ defmodule Mix.Tasks.Compile.Elixir do |> tracers_opts(tracers) |> profile_opts() + opts = + if "--no-protocol-consolidation" in args do + Keyword.put(opts, :consolidate_protocols, false) + else + opts + end + # Having compilations racing with other is most undesired, # so we wrap the compiler in a lock. diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index 860f5bcaff8..02ab8966038 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -71,7 +71,7 @@ defmodule Mix.Tasks.Compile do default case with dependencies) * `--no-prune-code-paths` - do not prune code paths before compilation, this keeps the entirety of Erlang/OTP available when the project starts - * `--no-consolidate-protocols` - skips protocol consolidation + * `--no-protocol-consolidation` - skips protocol consolidation * `--no-validate-compile-env` - does not validate the application compile environment * `--return-errors` - returns error status and diagnostics instead of exiting on error * `--warnings-as-errors` - exit with non-zero status if compilation has one or more diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index bc7c3f14eef..85ea8a2f0af 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -224,10 +224,10 @@ defmodule Mix.Tasks.CompileTest do end) end - test "skip protocol consolidation when --no-consolidate-protocols" do + test "skip protocol consolidation when --no-protocol-consolidation" do in_fixture("no_mixfile", fn -> File.rm("_build/dev/lib/sample/.mix/compile.protocols") - assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:ok, []} + assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:ok, []} assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam") refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam") end) @@ -283,7 +283,7 @@ defmodule Mix.Tasks.CompileTest do Mix.Project.push(WrongPath) ExUnit.CaptureIO.capture_io(fn -> - assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:noop, []} + assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:noop, []} end) end From ed825c378c41a60d8868b8f9920c5e1f6e3db373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:51:50 +0100 Subject: [PATCH 09/14] Fix --- lib/mix/lib/mix/compilers/protocol.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/compilers/protocol.ex b/lib/mix/lib/mix/compilers/protocol.ex index 45ae3502e1d..bf8f153f05b 100644 --- a/lib/mix/lib/mix/compilers/protocol.ex +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -7,7 +7,6 @@ defmodule Mix.Compilers.Protocol do def umbrella(args, res) do config = Mix.Project.config() - config_mtime = Mix.Project.config_mtime(config) {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, verbose: :boolean]) opts = @@ -18,6 +17,7 @@ defmodule Mix.Compilers.Protocol do 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 From f8c60540bc5be4b1c0e95facb652ef9cc977c50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 18:58:51 +0100 Subject: [PATCH 10/14] Deprecate --no-protocol-consolidation --- lib/mix/lib/mix/compilers/protocol.ex | 7 +++++-- lib/mix/lib/mix/tasks/archive.build.ex | 2 +- lib/mix/lib/mix/tasks/compile.elixir.ex | 2 ++ lib/mix/lib/mix/tasks/compile.protocols.ex | 2 +- lib/mix/test/mix/tasks/compile_test.exs | 6 +++--- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/compilers/protocol.ex b/lib/mix/lib/mix/compilers/protocol.ex index bf8f153f05b..69c5bd748bf 100644 --- a/lib/mix/lib/mix/compilers/protocol.ex +++ b/lib/mix/lib/mix/compilers/protocol.ex @@ -5,15 +5,18 @@ defmodule Mix.Compilers.Protocol do ## Umbrella handling + @switches [force: :boolean, verbose: :boolean, consolidate_protocols: :boolean] + def umbrella(args, res) do config = Mix.Project.config() - {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, verbose: :boolean]) + {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 - Keyword.take(config, [:consolidate_protocols]) ++ opts + opts ++ Keyword.take(config, [:consolidate_protocols]) end manifest = manifest() 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/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 4ef3e7df9c8..e114f6422bb 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, @@ -126,6 +127,7 @@ defmodule Mix.Tasks.Compile.Elixir do opts = if "--no-protocol-consolidation" in args do + # TODO: Deprecate me on Elixir v1.23 Keyword.put(opts, :consolidate_protocols, false) else opts diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex index c84bd5fd778..26ba910fbca 100644 --- a/lib/mix/lib/mix/tasks/compile.protocols.ex +++ b/lib/mix/lib/mix/tasks/compile.protocols.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Compile.Protocols do @moduledoc false use Mix.Task - # TODO: Warn on Elixir v1.23 + # TODO: Deprecate me on Elixir v1.23 def run(_args) do :noop end diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 85ea8a2f0af..bc7c3f14eef 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -224,10 +224,10 @@ defmodule Mix.Tasks.CompileTest do end) end - test "skip protocol consolidation when --no-protocol-consolidation" do + test "skip protocol consolidation when --no-consolidate-protocols" do in_fixture("no_mixfile", fn -> File.rm("_build/dev/lib/sample/.mix/compile.protocols") - assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:ok, []} + assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:ok, []} assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam") refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam") end) @@ -283,7 +283,7 @@ defmodule Mix.Tasks.CompileTest do Mix.Project.push(WrongPath) ExUnit.CaptureIO.capture_io(fn -> - assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:noop, []} + assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:noop, []} end) end From 8047f3d26723f23f7c41133b42aed94d53abc707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 19:40:29 +0100 Subject: [PATCH 11/14] Down to 3 failures --- lib/mix/lib/mix.ex | 2 +- lib/mix/lib/mix/tasks/compile.elixir.ex | 2 +- .../test/mix/tasks/compile.elixir_test.exs | 1 + lib/mix/test/mix/tasks/compile_test.exs | 19 +++---------------- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 6c3bbf5cdab..5c7a6005b35 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -1077,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/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index e114f6422bb..7f542b15ab9 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -130,7 +130,7 @@ defmodule Mix.Tasks.Compile.Elixir do # TODO: Deprecate me on Elixir v1.23 Keyword.put(opts, :consolidate_protocols, false) else - opts + opts ++ Keyword.take(project, [:consolidate_protocols]) end # Having compilations racing with other is most undesired, diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 8b130b74f02..728ff4947d2 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1315,6 +1315,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, []} diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index bc7c3f14eef..df2bbb5b5e5 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -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 @@ -224,10 +211,10 @@ defmodule Mix.Tasks.CompileTest do end) end - test "skip protocol consolidation when --no-consolidate-protocols" do + test "skip protocol consolidation when --no-protocol-consolidation" do in_fixture("no_mixfile", fn -> File.rm("_build/dev/lib/sample/.mix/compile.protocols") - assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:ok, []} + assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:ok, []} assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam") refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam") end) @@ -283,7 +270,7 @@ defmodule Mix.Tasks.CompileTest do Mix.Project.push(WrongPath) ExUnit.CaptureIO.capture_io(fn -> - assert Mix.Task.run("compile", ["--no-consolidate-protocols"]) == {:noop, []} + assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:noop, []} end) end From 3a7f43b829e2f23d6e7aaf5d0cc0bc755f59ff23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 20:09:28 +0100 Subject: [PATCH 12/14] More fixes --- lib/mix/lib/mix/compilers/elixir.ex | 35 +++++++++---------- .../test/mix/tasks/compile.protocols_test.exs | 28 +++++++-------- lib/mix/test/mix/umbrella_test.exs | 2 +- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 7e7a68d116f..d0c4d56db13 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -921,26 +921,21 @@ defmodule Mix.Compilers.Elixir do protocols_and_impls, timestamp ) do - if modules == %{} and sources == %{} do - File.rm(manifest) - else - File.mkdir_p!(Path.dirname(manifest)) - - term = - {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime, - config_mtime, protocols_and_impls} + File.mkdir_p!(Path.dirname(manifest)) - manifest_data = :erlang.term_to_binary(term, [:compressed]) - File.write!(manifest, manifest_data) - File.touch!(manifest, timestamp) - delete_checkpoint(manifest) + term = + {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime, + config_mtime, protocols_and_impls} - # 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 @@ -1105,7 +1100,7 @@ defmodule Mix.Compilers.Elixir do state = {modules, exports, sources, changed, pending_modules, stale_exports, - maybe_consolidate(consolidation, modules, opts)} + maybe_consolidate(consolidation, modules, pending_modules, opts)} {:ok, state} end @@ -1321,16 +1316,18 @@ defmodule Mix.Compilers.Elixir do defp protocols_and_impls(), do: {%{}, %{}} - defp maybe_consolidate({:off, _, _}, _, _) 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, diff --git a/lib/mix/test/mix/tasks/compile.protocols_test.exs b/lib/mix/test/mix/tasks/compile.protocols_test.exs index 6e7db79ad29..93168d74e3c 100644 --- a/lib/mix/test/mix/tasks/compile.protocols_test.exs +++ b/lib/mix/test/mix/tasks/compile.protocols_test.exs @@ -19,7 +19,7 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do end """) - assert compile_elixir_and_protocols() == :ok + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") # Implement a local protocol @@ -29,21 +29,21 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do end """) - assert compile_elixir_and_protocols() == :ok + assert Mix.Tasks.Compile.Elixir.run([]) == {: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 Mix.Tasks.Compile.Elixir.run([]) == {: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 + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") end) end @@ -67,7 +67,7 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do end """) - assert compile_elixir_and_protocols() == :ok + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") File.rm!("lib/protocol.ex") @@ -77,11 +77,12 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do end """) - assert compile_elixir_and_protocols() == :noop + purge_protocol(Compile.Protocol) + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} # Delete a local protocol File.rm!("lib/protocol.ex") - assert compile_elixir_and_protocols() == :noop + assert Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") end) end @@ -95,7 +96,7 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do purge_protocol(String.Chars) mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") - assert compile_elixir_and_protocols() == :noop + assert Mix.Tasks.Compile.Elixir.run([]) == {:noop, []} assert mtime("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") == @old # Implement a deps protocol @@ -108,12 +109,12 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do end """) - assert compile_elixir_and_protocols() == :ok + assert Mix.Tasks.Compile.Elixir.run([]) == {: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 Mix.Tasks.Compile.Elixir.run([]) == {:ok, []} assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old end) end @@ -121,7 +122,7 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do 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() + Mix.Task.run("compile") # Load consolidated :code.add_patha(~c"_build/dev/lib/sample/consolidated") @@ -142,11 +143,6 @@ defmodule Mix.Tasks.Compile.ProtocolsTest do 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 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}}) From b03922ed0bfc508bd7e11af5950e84ed6e02fbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Dec 2024 20:22:58 +0100 Subject: [PATCH 13/14] Merge modules --- .../test/mix/tasks/compile.elixir_test.exs | 247 ++++++++++++++---- .../test/mix/tasks/compile.protocols_test.exs | 161 ------------ 2 files changed, 202 insertions(+), 206 deletions(-) delete mode 100644 lib/mix/test/mix/tasks/compile.protocols_test.exs diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 728ff4947d2..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) @@ -1344,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) @@ -1729,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 93168d74e3c..00000000000 --- a/lib/mix/test/mix/tasks/compile.protocols_test.exs +++ /dev/null @@ -1,161 +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 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 - - # 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 - - # 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 "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 Mix.Tasks.Compile.Elixir.run([]) == {: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 Mix.Tasks.Compile.Elixir.run([]) == {: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 Mix.Tasks.Compile.Elixir.run([]) == {: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) - 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 - - 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 From c1764c6887b39fbb05a0322b923ea268cdac2c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Dec 2024 09:40:44 +0100 Subject: [PATCH 14/14] after_compile --- lib/elixir/lib/kernel/parallel_compiler.ex | 10 ++++++---- lib/mix/lib/mix/compilers/elixir.ex | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 06c43e8ea8f..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 @@ -139,8 +143,6 @@ defmodule Kernel.ParallelCompiler do * `{:runtime, modules, warnings}` - to stop compilation and verify the list of modules because dependent modules have changed - * `:after_persistence` - ... - * `:long_compilation_threshold` - the timeout (in seconds) to check for modules taking too long to compile. For each file that exceeds the threshold, the `:each_long_compilation` callback is invoked. From Elixir v1.11, only the time @@ -285,7 +287,7 @@ defmodule Kernel.ParallelCompiler do spawn_workers(files, %{}, %{}, [], %{}, [], [], %{ beam_timestamp: Keyword.get(options, :beam_timestamp), dest: Keyword.get(options, :dest), - after_persistence: Keyword.get(options, :after_persistence, fn -> :ok end), + 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), @@ -343,7 +345,7 @@ defmodule Kernel.ParallelCompiler do defp verify_modules(result, compile_warnings, dependent_modules, state) do modules = write_module_binaries(result, state.output, state.beam_timestamp) - _ = state.after_persistence.() + _ = state.after_compile.() runtime_warnings = maybe_check_modules(result, dependent_modules, state) info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings} {{:ok, modules, info}, state} diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index d0c4d56db13..e6634320687 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1021,8 +1021,8 @@ defmodule Mix.Compilers.Elixir do pid = spawn_link(fn -> compile_opts = [ - after_persistence: fn -> - compiler_call(parent, ref, {:after_persistence, 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}) @@ -1061,8 +1061,8 @@ defmodule Mix.Compilers.Elixir do defp compiler_loop(ref, pid, state, cwd) do receive do - {^ref, {:after_persistence, opts}} -> - {response, state} = after_persistence(state, opts) + {^ref, {:after_compile, opts}} -> + {response, state} = after_compile(state, opts) send(pid, {ref, response}) compiler_loop(ref, pid, state, cwd) @@ -1095,7 +1095,7 @@ defmodule Mix.Compilers.Elixir do end end - defp after_persistence(state, opts) do + defp after_compile(state, opts) do {modules, exports, sources, changed, pending_modules, stale_exports, consolidation} = state state =