Packages

Compile-time selective compilation and transparent distributed execution for Erlang/Elixir clusters

Current section

Files

Jump to
nebula_api lib mix tasks compile.nebula.ex
Raw

lib/mix/tasks/compile.nebula.ex

defmodule Mix.Tasks.Compile.Nebula do
@moduledoc """
Mix compiler that guards against forgetting `nebula_api_server/0`.
Opt in from a consuming app's `mix.exs`:
def project do
[
# ...
compilers: Mix.compilers() ++ [:nebula]
]
end
It runs after `:app` (so every module is compiled and its `.beam` written), reads the
persisted NebulaAPI attributes straight from the beams, and fails compilation when the
app has modules with local methods on this node but no `nebula_api_server()` wired into
its supervision tree — i.e. workers that would never start. This mirrors the compile
errors NebulaAPI already raises (e.g. a `defapi` for an unknown node).
"""
use Mix.Task.Compiler
# In umbrella projects, run once per child app (in each app's own context), like the
# built-in :elixir compiler — so `mix compile` from the umbrella root checks each app.
@recursive true
@impl true
def run(_argv) do
# No app in the current project (e.g. the umbrella root) → nothing of ours to check;
# the verification runs in each child app's own context, where `:app` is set.
case Mix.Project.config()[:app] do
nil ->
{:noop, []}
app ->
case NebulaAPI.CompilerCheck.verify(modules_attrs()) do
:ok ->
{:noop, []}
{:error, local_modules} ->
message = error_message(app, local_modules)
# Print for humans (Mix doesn't render compiler diagnostics on its own) and
# return the structured diagnostic for editors; :error status fails the build.
Mix.shell().error(message)
{:error, [diagnostic(message)]}
end
end
end
defp error_message(app, modules) do
app_module = application_module(app)
app_label = " Application: "
app_line =
if app_module do
caret_indent = String.duplicate(" ", String.length(app_label))
app_label <>
inspect(app_module) <>
"\n" <>
caret_indent <> "^------ hint: add nebula_api_server() to its supervisor's children"
else
app_label <> "(unknown)"
end
"""
Found #{length(modules)} module(s) using NebulaAPI with local methods in app #{inspect(app)}, \
but no nebula_api_server() has been found in #{inspect(app)}'s supervisor — their RPC workers \
will never start.
App: #{inspect(app)}
#{app_line}
Modules using NebulaAPI (with local methods on this node):
#{Enum.map_join(modules, "\n", &" - #{inspect(&1)}")}
"""
end
# The app's `mod:` from its `.app` file → the Application module where the supervisor
# (and thus nebula_api_server()) lives. The supervisor's registered name is a runtime
# arg, so it isn't knowable here; the Application module is, and it's the actionable spot.
defp application_module(app) do
app_file = Path.join(Mix.Project.compile_path(), "#{app}.app")
with {:ok, [{:application, ^app, props}]} <- :file.consult(String.to_charlist(app_file)),
{module, _args} <- Keyword.get(props, :mod) do
module
else
_ -> nil
end
end
# {module, persisted_attributes} for every compiled module of the current app,
# read from disk without loading the modules.
defp modules_attrs do
Mix.Project.compile_path()
|> Path.join("*.beam")
|> Path.wildcard()
|> Enum.flat_map(fn beam ->
case :beam_lib.chunks(String.to_charlist(beam), [:attributes]) do
{:ok, {module, [attributes: attrs]}} -> [{module, attrs}]
_ -> []
end
end)
end
defp diagnostic(message) do
%Mix.Task.Compiler.Diagnostic{
compiler_name: "nebula",
file: Path.relative_to_cwd(Mix.Project.project_file()),
severity: :error,
message: message,
position: 0,
details: nil
}
end
end