Skip to content

Commit 410a84b

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat: add 8 system package manager registry adapters
Homebrew, Nix, APT/Debian, RPM/Fedora, Alpine, Flatpak, Snap, and Guix. Total registry count: 42 ecosystems. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 137f1e0 commit 410a84b

10 files changed

Lines changed: 1010 additions & 5 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
defmodule Opsm.Registries.Alpine do
3+
@moduledoc """
4+
Alpine Linux (apk) package registry adapter.
5+
Queries the Alpine package database API.
6+
https://pkgs.alpinelinux.org/packages
7+
"""
8+
9+
alias Opsm.Types.{ManifestFormat, ResolvedPackage}
10+
alias Opsm.Verified.Http, as: VerifiedHttp
11+
12+
@api_url "https://pkgs.alpinelinux.org"
13+
14+
@doc """
15+
Fetch package metadata from Alpine packages.
16+
"""
17+
def fetch_package(name, version \\ "latest") do
18+
url = "#{@api_url}/package/edge/main/x86_64/#{name}"
19+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
20+
{:ok, body} ->
21+
ver = if version == "latest",
22+
do: body["version"] || body["pkgver"],
23+
else: version
24+
{:ok, parse_alpine_package(name, body, ver)}
25+
26+
{:error, :not_found} ->
27+
# Try community repo
28+
fetch_community(name, version)
29+
30+
{:error, %{status: 404}} ->
31+
fetch_community(name, version)
32+
33+
{:error, reason} ->
34+
{:error, reason}
35+
end
36+
end
37+
38+
defp fetch_community(name, version) do
39+
url = "#{@api_url}/package/edge/community/x86_64/#{name}"
40+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
41+
{:ok, body} ->
42+
ver = if version == "latest",
43+
do: body["version"] || body["pkgver"],
44+
else: version
45+
{:ok, parse_alpine_package(name, body, ver)}
46+
47+
{:error, :not_found} -> {:error, :not_found}
48+
{:error, %{status: 404}} -> {:error, :not_found}
49+
{:error, reason} -> {:error, reason}
50+
end
51+
end
52+
53+
defp parse_alpine_package(name, body, version) do
54+
deps = (body["depends"] || [])
55+
|> Enum.map(fn d ->
56+
dep_name = String.replace(d, ~r/[><=].*/, "")
57+
{dep_name, "*"}
58+
end)
59+
|> Map.new()
60+
61+
manifest = %ManifestFormat{
62+
name: name,
63+
version: version || "0.0.0",
64+
description: body["description"] || body["pkgdesc"],
65+
license: body["license"],
66+
homepage: body["url"],
67+
repository: "https://gitlab.alpinelinux.org/alpine/aports/-/tree/master/main/#{name}",
68+
dependencies: deps
69+
}
70+
71+
%ResolvedPackage{
72+
package: name,
73+
version: version || "0.0.0",
74+
forth: :alpine,
75+
manifest: manifest,
76+
tarball_url: nil,
77+
checksum: nil,
78+
attestations: [],
79+
}
80+
end
81+
82+
@doc """
83+
Get versions — Alpine typically has one version per branch.
84+
"""
85+
def get_versions(name) do
86+
case fetch_package(name) do
87+
{:ok, pkg} -> {:ok, [pkg.version]}
88+
{:error, _} = err -> err
89+
end
90+
end
91+
92+
@doc """
93+
Search for packages.
94+
"""
95+
def search(query, _opts \\ []) do
96+
url = "#{@api_url}/packages?name=#{URI.encode(query)}*&branch=edge&arch=x86_64"
97+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
98+
{:ok, body} when is_list(body) ->
99+
results = body
100+
|> Enum.take(20)
101+
|> Enum.map(fn p ->
102+
%{name: p["name"], version: p["version"], description: p["description"]}
103+
end)
104+
{:ok, results}
105+
106+
{:ok, _} -> {:ok, []}
107+
{:error, reason} -> {:error, reason}
108+
end
109+
end
110+
111+
def exists?(name) do
112+
case fetch_package(name) do
113+
{:ok, _} -> true
114+
{:error, _} -> false
115+
end
116+
end
117+
118+
def versions(name), do: get_versions(name)
119+
end

