Clarify Mix.Project usage

Clarify Mix.Project usage

defmodule Mix.Project do
  @moduledoc """
  Defines and manipulates Mix projects.

  A Mix project is defined by calling `use Mix.Project` in a module, usually
  placed in `mix.exs`:

      defmodule MyApp.MixProject do
        use Mix.Project

        def project do
          [
            app: :my_app,
            version: "1.0.0"
          ]
        end
      end

  ## Configuration

  In order to configure Mix, the module that `use`s `Mix.Project` should export
  a `project/0` function that returns a keyword list representing configuration
  for the project.

  This configuration can be read using `Mix.Project.config/0`. Note that
  `config/0` won't fail if a project is not defined; this allows many Mix tasks
  to work without a project.

  If a task requires a project to be defined or needs to access a
  special function within the project, the task can call `Mix.Project.get!/0`
  which fails with `Mix.NoProjectError` in the case a project is not
  defined.

  There isn't a comprehensive list of all the options that can be returned by
  `project/0` since many Mix tasks define their own options that they read from
  this configuration. For example, look at the "Configuration" section in the
  documentation for the `Mix.Tasks.Compile` task.

  These are a few options that are not used by just one Mix task (and will thus
  be documented here):

    * `:build_per_environment` - if `true`, builds will be *per-environment*. If
      `false`, builds will go in `_build/shared` regardless of the Mix
      environment. Defaults to `true`.

    * `:aliases` - a list of task aliases. For more information, check out the
      "Aliases" section in the documentation for the `Mix` module. Defaults to
      `[]`.

    * `:config_path` - a string representing the path of the main config
      file. See `config_files/0` for more information. Defaults to
      `"config/config.exs"`.

    * `:default_task` - a string representing the default task to be run by
      `mix` when no task is specified. Defaults to `"run"`.

    * `:deps` - a list of dependencies of this project. Refer to the
      documentation for the `Mix.Tasks.Deps` task for more information. Defaults
      to `[]`.

    * `:deps_path` - directory where dependencies are stored. Also see
      `deps_path/1`. Defaults to `"deps"`.

    * `:lockfile` - the name of the lockfile used by the `mix deps.*` family of
      tasks. Defaults to `"mix.lock"`.

    * `:preferred_cli_env` - a keyword list of `{task, env}` tuples where `task`
      is the task name as an atom (for example, `:"deps.get"`) and `env` is the
      preferred environment (for example, `:test`). This option overrides what
      is specified by the tasks with the `@preferred_cli_env` attribute (see the
      docs for `Mix.Task`). Defaults to `[]`.

    * `:preferred_cli_target` - a keyword list of `{task, target}` tuples where
      `task` is the task name as an atom (for example, `:test`) and `target`
      is the preferred target (for example, `:host`). Defaults to `[]`.

  For more options, keep an eye on the documentation for single Mix tasks; good
  examples are the `Mix.Tasks.Compile` task and all the specific compiler tasks
  (such as `Mix.Tasks.Compile.Elixir` or `Mix.Tasks.Compile.Erlang`).

  Note that sometimes the same configuration option is mentioned in the
  documentation for different tasks; this is just because it's common for many
  tasks to read and use the same configuration option (for example,
  `:erlc_paths` is used by `mix compile.erlang`, `mix compile.yecc`, and other
  tasks).

  ## Erlang projects

  Mix can be used to manage Erlang projects that don't have any Elixir code. To
  ensure Mix tasks work correctly for an Erlang project, `language: :erlang` has
  to be part of the configuration returned by `project/0`. This setting also
  makes sure Elixir is not added as a dependency to the generated `.app` file or
  to the escript generated with `mix escript.build`, and so on.

  ## Invoking this module

  This module contains many functions that return project information and
  metadata. However, since Mix is not included nor configured during releases,
  we recommend using the functions in this module only inside Mix tasks.
  If you need to configure your own app, consider using the application
  environment instead. For example, don't do this:

      def some_config do
        Mix.Project.config()[:some_config]
      end

  Nor this:

      @some_config Mix.Project.config()[:some_config]

  Instead, do this:

      def some_config do
        Application.get_env(:my_app, :some_config)
      end

  Or this:

      @some_config Application.compile_env(:my_app, :some_config)

  """

  @doc false
  defmacro __using__(_) do
    quote do
      @after_compile Mix.Project
    end
  end

  # Invoked after each Mix.Project is compiled.
  @doc false
  def __after_compile__(env, _binary) do
    push(env.module, env.file)
  end

  # Push a project onto the project stack.
  # Only the top of the stack can be accessed.
  @doc false
  def push(module, file \\ nil, app \\ nil) when is_atom(module) do
    file = file || (module && List.to_string(module.__info__(:compile)[:source]))
    config = Keyword.merge([app: app] ++ default_config(), get_project_config(module))

    case Mix.ProjectStack.push(module, config, file) do
      :ok ->
        :ok

      {:error, other} when is_binary(other) ->
        Mix.raise(
          "Trying to load #{inspect(module)} from #{inspect(file)}" <>
            " but another project with the same name was already defined at #{inspect(other)}"
        )
    end
  end

  # Pops a project from the stack.
  @doc false
  def pop do
    Mix.ProjectStack.pop()
  end

  # The configuration that is pushed down to dependencies.
  @doc false
  def deps_config(config \\ config()) do
    [
      build_embedded: config[:build_embedded],
      build_per_environment: config[:build_per_environment],
      consolidate_protocols: false,
      consolidation_path: consolidation_path(config),
      deps_path: deps_path(config),
      env_path: build_path(config)
    ]
  end

  @doc """
  Retrieves the current project if there is one.

  If there is no current project, `nil` is returned. This
  may happen in cases there is no `mix.exs` in the current
  directory.

  If you expect a project to be defined, i.e., it is a
  requirement of the current task, you should call
  `get!/0` instead.
  """
  @spec get() :: module | nil
  def get do
    case Mix.ProjectStack.peek() do
      %{name: name} -> name
      _ -> nil
    end
  end

  @doc """
  Same as `get/0`, but raises an exception if there is no current project.

  This is usually called by tasks that need additional
  functions on the project to be defined. Since such
  tasks usually depend on a project being defined, this
  function raises a `Mix.NoProjectError` exception in
  case no project is available.
  """
  @spec get!() :: module
  def get! do
    get() || raise Mix.NoProjectError, []
  end

  @doc """
  Returns the path to the file that defines the current project.

  The majority of the time, it will point to a `mix.exs` file.
  Returns nil if not inside a project.
  """
  @doc since: "1.13.0"
  @spec project_file() :: binary | nil
  defdelegate project_file(), to: Mix.ProjectStack

  @doc """
  Returns the project configuration.

  If there is no project defined, it still returns a keyword
  list with default values. This allows many Mix tasks to work
  without the need for an underlying project.

  Note this configuration is cached once the project is
  pushed onto the stack. Calling it multiple times won't
  cause it to be recomputed.

  Do not use `Mix.Project.config/0` to find the runtime configuration.
  Use it only to configure aspects of your project (like
  compilation directories) and not your application runtime.
  """
  @spec config() :: keyword
  def config do
    case Mix.ProjectStack.peek() do
      %{config: config} -> config
      _ -> default_config()
    end
  end

  @doc """
  Returns a list of project configuration files for this project.

  This function is usually used in compilation tasks to trigger
  a full recompilation whenever such configuration files change.

  It returns the lock manifest, and all config files in the `config`
  directory that do not start with a leading period (for example,
  `.my_config.exs`).

  Note: before Elixir v1.13.0, the `mix.exs` file was also included
  as a config file, but since then it has been moved to its own
  function called `project_file/0`.
  """
  @spec config_files() :: [Path.t()]
  def config_files do
    Mix.ProjectStack.config_files()
  end

  @doc """
  Returns the latest modification time from config files.

  This function is usually used in compilation tasks to trigger
  a full recompilation whenever such configuration files change.
  For this reason, the mtime is cached to avoid file system lookups.

  Note: before Elixir v1.13.0, the `mix.exs` file was also included
  in the mtimes, but not anymore. You can compute its modification
  date by calling  `project_file/0`.
  """
  @doc since: "1.7.0"
  @spec config_mtime() :: posix_mtime when posix_mtime: integer()
  def config_mtime do
    Mix.ProjectStack.config_mtime()
  end

  @doc """
  Returns `true` if `config` is the configuration for an umbrella project.

  When called with no arguments, tells whether the current project is
  an umbrella project.
  """
  @spec umbrella?(keyword) :: boolean
  def umbrella?(config \\ config()) do
    config[:apps_path] != nil
  end

  @doc """
  Returns a map with the umbrella child applications paths.

  These paths are based on the `:apps_path` and `:apps` configurations.

  If the given project configuration identifies an umbrella project, the return
  value is a map of `app => path` where `app` is a child app of the umbrella and
  `path` is its path relative to the root of the umbrella project.

  If the given project configuration does not identify an umbrella project,
  `nil` is returned.

  ## Examples

      Mix.Project.apps_paths()
      #=> %{my_app1: "apps/my_app1", my_app2: "apps/my_app2"}

  """
  @doc since: "1.4.0"
  @spec apps_paths(keyword) :: %{optional(atom) => Path.t()} | nil
  def apps_paths(config \\ config()) do
    if apps_path = config[:apps_path] do
      key = {:apps_paths, Mix.Project.get!()}

      if cache = Mix.State.read_cache(key) do
        cache
      else
        cache = config[:apps] |> umbrella_apps(apps_path) |> to_apps_paths(apps_path)
        Mix.State.write_cache(key, cache)
      end
    end
  end

  defp umbrella_apps(nil, apps_path) do
    case File.ls(apps_path) do
      {:ok, apps} -> Enum.map(apps, &String.to_atom/1)
      {:error, _} -> []
    end
  end

  defp umbrella_apps(apps, _apps_path) when is_list(apps) do
    apps
  end

  defp to_apps_paths(apps, apps_path) do
    for app <- apps,
        path = path_with_mix_exs_otherwise_warn(app, apps_path),
        do: {app, path},
        into: %{}
  end

  defp path_with_mix_exs_otherwise_warn(app, apps_path) do
    path = Path.join(apps_path, Atom.to_string(app))

    cond do
      File.regular?(Path.join(path, "mix.exs")) ->
        path

      File.dir?(path) ->
        Mix.shell().error(
          "warning: path #{inspect(Path.relative_to_cwd(path))} is a directory but " <>
            "it has no mix.exs. Mix won't consider this directory as part of your " <>
            "umbrella application. Please add a \"mix.exs\" or set the \":apps\" key " <>
            "in your umbrella configuration with all relevant apps names as atoms"
        )

        nil

      true ->
        # If it is a stray file, we just ignore it.
        nil
    end
  end

  @doc ~S"""
  Runs the given `fun` inside the given project.

  This function changes the current working directory and
  loads the project at the given directory onto the project
  stack.

  A `post_config` can be passed that will be merged into
  the project configuration.

  `fun` is called with the module name of the given `Mix.Project`.
  The return value of this function is the return value of `fun`.

  ## Examples

      Mix.Project.in_project(:my_app, "/path/to/my_app", fn module ->
        "Mix project is: #{inspect(module)}"
      end)
      #=> "Mix project is: MyApp.MixProject"

  """
  @spec in_project(atom, Path.t(), keyword, (module -> result)) :: result when result: term
  def in_project(app, path, post_config \\ [], fun)

  def in_project(app, ".", post_config, fun) when is_atom(app) do
    cached =
      try do
        load_project(app, post_config)
      rescue
        any ->
          Mix.shell().error("Error while loading project #{inspect(app)} at #{File.cwd!()}")
          reraise any, __STACKTRACE__
      end

    try do
      fun.(cached)
    after
      Mix.Project.pop()
    end
  end

  def in_project(app, path, post_config, fun) when is_atom(app) do
    File.cd!(path, fn ->
      in_project(app, ".", post_config, fun)
    end)
  end

  @doc """
  Returns the path where dependencies are stored for the given project.

  If no configuration is given, the one for the current project is used.

  The returned path will be expanded.

  ## Examples

      Mix.Project.deps_path()
      #=> "/path/to/project/deps"

  """
  @spec deps_path(keyword) :: Path.t()
  def deps_path(config \\ config()) do
    dir = System.get_env("MIX_DEPS_PATH") || config[:deps_path]
    Path.expand(dir)
  end

  @doc """
  Returns all dependencies app names.

  The order they are returned is guaranteed to be sorted
  for proper dependency resolution. For example, if A
  depends on B, then B will listed before A.
  """
  @doc since: "1.11.0"
  @spec deps_apps() :: [atom()]
  def deps_apps() do
    Mix.Dep.cached() |> Enum.map(& &1.app)
  end

  @doc """
  Returns the SCMs of all dependencies as a map.

  See `Mix.SCM` module documentation to learn more about SCMs.

  ## Options

    * `:depth` - only returns dependencies to the depth level,
      a depth of 1 will only return top-level dependencies
    * `:parents` - starts the dependency traversal from the
      given parents instead of the application root

  ## Examples

      Mix.Project.deps_scms()
      #=> %{foo: Mix.SCM.Path, bar: Mix.SCM.Git}

  """
  @doc since: "1.10.0"
  @spec deps_scms(keyword) :: %{optional(atom) => Mix.SCM.t()}
  def deps_scms(opts \\ []) do
    traverse_deps(opts, fn %{scm: scm} -> scm end)
  end

  @doc """
  Returns the full path of all dependencies as a map.

  ## Options

    * `:depth` - only returns dependencies to the depth level,
      a depth of 1 will only return top-level dependencies
    * `:parents` - starts the dependency traversal from the
      given parents instead of the application root

  ## Examples

      Mix.Project.deps_paths()
      #=> %{foo: "deps/foo", bar: "custom/path/dep"}

  """
  @spec deps_paths(keyword) :: %{optional(atom) => Path.t()}
  def deps_paths(opts \\ []) do
    traverse_deps(opts, fn %{opts: opts} -> opts[:dest] end)
  end

  defp traverse_deps(opts, fun) do
    all_deps = Mix.Dep.cached()
    parents = opts[:parents]
    depth = opts[:depth]

    if parents || depth do
      parent_filter = if parents, do: &(&1.app in parents), else: & &1.top_level

      all_deps
      |> Enum.filter(parent_filter)
      |> traverse_deps_map(fun)
      |> traverse_deps_depth(all_deps, 1, depth || :infinity)
    else
      traverse_deps_map(all_deps, fun)
    end
  end

  defp traverse_deps_map(deps, fun) do
    for %{app: app} = dep <- deps, do: {app, fun.(dep)}, into: %{}
  end

  defp traverse_deps_depth(deps, _all_deps, depth, depth) do
    deps
  end

  defp traverse_deps_depth(parents, all_deps, depth, target_depth) do
    children =
      for parent_dep <- all_deps,
          Map.has_key?(parents, parent_dep.app),
          %{app: app, opts: opts} <- parent_dep.deps,
          do: {app, opts[:dest]},
          into: %{}

    case Map.merge(parents, children) do
      ^parents -> parents
      new_parents -> traverse_deps_depth(new_parents, all_deps, depth + 1, target_depth)
    end
  end

  @doc """
  Clears the dependency for the current environment.

  Useful when dependencies need to be reloaded due to change of global state.

  For example, Nerves uses this function to force all dependencies to be
  reloaded after it updates the system environment. It goes roughly like
  this:

    1. Nerves fetches all dependencies and looks for the system specific deps
    2. Once the system specific dep is found, it loads it alongside env vars
    3. Nerves then clears the cache, forcing dependencies to be loaded again
    4. Dependencies are loaded again, now with an updated env environment

  """
  @doc since: "1.7.0"
  @spec clear_deps_cache() :: :ok
  def clear_deps_cache() do
    Mix.Dep.clear_cached()
    :ok
  end

  @doc """
  Returns the build path for the given project.

  If no configuration is given, the one for the current project is used.

  The returned path will be expanded.

  ## Examples

      Mix.Project.build_path()
      #=> "/path/to/project/_build/shared"

  If `:build_per_environment` is set to `true`, it will create a new build per
  environment:

      Mix.env()
      #=> :dev
      Mix.Project.build_path()
      #=> "/path/to/project/_build/dev"

  """
  @spec build_path(keyword) :: Path.t()
  def build_path(config \\ config()) do
    System.get_env("MIX_BUILD_PATH") || config[:env_path] || env_path(config)
  end

  defp env_path(config) do
    dir = System.get_env("MIX_BUILD_ROOT") || config[:build_path] || "_build"
    subdir = build_target() <> build_per_environment(config)
    Path.expand(dir <> "/" <> subdir)
  end

  defp build_target do
    case Mix.target() do
      :host -> ""
      other -> "#{other}_"
    end
  end

  defp build_per_environment(config) do
    case config[:build_per_environment] do
      true ->
        Atom.to_string(Mix.env())

      false ->
        "shared"

      other ->
        Mix.raise("The :build_per_environment option should be a boolean, got: #{inspect(other)}")
    end
  end

  @doc """
  Returns the path where manifests are stored.

  By default they are stored in the app path inside
  the build directory. Umbrella applications have
  the manifest path set to the root of the build directory.
  Directories may be changed in future releases.

  The returned path will be expanded.

  ## Examples

      Mix.Project.manifest_path()
      #=> "/path/to/project/_build/shared/lib/app/.mix"

  """
  @spec manifest_path(keyword) :: Path.t()
  def manifest_path(config \\ config()) do
    app_path =
      config[:app_path] ||
        if app = config[:app] do
          Path.join([build_path(config), "lib", Atom.to_string(app)])
        else
          build_path(config)
        end

    Path.join(app_path, ".mix")
  end

  @doc """
  Returns the application path inside the build.

  The returned path will be expanded.

  ## Examples

      Mix.Project.app_path()
      #=> "/path/to/project/_build/shared/lib/app"

  """
  @spec app_path(keyword) :: Path.t()
  def app_path(config \\ config()) do
    config[:app_path] ||
      cond do
        app = config[:app] ->
          Path.join([build_path(config), "lib", Atom.to_string(app)])

        config[:apps_path] ->
          raise "trying to access Mix.Project.app_path for an umbrella project but umbrellas have no app"

        true ->
          Mix.raise(
            "Cannot access build without an application name, " <>
              "please ensure you are in a directory with a mix.exs file and it defines " <>
              "an :app name under the project configuration"
          )
      end
  end

  @doc """
  Returns the paths the given project compiles to.

  If no configuration is given, the one for the current project will be used.

  The returned path will be expanded.

  ## Examples

      Mix.Project.compile_path()
      #=> "/path/to/project/_build/dev/lib/app/ebin"

  """
  @spec compile_path(keyword) :: Path.t()
  def compile_path(config \\ config()) do
    Path.join(app_path(config), "ebin")
  end

  @doc """
  Returns the path where protocol consolidations are stored.

  The returned path will be expanded.

  ## Examples

      Mix.Project.consolidation_path()
      #=> "/path/to/project/_build/dev/lib/my_app/consolidated"

  Inside umbrellas:

      Mix.Project.consolidation_path()
      #=> "/path/to/project/_build/dev/consolidated"

  """
  @spec consolidation_path(keyword) :: Path.t()
  def consolidation_path(config \\ config()) do
    config[:consolidation_path] ||
      if umbrella?(config) do
        Path.join(build_path(config), "consolidated")
      else
        Path.join(app_path(config), "consolidated")
      end
  end

  @doc false
  @deprecated "Use Mix.Task.run(\"compile\", args) instead"
  def compile(args, _config \\ []) do
    Mix.Task.run("compile", args)
  end

  @doc """
  Builds the project structure for the given application.

  ## Options

    * `:symlink_ebin` - symlink ebin instead of copying it

  """
  @spec build_structure(keyword, keyword) :: :ok
  def build_structure(config \\ config(), opts \\ []) do
    source = opts[:source] || File.cwd!()
    target = app_path(config)
    File.mkdir_p!(target)

    target_ebin = Path.join(target, "ebin")
    hard_copy? = config[:build_embedded]

    _ =
      cond do
        opts[:symlink_ebin] ->
          _ = Mix.Utils.symlink_or_copy(hard_copy?, Path.join(source, "ebin"), target_ebin)

        match?({:ok, _}, :file.read_link(target_ebin)) ->
          _ = File.rm_rf!(target_ebin)
          File.mkdir_p!(target_ebin)

        true ->
          File.mkdir_p!(target_ebin)
      end

    for dir <- ~w(include priv) do
      Mix.Utils.symlink_or_copy(hard_copy?, Path.join(source, dir), Path.join(target, dir))
    end

    :ok
  end

  @doc """
  Ensures the project structure for the given project exists.

  In case it does exist, it is a no-op. Otherwise, it is built.
  """
  @spec ensure_structure(keyword, keyword) :: :ok
  def ensure_structure(config \\ config(), opts \\ []) do
    if File.exists?(app_path(config)) do
      :ok
    else
      build_structure(config, opts)
    end
  end

  @deprecated "Use Mix.Project.compile_path/1 instead"
  def load_paths(config \\ config()) do
    if umbrella?(config) do
      []
    else
      [compile_path(config)]
    end
  end

  # Loads mix.exs in the current directory or loads the project from the
  # mixfile cache and pushes the project onto the project stack.
  defp load_project(app, post_config) do
    Mix.ProjectStack.post_config(post_config)

    if cached = Mix.State.read_cache({:app, app}) do
      {project, file} = cached
      push(project, file, app)
      project
    else
      file = Path.expand("mix.exs")
      old_proj = get()

      {new_proj, file} =
        if File.regular?(file) do
          old_undefined = Code.get_compiler_option(:no_warn_undefined)

          try do
            Code.compiler_options(relative_paths: false, no_warn_undefined: :all)
            _ = Code.compile_file(file)
            get()
          else
            ^old_proj -> Mix.raise("Could not find a Mix project at #{file}")
            new_proj -> {new_proj, file}
          after
            Code.compiler_options(relative_paths: true, no_warn_undefined: old_undefined)
          end
        else
          push(nil, file, app)
          {nil, "nofile"}
        end

      Mix.State.write_cache({:app, app}, {new_proj, file})
      new_proj
    end
  end

  defp default_config do
    [
      aliases: [],
      build_embedded: false,
      build_per_environment: true,
      build_scm: Mix.SCM.Path,
      config_path: "config/config.exs",
      consolidate_protocols: true,
      default_task: "run",
      deps: [],
      deps_path: "deps",
      elixirc_paths: ["lib"],
      erlc_paths: ["src"],
      erlc_include_path: "include",
      erlc_options: [],
      lockfile: "mix.lock",
      preferred_cli_env: [],
      start_permanent: false
    ]
  end

  @private_config [:app_path, :build_scm, :env_path]
  defp get_project_config(nil), do: []
  defp get_project_config(atom), do: atom.project |> Keyword.drop(@private_config)
