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.Module
  alias Zig.Nif

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

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

  defp normalize_nifs_option!(opts) do
    easy_c = Keyword.get(opts, :easy_c)

    if easy_c && !Keyword.has_key?(opts, :nifs) do
      raise CompileError, description: "nif specifications are required for easy_c nifs"
    end

    common = Keyword.merge(@default_options, Keyword.take(opts, @common_options))

    opts
    |> Keyword.update(:nifs, {:auto, []}, &Module.normalize_nifs_option!(&1, common, easy_c))
    |> Keyword.put(:default_options, common)
  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