lib/mix/tasks/compile.nerves_package.ex

defmodule Mix.Tasks.Compile.NervesPackage do
  @shortdoc "Nerves Package Compiler"
  @moduledoc """
  Compile a Nerves package into a local artifact

  This is only intended to be used by Nerves systems and toolchains
  and configured in thier mix.exs files. It should not be used manually
  when compiling a Nerves project. See `mix firmware` instead.
  """
  use Mix.Task
  import Mix.Nerves.IO

  require Logger

  @recursive true

  @impl Mix.Task
  def run(_args) do
    debug_info("Compile.NervesPackage start")

    if Nerves.Env.enabled?() do
      bootstrap_check!()

      package =
        case Nerves.Env.ensure_loaded(Mix.Project.config()[:app]) do
          {:ok, package} -> package
          {:error, err} -> Mix.raise(err)
        end

      toolchain = Nerves.Env.toolchain()

      ret =
        if Nerves.Artifact.stale?(package) do
          _ = Nerves.Artifact.build(package, toolchain)
          :ok
        else
          :noop
        end

      debug_info("Compile.NervesPackage end")
      ret
    else
      debug_info("Compile.NervesPackage disabled")
      :noop
    end
  end

  defp bootstrap_check!() do
    cond do
      bootstrap_started?() ->
        :ok

      not bootstrap_installed?() ->
        Mix.raise("""
        Compiling Nerves packages requires the nerves_bootstrap archive which is missing
        from the Elixir version currently in use (#{System.version()}).

        Please install it with:

          mix archive.install hex nerves_bootstrap
        """)

      true ->
        Mix.raise(
          """
          Compiling Nerves packages requires nerves_bootstrap to be started, which ought to
          happen in your generated `config.exs`. Please ensure that MIX_TARGET is set in your environment.
          """ <>
            if in_umbrella?(File.cwd!()) do
              """

              When compiling from an Umbrella project you must also ensure:

              * You are compiling from an application directory, not the root of the Umbrella

              * The Umbrella config (/config/config.exs) imports the generated Nerves config from your
              Nerves application (import_config "../apps/your_nerves_app/config/config.exs")

              """
            else
              ""
            end
        )
    end
  end

  defp bootstrap_installed?() do
    Mix.path_for(:archives)
    |> Path.join("*")
    |> Path.wildcard()
    |> Enum.any?(&(&1 =~ ~r/nerves_bootstrap/))
  end

  defp bootstrap_started?() do
    Application.started_applications()
    |> Enum.map(&Tuple.to_list/1)
    |> Enum.map(&List.first/1)
    |> Enum.member?(:nerves_bootstrap)
  end

  defp in_umbrella?(app_path) do
    umbrella = Path.expand(Path.join([app_path, "..", ".."]))
    mix_path = Path.join(umbrella, "mix.exs")
    apps_path = Path.join(umbrella, "apps")

    File.exists?(mix_path) && File.exists?(apps_path)
  end
end