Remove double space from application doc (#5537)

defmodule Application do
  @moduledoc """
  A module for working with applications and defining application callbacks.

  In Elixir (actually, in Erlang/OTP), an application is a component
  implementing some specific functionality, that can be started and stopped
  as a unit, and which can be re-used in other systems.

  Applications are defined with an application file named `APP.app` where
  `APP` is the application name, usually in `underscore_case`. The application
  file must reside in the same `ebin` directory as the compiled modules of the
  application.

  In Elixir, Mix is responsible for compiling your source code and
  generating your application `.app` file. Furthermore, Mix is also
  responsible for configuring, starting and stopping your application
  and its dependencies. For this reason, this documentation will focus
  on the remaining aspects of your application: the application environment
  and the application callback module.

  You can learn more about Mix generation of `.app` files by typing
  `mix help compile.app`.

  ## Application environment

  Once an application is started, OTP provides an application environment
  that can be used to configure the application.

  Assuming you are inside a Mix project, you can edit the `application/0`
  function in the `mix.exs` file to the following:

      def application do
        [env: [hello: :world]]
      end

  In the application function, we can define the default environment values
  for our application. By starting your application with `iex -S mix`, you
  can access the default value:

      Application.get_env(:APP_NAME, :hello)
      #=> :world

  It is also possible to put and delete values from the application value,
  including new values that are not defined in the environment file (although
  this should be avoided).

  Keep in mind that each application is responsible for its environment.
  Do not use the functions in this module for directly accessing or modifying
  the environment of other applications (as it may lead to inconsistent
  data in the application environment).

  ## Application module callback

  Often times, an application defines a supervision tree that must be started
  and stopped when the application starts and stops. For such, we need to
  define an application module callback. The first step is to define the
  module callback in the application definition in the `mix.exs` file:

      def application do
        [mod: {MyApp, []}]
      end

  Our application now requires the `MyApp` module to provide an application
  callback. This can be done by invoking `use Application` in that module and
  defining a `start/2` callback, for example:

      defmodule MyApp do
        use Application

        def start(_type, _args) do
          MyApp.Supervisor.start_link()
        end
      end

  `start/2` typically returns `{:ok, pid}` or `{:ok, pid, state}` where
  `pid` identifies the supervision tree and `state` is the application state.
  `args` is the second element of the tuple given to the `:mod` option.

  The `type` argument passed to `start/2` is usually `:normal` unless in a
  distributed setup where application takeovers and failovers are configured.
  This particular aspect of applications is explained in more detail in the
  OTP documentation:

    * [`:application` module](http://www.erlang.org/doc/man/application.html)
    * [Applications – OTP Design Principles](http://www.erlang.org/doc/design_principles/applications.html)

  A developer may also implement the `stop/1` callback (automatically defined
  by `use Application`) which does any application cleanup. It receives the
  application state and can return any value. Note that shutting down the
  supervisor is automatically handled by the VM.
  """

  @doc """
  Called when an application is started.

  This function is called when an application is started using
  `Application.start/2` (and functions on top of that, such as
  `Application.ensure_started/2`). This function should start the top-level
  process of the application (which should be the top supervisor of the
  application's supervision tree if the application follows the OTP design
  principles around supervision).

  `start_type` defines how the application is started:

    * `:normal` - used if the startup is a normal startup or if the application
      is distributed and is started on the current node because of a failover
      from another node and the application specification key `:start_phases`
      is `:undefined`.
    * `{:takeover, node}` - used if the application is distributed and is
      started on the current node because of a failover on the node `node`.
    * `{:failover, node}` - used if the application is distributed and is
      started on the current node because of a failover on node `node`, and the
      application specification key `:start_phases` is not `:undefined`.

  `start_args` are the arguments passed to the application in the `:mod`
  specification key (e.g., `mod: {MyApp, [:my_args]}`).

  This function should either return `{:ok, pid}` or `{:ok, pid, state}` if
  startup is successful. `pid` should be the PID of the top supervisor. `state`
  can be an arbitrary term, and if omitted will default to `[]`; if the
  application is later stopped, `state` is passed to the `stop/1` callback (see
  the documentation for the `c:stop/1` callback for more information).

  `use Application` provides no default implementation for the `start/2`
  callback.
  """
  @callback start(start_type, start_args :: term) ::
    {:ok, pid} |
    {:ok, pid, state} |
    {:error, reason :: term}

  @doc """
  Called when an application is stopped.

  This function is called when an application has stopped, i.e., when its
  supervision tree has been stopped. It should do the opposite of what the
  `start/2` callback did, and should perform any necessary cleanup. The return
  value of this callback is ignored.

  `state` is the return value of the `start/2` callback or the return value of
  the `prep_stop/1` function if the application module defines such a function.

  `use Application` defines a default implementation of this function which does
  nothing and just returns `:ok`.
  """
  @callback stop(state) :: term

  @doc false
  defmacro __using__(_) do
    quote location: :keep do
      @behaviour Application

      @doc false
      def stop(_state) do
        :ok
      end

      defoverridable [stop: 1]
    end
  end

  @type app :: atom
  @type key :: atom
  @type value :: term
  @type state :: term
  @type start_type :: :permanent | :transient | :temporary

  @application_keys [:description, :id, :vsn, :modules, :maxP, :maxT, :registered,
                     :included_applications, :applications, :mod, :start_phases]

  @doc """
  Returns the spec for `app`.

  The following keys are returned:

    * #{Enum.map_join @application_keys, "\n  * ", &inspect/1}

  Note the environment is not returned as it can be accessed via
  `fetch_env/2`. Returns `nil` if the application is not loaded.
  """
  @spec spec(app) :: [{key, value}] | nil
  def spec(app) do
    case :application.get_all_key(app) do
      {:ok, info} -> :lists.keydelete(:env, 1, info)
      :undefined  -> nil
    end
  end

  @doc """
  Returns the value for `key` in `app`'s specification.

  See `spec/1` for the supported keys. If the given
  specification parameter does not exist, this function
  will raise. Returns `nil` if the application is not loaded.
  """
  @spec spec(app, key) :: value | nil
  def spec(app, key) when key in @application_keys do
    case :application.get_key(app, key) do
      {:ok, value} -> value
      :undefined -> nil
    end
  end

  @doc """
  Gets the application for the given module.

  The application is located by analyzing the spec
  of all loaded applications. Returns `nil` if
  the module is not listed in any application spec.
  """
  @spec get_application(atom) :: atom | nil
  def get_application(module) when is_atom(module) do
    case :application.get_application(module) do
      {:ok, app} -> app
      :undefined -> nil
    end
  end

  @doc """
  Returns all key-value pairs for `app`.
  """
  @spec get_all_env(app) :: [{key, value}]
  def get_all_env(app) do
    :application.get_all_env(app)
  end

  @doc """
  Returns the value for `key` in `app`'s environment.

  If the configuration parameter does not exist, the function returns the
  `default` value.
  """
  @spec get_env(app, key, value) :: value
  def get_env(app, key, default \\ nil) do
    :application.get_env(app, key, default)
  end

  @doc """
  Returns the value for `key` in `app`'s environment in a tuple.

  If the configuration parameter does not exist, the function returns `:error`.
  """
  @spec fetch_env(app, key) :: {:ok, value} | :error
  def fetch_env(app, key) do
    case :application.get_env(app, key) do
      {:ok, value} -> {:ok, value}
      :undefined -> :error
    end
  end

  @doc """
  Returns the value for `key` in `app`'s environment.

  If the configuration parameter does not exist, raises `ArgumentError`.
  """
  @spec fetch_env!(app, key) :: value | no_return
  def fetch_env!(app, key) do
    case fetch_env(app, key) do
      {:ok, value} -> value
      :error ->
        raise ArgumentError,
          "application #{inspect app} is not loaded, " <>
          "or the configuration parameter #{inspect key} is not set"
    end
  end

  @doc """
  Puts the `value` in `key` for the given `app`.

  ## Options

    * `:timeout`    - the timeout for the change (defaults to 5000ms)
    * `:persistent` - persists the given value on application load and reloads

  If `put_env/4` is called before the application is loaded, the application
  environment values specified in the `.app` file will override the ones
  previously set.

  The persistent option can be set to `true` when there is a need to guarantee
  parameters set with this function will not be overridden by the ones defined
  in the application resource file on load. This means persistent values will
  stick after the application is loaded and also on application reload.
  """
  @spec put_env(app, key, value, [timeout: timeout, persistent: boolean]) :: :ok
  def put_env(app, key, value, opts \\ []) do
    :application.set_env(app, key, value, opts)
  end

  @doc """
  Deletes the `key` from the given `app` environment.

  See `put_env/4` for a description of the options.
  """
  @spec delete_env(app, key, [timeout: timeout, persistent: boolean]) :: :ok
  def delete_env(app, key, opts \\ []) do
    :application.unset_env(app, key, opts)
  end

  @doc """
  Ensures the given `app` is started.

  Same as `start/2` but returns `:ok` if the application was already
  started. This is useful in scripts and in test setup, where test
  applications need to be explicitly started:

      :ok = Application.ensure_started(:my_test_dep)

  """
  @spec ensure_started(app, start_type) :: :ok | {:error, term}
  def ensure_started(app, type \\ :temporary) when is_atom(app) do
    :application.ensure_started(app, type)
  end

  @doc """
  Ensures the given `app` and its applications are started.

  Same as `start/2` but also starts the applications listed under
  `:applications` in the `.app` file in case they were not previously
  started.
  """
  @spec ensure_all_started(app, start_type) :: {:ok, [app]} | {:error, {app, term}}
  def ensure_all_started(app, type \\ :temporary) when is_atom(app) do
    :application.ensure_all_started(app, type)
  end

  @doc """
  Starts the given `app`.

  If the `app` is not loaded, the application will first be loaded using `load/1`.
  Any included application, defined in the `:included_applications` key of the
  `.app` file will also be loaded, but they won't be started.

  Furthermore, all applications listed in the `:applications` key must be explicitly
  started before this application is. If not, `{:error, {:not_started, app}}` is
  returned, where `app` is the name of the missing application.

  In case you want to automatically load **and start** all of `app`'s dependencies,
  see `ensure_all_started/2`.

  The `type` argument specifies the type of the application:

    * `:permanent` - if `app` terminates, all other applications and the entire
      node are also terminated.

    * `:transient` - if `app` terminates with `:normal` reason, it is reported
      but no other applications are terminated. If a transient application
      terminates abnormally, all other applications and the entire node are
      also terminated.

    * `:temporary` - if `app` terminates, it is reported but no other
      applications are terminated (the default).

  Note that it is always possible to stop an application explicitly by calling
  `stop/1`. Regardless of the type of the application, no other applications will
  be affected.

  Note also that the `:transient` type is of little practical use, since when a
  supervision tree terminates, the reason is set to `:shutdown`, not `:normal`.
  """
  @spec start(app, start_type) :: :ok | {:error, term}
  def start(app, type \\ :temporary) when is_atom(app) do
    :application.start(app, type)
  end

  @doc """
  Stops the given `app`.

  When stopped, the application is still loaded.
  """
  @spec stop(app) :: :ok | {:error, term}
  def stop(app) do
    :application.stop(app)
  end

  @doc """
  Loads the given `app`.

  In order to be loaded, an `.app` file must be in the load paths.
  All `:included_applications` will also be loaded.

  Loading the application does not start it nor load its modules, but
  it does load its environment.
  """
  @spec load(app) :: :ok | {:error, term}
  def load(app) when is_atom(app) do
    :application.load(app)
  end

  @doc """
  Unloads the given `app`.

  It will also unload all `:included_applications`.
  Note that the function does not purge the application modules.
  """
  @spec unload(app) :: :ok | {:error, term}
  def unload(app) when is_atom(app) do
    :application.unload(app)
  end

  @doc """
  Gets the directory for app.

  This information is returned based on the code path. Here is an
  example:

      File.mkdir_p!("foo/ebin")
      Code.prepend_path("foo/ebin")
      Application.app_dir(:foo)
      #=> "foo"

  Even though the directory is empty and there is no `.app` file
  it is considered the application directory based on the name
  "foo/ebin". The name may contain a dash `-` which is considered
  to be the app version and it is removed for the lookup purposes:

      File.mkdir_p!("bar-123/ebin")
      Code.prepend_path("bar-123/ebin")
      Application.app_dir(:bar)
      #=> "bar-123"

  For more information on code paths, check the `Code` module in
  Elixir and also Erlang's [`:code` module](http://www.erlang.org/doc/man/code.html).
  """
  @spec app_dir(app) :: String.t
  def app_dir(app) when is_atom(app) do
    case :code.lib_dir(app) do
      lib when is_list(lib) -> IO.chardata_to_string(lib)
      {:error, :bad_name} -> raise ArgumentError, "unknown application: #{inspect app}"
    end
  end

  @doc """
  Returns the given path inside `app_dir/1`.
  """
  @spec app_dir(app, String.t | [String.t]) :: String.t
  def app_dir(app, path) when is_binary(path) do
    Path.join(app_dir(app), path)
  end
  def app_dir(app, path) when is_list(path) do
    Path.join([app_dir(app) | path])
  end

  @doc """
  Returns a list with information about the applications which are currently running.
  """
  @spec started_applications(timeout) :: [tuple]
  def started_applications(timeout \\ 5000) do
    :application.which_applications(timeout)
  end

  @doc """
  Returns a list with information about the applications which have been loaded.
  """
  @spec loaded_applications :: [tuple]
  def loaded_applications do
    :application.loaded_applications
  end

  @doc """
  Formats the error reason returned by `start/2`,
  `ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
  returns a string.
  """
  @spec format_error(any) :: String.t
  def format_error(reason) do
    try do
      do_format_error(reason)
    catch
      # A user could create an error that looks like a built-in one
      # causing an error.
      :error, _ ->
        inspect(reason)
    end
  end

  # exit(:normal) call is special cased, undo the special case.
  defp do_format_error({{:EXIT, :normal}, {mod, :start, args}}) do
    Exception.format_exit({:normal, {mod, :start, args}})
  end

  # {:error, reason} return value
  defp do_format_error({reason, {mod, :start, args}}) do
    Exception.format_mfa(mod, :start, args) <> " returned an error: " <>
      Exception.format_exit(reason)
  end

  # error or exit(reason) call, use exit reason as reason.
  defp do_format_error({:bad_return, {{mod, :start, args}, {:EXIT, reason}}}) do
    Exception.format_exit({reason, {mod, :start, args}})
  end

  # bad return value
  defp do_format_error({:bad_return, {{mod, :start, args}, return}}) do
    Exception.format_mfa(mod, :start, args) <>
      " returned a bad value: " <> inspect(return)
  end

  defp do_format_error({:already_started, app}) when is_atom(app) do
    "already started application #{app}"
  end

  defp do_format_error({:not_started, app}) when is_atom(app) do
    "not started application #{app}"
  end

  defp do_format_error({:bad_application, app}) do
    "bad application: #{inspect(app)}"
  end

  defp do_format_error({:already_loaded, app}) when is_atom(app) do
    "already loaded application #{app}"
  end

  defp do_format_error({:not_loaded, app}) when is_atom(app) do
    "not loaded application #{app}"
  end

  defp do_format_error({:invalid_restart_type, restart}) do
    "invalid application restart type: #{inspect(restart)}"
  end

  defp do_format_error({:invalid_name, name}) do
    "invalid application name: #{inspect(name)}"
  end

  defp do_format_error({:invalid_options, opts}) do
    "invalid application options: #{inspect(opts)}"
  end

  defp do_format_error({:badstartspec, spec}) do
    "bad application start specs: #{inspect(spec)}"
  end

  defp do_format_error({'no such file or directory', file}) do
    "could not find application file: #{file}"
  end

  defp do_format_error(reason) do
    Exception.format_exit(reason)
  end
end


Fix #445 - Warn when load-only deps are orphaned and required by other deps

defmodule Mix.Releases.Utils do
  @moduledoc false
  alias Mix.Releases.{Logger, Release, App}

  @doc """
  Loads a template from :distillery's `priv/templates` directory based on the provided name.
  Any parameters provided are configured as bindings for the template

  ## Example

      iex> {:ok, contents} = #{__MODULE__}.template("erl_script", [erts_vsn: "8.0"])
      ...> String.contains?(contents, "erts-8.0")
      true
  """
  @spec template(atom | String.t(), Keyword.t()) :: {:ok, String.t()} | {:error, term}
  def template(name, params \\ []) do
    Application.app_dir(:distillery, Path.join("priv", "templates"))
    |> Path.join("#{name}.eex")
    |> template_path(params)
  end

  @doc """
  Loads a template from the provided path
  Any parameters provided are configured as bindings for the template

  ## Example
      iex> path = Path.join(["#{:code.priv_dir(:distillery)}", "templates", "erl_script.eex"])
      ...> {:ok, contents} = #{__MODULE__}.template_path(path, [erts_vsn: "8.0"])
      ...> String.contains?(contents, "erts-8.0")
      true
  """
  @spec template_path(String.t(), Keyword.t()) :: {:ok, String.t()} | {:error, term}
  def template_path(template_path, params \\ []) do
    {:ok, EEx.eval_file(template_path, params)}
  rescue
    e ->
      {:error, {:template, e}}
  end

  @doc """
  Writes an Elixir/Erlang term to the provided path
  """
  def write_term(path, term) do
    path = String.to_charlist(path)
    contents = :io_lib.fwrite('~p.\n', [term])

    case :file.write_file(path, contents, encoding: :utf8) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, {:write_terms, :file, reason}}
    end
  end

  @doc """
  Writes a collection of Elixir/Erlang terms to the provided path
  """
  def write_terms(path, terms) when is_list(terms) do
    contents =
      String.duplicate("~p.\n\n", Enum.count(terms))
      |> String.to_charlist()
      |> :io_lib.fwrite(Enum.reverse(terms))

    case :file.write_file('#{path}', contents, encoding: :utf8) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, {:write_terms, :file, reason}}
    end
  end

  @doc """
  Reads a file as Erlang terms
  """
  @spec read_terms(String.t()) :: {:ok, [term]} :: {:error, term}
  def read_terms(path) do
    case :file.consult(String.to_charlist(path)) do
      {:ok, _} = result ->
        result

      {:error, reason} ->
        {:error, {:read_terms, :file, reason}}
    end
  end

  @type write_all_template_spec :: {:template, atom | String.t(), Keyword.t()}
  @type write_all_pair ::
          {String.t(), binary}
          | {String.t(), binary, pos_integer}
          | {String.t(), write_all_template_spec}
          | {String.t(), write_all_template_spec, pos_integer}

  @doc """
  Given a list of tuples containing paths to write, either
  the content to write or a template specification for the content,
  and an optional octal permissions value; write a file to the given
  path, using the content provided, and if given, assign permissions
  to the written file.

  ## Examples

      write_all([{"path/to/file", <<"hello world">>}])

      write_all([{"path/to/file", {:template, :foo_template, [key: :val]}}])

      write_all([{"path/to/file", <<"hello world">>, Oo777}])
  """
  @spec write_all([write_all_pair]) :: :ok | {:error, term}
  def write_all([]), do: :ok

  def write_all([{path, {:template, tmpl, params}} | files]) do
    case template(tmpl, params) do
      {:ok, contents} ->
        write_all([{path, contents} | files])

      err ->
        err
    end
  end

  def write_all([{path, contents} | files]) do
    case File.write(path, contents) do
      :ok ->
        write_all(files)

      err ->
        err
    end
  end

  def write_all([{path, {:template, tmpl, params}, permissions} | files]) do
    case template(tmpl, params) do
      {:ok, contents} ->
        write_all([{path, contents, permissions} | files])

      err ->
        err
    end
  end

  def write_all([{path, contents, permissions} | files]) do
    with :ok <- File.write(path, contents),
         :ok <- File.chmod(path, permissions) do
      write_all(files)
    end
  end

  @doc """
  Determines the current ERTS version
  """
  @spec erts_version() :: String.t()
  def erts_version, do: "#{:erlang.system_info(:version)}"

  @doc """
  Verified that the ERTS path provided is the right one.
  If no ERTS path is specified it's fine. Distillery will work out
  the system ERTS
  """
  @spec validate_erts(String.t() | nil | boolean) :: :ok | {:error, [{:error, term}]}
  def validate_erts(path) when is_binary(path) do
    erts =
      case Path.join(path, "erts-*") |> Path.wildcard() |> Enum.count() do
        0 -> {:error, {:invalid_erts, :missing_directory}}
        1 -> :ok
        _ -> {:error, {:invalid_erts, :too_many}}
      end

    bin =
      if File.exists?(Path.join(path, "bin")) do
        :ok
      else
        {:error, {:invalid_erts, :missing_bin}}
      end

    lib =
      case File.exists?(Path.join(path, "lib")) do
        false -> {:error, {:invalid_erts, :missing_lib}}
        true -> :ok
      end

    errors =
      [erts, bin, lib]
      |> Enum.filter(fn x -> x != :ok end)
      |> Enum.map(fn {:error, _} = err -> err end)

    if Enum.empty?(errors) do
      :ok
    else
      {:error, errors}
    end
  end

  def validate_erts(include_erts) when is_nil(include_erts) or is_boolean(include_erts), do: :ok

  @doc """
  Detects the version of ERTS in the given directory
  """
  @spec detect_erts_version(String.t()) :: {:ok, String.t()} | {:error, term}
  def detect_erts_version(path) when is_binary(path) do
    entries =
      path
      |> Path.expand()
      |> Path.join("erts-*")
      |> Path.wildcard()
      |> Enum.map(&Path.basename/1)

    case entries do
      [<<"erts-", vsn::binary>>] ->
        {:ok, vsn}

      _ ->
        {:error, {:invalid_erts, :cannot_determine_version}}
    end
  end

  @doc """
  Same as `insecure_mkdir_temp/0`, but raises on failure
  """
  @spec insecure_mkdir_temp!() :: String.t() | no_return
  def insecure_mkdir_temp!() do
    case insecure_mkdir_temp() do
      {:ok, dir} ->
        dir

      {:error, {:mkdir_temp, :file, reason}} ->
        raise "Failed to create temporary directory: #{inspect(reason)}"
    end
  end

  @doc """
  Creates a temporary directory with a random name in a canonical
  temporary files directory of the current system
  (i.e. `/tmp` on *NIX or `./tmp` on Windows)

  Returns an ok tuple with the path of the temp directory, or an error
  tuple with the reason it failed.
  """
  @spec insecure_mkdir_temp() :: {:ok, String.t()} | {:error, term}
  def insecure_mkdir_temp() do
    :rand.seed(:exs64)
    unique_num = :rand.uniform(1_000_000_000)

    tmpdir_path =
      case :erlang.system_info(:system_architecture) do
        'win32' ->
          Path.join(["./tmp", ".tmp_dir#{unique_num}"])

        _ ->
          Path.join(["/tmp", ".tmp_dir#{unique_num}"])
      end

    case File.mkdir_p(tmpdir_path) do
      :ok ->
        {:ok, tmpdir_path}

      {:error, reason} ->
        {:error, {:mkdir_temp, :file, reason}}
    end
  end

  @doc """
  Deletes the given path, if it exists.
  """
  def remove_if_exists(path) do
    if File.exists?(path) do
      case File.rm_rf(path) do
        {:ok, _} ->
          :ok

        {:error, reason, file} ->
          {:error, {:assembler, :file, {reason, file}}}
      end
    else
      :ok
    end
  end

  @doc """
  Deletes the given path properly, depending on whether it is a symlink or not
  """
  def remove_symlink_or_dir!(path) do
    case File.exists?(path) do
      true ->
        File.rm_rf!(path)

      false ->
        if symlink?(path) do
          File.rm!(path)
        end
    end

    :ok
  rescue
    e in [File.Error] ->
      {:error, {:assembler, :file, {e.reason, e.path}}}
  end

  @doc """
  Returns true if the given path is a symlink, otherwise false
  """
  @spec symlink?(String.t()) :: boolean
  def symlink?(path) do
    case :file.read_link_info('#{path}') do
      {:ok, info} ->
        elem(info, 2) == :symlink

      _ ->
        false
    end
  end

  @doc """
  Given a path to a release output directory, return a list
  of release versions that are present.

  ## Example

      iex> app_dir = Path.join([File.cwd!, "test", "fixtures", "mock_app"])
      ...> output_dir = Path.join([app_dir, "rel", "mock_app"])
      ...> #{__MODULE__}.get_release_versions(output_dir)
      ["0.2.2", "0.2.1-1-d3adb3f", "0.2.1", "0.2.0", "0.1.0"]
  """
  @valid_version_pattern ~r/^\d+.*$/
  @spec get_release_versions(String.t()) :: list(String.t())
  def get_release_versions(output_dir) do
    releases_path = Path.join([output_dir, "releases"])

    if File.exists?(releases_path) do
      releases_path
      |> File.ls!()
      |> Enum.filter(&Regex.match?(@valid_version_pattern, &1))
      |> sort_versions
    else
      []
    end
  end

  @git_describe_pattern ~r/(?<ver>\d+\.\d+\.\d+)-(?<commits>\d+)-(?<sha>[A-Ga-g0-9]+)/
  @doc """
  Sort a list of version strings, in reverse order (i.e. latest version comes first)
  Tries to use semver version compare, but can fall back to regular string compare.
  It also parses git-describe generated version strings and handles ordering them
  correctly.

  ## Example

      iex> #{__MODULE__}.sort_versions(["1.0.2", "1.0.1", "1.0.9", "1.0.10"])
      ["1.0.10", "1.0.9", "1.0.2", "1.0.1"]

      iex> #{__MODULE__}.sort_versions(["0.0.1", "0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef"])
      ["0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef", "0.0.1"]
  """
  @spec sort_versions(list(String.t())) :: list(String.t())
  def sort_versions(versions) do
    versions
    |> Enum.map(fn ver ->
      # Special handling for git-describe versions
      compared =
        case Regex.named_captures(@git_describe_pattern, ver) do
          nil ->
            {:standard, ver, nil}

          %{"ver" => version, "commits" => n, "sha" => sha} ->
            {:describe, <<version::binary, ?+, n::binary, ?-, sha::binary>>, String.to_integer(n)}
        end

      {ver, compared}
    end)
    |> Enum.sort(fn {_, {v1type, v1str, v1_commits_since}},
                    {_, {v2type, v2str, v2_commits_since}} ->
      case {parse_version(v1str), parse_version(v2str)} do
        {{:semantic, v1}, {:semantic, v2}} ->
          case Version.compare(v1, v2) do
            :gt ->
              true

            :eq ->
              case {v1type, v2type} do
                # probably always false
                {:standard, :standard} ->
                  v1 > v2

                # v2 is an incremental version over v1
                {:standard, :describe} ->
                  false

                # v1 is an incremental version over v2
                {:describe, :standard} ->
                  true

                # need to parse out the bits
                {:describe, :describe} ->
                  v1_commits_since > v2_commits_since
              end

            :lt ->
              false
          end

        {{_, v1}, {_, v2}} ->
          v1 > v2
      end
    end)
    |> Enum.map(fn {v, _} -> v end)
  end

  defp parse_version(ver) do
    case Version.parse(ver) do
      {:ok, semver} ->
        {:semantic, semver}

      :error ->
        {:unsemantic, ver}
    end
  end

  Code.ensure_loaded(Mix.Dep)

  if function_exported?(Mix.Dep, :load_on_environment, 1) do
    defp loaded_deps(opts), do: Mix.Dep.load_on_environment(opts)
  else
    defp loaded_deps(opts), do: Mix.Dep.loaded(opts)
  end

  @doc """
  Gets a list of {app, vsn} tuples for the current release.

  An optional second parameter enables/disables debug logging of discovered apps.
  """
  @spec get_apps(Mix.Releases.Release.t()) :: [{atom, String.t()}] | {:error, term}
  # Gets all applications which are part of the release application tree
  def get_apps(%Release{name: name, applications: apps} = release) do
    loaded_deps = loaded_deps([])

    apps =
      if Enum.member?(apps, name) do
        apps
      else
        apps ++ [name]
      end

    base_apps =
      apps
      |> Enum.reduce([], fn
        _, {:error, reason} ->
          {:error, {:apps, reason}}

        {a, start_type}, acc ->
          cond do
            App.valid_start_type?(start_type) ->
              if Enum.any?(acc, fn %App{name: app} -> a == app end) do
                # Override start type
                Enum.map(acc, fn
                  %App{name: ^a} = app -> %{app | start_type: start_type}
                  app -> app
                end)
              else
                get_apps(App.new(a, start_type, loaded_deps), loaded_deps, acc)
              end

            :else ->
              {:error, {:apps, {:invalid_start_type, a, start_type}}}
          end

        a, acc when is_atom(a) ->
          if Enum.any?(acc, fn %App{name: app} -> a == app end) do
            acc
          else
            get_apps(App.new(a, loaded_deps), loaded_deps, acc)
          end
      end)

    # Correct any ERTS libs which should be pulled from the correct
    # ERTS directory, not from the current environment.
    apps =
      case release.profile.include_erts do
        true ->
          base_apps

        false ->
          base_apps

        p when is_binary(p) ->
          lib_dir = Path.expand(Path.join(p, "lib"))

          Enum.reduce(base_apps, [], fn
            _, {:error, {:apps, _}} = err ->
              err

            _, {:error, reason} ->
              {:error, {:apps, reason}}

            %App{name: a} = app, acc ->
              if is_erts_lib?(app.path) do
                case Path.wildcard(Path.join(lib_dir, "#{a}-*")) do
                  [corrected_app_path | _] ->
                    [_, corrected_app_vsn] =
                      String.split(Path.basename(corrected_app_path), "-", trim: true)

                    [%{app | :vsn => corrected_app_vsn, :path => corrected_app_path} | acc]

                  _ ->
                    {:error, {:apps, {:missing_required_lib, a, lib_dir}}}
                end
              else
                [app | acc]
              end
          end)
      end

    case apps do
      {:error, _} = err ->
        err

      ^apps when is_list(apps) ->
        apps = Enum.reverse(apps)
        # Accumulate all unhandled deps, and see if they are present in the list
        # of applications, if so they can be ignored, if not, warn about them
        unhandled =
          apps
          |> Enum.flat_map(fn %App{} = app -> app.unhandled_deps end)
          |> MapSet.new()

        handled =
          apps
          |> Enum.flat_map(fn %App{name: a} = app ->
            Enum.concat([a | app.applications], app.included_applications)
          end)
          |> Enum.uniq()
          |> MapSet.new()

        ignore_missing = Application.get_env(:distillery, :no_warn_missing, [])
        missing = MapSet.to_list(MapSet.difference(unhandled, handled))

        missing =
          case ignore_missing do
            false ->
              missing
            true ->
              []
            ignore ->
              Enum.reject(missing, &Enum.member?(ignore, &1))
          end

        case missing do
          [] ->
            :ok

          _ ->
            Logger.warn(
              "One or more direct or transitive dependencies are missing from\n" <>
                "    :applications or :included_applications, they will not be included\n" <>
                "    in the release:\n\n" <>
                Enum.join(Enum.map(missing, fn a -> "    #{inspect(a)}" end), "\n") <>
                "\n\n    This can cause your application to fail at runtime. If you are sure\n" <>
                "    that this is not an issue, you may ignore this warning.\n"
            )
        end

        # Print apps
        Logger.debug("Discovered applications:")

        Enum.each(apps, fn %App{} = app ->
          where = Path.relative_to_cwd(app.path)

          Logger.debug(
            "  #{IO.ANSI.reset()}#{app.name}-#{app.vsn}#{IO.ANSI.cyan()}\n\tfrom: #{where}",
            :plain
          )

          case app.applications do
            [] ->
              Logger.debug("    applications: none", :plain)

            apps ->
              display_apps =
                apps
                |> Enum.map(&inspect/1)
                |> Enum.join("\n\t")

              Logger.debug("    applications:\n\t#{display_apps}", :plain)
          end

          case app.included_applications do
            [] ->
              Logger.debug("    includes: none\n", :plain)

            included_apps ->
              display_apps =
                included_apps
                |> Enum.map(&inspect/1)
                |> Enum.join("\n\t")

              Logger.debug("    includes:\n\t#{display_apps}", :plain)
          end
        end)

        # Determine if any applications with start type :load are not included
        # and are also depended upon by other applications. This condition is
        # likely to result in a release which appears to be started, but is not
        # actually fully operational
        loaded =
          apps
          |> Enum.filter(fn %App{start_type: type} -> type == :load end)
          |> Enum.map(fn %App{name: name} -> name end)
          |> MapSet.new
        included_apps =
          apps
          |> Enum.flat_map(fn %App{included_applications: ia} -> ia end)
          |> Enum.uniq
          |> MapSet.new
        required_apps =
          apps
          |> Enum.flat_map(fn %App{applications: a} -> a end)
          |> Enum.uniq
          |> MapSet.new
        loaded_not_included =
          loaded
          |> MapSet.difference(included_apps)
        loaded_but_required =
          loaded_not_included
          |> MapSet.intersection(required_apps)
        requiring_apps =
          apps
          |> Enum.filter(fn %App{applications: a} ->
            required_loaded =
              a
              |> MapSet.new
              |> MapSet.intersection(loaded_but_required)
              |> MapSet.to_list
            required_loaded != []
          end)
          |> Enum.map(fn %App{name: a} -> a end)
        required_transitively = require_transitively(apps, requiring_apps)

        if loaded_but_required != [] do
          Logger.warn(
            "You have specified a start type of :load for the following orphan applications:\n\n" <>
            Enum.join(Enum.map(loaded_but_required, fn a -> "        #{inspect(a)}" end), "\n") <>
            "\n\n    These applications are considered orphaned because they are not included by another\n" <>
            "    application (i.e. present in the included_applications list). Since they are only loaded,\n" <>
            "    neither the runtime, or any application is responsible for ensuring they are started." <>
            "\n\n    This is a problem because the following applications - either directly or transitively -\n" <>
            "    depend on the above applications to be started before they can start; and this cannot\n" <>
            "    be guaranteed:\n\n" <>
            Enum.join(Enum.map(required_transitively, fn a -> "        #{inspect(a)}" end), "\n") <>
            "\n\n    If you do not address this, your release may appear to start successfully, but may\n" <>
            "    in fact only be partially started, which can manifest as portions of your application\n" <>
            "    not working as expected. For example, a Phoenix endpoint not binding to it's configured port." <>
            "\n\n    You should either add all of these applications to :included_applications, and ensure\n" <>
            "    they are started as part of your application; or you should change the start type of the\n" <>
            "    first set of applications to :permanent or leave the start type unspecified. The latter\n" <>
            "    is the best approach when possible.\n"
          )
        end

        apps
    end
  end

  defp get_apps(nil, _loaded_deps, acc), do: Enum.uniq(acc)
  defp get_apps({:error, _} = err, _loaded_deps, _acc), do: err

  defp get_apps(%App{} = app, loaded_deps, acc) do
    new_acc =
      app.applications
      |> Enum.concat(app.included_applications)
      |> Enum.reduce(acc, fn
        {:error, _} = err, _acc ->
          err

        {a, load_type}, acc ->
          if Enum.any?(acc, fn %App{name: app} -> a == app end) do
            acc
          else
            case App.new(a, load_type, loaded_deps) do
              nil ->
                acc

              %App{} = app ->
                case get_apps(app, loaded_deps, acc) do
                  {:error, _} = err ->
                    err

                  children ->
                    Enum.concat(children, acc)
                end

              {:error, _} = err ->
                err
            end
          end

        a, acc ->
          if Enum.any?(acc, fn %App{name: app} -> a == app end) do
            acc
          else
            case App.new(a, loaded_deps) do
              nil ->
                acc

              %App{} = app ->
                case get_apps(app, loaded_deps, acc) do
                  {:error, _} = err ->
                    err

                  children ->
                    Enum.concat(children, acc)
                end

              {:error, _} = err ->
                err
            end
          end
      end)

    case new_acc do
      {:error, _} = err ->
        err

      apps ->
        Enum.uniq([app | apps])
    end
  end

  defp require_transitively(all, requiring) do
    require_transitively(all, requiring, requiring)
  end
  defp require_transitively(_all, [], acc), do: acc
  defp require_transitively(all, [app | rest], acc) do
    requiring =
      all
      |> Enum.filter(fn %App{applications: a} -> Enum.member?(a, app) end)
      |> Enum.reject(fn %App{name: a} -> Enum.member?(acc, a) end)
      |> Enum.map(fn %App{name: a} -> a end)
    require_transitively(all, rest ++ requiring, acc ++ requiring)
  end

  # Determines if the given application directory is part of the Erlang installation
  @spec is_erts_lib?(String.t()) :: boolean
  @spec is_erts_lib?(String.t(), String.t()) :: boolean
  def is_erts_lib?(app_dir), do: is_erts_lib?(app_dir, "#{:code.lib_dir()}")
  def is_erts_lib?(app_dir, lib_dir), do: String.starts_with?(app_dir, lib_dir)

  @doc false
  @spec newline() :: String.t()
  def newline() do
    case :os.type() do
      {:win32, _} -> "\r\n"
      {:unix, _} -> "\n"
    end
  end
