defmodule NPM.Platform do
@moduledoc """
Platform compatibility checks for npm packages.
Evaluates `os`, `cpu`, and `engines` fields from `package.json`
against the current system to detect incompatible packages.
"""
@doc """
Check if a package is compatible with the current OS.
The `os` field is an array of allowed (or disallowed with `!` prefix)
operating systems.
"""
@spec os_compatible?([String.t()]) :: boolean()
def os_compatible?([]), do: true
def os_compatible?(os_list) when is_list(os_list) do
current = current_os()
has_allowlist = Enum.any?(os_list, &(not String.starts_with?(&1, "!")))
has_blocklist = Enum.any?(os_list, &String.starts_with?(&1, "!"))
blocked = Enum.any?(os_list, &(&1 == "!#{current}"))
if has_allowlist do
Enum.member?(os_list, current) and not blocked
else
not blocked or not has_blocklist
end
end
def os_compatible?(_), do: true
@doc """
Check if a package is compatible with the current CPU architecture.
The `cpu` field is an array of allowed (or disallowed with `!` prefix)
CPU architectures.
"""
@spec cpu_compatible?([String.t()]) :: boolean()
def cpu_compatible?([]), do: true
def cpu_compatible?(cpu_list) when is_list(cpu_list) do
current = current_cpu()
has_allowlist = Enum.any?(cpu_list, &(not String.starts_with?(&1, "!")))
has_blocklist = Enum.any?(cpu_list, &String.starts_with?(&1, "!"))
blocked = Enum.any?(cpu_list, &(&1 == "!#{current}"))
if has_allowlist do
Enum.member?(cpu_list, current) and not blocked
else
not blocked or not has_blocklist
end
end
def cpu_compatible?(_), do: true
@doc "Get the current OS name in npm format."
@spec current_os :: String.t()
def current_os do
case :os.type() do
{:unix, :darwin} -> "darwin"
{:unix, :linux} -> "linux"
{:unix, :freebsd} -> "freebsd"
{:win32, _} -> "win32"
{_, os} -> to_string(os)
end
end
@doc "Get the current CPU architecture in npm format."
@spec current_cpu :: String.t()
def current_cpu do
arch = :erlang.system_info(:system_architecture) |> to_string()
cond do
String.contains?(arch, "x86_64") or String.contains?(arch, "amd64") -> "x64"
String.contains?(arch, "aarch64") or String.contains?(arch, "arm64") -> "arm64"
String.contains?(arch, "arm") -> "arm"
String.contains?(arch, "i686") or String.contains?(arch, "i386") -> "ia32"
true -> arch
end
end
@doc """
Check if the `engines` field is satisfied.
Returns a list of warning strings for unsatisfied engine constraints.
Currently only checks the `node` engine as informational.
"""
@spec check_engines(%{String.t() => String.t()}) :: [String.t()]
def check_engines(engines) when is_map(engines) and map_size(engines) == 0, do: []
def check_engines(engines) when is_map(engines) do
Enum.flat_map(engines, fn
{"node", range} -> ["requires node #{range}"]
{"npm", range} -> ["requires npm #{range}"]
_ -> []
end)
end
def check_engines(_), do: []
end