| muslkit: musl static libraries from Alpine, without Alpine 5feb050 nandi 21h ago | 1 | ## Talking to an Alpine mirror: index fetch, package download, local caching. |
| 2 | ## |
| 3 | ## Everything lands in a cache directory first, so a re-run of `muslkit add` is |
| 4 | ## offline and a sysroot can be rebuilt without touching the network. |
| 5 | |
| 6 | import std/[httpclient, os, strutils, times] |
| 7 | import ./index |
| 8 | |
| 9 | const |
| 10 | defaultMirror* = "https://dl-cdn.alpinelinux.org/alpine" |
| 11 | defaultBranch* = "v3.21" |
| 12 | defaultRepos* = @["main", "community"] |
| 13 | defaultArch* = "x86_64" |
| 14 | indexMaxAge* = initDuration(hours = 24) |
| 15 | |
| 16 | type Remote* = object |
| 17 | mirror*, branch*, arch*: string |
| 18 | repos*: seq[string] |
| 19 | cacheDir*: string |
| 20 | quiet*: bool |
| 21 | |
| 22 | proc initRemote*(mirror = defaultMirror, branch = defaultBranch, |
| 23 | arch = defaultArch, repos = defaultRepos, |
| 24 | cacheDir = "", quiet = false): Remote = |
| 25 | Remote(mirror: mirror, branch: branch, arch: arch, repos: repos, |
| 26 | cacheDir: if cacheDir.len > 0: cacheDir |
| 27 | else: getCacheDir() / "muslkit", |
| 28 | quiet: quiet) |
| 29 | |
| 30 | proc repoUrl*(r: Remote, repo: string): string = |
| 31 | r.mirror & "/" & r.branch & "/" & repo & "/" & r.arch |
| 32 | |
| 33 | proc note(r: Remote, msg: string) = |
| 34 | if not r.quiet: stderr.writeLine msg |
| 35 | |
| 36 | proc download(r: Remote, url, dest: string) = |
| 37 | createDir dest.parentDir |
| 38 | let tmp = dest & ".part" |
| 39 | var client = newHttpClient(timeout = 60_000) |
| 40 | defer: client.close() |
| 41 | try: |
| 42 | client.downloadFile(url, tmp) |
| 43 | except CatchableError as e: |
| 44 | removeFile tmp |
| 45 | raise newException(IOError, "fetching " & url & " failed: " & e.msg) |
| 46 | moveFile(tmp, dest) |
| 47 | |
| 48 | proc needsRefresh(path: string, maxAge: Duration): bool = |
| 49 | if not fileExists(path): return true |
| 50 | getTime() - path.getLastModificationTime() > maxAge |
| 51 | |
| 52 | proc untar(archive, dest: string, members: openArray[string] = []) = |
| 53 | ## Shell out to tar: an .apk is concatenated gzip streams, which GNU tar |
| 54 | ## already reads, and vendoring a decompressor would be the larger evil. |
| 55 | createDir dest |
| 56 | var cmd = "tar -xzf " & quoteShell(archive) & " -C " & quoteShell(dest) |
| 57 | for m in members: |
| 58 | cmd.add " " & quoteShell(m) |
| 59 | # apk metadata entries are not part of the package's file list. |
| 60 | cmd.add " --exclude=.PKGINFO --exclude=.SIGN.* --warning=no-unknown-keyword" |
| 61 | if execShellCmd(cmd) != 0: |
| 62 | raise newException(IOError, "tar failed on " & archive) |
| 63 | |
| 64 | proc fetchIndex*(r: Remote, refresh = false): Index = |
| 65 | ## Fetch (or reuse) every configured repository's APKINDEX, merged. |
| 66 | for repo in r.repos: |
| 67 | let |
| 68 | dir = r.cacheDir / r.branch / repo / r.arch |
| 69 | gz = dir / "APKINDEX.tar.gz" |
| 70 | plain = dir / "APKINDEX" |
| 71 | if refresh or needsRefresh(gz, indexMaxAge): |
| 72 | r.note "fetching index: " & repo & "/" & r.arch |
| 73 | r.download(r.repoUrl(repo) & "/APKINDEX.tar.gz", gz) |
| 74 | removeFile plain |
| 75 | if not fileExists(plain): |
| 76 | untar(gz, dir, ["APKINDEX"]) |
| 77 | result.merge parseIndex(readFile(plain), repo) |
| 78 | |
| 79 | proc apkFile*(pkg: Pkg): string = pkg.name & "-" & pkg.version & ".apk" |
| 80 | |
| 81 | proc fetchPackage*(r: Remote, pkg: Pkg): string = |
| 82 | ## Download one package if it is not already cached; returns the local path. |
| 83 | let dest = r.cacheDir / r.branch / pkg.repo / r.arch / apkFile(pkg) |
| 84 | if fileExists(dest) and getFileSize(dest) == pkg.size: |
| 85 | return dest |
| 86 | r.note "downloading " & pkg.name & " " & pkg.version & |
| 87 | " (" & $(pkg.size div 1024) & " KiB)" |
| 88 | r.download(r.repoUrl(pkg.repo) & "/" & apkFile(pkg), dest) |
| 89 | if pkg.size > 0 and getFileSize(dest) != pkg.size: |
| 90 | removeFile dest |
| 91 | raise newException(IOError, pkg.name & ": size mismatch against the index") |
| 92 | dest |
| 93 | |
| 94 | proc unpack*(archive, sysroot: string) = |
| 95 | untar(archive, sysroot) |