muslkit: musl static libraries from Alpine, without Alpine
Fetches an APKINDEX, resolves a dependency closure through both package names and provides (so:, pkgconfig:, …), downloads the .apk files and unpacks them into a sysroot. No apk, no container, no root. Flag emission knows the two things that break every static Nim build with TLS: -d:ssl dlopens libssl (so --dynlibOverride plus the archives), and OpenSSL 3 dropped SSL_get_peer_certificate (so the compat define). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5feb050 added
.gitignore +4 -0 | new file mode 100644 | ||
| @@ -0,0 +1,4 @@ | ||
| 1 | +/muslkit | |
| 2 | +/zigcc | |
| 3 | +tests/test_muslkit | |
| 4 | +nimcache/ | |
| new file mode 100644 | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | +/muslkit | ||
| 2 | +/zigcc | ||
| 3 | +tests/test_muslkit | ||
| 4 | +nimcache/ | ||
added
README.md +120 -0 | new file mode 100644 | ||
| @@ -0,0 +1,120 @@ | ||
| 1 | +# muslkit | |
| 2 | + | |
| 3 | +musl-linked static libraries from Alpine, without Alpine. | |
| 4 | + | |
| 5 | +Alpine builds everything against musl and ships `*-static` packages for most of | |
| 6 | +it. An `.apk` is just a tarball, and an `APKINDEX` is just a text file — so you | |
| 7 | +don't need apk, a chroot, a container or a distro to get `libcrypto.a` built | |
| 8 | +for musl. muslkit fetches the index, resolves a dependency closure, downloads | |
| 9 | +the packages and unpacks them into a sysroot directory you own. | |
| 10 | + | |
| 11 | +Nothing is installed system-wide. Nothing needs root. | |
| 12 | + | |
| 13 | +```bash | |
| 14 | +muslkit add openssl-libs-static | |
| 15 | +nim c -d:ssl $(muslkit nimflags) --cc:clang --clang.exe:./zigcc app.nim | |
| 16 | +``` | |
| 17 | + | |
| 18 | +## Why | |
| 19 | + | |
| 20 | +Getting a fully static Nim (or C, or Zig) binary with TLS on a glibc distro | |
| 21 | +means finding musl-built archives, and the usual answers are "install Alpine", | |
| 22 | +"run Docker" or "use Nix". Alpine's mirrors already serve exactly the files — | |
| 23 | +this just takes them. | |
| 24 | + | |
| 25 | +## Usage | |
| 26 | + | |
| 27 | +``` | |
| 28 | +muslkit add <pkg>... Download packages and unpack into the sysroot | |
| 29 | +muslkit list Show what the sysroot holds | |
| 30 | +muslkit libs Show the static libraries in the sysroot | |
| 31 | +muslkit search <text> Search the index (names and descriptions) | |
| 32 | +muslkit show <pkg> Index record for one package | |
| 33 | +muslkit nimflags [-l lib] Print nim flags for a static build | |
| 34 | +muslkit ccflags [-l lib] Print cc/clang flags | |
| 35 | +muslkit nimcfg [-o file] Write a nim.cfg fragment | |
| 36 | +muslkit zigcc [-o file] Write a `zig cc -target …-musl` wrapper script | |
| 37 | +muslkit env Shell exports (PKG_CONFIG_*, MUSLKIT_ROOT) | |
| 38 | +muslkit path Print the sysroot path | |
| 39 | +muslkit clean Remove the sysroot (cache is kept) | |
| 40 | +``` | |
| 41 | + | |
| 42 | +Options: `-r/--root`, `-b/--branch` (default `v3.21`, `edge` for rolling), | |
| 43 | +`-a/--arch`, `-m/--mirror`, `--repo main,community`, `-l/--lib`, `--cc`, | |
| 44 | +`--no-deps`, `--refresh`, `-q/--quiet`. | |
| 45 | + | |
| 46 | +The sysroot defaults to `$XDG_DATA_HOME/muslkit/sysroot` and honors | |
| 47 | +`MUSLKIT_ROOT`. Downloads are cached under `$XDG_CACHE_HOME/muslkit`, so a | |
| 48 | +second `add` is offline and the index is re-fetched only once a day. | |
| 49 | + | |
| 50 | +## A full static build, start to finish | |
| 51 | + | |
| 52 | +```bash | |
| 53 | +muslkit add openssl-libs-static zlib-static | |
| 54 | +muslkit zigcc # writes ./zigcc (needs zig on PATH) | |
| 55 | +nim c -d:release -d:ssl \ | |
| 56 | + --cc:clang --clang.exe:./zigcc --clang.linkerexe:./zigcc \ | |
| 57 | + $(muslkit nimflags) --passL:-s \ | |
| 58 | + -o:app src/app.nim | |
| 59 | +``` | |
| 60 | + | |
| 61 | +`muslkit nimflags` emits more than include and library paths, because two | |
| 62 | +things bite every static Nim build with TLS: | |
| 63 | + | |
| 64 | +- **`-d:ssl` makes Nim `dlopen` libssl at runtime.** A static binary cannot, | |
| 65 | + and dies at startup with `could not load: libcrypto.so(...)` — even on code | |
| 66 | + paths that never touch the network. The fix is `--dynlibOverride:ssl | |
| 67 | + --dynlibOverride:crypto` plus the archives on the link line. | |
| 68 | +- **OpenSSL 3 removed `SSL_get_peer_certificate`,** which Nim's wrapper still | |
| 69 | + references, so the link fails on one undefined symbol. The fix is | |
| 70 | + `-DSSL_get_peer_certificate=SSL_get1_peer_certificate`. | |
| 71 | + | |
| 72 | +Both are emitted automatically when `libssl.a` and `libcrypto.a` are present in | |
| 73 | +the sysroot. A static binary carries no CA trust store, so set `SSL_CERT_FILE` | |
| 74 | +to a bundle on the host that runs it. | |
| 75 | + | |
| 76 | +Cross-compiling is the same command with `--arch`; `muslkit add -a aarch64 | |
| 77 | +openssl-libs-static` and `muslkit zigcc -a aarch64` line up. | |
| 78 | + | |
| 79 | +## Trust | |
| 80 | + | |
| 81 | +Packages come over HTTPS from the mirror and are unpacked as-is. muslkit checks | |
| 82 | +the size recorded in the index but does **not** verify Alpine's RSA signatures — | |
| 83 | +apk's checksum field covers the package's control segment, not the file, so a | |
| 84 | +real check means implementing apk's signature format. Treat a sysroot as build | |
| 85 | +input, not as a trust root. If that matters for your use, pin a mirror you run. | |
| 86 | + | |
| 87 | +## Install | |
| 88 | + | |
| 89 | +```bash | |
| 90 | +nimble install | |
| 91 | +``` | |
| 92 | + | |
| 93 | +or build in place with `nim c -d:ssl -o:muslkit src/muslkit.nim`. Requires | |
| 94 | +`tar` on PATH (an `.apk` is concatenated gzip streams, which GNU tar reads) and, | |
| 95 | +for the `zigcc` helper, `zig`. | |
| 96 | + | |
| 97 | +## Library | |
| 98 | + | |
| 99 | +```nim | |
| 100 | +import muslkit | |
| 101 | + | |
| 102 | +let remote = initRemote(branch = "edge") | |
| 103 | +let idx = remote.fetchIndex() | |
| 104 | +for pkg in idx.resolve(["openssl-libs-static"]): | |
| 105 | + echo pkg.name, " ", pkg.version, " ", remote.fetchPackage(pkg) | |
| 106 | +``` | |
| 107 | + | |
| 108 | +Modules: `muslkit/index` (APKINDEX parsing, provides and dependency | |
| 109 | +resolution), `muslkit/repo` (mirror, cache, download, unpack), | |
| 110 | +`muslkit/sysroot` (manifest, static-lib discovery), `muslkit/flags` (nim/cc/ | |
| 111 | +pkg-config flag emission). | |
| 112 | + | |
| 113 | +## Tests | |
| 114 | + | |
| 115 | +```bash | |
| 116 | +nimble test | |
| 117 | +``` | |
| 118 | + | |
| 119 | +The suite is offline — index parsing, resolution through `so:`/`pkg:` provides, | |
| 120 | +manifest round-trips and flag emission all run against fixtures. | |
| new file mode 100644 | |||
| @@ -0,0 +1,120 @@ | |||
| 1 | +# muslkit | ||
| 2 | + | ||
| 3 | +musl-linked static libraries from Alpine, without Alpine. | ||
| 4 | + | ||
| 5 | +Alpine builds everything against musl and ships `*-static` packages for most of | ||
| 6 | +it. An `.apk` is just a tarball, and an `APKINDEX` is just a text file — so you | ||
| 7 | +don't need apk, a chroot, a container or a distro to get `libcrypto.a` built | ||
| 8 | +for musl. muslkit fetches the index, resolves a dependency closure, downloads | ||
| 9 | +the packages and unpacks them into a sysroot directory you own. | ||
| 10 | + | ||
| 11 | +Nothing is installed system-wide. Nothing needs root. | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +muslkit add openssl-libs-static | ||
| 15 | +nim c -d:ssl $(muslkit nimflags) --cc:clang --clang.exe:./zigcc app.nim | ||
| 16 | +``` | ||
| 17 | + | ||
| 18 | +## Why | ||
| 19 | + | ||
| 20 | +Getting a fully static Nim (or C, or Zig) binary with TLS on a glibc distro | ||
| 21 | +means finding musl-built archives, and the usual answers are "install Alpine", | ||
| 22 | +"run Docker" or "use Nix". Alpine's mirrors already serve exactly the files — | ||
| 23 | +this just takes them. | ||
| 24 | + | ||
| 25 | +## Usage | ||
| 26 | + | ||
| 27 | +``` | ||
| 28 | +muslkit add <pkg>... Download packages and unpack into the sysroot | ||
| 29 | +muslkit list Show what the sysroot holds | ||
| 30 | +muslkit libs Show the static libraries in the sysroot | ||
| 31 | +muslkit search <text> Search the index (names and descriptions) | ||
| 32 | +muslkit show <pkg> Index record for one package | ||
| 33 | +muslkit nimflags [-l lib] Print nim flags for a static build | ||
| 34 | +muslkit ccflags [-l lib] Print cc/clang flags | ||
| 35 | +muslkit nimcfg [-o file] Write a nim.cfg fragment | ||
| 36 | +muslkit zigcc [-o file] Write a `zig cc -target …-musl` wrapper script | ||
| 37 | +muslkit env Shell exports (PKG_CONFIG_*, MUSLKIT_ROOT) | ||
| 38 | +muslkit path Print the sysroot path | ||
| 39 | +muslkit clean Remove the sysroot (cache is kept) | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +Options: `-r/--root`, `-b/--branch` (default `v3.21`, `edge` for rolling), | ||
| 43 | +`-a/--arch`, `-m/--mirror`, `--repo main,community`, `-l/--lib`, `--cc`, | ||
| 44 | +`--no-deps`, `--refresh`, `-q/--quiet`. | ||
| 45 | + | ||
| 46 | +The sysroot defaults to `$XDG_DATA_HOME/muslkit/sysroot` and honors | ||
| 47 | +`MUSLKIT_ROOT`. Downloads are cached under `$XDG_CACHE_HOME/muslkit`, so a | ||
| 48 | +second `add` is offline and the index is re-fetched only once a day. | ||
| 49 | + | ||
| 50 | +## A full static build, start to finish | ||
| 51 | + | ||
| 52 | +```bash | ||
| 53 | +muslkit add openssl-libs-static zlib-static | ||
| 54 | +muslkit zigcc # writes ./zigcc (needs zig on PATH) | ||
| 55 | +nim c -d:release -d:ssl \ | ||
| 56 | + --cc:clang --clang.exe:./zigcc --clang.linkerexe:./zigcc \ | ||
| 57 | + $(muslkit nimflags) --passL:-s \ | ||
| 58 | + -o:app src/app.nim | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +`muslkit nimflags` emits more than include and library paths, because two | ||
| 62 | +things bite every static Nim build with TLS: | ||
| 63 | + | ||
| 64 | +- **`-d:ssl` makes Nim `dlopen` libssl at runtime.** A static binary cannot, | ||
| 65 | + and dies at startup with `could not load: libcrypto.so(...)` — even on code | ||
| 66 | + paths that never touch the network. The fix is `--dynlibOverride:ssl | ||
| 67 | + --dynlibOverride:crypto` plus the archives on the link line. | ||
| 68 | +- **OpenSSL 3 removed `SSL_get_peer_certificate`,** which Nim's wrapper still | ||
| 69 | + references, so the link fails on one undefined symbol. The fix is | ||
| 70 | + `-DSSL_get_peer_certificate=SSL_get1_peer_certificate`. | ||
| 71 | + | ||
| 72 | +Both are emitted automatically when `libssl.a` and `libcrypto.a` are present in | ||
| 73 | +the sysroot. A static binary carries no CA trust store, so set `SSL_CERT_FILE` | ||
| 74 | +to a bundle on the host that runs it. | ||
| 75 | + | ||
| 76 | +Cross-compiling is the same command with `--arch`; `muslkit add -a aarch64 | ||
| 77 | +openssl-libs-static` and `muslkit zigcc -a aarch64` line up. | ||
| 78 | + | ||
| 79 | +## Trust | ||
| 80 | + | ||
| 81 | +Packages come over HTTPS from the mirror and are unpacked as-is. muslkit checks | ||
| 82 | +the size recorded in the index but does **not** verify Alpine's RSA signatures — | ||
| 83 | +apk's checksum field covers the package's control segment, not the file, so a | ||
| 84 | +real check means implementing apk's signature format. Treat a sysroot as build | ||
| 85 | +input, not as a trust root. If that matters for your use, pin a mirror you run. | ||
| 86 | + | ||
| 87 | +## Install | ||
| 88 | + | ||
| 89 | +```bash | ||
| 90 | +nimble install | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +or build in place with `nim c -d:ssl -o:muslkit src/muslkit.nim`. Requires | ||
| 94 | +`tar` on PATH (an `.apk` is concatenated gzip streams, which GNU tar reads) and, | ||
| 95 | +for the `zigcc` helper, `zig`. | ||
| 96 | + | ||
| 97 | +## Library | ||
| 98 | + | ||
| 99 | +```nim | ||
| 100 | +import muslkit | ||
| 101 | + | ||
| 102 | +let remote = initRemote(branch = "edge") | ||
| 103 | +let idx = remote.fetchIndex() | ||
| 104 | +for pkg in idx.resolve(["openssl-libs-static"]): | ||
| 105 | + echo pkg.name, " ", pkg.version, " ", remote.fetchPackage(pkg) | ||
| 106 | +``` | ||
| 107 | + | ||
| 108 | +Modules: `muslkit/index` (APKINDEX parsing, provides and dependency | ||
| 109 | +resolution), `muslkit/repo` (mirror, cache, download, unpack), | ||
| 110 | +`muslkit/sysroot` (manifest, static-lib discovery), `muslkit/flags` (nim/cc/ | ||
| 111 | +pkg-config flag emission). | ||
| 112 | + | ||
| 113 | +## Tests | ||
| 114 | + | ||
| 115 | +```bash | ||
| 116 | +nimble test | ||
| 117 | +``` | ||
| 118 | + | ||
| 119 | +The suite is offline — index parsing, resolution through `so:`/`pkg:` provides, | ||
| 120 | +manifest round-trips and flag emission all run against fixtures. | ||
added
config.nims +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +# The mirror is HTTPS-only, so the stdlib client needs TLS. | |
| 2 | +switch("define", "ssl") | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +# The mirror is HTTPS-only, so the stdlib client needs TLS. | ||
| 2 | +switch("define", "ssl") | ||
added
muslkit.nimble +12 -0 | new file mode 100644 | ||
| @@ -0,0 +1,12 @@ | ||
| 1 | +version = "0.1.0" | |
| 2 | +author = "nandi" | |
| 3 | +description = "musl-linked static libraries from Alpine, without Alpine" | |
| 4 | +license = "MIT" | |
| 5 | +srcDir = "src" | |
| 6 | +bin = @["muslkit"] | |
| 7 | +installExt = @["nim"] | |
| 8 | + | |
| 9 | +requires "nim >= 2.0.0" | |
| 10 | + | |
| 11 | +task test, "Run the test suite": | |
| 12 | + exec "nim c -d:ssl --hints:off -r tests/test_muslkit.nim" | |
| new file mode 100644 | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | +version = "0.1.0" | ||
| 2 | +author = "nandi" | ||
| 3 | +description = "musl-linked static libraries from Alpine, without Alpine" | ||
| 4 | +license = "MIT" | ||
| 5 | +srcDir = "src" | ||
| 6 | +bin = @["muslkit"] | ||
| 7 | +installExt = @["nim"] | ||
| 8 | + | ||
| 9 | +requires "nim >= 2.0.0" | ||
| 10 | + | ||
| 11 | +task test, "Run the test suite": | ||
| 12 | + exec "nim c -d:ssl --hints:off -r tests/test_muslkit.nim" | ||
added
src/muslkit.nim +213 -0 | new file mode 100644 | ||
| @@ -0,0 +1,213 @@ | ||
| 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 | |
| new file mode 100644 | |||
| @@ -0,0 +1,213 @@ | |||
| 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 | ||
added
src/muslkit/flags.nim +78 -0 | new file mode 100644 | ||
| @@ -0,0 +1,78 @@ | ||
| 1 | +## Turning a sysroot into compiler flags. | |
| 2 | +## | |
| 3 | +## Nim's OpenSSL binding is the one place a static build needs more than paths: | |
| 4 | +## `-d:ssl` makes Nim dlopen libssl at runtime, which a static binary cannot do, | |
| 5 | +## and OpenSSL 3 dropped a symbol Nim still references. Both fixes are emitted | |
| 6 | +## here rather than left as folklore in someone's shell history. | |
| 7 | + | |
| 8 | +import std/[os, strutils] | |
| 9 | +import ./sysroot | |
| 10 | + | |
| 11 | +type Toolchain* = enum | |
| 12 | + tcNim = "nim", tcCc = "cc", tcPkgConfig = "pkg-config" | |
| 13 | + | |
| 14 | +proc includeDir*(root: string): string = root / "usr" / "include" | |
| 15 | +proc libDir*(root: string): string = root / "usr" / "lib" | |
| 16 | + | |
| 17 | +proc hasLib(root, name: string): bool = | |
| 18 | + fileExists(libDir(root) / ("lib" & name & ".a")) | |
| 19 | + | |
| 20 | +proc nimFlags*(root: string, libs: openArray[string] = [], | |
| 21 | + opensslCompat = true): seq[string] = | |
| 22 | + result = @["--passC:-I" & includeDir(root), | |
| 23 | + "--passL:-L" & libDir(root), | |
| 24 | + "--passL:-static"] | |
| 25 | + if hasLib(root, "ssl") and hasLib(root, "crypto"): | |
| 26 | + # Stop Nim dlopen'ing OpenSSL, and link the archives instead. | |
| 27 | + result.add "--dynlibOverride:ssl" | |
| 28 | + result.add "--dynlibOverride:crypto" | |
| 29 | + result.add "--passL:" & libDir(root) / "libssl.a" | |
| 30 | + result.add "--passL:" & libDir(root) / "libcrypto.a" | |
| 31 | + if opensslCompat: | |
| 32 | + # Removed in OpenSSL 3.0; Nim's wrapper still names it. | |
| 33 | + result.add "--passC:-DSSL_get_peer_certificate=SSL_get1_peer_certificate" | |
| 34 | + for lib in libs: | |
| 35 | + result.add "--passL:-l" & lib | |
| 36 | + | |
| 37 | +proc ccFlags*(root: string, libs: openArray[string] = []): seq[string] = | |
| 38 | + result = @["-I" & includeDir(root), "-L" & libDir(root), "-static"] | |
| 39 | + for lib in libs: | |
| 40 | + result.add "-l" & lib | |
| 41 | + | |
| 42 | +proc pkgConfigEnv*(root: string): seq[string] = | |
| 43 | + @["PKG_CONFIG_SYSROOT_DIR=" & root, | |
| 44 | + "PKG_CONFIG_LIBDIR=" & libDir(root) / "pkgconfig"] | |
| 45 | + | |
| 46 | +proc nimCfg*(root, cc: string, libs: openArray[string] = []): string = | |
| 47 | + ## A nim.cfg fragment: cross-compiler plus every flag from `nimFlags`. | |
| 48 | + result = "# Generated by muslkit — static musl build against " & root & | |
| 49 | + "\n# Regenerate with: muslkit nimcfg --root " & root & "\n" | |
| 50 | + if cc.len > 0: | |
| 51 | + result.add "--cc:clang\n" | |
| 52 | + result.add "--clang.exe:\"" & cc & "\"\n" | |
| 53 | + result.add "--clang.linkerexe:\"" & cc & "\"\n" | |
| 54 | + for f in nimFlags(root, libs): | |
| 55 | + result.add f & "\n" | |
| 56 | + | |
| 57 | +const zigccTemplate* = """#!/bin/sh | |
| 58 | +# Generated by muslkit: zig as a musl cross-compiler. | |
| 59 | +exec zig cc -target @TARGET@ "$@" | |
| 60 | +""" | |
| 61 | + | |
| 62 | +proc zigccScript*(target: string): string = | |
| 63 | + zigccTemplate.replace("@TARGET@", target) | |
| 64 | + | |
| 65 | +proc writeZigCc*(path, target: string) = | |
| 66 | + writeFile(path, zigccScript(target)) | |
| 67 | + setFilePermissions(path, {fpUserRead, fpUserWrite, fpUserExec, | |
| 68 | + fpGroupRead, fpGroupExec, | |
| 69 | + fpOthersRead, fpOthersExec}) | |
| 70 | + | |
| 71 | +proc describe*(root: string): string = | |
| 72 | + let libs = staticLibs(root) | |
| 73 | + if libs.len == 0: | |
| 74 | + return "no static libraries in " & root | |
| 75 | + result = $libs.len & " static libraries in " & libDir(root) & ":\n" | |
| 76 | + for l in libs: | |
| 77 | + result.add " " & l.extractFilename & " (" & | |
| 78 | + $(getFileSize(l) div 1024) & " KiB)\n" | |
| new file mode 100644 | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | +## Turning a sysroot into compiler flags. | ||
| 2 | +## | ||
| 3 | +## Nim's OpenSSL binding is the one place a static build needs more than paths: | ||
| 4 | +## `-d:ssl` makes Nim dlopen libssl at runtime, which a static binary cannot do, | ||
| 5 | +## and OpenSSL 3 dropped a symbol Nim still references. Both fixes are emitted | ||
| 6 | +## here rather than left as folklore in someone's shell history. | ||
| 7 | + | ||
| 8 | +import std/[os, strutils] | ||
| 9 | +import ./sysroot | ||
| 10 | + | ||
| 11 | +type Toolchain* = enum | ||
| 12 | + tcNim = "nim", tcCc = "cc", tcPkgConfig = "pkg-config" | ||
| 13 | + | ||
| 14 | +proc includeDir*(root: string): string = root / "usr" / "include" | ||
| 15 | +proc libDir*(root: string): string = root / "usr" / "lib" | ||
| 16 | + | ||
| 17 | +proc hasLib(root, name: string): bool = | ||
| 18 | + fileExists(libDir(root) / ("lib" & name & ".a")) | ||
| 19 | + | ||
| 20 | +proc nimFlags*(root: string, libs: openArray[string] = [], | ||
| 21 | + opensslCompat = true): seq[string] = | ||
| 22 | + result = @["--passC:-I" & includeDir(root), | ||
| 23 | + "--passL:-L" & libDir(root), | ||
| 24 | + "--passL:-static"] | ||
| 25 | + if hasLib(root, "ssl") and hasLib(root, "crypto"): | ||
| 26 | + # Stop Nim dlopen'ing OpenSSL, and link the archives instead. | ||
| 27 | + result.add "--dynlibOverride:ssl" | ||
| 28 | + result.add "--dynlibOverride:crypto" | ||
| 29 | + result.add "--passL:" & libDir(root) / "libssl.a" | ||
| 30 | + result.add "--passL:" & libDir(root) / "libcrypto.a" | ||
| 31 | + if opensslCompat: | ||
| 32 | + # Removed in OpenSSL 3.0; Nim's wrapper still names it. | ||
| 33 | + result.add "--passC:-DSSL_get_peer_certificate=SSL_get1_peer_certificate" | ||
| 34 | + for lib in libs: | ||
| 35 | + result.add "--passL:-l" & lib | ||
| 36 | + | ||
| 37 | +proc ccFlags*(root: string, libs: openArray[string] = []): seq[string] = | ||
| 38 | + result = @["-I" & includeDir(root), "-L" & libDir(root), "-static"] | ||
| 39 | + for lib in libs: | ||
| 40 | + result.add "-l" & lib | ||
| 41 | + | ||
| 42 | +proc pkgConfigEnv*(root: string): seq[string] = | ||
| 43 | + @["PKG_CONFIG_SYSROOT_DIR=" & root, | ||
| 44 | + "PKG_CONFIG_LIBDIR=" & libDir(root) / "pkgconfig"] | ||
| 45 | + | ||
| 46 | +proc nimCfg*(root, cc: string, libs: openArray[string] = []): string = | ||
| 47 | + ## A nim.cfg fragment: cross-compiler plus every flag from `nimFlags`. | ||
| 48 | + result = "# Generated by muslkit — static musl build against " & root & | ||
| 49 | + "\n# Regenerate with: muslkit nimcfg --root " & root & "\n" | ||
| 50 | + if cc.len > 0: | ||
| 51 | + result.add "--cc:clang\n" | ||
| 52 | + result.add "--clang.exe:\"" & cc & "\"\n" | ||
| 53 | + result.add "--clang.linkerexe:\"" & cc & "\"\n" | ||
| 54 | + for f in nimFlags(root, libs): | ||
| 55 | + result.add f & "\n" | ||
| 56 | + | ||
| 57 | +const zigccTemplate* = """#!/bin/sh | ||
| 58 | +# Generated by muslkit: zig as a musl cross-compiler. | ||
| 59 | +exec zig cc -target @TARGET@ "$@" | ||
| 60 | +""" | ||
| 61 | + | ||
| 62 | +proc zigccScript*(target: string): string = | ||
| 63 | + zigccTemplate.replace("@TARGET@", target) | ||
| 64 | + | ||
| 65 | +proc writeZigCc*(path, target: string) = | ||
| 66 | + writeFile(path, zigccScript(target)) | ||
| 67 | + setFilePermissions(path, {fpUserRead, fpUserWrite, fpUserExec, | ||
| 68 | + fpGroupRead, fpGroupExec, | ||
| 69 | + fpOthersRead, fpOthersExec}) | ||
| 70 | + | ||
| 71 | +proc describe*(root: string): string = | ||
| 72 | + let libs = staticLibs(root) | ||
| 73 | + if libs.len == 0: | ||
| 74 | + return "no static libraries in " & root | ||
| 75 | + result = $libs.len & " static libraries in " & libDir(root) & ":\n" | ||
| 76 | + for l in libs: | ||
| 77 | + result.add " " & l.extractFilename & " (" & | ||
| 78 | + $(getFileSize(l) div 1024) & " KiB)\n" | ||
added
src/muslkit/index.nim +113 -0 | new file mode 100644 | ||
| @@ -0,0 +1,113 @@ | ||
| 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 | |
| new file mode 100644 | |||
| @@ -0,0 +1,113 @@ | |||
| 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 | ||
added
src/muslkit/repo.nim +95 -0 | new file mode 100644 | ||
| @@ -0,0 +1,95 @@ | ||
| 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) | |
| new file mode 100644 | |||
| @@ -0,0 +1,95 @@ | |||
| 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) | ||
added
src/muslkit/sysroot.nim +48 -0 | new file mode 100644 | ||
| @@ -0,0 +1,48 @@ | ||
| 1 | +## The sysroot: a plain directory tree of unpacked Alpine packages, plus a | |
| 2 | +## manifest of what was installed so `list` and re-runs are cheap. | |
| 3 | + | |
| 4 | +import std/[algorithm, os, sequtils, strutils] | |
| 5 | +import ./index | |
| 6 | + | |
| 7 | +const manifestPath* = ".muslkit/installed.tsv" | |
| 8 | + | |
| 9 | +type Installed* = object | |
| 10 | + name*, version*, repo*: string | |
| 11 | + | |
| 12 | +proc manifestFile*(sysroot: string): string = sysroot / manifestPath | |
| 13 | + | |
| 14 | +proc readManifest*(sysroot: string): seq[Installed] = | |
| 15 | + let path = manifestFile(sysroot) | |
| 16 | + if not fileExists(path): return | |
| 17 | + for line in readFile(path).splitLines: | |
| 18 | + if line.len == 0 or line.startsWith("#"): continue | |
| 19 | + let f = line.split('\t') | |
| 20 | + if f.len >= 3: | |
| 21 | + result.add Installed(name: f[0], version: f[1], repo: f[2]) | |
| 22 | + | |
| 23 | +proc writeManifest*(sysroot: string, entries: seq[Installed]) = | |
| 24 | + createDir manifestFile(sysroot).parentDir | |
| 25 | + var lines = @["# name\tversion\trepo — written by muslkit"] | |
| 26 | + for e in entries.sortedByIt(it.name): | |
| 27 | + lines.add e.name & "\t" & e.version & "\t" & e.repo | |
| 28 | + writeFile(manifestFile(sysroot), lines.join("\n") & "\n") | |
| 29 | + | |
| 30 | +proc record*(sysroot: string, pkgs: openArray[Pkg]) = | |
| 31 | + ## Merge freshly installed packages into the manifest, newest version winning. | |
| 32 | + var entries = readManifest(sysroot) | |
| 33 | + for pkg in pkgs: | |
| 34 | + entries.keepItIf(it.name != pkg.name) | |
| 35 | + entries.add Installed(name: pkg.name, version: pkg.version, repo: pkg.repo) | |
| 36 | + writeManifest(sysroot, entries) | |
| 37 | + | |
| 38 | +proc installed*(sysroot, name: string): bool = | |
| 39 | + readManifest(sysroot).anyIt(it.name == name) | |
| 40 | + | |
| 41 | +proc staticLibs*(sysroot: string): seq[string] = | |
| 42 | + ## Every .a in the sysroot, as absolute paths. | |
| 43 | + for dir in ["usr/lib", "lib", "usr/lib64"]: | |
| 44 | + let d = sysroot / dir | |
| 45 | + if dirExists(d): | |
| 46 | + for path in walkFiles(d / "*.a"): | |
| 47 | + result.add path | |
| 48 | + result.sort() | |
| new file mode 100644 | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | +## The sysroot: a plain directory tree of unpacked Alpine packages, plus a | ||
| 2 | +## manifest of what was installed so `list` and re-runs are cheap. | ||
| 3 | + | ||
| 4 | +import std/[algorithm, os, sequtils, strutils] | ||
| 5 | +import ./index | ||
| 6 | + | ||
| 7 | +const manifestPath* = ".muslkit/installed.tsv" | ||
| 8 | + | ||
| 9 | +type Installed* = object | ||
| 10 | + name*, version*, repo*: string | ||
| 11 | + | ||
| 12 | +proc manifestFile*(sysroot: string): string = sysroot / manifestPath | ||
| 13 | + | ||
| 14 | +proc readManifest*(sysroot: string): seq[Installed] = | ||
| 15 | + let path = manifestFile(sysroot) | ||
| 16 | + if not fileExists(path): return | ||
| 17 | + for line in readFile(path).splitLines: | ||
| 18 | + if line.len == 0 or line.startsWith("#"): continue | ||
| 19 | + let f = line.split('\t') | ||
| 20 | + if f.len >= 3: | ||
| 21 | + result.add Installed(name: f[0], version: f[1], repo: f[2]) | ||
| 22 | + | ||
| 23 | +proc writeManifest*(sysroot: string, entries: seq[Installed]) = | ||
| 24 | + createDir manifestFile(sysroot).parentDir | ||
| 25 | + var lines = @["# name\tversion\trepo — written by muslkit"] | ||
| 26 | + for e in entries.sortedByIt(it.name): | ||
| 27 | + lines.add e.name & "\t" & e.version & "\t" & e.repo | ||
| 28 | + writeFile(manifestFile(sysroot), lines.join("\n") & "\n") | ||
| 29 | + | ||
| 30 | +proc record*(sysroot: string, pkgs: openArray[Pkg]) = | ||
| 31 | + ## Merge freshly installed packages into the manifest, newest version winning. | ||
| 32 | + var entries = readManifest(sysroot) | ||
| 33 | + for pkg in pkgs: | ||
| 34 | + entries.keepItIf(it.name != pkg.name) | ||
| 35 | + entries.add Installed(name: pkg.name, version: pkg.version, repo: pkg.repo) | ||
| 36 | + writeManifest(sysroot, entries) | ||
| 37 | + | ||
| 38 | +proc installed*(sysroot, name: string): bool = | ||
| 39 | + readManifest(sysroot).anyIt(it.name == name) | ||
| 40 | + | ||
| 41 | +proc staticLibs*(sysroot: string): seq[string] = | ||
| 42 | + ## Every .a in the sysroot, as absolute paths. | ||
| 43 | + for dir in ["usr/lib", "lib", "usr/lib64"]: | ||
| 44 | + let d = sysroot / dir | ||
| 45 | + if dirExists(d): | ||
| 46 | + for path in walkFiles(d / "*.a"): | ||
| 47 | + result.add path | ||
| 48 | + result.sort() | ||
added
tests/test_muslkit.nim +187 -0 | new file mode 100644 | ||
| @@ -0,0 +1,187 @@ | ||
| 1 | +import std/[os, strutils, tables, unittest] | |
| 2 | +import ../src/muslkit | |
| 3 | + | |
| 4 | +const sampleIndex = """ | |
| 5 | +C:Q1aaa= | |
| 6 | +P:openssl-libs-static | |
| 7 | +V:3.3.7-r1 | |
| 8 | +A:x86_64 | |
| 9 | +S:13040623 | |
| 10 | +T:Toolkit for Transport Layer Security (TLS) (static library) | |
| 11 | +L:Apache-2.0 | |
| 12 | +o:openssl | |
| 13 | + | |
| 14 | +C:Q1bbb= | |
| 15 | +P:openssl-dev | |
| 16 | +V:3.3.7-r1 | |
| 17 | +A:x86_64 | |
| 18 | +S:3000 | |
| 19 | +T:TLS toolkit (development files) | |
| 20 | +D:libcrypto3=3.3.7-r1 pkgconfig | |
| 21 | +p:openssl3-dev=3.3.7-r1 pc:libssl=3.3.7 | |
| 22 | + | |
| 23 | +C:Q1ccc= | |
| 24 | +P:libcrypto3 | |
| 25 | +V:3.3.7-r1 | |
| 26 | +A:x86_64 | |
| 27 | +S:2000 | |
| 28 | +T:crypto library | |
| 29 | +D:so:libc.musl-x86_64.so.1 | |
| 30 | +p:so:libcrypto.so.3=3 | |
| 31 | + | |
| 32 | +C:Q1ddd= | |
| 33 | +P:musl | |
| 34 | +V:1.2.5-r11 | |
| 35 | +A:x86_64 | |
| 36 | +S:400 | |
| 37 | +T:the musl c library | |
| 38 | +p:so:libc.musl-x86_64.so.1=1 | |
| 39 | + | |
| 40 | +C:Q1eee= | |
| 41 | +P:pkgconf | |
| 42 | +V:2.3.0-r0 | |
| 43 | +A:x86_64 | |
| 44 | +S:100 | |
| 45 | +T:pkg-config compatible | |
| 46 | +p:pkgconfig=2.3.0-r0 | |
| 47 | +""" | |
| 48 | + | |
| 49 | +suite "index parsing": | |
| 50 | + let idx = parseIndex(sampleIndex, "main") | |
| 51 | + | |
| 52 | + test "records and fields": | |
| 53 | + check idx.packages.len == 5 | |
| 54 | + let p = idx.packages["openssl-libs-static"] | |
| 55 | + check p.version == "3.3.7-r1" | |
| 56 | + check p.size == 13040623 | |
| 57 | + check p.repo == "main" | |
| 58 | + check p.description.endsWith("(static library)") | |
| 59 | + | |
| 60 | + test "provides are indexed": | |
| 61 | + check idx.find("pkgconfig") == "pkgconf" | |
| 62 | + check idx.find("so:libcrypto.so.3") == "libcrypto3" | |
| 63 | + check idx.find("openssl3-dev") == "openssl-dev" | |
| 64 | + check idx.find("nope") == "" | |
| 65 | + | |
| 66 | + test "version constraints are stripped": | |
| 67 | + check stripConstraint("musl=1.2.5-r11") == "musl" | |
| 68 | + check stripConstraint("so:libssl.so.3=3") == "so:libssl.so.3" | |
| 69 | + check stripConstraint("cmd>=2") == "cmd" | |
| 70 | + check stripConstraint("plain") == "plain" | |
| 71 | + | |
| 72 | + test "conflicts are recognized": | |
| 73 | + check isConflict("!busybox") | |
| 74 | + check not isConflict("busybox") | |
| 75 | + | |
| 76 | +suite "resolution": | |
| 77 | + let idx = parseIndex(sampleIndex, "main") | |
| 78 | + | |
| 79 | + test "dependencies come before dependents": | |
| 80 | + let order = idx.resolve(["openssl-dev"]) | |
| 81 | + let names = block: | |
| 82 | + var s: seq[string] | |
| 83 | + for p in order: s.add p.name | |
| 84 | + s | |
| 85 | + check names[^1] == "openssl-dev" | |
| 86 | + check "libcrypto3" in names | |
| 87 | + check "musl" in names # reached through so:libc.musl-x86_64.so.1 | |
| 88 | + check "pkgconf" in names # reached through the pkgconfig provide | |
| 89 | + check names.find("libcrypto3") < names.find("openssl-dev") | |
| 90 | + | |
| 91 | + test "a package is visited once": | |
| 92 | + let order = idx.resolve(["openssl-dev", "libcrypto3", "openssl-dev"]) | |
| 93 | + var seen: CountTable[string] | |
| 94 | + for p in order: seen.inc p.name | |
| 95 | + for name, n in seen: check n == 1 | |
| 96 | + | |
| 97 | + test "--no-deps stops at the request": | |
| 98 | + let order = idx.resolve(["openssl-dev"], withDeps = false) | |
| 99 | + check order.len == 1 | |
| 100 | + check order[0].name == "openssl-dev" | |
| 101 | + | |
| 102 | + test "an unsatisfiable request names its requester": | |
| 103 | + expect KeyError: | |
| 104 | + discard parseIndex("C:Q1\nP:lonely\nV:1\nD:ghost\n").resolve(["lonely"]) | |
| 105 | + | |
| 106 | + test "merge lets a later repo shadow an earlier one": | |
| 107 | + var a = parseIndex("C:Q1\nP:foo\nV:1\n", "main") | |
| 108 | + a.merge parseIndex("C:Q1\nP:foo\nV:2\n", "community") | |
| 109 | + check a.packages["foo"].version == "2" | |
| 110 | + check a.packages["foo"].repo == "community" | |
| 111 | + | |
| 112 | +suite "sysroot manifest": | |
| 113 | + let root = getTempDir() / "muslkit-test-root" | |
| 114 | + removeDir root | |
| 115 | + | |
| 116 | + test "record then read back": | |
| 117 | + record(root, [Pkg(name: "zlib-static", version: "1.3.2-r0", repo: "main")]) | |
| 118 | + record(root, [Pkg(name: "openssl-libs-static", version: "3.3.7-r1", repo: "main")]) | |
| 119 | + let entries = readManifest(root) | |
| 120 | + check entries.len == 2 | |
| 121 | + check installed(root, "zlib-static") | |
| 122 | + check not installed(root, "absent") | |
| 123 | + | |
| 124 | + test "re-recording replaces rather than duplicates": | |
| 125 | + record(root, [Pkg(name: "zlib-static", version: "1.3.3-r0", repo: "main")]) | |
| 126 | + let entries = readManifest(root) | |
| 127 | + check entries.len == 2 | |
| 128 | + for e in entries: | |
| 129 | + if e.name == "zlib-static": check e.version == "1.3.3-r0" | |
| 130 | + | |
| 131 | + test "static libs are found and sorted": | |
| 132 | + createDir root / "usr" / "lib" | |
| 133 | + for name in ["libz.a", "libcrypto.a"]: | |
| 134 | + writeFile(root / "usr" / "lib" / name, "") | |
| 135 | + let libs = staticLibs(root) | |
| 136 | + check libs.len == 2 | |
| 137 | + check libs[0].extractFilename == "libcrypto.a" | |
| 138 | + | |
| 139 | + removeDir root | |
| 140 | + | |
| 141 | +suite "flags": | |
| 142 | + let root = getTempDir() / "muslkit-test-flags" | |
| 143 | + removeDir root | |
| 144 | + createDir root / "usr" / "lib" | |
| 145 | + | |
| 146 | + test "plain sysroot": | |
| 147 | + let f = nimFlags(root).join(" ") | |
| 148 | + check ("--passC:-I" & root & "/usr/include") in f | |
| 149 | + check "--passL:-static" in f | |
| 150 | + check "dynlibOverride" notin f | |
| 151 | + | |
| 152 | + test "openssl archives switch on the static-TLS workarounds": | |
| 153 | + for name in ["libssl.a", "libcrypto.a"]: | |
| 154 | + writeFile(root / "usr" / "lib" / name, "") | |
| 155 | + let f = nimFlags(root, ["z"]).join(" ") | |
| 156 | + check "--dynlibOverride:ssl" in f | |
| 157 | + check "--dynlibOverride:crypto" in f | |
| 158 | + # OpenSSL 3 dropped the symbol Nim's wrapper still names | |
| 159 | + check "-DSSL_get_peer_certificate=SSL_get1_peer_certificate" in f | |
| 160 | + check "--passL:-lz" in f | |
| 161 | + | |
| 162 | + test "cc flags and pkg-config env": | |
| 163 | + check ccFlags(root, ["ssl"]) == @["-I" & root & "/usr/include", | |
| 164 | + "-L" & root & "/usr/lib", | |
| 165 | + "-static", "-lssl"] | |
| 166 | + check pkgConfigEnv(root)[0] == "PKG_CONFIG_SYSROOT_DIR=" & root | |
| 167 | + | |
| 168 | + test "nimcfg carries the compiler and the flags": | |
| 169 | + let cfg = nimCfg(root, "/tmp/zigcc") | |
| 170 | + check "--cc:clang" in cfg | |
| 171 | + check "--clang.exe:\"/tmp/zigcc\"" in cfg | |
| 172 | + check "--passL:-static" in cfg | |
| 173 | + | |
| 174 | + test "zigcc script targets the right triple": | |
| 175 | + let s = zigccScript("aarch64-linux-musl") | |
| 176 | + check "zig cc -target aarch64-linux-musl" in s | |
| 177 | + check "\"$@\"" in s | |
| 178 | + | |
| 179 | + removeDir root | |
| 180 | + | |
| 181 | +suite "remote": | |
| 182 | + test "urls are built from mirror, branch, repo and arch": | |
| 183 | + let r = initRemote(branch = "edge", arch = "aarch64") | |
| 184 | + check r.repoUrl("community") == | |
| 185 | + "https://dl-cdn.alpinelinux.org/alpine/edge/community/aarch64" | |
| 186 | + check apkFile(Pkg(name: "zlib-static", version: "1.3.2-r0")) == | |
| 187 | + "zlib-static-1.3.2-r0.apk" | |
| new file mode 100644 | |||
| @@ -0,0 +1,187 @@ | |||
| 1 | +import std/[os, strutils, tables, unittest] | ||
| 2 | +import ../src/muslkit | ||
| 3 | + | ||
| 4 | +const sampleIndex = """ | ||
| 5 | +C:Q1aaa= | ||
| 6 | +P:openssl-libs-static | ||
| 7 | +V:3.3.7-r1 | ||
| 8 | +A:x86_64 | ||
| 9 | +S:13040623 | ||
| 10 | +T:Toolkit for Transport Layer Security (TLS) (static library) | ||
| 11 | +L:Apache-2.0 | ||
| 12 | +o:openssl | ||
| 13 | + | ||
| 14 | +C:Q1bbb= | ||
| 15 | +P:openssl-dev | ||
| 16 | +V:3.3.7-r1 | ||
| 17 | +A:x86_64 | ||
| 18 | +S:3000 | ||
| 19 | +T:TLS toolkit (development files) | ||
| 20 | +D:libcrypto3=3.3.7-r1 pkgconfig | ||
| 21 | +p:openssl3-dev=3.3.7-r1 pc:libssl=3.3.7 | ||
| 22 | + | ||
| 23 | +C:Q1ccc= | ||
| 24 | +P:libcrypto3 | ||
| 25 | +V:3.3.7-r1 | ||
| 26 | +A:x86_64 | ||
| 27 | +S:2000 | ||
| 28 | +T:crypto library | ||
| 29 | +D:so:libc.musl-x86_64.so.1 | ||
| 30 | +p:so:libcrypto.so.3=3 | ||
| 31 | + | ||
| 32 | +C:Q1ddd= | ||
| 33 | +P:musl | ||
| 34 | +V:1.2.5-r11 | ||
| 35 | +A:x86_64 | ||
| 36 | +S:400 | ||
| 37 | +T:the musl c library | ||
| 38 | +p:so:libc.musl-x86_64.so.1=1 | ||
| 39 | + | ||
| 40 | +C:Q1eee= | ||
| 41 | +P:pkgconf | ||
| 42 | +V:2.3.0-r0 | ||
| 43 | +A:x86_64 | ||
| 44 | +S:100 | ||
| 45 | +T:pkg-config compatible | ||
| 46 | +p:pkgconfig=2.3.0-r0 | ||
| 47 | +""" | ||
| 48 | + | ||
| 49 | +suite "index parsing": | ||
| 50 | + let idx = parseIndex(sampleIndex, "main") | ||
| 51 | + | ||
| 52 | + test "records and fields": | ||
| 53 | + check idx.packages.len == 5 | ||
| 54 | + let p = idx.packages["openssl-libs-static"] | ||
| 55 | + check p.version == "3.3.7-r1" | ||
| 56 | + check p.size == 13040623 | ||
| 57 | + check p.repo == "main" | ||
| 58 | + check p.description.endsWith("(static library)") | ||
| 59 | + | ||
| 60 | + test "provides are indexed": | ||
| 61 | + check idx.find("pkgconfig") == "pkgconf" | ||
| 62 | + check idx.find("so:libcrypto.so.3") == "libcrypto3" | ||
| 63 | + check idx.find("openssl3-dev") == "openssl-dev" | ||
| 64 | + check idx.find("nope") == "" | ||
| 65 | + | ||
| 66 | + test "version constraints are stripped": | ||
| 67 | + check stripConstraint("musl=1.2.5-r11") == "musl" | ||
| 68 | + check stripConstraint("so:libssl.so.3=3") == "so:libssl.so.3" | ||
| 69 | + check stripConstraint("cmd>=2") == "cmd" | ||
| 70 | + check stripConstraint("plain") == "plain" | ||
| 71 | + | ||
| 72 | + test "conflicts are recognized": | ||
| 73 | + check isConflict("!busybox") | ||
| 74 | + check not isConflict("busybox") | ||
| 75 | + | ||
| 76 | +suite "resolution": | ||
| 77 | + let idx = parseIndex(sampleIndex, "main") | ||
| 78 | + | ||
| 79 | + test "dependencies come before dependents": | ||
| 80 | + let order = idx.resolve(["openssl-dev"]) | ||
| 81 | + let names = block: | ||
| 82 | + var s: seq[string] | ||
| 83 | + for p in order: s.add p.name | ||
| 84 | + s | ||
| 85 | + check names[^1] == "openssl-dev" | ||
| 86 | + check "libcrypto3" in names | ||
| 87 | + check "musl" in names # reached through so:libc.musl-x86_64.so.1 | ||
| 88 | + check "pkgconf" in names # reached through the pkgconfig provide | ||
| 89 | + check names.find("libcrypto3") < names.find("openssl-dev") | ||
| 90 | + | ||
| 91 | + test "a package is visited once": | ||
| 92 | + let order = idx.resolve(["openssl-dev", "libcrypto3", "openssl-dev"]) | ||
| 93 | + var seen: CountTable[string] | ||
| 94 | + for p in order: seen.inc p.name | ||
| 95 | + for name, n in seen: check n == 1 | ||
| 96 | + | ||
| 97 | + test "--no-deps stops at the request": | ||
| 98 | + let order = idx.resolve(["openssl-dev"], withDeps = false) | ||
| 99 | + check order.len == 1 | ||
| 100 | + check order[0].name == "openssl-dev" | ||
| 101 | + | ||
| 102 | + test "an unsatisfiable request names its requester": | ||
| 103 | + expect KeyError: | ||
| 104 | + discard parseIndex("C:Q1\nP:lonely\nV:1\nD:ghost\n").resolve(["lonely"]) | ||
| 105 | + | ||
| 106 | + test "merge lets a later repo shadow an earlier one": | ||
| 107 | + var a = parseIndex("C:Q1\nP:foo\nV:1\n", "main") | ||
| 108 | + a.merge parseIndex("C:Q1\nP:foo\nV:2\n", "community") | ||
| 109 | + check a.packages["foo"].version == "2" | ||
| 110 | + check a.packages["foo"].repo == "community" | ||
| 111 | + | ||
| 112 | +suite "sysroot manifest": | ||
| 113 | + let root = getTempDir() / "muslkit-test-root" | ||
| 114 | + removeDir root | ||
| 115 | + | ||
| 116 | + test "record then read back": | ||
| 117 | + record(root, [Pkg(name: "zlib-static", version: "1.3.2-r0", repo: "main")]) | ||
| 118 | + record(root, [Pkg(name: "openssl-libs-static", version: "3.3.7-r1", repo: "main")]) | ||
| 119 | + let entries = readManifest(root) | ||
| 120 | + check entries.len == 2 | ||
| 121 | + check installed(root, "zlib-static") | ||
| 122 | + check not installed(root, "absent") | ||
| 123 | + | ||
| 124 | + test "re-recording replaces rather than duplicates": | ||
| 125 | + record(root, [Pkg(name: "zlib-static", version: "1.3.3-r0", repo: "main")]) | ||
| 126 | + let entries = readManifest(root) | ||
| 127 | + check entries.len == 2 | ||
| 128 | + for e in entries: | ||
| 129 | + if e.name == "zlib-static": check e.version == "1.3.3-r0" | ||
| 130 | + | ||
| 131 | + test "static libs are found and sorted": | ||
| 132 | + createDir root / "usr" / "lib" | ||
| 133 | + for name in ["libz.a", "libcrypto.a"]: | ||
| 134 | + writeFile(root / "usr" / "lib" / name, "") | ||
| 135 | + let libs = staticLibs(root) | ||
| 136 | + check libs.len == 2 | ||
| 137 | + check libs[0].extractFilename == "libcrypto.a" | ||
| 138 | + | ||
| 139 | + removeDir root | ||
| 140 | + | ||
| 141 | +suite "flags": | ||
| 142 | + let root = getTempDir() / "muslkit-test-flags" | ||
| 143 | + removeDir root | ||
| 144 | + createDir root / "usr" / "lib" | ||
| 145 | + | ||
| 146 | + test "plain sysroot": | ||
| 147 | + let f = nimFlags(root).join(" ") | ||
| 148 | + check ("--passC:-I" & root & "/usr/include") in f | ||
| 149 | + check "--passL:-static" in f | ||
| 150 | + check "dynlibOverride" notin f | ||
| 151 | + | ||
| 152 | + test "openssl archives switch on the static-TLS workarounds": | ||
| 153 | + for name in ["libssl.a", "libcrypto.a"]: | ||
| 154 | + writeFile(root / "usr" / "lib" / name, "") | ||
| 155 | + let f = nimFlags(root, ["z"]).join(" ") | ||
| 156 | + check "--dynlibOverride:ssl" in f | ||
| 157 | + check "--dynlibOverride:crypto" in f | ||
| 158 | + # OpenSSL 3 dropped the symbol Nim's wrapper still names | ||
| 159 | + check "-DSSL_get_peer_certificate=SSL_get1_peer_certificate" in f | ||
| 160 | + check "--passL:-lz" in f | ||
| 161 | + | ||
| 162 | + test "cc flags and pkg-config env": | ||
| 163 | + check ccFlags(root, ["ssl"]) == @["-I" & root & "/usr/include", | ||
| 164 | + "-L" & root & "/usr/lib", | ||
| 165 | + "-static", "-lssl"] | ||
| 166 | + check pkgConfigEnv(root)[0] == "PKG_CONFIG_SYSROOT_DIR=" & root | ||
| 167 | + | ||
| 168 | + test "nimcfg carries the compiler and the flags": | ||
| 169 | + let cfg = nimCfg(root, "/tmp/zigcc") | ||
| 170 | + check "--cc:clang" in cfg | ||
| 171 | + check "--clang.exe:\"/tmp/zigcc\"" in cfg | ||
| 172 | + check "--passL:-static" in cfg | ||
| 173 | + | ||
| 174 | + test "zigcc script targets the right triple": | ||
| 175 | + let s = zigccScript("aarch64-linux-musl") | ||
| 176 | + check "zig cc -target aarch64-linux-musl" in s | ||
| 177 | + check "\"$@\"" in s | ||
| 178 | + | ||
| 179 | + removeDir root | ||
| 180 | + | ||
| 181 | +suite "remote": | ||
| 182 | + test "urls are built from mirror, branch, repo and arch": | ||
| 183 | + let r = initRemote(branch = "edge", arch = "aarch64") | ||
| 184 | + check r.repoUrl("community") == | ||
| 185 | + "https://dl-cdn.alpinelinux.org/alpine/edge/community/aarch64" | ||
| 186 | + check apkFile(Pkg(name: "zlib-static", version: "1.3.2-r0")) == | ||
| 187 | + "zlib-static-1.3.2-r0.apk" | ||