Skip to main content

src/rebar3_otter__compile.erl

-module(rebar3_otter__compile).
-moduledoc """
Provider that compiles Rust NIF crates via cargo.

Runs as a pre-compile hook so the shared library is in place
before the Erlang compiler runs.
""".

-behaviour(provider).

-export([init/1, do/1, format_error/1]).

%%------------------------------------------------------------------------------

-define(PROVIDER, otter_compile).
-define(DEPS, [{default, app_discovery}]).


%%%=============================================================================
%%% Callbacks

-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
  Provider = providers:create([
    {name, ?PROVIDER},
    {module, ?MODULE},
    {namespace, default},
    {bare, true},
    {deps, ?DEPS},
    {short_desc, "Compile Rust NIF crates"},
    {desc, "Compiles all Rust NIF crates listed in otter_crates"}
  ]),
  {ok, rebar_state:add_provider(State, Provider)}.

-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, {module(), term()}}.
do(State) ->
  %% Each project app declares its own otter_crates and owns the priv/native/
  %% directory the artifact is installed into, so the NIF lands where
  %% code:priv_dir/1 for that app resolves (correct in umbrella layouts too).
  Apps = rebar_state:project_apps(State),
  compile_apps(Apps, State).

-spec format_error(term()) -> string() | iolist().
format_error(cargo_not_found) ->
  "cargo not found on PATH. Install the Rust toolchain: https://rustup.rs";
format_error({cargo_failed, Name, Code}) ->
  io_lib:format("cargo build failed for crate '~s' (exit code ~p)", [Name, Code]);
format_error({no_cdylib, Name}) ->
  io_lib:format("no cdylib artifact found for crate '~s'. "
                "Ensure crate-type = [\"cdylib\"] is set in Cargo.toml", [Name]);
format_error({copy_failed, Name, Reason}) ->
  io_lib:format("failed to copy artifact for '~s': ~p", [Name, Reason]);
format_error(Other) ->
  io_lib:format("~p", [Other]).


%%%=============================================================================
%%% Private

-spec compile_apps([rebar_app_info:t()], rebar_state:t()) ->
  {ok, rebar_state:t()}.
compile_apps([], State) ->
  {ok, State};
compile_apps([App | Rest], State) ->
  Raw = rebar_app_info:get(App, otter_crates, []),
  case rebar3_otter__config:validate(Raw) of
    {ok, []} ->
      compile_apps(Rest, State);
    {ok, Crates} ->
      AppDir = rebar_app_info:dir(App),
      case compile_crates(Crates, AppDir, State) of
        {ok, State1} ->
          compile_apps(Rest, State1);
        {error, {?MODULE, Reason}} ->
          %% Same pre-hook quirk as the config-error branch below.
          rebar_api:abort("~s", [format_error(Reason)])
      end;
    {error, Reason} ->
      %% rebar3's pre-hook machinery rewrites any {error, _} from our do/1
      %% into a misleading "command not found in namespace" message
      %% (rebar_hooks.erl:70-73), so config errors have to halt the build
      %% directly with the formatted message instead of riding the
      %% format_error/1 path that top-level providers use.
      rebar_api:abort("~s", [rebar3_otter__config:format_error(Reason)])
  end.

-spec compile_crates([rebar3_otter__config:crate()], string(), rebar_state:t()) ->
  {ok, rebar_state:t()} | {error, {module(), term()}}.
compile_crates([], _AppDir, State) ->
  {ok, State};
compile_crates([Crate | Rest], AppDir, State) ->
  case compile_crate(Crate, AppDir) of
    ok              -> compile_crates(Rest, AppDir, State);
    {error, Reason} -> {error, {?MODULE, Reason}}
  end.

-spec compile_crate(rebar3_otter__config:crate(), string()) -> ok | {error, term()}.
compile_crate(#{name := Name, path := Path, mode := Mode,
                features := Features, target := Target}, AppDir) ->
  CratePath = filename:join(AppDir, Path),
  OutDir = filename:join([AppDir, "priv", "native"]),
  OutFile = filename:join(OutDir, rebar3_otter__cargo:nif_filename(Name)),
  rebar_api:info("Compiling Rust crate ~s", [Name]),
  case rebar3_otter__cargo:build(CratePath, Name, Mode, Features, Target) of
    {ok, ArtifactPath} ->
      install_artifact(ArtifactPath, OutDir, OutFile, Name);
    {error, _} = Err ->
      Err
  end.

-spec install_artifact(string(), string(), string(), string()) -> ok | {error, term()}.
install_artifact(ArtifactPath, OutDir, OutFile, Name) ->
  ok = filelib:ensure_dir(filename:join(OutDir, ".")),
  case file:copy(ArtifactPath, OutFile) of
    {ok, _} ->
      rebar_api:info("Installed ~s", [OutFile]),
      ok;
    {error, Reason} ->
      {error, {copy_failed, Name, Reason}}
  end.