Current section

Files

Jump to
mob_dev lib mix tasks mob.plugins.ex
Raw

lib/mix/tasks/mob.plugins.ex

defmodule Mix.Tasks.Mob.Plugins do
use Mix.Task
@shortdoc "List installed Mob plugins and their activation status"
@moduledoc """
Lists the Mob plugins this project depends on, with tier, hot-push status,
and whether each is activated.
mix mob.plugins
A dependency is shown as a plugin if it ships a `priv/mob_plugin.exs`
manifest, or if it is named in `config :mob, :plugins` in `mob.exs`.
Tier-0 plugins (pure Elixir, no manifest) are indistinguishable from
ordinary libraries until activated, so they appear only once listed in
`config :mob, :plugins`.
Activation is two-step by design (see `MOB_PLUGINS.md`): adding a plugin to
`deps` makes it *installed*; adding it to `config :mob, :plugins` makes it
*activated* — only then are its contributions merged into the build.
"""
alias MobDev.Plugin.{Manifest, Report, Validator}
@impl Mix.Task
def run(_args) do
Mix.Task.run("loadpaths")
deps = load_all_manifests()
activated = activated_plugins()
dep_dirs = Mix.Project.deps_paths()
deps
|> Report.rows(activated)
|> Report.with_vetting(dep_dirs)
|> Report.render()
|> then(&IO.puts("\n" <> &1 <> "\n"))
report_conflicts(deps, activated)
end
defp report_conflicts(deps, activated) do
activated_manifests = for {name, manifest} <- deps, name in activated, do: {name, manifest}
case Validator.cross_validate(activated_manifests) do
%{errors: []} ->
:ok
%{errors: errors} ->
Mix.shell().error("Plugin activation conflicts:")
Enum.each(errors, &Mix.shell().error(" ✗ #{&1}"))
IO.puts("")
end
end
defp load_all_manifests do
Mix.Project.deps_paths()
|> Enum.map(fn {app, path} ->
case Manifest.load(path) do
{:ok, manifest} ->
{app, manifest}
{:error, reason} ->
Mix.shell().info([:yellow, "[mob.plugins] skipping #{app}: #{reason}", :reset])
{app, nil}
end
end)
end
# Reads `config :mob, :plugins` from mob.exs (the build config). Falls back
# to the loaded Application env, then an empty list.
defp activated_plugins do
config_file = Path.join(File.cwd!(), "mob.exs")
raw =
if File.exists?(config_file) do
config_file
|> Config.Reader.read!()
|> Keyword.get(:mob, [])
|> Keyword.get(:plugins, [])
else
Application.get_env(:mob, :plugins, [])
end
normalize_activated(raw)
rescue
_ -> normalize_activated(Application.get_env(:mob, :plugins, []))
end
@doc false
# Pure kernel: coerces a `config :mob, :plugins` value into a clean list of
# atom plugin names. A misconfigured value (a non-list, or a list carrying
# non-atom entries like a stray string typo `"mob_haptic"`) must not crash
# `mix mob.plugins` — `name in activated` raises Protocol.UndefinedError on a
# non-list, and downstream `name in activated` silently mismatches string
# entries. Non-list → `[]`; lists are filtered down to their atom entries.
@spec normalize_activated(term()) :: [atom()]
def normalize_activated(plugins) when is_list(plugins),
do: Enum.filter(plugins, &is_atom/1)
def normalize_activated(_other), do: []
end