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
|
## Talking to an Alpine mirror: index fetch, package download, local caching.
##
## Everything lands in a cache directory first, so a re-run of `muslkit add` is
## offline and a sysroot can be rebuilt without touching the network.
import std/[httpclient, os, strutils, times]
import ./index
const
defaultMirror* = "https://dl-cdn.alpinelinux.org/alpine"
defaultBranch* = "v3.21"
defaultRepos* = @["main", "community"]
defaultArch* = "x86_64"
indexMaxAge* = initDuration(hours = 24)
type Remote* = object
mirror*, branch*, arch*: string
repos*: seq[string]
cacheDir*: string
quiet*: bool
proc initRemote*(mirror = defaultMirror, branch = defaultBranch,
arch = defaultArch, repos = defaultRepos,
cacheDir = "", quiet = false): Remote =
Remote(mirror: mirror, branch: branch, arch: arch, repos: repos,
cacheDir: if cacheDir.len > 0: cacheDir
else: getCacheDir() / "muslkit",
quiet: quiet)
proc repoUrl*(r: Remote, repo: string): string =
r.mirror & "/" & r.branch & "/" & repo & "/" & r.arch
proc note(r: Remote, msg: string) =
if not r.quiet: stderr.writeLine msg
proc download(r: Remote, url, dest: string) =
createDir dest.parentDir
let tmp = dest & ".part"
var client = newHttpClient(timeout = 60_000)
defer: client.close()
try:
client.downloadFile(url, tmp)
except CatchableError as e:
removeFile tmp
raise newException(IOError, "fetching " & url & " failed: " & e.msg)
moveFile(tmp, dest)
proc needsRefresh(path: string, maxAge: Duration): bool =
if not fileExists(path): return true
getTime() - path.getLastModificationTime() > maxAge
proc untar(archive, dest: string, members: openArray[string] = []) =
## Shell out to tar: an .apk is concatenated gzip streams, which GNU tar
## already reads, and vendoring a decompressor would be the larger evil.
createDir dest
var cmd = "tar -xzf " & quoteShell(archive) & " -C " & quoteShell(dest)
for m in members:
cmd.add " " & quoteShell(m)
# apk metadata entries are not part of the package's file list.
cmd.add " --exclude=.PKGINFO --exclude=.SIGN.* --warning=no-unknown-keyword"
if execShellCmd(cmd) != 0:
raise newException(IOError, "tar failed on " & archive)
proc fetchIndex*(r: Remote, refresh = false): Index =
## Fetch (or reuse) every configured repository's APKINDEX, merged.
for repo in r.repos:
let
dir = r.cacheDir / r.branch / repo / r.arch
gz = dir / "APKINDEX.tar.gz"
plain = dir / "APKINDEX"
if refresh or needsRefresh(gz, indexMaxAge):
r.note "fetching index: " & repo & "/" & r.arch
r.download(r.repoUrl(repo) & "/APKINDEX.tar.gz", gz)
removeFile plain
if not fileExists(plain):
untar(gz, dir, ["APKINDEX"])
result.merge parseIndex(readFile(plain), repo)
proc apkFile*(pkg: Pkg): string = pkg.name & "-" & pkg.version & ".apk"
proc fetchPackage*(r: Remote, pkg: Pkg): string =
## Download one package if it is not already cached; returns the local path.
let dest = r.cacheDir / r.branch / pkg.repo / r.arch / apkFile(pkg)
if fileExists(dest) and getFileSize(dest) == pkg.size:
return dest
r.note "downloading " & pkg.name & " " & pkg.version &
" (" & $(pkg.size div 1024) & " KiB)"
r.download(r.repoUrl(pkg.repo) & "/" & apkFile(pkg), dest)
if pkg.size > 0 and getFileSize(dest) != pkg.size:
removeFile dest
raise newException(IOError, pkg.name & ": size mismatch against the index")
dest
proc unpack*(archive, sysroot: string) =
untar(archive, sysroot)
|