Packages
nerves
1.8.0
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.0
1.11.3
1.11.2
1.11.1
1.11.0
1.10.5
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.3
1.9.2
retired
1.9.1
1.9.0
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
retired
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
retired
1.7.1
retired
1.7.0
retired
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.11.0
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.4.0-rc.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
Craft and deploy bulletproof embedded software
Current section
Files
Jump to
Current section
Files
scripts/nerves_system_compatibility.exs
#!/usr/bin/env elixir
#
# Print the Nerves System compatibility information based on Nerves Project's
# Github repos. By default, the Github API only allows us 60 requests per hour.
# With an API token, we could get our rate limit bumped to 5000 requests an hour.
# See https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api#authentication
#
# ## Usage
#
# nerves_system_compatibility.exs
#
# GITHUB_API_TOKEN=xxxxxx nerves_system_compatibility.exs
#
Mix.install([:req])
defmodule NervesSystemCompatibility do
@doc """
OTP versions that will be table rows.
"""
@spec otp_versions :: [binary]
def otp_versions do
~w[
23.2.4
23.2.7
23.3.1
24.0.2
24.0.5
24.1
24.1.2
24.1.4
24.1.7
24.2
24.2.2
24.3.2
25.0
]
end
@doc """
The Nerves System targets that are officially supported.
"""
@spec target_systems :: [atom]
def target_systems do
~w[
bbb
rpi
rpi0
rpi2
rpi3
rpi3a
rpi4
osd32mp1
x86_64
grisp2
]a
end
def run do
NervesSystemCompatibility.Data.get()
|> NervesSystemCompatibility.Table.build()
|> IO.puts()
end
end
defmodule NervesSystemCompatibility.API do
@spec fetch_nerves_br_versions! :: [binary]
def fetch_nerves_br_versions! do
fetch_package_versions!("nerves_system_br", requirement: ">= 1.14.0")
end
@spec fetch_nerves_system_versions! :: keyword([binary])
def fetch_nerves_system_versions! do
NervesSystemCompatibility.target_systems()
|> Task.async_stream(&{&1, fetch_nerves_system_versions!(&1)}, timeout: 10_000)
|> Enum.reduce([], fn {:ok, kv}, acc -> [kv | acc] end)
end
@spec fetch_nerves_system_versions!(binary | atom) :: [binary]
def fetch_nerves_system_versions!(target) do
fetch_package_versions!("nerves_system_#{target}")
end
defp fetch_package_versions!(project_name, opts \\ []) do
per_page = opts[:per_page] || 50
requirement = opts[:requirement] || ">= 0.1.0"
url = "https://api.github.com/repos/nerves-project/#{project_name}/tags?per_page=#{per_page}"
%{status: 200, body: body} =
if token = System.get_env("GITHUB_API_TOKEN") do
Req.get!(url, headers: [Authorization: "token #{token}"], cache: true)
else
Req.get!(url, cache: true)
end
for %{"name" => "v" <> version} <- body, Version.match?(version, requirement) do
version
end
|> Enum.sort(Version)
end
@spec fetch_buildroot_version!(binary) :: %{binary => binary}
def fetch_buildroot_version!(nerves_br_version) do
%{status: 200, body: body} =
Req.get!(
"https://raw.githubusercontent.com/nerves-project/nerves_system_br/v#{nerves_br_version}/create-build.sh",
cache: true
)
Regex.named_captures(
~r/NERVES_BR_VERSION=(?<buildroot_version>[0-9.]*)/,
body,
include_captures: true
)
|> Enum.into(%{"nerves_br_version" => nerves_br_version})
end
@spec fetch_otp_version!(binary) :: %{binary => binary}
def fetch_otp_version!(nerves_br_version) do
%{status: 200, body: body} =
Req.get!(
"https://raw.githubusercontent.com/nerves-project/nerves_system_br/v#{nerves_br_version}/.tool-versions",
cache: true
)
Regex.named_captures(
~r/erlang (?<otp_version>[0-9.]*)/,
body,
include_captures: true
)
|> Enum.into(%{"nerves_br_version" => nerves_br_version})
end
@spec fetch_nerves_br_version_for_target!(binary | atom, binary) :: %{binary => binary}
def fetch_nerves_br_version_for_target!(target, target_version) do
%{status: 200, body: body} =
Req.get!(
"https://raw.githubusercontent.com/nerves-project/nerves_system_#{target}/v#{target_version}/mix.lock",
cache: true
)
Regex.named_captures(
~r/:hex, :nerves_system_br, "(?<nerves_br_version>[0-9.]*)"/,
body,
include_captures: true
)
|> Enum.into(%{"target" => target, "target_version" => target_version})
end
end
defmodule NervesSystemCompatibility.Data do
alias NervesSystemCompatibility.API
@doc """
Returns compatibility information for Nerves Systems.
"""
@spec get :: [%{binary => any}]
def get do
nerves_br_version_to_metadata_map =
API.fetch_nerves_br_versions!()
|> Task.async_stream(&{&1, nerves_br_version_to_metadata(&1)}, timeout: 10_000)
|> Enum.reduce(%{}, fn {:ok, {nerves_br_version, nerves_br_metadata}}, acc ->
Map.put(acc, nerves_br_version, nerves_br_metadata)
end)
API.fetch_nerves_system_versions!()
|> Task.async_stream(
fn {target, versions} ->
build_target_metadata(target, versions, nerves_br_version_to_metadata_map)
end,
timeout: 10_000
)
|> Enum.reduce([], fn {:ok, target_metadata}, acc -> target_metadata ++ acc end)
end
defp build_target_metadata(target, target_versions, %{} = nerves_br_version_to_metadata_map) do
for target_version <- target_versions, into: [] do
nerves_br_version =
API.fetch_nerves_br_version_for_target!(target, target_version)
|> Access.fetch!("nerves_br_version")
if metadata_map = nerves_br_version_to_metadata_map[nerves_br_version] do
metadata_map
|> Map.put("target", target)
|> Map.put("target_version", target_version)
else
nil
end
end
|> Enum.reject(&is_nil/1)
end
defp nerves_br_version_to_metadata(nerves_br_version) do
[
Task.async(API, :fetch_buildroot_version!, [nerves_br_version]),
Task.async(API, :fetch_otp_version!, [nerves_br_version])
]
|> Task.await_many(:timer.seconds(10))
|> Enum.reduce(%{"nerves_br_version" => nerves_br_version}, fn %{} = data, acc ->
Map.merge(acc, data)
end)
end
@doc """
Groups the compatibility data by otp version and target.
"""
@spec group_data_by_otp_and_target([%{binary => any}]) :: %{binary => %{atom => any}}
def group_data_by_otp_and_target(compatibility_data) do
compatibility_data
|> Enum.group_by(&get_in(&1, ["otp_version"]))
|> Map.new(fn {otp, otp_entries} ->
{
otp,
otp_entries
|> Enum.group_by(&get_in(&1, ["target"]))
|> Map.new(fn {target, target_entries} ->
{
target,
# Pick the latest available nerves system version.
# Sometimes there are more than one available versions for the same OTP version.
target_entries
|> Enum.reject(fn %{"target_version" => target_version} ->
String.match?(target_version, ~r/-rc/)
end)
|> Enum.max_by(
fn %{"target_version" => target_version} ->
normalize_version(target_version)
end,
Version
)
}
end)
}
end)
end
@doc """
Supplements missing minor and patch values so that the version can be compared.
"""
def normalize_version(version) do
case version |> String.split(".") |> Enum.count(&String.to_integer/1) do
1 -> version <> ".0.0"
2 -> version <> ".0"
3 -> version
_ -> raise("invalid version #{inspect(version)}")
end
end
end
defmodule NervesSystemCompatibility.Table do
alias NervesSystemCompatibility.Data
@doc """
Converts the compatibility data to a markdown table.
"""
@spec build([%{binary => any}]) :: binary
def build(compatibility_data) do
targets = NervesSystemCompatibility.target_systems()
otp_versions = NervesSystemCompatibility.otp_versions()
[
header_row(targets),
divider_row(targets),
data_rows(targets, otp_versions, compatibility_data)
]
|> Enum.join("\n")
end
defp header_row(targets) when is_list(targets) do
[
"|",
[cell("", 12) | Enum.map(targets, &cell/1)] |> Enum.intersperse("|"),
"|"
]
|> Enum.join()
end
defp divider_row(targets) when is_list(targets) do
[
"|",
[cell("---", 12) | List.duplicate(cell("---"), length(targets))] |> Enum.intersperse("|"),
"|"
]
|> Enum.join()
end
defp data_rows(targets, otp_versions, compatibility_data) do
grouped_by_otp_and_target = compatibility_data |> Data.group_data_by_otp_and_target()
for otp_version <- otp_versions, reduce: [] do
acc ->
target_versions =
for target <- targets do
get_in(grouped_by_otp_and_target, [otp_version, target, "target_version"])
end
[data_row(otp_version, target_versions) | acc]
end
|> Enum.join("\n")
end
defp data_row(otp_version, row_values) when is_list(row_values) do
[
"|",
[cell("OTP #{otp_version}", 12) | Enum.map(row_values, &cell/1)] |> Enum.intersperse("|"),
"|"
]
|> Enum.join()
end
defp cell(value, count \\ 10) do
(" " <> to_string(value)) |> String.pad_trailing(count)
end
end
NervesSystemCompatibility.run()