| muslkit: musl static libraries from Alpine, without Alpine 5feb050 nandi 20h ago | 1 | ## muslkit — musl-linked static libraries from Alpine, without Alpine. |
| 2 | ## |
| 3 | ## muslkit add openssl-libs-static zlib-static |
| 4 | ## muslkit nimflags |
| 5 | ## eval "$(muslkit env)" |
| 6 | ## |
| 7 | ## It fetches Alpine's APKINDEX, resolves a dependency closure, downloads the |
| 8 | ## .apk files (they are just tarballs) and unpacks them into a sysroot |
| 9 | ## directory. Nothing is installed system-wide and nothing needs root. |
| 10 | |
| 11 | import muslkit/[flags, index, repo, sysroot] |
| 12 | export flags, index, repo, sysroot |
| 13 | |
| 14 | when isMainModule: |
| 15 | import std/[os, sequtils, strutils, tables] |
| 16 | |
| 17 | const usageText = """ |
| 18 | muslkit — musl static libraries from Alpine, without Alpine |
| 19 | |
| 20 | Usage: |
| 21 | muslkit add <pkg>... Download packages and unpack into the sysroot |
| 22 | muslkit list Show what the sysroot holds |
| 23 | muslkit libs Show the static libraries in the sysroot |
| 24 | muslkit search <text> Search the index (names and descriptions) |
| 25 | muslkit show <pkg> Index record for one package |
| 26 | muslkit nimflags [-l lib] Print nim flags for a static build |
| 27 | muslkit ccflags [-l lib] Print cc/clang flags |
| 28 | muslkit nimcfg [-o file] Write a nim.cfg fragment (adds --cc when zigcc is set) |
| 29 | muslkit zigcc [-o file] Write a `zig cc -target …-musl` wrapper script |
| 30 | muslkit env Shell exports (PKG_CONFIG_*, MUSLKIT_ROOT) |
| 31 | muslkit path Print the sysroot path |
| 32 | muslkit clean Remove the sysroot (cache is kept) |
| 33 | |
| 34 | Options: |
| 35 | -r, --root <dir> Sysroot (default $XDG_DATA_HOME/muslkit/sysroot) |
| 36 | -b, --branch <ver> Alpine branch (default v3.21; `edge` for rolling) |
| 37 | -a, --arch <arch> Target arch (default x86_64) |
| 38 | -m, --mirror <url> Mirror base (default https://dl-cdn.alpinelinux.org/alpine) |
| 39 | --repo <names> Comma-separated repositories (default main,community) |
| 40 | -l, --lib <name> Add -l<name> to emitted flags (repeatable) |
| 41 | --cc <path> Compiler for `nimcfg` (e.g. the script `muslkit zigcc` writes) |
| 42 | --no-deps Do not pull dependencies |
| 43 | --refresh Re-fetch the index even if it is fresh |
| 44 | -q, --quiet No progress on stderr |
| 45 | -h, --help Show help |
| 46 | |
| 47 | Packages are downloaded over HTTPS and unpacked as-is; muslkit does not check |
| 48 | Alpine's signatures, so treat a sysroot as build input, not as a trust root. |
| 49 | |
| 50 | Examples: |
| 51 | muslkit add openssl-libs-static |
| 52 | nim c -d:ssl $(muslkit nimflags) --cc:clang --clang.exe:./zigcc app.nim |
| 53 | muslkit add --branch edge --arch aarch64 zlib-static |
| 54 | """ |
| 55 | |
| 56 | type Opts = object |
| 57 | root, mirror, branch, arch, cc, output: string |
| 58 | repos, libs, args: seq[string] |
| 59 | noDeps, refresh, quiet, help: bool |
| 60 | |
| 61 | proc defaultRoot(): string = getDataDir() / "muslkit" / "sysroot" |
| 62 | |
| 63 | proc die(msg: string) = |
| 64 | stderr.writeLine "muslkit: " & msg |
| 65 | quit(1) |
| 66 | |
| 67 | proc parseOpts(): Opts = |
| 68 | ## Hand-rolled so `--root dir`, `--root=dir` and `-r dir` all work; parseopt |
| 69 | ## only accepts the attached forms. |
| 70 | result = Opts(root: getEnv("MUSLKIT_ROOT", defaultRoot()), |
| 71 | mirror: defaultMirror, branch: defaultBranch, |
| 72 | arch: defaultArch, repos: defaultRepos) |
| 73 | let argv = commandLineParams() |
| 74 | var i = 0 |
| 75 | while i < argv.len: |
| 76 | var arg = argv[i] |
| 77 | if not arg.startsWith("-") or arg == "-": |
| 78 | result.args.add arg |
| 79 | inc i |
| 80 | continue |
| 81 | var |
| 82 | key = arg.strip(leading = true, trailing = false, chars = {'-'}) |
| 83 | val = "" |
| 84 | attached = false |
| 85 | for sep in ['=', ':']: |
| 86 | let at = key.find(sep) |
| 87 | if at >= 0: |
| 88 | val = key[at + 1 .. ^1] |
| 89 | key = key[0 ..< at] |
| 90 | attached = true |
| 91 | break |
| 92 | |
| 93 | proc takeValue(): string = |
| 94 | if attached: return val |
| 95 | inc i |
| 96 | if i >= argv.len: die "option --" & key & " needs a value" |
| 97 | argv[i] |
| 98 | |
| 99 | case key |
| 100 | of "r", "root": result.root = takeValue() |
| 101 | of "b", "branch": result.branch = takeValue() |
| 102 | of "a", "arch": result.arch = takeValue() |
| 103 | of "m", "mirror": result.mirror = takeValue() |
| 104 | of "repo": result.repos = takeValue().split(',').filterIt(it.len > 0) |
| 105 | of "l", "lib": result.libs.add takeValue() |
| 106 | of "o", "output": result.output = takeValue() |
| 107 | of "cc": result.cc = takeValue() |
| 108 | of "no-deps": result.noDeps = true |
| 109 | of "refresh": result.refresh = true |
| 110 | of "q", "quiet": result.quiet = true |
| 111 | of "h", "help": result.help = true |
| 112 | else: die "unknown option: " & arg |
| 113 | inc i |
| 114 | |
| 115 | proc emit(o: Opts, text: string) = |
| 116 | if o.output.len > 0: |
| 117 | createDir o.output.parentDir |
| 118 | writeFile(o.output, text) |
| 119 | if not o.quiet: stderr.writeLine "wrote " & o.output |
| 120 | else: |
| 121 | stdout.write text |
| 122 | |
| 123 | proc cmdAdd(o: Opts, remote: Remote) = |
| 124 | if o.args.len == 0: die "add: name at least one package" |
| 125 | let |
| 126 | idx = remote.fetchIndex(o.refresh) |
| 127 | wanted = idx.resolve(o.args, withDeps = not o.noDeps) |
| 128 | for pkg in wanted: |
| 129 | let archive = remote.fetchPackage(pkg) |
| 130 | unpack(archive, o.root) |
| 131 | record(o.root, wanted) |
| 132 | if not o.quiet: |
| 133 | stderr.writeLine "unpacked " & $wanted.len & " package(s) into " & o.root |
| 134 | let libs = staticLibs(o.root) |
| 135 | if libs.len > 0: |
| 136 | stderr.writeLine "static libs: " & |
| 137 | libs.mapIt(it.extractFilename).join(" ") |
| 138 | |
| 139 | proc cmdList(o: Opts) = |
| 140 | let entries = readManifest(o.root) |
| 141 | if entries.len == 0: |
| 142 | stderr.writeLine "nothing installed in " & o.root |
| 143 | return |
| 144 | for e in entries: |
| 145 | echo e.name.alignLeft(32), e.version.alignLeft(20), e.repo |
| 146 | |
| 147 | proc cmdSearch(o: Opts, remote: Remote) = |
| 148 | if o.args.len == 0: die "search: give some text" |
| 149 | let |
| 150 | idx = remote.fetchIndex(o.refresh) |
| 151 | needle = o.args.join(" ").toLowerAscii |
| 152 | var hits = 0 |
| 153 | for name, pkg in idx.packages: |
| 154 | if needle in name.toLowerAscii or needle in pkg.description.toLowerAscii: |
| 155 | echo name.alignLeft(34), pkg.version.alignLeft(16), pkg.description |
| 156 | inc hits |
| 157 | if hits == 0: stderr.writeLine "no matches for " & needle |
| 158 | |
| 159 | proc cmdShow(o: Opts, remote: Remote) = |
| 160 | if o.args.len == 0: die "show: name a package" |
| 161 | let idx = remote.fetchIndex(o.refresh) |
| 162 | for want in o.args: |
| 163 | let name = idx.find(want) |
| 164 | if name.len == 0: die "no package provides '" & want & "'" |
| 165 | let pkg = idx.packages[name] |
| 166 | echo "name: ", pkg.name |
| 167 | echo "version: ", pkg.version |
| 168 | echo "repository: ", pkg.repo, "/", remote.arch |
| 169 | echo "size: ", pkg.size div 1024, " KiB" |
| 170 | echo "description: ", pkg.description |
| 171 | if pkg.depends.len > 0: echo "depends: ", pkg.depends.join(" ") |
| 172 | if pkg.provides.len > 0: echo "provides: ", pkg.provides.join(" ") |
| 173 | echo "url: ", remote.repoUrl(pkg.repo), "/", apkFile(pkg) |
| 174 | |
| 175 | proc main() = |
| 176 | let o = parseOpts() |
| 177 | if o.help or o.args.len == 0: |
| 178 | stdout.write usageText |
| 179 | quit(if o.help: 0 else: 1) |
| 180 | let |
| 181 | cmd = o.args[0] |
| 182 | rest = Opts(root: o.root, mirror: o.mirror, branch: o.branch, |
| 183 | arch: o.arch, cc: o.cc, output: o.output, repos: o.repos, |
| 184 | libs: o.libs, args: o.args[1 .. ^1], noDeps: o.noDeps, |
| 185 | refresh: o.refresh, quiet: o.quiet) |
| 186 | remote = initRemote(o.mirror, o.branch, o.arch, o.repos, quiet = o.quiet) |
| 187 | case cmd |
| 188 | of "add": cmdAdd(rest, remote) |
| 189 | of "list": cmdList(rest) |
| 190 | of "libs": stdout.write describe(rest.root) |
| 191 | of "search": cmdSearch(rest, remote) |
| 192 | of "show": cmdShow(rest, remote) |
| 193 | of "nimflags": echo nimFlags(rest.root, rest.libs).join(" ") |
| 194 | of "ccflags": echo ccFlags(rest.root, rest.libs).join(" ") |
| 195 | of "nimcfg": rest.emit nimCfg(rest.root, rest.cc, rest.libs) |
| 196 | of "zigcc": |
| 197 | let path = if rest.output.len > 0: rest.output else: "zigcc" |
| 198 | writeZigCc(path, rest.arch & "-linux-musl") |
| 199 | if not rest.quiet: stderr.writeLine "wrote " & path |
| 200 | of "env": |
| 201 | for kv in pkgConfigEnv(rest.root): |
| 202 | echo "export ", kv |
| 203 | echo "export MUSLKIT_ROOT=", rest.root |
| 204 | of "path": echo rest.root |
| 205 | of "clean": |
| 206 | removeDir rest.root |
| 207 | if not rest.quiet: stderr.writeLine "removed " & rest.root |
| 208 | else: die "unknown command: " & cmd & " (try --help)" |
| 209 | |
| 210 | try: |
| 211 | main() |
| 212 | except CatchableError as e: |
| 213 | die e.msg |