end

defmodule Mix.Releases.Utils do
  @moduledoc false
  alias Mix.Releases.{Logger, Release, App}

  @doc """
  Loads a template from :distillery's `priv/templates` directory based on the provided name.
  Any parameters provided are configured as bindings for the template

  ## Example

      iex> {:ok, contents} = #{__MODULE__}.template("erl_script", [erts_vsn: "8.0"])
      ...> String.contains?(contents, "erts-8.0")
      true
  """
  @spec template(atom | String.t(), Keyword.t()) :: {:ok, String.t()} | {:error, term}
  def template(name, params \\ []) do
    Application.app_dir(:distillery, Path.join("priv", "templates"))
    |> Path.join("#{name}.eex")
    |> template_path(params)
  end

  @doc """
  Loads a template from the provided path
  Any parameters provided are configured as bindings for the template

  ## Example
      iex> path = Path.join(["#{:code.priv_dir(:distillery)}", "templates", "erl_script.eex"])
      ...> {:ok, contents} = #{__MODULE__}.template_path(path, [erts_vsn: "8.0"])
      ...> String.contains?(contents, "erts-8.0")
      true
  """
  @spec template_path(String.t(), Keyword.t()) :: {:ok, String.t()} | {:error, term}
  def template_path(template_path, params \\ []) do
    {:ok, EEx.eval_file(template_path, params)}
  rescue
    e ->
      {:error, {:template, e}}
  end

  @doc """
  Writes an Elixir/Erlang term to the provided path
  """
  def write_term(path, term) do
    path = String.to_charlist(path)
    contents = :io_lib.fwrite('~p.\n', [term])

    case :file.write_file(path, contents, encoding: :utf8) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, {:write_terms, :file, reason}}
    end
  end

  @doc """
  Writes a collection of Elixir/Erlang terms to the provided path
  """
  def write_terms(path, terms) when is_list(terms) do
    contents =
      String.duplicate("~p.\n\n", Enum.count(terms))
      |> String.to_charlist()
      |> :io_lib.fwrite(Enum.reverse(terms))

    case :file.write_file('#{path}', contents, encoding: :utf8) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, {:write_terms, :file, reason}}
    end
  end

  @doc """
  Reads a file as Erlang terms
  """
  @spec read_terms(String.t()) :: {:ok, [term]} :: {:error, term}
  def read_terms(path) do
    case :file.consult(String.to_charlist(path)) do
      {:ok, _} = result ->
        result

      {:error, reason} ->
        {:error, {:read_terms, :file, reason}}
    end
  end

  @type write_all_template_spec :: {:template, atom | String.t(), Keyword.t()}
  @type write_all_pair ::
          {String.t(), binary}
          | {String.t(), binary, pos_integer}
          | {String.t(), write_all_template_spec}
          | {String.t(), write_all_template_spec, pos_integer}

  @doc """
  Given a list of tuples containing paths to write, either
  the content to write or a template specification for the content,
  and an optional octal permissions value; write a file to the given
  path, using the content provided, and if given, assign permissions
  to the written file.

  ## Examples

      write_all([{"path/to/file", <<"hello world">>}])

      write_all([{"path/to/file", {:template, :foo_template, [key: :val]}}])

      write_all([{"path/to/file", <<"hello world">>, Oo777}])
  """
  @spec write_all([write_all_pair]) :: :ok | {:error, term}
  def write_all([]), do: :ok

  def write_all([{path, {:template, tmpl, params}} | files]) do
    case template(tmpl, params) do
      {:ok, contents} ->
        write_all([{path, contents} | files])

      err ->
        err
    end
  end

  def write_all([{path, contents} | files]) do
    case File.write(path, contents) do
      :ok ->
        write_all(files)

      err ->
        err
    end
  end

  def write_all([{path, {:template, tmpl, params}, permissions} | files]) do
    case template(tmpl, params) do
      {:ok, contents} ->
        write_all([{path, contents, permissions} | files])

      err ->
        err
    end
  end

  def write_all([{path, contents, permissions} | files]) do
    with :ok <- File.write(path, contents),
         :ok <- File.chmod(path, permissions) do
      write_all(files)
    end
  end

  @doc """
  Determines the current ERTS version
  """
  @spec erts_version() :: String.t()
  def erts_version, do: "#{:erlang.system_info(:version)}"

  @doc """
  Verified that the ERTS path provided is the right one.
  If no ERTS path is specified it's fine. Distillery will work out
  the system ERTS
  """
  @spec validate_erts(String.t() | nil | boolean) :: :ok | {:error, [{:error, term}]}
  def validate_erts(path) when is_binary(path) do
    erts =
      case Path.join(path, "erts-*") |> Path.wildcard() |> Enum.count() do
        0 -> {:error, {:invalid_erts, :missing_directory}}
        1 -> :ok
        _ -> {:error, {:invalid_erts, :too_many}}
      end

    bin =
      if File.exists?(Path.join(path, "bin")) do
        :ok
      else
        {:error, {:invalid_erts, :missing_bin}}
      end

    lib =
      case File.exists?(Path.join(path, "lib")) do
        false -> {:error, {:invalid_erts, :missing_lib}}
        true -> :ok
      end

    errors =
      [erts, bin, lib]
      |> Enum.filter(fn x -> x != :ok end)
      |> Enum.map(fn {:error, _} = err -> err end)

    if Enum.empty?(errors) do
      :ok
    else
      {:error, errors}
    end
  end

  def validate_erts(include_erts) when is_nil(include_erts) or is_boolean(include_erts), do: :ok

  @doc """
  Detects the version of ERTS in the given directory
  """
  @spec detect_erts_version(String.t()) :: {:ok, String.t()} | {:error, term}
  def detect_erts_version(path) when is_binary(path) do
    entries =
      path
      |> Path.expand()
      |> Path.join("erts-*")
      |> Path.wildcard()
      |> Enum.map(&Path.basename/1)

    case entries do
      [<<"erts-", vsn::binary>>] ->
        {:ok, vsn}

      _ ->
        {:error, {:invalid_erts, :cannot_determine_version}}
    end
  end

  @doc """
  Same as `insecure_mkdir_temp/0`, but raises on failure
  """
  @spec insecure_mkdir_temp!() :: String.t() | no_return
  def insecure_mkdir_temp!() do
    case insecure_mkdir_temp() do
      {:ok, dir} ->
        dir

      {:error, {:mkdir_temp, :file, reason}} ->
        raise "Failed to create temporary directory: #{inspect(reason)}"
    end
  end

  @doc """
  Creates a temporary directory with a random name in a canonical
  temporary files directory of the current system
  (i.e. `/tmp` on *NIX or `./tmp` on Windows)

  Returns an ok tuple with the path of the temp directory, or an error
  tuple with the reason it failed.
  """
  @spec insecure_mkdir_temp() :: {:ok, String.t()} | {:error, term}
  def insecure_mkdir_temp() do
    :rand.seed(:exs64)
    unique_num = :rand.uniform(1_000_000_000)

    tmpdir_path =
      case :erlang.system_info(:system_architecture) do
        'win32' ->
          Path.join(["./tmp", ".tmp_dir#{unique_num}"])

        _ ->
          Path.join(["/tmp", ".tmp_dir#{unique_num}"])
      end

    case File.mkdir_p(tmpdir_path) do
      :ok ->
        {:ok, tmpdir_path}

      {:error, reason} ->
        {:error, {:mkdir_temp, :file, reason}}
    end
  end

  @doc """
  Deletes the given path, if it exists.
  """
  def remove_if_exists(path) do
    if File.exists?(path) do
      case File.rm_rf(path) do
        {:ok, _} ->
          :ok

        {:error, reason, file} ->
          {:error, {:assembler, :file, {reason, file}}}
      end
    else
      :ok
    end
  end

  @doc """
  Deletes the given path properly, depending on whether it is a symlink or not
  """
  def remove_symlink_or_dir!(path) do
    case File.exists?(path) do
      true ->
        File.rm_rf!(path)

      false ->
        if symlink?(path) do
          File.rm!(path)
        end
    end

    :ok
  rescue
    e in [File.Error] ->
      {:error, {:assembler, :file, {e.reason, e.path}}}
  end

  @doc """
  Returns true if the given path is a symlink, otherwise false
  """
  @spec symlink?(String.t()) :: boolean
  def symlink?(path) do
    case :file.read_link_info('#{path}') do
      {:ok, info} ->
        elem(info, 2) == :symlink

      _ ->
        false
    end
  end

  @doc """
  Given a path to a release output directory, return a list
  of release versions that are present.

  ## Example

      iex> app_dir = Path.join([File.cwd!, "test", "fixtures", "mock_app"])
      ...> output_dir = Path.join([app_dir, "rel", "mock_app"])
      ...> #{__MODULE__}.get_release_versions(output_dir)
      ["0.2.2", "0.2.1-1-d3adb3f", "0.2.1", "0.2.0", "0.1.0"]
  """
  @valid_version_pattern ~r/^\d+.*$/
  @spec get_release_versions(String.t()) :: list(String.t())
  def get_release_versions(output_dir) do
    releases_path = Path.join([output_dir, "releases"])

    if File.exists?(releases_path) do
      releases_path
      |> File.ls!()
      |> Enum.filter(&Regex.match?(@valid_version_pattern, &1))
      |> sort_versions
    else
      []
    end
  end

  @git_describe_pattern ~r/(?<ver>\d+\.\d+\.\d+)-(?<commits>\d+)-(?<sha>[A-Ga-g0-9]+)/
  @doc """
  Sort a list of version strings, in reverse order (i.e. latest version comes first)
  Tries to use semver version compare, but can fall back to regular string compare.
  It also parses git-describe generated version strings and handles ordering them
  correctly.

  ## Example

      iex> #{__MODULE__}.sort_versions(["1.0.2", "1.0.1", "1.0.9", "1.0.10"])
      ["1.0.10", "1.0.9", "1.0.2", "1.0.1"]

      iex> #{__MODULE__}.sort_versions(["0.0.1", "0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef"])
      ["0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef", "0.0.1"]
  """
  @spec sort_versions(list(String.t())) :: list(String.t())
  def sort_versions(versions) do
    versions
    |> Enum.map(fn ver ->
      # Special handling for git-describe versions
      compared =
        case Regex.named_captures(@git_describe_pattern, ver) do
          nil ->
            {:standard, ver, nil}

          %{"ver" => version, "commits" => n, "sha" => sha} ->
            {:describe, <<version::binary, ?+, n::binary, ?-, sha::binary>>, String.to_integer(n)}
        end

      {ver, compared}
    end)
    |> Enum.sort(fn {_, {v1type, v1str, v1_commits_since}},
                    {_, {v2type, v2str, v2_commits_since}} ->
      case {parse_version(v1str), parse_version(v2str)} do
        {{:semantic, v1}, {:semantic, v2}} ->
          case Version.compare(v1, v2) do
            :gt ->
              true

            :eq ->
              case {v1type, v2type} do
                # probably always false
                {:standard, :standard} ->
                  v1 > v2

                # v2 is an incremental version over v1
                {:standard, :describe} ->
                  false

                # v1 is an incremental version over v2
                {:describe, :standard} ->
                  true

                # need to parse out the bits
                {:describe, :describe} ->
                  v1_commits_since > v2_commits_since
              end

            :lt ->
              false
          end

        {{_, v1}, {_, v2}} ->
          v1 > v2
      end
    end)
    |> Enum.map(fn {v, _} -> v end)
  end

  defp parse_version(ver) do
    case Version.parse(ver) do
      {:ok, semver} ->
        {:semantic, semver}

      :error ->
        {:unsemantic, ver}
    end
  end

  Code.ensure_loaded(Mix.Dep)

  if function_exported?(Mix.Dep, :load_on_environment, 1) do
    defp loaded_deps(opts), do: Mix.Dep.load_on_environment(opts)
  else
    defp loaded_deps(opts), do: Mix.Dep.loaded(opts)
  end

  @doc """
  Gets a list of {app, vsn} tuples for the current release.

  An optional second parameter enables/disables debug logging of discovered apps.
  """
  @spec get_apps(Mix.Releases.Release.t()) :: [{atom, String.t()}] | {:error, term}
  # Gets all applications which are part of the release application tree
  def get_apps(%Release{name: name, applications: apps} = release) do
    loaded_deps = loaded_deps([])

    apps =
      if Enum.member?(apps, name) do
        apps
      else
        apps ++ [name]
      end

    base_apps =
      apps
      |> Enum.reduce([], fn
        _, {:error, reason} ->
          {:error, {:apps, reason}}

        {a, start_type}, acc ->
          cond do
            App.valid_start_type?(start_type) ->
              if Enum.any?(acc, fn %App{name: app} -> a == app end) do
                # Override start type
                Enum.map(acc, fn
                  %App{name: ^a} = app -> %{app | start_type: start_type}
                  app -> app
                end)
              else
                get_apps(App.new(a, start_type, loaded_deps), loaded_deps, acc)
              end

            :else ->
              {:error, {:apps, {:invalid_start_type, a, start_type}}}
          end

        a, acc when is_atom(a) ->
          if Enum.any?(acc, fn %App{name: app} -> a == app end) do
            acc
          else
            get_apps(App.new(a, loaded_deps), loaded_deps, acc)
          end
      end)

    # Correct any ERTS libs which should be pulled from the correct
    # ERTS directory, not from the current environment.
    apps =
      case release.profile.include_erts do
        true ->
          base_apps

        false ->
          base_apps

        p when is_binary(p) ->
          lib_dir = Path.expand(Path.join(p, "lib"))

          Enum.reduce(base_apps, [], fn
            _, {:error, {:apps, _}} = err ->
              err

            _, {:error, reason} ->
              {:error, {:apps, reason}}

            %App{name: a} = app, acc ->
              if is_erts_lib?(app.path) do
                case Path.wildcard(Path.join(lib_dir, "#{a}-*")) do
                  [corrected_app_path | _] ->
                    [_, corrected_app_vsn] =
                      String.split(Path.basename(corrected_app_path), "-", trim: true)

                    [%{app | :vsn => corrected_app_vsn, :path => corrected_app_path} | acc]

                  _ ->
                    {:error, {:apps, {:missing_required_lib, a, lib_dir}}}
                end
              else
                [app | acc]
              end
          end)
      end

    case apps do
      {:error, _} = err ->
        err

      ^apps when is_list(apps) ->
        apps = Enum.reverse(apps)
        # Accumulate all unhandled deps, and see if they are present in the list
        # of applications, if so they can be ignored, if not, warn about them
        unhandled =
          apps
          |> Enum.flat_map(fn %App{} = app -> app.unhandled_deps end)
          |> MapSet.new()

        handled =
          apps
          |> Enum.flat_map(fn %App{name: a} = app ->
            Enum.concat([a | app.applications], app.included_applications)
          end)
          |> Enum.uniq()
          |> MapSet.new()

        ignore_missing = Application.get_env(:distillery, :no_warn_missing, [])
        missing = MapSet.to_list(MapSet.difference(unhandled, handled))

        missing =
          case ignore_missing do
            false -> missing
            true -> []
            ignore -> Enum.reject(missing, &Enum.member?(ignore, &1))
          end

        case missing do
          [] ->
            :ok

          _ ->
            Logger.warn(
              "One or more direct or transitive dependencies are missing from\n" <>
                "    :applications or :included_applications, they will not be included\n" <>
                "    in the release:\n\n" <>
                Enum.join(Enum.map(missing, fn a -> "    #{inspect(a)}" end), "\n") <>
                "\n\n    This can cause your application to fail at runtime. If you are sure\n" <>
                "    that this is not an issue, you may ignore this warning.\n"
            )
        end

        # Print apps
        if is_list(apps) do
          Logger.debug("Discovered applications:")

          Enum.each(apps, fn %App{} = app ->
            where = Path.relative_to_cwd(app.path)

            Logger.debug(
              "  #{IO.ANSI.reset()}#{app.name}-#{app.vsn}#{IO.ANSI.cyan()}\n\tfrom: #{where}",
              :plain
            )

            case app.applications do
              [] ->
                Logger.debug("    applications: none", :plain)

              apps ->
                display_apps =
                  apps
                  |> Enum.map(&inspect/1)
                  |> Enum.join("\n\t")

                Logger.debug("    applications:\n\t#{display_apps}", :plain)
            end

            case app.included_applications do
              [] ->
                Logger.debug("    includes: none\n", :plain)

              included_apps ->
                display_apps =
                  included_apps
                  |> Enum.map(&inspect/1)
                  |> Enum.join("\n\t")

                Logger.debug("    includes:\n\t#{display_apps}", :plain)
            end
          end)
        end

        apps
    end
  end

  defp get_apps(nil, _loaded_deps, acc), do: Enum.uniq(acc)
  defp get_apps({:error, _} = err, _loaded_deps, _acc), do: err

  defp get_apps(%App{} = app, loaded_deps, acc) do
    new_acc =
      app.applications
      |> Enum.concat(app.included_applications)
      |> Enum.reduce(acc, fn
        {:error, _} = err, _acc ->
          err

        {a, load_type}, acc ->
          if Enum.any?(acc, fn %App{name: app} -> a == app end) do
            acc
          else
            case App.new(a, load_type, loaded_deps) do
              nil ->
                acc

              %App{} = app ->
                case get_apps(app, loaded_deps, acc) do
                  {:error, _} = err ->
                    err

                  children ->
                    Enum.concat(children, acc)
                end

              {:error, _} = err ->
                err
            end
          end

        a, acc ->
          if Enum.any?(acc, fn %App{name: app} -> a == app end) do
            acc
          else
            case App.new(a, loaded_deps) do
              nil ->
                acc

              %App{} = app ->
                case get_apps(app, loaded_deps, acc) do
                  {:error, _} = err ->
                    err

                  children ->
                    Enum.concat(children, acc)
                end

              {:error, _} = err ->
                err
            end
          end
      end)

    case new_acc do
      {:error, _} = err ->
        err

      apps ->
        Enum.uniq([app | apps])
    end
  end

  # Determines if the given application directory is part of the Erlang installation
  @spec is_erts_lib?(String.t()) :: boolean
  @spec is_erts_lib?(String.t(), String.t()) :: boolean
  def is_erts_lib?(app_dir), do: is_erts_lib?(app_dir, "#{:code.lib_dir()}")
  def is_erts_lib?(app_dir, lib_dir), do: String.starts_with?(app_dir, lib_dir)

  @doc false
  @spec newline() :: String.t()
  def newline() do
    case :os.type() do
      {:win32, _} -> "\r\n"
      {:unix, _} -> "\n"
    end
  end
