defmodule Mix.Tasks.Monorepo.Changes do @moduledoc """ Shows the known changes that should lead to recompilation of a application. A change is detected by looking at `elixirc_paths` in each project, finding all elixir files matching and then lstating them. The results is compared to the values in `./.mix/monorepo.changes` (for each build). If any of the files changed then it's shown. The manifests for each compiler in the project can help in giving a dependency graph which can be used to decide which deps and apps needs recompilation. """ @shortdoc "Show all the changes in monorepo, like `git status`" use Mix.Task @impl true def run([]), do: run([File.cwd!()]) def run([path]) do repo = ExMonorepo.parse_repofile(List.first(ExMonorepo.repofiles(path))) applications = ExMonorepo.SCM.scan(exmonorepo: repo) projects = Enum.filter(scan_for_changes(repo), fn {_, changes} -> [] != changes end) IO.puts("Found #{length(projects)} applications with changes. ") for {app, [_ | _] = changes} <- projects do IO.puts("\n :#{app} (path=#{Path.relative_to(applications[app], Path.expand(path))})") for file <- changes do IO.puts(" - #{Path.relative_to(file, Path.expand(path))}") end end :ok end @doc """ Retrieve a keyword list of applications in monorepo with changes. """ def scan_for_changes(repo) do applications = ExMonorepo.SCM.scan(exmonorepo: repo) # Get a map of app => {module, project} for {app, path} <- applications do Mix.Project.in_project(app, path, fn module -> project = module.project() # `["lib"]` is default as of elixir 1.15 elixirc_paths = project[:elixirc_paths] || ["lib"] pattern = "{" <> Enum.join(elixirc_paths, ",") <> "}" sources = Path.wildcard(Path.join([path, pattern, "**/*.ex"])) all_manifests = manifests(project) # Checking manifests mtime. Assume the oldest mtime is the last # successfull compilation. This may not be correct, the compiler # may not have the manifest because it has not run or it may not # exist since there are no source files for the compiler. mtime = Enum.flat_map(all_manifests, &List.wrap(get_mtime(&1))) oldest = List.first(Enum.sort(mtime, DateTime)) changes = if nil == oldest do sources else Enum.filter(sources, fn file -> mtime = get_mtime(file) nil == mtime or :gt != DateTime.compare(oldest, mtime) end) end {app, changes} end) end end defp get_mtime(file) do case File.lstat(file) do {:ok, %{mtime: mtime}} -> mtime = mtime |> :calendar.datetime_to_gregorian_seconds() |> DateTime.from_gregorian_seconds() mtime {:error, _} -> nil end end defp manifests(project) do compilers = Mix.Tasks.Compile.compilers(project) Enum.flat_map(compilers, fn compiler -> module = Mix.Task.get("compile.#{compiler}") if module && function_exported?(module, :manifests, 0) do module.manifests else [] end end) end end