defmodule FennecPrecompile.SystemInfo do @available_nif_versions ~w(2.14 2.15 2.16) @doc """ Returns the target triple for download or compile and load. This function is translating and adding more info to the system architecture returned by Elixir/Erlang to one used by `convention`. ## Examples iex> FennecPrecompile.SystemInfo.target(:rust) {:ok, "aarch64-apple-darwin"} iex> FennecPrecompile.SystemInfo.target(:zig) {:ok, "aarch64-macos"} """ def target(convention, opts \\ []) do config = opts[:target_config] || target_config() normalize_target_system = opts[:normalize_target_system] || &FennecPrecompile.SystemInfo.normalize_target_system(&1, &2) available_targets = opts[:available_targets] || default_targets(convention) available_nif_versions = opts[:available_nif_versions] || @available_nif_versions arch_os = case config.os_type do {:unix, _} -> config.target_system |> normalize_target_system.(convention) |> system_architecture_to_string() {:win32, _} -> existing_target = config.target_system |> system_architecture_to_string() # For when someone is setting "TARGET_*" vars on Windows if existing_target in available_targets do existing_target else # 32 or 64 bits arch = case convention do :rust -> case config.word_size do 4 -> "i686" 8 -> "x86_64" _ -> "unknown" end :zig -> case config.word_size do 4 -> "x86" 8 -> "x64" _ -> "unknown" end end config.target_system |> Map.put_new(:arch, arch) |> Map.put_new(:os, "windows") |> Map.put_new(:abi, "msvc") |> system_architecture_to_string() end end cond do arch_os not in available_targets -> {:error, "precompiled NIF is not available for this target: #{inspect(arch_os)}.\n" <> "The available targets are:\n - #{Enum.join(available_targets, "\n - ")}"} config.nif_version not in available_nif_versions -> {:error, "precompiled NIF is not available for this NIF version: #{inspect(config.nif_version)}.\n" <> "The available NIF versions are:\n - #{Enum.join(available_nif_versions, "\n - ")}"} true -> {:ok, arch_os} end end @doc """ Get a config map for current node. ## Parameters - `allow_env_var_override`. Defaults to `true`. Indicating whether allows environment variables to override values for `:arch`, `:vendor`, `:os` and `:abi`. The following environment variables will be tested and replace correspondingly if not empty. | Environment Variable | Maps to Key | |----------------------|-------------| | `TARGET_ARCH` | `:arch` | | `TARGET_VENDOR` | `:vendor` | | `TARGET_OS ` | `:os` | | `TARGET_ABI` | `:abi` | ## Return The map includes 4 keys: - `:os_type`. Value returned from `:os.type()`. - `:target_system`. - `:word_size`. Value returned from `:erlang.system_info(:wordsize)`. - `:nif_version`. Exact or compatible nif version for current node. ## Example iex> FennecPrecompile.SystemInfo.target_config(false) %{ nif_version: "2.16", os_type: {:unix, :darwin}, target_system: %{abi: "darwin21.4.0", arch: "aarch64", os: "apple"}, word_size: 8 } iex> System.put_env("TARGET_ARCH", "x86_64") :ok iex> FennecPrecompile.SystemInfo.target_config(true) %{ nif_version: "2.16", os_type: {:unix, :darwin}, target_system: %{abi: "darwin21.4.0", arch: "x86_64", os: "apple"}, word_size: 8 } """ @spec target_config(boolean()) :: %{ os_type: {:unix, atom} | {:win32, atom}, target_system: %{}, word_size: 4 | 8, nif_version: String.t() } def target_config(allow_env_var_override \\ true) do current_nif_version = current_nif_version() override = if allow_env_var_override do &(maybe_override_with_env_vars(&1)) else &(&1) end nif_version = case find_compatible_nif_version(current_nif_version, @available_nif_versions) do {:ok, vsn} -> vsn :error -> # In case of error, use the current so we can tell the user. current_nif_version end current_system_arch = system_architecture() %{ os_type: :os.type(), target_system: override.(current_system_arch), word_size: :erlang.system_info(:wordsize), nif_version: nif_version } end @doc """ Normalize the given target system map accordingly `convention` can be `:rust` or `:zig` """ def normalize_target_system(target_system, convention) def normalize_target_system(target_system, :rust) do cond do target_system.abi =~ "darwin" -> arch = with "arm" <- target_system.arch, do: "aarch64" %{target_system | arch: arch, os: "apple", abi: "darwin"} target_system.os =~ "linux" -> arch = with "amd64" <- target_system.arch, do: "x86_64" %{target_system | arch: arch} true -> target_system end end def normalize_target_system(target_system, :zig) do cond do target_system.abi =~ "darwin" -> arch = with "arm" <- target_system.arch, do: "aarch64" %{target_system | arch: arch, os: "macos", abi: nil} target_system.os =~ "linux" -> arch = with "amd64" <- target_system.arch, do: "x86_64" %{target_system | arch: arch} target_system.os =~ "windows" -> arch = case target_system.arch do "amd64" -> "x64" "x86_64" -> "x64" _ -> target_system.arch end %{target_system | arch: arch} true -> target_system end end @doc """ Convert system architecture map to its string form. ## Example iex> system_architecture = FennecPrecompile.SystemInfo.system_architecture() %{abi: "darwin21.4.0", arch: "aarch64", os: "apple"} iex> FennecPrecompile.SystemInfo.system_architecture_to_string(system_architecture) "aarch64-apple-darwin21.4.0" """ def system_architecture_to_string(system_architecture) do values = for key <- [:arch, :vendor, :os, :abi], value = system_architecture[key], do: value Enum.join(values, "-") end @doc """ Get nif version for current node. ## Example iex> FennecPrecompile.SystemInfo.current_nif_version() "2.16" """ @spec current_nif_version() :: String.t() def current_nif_version do :erlang.system_info(:nif_version) |> List.to_string() end @doc """ In case one is using this lib in a newer OTP version, we try to find the latest compatible NIF version. """ @spec find_compatible_nif_version(String.t(), [String.t()]) :: {:ok, String.t()} | :error def find_compatible_nif_version(vsn, available \\ @available_nif_versions) do if vsn in available do {:ok, vsn} else [major, minor | _] = parse_version(vsn) available |> Enum.map(&parse_version/1) |> Enum.filter(fn [^major, available_minor | _] when available_minor <= minor -> true [_ | _] -> false end) |> case do [] -> :error match -> {:ok, match |> Enum.max() |> Enum.join(".")} end end end defp parse_version(vsn) do vsn |> String.split(".") |> Enum.map(&String.to_integer/1) end @doc """ Parse `:erlang.system_info(:system_architecture)` into a map The result map may include `:arch`, `:vendor`, `:os` and `:abi` keys if `:erlang.system_info/1` returns "ARCH-VENDOR-OS-ABI". Or the result will include `:arch`, `:os` and `:abi` keys if `:erlang.system_info/1` returns "ARCH-OS-ABI". For other cases, an empty map is returned. ## Examples iex> FennecPrecompile.SystemInfo.system_architecture() %{abi: "darwin21.4.0", arch: "aarch64", os: "apple"} """ @spec system_architecture() :: %{atom => String.t()} | %{} def system_architecture do base = :erlang.system_info(:system_architecture) |> List.to_string() |> String.split("-") triple_keys = case length(base) do 4 -> [:arch, :vendor, :os, :abi] 3 -> [:arch, :os, :abi] _ -> # It's too complicated to find out, and we won't support this for now. [] end triple_keys |> Enum.zip(base) |> Enum.into(%{}) end @doc """ Override system architeture map with environment variables to support systems like Nerves. See: https://hexdocs.pm/nerves/compiling-non-beam-code.html#target-cpu-arch-os-and-abi """ def maybe_override_with_env_vars(original_sys_arch, get_env \\ &System.get_env/1) do envs_with_keys = [ arch: "TARGET_ARCH", vendor: "TARGET_VENDOR", os: "TARGET_OS", abi: "TARGET_ABI" ] Enum.reduce(envs_with_keys, original_sys_arch, fn {key, env_key}, acc -> if env_value = get_env.(env_key) do Map.put(acc, key, env_value) else acc end end) end @doc """ Return a list of default targets. """ def default_targets(convention) def default_targets(:rust) do ~w( aarch64-apple-darwin x86_64-apple-darwin x86_64-unknown-linux-gnu x86_64-unknown-linux-musl arm-unknown-linux-gnueabihf aarch64-unknown-linux-gnu x86_64-pc-windows-msvc x86_64-pc-windows-gnu ) end def default_targets(:zig) do common_targest = ~w( x86_64-linux-gnu x86_64-linux-musl x86_64-windows-gnu aarch64-linux-gnu aarch64-linux-musl riscv64-linux-musl ) with {:unix, :darwin} <- :os.type() do ~w( x86_64-macos aarch64-macos ) else _ -> [] end ++ common_targest end @doc """ Returns user cache directory. """ def cache_dir(sub_dir \\ "") do cache_opts = if System.get_env("MIX_XDG"), do: %{os: :linux}, else: %{} cache_dir = :filename.basedir(:user_cache, sub_dir, cache_opts) File.mkdir_p!(cache_dir) cache_dir end end