end


Add tests to utils module

Add tests to utils module

defmodule Mix.Releases.Utils do
  @moduledoc false

  @doc """
  Loads a template from :distillery's `priv/templates` directory based on the provided name.
  Any parameters provided are configured as bindings for the template

  ## Example

      iex> {:ok, contents} = #{__MODULE__}.template("vm.args", [rel_name: :test])
      ...> String.contains?(contents, "-name test@127.0.0.1")
      true
  """
  @spec template(atom | String.t, Keyword.t) :: {:ok, String.t} | {:error, String.t}
  def template(name, params \\ []) do
    try do
      template_path = Path.join(["#{:code.priv_dir(:distillery)}", "templates", "#{name}.eex"])
      {:ok, EEx.eval_file(template_path, params)}
    rescue
      e ->
        {:error, e.__struct__.message(e)}
    end
  end

  @doc """
  Writes an Elixir/Erlang term to the provided path
  """
  @spec write_term(String.t, term) :: :ok | {:error, term}
  def write_term(path, term) do
    :file.write_file('#{path}', :io_lib.fwrite('~p.\n', [term]), [encoding: :utf8])
  end

  @doc """
  Writes a collection of Elixir/Erlang terms to the provided path
  """
  @spec write_terms(String.t, [term]) :: :ok | {:error, term}
  def write_terms(path, terms) when is_list(terms) do
    contents = String.duplicate("~p.\n\n", Enum.count(terms))
       |> String.to_char_list
       |> :io_lib.fwrite(Enum.reverse(terms))
    :file.write_file('#{path}', contents, [encoding: :utf8])
  end

  @doc """
  Reads a file as Erlang terms
  """
  @spec read_terms(String.t) :: {:ok, [term]} :: {:error, String.t}
  def read_terms(path) do
    case :file.consult(String.to_charlist(path)) do
      {:ok, _} = result ->
        result
      {:error, {line, type, msg}} ->
        {:error, "Parse failed - #{path}@#{line} (#{type}): #{msg}"}
      {:error, reason} ->
        {:error, "Unable to access #{path} (#{reason})"}
    end
  end

  @doc """
  Determines the current ERTS version
  """
  @spec erts_version() :: String.t
  def erts_version, do: "#{:erlang.system_info(:version)}"

  @doc """
  Creates a temporary directory with a random name in a canonical
  temporary files directory of the current system
  (i.e. `/tmp` on *NIX or `./tmp` on Windows)

  Returns an ok tuple with the path of the temp directory, or an error
  tuple with the reason it failed.
  """
  @spec insecure_mkdir_temp() :: {:ok, String.t} | {:error, term}
  def insecure_mkdir_temp() do
    unique_num = trunc(:random.uniform() * 1000000000000)
    tmpdir_path = case :erlang.system_info(:system_architecture) do
                    'win32' ->
                      Path.join(["./tmp", ".tmp_dir#{unique_num}"])
                    _ ->
                      Path.join(["/tmp", ".tmp_dir#{unique_num}"])
                  end
    case File.mkdir_p(tmpdir_path) do
      :ok -> {:ok, tmpdir_path}
      {:error, _} = err -> err
    end
  end

  @doc """
  Given a path to a release output directory, return a list
  of release versions that are present.

  ## Example

      iex> app_dir = Path.join([File.cwd!, "test", "fixtures", "mock_app"])
      ...> output_dir = Path.join([app_dir, "rel", "mock_app"])
      ...> #{__MODULE__}.get_release_versions(output_dir)
      ["0.2.2", "0.2.1-1-d3adb3f", "0.2.1", "0.2.0", "0.1.0"]
  """
  @spec get_release_versions(String.t) :: list(String.t)
  def get_release_versions(output_dir) do
    releases_path = Path.join([output_dir, "releases"])
    case File.exists?(releases_path) do
      false -> []
      true  ->
        releases_path
        |> File.ls!
        |> Enum.reject(fn entry -> entry in ["RELEASES", "start_erl.data"] end)
        |> sort_versions
    end
  end

  @git_describe_pattern ~r/(?<ver>\d+\.\d+\.\d+)-(?<commits>\d+)-(?<sha>[A-Ga-g0-9]+)/
  @doc """
  Sort a list of version strings, in reverse order (i.e. latest version comes first)
  Tries to use semver version compare, but can fall back to regular string compare.
  It also parses git-describe generated version strings and handles ordering them
  correctly.

  ## Example

      iex> #{__MODULE__}.sort_versions(["1.0.2", "1.0.1", "1.0.9", "1.0.10"])
      ["1.0.10", "1.0.9", "1.0.2", "1.0.1"]

      iex> #{__MODULE__}.sort_versions(["0.0.1", "0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef"])
      ["0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef", "0.0.1"]
  """
  @spec sort_versions(list(String.t)) :: list(String.t)
  def sort_versions(versions) do
    versions
    |> Enum.map(fn ver ->
        # Special handling for git-describe versions
        compared = case Regex.named_captures(@git_describe_pattern, ver) do
          nil ->
            {:standard, ver, nil}
          %{"ver" => version, "commits" => n, "sha" => sha} ->
            {:describe, <<version::binary, ?+, n::binary, ?-, sha::binary>>, String.to_integer(n)}
        end
        {ver, compared}
      end)
    |> Enum.sort(
      fn {_, {v1type, v1str, v1_commits_since}}, {_, {v2type, v2str, v2_commits_since}} ->
        case { parse_version(v1str), parse_version(v2str) } do
          {{:semantic, v1}, {:semantic, v2}} ->
            case Version.compare(v1, v2) do
              :gt -> true
              :eq ->
                case {v1type, v2type} do
                  {:standard, :standard} -> v1 > v2 # probably always false
                  {:standard, :describe} -> false   # v2 is an incremental version over v1
                  {:describe, :standard} -> true    # v1 is an incremental version over v2
                  {:describe, :describe} ->         # need to parse out the bits
                    v1_commits_since > v2_commits_since
                end
              :lt -> false
            end;
          {{_, v1}, {_, v2}} ->
            v1 >  v2
        end
      end)
    |> Enum.map(fn {v, _} -> v end)
  end

  defp parse_version(ver) do
    case Version.parse(ver) do
      {:ok, semver} -> {:semantic, semver}
      :error        -> {:unsemantic, ver}
    end
  end

  @doc """
  Fetches a mix dep by name.
  An optional key can be provided, which will be fetched from the Dep struct,
  or a key path, i.e. list of atoms, which will be used via `get_in/2` to fetch
  a value from the struct.
  """
  @spec get_mix_dep(atom) :: Mix.Dep.t | nil
  @spec get_mix_dep(atom, atom | [atom]) :: term | nil
  def get_mix_dep(name, key \\ nil) do
    try do
      [dep] = Mix.Dep.loaded_by_name([name], [])
      case key do
        nil -> dep
        k when is_atom(k) -> get_in(Map.from_struct(dep), [k])
        k when is_list(k) -> get_in(Map.from_struct(dep), k)
      end
    rescue
      _ -> nil
    end
  end

