From 7cd93bc7b80312984cedcb6920fbddb21fa45fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 12 Mar 2026 23:18:49 +0100 Subject: [PATCH 1/2] Fix compile env change triggering full recompilation of path dependencies --- lib/mix/lib/mix/dep.ex | 23 +++++++++++++++---- lib/mix/lib/mix/dep/loader.ex | 2 +- lib/mix/lib/mix/tasks/compile.app.ex | 11 +++++---- lib/mix/lib/mix/tasks/deps.compile.ex | 2 +- .../custom/raw_repo/lib/raw_repo.ex | 2 +- lib/mix/test/mix/tasks/deps_test.exs | 17 ++++++++++++-- 6 files changed, 43 insertions(+), 14 deletions(-) diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex index 96acd4e4fcb..f7b698bdecb 100644 --- a/lib/mix/lib/mix/dep.ex +++ b/lib/mix/lib/mix/dep.ex @@ -338,6 +338,10 @@ defmodule Mix.Dep do "the dependency build is outdated, please run \"#{mix_env_var()}mix deps.compile\"" end + def format_status(%Mix.Dep{status: :envoutdated}) do + "the dependency compile environment is outdated, please run \"#{mix_env_var()}mix deps.compile\"" + end + def format_status(%Mix.Dep{app: app, status: {:divergedreq, vsn, other}} = dep) do "the dependency #{app} #{vsn}\n" <> dep_status(dep) <> @@ -505,11 +509,20 @@ defmodule Mix.Dep do @doc """ Returns `true` if the dependency is compilable. """ - def compilable?(%Mix.Dep{status: {:vsnlock, _}}), do: true - def compilable?(%Mix.Dep{status: {:noappfile, {_, _}}}), do: true - def compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true - def compilable?(%Mix.Dep{status: :compile}), do: true - def compilable?(_), do: false + def compilable?(%Mix.Dep{status: :envoutdated}), do: true + def compilable?(dep), do: force_compilable?(dep) + + @doc """ + Returns `true` if the dependency is force compilable. + + This is a subset of compilable. This is used in `deps.compile` to + clean the build path before compiling. + """ + def force_compilable?(%Mix.Dep{status: {:vsnlock, _}}), do: true + def force_compilable?(%Mix.Dep{status: {:noappfile, {_, _}}}), do: true + def force_compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true + def force_compilable?(%Mix.Dep{status: :compile}), do: true + def force_compilable?(_), do: false @doc """ Formats a dependency for printing. diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 6b34b32fb2a..7b576a52842 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -444,7 +444,7 @@ defmodule Mix.Dep.Loader do defp compile_env_status(vsn, properties) do with [_ | _] = compile_env <- properties[:compile_env], false <- Config.Provider.valid_compile_env?(compile_env) do - :compile + :envoutdated else _ -> {:ok, vsn, properties} end diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 39197313082..8a4b99d617d 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -161,10 +161,13 @@ defmodule Mix.Tasks.Compile.App do |> max(Mix.Utils.last_modified(compile_path)) current_properties = current_app_properties(target) + current_compile_env = Mix.ProjectStack.compile_env(nil) {changed?, modules} = cond do - opts[:force] || new_mtime > Mix.Utils.last_modified(target) -> + opts[:force] || new_mtime > Mix.Utils.last_modified(target) || + (current_compile_env != nil and + current_compile_env != Keyword.get(current_properties, :compile_env, [])) -> {true, nil} Keyword.get(config, :reliable_dir_mtime, fn -> not match?({:win32, _}, :os.type()) end) -> @@ -184,7 +187,7 @@ defmodule Mix.Tasks.Compile.App do ] |> merge_project_application(project) |> handle_extra_applications(config) - |> add_compile_env(current_properties) + |> add_compile_env(current_compile_env, current_properties) |> add_modules(modules, compile_path) contents = @@ -389,12 +392,12 @@ defmodule Mix.Tasks.Compile.App do defp typed_app?({app, type}) when is_atom(app) and type in [:required, :optional], do: true defp typed_app?(_), do: false - defp add_compile_env(properties, current_properties) do + defp add_compile_env(properties, current_compile_env, current_properties) do # If someone calls compile.elixir and then compile.app across two # separate OS calls, then the compile_env won't be properly reflected. # This is ok because compile_env is not used for correctness. It is # simply to catch possible errors early. - case Mix.ProjectStack.compile_env(nil) do + case current_compile_env do nil -> Keyword.take(current_properties, [:compile_env]) ++ properties [] -> properties compile_env -> Keyword.put(properties, :compile_env, compile_env) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 6ba31899d0b..0e0f7ab5116 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -125,7 +125,7 @@ defmodule Mix.Tasks.Deps.Compile do # If a dependency was marked as fetched or with an out of date lock # or missing the app file, we always compile it from scratch. - if force? or Mix.Dep.compilable?(dep) do + if force? or Mix.Dep.force_compilable?(dep) do File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)])) end diff --git a/lib/mix/test/fixtures/deps_status/custom/raw_repo/lib/raw_repo.ex b/lib/mix/test/fixtures/deps_status/custom/raw_repo/lib/raw_repo.ex index b893d48bc67..08e5484b892 100644 --- a/lib/mix/test/fixtures/deps_status/custom/raw_repo/lib/raw_repo.ex +++ b/lib/mix/test/fixtures/deps_status/custom/raw_repo/lib/raw_repo.ex @@ -1,4 +1,4 @@ -Application.compile_env(:raw_repo, :compile_env) +Application.compile_env(:anyapp, :anything) defmodule RawRepo do def hello do diff --git a/lib/mix/test/mix/tasks/deps_test.exs b/lib/mix/test/mix/tasks/deps_test.exs index 6e9c3214408..bd0dfebcdc1 100644 --- a/lib/mix/test/mix/tasks/deps_test.exs +++ b/lib/mix/test/mix/tasks/deps_test.exs @@ -837,8 +837,16 @@ defmodule Mix.Tasks.DepsTest do test "checks if compile env changed" do in_fixture("deps_status", fn -> + # Write another file, it should not be recompiled upon compiled env change. + File.write!("custom/raw_repo/lib/foo.ex", """ + defmodule RawRepo.Foo do + end + """) + Mix.Project.push(RawRepoDepApp) Mix.Tasks.Deps.Loadpaths.run([]) + # 2 files + assert_receive {:mix_shell, :info, ["Compiling 2 files (.ex)"]} assert_receive {:mix_shell, :info, ["Generated raw_repo app"]} assert Application.spec(:raw_repo, :vsn) @@ -846,7 +854,7 @@ defmodule Mix.Tasks.DepsTest do File.write!("config/config.exs", """ import Config - config :raw_repo, :compile_env, :new_value + config :anyapp, :anything, :anyvalue """) Application.unload(:raw_repo) @@ -859,10 +867,15 @@ defmodule Mix.Tasks.DepsTest do Mix.Tasks.Deps.run([]) assert_receive {:mix_shell, :info, - [" the dependency build is outdated, please run \"mix deps.compile\""]} + [ + " the dependency compile environment is outdated, please run \"mix deps.compile\"" + ]} + + Mix.shell().flush() Mix.Tasks.Deps.Loadpaths.run([]) + assert_receive {:mix_shell, :info, ["Compiling 1 file (.ex)"]} assert_receive {:mix_shell, :info, ["Generated raw_repo app"]} assert Application.spec(:raw_repo, :vsn) end) From 6fa806e252a52dbf374e86f9f3fdcdd1b1fc5867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 12 Mar 2026 23:35:35 +0100 Subject: [PATCH 2/2] Apply suggestion from @josevalim --- lib/mix/test/mix/tasks/deps_test.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mix/test/mix/tasks/deps_test.exs b/lib/mix/test/mix/tasks/deps_test.exs index bd0dfebcdc1..f09a00bc389 100644 --- a/lib/mix/test/mix/tasks/deps_test.exs +++ b/lib/mix/test/mix/tasks/deps_test.exs @@ -845,7 +845,6 @@ defmodule Mix.Tasks.DepsTest do Mix.Project.push(RawRepoDepApp) Mix.Tasks.Deps.Loadpaths.run([]) - # 2 files assert_receive {:mix_shell, :info, ["Compiling 2 files (.ex)"]} assert_receive {:mix_shell, :info, ["Generated raw_repo app"]} assert Application.spec(:raw_repo, :vsn)