From a260b1fe97ce7d95ae1752aadd3a08114fc6671a Mon Sep 17 00:00:00 2001 From: Jechol Lee Date: Wed, 10 Dec 2025 16:15:19 +0900 Subject: [PATCH 1/2] Store initial dbg_callback to ignore runtime modifications When :elixir app starts, store the dbg_callback value in dbg_callback_initial. Mix compiler now compares against dbg_callback_initial instead of dbg_callback. This prevents unnecessary recompilation when tools like Kino modify dbg_callback at runtime. Previously, such runtime modifications would trigger a full recompilation of all files using dbg/2, even though the config hadn't actually changed. The key insight is that dbg/2 is a compile-time macro, so runtime modifications to dbg_callback don't affect already-compiled code. Only actual config changes (reflected in dbg_callback_initial) should trigger recompilation. This is a more general solution than detecting specific wrapping patterns, as it works with any tool that modifies dbg_callback at runtime. --- lib/elixir/src/elixir.erl | 10 ++++++ lib/mix/lib/mix/compilers/elixir.ex | 8 ++++- .../test/mix/tasks/compile.elixir_test.exs | 33 ++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 352c529dc61..6d274e2256b 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -64,6 +64,16 @@ start(_Type, _Args) -> application:set_env(elixir, ansi_enabled, prim_tty:isatty(stdout) == true) end, + %% Store the initial dbg_callback value before any runtime modifications. + %% This allows Mix compiler to detect config changes vs runtime changes + %% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation). + case application:get_env(elixir, dbg_callback) of + {ok, DbgCallback} -> + application:set_env(elixir, dbg_callback_initial, DbgCallback); + undefined -> + ok + end, + Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of {module, Mod} -> Mod; _ -> elixir_tokenizer diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 8a25b7aa647..3392b26bd04 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -287,7 +287,13 @@ defmodule Mix.Compilers.Elixir do end defp deps_config_compile_env_apps(deps_config) do - if deps_config[:dbg] != Application.fetch_env!(:elixir, :dbg_callback) do + # Use dbg_callback_initial instead of dbg_callback to ignore runtime modifications. + # Tools like Kino modify dbg_callback at runtime to customize dbg/2 behavior, + # but this should not trigger recompilation since the config hasn't actually changed. + # dbg_callback_initial is set when :elixir app starts, before any runtime modifications. + initial_dbg = Application.fetch_env!(:elixir, :dbg_callback_initial) + + if deps_config[:dbg] != initial_dbg do [:elixir] else [] diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index fdfccdef4b6..6341c7e7d7a 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -278,9 +278,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - # Change the dbg_callback at runtime + # Simulate a config change by updating both dbg_callback and dbg_callback_initial. + # This represents the case where the user actually changed the config file. File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) Application.put_env(:elixir, :dbg_callback, {__MODULE__, :dbg, []}) + Application.put_env(:elixir, :dbg_callback_initial, {__MODULE__, :dbg, []}) assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} @@ -289,6 +291,35 @@ defmodule Mix.Tasks.Compile.ElixirTest do end) after Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []}) + Application.put_env(:elixir, :dbg_callback_initial, {Macro, :dbg, []}) + end + + test "does not recompile when dbg_callback is modified at runtime but initial is unchanged" do + in_fixture("no_mixfile", fn -> + Mix.Project.push(MixTest.Case.Sample) + + File.write!("lib/a.ex", """ + defmodule A do + def a, do: dbg(:ok) + end + """) + + assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} + assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} + + # Simulate a tool like Kino modifying dbg_callback at runtime. + # Since dbg_callback_initial remains unchanged, this should NOT trigger recompilation. + original_dbg = Application.fetch_env!(:elixir, :dbg_callback) + Application.put_env(:elixir, :dbg_callback_initial, original_dbg) + Application.put_env(:elixir, :dbg_callback, {SomeDebugTool, :dbg, [original_dbg]}) + + # Should NOT trigger recompilation since dbg_callback_initial is unchanged + assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []} + refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} + end) + after + Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []}) + Application.put_env(:elixir, :dbg_callback_initial, {Macro, :dbg, []}) end test "recompiles files when config changes export dependencies" do From 9053e4e47a560633a245cdaeaabc9d8538550d16 Mon Sep 17 00:00:00 2001 From: Jechol Lee Date: Wed, 10 Dec 2025 17:07:28 +0900 Subject: [PATCH 2/2] Use elixir_config instead of application env for initial_dbg_callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per José's feedback, store initial_dbg_callback in elixir_config instead of using application:set_env. This is more consistent with how other Elixir internal configs are managed. --- lib/elixir/src/elixir.erl | 8 ++------ lib/mix/lib/mix/compilers/elixir.ex | 6 +++--- lib/mix/test/mix/tasks/compile.elixir_test.exs | 14 +++++++------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 6d274e2256b..169cf7c7134 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -67,12 +67,7 @@ start(_Type, _Args) -> %% Store the initial dbg_callback value before any runtime modifications. %% This allows Mix compiler to detect config changes vs runtime changes %% (e.g., Kino wrapping dbg_callback at runtime should not trigger recompilation). - case application:get_env(elixir, dbg_callback) of - {ok, DbgCallback} -> - application:set_env(elixir, dbg_callback_initial, DbgCallback); - undefined -> - ok - end, + {ok, InitialDbgCallback} = application:get_env(elixir, dbg_callback), Tokenizer = case code:ensure_loaded('Elixir.String.Tokenizer') of {module, Mod} -> Mod; @@ -100,6 +95,7 @@ start(_Type, _Args) -> {docs, true}, {ignore_already_consolidated, false}, {ignore_module_conflict, false}, + {initial_dbg_callback, InitialDbgCallback}, {infer_signatures, [elixir]}, {on_undefined_variable, raise}, {parser_options, [{columns, true}]}, diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 3392b26bd04..74e405ee27c 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -287,11 +287,11 @@ defmodule Mix.Compilers.Elixir do end defp deps_config_compile_env_apps(deps_config) do - # Use dbg_callback_initial instead of dbg_callback to ignore runtime modifications. + # Use initial_dbg_callback instead of dbg_callback to ignore runtime modifications. # Tools like Kino modify dbg_callback at runtime to customize dbg/2 behavior, # but this should not trigger recompilation since the config hasn't actually changed. - # dbg_callback_initial is set when :elixir app starts, before any runtime modifications. - initial_dbg = Application.fetch_env!(:elixir, :dbg_callback_initial) + # initial_dbg_callback is set when :elixir app starts, before any runtime modifications. + initial_dbg = :elixir_config.get(:initial_dbg_callback) if deps_config[:dbg] != initial_dbg do [:elixir] diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 6341c7e7d7a..2a36f35d394 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -278,11 +278,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - # Simulate a config change by updating both dbg_callback and dbg_callback_initial. + # Simulate a config change by updating both dbg_callback and initial_dbg_callback. # This represents the case where the user actually changed the config file. File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) Application.put_env(:elixir, :dbg_callback, {__MODULE__, :dbg, []}) - Application.put_env(:elixir, :dbg_callback_initial, {__MODULE__, :dbg, []}) + :elixir_config.put(:initial_dbg_callback, {__MODULE__, :dbg, []}) assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} @@ -291,7 +291,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do end) after Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []}) - Application.put_env(:elixir, :dbg_callback_initial, {Macro, :dbg, []}) + :elixir_config.put(:initial_dbg_callback, {Macro, :dbg, []}) end test "does not recompile when dbg_callback is modified at runtime but initial is unchanged" do @@ -308,18 +308,18 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} # Simulate a tool like Kino modifying dbg_callback at runtime. - # Since dbg_callback_initial remains unchanged, this should NOT trigger recompilation. + # Since initial_dbg_callback remains unchanged, this should NOT trigger recompilation. original_dbg = Application.fetch_env!(:elixir, :dbg_callback) - Application.put_env(:elixir, :dbg_callback_initial, original_dbg) + :elixir_config.put(:initial_dbg_callback, original_dbg) Application.put_env(:elixir, :dbg_callback, {SomeDebugTool, :dbg, [original_dbg]}) - # Should NOT trigger recompilation since dbg_callback_initial is unchanged + # Should NOT trigger recompilation since initial_dbg_callback is unchanged assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []} refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} end) after Application.put_env(:elixir, :dbg_callback, {Macro, :dbg, []}) - Application.put_env(:elixir, :dbg_callback_initial, {Macro, :dbg, []}) + :elixir_config.put(:initial_dbg_callback, {Macro, :dbg, []}) end test "recompiles files when config changes export dependencies" do