end

defmodule Mix.Releases.Utils do
  @moduledoc false

  @doc """
  Loads a template from :distillery's `priv/templates` directory based on the provided name.
  Any parameters provided are configured as bindings for the template
  """
  @spec template(atom | String.t, Keyword.t) :: {:ok, String.t} | {:error, String.t}
  def template(name, params \\ []) do
    try do
      template_path = Path.join(["#{:code.priv_dir(:distillery)}", "templates", "#{name}.eex"])
      {:ok, EEx.eval_file(template_path, params)}
    rescue
      e in [File.Error] ->
        {:error, "could not read template #{name} (#{inspect e.reason})"}
      e in [CompileError] ->
        {:error, "error in template #{name}, line #{e.line}: #{e.description}"}
      e ->
        {:error, "error in template #{name}: #{e.__struct__.message(e)}"}
    end
  end

  @doc """
  Writes an Elixir/Erlang term to the provided path
  """
  @spec write_term(String.t, term) :: :ok | {:error, term}
  def write_term(path, term) do
    :file.write_file('#{path}', :io_lib.fwrite('~p.\n', [term]), [encoding: :utf8])
  end

  @doc """
  Writes a collection of Elixir/Erlang terms to the provided path
  """
  @spec write_terms(String.t, [term]) :: :ok | {:error, term}
  def write_terms(path, terms) when is_list(terms) do
    contents = String.duplicate("~p.\n\n", Enum.count(terms))
       |> String.to_char_list
       |> :io_lib.fwrite(Enum.reverse(terms))
    :file.write_file('#{path}', contents, [encoding: :utf8])
  end

  @doc """
  Reads a file as Erlang terms
  """
  @spec read_terms(String.t) :: {:ok, [term]} :: {:error, String.t}
  def read_terms(path) do
    case :file.consult(String.to_charlist(path)) do
      {:ok, _} = result ->
        result
      {:error, {line, type, msg}} ->
        {:error, "Parse failed - #{path}@#{line} (#{type}): #{msg}"}
      {:error, reason} ->
        {:error, "Unable to access #{path} (#{reason})"}
    end
  end

  @doc """
  Determines the current ERTS version
  """
  @spec erts_version() :: String.t
  def erts_version, do: "#{:erlang.system_info(:version)}"

  @doc """
  Creates a temporary directory with a random name in a canonical
  temporary files directory of the current system
  (i.e. `/tmp` on *NIX or `./tmp` on Windows)

  Returns an ok tuple with the path of the temp directory, or an error
  tuple with the reason it failed.
  """
  @spec insecure_mkdir_temp() :: {:ok, String.t} | {:error, term}
  def insecure_mkdir_temp() do
    unique_num = trunc(:random.uniform() * 1000000000000)
    tmpdir_path = case :erlang.system_info(:system_architecture) do
                    'win32' ->
                      Path.join(["./tmp", ".tmp_dir#{unique_num}"])
                    _ ->
                      Path.join(["/tmp", ".tmp_dir#{unique_num}"])
                  end
    case File.mkdir_p(tmpdir_path) do
      :ok -> {:ok, tmpdir_path}
      {:error, _} = err -> err
    end
  end

  @doc """
  Given a path to a release output directory, return a list
  of release versions that are present.

  ## Example

      iex> app_dir = Path.join([File.cwd!, "test", "fixtures", "mock_app"])
      ...> output_dir = Path.join([app_dir, "rel", "mock_app"])
      ...> #{__MODULE__}.get_release_versions(output_dir)
      ["0.2.2", "0.2.1-1-d3adb3f", "0.2.1", "0.2.0", "0.1.0"]
  """
  @spec get_release_versions(String.t) :: list(String.t)
  def get_release_versions(output_dir) do
    releases_path = Path.join([output_dir, "releases"])
    case File.exists?(releases_path) do
      false -> []
      true  ->
        releases_path
        |> File.ls!
        |> Enum.reject(fn entry -> entry in ["RELEASES", "start_erl.data"] end)
        |> sort_versions
    end
  end

  @git_describe_pattern ~r/(?<ver>\d+\.\d+\.\d+)-(?<commits>\d+)-(?<sha>[A-Ga-g0-9]+)/
  @doc """
  Sort a list of version strings, in reverse order (i.e. latest version comes first)
  Tries to use semver version compare, but can fall back to regular string compare.
  It also parses git-describe generated version strings and handles ordering them
  correctly.

  ## Example

      iex> #{__MODULE__}.sort_versions(["1.0.2", "1.0.1", "1.0.9", "1.0.10"])
      ["1.0.10", "1.0.9", "1.0.2", "1.0.1"]

      iex> #{__MODULE__}.sort_versions(["0.0.1", "0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef"])
      ["0.0.2", "0.0.1-2-a1d2g3f", "0.0.1-1-deadbeef", "0.0.1"]
  """
  @spec sort_versions(list(String.t)) :: list(String.t)
  def sort_versions(versions) do
    versions
    |> Enum.map(fn ver ->
        # Special handling for git-describe versions
        compared = case Regex.named_captures(@git_describe_pattern, ver) do
          nil ->
            {:standard, ver, nil}
          %{"ver" => version, "commits" => n, "sha" => sha} ->
            {:describe, <<version::binary, ?+, n::binary, ?-, sha::binary>>, String.to_integer(n)}
        end
        {ver, compared}
      end)
    |> Enum.sort(
      fn {_, {v1type, v1str, v1_commits_since}}, {_, {v2type, v2str, v2_commits_since}} ->
        case { parse_version(v1str), parse_version(v2str) } do
          {{:semantic, v1}, {:semantic, v2}} ->
            case Version.compare(v1, v2) do
              :gt -> true
              :eq ->
                case {v1type, v2type} do
                  {:standard, :standard} -> v1 > v2 # probably always false
                  {:standard, :describe} -> false   # v2 is an incremental version over v1
                  {:describe, :standard} -> true    # v1 is an incremental version over v2
                  {:describe, :describe} ->         # need to parse out the bits
                    v1_commits_since > v2_commits_since
                end
              :lt -> false
            end;
          {{_, v1}, {_, v2}} ->
            v1 >  v2
        end
      end)
    |> Enum.map(fn {v, _} -> v end)
  end

  defp parse_version(ver) do
    case Version.parse(ver) do
      {:ok, semver} -> {:semantic, semver}
      :error        -> {:unsemantic, ver}
    end
  end

  @doc """
  Fetches a mix dep by name.
  An optional key can be provided, which will be fetched from the Dep struct,
  or a key path, i.e. list of atoms, which will be used via `get_in/2` to fetch
  a value from the struct.
  """
  @spec get_mix_dep(atom) :: Mix.Dep.t | nil
  @spec get_mix_dep(atom, atom | [atom]) :: term | nil
  def get_mix_dep(name, key \\ nil) do
    try do
      [dep] = Mix.Dep.loaded_by_name([name], [])
      case key do
        nil -> dep
        k when is_atom(k) -> get_in(Map.from_struct(dep), [k])
        k when is_list(k) -> get_in(Map.from_struct(dep), k)
      end
    rescue
      e -> IO.inspect(e); nil
    end
  end

