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