end

defmodule Mix.Project do
  @moduledoc """
  Defines and manipulates Mix projects.

  A Mix project is defined by calling `use Mix.Project` in a module, usually
  placed in `mix.exs`:

      defmodule MyApp.MixProject do
        use Mix.Project

        def project do
          [
            app: :my_app,
            version: "1.0.0"
          ]
        end
      end

  ## Configuration

  In order to configure Mix, the module that `use`s `Mix.Project` should export
  a `project/0` function that returns a keyword list representing configuration
  for the project.

  This configuration can be read using `Mix.Project.config/0`. Note that
  `config/0` won't fail if a project is not defined; this allows many Mix tasks
  to work without a project.

  If a task requires a project to be defined or needs to access a
  special function within the project, the task can call `Mix.Project.get!/0`
  which fails with `Mix.NoProjectError` in the case a project is not
  defined.

  There isn't a comprehensive list of all the options that can be returned by
  `project/0` since many Mix tasks define their own options that they read from
  this configuration. For example, look at the "Configuration" section in the
  documentation for the `Mix.Tasks.Compile` task.

  These are a few options that are not used by just one Mix task (and will thus
  be documented here):

    * `:build_per_environment` - if `true`, builds will be *per-environment*. If
      `false`, builds will go in `_build/shared` regardless of the Mix
      environment. Defaults to `true`.

    * `:aliases` - a list of task aliases. For more information, check out the
      "Aliases" section in the documentation for the `Mix` module. Defaults to
      `[]`.

    * `:config_path` - a string representing the path of the main config
      file. See `config_files/0` for more information. Defaults to
      `"config/config.exs"`.

    * `:default_task` - a string representing the default task to be run by
      `mix` when no task is specified. Defaults to `"run"`.

    * `:deps` - a list of dependencies of this project. Refer to the
      documentation for the `Mix.Tasks.Deps` task for more information. Defaults
      to `[]`.

    * `:deps_path` - directory where dependencies are stored. Also see
      `deps_path/1`. Defaults to `"deps"`.

    * `:lockfile` - the name of the lockfile used by the `mix deps.*` family of
      tasks. Defaults to `"mix.lock"`.

    * `:preferred_cli_env` - a keyword list of `{task, env}` tuples where `task`
      is the task name as an atom (for example, `:"deps.get"`) and `env` is the
      preferred environment (for example, `:test`). This option overrides what
      is specified by the tasks with the `@preferred_cli_env` attribute (see the
      docs for `Mix.Task`). Defaults to `[]`.

    * `:preferred_cli_target` - a keyword list of `{task, target}` tuples where
      `task` is the task name as an atom (for example, `:test`) and `target`
      is the preferred target (for example, `:host`). Defaults to `[]`.

  For more options, keep an eye on the documentation for single Mix tasks; good
  examples are the `Mix.Tasks.Compile` task and all the specific compiler tasks
  (such as `Mix.Tasks.Compile.Elixir` or `Mix.Tasks.Compile.Erlang`).

  Note that sometimes the same configuration option is mentioned in the
  documentation for different tasks; this is just because it's common for many
  tasks to read and use the same configuration option (for example,
  `:erlc_paths` is used by `mix compile.erlang`, `mix compile.yecc`, and other
  tasks).

  ## Erlang projects

  Mix can be used to manage Erlang projects that don't have any Elixir code. To
  ensure Mix tasks work correctly for an Erlang project, `language: :erlang` has
  to be part of the configuration returned by `project/0`. This setting also
  makes sure Elixir is not added as a dependency to the generated `.app` file or
  to the escript generated with `mix escript.build`, and so on.
  """

  @doc false
  defmacro __using__(_) do
    quote do
      @after_compile Mix.Project
    end
  end

  # Invoked after each Mix.Project is compiled.
  @doc false
  def __after_compile__(env, _binary) do
    push(env.module, env.file)
  end

  # Push a project onto the project stack.
  # Only the top of the stack can be accessed.
  @doc false
  def push(module, file \\ nil, app \\ nil) when is_atom(module) do
    file = file || (module && List.to_string(module.__info__(:compile)[:source]))
    config = Keyword.merge([app: app] ++ default_config(), get_project_config(module))

    case Mix.ProjectStack.push(module, config, file) do
      :ok ->
        :ok

      {:error, other} when is_binary(other) ->
        Mix.raise(
          "Trying to load #{inspect(module)} from #{inspect(file)}" <>
            " but another project with the same name was already defined at #{inspect(other)}"
        )
    end
  end

  # Pops a project from the stack.
  @doc false
  def pop do
    Mix.ProjectStack.pop()
  end

  # The configuration that is pushed down to dependencies.
  @doc false
  def deps_config(config \\ config()) do
    [
      build_embedded: config[:build_embedded],
      build_per_environment: config[:build_per_environment],
      consolidate_protocols: false,
      consolidation_path: consolidation_path(config),
      deps_path: deps_path(config),
      env_path: build_path(config)
    ]
  end

  @doc """
  Retrieves the current project if there is one.

  If there is no current project, `nil` is returned. This
  may happen in cases there is no `mix.exs` in the current
  directory.

  If you expect a project to be defined, i.e., it is a
  requirement of the current task, you should call
  `get!/0` instead.
  """
  @spec get() :: module | nil
  def get do
    case Mix.ProjectStack.peek() do
      %{name: name} -> name
      _ -> nil
    end
  end

  @doc """
  Same as `get/0`, but raises an exception if there is no current project.

  This is usually called by tasks that need additional
  functions on the project to be defined. Since such
  tasks usually depend on a project being defined, this
  function raises a `Mix.NoProjectError` exception in
  case no project is available.
  """
  @spec get!() :: module
  def get! do
    get() || raise Mix.NoProjectError, []
  end

  @doc """
  Returns the path to the file that defines the current project.

  The majority of the time, it will point to a `mix.exs` file.
  Returns nil if not inside a project.
  """
  @doc since: "1.13.0"
  @spec project_file() :: binary | nil
  defdelegate project_file(), to: Mix.ProjectStack

  @doc """
  Returns the project configuration.

  If there is no project defined, it still returns a keyword
  list with default values. This allows many Mix tasks to work
  without the need for an underlying project.

  Note this configuration is cached once the project is
  pushed onto the stack. Calling it multiple times won't
  cause it to be recomputed.

  Do not use `Mix.Project.config/0` to find the runtime configuration.
  Use it only to configure aspects of your project (like
  compilation directories) and not your application runtime.
  """
  @spec config() :: keyword
  def config do
    case Mix.ProjectStack.peek() do
      %{config: config} -> config
      _ -> default_config()
    end
  end

  @doc """
  Returns a list of project configuration files for this project.

  This function is usually used in compilation tasks to trigger
  a full recompilation whenever such configuration files change.

  It returns the lock manifest, and all config files in the `config`
  directory that do not start with a leading period (for example,
  `.my_config.exs`).

  Note: before Elixir v1.13.0, the `mix.exs` file was also included
  as a config file, but since then it has been moved to its own
  function called `project_file/0`.
  """
  @spec config_files() :: [Path.t()]
  def config_files do
    Mix.ProjectStack.config_files()
  end

  @doc """
  Returns the latest modification time from config files.

  This function is usually used in compilation tasks to trigger
  a full recompilation whenever such configuration files change.
  For this reason, the mtime is cached to avoid file system lookups.

  Note: before Elixir v1.13.0, the `mix.exs` file was also included
  in the mtimes, but not anymore. You can compute its modification
  date by calling  `project_file/0`.
  """
  @doc since: "1.7.0"
  @spec config_mtime() :: posix_mtime when posix_mtime: integer()
  def config_mtime do
    Mix.ProjectStack.config_mtime()
  end

  @doc """
  Returns `true` if `config` is the configuration for an umbrella project.

  When called with no arguments, tells whether the current project is
  an umbrella project.
  """
  @spec umbrella?(keyword) :: boolean
  def umbrella?(config \\ config()) do
    config[:apps_path] != nil
  end

  @doc """
  Returns a map with the umbrella child applications paths.

  These paths are based on the `:apps_path` and `:apps` configurations.

  If the given project configuration identifies an umbrella project, the return
  value is a map of `app => path` where `app` is a child app of the umbrella and
  `path` is its path relative to the root of the umbrella project.

  If the given project configuration does not identify an umbrella project,
  `nil` is returned.

  ## Examples

      Mix.Project.apps_paths()
      #=> %{my_app1: "apps/my_app1", my_app2: "apps/my_app2"}

  """
  @doc since: "1.4.0"
  @spec apps_paths(keyword) :: %{optional(atom) => Path.t()} | nil
  def apps_paths(config \\ config()) do
    if apps_path = config[:apps_path] do
      key = {:apps_paths, Mix.Project.get!()}

      if cache = Mix.State.read_cache(key) do
        cache
      else
        cache = config[:apps] |> umbrella_apps(apps_path) |> to_apps_paths(apps_path)
        Mix.State.write_cache(key, cache)
      end
    end
  end

  defp umbrella_apps(nil, apps_path) do
    case File.ls(apps_path) do
      {:ok, apps} -> Enum.map(apps, &String.to_atom/1)
      {:error, _} -> []
    end
  end

  defp umbrella_apps(apps, _apps_path) when is_list(apps) do
    apps
  end

  defp to_apps_paths(apps, apps_path) do
    for app <- apps,
        path = path_with_mix_exs_otherwise_warn(app, apps_path),
        do: {app, path},
        into: %{}
  end

  defp path_with_mix_exs_otherwise_warn(app, apps_path) do
    path = Path.join(apps_path, Atom.to_string(app))

    cond do
      File.regular?(Path.join(path, "mix.exs")) ->
        path

      File.dir?(path) ->
        Mix.shell().error(
          "warning: path #{inspect(Path.relative_to_cwd(path))} is a directory but " <>
            "it has no mix.exs. Mix won't consider this directory as part of your " <>
            "umbrella application. Please add a \"mix.exs\" or set the \":apps\" key " <>
            "in your umbrella configuration with all relevant apps names as atoms"
        )

        nil

      true ->
        # If it is a stray file, we just ignore it.
        nil
    end
  end

  @doc ~S"""
  Runs the given `fun` inside the given project.

  This function changes the current working directory and
  loads the project at the given directory onto the project
  stack.

  A `post_config` can be passed that will be merged into
  the project configuration.

  `fun` is called with the module name of the given `Mix.Project`.
  The return value of this function is the return value of `fun`.

  ## Examples

      Mix.Project.in_project(:my_app, "/path/to/my_app", fn module ->
        "Mix project is: #{inspect(module)}"
      end)
      #=> "Mix project is: MyApp.MixProject"

  """
  @spec in_project(atom, Path.t(), keyword, (module -> result)) :: result when result: term
  def in_project(app, path, post_config \\ [], fun)

  def in_project(app, ".", post_config, fun) when is_atom(app) do
    cached =
      try do
        load_project(app, post_config)
      rescue
        any ->
          Mix.shell().error("Error while loading project #{inspect(app)} at #{File.cwd!()}")
          reraise any, __STACKTRACE__
      end

    try do
      fun.(cached)
    after
      Mix.Project.pop()
    end
  end

  def in_project(app, path, post_config, fun) when is_atom(app) do
    File.cd!(path, fn ->
      in_project(app, ".", post_config, fun)
    end)
  end

  @doc """
  Returns the path where dependencies are stored for the given project.

  If no configuration is given, the one for the current project is used.

  The returned path will be expanded.

  ## Examples

      Mix.Project.deps_path()
      #=> "/path/to/project/deps"

  """
  @spec deps_path(keyword) :: Path.t()
  def deps_path(config \\ config()) do
    dir = System.get_env("MIX_DEPS_PATH") || config[:deps_path]
    Path.expand(dir)
  end

  @doc """
  Returns all dependencies app names.

  The order they are returned is guaranteed to be sorted
  for proper dependency resolution. For example, if A
  depends on B, then B will listed before A.
  """
  @doc since: "1.11.0"
  @spec deps_apps() :: [atom()]
  def deps_apps() do
    Mix.Dep.cached() |> Enum.map(& &1.app)
  end

  @doc """
  Returns the SCMs of all dependencies as a map.

  See `Mix.SCM` module documentation to learn more about SCMs.

  ## Options

    * `:depth` - only returns dependencies to the depth level,
      a depth of 1 will only return top-level dependencies
    * `:parents` - starts the dependency traversal from the
      given parents instead of the application root

  ## Examples

      Mix.Project.deps_scms()
      #=> %{foo: Mix.SCM.Path, bar: Mix.SCM.Git}

  """
  @doc since: "1.10.0"
  @spec deps_scms(keyword) :: %{optional(atom) => Mix.SCM.t()}
  def deps_scms(opts \\ []) do
    traverse_deps(opts, fn %{scm: scm} -> scm end)
  end

  @doc """
  Returns the full path of all dependencies as a map.

  ## Options

    * `:depth` - only returns dependencies to the depth level,
      a depth of 1 will only return top-level dependencies
    * `:parents` - starts the dependency traversal from the
      given parents instead of the application root

  ## Examples

      Mix.Project.deps_paths()
      #=> %{foo: "deps/foo", bar: "custom/path/dep"}

  """
  @spec deps_paths(keyword) :: %{optional(atom) => Path.t()}
  def deps_paths(opts \\ []) do
    traverse_deps(opts, fn %{opts: opts} -> opts[:dest] end)
  end

  defp traverse_deps(opts, fun) do
    all_deps = Mix.Dep.cached()
    parents = opts[:parents]
    depth = opts[:depth]

    if parents || depth do
      parent_filter = if parents, do: &(&1.app in parents), else: & &1.top_level

      all_deps
      |> Enum.filter(parent_filter)
      |> traverse_deps_map(fun)
      |> traverse_deps_depth(all_deps, 1, depth || :infinity)
    else
      traverse_deps_map(all_deps, fun)
    end
  end

  defp traverse_deps_map(deps, fun) do
    for %{app: app} = dep <- deps, do: {app, fun.(dep)}, into: %{}
  end

  defp traverse_deps_depth(deps, _all_deps, depth, depth) do
    deps
  end

  defp traverse_deps_depth(parents, all_deps, depth, target_depth) do
    children =
      for parent_dep <- all_deps,
          Map.has_key?(parents, parent_dep.app),
          %{app: app, opts: opts} <- parent_dep.deps,
          do: {app, opts[:dest]},
          into: %{}

    case Map.merge(parents, children) do
      ^parents -> parents
      new_parents -> traverse_deps_depth(new_parents, all_deps, depth + 1, target_depth)
    end
  end

  @doc """
  Clears the dependency for the current environment.

  Useful when dependencies need to be reloaded due to change of global state.

  For example, Nerves uses this function to force all dependencies to be
  reloaded after it updates the system environment. It goes roughly like
  this:

    1. Nerves fetches all dependencies and looks for the system specific deps
    2. Once the system specific dep is found, it loads it alongside env vars
    3. Nerves then clears the cache, forcing dependencies to be loaded again
    4. Dependencies are loaded again, now with an updated env environment

  """
  @doc since: "1.7.0"
  @spec clear_deps_cache() :: :ok
  def clear_deps_cache() do
    Mix.Dep.clear_cached()
    :ok
  end

  @doc """
  Returns the build path for the given project.

  If no configuration is given, the one for the current project is used.

  The returned path will be expanded.

  ## Examples

      Mix.Project.build_path()
      #=> "/path/to/project/_build/shared"

  If `:build_per_environment` is set to `true`, it will create a new build per
  environment:

      Mix.env()
      #=> :dev
      Mix.Project.build_path()
      #=> "/path/to/project/_build/dev"

  """
  @spec build_path(keyword) :: Path.t()
  def build_path(config \\ config()) do
    System.get_env("MIX_BUILD_PATH") || config[:env_path] || env_path(config)
  end

  defp env_path(config) do
    dir = System.get_env("MIX_BUILD_ROOT") || config[:build_path] || "_build"
    subdir = build_target() <> build_per_environment(config)
    Path.expand(dir <> "/" <> subdir)
  end

  defp build_target do
    case Mix.target() do
      :host -> ""
      other -> "#{other}_"
    end
  end

  defp build_per_environment(config) do
    case config[:build_per_environment] do
      true ->
        Atom.to_string(Mix.env())

      false ->
        "shared"

      other ->
        Mix.raise("The :build_per_environment option should be a boolean, got: #{inspect(other)}")
    end
  end

  @doc """
  Returns the path where manifests are stored.

  By default they are stored in the app path inside
  the build directory. Umbrella applications have
  the manifest path set to the root of the build directory.
  Directories may be changed in future releases.

  The returned path will be expanded.

  ## Examples

      Mix.Project.manifest_path()
      #=> "/path/to/project/_build/shared/lib/app/.mix"

  """
  @spec manifest_path(keyword) :: Path.t()
  def manifest_path(config \\ config()) do
    app_path =
      config[:app_path] ||
        if app = config[:app] do
          Path.join([build_path(config), "lib", Atom.to_string(app)])
        else
          build_path(config)
        end

    Path.join(app_path, ".mix")
  end

  @doc """
  Returns the application path inside the build.

  The returned path will be expanded.

  ## Examples

      Mix.Project.app_path()
      #=> "/path/to/project/_build/shared/lib/app"

  """
  @spec app_path(keyword) :: Path.t()
  def app_path(config \\ config()) do
    config[:app_path] ||
      cond do
        app = config[:app] ->
          Path.join([build_path(config), "lib", Atom.to_string(app)])

        config[:apps_path] ->
          raise "trying to access Mix.Project.app_path for an umbrella project but umbrellas have no app"

        true ->
          Mix.raise(
            "Cannot access build without an application name, " <>
              "please ensure you are in a directory with a mix.exs file and it defines " <>
              "an :app name under the project configuration"
          )
      end
  end

  @doc """
  Returns the paths the given project compiles to.

  If no configuration is given, the one for the current project will be used.

  The returned path will be expanded.

  ## Examples

      Mix.Project.compile_path()
      #=> "/path/to/project/_build/dev/lib/app/ebin"

  """
  @spec compile_path(keyword) :: Path.t()
  def compile_path(config \\ config()) do
    Path.join(app_path(config), "ebin")
  end

  @doc """
  Returns the path where protocol consolidations are stored.

  The returned path will be expanded.

  ## Examples

      Mix.Project.consolidation_path()
      #=> "/path/to/project/_build/dev/lib/my_app/consolidated"

  Inside umbrellas:

      Mix.Project.consolidation_path()
      #=> "/path/to/project/_build/dev/consolidated"

  """
  @spec consolidation_path(keyword) :: Path.t()
  def consolidation_path(config \\ config()) do
    config[:consolidation_path] ||
      if umbrella?(config) do
        Path.join(build_path(config), "consolidated")
      else
        Path.join(app_path(config), "consolidated")
      end
  end

  @doc false
  @deprecated "Use Mix.Task.run(\"compile\", args) instead"
  def compile(args, _config \\ []) do
    Mix.Task.run("compile", args)
  end

  @doc """
  Builds the project structure for the given application.

  ## Options

    * `:symlink_ebin` - symlink ebin instead of copying it

  """
  @spec build_structure(keyword, keyword) :: :ok
  def build_structure(config \\ config(), opts \\ []) do
    source = opts[:source] || File.cwd!()
    target = app_path(config)
    File.mkdir_p!(target)

    target_ebin = Path.join(target, "ebin")
    hard_copy? = config[:build_embedded]

    _ =
      cond do
        opts[:symlink_ebin] ->
          _ = Mix.Utils.symlink_or_copy(hard_copy?, Path.join(source, "ebin"), target_ebin)

        match?({:ok, _}, :file.read_link(target_ebin)) ->
          _ = File.rm_rf!(target_ebin)
          File.mkdir_p!(target_ebin)

        true ->
          File.mkdir_p!(target_ebin)
      end

    for dir <- ~w(include priv) do
      Mix.Utils.symlink_or_copy(hard_copy?, Path.join(source, dir), Path.join(target, dir))
    end

    :ok
  end

  @doc """
  Ensures the project structure for the given project exists.

  In case it does exist, it is a no-op. Otherwise, it is built.
  """
  @spec ensure_structure(keyword, keyword) :: :ok
  def ensure_structure(config \\ config(), opts \\ []) do
    if File.exists?(app_path(config)) do
      :ok
    else
      build_structure(config, opts)
    end
  end

  @deprecated "Use Mix.Project.compile_path/1 instead"
  def load_paths(config \\ config()) do
    if umbrella?(config) do
      []
    else
      [compile_path(config)]
    end
  end

  # Loads mix.exs in the current directory or loads the project from the
  # mixfile cache and pushes the project onto the project stack.
  defp load_project(app, post_config) do
    Mix.ProjectStack.post_config(post_config)

    if cached = Mix.State.read_cache({:app, app}) do
      {project, file} = cached
      push(project, file, app)
      project
    else
      file = Path.expand("mix.exs")
      old_proj = get()

      {new_proj, file} =
        if File.regular?(file) do
          old_undefined = Code.get_compiler_option(:no_warn_undefined)

          try do
            Code.compiler_options(relative_paths: false, no_warn_undefined: :all)
            _ = Code.compile_file(file)
            get()
          else
            ^old_proj -> Mix.raise("Could not find a Mix project at #{file}")
            new_proj -> {new_proj, file}
          after
            Code.compiler_options(relative_paths: true, no_warn_undefined: old_undefined)
          end
        else
          push(nil, file, app)
          {nil, "nofile"}
        end

      Mix.State.write_cache({:app, app}, {new_proj, file})
      new_proj
    end
  end

  defp default_config do
    [
      aliases: [],
      build_embedded: false,
      build_per_environment: true,
      build_scm: Mix.SCM.Path,
      config_path: "config/config.exs",
      consolidate_protocols: true,
      default_task: "run",
      deps: [],
      deps_path: "deps",
      elixirc_paths: ["lib"],
      erlc_paths: ["src"],
      erlc_include_path: "include",
      erlc_options: [],
      lockfile: "mix.lock",
      preferred_cli_env: [],
      start_permanent: false
    ]
  end

  @private_config [:app_path, :build_scm, :env_path]
  defp get_project_config(nil), do: []
  defp get_project_config(atom), do: atom.project |> Keyword.drop(@private_config)