end


docs: improve poorly worded phrasing

docs: improve poorly worded phrasing

defmodule Tesla.Middleware.Retry do
  @moduledoc """
  Retry using exponential backoff and full jitter. This middleware only retries in the
  case of connection errors (`nxdomain`, `connrefused` etc). Application error
  checking for retry can be customized through `:should_retry` option by
  providing a function in returning a boolean.

  ## Backoff algorithm

  The backoff algorithm optimizes for tight bounds on completing a request successfully.
  It does this by first calculating an exponential backoff factor based on the
  number of retries that have been performed. It then multiplies this factor against the
  base delay. The total maximum delay is found by taking the minimum of either the calculated delay
  or the maximum delay specified. This creates an upper bound on the maximum delay
  we can see.

  In order to find the actual delay value we apply additive noise which is proportional to the
  current desired delay. This ensures that the actual delay is kept within the expected order of magnitude,
  while still having some randomness, which ensures that our retried requests don't "harmonize" making it harder for the downstream
  service to heal.

  ## Example

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Retry,
      delay: 500,
      max_retries: 10,
      max_delay: 4_000,
      should_retry: fn
        {:ok, %{status: status}} when status in [400, 500] -> true
        {:ok, _} -> false
        {:error, _} -> true
      end
  end
  ```

  ## Options

  - `:delay` - The base delay in milliseconds (positive integer, defaults to 50)
  - `:max_retries` - maximum number of retries (non-negative integer, defaults to 5)
  - `:max_delay` - maximum delay in milliseconds (positive integer, defaults to 5000)
  - `:should_retry` - function to determine if request should be retried
  - `:jitter_factor` -
  """

  # Not necessary in Elixir 1.10+
  use Bitwise, skip_operators: true

  @behaviour Tesla.Middleware

  @defaults [
    delay: 50,
    max_retries: 5,
    max_delay: 5_000,
    jitter_factor: 0.2
  ]

  @impl Tesla.Middleware
  def call(env, next, opts) do
    opts = opts || []

    context = %{
      retries: 0,
      delay: integer_opt!(opts, :delay, 1),
      max_retries: integer_opt!(opts, :max_retries, 0),
      max_delay: integer_opt!(opts, :max_delay, 1),
      should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1)),
      jitter_factor: float_opt!(opts, :jitter_factor, 0, 1)
    }

    retry(env, next, context)
  end

  # If we have max retries set to 0 don't retry
  defp retry(env, next, %{max_retries: 0}), do: Tesla.run(env, next)

  # If we're on our last retry then just run and don't handle the error
  defp retry(env, next, %{max_retries: max, retries: max}) do
    Tesla.run(env, next)
  end

  # Otherwise we retry if we get a retriable error
  defp retry(env, next, context) do
    res = Tesla.run(env, next)

    if context.should_retry.(res) do
      backoff(context.max_delay, context.delay, context.retries, context.jitter_factor)
      context = update_in(context, [:retries], &(&1 + 1))
      retry(env, next, context)
    else
      res
    end
  end

  # Exponential backoff with jitter
  defp backoff(cap, base, attempt, jitter_factor) do
    factor = :math.pow(2, attempt)
    max_sleep = min(cap, base * factor)

    # This ensures that the delay's order of magnitude is kept intact,
    # while still having some jitter.

    # This generates a value x where 1-jitter_factor <= x <= 1 + jitter_factor
    jitter = 1 + 2 * jitter_factor * :rand.uniform() - jitter_factor

    # The actual delay is in the range max_sleep * (1 - jitter_factor), max_sleep * (1 + jitter_factor)
    delay = trunc(max_sleep + jitter)

    :timer.sleep(delay)
  end

  defp integer_opt!(opts, key, min) do
    case Keyword.fetch(opts, key) do
      {:ok, value} when is_integer(value) and value >= min -> value
      {:ok, invalid} -> invalid_integer(key, invalid, min)
      :error -> @defaults[key]
    end
  end

  defp float_opt!(opts, key, min, max) do
    case Keyword.fetch(opts, key) do
      {:ok, value} when is_float(value) and value >= min and value <= max -> value
      {:ok, invalid} -> invalid_float(key, invalid, min, max)
      :error -> @defaults[key]
    end
  end

  defp invalid_integer(key, value, min) do
    raise(ArgumentError, "expected :#{key} to be an integer >= #{min}, got #{inspect(value)}")
  end

  defp invalid_float(key, value, min, max) do
    raise(
      ArgumentError,
      "expected :#{key} to be a float >= #{min} and <= #{max}, got #{inspect(value)}"
    )
  end
