Skip to main content

lib/mix/tasks/marea.build.ex

defmodule Mix.Tasks.Marea.Build do
  @moduledoc """
  Builds the marea escript and wraps it as a self-contained executable.

  This task must be run from the marea source project itself.
  Dependent projects get `./marea` via `marea setup init-umbrella`, or
  by symlinking the shipped `priv/marea` binary manually
  (`ln -s /path/to/marea .`).
  """
  @shortdoc "Build self-contained marea executable (run from marea source)"

  use Mix.Task

  @escript_name "marea"

  @header ~S"""
  #!/bin/sh
  _TMP="${TMPDIR:-/tmp}/.marea_$$"
  tail -n +10 "$0" > "$_TMP"
  escript "$_TMP" "$@"
  _rc=$?
  rm -f "$_TMP"
  [ $_rc -ne 0 ] && exit $_rc
  [ -x .marea/next_cmd ] && exec .marea/next_cmd
  exit 0
  """

  @impl true
  def run(args) do
    name = project_escript_name()

    # Remove existing symlink before building so escript.build writes a real file
    if File.exists?(name), do: File.rm!(name)

    Mix.Task.run("escript.build", args)

    escript_data = File.read!(name)
    File.write!(name, @header <> escript_data)
    File.chmod!(name, 0o755)

    # Copy to priv/marea for distribution
    priv_path = Path.join("priv", name)
    File.cp!(name, priv_path)
    Mix.shell().info("Copied to #{priv_path}")

    # Replace local binary with symlink to priv
    File.rm!(name)
    File.ln_s!(priv_path, name)
    Mix.shell().info("Linked ./#{name} -> #{priv_path}")
  end

  defp project_escript_name do
    config = Mix.Project.config()

    case get_in(config, [:escript, :name]) do
      nil -> config[:app] |> to_string()
      name -> to_string(name)
    end
  end

  @doc "The fixed escript name (`\"marea\"`)."
  @spec escript_name() :: String.t()
  def escript_name, do: @escript_name

  @doc "The shell wrapper that prefixes the built escript bytes."
  @spec header() :: String.t()
  def header, do: @header
end