From be3dbdf5390de790919e20290b070d604d40da90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 29 Mar 2023 12:54:51 +0200 Subject: Ensure changing both local and remote deps trigger runtime verification --- lib/mix/lib/mix/compilers/elixir.ex | 59 ++++++++++++++++++++++++------------- lib/mix/lib/mix/utils.ex | 1 + lib/mix/test/mix/umbrella_test.exs | 26 ++++++++++++++++ 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 93d9836e2..33160a347 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 15 + @manifest_vsn 16 @checkpoint_vsn 2 import Record @@ -62,14 +62,19 @@ defmodule Mix.Compilers.Elixir do [] end + local_deps = Enum.reject(Mix.Dep.cached(), & &1.scm.fetchable?) + # If mix.exs has changed, recompile anything that calls Mix.Project. stale = if Mix.Utils.stale?([Mix.Project.project_file()], [modified]), do: [Mix.Project | stale], else: stale - # If the dependencies have changed, we need to traverse lock/config files. - deps_changed? = Mix.Utils.stale?([Mix.Project.config_mtime()], [modified]) + # If the lock has changed or a local dependency was added ore removed, + # we need to traverse lock/config files. + deps_changed? = + Mix.Utils.stale?([Mix.Project.config_mtime()], [modified]) or + local_deps_changed?(old_deps_config, local_deps) # If a configuration is only accessed at compile-time, we don't need to # track modules, only the compile env. So far this is only true for Elixir's @@ -79,11 +84,12 @@ defmodule Mix.Compilers.Elixir do {force?, stale, new_deps_config} = cond do !!opts[:force] or is_nil(old_deps_config) or old_cache_key != new_cache_key -> - {true, stale, deps_config()} + {true, stale, deps_config(local_deps)} deps_changed? or compile_env_apps != [] -> - new_deps_config = deps_config() - config_apps = merge_appset(old_deps_config.config, new_deps_config.config, []) + new_deps_config = deps_config(local_deps) + local_apps = merge_appset(old_deps_config.local, new_deps_config.local, []) + config_apps = merge_appset(old_deps_config.config, new_deps_config.config, local_apps) apps = merge_appset(old_deps_config.lock, new_deps_config.lock, config_apps) if Mix.Project.config()[:app] in apps do @@ -121,7 +127,7 @@ defmodule Mix.Compilers.Elixir do end {stale_modules, stale_exports, all_local_exports} = - stale_local_deps(manifest, stale, modified, all_local_exports) + stale_local_deps(local_deps, manifest, stale, modified, all_local_exports) prev_paths = for source(source: source) <- all_sources, do: source removed = prev_paths -- all_paths @@ -150,7 +156,7 @@ defmodule Mix.Compilers.Elixir do {sources, removed_modules} = update_stale_sources(sources, stale, removed_modules, sources_stats) - if stale != [] do + if stale != [] or stale_modules != %{} do path = opts[:purge_consolidation_path_if_stale] if is_binary(path) and Code.delete_path(path) do @@ -165,7 +171,7 @@ defmodule Mix.Compilers.Elixir do try do state = {[], exports, sources, modules, removed_modules} - compiler_loop(stale, dest, timestamp, opts, state, digester) + compiler_loop(stale, stale_modules, dest, timestamp, opts, state, digester) else {:ok, info, state} -> {modules, _exports, sources, pending_modules, _pending_exports} = state @@ -240,15 +246,20 @@ defmodule Mix.Compilers.Elixir do end end - defp deps_config do + defp deps_config(local_deps) do # If you change this config, you need to bump @manifest_vsn %{ + local: Enum.sort(Enum.map(local_deps, &{&1.app, true})), lock: Enum.sort(Mix.Dep.Lock.read()), config: Enum.sort(Mix.Tasks.Loadconfig.read_compile()), dbg: Application.fetch_env!(:elixir, :dbg_callback) } end + defp local_deps_changed?(deps_config, local_deps) do + is_map(deps_config) and Enum.sort(Enum.map(local_deps, &{&1.app, true})) != deps_config.local + end + defp deps_config_compile_env_apps(deps_config) do if deps_config[:dbg] != Application.fetch_env!(:elixir, :dbg_callback) do [:elixir] @@ -556,15 +567,14 @@ defmodule Mix.Compilers.Elixir do Enum.any?(enumerable, &Map.has_key?(map, &1)) end - defp stale_local_deps(manifest, stale_modules, modified, old_exports) do + defp stale_local_deps(local_deps, manifest, stale_modules, modified, old_exports) do base = Path.basename(manifest) # The stale modules so far will become both stale_modules and stale_exports, # as any export from a dependency needs to be recompiled. stale_modules = Map.from_keys(stale_modules, true) - for %{scm: scm, opts: opts} = dep <- Mix.Dep.cached(), - not scm.fetchable?, + for %{opts: opts} = dep <- local_deps, manifest = Path.join([opts[:build], ".mix", base]), Mix.Utils.last_modified(manifest) > modified, reduce: {stale_modules, stale_modules, old_exports} do @@ -802,7 +812,7 @@ defmodule Mix.Compilers.Elixir do {@manifest_vsn, modules, sources, local_exports, parent, cache_key, deps_config} -> {modules, sources, local_exports, parent, cache_key, deps_config} - # {vsn, modules, sources, ...} v5-v14 + # {vsn, modules, sources, ...} v5-v15 manifest when is_tuple(manifest) and is_integer(elem(manifest, 0)) -> purge_old_manifest(compile_path, elem(manifest, 1)) @@ -916,7 +926,7 @@ defmodule Mix.Compilers.Elixir do ## Compiler loop # The compiler is invoked in a separate process so we avoid blocking its main loop. - defp compiler_loop(stale, dest, timestamp, opts, state, digester) do + defp compiler_loop(stale, stale_modules, dest, timestamp, opts, state, digester) do ref = make_ref() parent = self() threshold = opts[:long_compilation_threshold] || 10 @@ -926,7 +936,9 @@ defmodule Mix.Compilers.Elixir do pid = spawn_link(fn -> compile_opts = [ - each_cycle: fn -> compiler_call(parent, ref, {:each_cycle, dest, timestamp}) end, + each_cycle: fn -> + 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}) end, @@ -961,8 +973,8 @@ defmodule Mix.Compilers.Elixir do defp compiler_loop(ref, pid, state, digester, cwd) do receive do - {^ref, {:each_cycle, dest, timestamp}} -> - {response, state} = each_cycle(dest, timestamp, state) + {^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, digester, cwd) @@ -990,7 +1002,7 @@ defmodule Mix.Compilers.Elixir do end end - defp each_cycle(compile_path, timestamp, state) do + defp each_cycle(stale_modules, compile_path, timestamp, state) do {modules, _exports, sources, pending_modules, pending_exports} = state {pending_modules, exports, changed} = @@ -1003,8 +1015,13 @@ defmodule Mix.Compilers.Elixir do end if changed == [] do - modules_set = modules |> Enum.map(&module(&1, :module)) |> Map.from_keys(true) - {_, runtime_modules, sources} = fixpoint_runtime_modules(sources, modules_set) + stale_modules = + modules + |> Enum.map(&module(&1, :module)) + |> Map.from_keys(true) + |> Map.merge(stale_modules) + + {_, runtime_modules, sources} = fixpoint_runtime_modules(sources, stale_modules) runtime_paths = Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")}) diff --git a/lib/mix/lib/mix/utils.ex b/lib/mix/lib/mix/utils.ex index d5c7714cc..e71c0eb39 100644 --- a/lib/mix/lib/mix/utils.ex +++ b/lib/mix/lib/mix/utils.ex @@ -225,6 +225,7 @@ defmodule Mix.Utils do @doc """ Prints n files are being compiled with the given extension. """ + def compiling_n(0, _ext), do: :ok def compiling_n(1, ext), do: Mix.shell().info("Compiling 1 file (.#{ext})") def compiling_n(n, ext), do: Mix.shell().info("Compiling #{n} files (.#{ext})") diff --git a/lib/mix/test/mix/umbrella_test.exs b/lib/mix/test/mix/umbrella_test.exs index d8b583024..1639b97b6 100644 --- a/lib/mix/test/mix/umbrella_test.exs +++ b/lib/mix/test/mix/umbrella_test.exs @@ -488,6 +488,32 @@ defmodule Mix.UmbrellaTest do end) end + test "reverifies when path dependency is added" do + in_fixture("umbrella_dep/deps/umbrella/apps", fn -> + Mix.Project.in_project(:bar, "bar", [deps: []], fn _ -> + File.write!("lib/bar.ex", """ + defmodule Bar do + def foo, do: Foo.foo() + end + """) + + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + Mix.Task.run("compile", ["--verbose"]) + end) =~ "Foo.foo/0 is undefined" + + refute Code.ensure_loaded?(Foo) + assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]} + end) + + Mix.Task.clear() + + Mix.Project.in_project(:bar, "bar", fn _ -> + Mix.Task.run("deps.compile") + assert Mix.Task.run("compile", ["--verbose", "--all-warnings"]) == {:ok, []} + end) + end) + end + test "reloads app in app cache if .app changes" do in_fixture("umbrella_dep/deps/umbrella/apps", fn -> deps = [{:foo, in_umbrella: true}] -- cgit v1.2.1