end

defmodule Tesla.Middleware.Retry do
  @moduledoc """
  Retry using exponential backoff and full jitter. This middleware only retries in the
  case of connection errors (`nxdomain`, `connrefused` etc). Application error
  checking for retry can be customized through `:should_retry` option by
  providing a function in returning a boolean.

  ## Backoff algorithm

  The backoff algorithm optimizes for tight bounds on completing a request successfully.
  It does this by first calculating an exponential backoff factor based on the
  number of retries that have been performed. It then multiplies this factor against the
  base delay. The total maximum delay is found by taking the minimum of either the calculated delay
  or the maximum delay specified. This creates an upper bound on the maximum delay
  we can see.

  In order to find the actual delay value we apply a proportional noise which is proportional to the
  current desired delay. This ensures that the actual delay is kept within the expected order of magnitude,
  while still having some randomness, which ensures that our retried requests don't "harmonize" making it harder for the downstream
  service to heal.

  ## Example

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Retry,
      delay: 500,
      max_retries: 10,
      max_delay: 4_000,
      should_retry: fn
        {:ok, %{status: status}} when status in [400, 500] -> true
        {:ok, _} -> false
        {:error, _} -> true
      end
  end
  ```

  ## Options

  - `:delay` - The base delay in milliseconds (positive integer, defaults to 50)
  - `:max_retries` - maximum number of retries (non-negative integer, defaults to 5)
  - `:max_delay` - maximum delay in milliseconds (positive integer, defaults to 5000)
  - `:should_retry` - function to determine if request should be retried
  - `:jitter_factor` -
  """

  # Not necessary in Elixir 1.10+
  use Bitwise, skip_operators: true

  @behaviour Tesla.Middleware

  @defaults [
    delay: 50,
    max_retries: 5,
    max_delay: 5_000,
    jitter_factor: 0.2
  ]

  @impl Tesla.Middleware
  def call(env, next, opts) do
    opts = opts || []

    context = %{
      retries: 0,
      delay: integer_opt!(opts, :delay, 1),
      max_retries: integer_opt!(opts, :max_retries, 0),
      max_delay: integer_opt!(opts, :max_delay, 1),
      should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1)),
      jitter_factor: float_opt!(opts, :jitter_factor, 0, 1)
    }

    retry(env, next, context)
  end

  # If we have max retries set to 0 don't retry
  defp retry(env, next, %{max_retries: 0}), do: Tesla.run(env, next)

  # If we're on our last retry then just run and don't handle the error
  defp retry(env, next, %{max_retries: max, retries: max}) do
    Tesla.run(env, next)
  end

  # Otherwise we retry if we get a retriable error
  defp retry(env, next, context) do
    res = Tesla.run(env, next)

    if context.should_retry.(res) do
      backoff(context.max_delay, context.delay, context.retries, context.jitter_factor)
      context = update_in(context, [:retries], &(&1 + 1))
      retry(env, next, context)
    else
      res
    end
  end

  # Exponential backoff with jitter
  defp backoff(cap, base, attempt, jitter_factor) do
    factor = :math.pow(2, attempt)
    max_sleep = min(cap, base * factor)

    # This ensures that the delay's order of magnitude is kept intact,
    # while still having some jitter.

    # This generates a value x where 1-jitter_factor <= x <= 1 + jitter_factor
    jitter = 1 + 2 * jitter_factor * :rand.uniform() - jitter_factor

    # The actual delay is in the range max_sleep * (1 - jitter_factor), max_sleep * (1 + jitter_factor)
    delay = trunc(max_sleep + jitter)

    :timer.sleep(delay)
  end

  defp integer_opt!(opts, key, min) do
    case Keyword.fetch(opts, key) do
      {:ok, value} when is_integer(value) and value >= min -> value
      {:ok, invalid} -> invalid_integer(key, invalid, min)
      :error -> @defaults[key]
    end
  end

  defp float_opt!(opts, key, min, max) do
    case Keyword.fetch(opts, key) do
      {:ok, value} when is_float(value) and value >= min and value <= max -> value
      {:ok, invalid} -> invalid_float(key, invalid, min, max)
      :error -> @defaults[key]
    end
  end

  defp invalid_integer(key, value, min) do
    raise(ArgumentError, "expected :#{key} to be an integer >= #{min}, got #{inspect(value)}")
  end

  defp invalid_float(key, value, min, max) do
    raise(
      ArgumentError,
      "expected :#{key} to be a float >= #{min} and <= #{max}, got #{inspect(value)}"
    )
  end
