diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 5ad6b97dd8e..ba49f7f4ebf 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -55,7 +55,7 @@ defmodule Mix.Task.Compiler do * `:app` - app which modules have been compiled. - * `:build_scm` - the SCM module of the compiled project. + * `:scm` - the SCM module of the compiled project. * `:modules_diff` - information about the compiled modules. The value is a map with keys: `:added`, `:changed`, `:removed`, @@ -68,6 +68,21 @@ defmodule Mix.Task.Compiler do with `System.pid/0` to determine if compilation happened in the same OS process as the listener. + * `{:dep_compiled, info}` - delivered after a dependency is compiled. + `info` is a map with the following keys: + + * `:app` - the dependency app. + + * `:scm` - the SCM module of the dependency. + + * `:manager` - the dependency project management, possible values: + `:rebar3`, `:mix`, `:make`, `nil`. + + * `:os_pid` - the operating system PID of the process that run + the compilation. The value is a string and it can be compared + with `System.pid/0` to determine if compilation happened in + the same OS process as the listener. + Note that the listener starts before any of the project apps are started. """ @@ -222,7 +237,7 @@ defmodule Mix.Task.Compiler do lazy_message = fn -> info = %{ app: config[:app], - build_scm: config[:build_scm], + scm: config[:build_scm], modules_diff: lazy_modules_diff.(), os_pid: System.pid() } diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 89bed61743e..3049f0670eb 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -106,6 +106,23 @@ defmodule Mix.Tasks.Deps.Compile do false end + if compiled? do + build_path = Mix.Project.build_path(config) + + lazy_message = fn -> + info = %{ + app: dep.app, + scm: dep.scm, + manager: dep.manager, + os_pid: System.pid() + } + + {:dep_compiled, info} + end + + Mix.Sync.PubSub.broadcast(build_path, lazy_message) + end + # We should touch fetchable dependencies even if they # did not compile otherwise they will always be marked # as stale, even when there is nothing to do. diff --git a/lib/mix/lib/mix/tasks/deps.loadpaths.ex b/lib/mix/lib/mix/tasks/deps.loadpaths.ex index 03fa0093d4e..1f6566216a9 100644 --- a/lib/mix/lib/mix/tasks/deps.loadpaths.ex +++ b/lib/mix/lib/mix/tasks/deps.loadpaths.ex @@ -29,6 +29,19 @@ defmodule Mix.Tasks.Deps.Loadpaths do @impl true def run(args) do + # Note that we need to ensure the dependencies are compiled first, + # before we can start the pub/sub listeners, since those come from + # the dependencies. Theoretically, between compiling dependencies + # and starting the listeners, there may be a concurrent compilation + # of the dependencies, which we would miss, and we would already + # have modules from our compilation loaded. To avoid this race + # condition we start the pub/sub beforehand and we accumulate all + # events until the listeners are started. Alternatively we could + # use a lock around compilation and sterning the listeners, however + # the added benefit of the current approach is that we consistently + # receive events for all dependency compilations. Also, if we ever + # decide to start the listeners later (e.g. after loadspaths), the + # accumulation approach still works. Mix.PubSub.start() if "--no-archives-check" not in args do diff --git a/lib/mix/test/fixtures/compile_listeners/deps/reloader/lib/reloader.ex b/lib/mix/test/fixtures/compile_listeners/deps/reloader/lib/reloader.ex index bb4027bfffe..6584dc5c772 100644 --- a/lib/mix/test/fixtures/compile_listeners/deps/reloader/lib/reloader.ex +++ b/lib/mix/test/fixtures/compile_listeners/deps/reloader/lib/reloader.ex @@ -15,7 +15,7 @@ defmodule Reloader do %{ modules_diff: %{added: added, changed: changed, removed: removed, timestamp: _timestamp}, app: app, - build_scm: build_scm, + scm: scm, os_pid: os_pid } = info @@ -23,7 +23,26 @@ defmodule Reloader do Received :modules_compiled with added: #{inspect(Enum.sort(added))}, changed: #{inspect(Enum.sort(changed))}, removed: #{inspect(Enum.sort(removed))} app: #{inspect(app)} - build_scm: #{inspect(build_scm)} + scm: #{inspect(scm)} + os_pid: #{inspect(os_pid)} + """) + + {:noreply, state} + end + + def handle_info({:dep_compiled, info}, state) do + %{ + app: app, + scm: scm, + manager: manager, + os_pid: os_pid + } = info + + IO.write(""" + Received :dep_compiled with + app: #{inspect(app)} + scm: #{inspect(scm)} + manager: #{inspect(manager)} os_pid: #{inspect(os_pid)} """) diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 4696196227e..5571f05f010 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -439,13 +439,13 @@ defmodule Mix.Tasks.CompileTest do assert_receive {^port, {:data, "ok\n"}}, timeout send(parent, :mix_started) - assert_receive {^port, {:data, output_erl}}, timeout - assert_receive {^port, {:data, output_ex}}, timeout - send(parent, {:output, output_erl <> output_ex}) + send_port_outputs_to(port, parent) end) assert_receive :mix_started, timeout + # Project compilation + output = mix(["do", "compile", "+", "eval", "IO.write System.pid"]) os_pid = output |> String.split("\n") |> List.last() @@ -455,12 +455,17 @@ defmodule Mix.Tasks.CompileTest do Received :modules_compiled with added: [:a, :b, :c], changed: [], removed: [] app: :with_reloader - build_scm: Mix.SCM.Path + scm: Mix.SCM.Path os_pid: "#{os_pid}" + """ + + assert_receive {:output, output}, timeout + + assert output == """ Received :modules_compiled with added: [A, B, C], changed: [], removed: [] app: :with_reloader - build_scm: Mix.SCM.Path + scm: Mix.SCM.Path os_pid: "#{os_pid}" """ @@ -475,26 +480,7 @@ defmodule Mix.Tasks.CompileTest do File.write!("lib/d.ex", "defmodule D do end") File.write!("src/d.erl", "-module(d).") - spawn_link(fn -> - port = - mix_port([ - "run", - "--no-halt", - "--no-compile", - "--no-start", - "--eval", - ~s/IO.puts("ok"); IO.gets(""); System.halt()/ - ]) - - assert_receive {^port, {:data, "ok\n"}}, timeout - send(parent, :mix_started) - - assert_receive {^port, {:data, output_erl}}, timeout - assert_receive {^port, {:data, output_ex}}, timeout - send(parent, {:output, output_erl <> output_ex}) - end) - - assert_receive :mix_started, timeout + # Project recompilation output = mix(["do", "compile", "+", "eval", "IO.write System.pid"]) os_pid = output |> String.split("\n") |> List.last() @@ -505,14 +491,52 @@ defmodule Mix.Tasks.CompileTest do Received :modules_compiled with added: [:d], changed: [:a], removed: [:b] app: :with_reloader - build_scm: Mix.SCM.Path + scm: Mix.SCM.Path os_pid: "#{os_pid}" + """ + + assert_receive {:output, output}, timeout + + assert output == """ Received :modules_compiled with added: [D], changed: [A], removed: [B] app: :with_reloader - build_scm: Mix.SCM.Path + scm: Mix.SCM.Path + os_pid: "#{os_pid}" + """ + + # Dependency recompilation + + output = mix(["do", "deps.compile", "--force", "+", "eval", "IO.write System.pid"]) + os_pid = output |> String.split("\n") |> List.last() + + assert_receive {:output, output}, timeout + + assert output == """ + Received :modules_compiled with + added: [Reloader], changed: [], removed: [] + app: :reloader + scm: Mix.SCM.Path + os_pid: "#{os_pid}" + """ + + assert_receive {:output, output}, timeout + + assert output == """ + Received :dep_compiled with + app: :reloader + scm: Mix.SCM.Path + manager: :mix os_pid: "#{os_pid}" """ end) end + + defp send_port_outputs_to(port, pid) do + receive do + {^port, {:data, output}} -> + send(pid, {:output, output}) + send_port_outputs_to(port, pid) + end + end end