lib/mix/tasks/cmake.ex

defmodule Mix.Tasks.Cmake do
  use Mix.Task
  alias Mix.Tasks.Cmake

  defmacro conj_front(list, val, form) do
    quote do
      if unquote(val), do: unquote(form) ++ unquote(list), else: unquote(list)
    end
  end

  @shortdoc "Generate CMake buiid scripts and then build/install the application"
  @moduledoc """
  Generate CMake buiid scripts and then build/install the application.
  
  $ mix cmake [opt] [build_dir] [source_dir]
  
  ## Command line options
  
  * `--config`    - generate build script
  * `--generator` - specify generator
  * `--parallel`  - parallel jobs level
  * `--target`    - build target
  * `--clean`     - clean before build target
  * `--strip`     - remove debug info from executable
  * `--verbose`   - print process detail
  
  ## Configuration
  Add following configurations at project/1 in your mix.exs if you need.

  ```elixir
  def project do
    [
      cmake: [...]
    ]
  end
  ```

  * `:build_dir`  - working directory {:local, :global, any_directory}
  * `:source_dir` - source directory
  * `:generator`  - specify generator
  * `:build_parallel_level` - parallel jobs level
  """

  @switches [
    config:    :boolean,
    platform:  :string,
    generator: :string,
    parallel:  :integer,
    target:    :string,
    clean:     :boolean,
    strip:     :boolean,
    verbose:   :boolean
  ]

  def run(argv) do
    with\
      {:ok, opts, dirs, _cmake_args} <- parse_argv(argv, strict: @switches)
    do
      if opts[:config] do
        Cmake.Config.cmd(dirs, opts)
      end

      Cmake.Build.cmd(dirs, opts)
      Cmake.Install.cmd(dirs, opts)
    end
  end

  @doc """
  Invoke cmake command with `args`.
  """
  def cmake(build_dir, args, env) do
    build_path  = build_path(build_dir)

    opts = [
      cd: build_path,
      env: env,
      into: IO.stream(:stdio, :line),
      stderr_to_stdout: true
    ]

    # make build directory

    unless File.exists?(build_path), do: File.mkdir_p(build_path)

    if "--verbose" in args do
      IO.inspect([args: args, opts: opts])
    end

    {%IO.Stream{}, status} = System.cmd("cmake", args, opts)
    (status == 0)
  end

  @doc """
  Returns true if the build directory exists.
  """
  def build_dir_exists?(build_dir) do
    build_path(build_dir)
    |> File.exists?()
  end

  @doc """
  Remove cmake build directory. (interpret pseudo-path)
  """
  def remove_build(build_dir) do
    build_path = build_path(build_dir)
    File.rm_rf!(build_path)
  end

  # interpret pseudo-path

  defp build_path(:local),  do: Mix.Project.build_path() |> Path.join(".cmake_build")
  defp build_path(:global), do: Path.absname(System.user_home) |> Path.join(".#{app_name()}")
  defp build_path(dir),     do: Path.expand(dir)

  @doc """
  Get application name.
  """
  def app_name(), do: Atom.to_string(Mix.Project.config[:app])

  @doc """
  Get build/source directory.
  """
  def get_dirs(dirs, config) do
    case dirs do
      [build, source] -> [build, source]
      [build]         -> [build, config[:source_dir]]
      []              -> [config[:build_dir], config[:source_dir]]
      _ -> exit("illegal arguments")
    end
  end
  
  @doc """
  Get :cmake configuration from Mix.exs.
  """
  def get_config() do
    Keyword.get(Mix.Project.config(), :cmake, [])
    # default setting if it has no configuration

    |> Keyword.put_new(:build_dir, :local)
    |> Keyword.put_new(:source_dir, File.cwd!)
    |> Keyword.put_new(:config_opts, [])
    |> Keyword.put_new(:build_opts, [])
  end

  @doc """
  Return a map of default environment variables.
  """
  def default_env() do
    root_dir = :code.root_dir()
    erl_interface_dir = Path.join(root_dir, "usr")
    erts_dir = Path.join(root_dir, "erts-#{:erlang.system_info(:version)}")
    erts_include_dir = Path.join(erts_dir, "include")
    erl_ei_lib_dir = Path.join(erl_interface_dir, "lib")
    erl_ei_include_dir = Path.join(erl_interface_dir, "include")

    %{
      # Don't use Mix.target/0 here for backwards compatability

      "MIX_TARGET" => env("MIX_TARGET", "host"),
      "MIX_ENV" => to_string(Mix.env()),
      "MIX_BUILD_PATH" => Mix.Project.build_path(),
      "MIX_APP_PATH" => Mix.Project.app_path(),
      "MIX_COMPILE_PATH" => Mix.Project.compile_path(),
      "MIX_CONSOLIDATION_PATH" => Mix.Project.consolidation_path(),
      "MIX_DEPS_PATH" => Mix.Project.deps_path(),
      "MIX_MANIFEST_PATH" => Mix.Project.manifest_path(),

      # Rebar naming

      "ERL_EI_LIBDIR" => env("ERL_EI_LIBDIR", erl_ei_lib_dir),
      "ERL_EI_INCLUDE_DIR" => env("ERL_EI_INCLUDE_DIR", erl_ei_include_dir),

      # erlang.mk naming

      "ERTS_INCLUDE_DIR" => env("ERTS_INCLUDE_DIR", erts_include_dir),
      "ERL_INTERFACE_LIB_DIR" => env("ERL_INTERFACE_LIB_DIR", erl_ei_lib_dir),
      "ERL_INTERFACE_INCLUDE_DIR" => env("ERL_INTERFACE_INCLUDE_DIR", erl_ei_include_dir)
    }
  end

  defp env(var, default), do: (System.get_env(var) || default)

  @doc """
  Add an environment variable for child process.
  """
  def add_env(env, _name, nil),                  do: env
  def add_env(env, name, true),                  do: Map.put(env, name, "true")
  def add_env(env, name, i) when is_integer(i), do: Map.put(env, name, Integer.to_string(i))
  def add_env(env, name, f) when is_float(f),   do: Map.put(env, name, Float.to_string(f))
  def add_env(env, name, a) when is_atom(a),    do: Map.put(env, name, Atom.to_string(a))
  def add_env(env, name, s),                     do: Map.put(env, name, s)

  @doc """
  parse command line arguments. (custom)
  """
  def parse_argv(argv, config \\ []) when is_list(argv) and is_list(config) do
    do_parse(argv, config, [], [])
  end

  defp do_parse([], _config, opts, args) do
    {:ok, opts, Enum.reverse(args), []}
  end

  defp do_parse(argv, config, opts, args) do
    case next(argv, config) do
      {:second, rest} ->  # start of 2nd args

        {:ok, opts, Enum.reverse(args), rest}
      {:ok, option, value, rest} ->
        do_parse(rest, config, [{option, value}|Keyword.delete(opts, option)], args)
      {:invalid, key, value, _rest} ->
        {:invalid, key, value}
      {:undefined, _key, _value, rest} ->
        do_parse(rest, config, opts, args)
      {:error, [<<":",atom::binary>>|rest]} -> # atom formed

        do_parse(rest, config, opts, [String.to_atom(atom)|args])
      {:error, [arg|rest]} ->
        do_parse(rest, config, opts, [arg|args])
    end
  end

  def next(argv, opts \\ [])
  def next(["++"|rest], _opts), do: {:second, rest}
  def next(["--"|rest], _opts), do: {:second, rest}
  defdelegate next(argv, opts), to: OptionParser
end