lib/mix/tasks/harness.loadpaths.ex

defmodule Mix.Tasks.Harness.Loadpaths do
  @shortdoc "Checks, compiles, and loads all harness packages"
  @moduledoc """
  Checks, compiles, and loads all harness packages

  ## Command line options

    * `--no-compile` - skips the compilation of harness packages
    * `--no-deps-check` - skips a check for out of date dependencies
  """

  # the upstream of this code is here: https://github.com/elixir-lang/elixir/blob/58518794306c70204de14f9ed214fb7f296769d9/lib/mix/lib/mix/tasks/deps.loadpaths.ex
  # this code should be considered copyright the Elixir Core Team

  use Mix.Task

  @impl Mix.Task
  def run(args) do
    path = "."

    Harness.Manifest.load(path)

    unless "--no-deps-check" in args do
      Mix.Dep.load_and_cache()
      |> deps_check("--no-compile" in args)
    end
  end

  defp deps_check(deps, no_compile?) do
    deps = Enum.map(deps, &Mix.Dep.check_lock/1)
    {not_ok, compile} = partition(deps, [], [])

    cond do
      not_ok != [] ->
        show_not_ok!(not_ok)

      compile == [] or no_compile? ->
        :ok

      true ->
        Mix.Tasks.Deps.Compile.compile(compile)

        compile
        |> Enum.map(& &1.app)
        |> Mix.Dep.filter_by_name(Mix.Dep.load_and_cache())
        |> Enum.filter(&(not Mix.Dep.ok?(&1)))
        |> show_not_ok!
    end
  end

  defp partition([dep | deps], not_ok, compile) do
    cond do
      Mix.Dep.compilable?(dep) or
          (Mix.Dep.ok?(dep) and dep.scm.fetchable? == false) ->
        if dep.opts[:from_umbrella] do
          partition(deps, not_ok, compile)
        else
          partition(deps, not_ok, [dep | compile])
        end

      Mix.Dep.ok?(dep) ->
        partition(deps, not_ok, compile)

      true ->
        partition(deps, [dep | not_ok], compile)
    end
  end

  defp partition([], not_ok, compile) do
    {Enum.reverse(not_ok), Enum.reverse(compile)}
  end

  defp show_not_ok!([]) do
    :ok
  end

  defp show_not_ok!(deps) do
    shell = Mix.shell()
    shell.error("Unchecked dependencies for environment #{Mix.env()}:")

    Enum.each(deps, fn dep ->
      shell.error("* #{Mix.Dep.format_dep(dep)}")
      shell.error("  #{format_status(dep)}")
    end)

    Mix.raise("Can't continue due to errors on dependencies")
  end

  defp format_status(mix_dep) do
    mix_dep
    |> Mix.Dep.format_status()
    |> String.replace(~r/mix deps.get/, "mix harness.get")
    |> String.replace(~r/mix deps.compile/, "mix harness.compile")
    |> String.replace(~r/mix.lock/, "harness.lock")
  end
end