end


Support older gits in mix

Support older gits in mix

Do not add `--progress` flag to `git fetch` command unless git version
is 1.7.1 or newer.

defmodule Mix.SCM.Git do
  @behaviour Mix.SCM
  @moduledoc false

  def fetchable? do
    true
  end

  def format(opts) do
    opts[:git]
  end

  def format_lock(opts) do
    case opts[:lock] do
      { :git, _, lock_rev, lock_opts } ->
        lock = String.slice(lock_rev, 0, 7)
        case Enum.find_value [:branch, :ref, :tag], &List.keyfind(lock_opts, &1, 0) do
          { :ref, _ }  -> lock <> " (ref)"
          { key, val } -> lock <> " (#{key}: #{val})"
          nil          -> lock
        end
      _ ->
        nil
    end
  end

  def accepts_options(_app, opts) do
    cond do
      gh = opts[:github] ->
        opts |> Keyword.delete(:github) |> Keyword.put(:git, "git://github.com/#{gh}.git")
      opts[:git] ->
        opts
      true ->
        nil
    end
  end

  def checked_out?(opts) do
    File.dir?(Path.join(opts[:dest], ".git"))
  end

  def lock_status(opts) do
    case opts[:lock] do
      { :git, lock_repo, lock_rev, lock_opts } ->
        File.cd!(opts[:dest], fn ->
          rev_info = get_rev_info
          cond do
            lock_repo != opts[:git]          -> :outdated
            lock_opts != get_lock_opts(opts) -> :outdated
            lock_rev  != rev_info[:rev]      -> :mismatch
            lock_repo != rev_info[:origin]   -> :outdated
            true -> :ok
          end
        end)
      nil ->
        :mismatch
      _ ->
        :outdated
    end
  end

  def equal?(opts1, opts2) do
    opts1[:git] == opts2[:git] &&
      get_lock_opts(opts1) == get_lock_opts(opts2)
  end

  def checkout(opts) do
    path     = opts[:dest]
    location = opts[:git]
    command  = ~s(git clone --no-checkout --progress "#{location}" "#{path}")

    run_cmd_or_raise(command)
    File.cd! path, fn -> do_checkout(opts) end
  end

  def update(opts) do
    File.cd! opts[:dest], fn ->
      # Ensures origin is set the lock repo
      location = opts[:git]
      update_origin(location)

      command = "git fetch --force"

      if { 1, 7, 1 } <= git_version() do
        command = command <> " --progress"
      end

      if opts[:tag] do
        command = command <> " --tags"
      end

      run_cmd_or_raise(command)
      do_checkout(opts)
    end
  end

  ## Helpers

  defp do_checkout(opts) do
    ref = get_lock_rev(opts[:lock]) || get_opts_rev(opts)
    run_cmd_or_raise "git checkout --quiet #{ref}"

    if opts[:submodules] do
      run_cmd_or_raise "git submodule update --init --recursive"
    end

    get_lock(opts)
  end

  defp get_lock(opts) do
    rev_info = get_rev_info
    { :git, opts[:git], rev_info[:rev], get_lock_opts(opts) }
  end

  defp get_lock_rev({ :git, _repo, lock, _opts }) when is_binary(lock), do: lock
  defp get_lock_rev(_), do: nil

  defp get_lock_opts(opts) do
    lock_opts = Enum.find_value [:branch, :ref, :tag], &List.keyfind(opts, &1, 0)
    lock_opts = List.wrap(lock_opts)
    if opts[:submodules] do
      lock_opts ++ [submodules: true]
    else
      lock_opts
    end
  end

  defp get_opts_rev(opts) do
    if branch = opts[:branch] do
      "origin/#{branch}"
    else
      opts[:ref] || opts[:tag] || "origin/master"
    end
  end

  defp get_rev_info do
    destructure [origin, rev],
      System.cmd('git config remote.origin.url && git rev-parse --verify --quiet HEAD')
      |> iolist_to_binary
      |> String.split("\n", trim: true)
    [ origin: origin, rev: rev ]
  end

  defp update_origin(location) do
    System.cmd('git config remote.origin.url #{location}')
  end

  defp run_cmd_or_raise(command) do
    if Mix.shell.cmd(command) != 0 do
      raise Mix.Error, message: "Command `#{command}` failed"
    end
    true
  end

  defp git_version do
    case :application.get_env(:mix, :git_version) do
      { :ok, version } ->
        version
      :undefined ->
        "git version " <> version = String.strip System.cmd("git --version")
        version = String.split(version, ".")
                  |> Enum.take(3)
                  |> Enum.map(&binary_to_integer(&1))
                  |> list_to_tuple

        :application.set_env(:mix, :git_version, version)
        version
    end
  end
