defmodule ExMonorepo do
@moduledoc """
Enable searching a directory for nested set of dependencies.
These are free form and coverted to a local path instead of using
hex, git or similar to fetch it. The monorepo may optionally export
all dependencies to archives using the command `mix monorepo.export`.
## Example
defmodule TestProject.MixProject do
use Mix.Project
use ExMonorepo
end
## Detecting monorepo
A monorepo should have the file repo.exs to define it's configuration (if any).
the content is a set of variables like `root`, `dist` which will be set in
the project options.
## Using archives
If `dist` option is used all the non-monorepo dependencies
are exported to a `.ez` file tagged with app, version and checksum.
When starting a mix command all the archives are added to path and
may be used without recompiling.
If the archive is deleted the dependency will revert to normal
dependency handling.
"""
defmacro __using__(_opts) do
mod = __MODULE__
quote do
repo = List.first(unquote(mod).repofiles(__DIR__))
cond do
nil == repo ->
raise RuntimeError,
"fatal: not a monorepo, can't find repo.exs (or any parent up to mount point /)"
nil != (binding = :persistent_term.get(ExMonorepo, nil)) ->
IO.puts(
" => Using cached config from #{binding[:source]}:\n#{inspect(binding, pretty: true)}\n\n"
)
Mix.SCM.prepend(ExMonorepo.SCM)
true ->
binding = unquote(mod).parse_repofile(repo)
IO.puts(" => Found config #{repo}: #{inspect(binding, pretty: true)}\n\n")
:persistent_term.put(ExMonorepo, binding)
Mix.SCM.prepend(ExMonorepo.SCM)
end
end
end
@doc """
Get all possible repofiles from `path` to filesystem root (`/`)
"""
def repofiles(path) do
Path.split(Path.expand(path))
|> Enum.reduce(["/monorepo.exs"], fn p, [t | acc] ->
[Path.join([Path.dirname(t), p, "monorepo.exs"]), t | acc]
end)
|> Enum.uniq()
|> Enum.filter(&File.exists?/1)
end
@doc """
Extract values from a repofile
"""
def parse_repofile(file) do
{_, binding} = Code.eval_file(file)
binding =
Enum.map(binding, fn e ->
unquote(__MODULE__).map_config_item(e, Path.dirname(file))
end)
Keyword.put(binding, :source, file)
end
@doc """
Expand common options
Takes some variables supporting relative path and expands them according
to the directory (typically the dirname of monorepo.exs)
"""
def map_config_item(e, cwd \\ File.cwd!())
def map_config_item({:repo, path}, cwd), do: {:repo, Path.expand(path, cwd)}
def map_config_item({:dist, path}, cwd), do: {:dist, Path.expand(path, cwd)}
def map_config_item(e, _cwd), do: e
end