end


Optimize backoff factor calculation

Optimize backoff factor calculation

By favoring Bitwise.bsl over :math.pow for powers of 2 we make it faster and
avoid the need to truncate it afterwards.

defmodule Tesla.Middleware.Retry do
  @moduledoc """
  Retry using exponential backoff and full jitter. This middleware only retries in the
  case of connection errors (`nxdomain`, `connrefused` etc). Application error
  checking for retry can be customized through `:should_retry` option by
  providing a function in returning a boolean.

  ## Backoff algorithm

  The backoff algorithm optimizes for tight bounds on completing a request successfully.
  It does this by first calculating an exponential backoff factor based on the
  number of retries that have been performed. It then multiplies this factor against the
  base delay. The total maximum delay is found by taking the minimum of either the calculated delay
  or the maximum delay specified. This creates an upper bound on the maximum delay
  we can see.

  In order to find the actual delay value we take a random number between 0 and
  the maximum delay based on a uniform distribution. This randomness ensures that
  our retried requests don't "harmonize" making it harder for the downstream
  service to heal.

  ## Example

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Retry,
      delay: 500,
      max_retries: 10,
      max_delay: 4_000,
      should_retry: fn
        {:ok, %{status: status}} when status in [400, 500] -> true
        {:ok, _} -> false
        {:error, _} -> true
      end
  end
  ```

  ## Options

  - `:delay` - The base delay in milliseconds (defaults to 50)
  - `:max_retries` - maximum number of retries (defaults to 5)
  - `:max_delay` - maximum delay in milliseconds (defaults to 5000)
  - `:should_retry` - function to determine if request should be retried
  """

  # Not necessary in Elixir 1.10+
  use Bitwise, skip_operators: true

  @behaviour Tesla.Middleware

  @defaults [
    delay: 50,
    max_retries: 5,
    max_delay: 5_000
  ]

  @impl Tesla.Middleware
  def call(env, next, opts) do
    opts = opts || []

    context = %{
      retries: 0,
      delay: Keyword.get(opts, :delay, @defaults[:delay]),
      max_retries: Keyword.get(opts, :max_retries, @defaults[:max_retries]),
      max_delay: Keyword.get(opts, :max_delay, @defaults[:max_delay]),
      should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1))
    }

    retry(env, next, context)
  end

  # If we have max retries set to 0 don't retry
  defp retry(env, next, %{max_retries: 0}), do: Tesla.run(env, next)

  # If we're on our last retry then just run and don't handle the error
  defp retry(env, next, %{max_retries: max, retries: max}) do
    Tesla.run(env, next)
  end

  # Otherwise we retry if we get a retriable error
  defp retry(env, next, context) do
    res = Tesla.run(env, next)

    if context.should_retry.(res) do
      backoff(context.max_delay, context.delay, context.retries)
      context = update_in(context, [:retries], &(&1 + 1))
      retry(env, next, context)
    else
      res
    end
  end

  # Exponential backoff with jitter
  defp backoff(cap, base, attempt) do
    factor = Bitwise.bsl(1, attempt)
    max_sleep = min(cap, base * factor)
    delay = :rand.uniform(max_sleep)

    :timer.sleep(delay)
  end