end

defmodule Mix.SCM.Git do
  @behaviour Mix.SCM
  @moduledoc false

  def fetchable? do
    true
  end

  def format(opts) do
    opts[:git]
  end

  def format_lock(opts) do
    case opts[:lock] do
      { :git, _, lock_rev, lock_opts } ->
        lock = String.slice(lock_rev, 0, 7)
        case Enum.find_value [:branch, :ref, :tag], &List.keyfind(lock_opts, &1, 0) do
          { :ref, _ }  -> lock <> " (ref)"
          { key, val } -> lock <> " (#{key}: #{val})"
          nil          -> lock
        end
      _ ->
        nil
    end
  end

  def accepts_options(_app, opts) do
    cond do
      gh = opts[:github] ->
        opts |> Keyword.delete(:github) |> Keyword.put(:git, "git://github.com/#{gh}.git")
      opts[:git] ->
        opts
      true ->
        nil
    end
  end

  def checked_out?(opts) do
    File.dir?(Path.join(opts[:dest], ".git"))
  end

  def lock_status(opts) do
    case opts[:lock] do
      { :git, lock_repo, lock_rev, lock_opts } ->
        File.cd!(opts[:dest], fn ->
          rev_info = get_rev_info
          cond do
            lock_repo != opts[:git]          -> :outdated
            lock_opts != get_lock_opts(opts) -> :outdated
            lock_rev  != rev_info[:rev]      -> :mismatch
            lock_repo != rev_info[:origin]   -> :outdated
            true -> :ok
          end
        end)
      nil ->
        :mismatch
      _ ->
        :outdated
    end
  end

  def equal?(opts1, opts2) do
    opts1[:git] == opts2[:git] &&
      get_lock_opts(opts1) == get_lock_opts(opts2)
  end

  def checkout(opts) do
    path     = opts[:dest]
    location = opts[:git]
    command  = ~s(git clone --no-checkout --progress "#{location}" "#{path}")

    run_cmd_or_raise(command)
    File.cd! path, fn -> do_checkout(opts) end
  end

  def update(opts) do
    File.cd! opts[:dest], fn ->
      # Ensures origin is set the lock repo
      location = opts[:git]
      update_origin(location)

      command = "git fetch --force --progress"
      if opts[:tag] do
        command = command <> " --tags"
      end

      run_cmd_or_raise(command)
      do_checkout(opts)
    end
  end

  ## Helpers

  defp do_checkout(opts) do
    ref = get_lock_rev(opts[:lock]) || get_opts_rev(opts)
    run_cmd_or_raise "git checkout --quiet #{ref}"

    if opts[:submodules] do
      run_cmd_or_raise "git submodule update --init --recursive"
    end

    get_lock(opts)
  end

  defp get_lock(opts) do
    rev_info = get_rev_info
    { :git, opts[:git], rev_info[:rev], get_lock_opts(opts) }
  end

  defp get_lock_rev({ :git, _repo, lock, _opts }) when is_binary(lock), do: lock
  defp get_lock_rev(_), do: nil

  defp get_lock_opts(opts) do
    lock_opts = Enum.find_value [:branch, :ref, :tag], &List.keyfind(opts, &1, 0)
    lock_opts = List.wrap(lock_opts)
    if opts[:submodules] do
      lock_opts ++ [submodules: true]
    else
      lock_opts
    end
  end

  defp get_opts_rev(opts) do
    if branch = opts[:branch] do
      "origin/#{branch}"
    else
      opts[:ref] || opts[:tag] || "origin/master"
    end
  end

  defp get_rev_info do
    destructure [origin, rev],
      System.cmd('git config remote.origin.url && git rev-parse --verify --quiet HEAD')
      |> iolist_to_binary
      |> String.split("\n", trim: true)
    [ origin: origin, rev: rev ]
  end

  defp update_origin(location) do
    System.cmd('git config remote.origin.url #{location}')
  end

  defp run_cmd_or_raise(command) do
    if Mix.shell.cmd(command) != 0 do
      raise Mix.Error, message: "Command `#{command}` failed"
    end
    true
  end
