lib/zig/options.ex

defmodule Zig.Options do
  @moduledoc """
  parses and normalizes zig options.

  Also sets up

  `options.zig` file which is mapped to `@import("zigler_options")` in
  `beam.zig`.  This is then exposed as `@import("beam").options` in your code.
  """

  alias Zig.EasyC
  alias Zig.Nif

  @spec elixir_normalize!(keyword) :: keyword
  @doc """
  Performs early normalization of options.  For Elixir only, this converts
  all AST representations which must be escaped before moving on to presenting
  options to the Zigler compiler.
  """
  def elixir_normalize!(opts) do
    # if the nifs option exists (explicit specification of nifs), then search
    # through any nifs that define typespecs and macro-escape them.
    opts
    |> Keyword.replace_lazy(:nifs, &escape_nif_specs/1)
    |> set_auto(opts)
  end

  defp escape_nif_specs(opts) do
    Enum.flat_map(opts, fn
      :auto ->
        []

      {:..., _, nil} ->
        []

      function when is_atom(function) ->
        [{function, []}]

      {nif, nif_opts} ->
        [{nif, escape_spec(nif_opts)}]
    end)
  end

  defp escape_spec(nif_opts) do
    Enum.map(nif_opts, fn
      {:spec, spec} -> {:spec, Macro.escape(spec)}
      other -> other
    end)
  end

  def erlang_normalize!(opts) do
    opts
    |> Keyword.replace_lazy(:nifs, fn
      :auto -> []
      [:auto | rest] -> rest
      explicit -> explicit
    end)
    |> set_auto(opts)
  end

  def set_auto(new_opts, old_opts) do
    explicit_auto = old_opts
    |> Keyword.get(:nifs)
    |> List.wrap()
    |> Enum.any?(fn
      :auto -> true
      {:..., _, nil} -> true
      _ -> false
    end)

    cond do
      explicit_auto ->
        Keyword.update!(new_opts, :nifs, &{:auto, &1})
      Keyword.get(new_opts, :nifs) ->
        new_opts
      true ->
        # this is the implicit auto case
        Keyword.put(new_opts, :nifs, {:auto, []})
    end
  end

  @spec normalize!(keyword) :: keyword
  def normalize!(opts) do
    opts
    |> normalize_nifs
    |> normalize_libs
    |> normalize_build_opts
    |> normalize_include_dirs
    |> normalize_c_src
    |> EasyC.normalize_aliasing()
  end

  @common_options_keys ~w[leak_check]a
  @default_options Nif.default_options()

  def normalize_nifs(opts) do
    common_options = Keyword.take(opts, @common_options_keys)
    opts = Keyword.merge(opts, default_options: @default_options)

    Keyword.update!(opts, :nifs, fn
      {:auto, opts} ->
        {:auto, Enum.map(opts, &Nif.normalize_options!(&1, common_options))}
      opts ->
        Enum.map(opts, &Nif.normalize_options!(&1, common_options))
    end)
  end

  defp normalize_libs(opts) do
    Keyword.put(opts, :link_lib, List.wrap(opts[:link_lib]))
  end

  @use_gpa {:bool, "use_gpa", true}
  defp normalize_build_opts(opts) do
    # creates build_opts out of a list of build opt shortcuts
    use_gpa = Keyword.get(opts, :use_gpa, false)

    if use_gpa do
      Keyword.update(opts, :build_opts, [@use_gpa], fn list ->
        [@use_gpa | list]
      end)
    else
      opts
    end
  end

  defp normalize_include_dirs(opts) do
    Keyword.update(opts, :include_dir, [], fn
      path_or_paths ->
        path_or_paths
        |> List.wrap()
        |> Enum.map(&absolute_path_for(&1, opts))
    end)
  end

  defp absolute_path_for("/" <> _ = path, _opts), do: path

  defp absolute_path_for(relative_path, opts) do
    opts
    |> Keyword.fetch!(:mod_file)
    |> Path.dirname()
    |> Path.join(relative_path)
  end

  # converts optional c_src option to a list of
  # {path, [<c compiler options>]}
  defp normalize_c_src(opts) do
    Keyword.update(opts, :c_src, [], fn
      path_or_paths ->
        path_or_paths
        |> List.wrap()
        |> Enum.flat_map(&normalize_c_src_paths(&1, opts))
    end)
  end

  defp normalize_c_src_paths({path, c_opts}, opts) do
    unless is_list(c_opts), do: raise("c options for c source files must be a list")

    path
    |> absolute_path_for(opts)
    |> expand_directories
    |> Enum.map(fn file -> {file, c_opts} end)
  end

  defp normalize_c_src_paths(path, opts) when is_binary(path) do
    path
    |> absolute_path_for(opts)
    |> expand_directories
    |> Enum.map(fn file -> {file, []} end)
  end

  defp expand_directories(path) do
    List.wrap(
      if String.ends_with?(path, "/*") do
        path = String.replace_suffix(path, "/*", "")

        path
        |> File.ls!()
        |> Enum.flat_map(fn file ->
          List.wrap(
            if String.ends_with?(file, ".c") or String.ends_with?(file, ".cpp") do
              Path.join(path, file)
            end
          )
        end)
      else
        path
      end
    )
  end
end