end

defmodule Tesla.Middleware.Retry do
  @moduledoc """
  Retry using exponential backoff and full jitter. This middleware only retries in the
  case of connection errors (`nxdomain`, `connrefused` etc). Application error
  checking for retry can be customized through `:should_retry` option by
  providing a function in returning a boolean.

  ## Backoff algorithm

  The backoff algorithm optimizes for tight bounds on completing a request successfully.
  It does this by first calculating an exponential backoff factor based on the
  number of retries that have been performed. It then multiplies this factor against the
  base delay. The total maximum delay is found by taking the minimum of either the calculated delay
  or the maximum delay specified. This creates an upper bound on the maximum delay
  we can see.

  In order to find the actual delay value we take a random number between 0 and
  the maximum delay based on a uniform distribution. This randomness ensures that
  our retried requests don't "harmonize" making it harder for the downstream
  service to heal.

  ## Example

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Retry,
      delay: 500,
      max_retries: 10,
      max_delay: 4_000,
      should_retry: fn
        {:ok, %{status: status}} when status in [400, 500] -> true
        {:ok, _} -> false
        {:error, _} -> true
      end
  end
  ```

  ## Options

  - `:delay` - The base delay in milliseconds (defaults to 50)
  - `:max_retries` - maximum number of retries (defaults to 5)
  - `:max_delay` - maximum delay in milliseconds (defaults to 5000)
  - `:should_retry` - function to determine if request should be retried
  """

  @behaviour Tesla.Middleware

  @defaults [
    delay: 50,
    max_retries: 5,
    max_delay: 5_000
  ]

  @impl Tesla.Middleware
  def call(env, next, opts) do
    opts = opts || []

    context = %{
      retries: 0,
      delay: Keyword.get(opts, :delay, @defaults[:delay]),
      max_retries: Keyword.get(opts, :max_retries, @defaults[:max_retries]),
      max_delay: Keyword.get(opts, :max_delay, @defaults[:max_delay]),
      should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1))
    }

    retry(env, next, context)
  end

  # If we have max retries set to 0 don't retry
  defp retry(env, next, %{max_retries: 0}), do: Tesla.run(env, next)

  # If we're on our last retry then just run and don't handle the error
  defp retry(env, next, %{max_retries: max, retries: max}) do
    Tesla.run(env, next)
  end

  # Otherwise we retry if we get a retriable error
  defp retry(env, next, context) do
    res = Tesla.run(env, next)

    if context.should_retry.(res) do
      backoff(context.max_delay, context.delay, context.retries)
      context = update_in(context, [:retries], &(&1 + 1))
      retry(env, next, context)
    else
      res
    end
  end

  # Exponential backoff with jitter
  defp backoff(cap, base, attempt) do
    factor = :math.pow(2, attempt)
    max_sleep = trunc(min(cap, base * factor))
    delay = :rand.uniform(max_sleep)

    :timer.sleep(delay)
  end
end


fixed default dashboard

fixed default dashboard

defmodule ExAdmin.Page do
  @moduledoc """
  Define pages in ExAdmin that don't render models, like a dashboard
  page.

  """

  import ExAdmin.DslUtils
  import ExAdmin.Theme.Helpers

  defmacro __using__(_) do
    quote do
      import ExAdmin.Form, except: [content: 1]
      import unquote(__MODULE__)
    end
  end

  @doc """
  Display contents on a page. Use Xain markup to create the page.

  ## Examples

      register_page "Dashboard" do
        menu priority: 1, label: "Dashboard"
        content do
          columns do
            column do
              panel "Recent Orders" do
                Repo.all Order.complete(5)
                |> table_for do
                  column "State", fn(o) -> status_tag Order.state(o) end
                  column "Customer", fn(o) ->
                    a o.user.email, href: "/admin/users/\#{o.user.id}"
                  end
                  column "Total", fn(o) -> text decimal_to_currency(o.total_price) end
                end
              end
            end
            column do
              panel "Recent Customers" do
                order_by(User, desc: :id)
                |> limit(5)
                |> Repo.all
                |> table_for do
                  column "email", fn(c) -> a c.email, href: "/admin/users/\#{c.id}" end
                end
              end
            end
          end
        end
      end
  """
  defmacro content(opts \\ [], do: block) do

    bdy = quote do
      unquote(block)
    end

    quote location: :keep, bind_quoted: [opts: escape(opts), bdy: escape(bdy)] do
      def page_view(var!(conn)) do
        import Kernel, except: [div: 2, to_string: 1]
        import ExAdmin.ViewHelpers
        use Xain
        markup safe: true do
          unquote(bdy)
        end
      end
    end

  end

  @doc """
  Start one or more columns.
  """
  defmacro columns(do: block) do
    quote do
      var!(columns, ExAdmin.Show) = []
      var!(columns, ExAdmin.Page) = []
      unquote(block)
      cols = var!(columns, ExAdmin.Page) |> Enum.reverse
      theme_module(Page).columns(cols)
      var!(columns, ExAdmin.Page) = []
    end
  end

  @doc """
  Define a column.
  """
  defmacro column([do: block]) do
    quote do
      html = markup :nested do
        unquote(block)
      end
      var!(columns, ExAdmin.Page) = [html | var!(columns, ExAdmin.Page)]
    end
  end

end

defmodule ExAdmin.Page do
  @moduledoc """
  Define pages in ExAdmin that don't render models, like a dashboard
  page.

  """

  import ExAdmin.DslUtils
  import ExAdmin.Theme.Helpers

  defmacro __using__(_) do
    quote do
      import ExAdmin.Form, except: [content: 1]
      import unquote(__MODULE__)
    end
  end

  @doc """
  Display contents on a page. Use Xain markup to create the page.

  ## Examples

      register_page "Dashboard" do
        menu priority: 1, label: "Dashboard"
        content do
          columns do
            column do
              panel "Recent Orders" do
                Repo.all Order.complete(5)
                |> table_for do
                  column "State", fn(o) -> status_tag Order.state(o) end
                  column "Customer", fn(o) ->
                    a o.user.email, href: "/admin/users/\#{o.user.id}"
                  end
                  column "Total", fn(o) -> text decimal_to_currency(o.total_price) end
                end
              end
            end
            column do
              panel "Recent Customers" do
                order_by(User, desc: :id)
                |> limit(5)
                |> Repo.all
                |> table_for do
                  column "email", fn(c) -> a c.email, href: "/admin/users/\#{c.id}" end
                end
              end
            end
          end
        end
      end
  """
  defmacro content(opts \\ [], do: block) do

    bdy = quote do
      unquote(block)
    end

    quote location: :keep, bind_quoted: [opts: escape(opts), bdy: escape(bdy)] do
      def page_view(var!(conn)) do
        import Kernel, except: [div: 2, to_string: 1]
        import ExAdmin.ViewHelpers
        use Xain
        markup do
          unquote(bdy)
        end
      end
    end

  end

  @doc """
  Start one or more columns.
  """
  defmacro columns(do: block) do
    quote do
      var!(columns, ExAdmin.Show) = []
      var!(columns, ExAdmin.Page) = []
      unquote(block)
      cols = var!(columns, ExAdmin.Page) |> Enum.reverse
      theme_module(Page).columns(cols)
      var!(columns, ExAdmin.Page) = []
    end
  end

  @doc """
  Define a column.
  """
  defmacro column([do: block]) do
    quote do
      html = markup :nested do
        unquote(block)
      end
      var!(columns, ExAdmin.Page) = [html | var!(columns, ExAdmin.Page)]
    end
  end

end