end


Use correct default type

Use correct default type

Without this, you get an error in the default config:
```
** (CaseClauseError) no case clause matching: ElixirALE.GPIO
    (stdlib) :erl_eval.expr/3
```

@moduledoc """
A schema is a keyword list which represents how to map, transform, and validate
configuration values parsed from the .conf file. The following is an explanation of
each key in the schema definition in order of appearance, and how to use them.

## Import

A list of application names (as atoms), which represent apps to load modules from
which you can then reference in your schema definition. This is how you import your
own custom Validator/Transform modules, or general utility modules for use in
validator/transform functions in the schema. For example, if you have an application
`:foo` which contains a custom Transform module, you would add it to your schema like so:

`[ import: [:foo], ..., transforms: ["myapp.some.setting": MyApp.SomeTransform]]`

## Extends

A list of application names (as atoms), which contain schemas that you want to extend
with this schema. By extending a schema, you effectively re-use definitions in the
extended schema. You may also override definitions from the extended schema by redefining them
in the extending schema. You use `:extends` like so:

`[ extends: [:foo], ... ]`

## Mappings

Mappings define how to interpret settings in the .conf when they are translated to
runtime configuration. They also define how the .conf will be generated, things like
documention, @see references, example values, etc.

See the moduledoc for `Conform.Schema.Mapping` for more details.

## Transforms

Transforms are custom functions which are executed to build the value which will be
stored at the path defined by the key. Transforms have access to the current config
state via the `Conform.Conf` module, and can use that to build complex configuration
from a combination of other config values.

See the moduledoc for `Conform.Schema.Transform` for more details and examples.

## Validators

Validators are simple functions which take two arguments, the value to be validated,
and arguments provided to the validator (used only by custom validators). A validator
checks the value, and returns `:ok` if it is valid, `{:warn, message}` if it is valid,
but should be brought to the users attention, or `{:error, message}` if it is invalid.

See the moduledoc for `Conform.Schema.Validator` for more details and examples.
"""
[
  extends: [],
  import: [],
  mappings: [
    "gatekeeper.rfidreader.device": [
      commented: false,
      datatype: :binary,
      default: "/dev/ttyUSB0",
      doc: "Path to device file for RFID reader",
      hidden: false,
      to: "gatekeeper.rfidreader.device"
    ],
    "gatekeeper.doorlock.type": [
      commented: false,
      datatype: :binary,
      default: "gpio",
      doc: "Module name representing the driver for the Door Lock",
      hidden: true,
      to: "gatekeeper.doorlock.type"
    ],
    "gatekeeper.doorlock.gpio_pin": [
      commented: false,
      datatype: :integer,
      default: 4,
      doc: "GPIO pin for signaling the door strike to unlock",
      hidden: false,
      to: "gatekeeper.doorlock.gpio_pin"
    ],
    "gatekeeper.doorlock.duration": [
      commented: false,
      datatype: :integer,
      default: 3000,
      doc: "How long (in milliseconds) to keep the door unlocked after reading a badge",
      hidden: false,
      to: "gatekeeper.doorlock.duration"
    ],
    "gatekeeper.doorlock.door_id": [
      commented: false,
      datatype: :integer,
      default: 1,
      doc: "ID of the door controlled by this instance of Gatekeeper",
      hidden: false,
      to: "gatekeeper.doorlock.door_id"
    ],
    "gatekeeper.doorbell.type": [
      commented: false,
      datatype: :binary,
      default: "gpio",
      doc: "Module name representing the driver for the Door Bell",
      hidden: true,
      to: "gatekeeper.doorbell.type"
    ],
    "gatekeeper.doorbell.gpio_pin": [
      commented: false,
      datatype: :integer,
      default: 17,
      doc: "GPIO pin for signaling that the door bell has been rung",
      hidden: false,
      to: "gatekeeper.doorbell.gpio_pin"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.url.host": [
      commented: false,
      datatype: :binary,
      default: "localhost",
      doc: "Domain name at which application is running, for web server to generate links",
      hidden: false,
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.url.host"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.root": [
      commented: false,
      datatype: :binary,
      default: Path.dirname(__DIR__),
      doc: "Base directory for Gatekeeper application",
      hidden: false,
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.root"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.secret_key_base": [
      commented: false,
      datatype: :binary,
      doc: "Secret key used for securing session cookies",
      hidden: false,
      env_var: "SECRET_KEY_BASE",
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.secret_key_base"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.http.port": [
      commented: false,
      datatype: :integer,
      default: 4000,
      doc: "Port on which application listens for HTTP connections",
      hidden: false,
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.http.port"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.username": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database username for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_USERNAME",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.username"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.password": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database password for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_PASSWORD",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.password"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.database": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper_dev",
      doc: "Database name for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_DATABASE",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.database"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.hostname": [
      commented: false,
      datatype: :binary,
      default: "localhost",
      doc: "Database hostname for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_HOSTNAME",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.hostname"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.template": [
      commented: false,
      datatype: :binary,
      default: "template0",
      doc: "Postgresql template for the READ ONLY connection",
      hidden: true,
      to: "gatekeeper.Elixir.Gatekeeper.Repo.template"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.pool_size": [
      commented: false,
      datatype: :integer,
      default: 10,
      doc: "Number of database connections available in the pool for the READ ONLY connection",
      hidden: false,
      to: "gatekeeper.Elixir.Gatekeeper.Repo.pool_size"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.username": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database username for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_USERNAME",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.username"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.password": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database password for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_PASSWORD",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.password"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.database": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper_dev",
      doc: "Database name for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_DATABASE",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.database"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.hostname": [
      commented: false,
      datatype: :binary,
      default: "localhost",
      doc: "Database hostname for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_HOSTNAME",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.hostname"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.template": [
      commented: false,
      datatype: :binary,
      default: "template0",
      doc: "Postgresql template for the WRITABLE connection",
      hidden: true,
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.template"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.pool_size": [
      commented: false,
      datatype: :integer,
      default: 10,
      doc: "Number of database connections available in the pool for the WRITABLE connection",
      hidden: false,
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.pool_size"
    ],
    "ueberauth.Elixir.Ueberauth.providers.slack": [
      commented: false,
      datatype: :binary,
      doc: "Team ID for Slack authentication",
      hidden: false,
      env_var: "SLACK_TEAM_ID",
      to: "ueberauth.Elixir.Ueberauth.providers.slack"
    ],
    "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_id": [
      commented: false,
      datatype: :binary,
      default: "",
      doc: "Slack OAuth Client ID",
      hidden: false,
      env_var: "SLACK_CLIENT_ID",
      to: "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_id"
    ],
    "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_secret": [
      commented: false,
      datatype: :binary,
      default: "",
      doc: "Slack OAuth Client Secret",
      hidden: false,
      env_var: "SLACK_CLIENT_SECRET",
      to: "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_secret"
    ],
    "guardian.Elixir.Guardian.ttl": [
      commented: false,
      datatype: {:integer, :atom},
      default: {30, :days},
      doc: "Web session duration",
      hidden: false,
      to: "guardian.Elixir.Guardian.ttl"
    ],
    "guardian.Elixir.Guardian.secret_key": [
      commented: false,
      datatype: :binary,
      default: "",
      doc: "Secret key to sign/secure session cookies",
      hidden: false,
      env_var: "GUARDIAN_SECRET_KEY",
      to: "guardian.Elixir.Guardian.secret_key"
    ],
  ],
  transforms: [
    "ueberauth.Elixir.Ueberauth.providers.slack": fn conf ->
      [{_, team_id}] = Conform.Conf.get(conf, "ueberauth.Elixir.Ueberauth.providers.slack")
      {Ueberauth.Strategy.Slack, [team: team_id]}
    end,
    "gatekeeper.doorlock.type": fn conf ->
      [{_, type}] = Conform.Conf.get(conf, "gatekeeper.doorlock.type")
      case type do
        "gpio" -> ElixirALE.GPIO
        "dummy" -> Gatekeeper.GPIODummy
      end
    end,
    "gatekeeper.doorbell.type": fn conf ->
      [{_, type}] = Conform.Conf.get(conf, "gatekeeper.doorbell.type")
      case type do
        "gpio" -> ElixirALE.GPIO
        "dummy" -> Gatekeeper.GPIODummy
      end
    end
  ],
  validators: []
]

@moduledoc """
A schema is a keyword list which represents how to map, transform, and validate
configuration values parsed from the .conf file. The following is an explanation of
each key in the schema definition in order of appearance, and how to use them.

## Import

A list of application names (as atoms), which represent apps to load modules from
which you can then reference in your schema definition. This is how you import your
own custom Validator/Transform modules, or general utility modules for use in
validator/transform functions in the schema. For example, if you have an application
`:foo` which contains a custom Transform module, you would add it to your schema like so:

`[ import: [:foo], ..., transforms: ["myapp.some.setting": MyApp.SomeTransform]]`

## Extends

A list of application names (as atoms), which contain schemas that you want to extend
with this schema. By extending a schema, you effectively re-use definitions in the
extended schema. You may also override definitions from the extended schema by redefining them
in the extending schema. You use `:extends` like so:

`[ extends: [:foo], ... ]`

## Mappings

Mappings define how to interpret settings in the .conf when they are translated to
runtime configuration. They also define how the .conf will be generated, things like
documention, @see references, example values, etc.

See the moduledoc for `Conform.Schema.Mapping` for more details.

## Transforms

Transforms are custom functions which are executed to build the value which will be
stored at the path defined by the key. Transforms have access to the current config
state via the `Conform.Conf` module, and can use that to build complex configuration
from a combination of other config values.

See the moduledoc for `Conform.Schema.Transform` for more details and examples.

## Validators

Validators are simple functions which take two arguments, the value to be validated,
and arguments provided to the validator (used only by custom validators). A validator
checks the value, and returns `:ok` if it is valid, `{:warn, message}` if it is valid,
but should be brought to the users attention, or `{:error, message}` if it is invalid.

See the moduledoc for `Conform.Schema.Validator` for more details and examples.
"""
[
  extends: [],
  import: [],
  mappings: [
    "gatekeeper.rfidreader.device": [
      commented: false,
      datatype: :binary,
      default: "/dev/ttyUSB0",
      doc: "Path to device file for RFID reader",
      hidden: false,
      to: "gatekeeper.rfidreader.device"
    ],
    "gatekeeper.doorlock.type": [
      commented: false,
      datatype: :binary,
      default: ElixirALE.GPIO,
      doc: "Module name representing the driver for the Door Lock",
      hidden: true,
      to: "gatekeeper.doorlock.type"
    ],
    "gatekeeper.doorlock.gpio_pin": [
      commented: false,
      datatype: :integer,
      default: 4,
      doc: "GPIO pin for signaling the door strike to unlock",
      hidden: false,
      to: "gatekeeper.doorlock.gpio_pin"
    ],
    "gatekeeper.doorlock.duration": [
      commented: false,
      datatype: :integer,
      default: 3000,
      doc: "How long (in milliseconds) to keep the door unlocked after reading a badge",
      hidden: false,
      to: "gatekeeper.doorlock.duration"
    ],
    "gatekeeper.doorlock.door_id": [
      commented: false,
      datatype: :integer,
      default: 1,
      doc: "ID of the door controlled by this instance of Gatekeeper",
      hidden: false,
      to: "gatekeeper.doorlock.door_id"
    ],
    "gatekeeper.doorbell.type": [
      commented: false,
      datatype: :binary,
      default: ElixirALE.GPIO,
      doc: "Module name representing the driver for the Door Bell",
      hidden: true,
      to: "gatekeeper.doorbell.type"
    ],
    "gatekeeper.doorbell.gpio_pin": [
      commented: false,
      datatype: :integer,
      default: 17,
      doc: "GPIO pin for signaling that the door bell has been rung",
      hidden: false,
      to: "gatekeeper.doorbell.gpio_pin"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.url.host": [
      commented: false,
      datatype: :binary,
      default: "localhost",
      doc: "Domain name at which application is running, for web server to generate links",
      hidden: false,
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.url.host"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.root": [
      commented: false,
      datatype: :binary,
      default: Path.dirname(__DIR__),
      doc: "Base directory for Gatekeeper application",
      hidden: false,
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.root"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.secret_key_base": [
      commented: false,
      datatype: :binary,
      doc: "Secret key used for securing session cookies",
      hidden: false,
      env_var: "SECRET_KEY_BASE",
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.secret_key_base"
    ],
    "gatekeeper.Elixir.GatekeeperWeb.Endpoint.http.port": [
      commented: false,
      datatype: :integer,
      default: 4000,
      doc: "Port on which application listens for HTTP connections",
      hidden: false,
      to: "gatekeeper.Elixir.GatekeeperWeb.Endpoint.http.port"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.username": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database username for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_USERNAME",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.username"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.password": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database password for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_PASSWORD",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.password"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.database": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper_dev",
      doc: "Database name for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_DATABASE",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.database"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.hostname": [
      commented: false,
      datatype: :binary,
      default: "localhost",
      doc: "Database hostname for the READ ONLY connection",
      hidden: false,
      env_var: "DATABASE_READ_HOSTNAME",
      to: "gatekeeper.Elixir.Gatekeeper.Repo.hostname"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.template": [
      commented: false,
      datatype: :binary,
      default: "template0",
      doc: "Postgresql template for the READ ONLY connection",
      hidden: true,
      to: "gatekeeper.Elixir.Gatekeeper.Repo.template"
    ],
    "gatekeeper.Elixir.Gatekeeper.Repo.pool_size": [
      commented: false,
      datatype: :integer,
      default: 10,
      doc: "Number of database connections available in the pool for the READ ONLY connection",
      hidden: false,
      to: "gatekeeper.Elixir.Gatekeeper.Repo.pool_size"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.username": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database username for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_USERNAME",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.username"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.password": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper",
      doc: "Database password for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_PASSWORD",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.password"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.database": [
      commented: false,
      datatype: :binary,
      default: "gatekeeper_dev",
      doc: "Database name for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_DATABASE",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.database"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.hostname": [
      commented: false,
      datatype: :binary,
      default: "localhost",
      doc: "Database hostname for the WRITABLE connection",
      hidden: false,
      env_var: "DATABASE_WRITE_HOSTNAME",
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.hostname"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.template": [
      commented: false,
      datatype: :binary,
      default: "template0",
      doc: "Postgresql template for the WRITABLE connection",
      hidden: true,
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.template"
    ],
    "gatekeeper.Elixir.Gatekeeper.WriteRepo.pool_size": [
      commented: false,
      datatype: :integer,
      default: 10,
      doc: "Number of database connections available in the pool for the WRITABLE connection",
      hidden: false,
      to: "gatekeeper.Elixir.Gatekeeper.WriteRepo.pool_size"
    ],
    "ueberauth.Elixir.Ueberauth.providers.slack": [
      commented: false,
      datatype: :binary,
      doc: "Team ID for Slack authentication",
      hidden: false,
      env_var: "SLACK_TEAM_ID",
      to: "ueberauth.Elixir.Ueberauth.providers.slack"
    ],
    "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_id": [
      commented: false,
      datatype: :binary,
      default: "",
      doc: "Slack OAuth Client ID",
      hidden: false,
      env_var: "SLACK_CLIENT_ID",
      to: "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_id"
    ],
    "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_secret": [
      commented: false,
      datatype: :binary,
      default: "",
      doc: "Slack OAuth Client Secret",
      hidden: false,
      env_var: "SLACK_CLIENT_SECRET",
      to: "ueberauth.Elixir.Ueberauth.Strategy.Slack.OAuth.client_secret"
    ],
    "guardian.Elixir.Guardian.ttl": [
      commented: false,
      datatype: {:integer, :atom},
      default: {30, :days},
      doc: "Web session duration",
      hidden: false,
      to: "guardian.Elixir.Guardian.ttl"
    ],
    "guardian.Elixir.Guardian.secret_key": [
      commented: false,
      datatype: :binary,
      default: "",
      doc: "Secret key to sign/secure session cookies",
      hidden: false,
      env_var: "GUARDIAN_SECRET_KEY",
      to: "guardian.Elixir.Guardian.secret_key"
    ],
  ],
  transforms: [
    "ueberauth.Elixir.Ueberauth.providers.slack": fn conf ->
      [{_, team_id}] = Conform.Conf.get(conf, "ueberauth.Elixir.Ueberauth.providers.slack")
      {Ueberauth.Strategy.Slack, [team: team_id]}
    end,
    "gatekeeper.doorlock.type": fn conf ->
      [{_, type}] = Conform.Conf.get(conf, "gatekeeper.doorlock.type")
      case type do
        "gpio" -> ElixirALE.GPIO
        "dummy" -> Gatekeeper.GPIODummy
      end
    end,
    "gatekeeper.doorbell.type": fn conf ->
      [{_, type}] = Conform.Conf.get(conf, "gatekeeper.doorbell.type")
      case type do
        "gpio" -> ElixirALE.GPIO
        "dummy" -> Gatekeeper.GPIODummy
      end
    end
  ],
  validators: []
]


Replace :previous_line tagging with eval/1 in IEx.AutocompleteTest

Code.require_file "../test_helper.exs", __DIR__

defmodule IEx.AutocompleteTest do
  use ExUnit.Case, async: true

  setup do
    evaluator = IEx.Server.start_evaluator([])
    Process.put(:evaluator, evaluator)
    :ok
  end

  defmodule MyServer do
    def evaluator do
      Process.get(:evaluator)
    end
  end

  defp eval(line) do
    ExUnit.CaptureIO.capture_io(fn ->
      evaluator = MyServer.evaluator
      Process.group_leader(evaluator, Process.group_leader)
      send evaluator, {:eval, self(), line <> "\n", %IEx.State{}}
      assert_receive {:evaled, _, _}
    end)
  end

  defp expand(expr) do
    IEx.Autocomplete.expand(Enum.reverse(expr), MyServer)
  end

  test "Erlang module completion" do
    assert expand(':zl') == {:yes, 'ib', []}
  end

  test "Erlang module no completion" do
    assert expand(':unknown') == {:no, '', []}
    assert expand('Enum:') == {:no, '', []}
  end

  test "Erlang module multiple values completion" do
    {:yes, '', list} = expand(':user')
    assert 'user' in list
    assert 'user_drv' in list
  end

  test "Erlang root completion" do
    {:yes, '', list} = expand(':')
    assert is_list(list)
    assert 'lists' in list
  end

  test "Elixir proxy" do
    {:yes, '', list} = expand('E')
    assert 'Elixir' in list
  end

  test "Elixir completion" do
    assert expand('En') == {:yes, 'um', []}
    assert expand('Enumera') == {:yes, 'ble', []}
  end

  test "Elixir completion with self" do
    assert expand('Enumerable') == {:yes, '.', []}
  end

  test "Elixir completion on modules from load path" do
    assert expand('Str') == {:yes, [], ['Stream', 'String', 'StringIO']}
    assert expand('Ma') == {:yes, '', ['Macro', 'Map', 'MapSet', 'MatchError']}
    assert expand('Dic') == {:yes, 't', []}
    assert expand('Ex')  == {:yes, [], ['ExUnit', 'Exception']}
  end

  test "Elixir no completion for underscored functions with no doc" do
    {:module, _, bytecode, _} =
      defmodule Elixir.Sample do
        def __foo__(), do: 0
        @doc "Bar doc"
        def __bar__(), do: 1
      end
    File.write!("Elixir.Sample.beam", bytecode)
    assert Code.get_docs(Sample, :docs)
    assert expand('Sample._') == {:yes, '_bar__', []}
  after
    File.rm("Elixir.Sample.beam")
    :code.purge(Sample)
    :code.delete(Sample)
  end

  test "Elixir no completion" do
    assert expand('.')   == {:no, '', []}
    assert expand('Xyz') == {:no, '', []}
    assert expand('x.Foo') == {:no, '', []}
  end

  test "Elixir root submodule completion" do
    assert expand('Elixir.Acce') == {:yes, 'ss', []}
  end

  test "Elixir submodule completion" do
    assert expand('String.Cha') == {:yes, 'rs', []}
  end

  test "Elixir submodule no completion" do
    assert expand('IEx.Xyz') == {:no, '', []}
  end

  test "function completion" do
    assert expand('System.ve') == {:yes, 'rsion', []}
    assert expand(':ets.fun2') == {:yes, 'ms', []}
  end

  test "function completion with arity" do
    assert expand('String.printable?')  == {:yes, '', ['printable?/1']}
    assert expand('String.printable?/') == {:yes, '', ['printable?/1']}
  end

  test "function completion using a variable bound to a module" do
    eval("mod = String")
    assert expand('mod.print') == {:yes, 'able?', []}
  end

  test "map atom key completion is supported" do
    eval("map = %{foo: 1, bar_1: 23, bar_2: 14}")
    assert expand('map.f') == {:yes, 'oo', []}
    assert expand('map.b') == {:yes, 'ar_', []}
    assert expand('map.bar_') == {:yes, '', ['bar_1', 'bar_2']}
    assert expand('map.c') == {:no, '', []}
    assert expand('map.') == {:yes, '', ['bar_1', 'bar_2', 'foo']}
    assert expand('map.foo') == {:no, '', []}
  end

  test "nested map atom key completion is supported" do
    eval("map = %{nested: %{deeply: %{foo: 1, bar_1: 23, bar_2: 14, mod: String, num: 1}}}")
    assert expand('map.nested.deeply.f') == {:yes, 'oo', []}
    assert expand('map.nested.deeply.b') == {:yes, 'ar_', []}
    assert expand('map.nested.deeply.bar_') == {:yes, '', ['bar_1', 'bar_2']}
    assert expand('map.nested.deeply.') == {:yes, '', ['bar_1', 'bar_2', 'foo', 'mod', 'num']}
    assert expand('map.nested.deeply.mod.print') == {:yes, 'able?', []}

    assert expand('map.nested') == {:yes, '.', []}
    assert expand('map.nested.deeply') == {:yes, '.', []}
    assert expand('map.nested.deeply.foo') == {:no, '', []}

    assert expand('map.nested.deeply.c') == {:no, '', []}
    assert expand('map.a.b.c.f') == {:no, '', []}
  end

  test "map string key completion is not supported" do
    eval(~S(map = %{"foo" => 1}))
    assert expand('map.f') == {:no, '', []}
  end

  test "autocompletion off a bound variable only works for modules and maps" do
    eval("num = 5; map = %{nested: %{num: 23}}")
    assert expand('num.print') == {:no, '', []}
    assert expand('map.nested.num.f') == {:no, '', []}
    assert expand('map.nested.num.key.f') == {:no, '', []}
  end

  test "autocompletion using access syntax does is not supported" do
    eval("map = %{nested: %{deeply: %{num: 23}}}")
    assert expand('map[:nested][:deeply].n') == {:no, '', []}
    assert expand('map[:nested].deeply.n') == {:no, '', []}
    assert expand('map.nested.[:deeply].n') == {:no, '', []}
  end

  test "autocompletion off of unbound variables is not supported" do
    eval("num = 5")
    assert expand('other_var.f') == {:no, '', []}
    assert expand('a.b.c.d') == {:no, '', []}
  end

  test "macro completion" do
    {:yes, '', list} = expand('Kernel.is_')
    assert is_list(list)
  end

  test "imports completion" do
    {:yes, '', list} = expand('')
    assert is_list(list)
    assert 'h/1' in list
    assert 'unquote/1' in list
    assert 'pwd/0' in list
  end

  test "kernel import completion" do
    assert expand('defstru') == {:yes, 'ct', []}
    assert expand('put_') == {:yes, '', ['put_elem/3', 'put_in/2', 'put_in/3']}
  end

  test "variable name completion" do
    eval("numeral = 3; number = 3; nothing = nil")
    assert expand('numb') == {:yes, 'er', []}
    assert expand('num') == {:yes, '', ['number', 'numeral']}
    assert expand('no') == {:yes, '', ['nothing', 'node/0', 'node/1', 'not/1']}
  end

  test "completion of manually imported functions and macros" do
    eval("import Enum; import Supervisor, only: [count_children: 1]; import Protocol")
    assert expand('take') == {:yes, '', ['take/2', 'take_every/2', 'take_random/2', 'take_while/2']}
    assert expand('count') == {:yes, '', ['count/1', 'count/2', 'count_children/1']}
    assert expand('der') == {:yes, 'ive', []}
  end

  defmacro define_var do
    quote do: var!(my_var_1, Elixir) = 1
  end

  test "ignores quoted variables when performing variable completion" do
    eval("require #{__MODULE__}; #{__MODULE__}.define_var(); my_var_2 = 2")
    assert expand('my_var') == {:yes, '_2', []}
  end

  test "kernel special form completion" do
    assert expand('unquote_spl') == {:yes, 'icing', []}
  end

  test "completion inside expression" do
    assert expand('1 En') == {:yes, 'um', []}
    assert expand('Test(En') == {:yes, 'um', []}
    assert expand('Test :zl') == {:yes, 'ib', []}
    assert expand('[:zl') == {:yes, 'ib', []}
    assert expand('{:zl') == {:yes, 'ib', []}
  end

  test "ampersand completion" do
    assert expand('&Enu') == {:yes, 'm', []}
    assert expand('&Enum.a') == {:yes, [], ['all?/1', 'all?/2', 'any?/1', 'any?/2', 'at/2', 'at/3']}
    assert expand('f = &Enum.a') == {:yes, [], ['all?/1', 'all?/2', 'any?/1', 'any?/2', 'at/2', 'at/3']}
  end

  defmodule SublevelTest.LevelA.LevelB do
  end

  test "Elixir completion sublevel" do
    assert expand('IEx.AutocompleteTest.SublevelTest.') == {:yes, 'LevelA', []}
  end

  test "complete aliases of Elixir modules" do
    eval("alias List, as: MyList")
    assert expand('MyL') == {:yes, 'ist', []}
    assert expand('MyList') == {:yes, '.', []}
    assert expand('MyList.to_integer') == {:yes, [], ['to_integer/1', 'to_integer/2']}
  end

  test "complete aliases of Erlang modules" do
    eval("alias :lists, as: EList")
    assert expand('EL') == {:yes, 'ist', []}
    assert expand('EList') == {:yes, '.', []}
    assert expand('EList.map') == {:yes, [], ['map/2', 'mapfoldl/3', 'mapfoldr/3']}
  end

  test "completion for functions added when compiled module is reloaded" do
    {:module, _, bytecode, _} =
      defmodule Sample do
        def foo(), do: 0
      end
    File.write!("Elixir.IEx.AutocompleteTest.Sample.beam", bytecode)
    assert Code.get_docs(Sample, :docs)
    assert expand('IEx.AutocompleteTest.Sample.foo') == {:yes, '', ['foo/0']}

    Code.compiler_options(ignore_module_conflict: true)
    defmodule Sample do
      def foo(), do: 0
      def foobar(), do: 0
    end
    assert expand('IEx.AutocompleteTest.Sample.foo') == {:yes, '', ['foo/0', 'foobar/0']}
  after
    File.rm("Elixir.IEx.AutocompleteTest.Sample.beam")
    Code.compiler_options(ignore_module_conflict: false)
    :code.purge(Sample)
    :code.delete(Sample)
  end

  defmodule MyStruct do
    defstruct [:my_val]
  end

  test "completion for struct names" do
    assert expand('%IEx.AutocompleteTest.MyStr') == {:yes, 'uct', []}
  end

  test "completion for struct keys" do
    eval("struct = %IEx.AutocompleteTest.MyStruct{}")
    assert expand('struct.my') == {:yes, '_val', []}
  end
end

Code.require_file "../test_helper.exs", __DIR__

defmodule IEx.AutocompleteTest do
  use ExUnit.Case, async: true

  setup context do
    ExUnit.CaptureIO.capture_io(fn ->
      evaluator = IEx.Server.start_evaluator([])
      Process.put(:evaluator, evaluator)

      previous_line = context[:previous_line]
      if previous_line do
        send evaluator, {:eval, self(), previous_line <> "\n", %IEx.State{}}
        assert_receive {:evaled, _, _}
      end
    end)

    :ok
  end

  defmodule MyServer do
    def evaluator do
      Process.get(:evaluator)
    end
  end

  defp expand(expr) do
    IEx.Autocomplete.expand(Enum.reverse(expr), MyServer)
  end

  test "Erlang module completion" do
    assert expand(':zl') == {:yes, 'ib', []}
  end

  test "Erlang module no completion" do
    assert expand(':unknown') == {:no, '', []}
    assert expand('Enum:') == {:no, '', []}
  end

  test "Erlang module multiple values completion" do
    {:yes, '', list} = expand(':user')
    assert 'user' in list
    assert 'user_drv' in list
  end

  test "Erlang root completion" do
    {:yes, '', list} = expand(':')
    assert is_list(list)
    assert 'lists' in list
  end

  test "Elixir proxy" do
    {:yes, '', list} = expand('E')
    assert 'Elixir' in list
  end

  test "Elixir completion" do
    assert expand('En') == {:yes, 'um', []}
    assert expand('Enumera') == {:yes, 'ble', []}
  end

  test "Elixir completion with self" do
    assert expand('Enumerable') == {:yes, '.', []}
  end

  test "Elixir completion on modules from load path" do
    assert expand('Str') == {:yes, [], ['Stream', 'String', 'StringIO']}
    assert expand('Ma') == {:yes, '', ['Macro', 'Map', 'MapSet', 'MatchError']}
    assert expand('Dic') == {:yes, 't', []}
    assert expand('Ex')  == {:yes, [], ['ExUnit', 'Exception']}
  end

  test "Elixir no completion for underscored functions with no doc" do
    {:module, _, bytecode, _} =
      defmodule Elixir.Sample do
        def __foo__(), do: 0
        @doc "Bar doc"
        def __bar__(), do: 1
      end
    File.write!("Elixir.Sample.beam", bytecode)
    assert Code.get_docs(Sample, :docs)
    assert expand('Sample._') == {:yes, '_bar__', []}
  after
    File.rm("Elixir.Sample.beam")
    :code.purge(Sample)
    :code.delete(Sample)
  end

  test "Elixir no completion" do
    assert expand('.')   == {:no, '', []}
    assert expand('Xyz') == {:no, '', []}
    assert expand('x.Foo') == {:no, '', []}
  end

  test "Elixir root submodule completion" do
    assert expand('Elixir.Acce') == {:yes, 'ss', []}
  end

  test "Elixir submodule completion" do
    assert expand('String.Cha') == {:yes, 'rs', []}
  end

  test "Elixir submodule no completion" do
    assert expand('IEx.Xyz') == {:no, '', []}
  end

  test "function completion" do
    assert expand('System.ve') == {:yes, 'rsion', []}
    assert expand(':ets.fun2') == {:yes, 'ms', []}
  end

  test "function completion with arity" do
    assert expand('String.printable?')  == {:yes, '', ['printable?/1']}
    assert expand('String.printable?/') == {:yes, '', ['printable?/1']}
  end

  @tag previous_line: "mod = String"
  test "function completion using a variable bound to a module" do
    assert expand('mod.print') == {:yes, 'able?', []}
  end

  @tag previous_line: "map = %{foo: 1, bar_1: 23, bar_2: 14}"
  test "map atom key completion is supported" do
    assert expand('map.f') == {:yes, 'oo', []}
    assert expand('map.b') == {:yes, 'ar_', []}
    assert expand('map.bar_') == {:yes, '', ['bar_1', 'bar_2']}
    assert expand('map.c') == {:no, '', []}
    assert expand('map.') == {:yes, '', ['bar_1', 'bar_2', 'foo']}
    assert expand('map.foo') == {:no, '', []}
  end

  @tag previous_line: "map = %{nested: %{deeply: %{foo: 1, bar_1: 23, bar_2: 14, mod: String, num: 1}}}"
  test "nested map atom key completion is supported" do
    assert expand('map.nested.deeply.f') == {:yes, 'oo', []}
    assert expand('map.nested.deeply.b') == {:yes, 'ar_', []}
    assert expand('map.nested.deeply.bar_') == {:yes, '', ['bar_1', 'bar_2']}
    assert expand('map.nested.deeply.') == {:yes, '', ['bar_1', 'bar_2', 'foo', 'mod', 'num']}
    assert expand('map.nested.deeply.mod.print') == {:yes, 'able?', []}

    assert expand('map.nested') == {:yes, '.', []}
    assert expand('map.nested.deeply') == {:yes, '.', []}
    assert expand('map.nested.deeply.foo') == {:no, '', []}

    assert expand('map.nested.deeply.c') == {:no, '', []}
    assert expand('map.a.b.c.f') == {:no, '', []}
  end

  @tag previous_line: ~s(map = %{"foo" => 1})
  test "map string key completion is not supported" do
    assert expand('map.f') == {:no, '', []}
  end

  @tag previous_line: "num = 5; map = %{nested: %{num: 23}}"
  test "autocompletion off a bound variable only works for modules and maps" do
    assert expand('num.print') == {:no, '', []}
    assert expand('map.nested.num.f') == {:no, '', []}
    assert expand('map.nested.num.key.f') == {:no, '', []}
  end

  @tag previous_line: "map = %{nested: %{deeply: %{num: 23}}}"
  test "autocompletion using access syntax does is not supported" do
    assert expand('map[:nested][:deeply].n') == {:no, '', []}
    assert expand('map[:nested].deeply.n') == {:no, '', []}
    assert expand('map.nested.[:deeply].n') == {:no, '', []}
  end

  @tag previous_line: "num = 5"
  test "autocompletion off of unbound variables is not supported" do
    assert expand('other_var.f') == {:no, '', []}
    assert expand('a.b.c.d') == {:no, '', []}
  end

  test "macro completion" do
    {:yes, '', list} = expand('Kernel.is_')
    assert is_list(list)
  end

  test "imports completion" do
    {:yes, '', list} = expand('')
    assert is_list(list)
    assert 'h/1' in list
    assert 'unquote/1' in list
    assert 'pwd/0' in list
  end

  test "kernel import completion" do
    assert expand('defstru') == {:yes, 'ct', []}
    assert expand('put_') == {:yes, '', ['put_elem/3', 'put_in/2', 'put_in/3']}
  end

  @tag previous_line: "numeral = 3; number = 3; nothing = nil"
  test "variable name completion" do
    assert expand('numb') == {:yes, 'er', []}
    assert expand('num') == {:yes, '', ['number', 'numeral']}
    assert expand('no') == {:yes, '', ['nothing', 'node/0', 'node/1', 'not/1']}
  end

  @tag previous_line: "import Enum; import Supervisor, only: [count_children: 1]; import Protocol"
  test "completion of manually imported functions and macros" do
    assert expand('take') == {:yes, '', ['take/2', 'take_every/2', 'take_random/2', 'take_while/2']}
    assert expand('count') == {:yes, '', ['count/1', 'count/2', 'count_children/1']}
    assert expand('der') == {:yes, 'ive', []}
  end

  defmacro define_var do
    quote do: var!(my_var_1, Elixir) = 1
  end

  @tag previous_line: "require #{__MODULE__}; #{__MODULE__}.define_var(); my_var_2 = 2"
  test "ignores quoted variables when performing variable completion" do
    assert expand('my_var') == {:yes, '_2', []}
  end

  test "kernel special form completion" do
    assert expand('unquote_spl') == {:yes, 'icing', []}
  end

  test "completion inside expression" do
    assert expand('1 En') == {:yes, 'um', []}
    assert expand('Test(En') == {:yes, 'um', []}
    assert expand('Test :zl') == {:yes, 'ib', []}
    assert expand('[:zl') == {:yes, 'ib', []}
    assert expand('{:zl') == {:yes, 'ib', []}
  end

  test "ampersand completion" do
    assert expand('&Enu') == {:yes, 'm', []}
    assert expand('&Enum.a') == {:yes, [], ['all?/1', 'all?/2', 'any?/1', 'any?/2', 'at/2', 'at/3']}
    assert expand('f = &Enum.a') == {:yes, [], ['all?/1', 'all?/2', 'any?/1', 'any?/2', 'at/2', 'at/3']}
  end

  defmodule SublevelTest.LevelA.LevelB do
  end

  test "Elixir completion sublevel" do
    assert expand('IEx.AutocompleteTest.SublevelTest.') == {:yes, 'LevelA', []}
  end

  @tag previous_line: "alias List, as: MyList"
  test "complete aliases of Elixir modules" do
    assert expand('MyL') == {:yes, 'ist', []}
    assert expand('MyList') == {:yes, '.', []}
    assert expand('MyList.to_integer') == {:yes, [], ['to_integer/1', 'to_integer/2']}
  end

  @tag previous_line: "alias :lists, as: EList"
  test "complete aliases of Erlang modules" do
    assert expand('EL') == {:yes, 'ist', []}
    assert expand('EList') == {:yes, '.', []}
    assert expand('EList.map') == {:yes, [], ['map/2', 'mapfoldl/3', 'mapfoldr/3']}
  end

  test "completion for functions added when compiled module is reloaded" do
    {:module, _, bytecode, _} =
      defmodule Sample do
        def foo(), do: 0
      end
    File.write!("Elixir.IEx.AutocompleteTest.Sample.beam", bytecode)
    assert Code.get_docs(Sample, :docs)
    assert expand('IEx.AutocompleteTest.Sample.foo') == {:yes, '', ['foo/0']}

    Code.compiler_options(ignore_module_conflict: true)
    defmodule Sample do
      def foo(), do: 0
      def foobar(), do: 0
    end
    assert expand('IEx.AutocompleteTest.Sample.foo') == {:yes, '', ['foo/0', 'foobar/0']}
  after
    File.rm("Elixir.IEx.AutocompleteTest.Sample.beam")
    Code.compiler_options(ignore_module_conflict: false)
    :code.purge(Sample)
    :code.delete(Sample)
  end

  defmodule MyStruct do
    defstruct [:my_val]
  end

  test "completion for struct names" do
    assert expand('%IEx.AutocompleteTest.MyStr') == {:yes, 'uct', []}
  end

  @tag previous_line: "struct = %IEx.AutocompleteTest.MyStruct{}"
  test "completion for struct keys" do
    assert expand('struct.my') == {:yes, '_val', []}
  end
end