defmodule NPM.PlatformOptional do @moduledoc """ Selects optional dependencies that match the current platform. Many npm packages publish one optional package per operating system and CPU, for example native bindings for Linux, macOS, Windows, x64, or arm64. The lockfile can contain all of those packages for portability, but linking should only install the package that can run on the current machine. This module groups platform-specific package families and keeps the best match for the current OS/CPU while leaving ordinary optional dependencies untouched. """ @spec select(map()) :: map() def select(optional_dependencies) when map_size(optional_dependencies) == 0, do: %{} def select(optional_dependencies) when is_map(optional_dependencies) do cache_key = :erlang.phash2(optional_dependencies) case Process.get({__MODULE__, cache_key}) do nil -> result = do_select(optional_dependencies) Process.put({__MODULE__, cache_key}, result) result cached -> cached end end defp do_select(optional_dependencies) do optional_dependencies |> Enum.group_by(fn {name, _range} -> package_family(name) end) |> Enum.flat_map(fn {_family, deps} -> select_group(deps) end) |> Map.new() end defp select_group([{name, range}]) do [{name, range}] end defp select_group(deps) do exact_matches = Enum.filter(deps, fn {name, _range} -> current_match(name) end) case exact_matches do [first | _] -> [first] [] -> best_platform_match(deps) end end defp best_platform_match(deps) do case Enum.filter(deps, fn {name, _range} -> package_matches_platform?(name) end) do [] -> [] matches -> [Enum.max_by(matches, fn {name, _range} -> platform_score(name) end)] end end defp package_matches_platform?(name) do platform_score(name) > 0 end defp platform_score(name) do case NPM.Registry.get_packument(name) do {:ok, packument} -> packument.versions |> Map.values() |> Enum.map(&version_score/1) |> Enum.max(fn -> 0 end) _ -> 0 end end defp version_score(info) do os_match = if NPM.Platform.os_compatible?(info.os), do: 1, else: 0 cpu_match = if NPM.Platform.cpu_compatible?(info.cpu), do: 1, else: 0 os_match + cpu_match end @platform_tokens ~w(darwin linux win32 freebsd android openharmony arm64 x64 ia32 arm x86 ppc64 s390x riscv64 musl gnu msvc gnueabihf musleabihf) defp package_family(name) do if platform_binding?(name), do: binding_family(name), else: name end defp binding_family(name) do case Regex.run(~r/^(@[^\/]+\/)/, name) do [scope | _] -> scope nil -> name |> String.split("-", parts: 2) |> hd() end end defp platform_binding?(name) do lower = String.downcase(name) Enum.count(@platform_tokens, &String.contains?(lower, &1)) >= 2 end @spec current_match(String.t()) :: boolean() def current_match(name) do os = NPM.Platform.current_os() cpu = NPM.Platform.current_cpu() lower = String.downcase(name) String.contains?(lower, os) and String.contains?(lower, cpu) end end