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