1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
## APKINDEX parsing and dependency resolution.
##
## An APKINDEX is blank-line-separated records of single-letter fields:
##
## P:openssl-dev
## V:3.3.7-r1
## D:libcrypto3=3.3.7-r1 libssl3=3.3.7-r1 pkgconfig
## p:openssl3-dev=3.3.7-r1 pc:libssl=3.3.7
##
## `D:` depends and `p:` provides both carry optional version constraints, and a
## dependency may name a *provide* (`so:libssl.so.3`) rather than a package.
import std/[sets, strutils, tables]
type
Pkg* = object
name*, version*, arch*, description*, origin*, license*: string
size*: int ## download size in bytes, from `S:`
checksum*: string ## `C:` — apk's control-segment hash, not the file's
depends*: seq[string]
provides*: seq[string]
repo*: string ## main | community | …, filled in by the caller
Index* = object
packages*: OrderedTable[string, Pkg]
providers*: Table[string, string] ## provide name → package name
func stripConstraint*(dep: string): string =
## `musl=1.2.5-r11` → `musl`; `so:libssl.so.3=3` → `so:libssl.so.3`.
## Alpine writes constraints as =, >=, <=, >, < or ~.
for i, c in dep:
if c in {'=', '>', '<', '~'}:
return dep[0 ..< i]
dep
func isConflict*(dep: string): bool = dep.startsWith("!")
proc parseIndex*(text: string, repo = ""): Index =
## Parse one APKINDEX body. Later records win, matching apk's own behavior of
## letting a repository shadow an earlier one.
var
idx: Index
cur: Pkg
proc flush(idx: var Index, cur: var Pkg, repo: string) =
if cur.name.len > 0:
cur.repo = repo
idx.packages[cur.name] = cur
idx.providers[cur.name] = cur.name
for p in cur.provides:
idx.providers[stripConstraint(p)] = cur.name
cur = Pkg()
for rawLine in text.splitLines:
let line = rawLine.strip(leading = false)
if line.len == 0:
flush(idx, cur, repo)
continue
if line.len < 2 or line[1] != ':':
continue
let val = line[2 .. ^1]
case line[0]
of 'P': cur.name = val
of 'V': cur.version = val
of 'A': cur.arch = val
of 'T': cur.description = val
of 'L': cur.license = val
of 'o': cur.origin = val
of 'C': cur.checksum = val
of 'S': cur.size = try: parseInt(val) except ValueError: 0
of 'D': cur.depends = val.splitWhitespace()
of 'p': cur.provides = val.splitWhitespace()
else: discard
flush(idx, cur, repo)
idx
proc merge*(a: var Index, b: Index) =
## Fold another repository's index in. `b` wins on collisions.
for name, pkg in b.packages:
a.packages[name] = pkg
for provide, name in b.providers:
a.providers[provide] = name
proc find*(idx: Index, want: string): string =
## Resolve a name-or-provide to a package name, or "" if nothing supplies it.
let key = stripConstraint(want)
if key in idx.packages: return key
idx.providers.getOrDefault(key, "")
proc resolve*(idx: Index, wanted: openArray[string], withDeps = true): seq[Pkg] =
## Depth-first closure over `wanted`, in install order (dependencies first).
## Raises KeyError naming the first request nothing in the index supplies.
var seen: HashSet[string]
var order: seq[Pkg]
proc visit(want, requestedBy: string) =
if isConflict(want): return
let name = idx.find(want)
if name.len == 0:
let ctx = if requestedBy.len > 0: " (needed by " & requestedBy & ")" else: ""
raise newException(KeyError, "no package provides '" &
stripConstraint(want) & "'" & ctx)
if name in seen: return
seen.incl name
let pkg = idx.packages[name]
if withDeps:
for dep in pkg.depends:
visit(dep, name)
order.add pkg
for want in wanted:
visit(want, "")
order
|