opsm_ex/lib/opsm/registries/apt.ex

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
defmodule Opsm.Registries.Apt do
3+
@moduledoc """
4+
APT/dpkg (Debian/Ubuntu) package registry adapter.
5+
Queries the Debian Sources/Packages API and Ubuntu Launchpad.
6+
https://sources.debian.org/api/
7+
"""
8+
9+
alias Opsm.Types.{ManifestFormat, ResolvedPackage}
10+
alias Opsm.Verified.Http, as: VerifiedHttp
11+
12+
@debian_api "https://sources.debian.org/api"
13+
# Ubuntu Launchpad API for future use
14+
# @ubuntu_api "https://api.launchpad.net/1.0/ubuntu/+archive/primary"
15+
16+
@doc """
17+
Fetch package metadata from Debian sources API.
18+
"""
19+
def fetch_package(name, version \\ "latest") do
20+
url = "#{@debian_api}/src/#{name}/"
21+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
22+
{:ok, body} ->
23+
versions = body["versions"] || []
24+
case versions do
25+
[] -> {:error, :not_found}
26+
_ ->
27+
ver = if version == "latest" do
28+
latest = List.first(versions)
29+
latest["version"]
30+
else
31+
version
32+
end
33+
{:ok, parse_debian_package(name, body, ver, versions)}
34+
end
35+
36+
{:error, :not_found} -> {:error, :not_found}
37+
{:error, %{status: 404}} -> {:error, :not_found}
38+
{:error, reason} -> {:error, reason}
39+
end
40+
end
41+
42+
defp parse_debian_package(name, _body, version, _versions) do
43+
manifest = %ManifestFormat{
44+
name: name,
45+
version: version || "0.0.0",
46+
description: nil,
47+
license: nil,
48+
homepage: "https://packages.debian.org/#{name}",
49+
repository: "https://salsa.debian.org/debian/#{name}",
50+
dependencies: %{}
51+
}
52+
53+
%ResolvedPackage{
54+
package: name,
55+
version: version || "0.0.0",
56+
forth: :apt,
57+
manifest: manifest,
58+
tarball_url: nil,
59+
checksum: nil,
60+
attestations: [],
61+
}
62+
end
63+
64+
@doc """
65+
Get all available versions from Debian.
66+
"""
67+
def get_versions(name) do
68+
url = "#{@debian_api}/src/#{name}/"
69+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
70+
{:ok, body} ->
71+
versions = (body["versions"] || [])
72+
|> Enum.map(fn v -> v["version"] end)
73+
|> Enum.reject(&is_nil/1)
74+
{:ok, versions}
75+
76+
{:error, _} = err -> err
77+
end
78+
end
79+
80+
@doc """
81+
Search for packages in Debian.
82+
"""
83+
def search(query, _opts \\ []) do
84+
url = "#{@debian_api}/search/#{URI.encode(query)}/"
85+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
86+
{:ok, body} ->
87+
results = (body["results"] || %{})
88+
|> Map.get("exact", %{})
89+
|> Map.get("source", [])
90+
|> Enum.take(20)
91+
|> Enum.map(fn pkg ->
92+
%{name: pkg["name"], version: nil, description: nil}
93+
end)
94+
{:ok, results}
95+
96+
{:error, reason} -> {:error, reason}
97+
end
98+
end
99+
100+
def exists?(name) do
101+
case fetch_package(name) do
102+
{:ok, _} -> true
103+
{:error, _} -> false
104+
end
105+
end
106+
107+
def versions(name), do: get_versions(name)
108+
end
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
defmodule Opsm.Registries.Flatpak do
3+
@moduledoc """
4+
Flatpak (Flathub) registry adapter.
5+
https://flathub.org/api/v2/
6+
Queries the Flathub API for Flatpak application metadata.
7+
"""
8+
9+
alias Opsm.Types.{ManifestFormat, ResolvedPackage}
10+
alias Opsm.Verified.Http, as: VerifiedHttp
11+
12+
@api_url "https://flathub.org/api/v2"
13+
14+
@doc """
15+
Fetch application metadata from Flathub.
16+
Name should be the app ID (e.g., "org.mozilla.firefox").
17+
"""
18+
def fetch_package(name, version \\ "latest") do
19+
url = "#{@api_url}/appstream/#{name}"
20+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
21+
{:ok, body} ->
22+
ver = if version == "latest" do
23+
releases = get_in(body, ["releases"]) || []
24+
case releases do
25+
[latest | _] -> latest["version"]
26+
_ -> "0.0.0"
27+
end
28+
else
29+
version
30+
end
31+
{:ok, parse_flatpak(name, body, ver)}
32+
33+
{:error, :not_found} -> {:error, :not_found}
34+
{:error, %{status: 404}} -> {:error, :not_found}
35+
{:error, reason} -> {:error, reason}
36+
end
37+
end
38+
39+
defp parse_flatpak(name, body, version) do
40+
manifest = %ManifestFormat{
41+
name: name,
42+
version: version || "0.0.0",
43+
description: body["summary"],
44+
license: body["project_license"],
45+
homepage: get_in(body, ["urls", "homepage"]),
46+
repository: get_in(body, ["urls", "vcs_browser"]),
47+
dependencies: %{}
48+
}
49+
50+
%ResolvedPackage{
51+
package: name,
52+
version: version || "0.0.0",
53+
forth: :flatpak,
54+
manifest: manifest,
55+
tarball_url: nil,
56+
checksum: nil,
57+
attestations: [],
58+
}
59+
end
60+
61+
@doc """
62+
Get available versions (releases) from Flathub.
63+
"""
64+
def get_versions(name) do
65+
url = "#{@api_url}/appstream/#{name}"
66+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
67+
{:ok, body} ->
68+
versions = (body["releases"] || [])
69+
|> Enum.map(fn r -> r["version"] end)
70+
|> Enum.reject(&is_nil/1)
71+
{:ok, versions}
72+
73+
{:error, _} = err -> err
74+
end
75+
end
76+
77+
@doc """
78+
Search for applications on Flathub.
79+
"""
80+
def search(query, _opts \\ []) do
81+
url = "#{@api_url}/search?q=#{URI.encode(query)}"
82+
case VerifiedHttp.get_json(url, receive_timeout: 10_000) do
83+
{:ok, body} when is_list(body) ->
84+
results = body
85+
|> Enum.take(20)
86+
|> Enum.map(fn app ->
87+
%{name: app["id"], version: nil, description: app["summary"]}
88+
end)
89+
{:ok, results}
90+
91+
{:ok, %{"hits" => hits}} when is_list(hits) ->
92+
results = hits
93+
|> Enum.take(20)
94+
|> Enum.map(fn app ->
95+
%{name: app["app_id"] || app["id"], version: nil, description: app["summary"]}
96+
end)
97+
{:ok, results}
98+
99+
{:ok, _} -> {:ok, []}
100+
{:error, reason} -> {:error, reason}
101+
end
102+
end
103+
104+
def exists?(name) do
105+
case fetch_package(name) do
106+
{:ok, _} -> true
107+
{:error, _} -> false
108+
end
109+
end
110+
111+
def versions(name), do: get_versions(name)
112+
end

0 commit comments

Comments
 (0)