Rename to nimstatic, and detect dependencies from the source
The headline command is now `nimstatic app.nim`: detect, fetch, build. Detection asks the compiler rather than reading import lines, which would be wrong both ways — a transitive import three modules deep still needs its library, and an import behind a dead `when` does not. A --compileOnly --genScript probe yields the nimcache the real build would have, and that says it twice: the build json's link command gives every -lfoo, and the generated C holds the dynlib candidate strings Nim would dlopen at runtime. The second set never reaches a link line and is exactly what breaks a static binary. Libraries musl already provides are skipped, the rest map to Alpine packages (--map teaches it new ones), and unmapped libraries are reported rather than silently dropped. An override is only emitted for a library whose archive is actually in the sysroot, since an override without one turns a runtime failure into a link failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53cd879 parent: 5feb050 modified
.gitignore +2 -2 | @@ -1,4 +1,4 @@ | ||
| 1 | -/muslkit | |
| 1 | +/nimstatic | |
| 2 | 2 | /zigcc |
| 3 | -tests/test_muslkit | |
| 3 | +tests/test_nimstatic | |
| 4 | 4 | nimcache/ |
| @@ -1,4 +1,4 @@ | |||
| 1 | -/muslkit | 1 | +/nimstatic |
| 2 | /zigcc | 2 | /zigcc |
| 3 | -tests/test_muslkit | 3 | +tests/test_nimstatic |
| 4 | nimcache/ | 4 | nimcache/ |
modified
README.md +130 -77 | @@ -1,114 +1,165 @@ | ||
| 1 | -# muslkit | |
| 1 | +# nimstatic | |
| 2 | 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. | |
| 3 | +Fully static Nim binaries, dependencies and all. | |
| 12 | 4 | |
| 13 | 5 | ```bash |
| 14 | -muslkit add openssl-libs-static | |
| 15 | -nim c -d:ssl $(muslkit nimflags) --cc:clang --clang.exe:./zigcc app.nim | |
| 6 | +nimstatic app.nim | |
| 16 | 7 | ``` |
| 17 | 8 | |
| 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 | |
| 9 | +That's the whole thing. nimstatic asks the Nim compiler what `app.nim` actually | |
| 10 | +needs, fetches musl-built static libraries for it from Alpine's mirrors, and | |
| 11 | +compiles against a sysroot it owns. No apk, no container, no root, no Nix. | |
| 26 | 12 | |
| 27 | 13 | ``` |
| 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) | |
| 14 | +$ nimstatic freeqsay.nim -- -d:ssl | |
| 15 | +probing freeqsay.nim … | |
| 16 | + ssl dlopen openssl-libs-static | |
| 17 | + crypto dlopen openssl-libs-static | |
| 18 | +downloading openssl-libs-static 3.3.7-r1 (12734 KiB) | |
| 19 | +building … | |
| 20 | +wrote freeqsay (20687 KiB, static) | |
| 40 | 21 | ``` |
| 41 | 22 | |
| 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`. | |
| 23 | +The result runs anywhere with a Linux kernel — `ldd` says *not a dynamic | |
| 24 | +executable* — and its HTTPS still works. | |
| 45 | 25 | |
| 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. | |
| 26 | +## How it knows what you need | |
| 49 | 27 | |
| 50 | -## A full static build, start to finish | |
| 28 | +Guessing from `import` lines would be wrong in both directions: a transitive | |
| 29 | +import three modules deep still needs its library, and an import behind a | |
| 30 | +`when` that never fires does not. So nimstatic asks the compiler instead. A | |
| 31 | +`--compileOnly --genScript` probe produces the nimcache your real build would | |
| 32 | +have, and that cache answers twice over: | |
| 51 | 33 | |
| 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 | |
| 34 | +- **`<project>.json`** carries the link command, so every `-lfoo` is explicit. | |
| 35 | +- **The generated C** contains the dynlib candidate strings Nim will `dlopen` | |
| 36 | + at runtime — `"libssl.so(.3|.1.1|…)"`. These never appear on a link line, and | |
| 37 | + they are exactly what breaks a static binary. | |
| 38 | + | |
| 39 | +Libraries musl already provides (`m`, `rt`, `dl`, `pthread`, …) are skipped. | |
| 40 | +Anything else is looked up in a table of Alpine packages; whatever isn't mapped | |
| 41 | +is reported rather than silently dropped: | |
| 42 | + | |
| 43 | +``` | |
| 44 | +$ nimstatic detect app.nim | |
| 45 | +library how alpine package | |
| 46 | +sqlite3 link sqlite-static | |
| 47 | +ssl dlopen openssl-libs-static | |
| 48 | +mystery dlopen (unmapped) | |
| 49 | + | |
| 50 | +unmapped: mystery | |
| 51 | +search for one with: nimstatic search mystery | |
| 59 | 52 | ``` |
| 60 | 53 | |
| 61 | -`muslkit nimflags` emits more than include and library paths, because two | |
| 62 | -things bite every static Nim build with TLS: | |
| 54 | +Then `--map mystery=mystery-static` teaches it, for that build. | |
| 55 | + | |
| 56 | +## The two things that silently break static Nim | |
| 57 | + | |
| 58 | +Both are handled automatically; they're documented here because they cost | |
| 59 | +everyone an afternoon at least once. | |
| 63 | 60 | |
| 64 | 61 | - **`-d:ssl` makes Nim `dlopen` libssl at runtime.** A static binary cannot, |
| 65 | 62 | 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. | |
| 63 | + paths that never touch the network. Fix: `--dynlibOverride:ssl | |
| 64 | + --dynlibOverride:crypto` plus the archives on the link line. nimstatic emits | |
| 65 | + an override only for libraries it actually has an archive for, since an | |
| 66 | + override without one turns a runtime failure into a link failure. | |
| 68 | 67 | - **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 | |
| 68 | + names, so the link dies on one undefined symbol. Fix: | |
| 70 | 69 | `-DSSL_get_peer_certificate=SSL_get1_peer_certificate`. |
| 71 | 70 | |
| 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. | |
| 71 | +A static binary also carries no CA trust store, so set `SSL_CERT_FILE` on the | |
| 72 | +host that runs it. | |
| 75 | 73 | |
| 76 | -Cross-compiling is the same command with `--arch`; `muslkit add -a aarch64 | |
| 77 | -openssl-libs-static` and `muslkit zigcc -a aarch64` line up. | |
| 74 | +## Usage | |
| 78 | 75 | |
| 79 | -## Trust | |
| 76 | +``` | |
| 77 | +nimstatic <file.nim> [-- <nim args>] Detect, fetch, build static | |
| 78 | +nimstatic detect <file.nim> Show what it needs, change nothing | |
| 79 | +nimstatic add <pkg>... Put packages in the sysroot by hand | |
| 80 | +nimstatic list Show what the sysroot holds | |
| 81 | +nimstatic libs Show the sysroot's static libraries | |
| 82 | +nimstatic search <text> Search Alpine's index | |
| 83 | +nimstatic show <pkg> Index record for one package | |
| 84 | +nimstatic nimflags [-l lib] Print nim flags for the sysroot | |
| 85 | +nimstatic ccflags [-l lib] Print cc/clang flags | |
| 86 | +nimstatic nimcfg [-o file] Write a nim.cfg fragment | |
| 87 | +nimstatic zigcc [-o file] Write a `zig cc -target …-musl` wrapper | |
| 88 | +nimstatic env Shell exports (PKG_CONFIG_*, NIMSTATIC_ROOT) | |
| 89 | +nimstatic path Print the sysroot path | |
| 90 | +nimstatic clean Remove the sysroot (cache is kept) | |
| 91 | +``` | |
| 80 | 92 | |
| 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. | |
| 93 | +Everything after `--` goes to the Nim compiler for **both** the probe and the | |
| 94 | +build, so conditional imports resolve the same way twice: | |
| 86 | 95 | |
| 87 | -## Install | |
| 96 | +```bash | |
| 97 | +nimstatic app.nim -- -d:ssl -d:danger | |
| 98 | +``` | |
| 99 | + | |
| 100 | +Build options: `-o/--output`, `-d/--debug` (skip `-d:release`), `-n/--dry-run` | |
| 101 | +(print the command instead of running it), `--map lib=pkg`, `--pkg name`, | |
| 102 | +`--cc path`, `--nim path`. | |
| 103 | + | |
| 104 | +Sysroot options: `-r/--root`, `-b/--branch` (default `v3.21`, `edge` for | |
| 105 | +rolling), `-a/--arch`, `-m/--mirror`, `--repo main,community`, `-l/--lib`, | |
| 106 | +`--no-deps`, `--refresh`, `-q/--quiet`. | |
| 107 | + | |
| 108 | +The sysroot defaults to `$XDG_DATA_HOME/nimstatic/sysroot` and honors | |
| 109 | +`NIMSTATIC_ROOT`. Downloads cache under `$XDG_CACHE_HOME/nimstatic`, so the | |
| 110 | +second build is offline and the index is re-fetched once a day. | |
| 111 | + | |
| 112 | +## Cross-compiling | |
| 113 | + | |
| 114 | +Same command, one flag — the sysroot, the packages and the zig target all | |
| 115 | +follow `--arch`: | |
| 88 | 116 | |
| 89 | 117 | ```bash |
| 90 | -nimble install | |
| 118 | +nimstatic app.nim --arch aarch64 -o app-arm64 | |
| 91 | 119 | ``` |
| 92 | 120 | |
| 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`. | |
| 121 | +## Requirements | |
| 122 | + | |
| 123 | +- **zig** on PATH — used as `zig cc -target x86_64-linux-musl`. Pass `--cc` to | |
| 124 | + use a musl cross-compiler you already have instead. | |
| 125 | +- **tar** — an `.apk` is concatenated gzip streams, which GNU tar reads. | |
| 96 | 126 | |
| 97 | -## Library | |
| 127 | +## Using it as a library | |
| 98 | 128 | |
| 99 | 129 | ```nim |
| 100 | -import muslkit | |
| 130 | +import nimstatic | |
| 131 | + | |
| 132 | +let d = detect("app.nim", ["-d:ssl"]) | |
| 133 | +echo packages(d) # @["openssl-libs-static"] | |
| 134 | +echo dynlibOverrides(d) # @["ssl", "crypto"] | |
| 101 | 135 | |
| 102 | 136 | let remote = initRemote(branch = "edge") |
| 103 | -let idx = remote.fetchIndex() | |
| 104 | -for pkg in idx.resolve(["openssl-libs-static"]): | |
| 137 | +for pkg in remote.fetchIndex().resolve(packages(d)): | |
| 105 | 138 | echo pkg.name, " ", pkg.version, " ", remote.fetchPackage(pkg) |
| 106 | 139 | ``` |
| 107 | 140 | |
| 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). | |
| 141 | +Modules: `nimstatic/detect` (probe, parse, map), `nimstatic/build` (the | |
| 142 | +one-command path), `nimstatic/index` (APKINDEX parsing, provides and dependency | |
| 143 | +resolution), `nimstatic/repo` (mirror, cache, download, unpack), | |
| 144 | +`nimstatic/sysroot` (manifest, static-lib discovery), `nimstatic/flags` (flag | |
| 145 | +emission). | |
| 146 | + | |
| 147 | +## Trust | |
| 148 | + | |
| 149 | +Packages come over HTTPS from the mirror and are unpacked as-is. nimstatic | |
| 150 | +checks the size recorded in the index but does **not** verify Alpine's RSA | |
| 151 | +signatures — apk's checksum field covers a package's control segment rather | |
| 152 | +than the file, so a real check means implementing apk's signature format. Treat | |
| 153 | +a sysroot as build input, not as a trust root. If that matters, pin a mirror | |
| 154 | +you run. | |
| 155 | + | |
| 156 | +## Install | |
| 157 | + | |
| 158 | +```bash | |
| 159 | +nimble install | |
| 160 | +``` | |
| 161 | + | |
| 162 | +or build in place with `nim c -d:ssl -o:nimstatic src/nimstatic.nim`. | |
| 112 | 163 | |
| 113 | 164 | ## Tests |
| 114 | 165 | |
| @@ -116,5 +167,7 @@ pkg-config flag emission). | ||
| 116 | 167 | nimble test |
| 117 | 168 | ``` |
| 118 | 169 | |
| 119 | -The suite is offline — index parsing, resolution through `so:`/`pkg:` provides, | |
| 120 | -manifest round-trips and flag emission all run against fixtures. | |
| 170 | +25 tests, all offline: soname parsing, link-command parsing, dynlib discovery | |
| 171 | +in generated C, package mapping, APKINDEX parsing, dependency resolution | |
| 172 | +through `so:`/`pkgconfig` provides, manifest round-trips and flag emission | |
| 173 | +(including link order and the dropped-override case). | |
| @@ -1,114 +1,165 @@ | |||
| 1 | -# muslkit | 1 | +# nimstatic |
| 2 | 2 | ||
| 3 | -musl-linked static libraries from Alpine, without Alpine. | 3 | +Fully static Nim binaries, dependencies and all. |
| 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 | 4 | ||
| 13 | ```bash | 5 | ```bash |
| 14 | -muslkit add openssl-libs-static | 6 | +nimstatic app.nim |
| 15 | -nim c -d:ssl $(muslkit nimflags) --cc:clang --clang.exe:./zigcc app.nim | ||
| 16 | ``` | 7 | ``` |
| 17 | 8 | ||
| 18 | -## Why | 9 | +That's the whole thing. nimstatic asks the Nim compiler what `app.nim` actually |
| 19 | - | 10 | +needs, fetches musl-built static libraries for it from Alpine's mirrors, and |
| 20 | -Getting a fully static Nim (or C, or Zig) binary with TLS on a glibc distro | 11 | +compiles against a sysroot it owns. No apk, no container, no root, no Nix. |
| 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 | 12 | ||
| 27 | ``` | 13 | ``` |
| 28 | -muslkit add <pkg>... Download packages and unpack into the sysroot | 14 | +$ nimstatic freeqsay.nim -- -d:ssl |
| 29 | -muslkit list Show what the sysroot holds | 15 | +probing freeqsay.nim … |
| 30 | -muslkit libs Show the static libraries in the sysroot | 16 | + ssl dlopen openssl-libs-static |
| 31 | -muslkit search <text> Search the index (names and descriptions) | 17 | + crypto dlopen openssl-libs-static |
| 32 | -muslkit show <pkg> Index record for one package | 18 | +downloading openssl-libs-static 3.3.7-r1 (12734 KiB) |
| 33 | -muslkit nimflags [-l lib] Print nim flags for a static build | 19 | +building … |
| 34 | -muslkit ccflags [-l lib] Print cc/clang flags | 20 | +wrote freeqsay (20687 KiB, static) |
| 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 | ``` | 21 | ``` |
| 41 | 22 | ||
| 42 | -Options: `-r/--root`, `-b/--branch` (default `v3.21`, `edge` for rolling), | 23 | +The result runs anywhere with a Linux kernel — `ldd` says *not a dynamic |
| 43 | -`-a/--arch`, `-m/--mirror`, `--repo main,community`, `-l/--lib`, `--cc`, | 24 | +executable* — and its HTTPS still works. |
| 44 | -`--no-deps`, `--refresh`, `-q/--quiet`. | ||
| 45 | 25 | ||
| 46 | -The sysroot defaults to `$XDG_DATA_HOME/muslkit/sysroot` and honors | 26 | +## How it knows what you need |
| 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 | 27 | ||
| 50 | -## A full static build, start to finish | 28 | +Guessing from `import` lines would be wrong in both directions: a transitive |
| 29 | +import three modules deep still needs its library, and an import behind a | ||
| 30 | +`when` that never fires does not. So nimstatic asks the compiler instead. A | ||
| 31 | +`--compileOnly --genScript` probe produces the nimcache your real build would | ||
| 32 | +have, and that cache answers twice over: | ||
| 51 | 33 | ||
| 52 | -```bash | 34 | +- **`<project>.json`** carries the link command, so every `-lfoo` is explicit. |
| 53 | -muslkit add openssl-libs-static zlib-static | 35 | +- **The generated C** contains the dynlib candidate strings Nim will `dlopen` |
| 54 | -muslkit zigcc # writes ./zigcc (needs zig on PATH) | 36 | + at runtime — `"libssl.so(.3|.1.1|…)"`. These never appear on a link line, and |
| 55 | -nim c -d:release -d:ssl \ | 37 | + they are exactly what breaks a static binary. |
| 56 | - --cc:clang --clang.exe:./zigcc --clang.linkerexe:./zigcc \ | 38 | + |
| 57 | - $(muslkit nimflags) --passL:-s \ | 39 | +Libraries musl already provides (`m`, `rt`, `dl`, `pthread`, …) are skipped. |
| 58 | - -o:app src/app.nim | 40 | +Anything else is looked up in a table of Alpine packages; whatever isn't mapped |
| 41 | +is reported rather than silently dropped: | ||
| 42 | + | ||
| 43 | +``` | ||
| 44 | +$ nimstatic detect app.nim | ||
| 45 | +library how alpine package | ||
| 46 | +sqlite3 link sqlite-static | ||
| 47 | +ssl dlopen openssl-libs-static | ||
| 48 | +mystery dlopen (unmapped) | ||
| 49 | + | ||
| 50 | +unmapped: mystery | ||
| 51 | +search for one with: nimstatic search mystery | ||
| 59 | ``` | 52 | ``` |
| 60 | 53 | ||
| 61 | -`muslkit nimflags` emits more than include and library paths, because two | 54 | +Then `--map mystery=mystery-static` teaches it, for that build. |
| 62 | -things bite every static Nim build with TLS: | 55 | + |
| 56 | +## The two things that silently break static Nim | ||
| 57 | + | ||
| 58 | +Both are handled automatically; they're documented here because they cost | ||
| 59 | +everyone an afternoon at least once. | ||
| 63 | 60 | ||
| 64 | - **`-d:ssl` makes Nim `dlopen` libssl at runtime.** A static binary cannot, | 61 | - **`-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 | 62 | 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 | 63 | + paths that never touch the network. Fix: `--dynlibOverride:ssl |
| 67 | - --dynlibOverride:crypto` plus the archives on the link line. | 64 | + --dynlibOverride:crypto` plus the archives on the link line. nimstatic emits |
| 65 | + an override only for libraries it actually has an archive for, since an | ||
| 66 | + override without one turns a runtime failure into a link failure. | ||
| 68 | - **OpenSSL 3 removed `SSL_get_peer_certificate`,** which Nim's wrapper still | 67 | - **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 | 68 | + names, so the link dies on one undefined symbol. Fix: |
| 70 | `-DSSL_get_peer_certificate=SSL_get1_peer_certificate`. | 69 | `-DSSL_get_peer_certificate=SSL_get1_peer_certificate`. |
| 71 | 70 | ||
| 72 | -Both are emitted automatically when `libssl.a` and `libcrypto.a` are present in | 71 | +A static binary also carries no CA trust store, so set `SSL_CERT_FILE` on the |
| 73 | -the sysroot. A static binary carries no CA trust store, so set `SSL_CERT_FILE` | 72 | +host that runs it. |
| 74 | -to a bundle on the host that runs it. | ||
| 75 | 73 | ||
| 76 | -Cross-compiling is the same command with `--arch`; `muslkit add -a aarch64 | 74 | +## Usage |
| 77 | -openssl-libs-static` and `muslkit zigcc -a aarch64` line up. | ||
| 78 | 75 | ||
| 79 | -## Trust | 76 | +``` |
| 77 | +nimstatic <file.nim> [-- <nim args>] Detect, fetch, build static | ||
| 78 | +nimstatic detect <file.nim> Show what it needs, change nothing | ||
| 79 | +nimstatic add <pkg>... Put packages in the sysroot by hand | ||
| 80 | +nimstatic list Show what the sysroot holds | ||
| 81 | +nimstatic libs Show the sysroot's static libraries | ||
| 82 | +nimstatic search <text> Search Alpine's index | ||
| 83 | +nimstatic show <pkg> Index record for one package | ||
| 84 | +nimstatic nimflags [-l lib] Print nim flags for the sysroot | ||
| 85 | +nimstatic ccflags [-l lib] Print cc/clang flags | ||
| 86 | +nimstatic nimcfg [-o file] Write a nim.cfg fragment | ||
| 87 | +nimstatic zigcc [-o file] Write a `zig cc -target …-musl` wrapper | ||
| 88 | +nimstatic env Shell exports (PKG_CONFIG_*, NIMSTATIC_ROOT) | ||
| 89 | +nimstatic path Print the sysroot path | ||
| 90 | +nimstatic clean Remove the sysroot (cache is kept) | ||
| 91 | +``` | ||
| 80 | 92 | ||
| 81 | -Packages come over HTTPS from the mirror and are unpacked as-is. muslkit checks | 93 | +Everything after `--` goes to the Nim compiler for **both** the probe and the |
| 82 | -the size recorded in the index but does **not** verify Alpine's RSA signatures — | 94 | +build, so conditional imports resolve the same way twice: |
| 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 | 95 | ||
| 87 | -## Install | 96 | +```bash |
| 97 | +nimstatic app.nim -- -d:ssl -d:danger | ||
| 98 | +``` | ||
| 99 | + | ||
| 100 | +Build options: `-o/--output`, `-d/--debug` (skip `-d:release`), `-n/--dry-run` | ||
| 101 | +(print the command instead of running it), `--map lib=pkg`, `--pkg name`, | ||
| 102 | +`--cc path`, `--nim path`. | ||
| 103 | + | ||
| 104 | +Sysroot options: `-r/--root`, `-b/--branch` (default `v3.21`, `edge` for | ||
| 105 | +rolling), `-a/--arch`, `-m/--mirror`, `--repo main,community`, `-l/--lib`, | ||
| 106 | +`--no-deps`, `--refresh`, `-q/--quiet`. | ||
| 107 | + | ||
| 108 | +The sysroot defaults to `$XDG_DATA_HOME/nimstatic/sysroot` and honors | ||
| 109 | +`NIMSTATIC_ROOT`. Downloads cache under `$XDG_CACHE_HOME/nimstatic`, so the | ||
| 110 | +second build is offline and the index is re-fetched once a day. | ||
| 111 | + | ||
| 112 | +## Cross-compiling | ||
| 113 | + | ||
| 114 | +Same command, one flag — the sysroot, the packages and the zig target all | ||
| 115 | +follow `--arch`: | ||
| 88 | 116 | ||
| 89 | ```bash | 117 | ```bash |
| 90 | -nimble install | 118 | +nimstatic app.nim --arch aarch64 -o app-arm64 |
| 91 | ``` | 119 | ``` |
| 92 | 120 | ||
| 93 | -or build in place with `nim c -d:ssl -o:muslkit src/muslkit.nim`. Requires | 121 | +## Requirements |
| 94 | -`tar` on PATH (an `.apk` is concatenated gzip streams, which GNU tar reads) and, | 122 | + |
| 95 | -for the `zigcc` helper, `zig`. | 123 | +- **zig** on PATH — used as `zig cc -target x86_64-linux-musl`. Pass `--cc` to |
| 124 | + use a musl cross-compiler you already have instead. | ||
| 125 | +- **tar** — an `.apk` is concatenated gzip streams, which GNU tar reads. | ||
| 96 | 126 | ||
| 97 | -## Library | 127 | +## Using it as a library |
| 98 | 128 | ||
| 99 | ```nim | 129 | ```nim |
| 100 | -import muslkit | 130 | +import nimstatic |
| 131 | + | ||
| 132 | +let d = detect("app.nim", ["-d:ssl"]) | ||
| 133 | +echo packages(d) # @["openssl-libs-static"] | ||
| 134 | +echo dynlibOverrides(d) # @["ssl", "crypto"] | ||
| 101 | 135 | ||
| 102 | let remote = initRemote(branch = "edge") | 136 | let remote = initRemote(branch = "edge") |
| 103 | -let idx = remote.fetchIndex() | 137 | +for pkg in remote.fetchIndex().resolve(packages(d)): |
| 104 | -for pkg in idx.resolve(["openssl-libs-static"]): | ||
| 105 | echo pkg.name, " ", pkg.version, " ", remote.fetchPackage(pkg) | 138 | echo pkg.name, " ", pkg.version, " ", remote.fetchPackage(pkg) |
| 106 | ``` | 139 | ``` |
| 107 | 140 | ||
| 108 | -Modules: `muslkit/index` (APKINDEX parsing, provides and dependency | 141 | +Modules: `nimstatic/detect` (probe, parse, map), `nimstatic/build` (the |
| 109 | -resolution), `muslkit/repo` (mirror, cache, download, unpack), | 142 | +one-command path), `nimstatic/index` (APKINDEX parsing, provides and dependency |
| 110 | -`muslkit/sysroot` (manifest, static-lib discovery), `muslkit/flags` (nim/cc/ | 143 | +resolution), `nimstatic/repo` (mirror, cache, download, unpack), |
| 111 | -pkg-config flag emission). | 144 | +`nimstatic/sysroot` (manifest, static-lib discovery), `nimstatic/flags` (flag |
| 145 | +emission). | ||
| 146 | + | ||
| 147 | +## Trust | ||
| 148 | + | ||
| 149 | +Packages come over HTTPS from the mirror and are unpacked as-is. nimstatic | ||
| 150 | +checks the size recorded in the index but does **not** verify Alpine's RSA | ||
| 151 | +signatures — apk's checksum field covers a package's control segment rather | ||
| 152 | +than the file, so a real check means implementing apk's signature format. Treat | ||
| 153 | +a sysroot as build input, not as a trust root. If that matters, pin a mirror | ||
| 154 | +you run. | ||
| 155 | + | ||
| 156 | +## Install | ||
| 157 | + | ||
| 158 | +```bash | ||
| 159 | +nimble install | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +or build in place with `nim c -d:ssl -o:nimstatic src/nimstatic.nim`. | ||
| 112 | 163 | ||
| 113 | ## Tests | 164 | ## Tests |
| 114 | 165 | ||
| @@ -116,5 +167,7 @@ pkg-config flag emission). | |||
| 116 | nimble test | 167 | nimble test |
| 117 | ``` | 168 | ``` |
| 118 | 169 | ||
| 119 | -The suite is offline — index parsing, resolution through `so:`/`pkg:` provides, | 170 | +25 tests, all offline: soname parsing, link-command parsing, dynlib discovery |
| 120 | -manifest round-trips and flag emission all run against fixtures. | 171 | +in generated C, package mapping, APKINDEX parsing, dependency resolution |
| 172 | +through `so:`/`pkgconfig` provides, manifest round-trips and flag emission | ||
| 173 | +(including link order and the dropped-override case). | ||
added
muslkit +0 -0 | new file mode 100755 | ||
| Binary files /dev/null and b/muslkit differ | ||
| new file mode 100755 | |||
| Binary files /dev/null and b/muslkit differ | Binary files /dev/null and b/muslkit differ | ||
deleted
muslkit.nimble +0 -12 | deleted file mode 100644 | ||
| @@ -1,12 +0,0 @@ | ||
| 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" | |
| deleted file mode 100644 | |||
| @@ -1,12 +0,0 @@ | |||
| 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
nimstatic.nimble +12 -0 | new file mode 100644 | ||
| @@ -0,0 +1,12 @@ | ||
| 1 | +version = "0.1.0" | |
| 2 | +author = "nandi" | |
| 3 | +description = "Fully static Nim binaries, dependencies and all — detects what you link and fetches musl archives from Alpine" | |
| 4 | +license = "MIT" | |
| 5 | +srcDir = "src" | |
| 6 | +bin = @["nimstatic"] | |
| 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_nimstatic.nim" | |
| new file mode 100644 | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | +version = "0.1.0" | ||
| 2 | +author = "nandi" | ||
| 3 | +description = "Fully static Nim binaries, dependencies and all — detects what you link and fetches musl archives from Alpine" | ||
| 4 | +license = "MIT" | ||
| 5 | +srcDir = "src" | ||
| 6 | +bin = @["nimstatic"] | ||
| 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_nimstatic.nim" | ||
deleted
src/muslkit.nim +0 -213 | deleted file mode 100644 | ||
| @@ -1,213 +0,0 @@ | ||
| 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 | |
| deleted file mode 100644 | |||
| @@ -1,213 +0,0 @@ | |||
| 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 | ||
deleted
src/muslkit/flags.nim +0 -78 | deleted file mode 100644 | ||
| @@ -1,78 +0,0 @@ | ||
| 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" | |
| deleted file mode 100644 | |||
| @@ -1,78 +0,0 @@ | |||
| 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/nimstatic.nim +272 -0 | new file mode 100644 | ||
| @@ -0,0 +1,272 @@ | ||
| 1 | +## nimstatic — fully static Nim binaries, dependencies and all. | |
| 2 | +## | |
| 3 | +## nimstatic foo.nim | |
| 4 | +## | |
| 5 | +## It asks the Nim compiler what `foo.nim` links against (including the | |
| 6 | +## libraries Nim would `dlopen` at runtime, which never touch the link line), | |
| 7 | +## fetches musl-built static archives for them from Alpine's mirrors, and | |
| 8 | +## compiles against a sysroot it owns. No apk, no container, no root. | |
| 9 | + | |
| 10 | +import nimstatic/[build, detect, flags, index, repo, sysroot] | |
| 11 | +export build, detect, flags, index, repo, sysroot | |
| 12 | + | |
| 13 | +when isMainModule: | |
| 14 | + import std/[os, sequtils, strutils, tables] | |
| 15 | + | |
| 16 | + const usageText = """ | |
| 17 | +nimstatic — fully static Nim binaries, dependencies and all | |
| 18 | + | |
| 19 | +Usage: | |
| 20 | + nimstatic <file.nim> [-- <nim args>] Detect, fetch, build static | |
| 21 | + nimstatic detect <file.nim> Show what it needs, change nothing | |
| 22 | + nimstatic add <pkg>... Put packages in the sysroot by hand | |
| 23 | + nimstatic list Show what the sysroot holds | |
| 24 | + nimstatic libs Show the sysroot's static libraries | |
| 25 | + nimstatic search <text> Search Alpine's index | |
| 26 | + nimstatic show <pkg> Index record for one package | |
| 27 | + nimstatic nimflags [-l lib] Print nim flags for the sysroot | |
| 28 | + nimstatic ccflags [-l lib] Print cc/clang flags | |
| 29 | + nimstatic nimcfg [-o file] Write a nim.cfg fragment | |
| 30 | + nimstatic zigcc [-o file] Write a `zig cc -target …-musl` wrapper | |
| 31 | + nimstatic env Shell exports (PKG_CONFIG_*, NIMSTATIC_ROOT) | |
| 32 | + nimstatic path Print the sysroot path | |
| 33 | + nimstatic clean Remove the sysroot (cache is kept) | |
| 34 | + | |
| 35 | +Build options: | |
| 36 | + -o, --output <path> Binary to write (default: the source's name) | |
| 37 | + -d, --debug Skip -d:release | |
| 38 | + -n, --dry-run Print the build command instead of running it | |
| 39 | + --map <lib=pkg> Map a library to an Alpine package (repeatable) | |
| 40 | + --pkg <name> Also install this package (repeatable) | |
| 41 | + --cc <path> Compiler to use instead of a generated zig wrapper | |
| 42 | + --nim <path> Nim executable (default: nim) | |
| 43 | + | |
| 44 | +Sysroot options: | |
| 45 | + -r, --root <dir> Sysroot (default $XDG_DATA_HOME/nimstatic/sysroot) | |
| 46 | + -b, --branch <ver> Alpine branch (default v3.21; `edge` for rolling) | |
| 47 | + -a, --arch <arch> Target arch (default x86_64) | |
| 48 | + -m, --mirror <url> Mirror base URL | |
| 49 | + --repo <names> Comma-separated repositories (default main,community) | |
| 50 | + -l, --lib <name> Add -l<name> to emitted flags (repeatable) | |
| 51 | + --no-deps Do not pull dependencies (add only) | |
| 52 | + --refresh Re-fetch the index even if it is fresh | |
| 53 | + -q, --quiet No progress on stderr | |
| 54 | + -h, --help Show help | |
| 55 | + | |
| 56 | +Everything after `--` goes to the Nim compiler, for both the probe and the | |
| 57 | +build, so conditional imports resolve the same way twice: | |
| 58 | + | |
| 59 | + nimstatic app.nim -- -d:ssl -d:danger | |
| 60 | + | |
| 61 | +Packages come over HTTPS and are unpacked as-is; nimstatic does not verify | |
| 62 | +Alpine's signatures, so treat a sysroot as build input, not as a trust root. | |
| 63 | +""" | |
| 64 | + | |
| 65 | + type Opts = object | |
| 66 | + root, mirror, branch, arch, cc, output, nimExe: string | |
| 67 | + repos, libs, args, nimArgs, pkgs: seq[string] | |
| 68 | + maps: Table[string, string] | |
| 69 | + noDeps, refresh, quiet, help, debug, dryRun: bool | |
| 70 | + | |
| 71 | + proc defaultRoot(): string = getDataDir() / "nimstatic" / "sysroot" | |
| 72 | + | |
| 73 | + proc die(msg: string) = | |
| 74 | + stderr.writeLine "nimstatic: " & msg | |
| 75 | + quit(1) | |
| 76 | + | |
| 77 | + proc parseOpts(): Opts = | |
| 78 | + ## Hand-rolled so `--root dir`, `--root=dir` and `-r dir` all work, and so | |
| 79 | + ## everything past `--` can be handed to the compiler untouched. | |
| 80 | + result = Opts(root: getEnv("NIMSTATIC_ROOT", defaultRoot()), | |
| 81 | + mirror: defaultMirror, branch: defaultBranch, | |
| 82 | + arch: defaultArch, repos: defaultRepos, nimExe: "nim") | |
| 83 | + let argv = commandLineParams() | |
| 84 | + var i = 0 | |
| 85 | + while i < argv.len: | |
| 86 | + let arg = argv[i] | |
| 87 | + if arg == "--": | |
| 88 | + result.nimArgs = argv[i + 1 .. ^1] | |
| 89 | + break | |
| 90 | + if not arg.startsWith("-") or arg == "-": | |
| 91 | + result.args.add arg | |
| 92 | + inc i | |
| 93 | + continue | |
| 94 | + var | |
| 95 | + key = arg.strip(leading = true, trailing = false, chars = {'-'}) | |
| 96 | + val = "" | |
| 97 | + attached = false | |
| 98 | + for sep in ['=', ':']: | |
| 99 | + let at = key.find(sep) | |
| 100 | + if at >= 0: | |
| 101 | + val = key[at + 1 .. ^1] | |
| 102 | + key = key[0 ..< at] | |
| 103 | + attached = true | |
| 104 | + break | |
| 105 | + | |
| 106 | + proc takeValue(): string = | |
| 107 | + if attached: return val | |
| 108 | + inc i | |
| 109 | + if i >= argv.len: die "option --" & key & " needs a value" | |
| 110 | + argv[i] | |
| 111 | + | |
| 112 | + case key | |
| 113 | + of "r", "root": result.root = takeValue() | |
| 114 | + of "b", "branch": result.branch = takeValue() | |
| 115 | + of "a", "arch": result.arch = takeValue() | |
| 116 | + of "m", "mirror": result.mirror = takeValue() | |
| 117 | + of "repo": result.repos = takeValue().split(',').filterIt(it.len > 0) | |
| 118 | + of "l", "lib": result.libs.add takeValue() | |
| 119 | + of "o", "output": result.output = takeValue() | |
| 120 | + of "cc": result.cc = takeValue() | |
| 121 | + of "nim": result.nimExe = takeValue() | |
| 122 | + of "pkg": result.pkgs.add takeValue() | |
| 123 | + of "map": | |
| 124 | + let m = takeValue().split('=', 1) | |
| 125 | + if m.len != 2: die "--map wants lib=package, got: " & m.join("=") | |
| 126 | + result.maps[m[0]] = m[1] | |
| 127 | + of "d", "debug": result.debug = true | |
| 128 | + of "n", "dry-run": result.dryRun = true | |
| 129 | + of "no-deps": result.noDeps = true | |
| 130 | + of "refresh": result.refresh = true | |
| 131 | + of "q", "quiet": result.quiet = true | |
| 132 | + of "h", "help": result.help = true | |
| 133 | + else: die "unknown option: " & arg | |
| 134 | + inc i | |
| 135 | + | |
| 136 | + proc emit(o: Opts, text: string) = | |
| 137 | + if o.output.len > 0: | |
| 138 | + createDir o.output.parentDir | |
| 139 | + writeFile(o.output, text) | |
| 140 | + if not o.quiet: stderr.writeLine "wrote " & o.output | |
| 141 | + else: | |
| 142 | + stdout.write text | |
| 143 | + | |
| 144 | + proc buildOpts(o: Opts): BuildOpts = | |
| 145 | + BuildOpts(root: o.root, cc: o.cc, output: o.output, nimExe: o.nimExe, | |
| 146 | + target: o.arch & "-linux-musl", nimArgs: o.nimArgs, | |
| 147 | + extraMap: o.maps, extraPackages: o.pkgs, | |
| 148 | + release: not o.debug, dryRun: o.dryRun, quiet: o.quiet) | |
| 149 | + | |
| 150 | + proc cmdBuild(o: Opts, remote: Remote, source: string) = | |
| 151 | + let res = buildOpts(o).build(remote, source) | |
| 152 | + if o.dryRun: | |
| 153 | + echo res.command.quoteShellCommand | |
| 154 | + return | |
| 155 | + if not o.quiet: | |
| 156 | + let size = if fileExists(res.output): getFileSize(res.output) div 1024 else: 0 | |
| 157 | + stderr.writeLine "wrote " & res.output & " (" & $size & " KiB, static)" | |
| 158 | + | |
| 159 | + proc cmdDetect(o: Opts, source: string) = | |
| 160 | + let d = detect(source, o.nimArgs, o.nimExe, o.maps) | |
| 161 | + if d.needs.len == 0: | |
| 162 | + echo "no external libraries needed — plain --passL:-static will do" | |
| 163 | + return | |
| 164 | + echo "library".alignLeft(16), "how".alignLeft(9), "alpine package" | |
| 165 | + for need in d.needs: | |
| 166 | + echo need.lib.alignLeft(16), | |
| 167 | + (if need.dynlib: "dlopen" else: "link").alignLeft(9), | |
| 168 | + (if need.package.len > 0: need.package else: "(unmapped)") | |
| 169 | + if packages(d).len > 0: | |
| 170 | + echo "\nnimstatic add ", packages(d).join(" ") | |
| 171 | + if d.unmapped.len > 0: | |
| 172 | + echo "\nunmapped: ", d.unmapped.join(", "), | |
| 173 | + "\nsearch for one with: nimstatic search ", d.unmapped[0] | |
| 174 | + | |
| 175 | + proc cmdAdd(o: Opts, remote: Remote) = | |
| 176 | + if o.args.len == 0: die "add: name at least one package" | |
| 177 | + let pkgs = remote.fetchIndex(o.refresh).resolve(o.args, not o.noDeps) | |
| 178 | + for pkg in pkgs: | |
| 179 | + unpack(remote.fetchPackage(pkg), o.root) | |
| 180 | + record(o.root, pkgs) | |
| 181 | + if not o.quiet: | |
| 182 | + stderr.writeLine "unpacked " & $pkgs.len & " package(s) into " & o.root | |
| 183 | + | |
| 184 | + proc cmdList(o: Opts) = | |
| 185 | + let entries = readManifest(o.root) | |
| 186 | + if entries.len == 0: | |
| 187 | + stderr.writeLine "nothing installed in " & o.root | |
| 188 | + return | |
| 189 | + for e in entries: | |
| 190 | + echo e.name.alignLeft(32), e.version.alignLeft(20), e.repo | |
| 191 | + | |
| 192 | + proc cmdSearch(o: Opts, remote: Remote) = | |
| 193 | + if o.args.len == 0: die "search: give some text" | |
| 194 | + let | |
| 195 | + idx = remote.fetchIndex(o.refresh) | |
| 196 | + needle = o.args.join(" ").toLowerAscii | |
| 197 | + var hits = 0 | |
| 198 | + for name, pkg in idx.packages: | |
| 199 | + if needle in name.toLowerAscii or needle in pkg.description.toLowerAscii: | |
| 200 | + echo name.alignLeft(34), pkg.version.alignLeft(16), pkg.description | |
| 201 | + inc hits | |
| 202 | + if hits == 0: stderr.writeLine "no matches for " & needle | |
| 203 | + | |
| 204 | + proc cmdShow(o: Opts, remote: Remote) = | |
| 205 | + if o.args.len == 0: die "show: name a package" | |
| 206 | + let idx = remote.fetchIndex(o.refresh) | |
| 207 | + for want in o.args: | |
| 208 | + let name = idx.find(want) | |
| 209 | + if name.len == 0: die "no package provides '" & want & "'" | |
| 210 | + let pkg = idx.packages[name] | |
| 211 | + echo "name: ", pkg.name | |
| 212 | + echo "version: ", pkg.version | |
| 213 | + echo "repository: ", pkg.repo, "/", remote.arch | |
| 214 | + echo "size: ", pkg.size div 1024, " KiB" | |
| 215 | + echo "description: ", pkg.description | |
| 216 | + if pkg.depends.len > 0: echo "depends: ", pkg.depends.join(" ") | |
| 217 | + if pkg.provides.len > 0: echo "provides: ", pkg.provides.join(" ") | |
| 218 | + echo "url: ", remote.repoUrl(pkg.repo), "/", apkFile(pkg) | |
| 219 | + | |
| 220 | + proc main() = | |
| 221 | + let o = parseOpts() | |
| 222 | + if o.help or o.args.len == 0: | |
| 223 | + stdout.write usageText | |
| 224 | + quit(if o.help: 0 else: 1) | |
| 225 | + let | |
| 226 | + head = o.args[0] | |
| 227 | + rest = block: | |
| 228 | + var r = o | |
| 229 | + r.args = o.args[1 .. ^1] | |
| 230 | + r | |
| 231 | + remote = initRemote(o.mirror, o.branch, o.arch, o.repos, quiet = o.quiet) | |
| 232 | + | |
| 233 | + # A .nim path is the whole point, so it needs no subcommand. | |
| 234 | + if head.endsWith(".nim") or fileExists(head): | |
| 235 | + cmdBuild(o, remote, head) | |
| 236 | + return | |
| 237 | + | |
| 238 | + case head | |
| 239 | + of "build": | |
| 240 | + if rest.args.len == 0: die "build: name a .nim file" | |
| 241 | + cmdBuild(rest, remote, rest.args[0]) | |
| 242 | + of "detect": | |
| 243 | + if rest.args.len == 0: die "detect: name a .nim file" | |
| 244 | + cmdDetect(rest, rest.args[0]) | |
| 245 | + of "add": cmdAdd(rest, remote) | |
| 246 | + of "list": cmdList(rest) | |
| 247 | + of "libs": stdout.write describe(rest.root) | |
| 248 | + of "search": cmdSearch(rest, remote) | |
| 249 | + of "show": cmdShow(rest, remote) | |
| 250 | + of "nimflags": | |
| 251 | + echo nimFlags(rest.root, overridesIn(rest.root), rest.libs).join(" ") | |
| 252 | + of "ccflags": echo ccFlags(rest.root, rest.libs).join(" ") | |
| 253 | + of "nimcfg": | |
| 254 | + rest.emit nimCfg(rest.root, rest.cc, overridesIn(rest.root), rest.libs) | |
| 255 | + of "zigcc": | |
| 256 | + let path = if rest.output.len > 0: rest.output else: "zigcc" | |
| 257 | + writeZigCc(path, rest.arch & "-linux-musl") | |
| 258 | + if not rest.quiet: stderr.writeLine "wrote " & path | |
| 259 | + of "env": | |
| 260 | + for kv in pkgConfigEnv(rest.root): | |
| 261 | + echo "export ", kv | |
| 262 | + echo "export NIMSTATIC_ROOT=", rest.root | |
| 263 | + of "path": echo rest.root | |
| 264 | + of "clean": | |
| 265 | + removeDir rest.root | |
| 266 | + if not rest.quiet: stderr.writeLine "removed " & rest.root | |
| 267 | + else: die "unknown command: " & head & " (try --help)" | |
| 268 | + | |
| 269 | + try: | |
| 270 | + main() | |
| 271 | + except CatchableError as e: | |
| 272 | + die e.msg | |
| new file mode 100644 | |||
| @@ -0,0 +1,272 @@ | |||
| 1 | +## nimstatic — fully static Nim binaries, dependencies and all. | ||
| 2 | +## | ||
| 3 | +## nimstatic foo.nim | ||
| 4 | +## | ||
| 5 | +## It asks the Nim compiler what `foo.nim` links against (including the | ||
| 6 | +## libraries Nim would `dlopen` at runtime, which never touch the link line), | ||
| 7 | +## fetches musl-built static archives for them from Alpine's mirrors, and | ||
| 8 | +## compiles against a sysroot it owns. No apk, no container, no root. | ||
| 9 | + | ||
| 10 | +import nimstatic/[build, detect, flags, index, repo, sysroot] | ||
| 11 | +export build, detect, flags, index, repo, sysroot | ||
| 12 | + | ||
| 13 | +when isMainModule: | ||
| 14 | + import std/[os, sequtils, strutils, tables] | ||
| 15 | + | ||
| 16 | + const usageText = """ | ||
| 17 | +nimstatic — fully static Nim binaries, dependencies and all | ||
| 18 | + | ||
| 19 | +Usage: | ||
| 20 | + nimstatic <file.nim> [-- <nim args>] Detect, fetch, build static | ||
| 21 | + nimstatic detect <file.nim> Show what it needs, change nothing | ||
| 22 | + nimstatic add <pkg>... Put packages in the sysroot by hand | ||
| 23 | + nimstatic list Show what the sysroot holds | ||
| 24 | + nimstatic libs Show the sysroot's static libraries | ||
| 25 | + nimstatic search <text> Search Alpine's index | ||
| 26 | + nimstatic show <pkg> Index record for one package | ||
| 27 | + nimstatic nimflags [-l lib] Print nim flags for the sysroot | ||
| 28 | + nimstatic ccflags [-l lib] Print cc/clang flags | ||
| 29 | + nimstatic nimcfg [-o file] Write a nim.cfg fragment | ||
| 30 | + nimstatic zigcc [-o file] Write a `zig cc -target …-musl` wrapper | ||
| 31 | + nimstatic env Shell exports (PKG_CONFIG_*, NIMSTATIC_ROOT) | ||
| 32 | + nimstatic path Print the sysroot path | ||
| 33 | + nimstatic clean Remove the sysroot (cache is kept) | ||
| 34 | + | ||
| 35 | +Build options: | ||
| 36 | + -o, --output <path> Binary to write (default: the source's name) | ||
| 37 | + -d, --debug Skip -d:release | ||
| 38 | + -n, --dry-run Print the build command instead of running it | ||
| 39 | + --map <lib=pkg> Map a library to an Alpine package (repeatable) | ||
| 40 | + --pkg <name> Also install this package (repeatable) | ||
| 41 | + --cc <path> Compiler to use instead of a generated zig wrapper | ||
| 42 | + --nim <path> Nim executable (default: nim) | ||
| 43 | + | ||
| 44 | +Sysroot options: | ||
| 45 | + -r, --root <dir> Sysroot (default $XDG_DATA_HOME/nimstatic/sysroot) | ||
| 46 | + -b, --branch <ver> Alpine branch (default v3.21; `edge` for rolling) | ||
| 47 | + -a, --arch <arch> Target arch (default x86_64) | ||
| 48 | + -m, --mirror <url> Mirror base URL | ||
| 49 | + --repo <names> Comma-separated repositories (default main,community) | ||
| 50 | + -l, --lib <name> Add -l<name> to emitted flags (repeatable) | ||
| 51 | + --no-deps Do not pull dependencies (add only) | ||
| 52 | + --refresh Re-fetch the index even if it is fresh | ||
| 53 | + -q, --quiet No progress on stderr | ||
| 54 | + -h, --help Show help | ||
| 55 | + | ||
| 56 | +Everything after `--` goes to the Nim compiler, for both the probe and the | ||
| 57 | +build, so conditional imports resolve the same way twice: | ||
| 58 | + | ||
| 59 | + nimstatic app.nim -- -d:ssl -d:danger | ||
| 60 | + | ||
| 61 | +Packages come over HTTPS and are unpacked as-is; nimstatic does not verify | ||
| 62 | +Alpine's signatures, so treat a sysroot as build input, not as a trust root. | ||
| 63 | +""" | ||
| 64 | + | ||
| 65 | + type Opts = object | ||
| 66 | + root, mirror, branch, arch, cc, output, nimExe: string | ||
| 67 | + repos, libs, args, nimArgs, pkgs: seq[string] | ||
| 68 | + maps: Table[string, string] | ||
| 69 | + noDeps, refresh, quiet, help, debug, dryRun: bool | ||
| 70 | + | ||
| 71 | + proc defaultRoot(): string = getDataDir() / "nimstatic" / "sysroot" | ||
| 72 | + | ||
| 73 | + proc die(msg: string) = | ||
| 74 | + stderr.writeLine "nimstatic: " & msg | ||
| 75 | + quit(1) | ||
| 76 | + | ||
| 77 | + proc parseOpts(): Opts = | ||
| 78 | + ## Hand-rolled so `--root dir`, `--root=dir` and `-r dir` all work, and so | ||
| 79 | + ## everything past `--` can be handed to the compiler untouched. | ||
| 80 | + result = Opts(root: getEnv("NIMSTATIC_ROOT", defaultRoot()), | ||
| 81 | + mirror: defaultMirror, branch: defaultBranch, | ||
| 82 | + arch: defaultArch, repos: defaultRepos, nimExe: "nim") | ||
| 83 | + let argv = commandLineParams() | ||
| 84 | + var i = 0 | ||
| 85 | + while i < argv.len: | ||
| 86 | + let arg = argv[i] | ||
| 87 | + if arg == "--": | ||
| 88 | + result.nimArgs = argv[i + 1 .. ^1] | ||
| 89 | + break | ||
| 90 | + if not arg.startsWith("-") or arg == "-": | ||
| 91 | + result.args.add arg | ||
| 92 | + inc i | ||
| 93 | + continue | ||
| 94 | + var | ||
| 95 | + key = arg.strip(leading = true, trailing = false, chars = {'-'}) | ||
| 96 | + val = "" | ||
| 97 | + attached = false | ||
| 98 | + for sep in ['=', ':']: | ||
| 99 | + let at = key.find(sep) | ||
| 100 | + if at >= 0: | ||
| 101 | + val = key[at + 1 .. ^1] | ||
| 102 | + key = key[0 ..< at] | ||
| 103 | + attached = true | ||
| 104 | + break | ||
| 105 | + | ||
| 106 | + proc takeValue(): string = | ||
| 107 | + if attached: return val | ||
| 108 | + inc i | ||
| 109 | + if i >= argv.len: die "option --" & key & " needs a value" | ||
| 110 | + argv[i] | ||
| 111 | + | ||
| 112 | + case key | ||
| 113 | + of "r", "root": result.root = takeValue() | ||
| 114 | + of "b", "branch": result.branch = takeValue() | ||
| 115 | + of "a", "arch": result.arch = takeValue() | ||
| 116 | + of "m", "mirror": result.mirror = takeValue() | ||
| 117 | + of "repo": result.repos = takeValue().split(',').filterIt(it.len > 0) | ||
| 118 | + of "l", "lib": result.libs.add takeValue() | ||
| 119 | + of "o", "output": result.output = takeValue() | ||
| 120 | + of "cc": result.cc = takeValue() | ||
| 121 | + of "nim": result.nimExe = takeValue() | ||
| 122 | + of "pkg": result.pkgs.add takeValue() | ||
| 123 | + of "map": | ||
| 124 | + let m = takeValue().split('=', 1) | ||
| 125 | + if m.len != 2: die "--map wants lib=package, got: " & m.join("=") | ||
| 126 | + result.maps[m[0]] = m[1] | ||
| 127 | + of "d", "debug": result.debug = true | ||
| 128 | + of "n", "dry-run": result.dryRun = true | ||
| 129 | + of "no-deps": result.noDeps = true | ||
| 130 | + of "refresh": result.refresh = true | ||
| 131 | + of "q", "quiet": result.quiet = true | ||
| 132 | + of "h", "help": result.help = true | ||
| 133 | + else: die "unknown option: " & arg | ||
| 134 | + inc i | ||
| 135 | + | ||
| 136 | + proc emit(o: Opts, text: string) = | ||
| 137 | + if o.output.len > 0: | ||
| 138 | + createDir o.output.parentDir | ||
| 139 | + writeFile(o.output, text) | ||
| 140 | + if not o.quiet: stderr.writeLine "wrote " & o.output | ||
| 141 | + else: | ||
| 142 | + stdout.write text | ||
| 143 | + | ||
| 144 | + proc buildOpts(o: Opts): BuildOpts = | ||
| 145 | + BuildOpts(root: o.root, cc: o.cc, output: o.output, nimExe: o.nimExe, | ||
| 146 | + target: o.arch & "-linux-musl", nimArgs: o.nimArgs, | ||
| 147 | + extraMap: o.maps, extraPackages: o.pkgs, | ||
| 148 | + release: not o.debug, dryRun: o.dryRun, quiet: o.quiet) | ||
| 149 | + | ||
| 150 | + proc cmdBuild(o: Opts, remote: Remote, source: string) = | ||
| 151 | + let res = buildOpts(o).build(remote, source) | ||
| 152 | + if o.dryRun: | ||
| 153 | + echo res.command.quoteShellCommand | ||
| 154 | + return | ||
| 155 | + if not o.quiet: | ||
| 156 | + let size = if fileExists(res.output): getFileSize(res.output) div 1024 else: 0 | ||
| 157 | + stderr.writeLine "wrote " & res.output & " (" & $size & " KiB, static)" | ||
| 158 | + | ||
| 159 | + proc cmdDetect(o: Opts, source: string) = | ||
| 160 | + let d = detect(source, o.nimArgs, o.nimExe, o.maps) | ||
| 161 | + if d.needs.len == 0: | ||
| 162 | + echo "no external libraries needed — plain --passL:-static will do" | ||
| 163 | + return | ||
| 164 | + echo "library".alignLeft(16), "how".alignLeft(9), "alpine package" | ||
| 165 | + for need in d.needs: | ||
| 166 | + echo need.lib.alignLeft(16), | ||
| 167 | + (if need.dynlib: "dlopen" else: "link").alignLeft(9), | ||
| 168 | + (if need.package.len > 0: need.package else: "(unmapped)") | ||
| 169 | + if packages(d).len > 0: | ||
| 170 | + echo "\nnimstatic add ", packages(d).join(" ") | ||
| 171 | + if d.unmapped.len > 0: | ||
| 172 | + echo "\nunmapped: ", d.unmapped.join(", "), | ||
| 173 | + "\nsearch for one with: nimstatic search ", d.unmapped[0] | ||
| 174 | + | ||
| 175 | + proc cmdAdd(o: Opts, remote: Remote) = | ||
| 176 | + if o.args.len == 0: die "add: name at least one package" | ||
| 177 | + let pkgs = remote.fetchIndex(o.refresh).resolve(o.args, not o.noDeps) | ||
| 178 | + for pkg in pkgs: | ||
| 179 | + unpack(remote.fetchPackage(pkg), o.root) | ||
| 180 | + record(o.root, pkgs) | ||
| 181 | + if not o.quiet: | ||
| 182 | + stderr.writeLine "unpacked " & $pkgs.len & " package(s) into " & o.root | ||
| 183 | + | ||
| 184 | + proc cmdList(o: Opts) = | ||
| 185 | + let entries = readManifest(o.root) | ||
| 186 | + if entries.len == 0: | ||
| 187 | + stderr.writeLine "nothing installed in " & o.root | ||
| 188 | + return | ||
| 189 | + for e in entries: | ||
| 190 | + echo e.name.alignLeft(32), e.version.alignLeft(20), e.repo | ||
| 191 | + | ||
| 192 | + proc cmdSearch(o: Opts, remote: Remote) = | ||
| 193 | + if o.args.len == 0: die "search: give some text" | ||
| 194 | + let | ||
| 195 | + idx = remote.fetchIndex(o.refresh) | ||
| 196 | + needle = o.args.join(" ").toLowerAscii | ||
| 197 | + var hits = 0 | ||
| 198 | + for name, pkg in idx.packages: | ||
| 199 | + if needle in name.toLowerAscii or needle in pkg.description.toLowerAscii: | ||
| 200 | + echo name.alignLeft(34), pkg.version.alignLeft(16), pkg.description | ||
| 201 | + inc hits | ||
| 202 | + if hits == 0: stderr.writeLine "no matches for " & needle | ||
| 203 | + | ||
| 204 | + proc cmdShow(o: Opts, remote: Remote) = | ||
| 205 | + if o.args.len == 0: die "show: name a package" | ||
| 206 | + let idx = remote.fetchIndex(o.refresh) | ||
| 207 | + for want in o.args: | ||
| 208 | + let name = idx.find(want) | ||
| 209 | + if name.len == 0: die "no package provides '" & want & "'" | ||
| 210 | + let pkg = idx.packages[name] | ||
| 211 | + echo "name: ", pkg.name | ||
| 212 | + echo "version: ", pkg.version | ||
| 213 | + echo "repository: ", pkg.repo, "/", remote.arch | ||
| 214 | + echo "size: ", pkg.size div 1024, " KiB" | ||
| 215 | + echo "description: ", pkg.description | ||
| 216 | + if pkg.depends.len > 0: echo "depends: ", pkg.depends.join(" ") | ||
| 217 | + if pkg.provides.len > 0: echo "provides: ", pkg.provides.join(" ") | ||
| 218 | + echo "url: ", remote.repoUrl(pkg.repo), "/", apkFile(pkg) | ||
| 219 | + | ||
| 220 | + proc main() = | ||
| 221 | + let o = parseOpts() | ||
| 222 | + if o.help or o.args.len == 0: | ||
| 223 | + stdout.write usageText | ||
| 224 | + quit(if o.help: 0 else: 1) | ||
| 225 | + let | ||
| 226 | + head = o.args[0] | ||
| 227 | + rest = block: | ||
| 228 | + var r = o | ||
| 229 | + r.args = o.args[1 .. ^1] | ||
| 230 | + r | ||
| 231 | + remote = initRemote(o.mirror, o.branch, o.arch, o.repos, quiet = o.quiet) | ||
| 232 | + | ||
| 233 | + # A .nim path is the whole point, so it needs no subcommand. | ||
| 234 | + if head.endsWith(".nim") or fileExists(head): | ||
| 235 | + cmdBuild(o, remote, head) | ||
| 236 | + return | ||
| 237 | + | ||
| 238 | + case head | ||
| 239 | + of "build": | ||
| 240 | + if rest.args.len == 0: die "build: name a .nim file" | ||
| 241 | + cmdBuild(rest, remote, rest.args[0]) | ||
| 242 | + of "detect": | ||
| 243 | + if rest.args.len == 0: die "detect: name a .nim file" | ||
| 244 | + cmdDetect(rest, rest.args[0]) | ||
| 245 | + of "add": cmdAdd(rest, remote) | ||
| 246 | + of "list": cmdList(rest) | ||
| 247 | + of "libs": stdout.write describe(rest.root) | ||
| 248 | + of "search": cmdSearch(rest, remote) | ||
| 249 | + of "show": cmdShow(rest, remote) | ||
| 250 | + of "nimflags": | ||
| 251 | + echo nimFlags(rest.root, overridesIn(rest.root), rest.libs).join(" ") | ||
| 252 | + of "ccflags": echo ccFlags(rest.root, rest.libs).join(" ") | ||
| 253 | + of "nimcfg": | ||
| 254 | + rest.emit nimCfg(rest.root, rest.cc, overridesIn(rest.root), rest.libs) | ||
| 255 | + of "zigcc": | ||
| 256 | + let path = if rest.output.len > 0: rest.output else: "zigcc" | ||
| 257 | + writeZigCc(path, rest.arch & "-linux-musl") | ||
| 258 | + if not rest.quiet: stderr.writeLine "wrote " & path | ||
| 259 | + of "env": | ||
| 260 | + for kv in pkgConfigEnv(rest.root): | ||
| 261 | + echo "export ", kv | ||
| 262 | + echo "export NIMSTATIC_ROOT=", rest.root | ||
| 263 | + of "path": echo rest.root | ||
| 264 | + of "clean": | ||
| 265 | + removeDir rest.root | ||
| 266 | + if not rest.quiet: stderr.writeLine "removed " & rest.root | ||
| 267 | + else: die "unknown command: " & head & " (try --help)" | ||
| 268 | + | ||
| 269 | + try: | ||
| 270 | + main() | ||
| 271 | + except CatchableError as e: | ||
| 272 | + die e.msg | ||
added
src/nimstatic/build.nim +90 -0 | new file mode 100644 | ||
| @@ -0,0 +1,90 @@ | ||
| 1 | +## The one-command path: source file in, static binary out. | |
| 2 | +## | |
| 3 | +## detect → fetch what it named → compile against the sysroot. | |
| 4 | + | |
| 5 | +import std/[os, osproc, sequtils, strutils, tables] | |
| 6 | +import ./detect, ./flags, ./index, ./repo, ./sysroot | |
| 7 | + | |
| 8 | +type | |
| 9 | + BuildOpts* = object | |
| 10 | + root*, cc*, output*, nimExe*, target*: string | |
| 11 | + nimArgs*: seq[string] | |
| 12 | + extraMap*: Table[string, string] | |
| 13 | + extraPackages*: seq[string] | |
| 14 | + release*, dryRun*, quiet*: bool | |
| 15 | + | |
| 16 | + BuildResult* = object | |
| 17 | + detection*: Detection | |
| 18 | + fetched*: seq[string] | |
| 19 | + command*: seq[string] | |
| 20 | + output*: string | |
| 21 | + | |
| 22 | +proc note(o: BuildOpts, msg: string) = | |
| 23 | + if not o.quiet: stderr.writeLine msg | |
| 24 | + | |
| 25 | +proc ensurePackages*(o: BuildOpts, remote: Remote, wanted: seq[string]): seq[string] = | |
| 26 | + ## Install whatever the sysroot is missing. Already-present packages are not | |
| 27 | + ## re-downloaded, so a second build is offline. | |
| 28 | + let missing = wanted.filterIt(not installed(o.root, it)) | |
| 29 | + if missing.len == 0: return | |
| 30 | + let | |
| 31 | + idx = remote.fetchIndex() | |
| 32 | + pkgs = idx.resolve(missing) | |
| 33 | + for pkg in pkgs: | |
| 34 | + unpack(remote.fetchPackage(pkg), o.root) | |
| 35 | + record(o.root, pkgs) | |
| 36 | + pkgs.mapIt(it.name) | |
| 37 | + | |
| 38 | +proc resolveCc*(o: BuildOpts): string = | |
| 39 | + ## An explicit --cc wins; otherwise generate a zig wrapper beside the sysroot. | |
| 40 | + if o.cc.len > 0: return o.cc | |
| 41 | + if findExe("zig").len == 0: | |
| 42 | + raise newException(OSError, | |
| 43 | + "no C compiler for musl: install zig (used as `zig cc -target " & | |
| 44 | + o.target & "`) or pass --cc with a musl-targeting compiler") | |
| 45 | + let path = o.root.parentDir / ("zigcc-" & o.target) | |
| 46 | + writeZigCc(path, o.target) | |
| 47 | + path | |
| 48 | + | |
| 49 | +proc buildCommand*(o: BuildOpts, source: string, detection: Detection, | |
| 50 | + cc: string): seq[string] = | |
| 51 | + result = @[o.nimExe, "c"] | |
| 52 | + if o.release: result.add "-d:release" | |
| 53 | + result.add o.nimArgs | |
| 54 | + result.add ["--cc:clang", "--clang.exe:" & cc, "--clang.linkerexe:" & cc] | |
| 55 | + result.add nimFlags(o.root, dynlibOverrides(detection)) | |
| 56 | + if o.output.len > 0: | |
| 57 | + result.add "-o:" & o.output | |
| 58 | + result.add source | |
| 59 | + | |
| 60 | +proc build*(o: BuildOpts, remote: Remote, source: string): BuildResult = | |
| 61 | + if not fileExists(source): | |
| 62 | + raise newException(IOError, "no such file: " & source) | |
| 63 | + | |
| 64 | + o.note "probing " & source.extractFilename & " …" | |
| 65 | + result.detection = detect(source, o.nimArgs, o.nimExe, o.extraMap) | |
| 66 | + let det = result.detection | |
| 67 | + | |
| 68 | + if det.needs.len == 0: | |
| 69 | + o.note "no external libraries needed" | |
| 70 | + else: | |
| 71 | + for need in det.needs: | |
| 72 | + let how = if need.dynlib: "dlopen" else: "link" | |
| 73 | + o.note " " & need.lib.alignLeft(14) & how.alignLeft(8) & | |
| 74 | + (if need.package.len > 0: need.package else: "UNMAPPED") | |
| 75 | + if det.unmapped.len > 0: | |
| 76 | + o.note "warning: no Alpine package known for: " & det.unmapped.join(", ") & | |
| 77 | + "\n map it with --map " & det.unmapped[0] & "=<package>" & | |
| 78 | + ", or the link will fail" | |
| 79 | + | |
| 80 | + result.fetched = ensurePackages(o, remote, packages(det) & o.extraPackages) | |
| 81 | + let cc = resolveCc(o) | |
| 82 | + result.command = buildCommand(o, source, det, cc) | |
| 83 | + result.output = if o.output.len > 0: o.output | |
| 84 | + else: source.changeFileExt("") | |
| 85 | + | |
| 86 | + if o.dryRun: | |
| 87 | + return | |
| 88 | + o.note "building …" | |
| 89 | + if execCmd(result.command.quoteShellCommand) != 0: | |
| 90 | + raise newException(OSError, "the static build failed") | |
| new file mode 100644 | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | +## The one-command path: source file in, static binary out. | ||
| 2 | +## | ||
| 3 | +## detect → fetch what it named → compile against the sysroot. | ||
| 4 | + | ||
| 5 | +import std/[os, osproc, sequtils, strutils, tables] | ||
| 6 | +import ./detect, ./flags, ./index, ./repo, ./sysroot | ||
| 7 | + | ||
| 8 | +type | ||
| 9 | + BuildOpts* = object | ||
| 10 | + root*, cc*, output*, nimExe*, target*: string | ||
| 11 | + nimArgs*: seq[string] | ||
| 12 | + extraMap*: Table[string, string] | ||
| 13 | + extraPackages*: seq[string] | ||
| 14 | + release*, dryRun*, quiet*: bool | ||
| 15 | + | ||
| 16 | + BuildResult* = object | ||
| 17 | + detection*: Detection | ||
| 18 | + fetched*: seq[string] | ||
| 19 | + command*: seq[string] | ||
| 20 | + output*: string | ||
| 21 | + | ||
| 22 | +proc note(o: BuildOpts, msg: string) = | ||
| 23 | + if not o.quiet: stderr.writeLine msg | ||
| 24 | + | ||
| 25 | +proc ensurePackages*(o: BuildOpts, remote: Remote, wanted: seq[string]): seq[string] = | ||
| 26 | + ## Install whatever the sysroot is missing. Already-present packages are not | ||
| 27 | + ## re-downloaded, so a second build is offline. | ||
| 28 | + let missing = wanted.filterIt(not installed(o.root, it)) | ||
| 29 | + if missing.len == 0: return | ||
| 30 | + let | ||
| 31 | + idx = remote.fetchIndex() | ||
| 32 | + pkgs = idx.resolve(missing) | ||
| 33 | + for pkg in pkgs: | ||
| 34 | + unpack(remote.fetchPackage(pkg), o.root) | ||
| 35 | + record(o.root, pkgs) | ||
| 36 | + pkgs.mapIt(it.name) | ||
| 37 | + | ||
| 38 | +proc resolveCc*(o: BuildOpts): string = | ||
| 39 | + ## An explicit --cc wins; otherwise generate a zig wrapper beside the sysroot. | ||
| 40 | + if o.cc.len > 0: return o.cc | ||
| 41 | + if findExe("zig").len == 0: | ||
| 42 | + raise newException(OSError, | ||
| 43 | + "no C compiler for musl: install zig (used as `zig cc -target " & | ||
| 44 | + o.target & "`) or pass --cc with a musl-targeting compiler") | ||
| 45 | + let path = o.root.parentDir / ("zigcc-" & o.target) | ||
| 46 | + writeZigCc(path, o.target) | ||
| 47 | + path | ||
| 48 | + | ||
| 49 | +proc buildCommand*(o: BuildOpts, source: string, detection: Detection, | ||
| 50 | + cc: string): seq[string] = | ||
| 51 | + result = @[o.nimExe, "c"] | ||
| 52 | + if o.release: result.add "-d:release" | ||
| 53 | + result.add o.nimArgs | ||
| 54 | + result.add ["--cc:clang", "--clang.exe:" & cc, "--clang.linkerexe:" & cc] | ||
| 55 | + result.add nimFlags(o.root, dynlibOverrides(detection)) | ||
| 56 | + if o.output.len > 0: | ||
| 57 | + result.add "-o:" & o.output | ||
| 58 | + result.add source | ||
| 59 | + | ||
| 60 | +proc build*(o: BuildOpts, remote: Remote, source: string): BuildResult = | ||
| 61 | + if not fileExists(source): | ||
| 62 | + raise newException(IOError, "no such file: " & source) | ||
| 63 | + | ||
| 64 | + o.note "probing " & source.extractFilename & " …" | ||
| 65 | + result.detection = detect(source, o.nimArgs, o.nimExe, o.extraMap) | ||
| 66 | + let det = result.detection | ||
| 67 | + | ||
| 68 | + if det.needs.len == 0: | ||
| 69 | + o.note "no external libraries needed" | ||
| 70 | + else: | ||
| 71 | + for need in det.needs: | ||
| 72 | + let how = if need.dynlib: "dlopen" else: "link" | ||
| 73 | + o.note " " & need.lib.alignLeft(14) & how.alignLeft(8) & | ||
| 74 | + (if need.package.len > 0: need.package else: "UNMAPPED") | ||
| 75 | + if det.unmapped.len > 0: | ||
| 76 | + o.note "warning: no Alpine package known for: " & det.unmapped.join(", ") & | ||
| 77 | + "\n map it with --map " & det.unmapped[0] & "=<package>" & | ||
| 78 | + ", or the link will fail" | ||
| 79 | + | ||
| 80 | + result.fetched = ensurePackages(o, remote, packages(det) & o.extraPackages) | ||
| 81 | + let cc = resolveCc(o) | ||
| 82 | + result.command = buildCommand(o, source, det, cc) | ||
| 83 | + result.output = if o.output.len > 0: o.output | ||
| 84 | + else: source.changeFileExt("") | ||
| 85 | + | ||
| 86 | + if o.dryRun: | ||
| 87 | + return | ||
| 88 | + o.note "building …" | ||
| 89 | + if execCmd(result.command.quoteShellCommand) != 0: | ||
| 90 | + raise newException(OSError, "the static build failed") | ||
added
src/nimstatic/detect.nim +155 -0 | new file mode 100644 | ||
| @@ -0,0 +1,155 @@ | ||
| 1 | +## Working out what a Nim program actually links against. | |
| 2 | +## | |
| 3 | +## Guessing from `import` lines would be wrong in both directions: a transitive | |
| 4 | +## import three modules deep still needs its library, and an import behind a | |
| 5 | +## `when` that never fires does not. So instead we ask the compiler — a | |
| 6 | +## `--compileOnly --genScript` probe writes a nimcache the real build would have | |
| 7 | +## produced, and that cache states the answer twice over: | |
| 8 | +## | |
| 9 | +## * `<project>.json` carries the link command, so every `-lfoo` is explicit. | |
| 10 | +## * The generated C contains the dynlib candidate strings Nim will `dlopen` | |
| 11 | +## at runtime — `"libssl.so(.3|.1.1|…)"` — which never reach the link line | |
| 12 | +## at all, and are exactly what breaks a static binary. | |
| 13 | + | |
| 14 | +import std/[json, os, osproc, sequtils, sets, strutils, tables] | |
| 15 | + | |
| 16 | +type | |
| 17 | + Need* = object | |
| 18 | + lib*: string ## base name: `ssl`, `crypto`, `pcre` | |
| 19 | + dynlib*: bool ## dlopen'd at runtime rather than linked | |
| 20 | + package*: string ## Alpine package, "" when unmapped | |
| 21 | + | |
| 22 | + Detection* = object | |
| 23 | + needs*: seq[Need] | |
| 24 | + unmapped*: seq[string] | |
| 25 | + nimcache*: string | |
| 26 | + | |
| 27 | +const libcProvided* = ["c", "m", "rt", "dl", "pthread", "util", "resolv", "crypt"] | |
| 28 | + ## Linked by name on glibc, but part of musl itself — asking Alpine for a | |
| 29 | + ## package would find nothing, and none is needed. | |
| 30 | + | |
| 31 | +const packageFor* = { | |
| 32 | + "ssl": "openssl-libs-static", | |
| 33 | + "crypto": "openssl-libs-static", | |
| 34 | + "z": "zlib-static", | |
| 35 | + "bz2": "bzip2-static", | |
| 36 | + "lzma": "xz-static", | |
| 37 | + "zstd": "zstd-static", | |
| 38 | + "pcre": "pcre-dev", # Alpine keeps pcre's .a in -dev | |
| 39 | + "pcre2-8": "pcre2-dev", | |
| 40 | + "sqlite3": "sqlite-static", | |
| 41 | + "curl": "curl-static", | |
| 42 | + "ffi": "libffi-dev", | |
| 43 | + "readline": "readline-static", | |
| 44 | + "ncurses": "ncurses-static", | |
| 45 | + "ncursesw": "ncurses-static", | |
| 46 | + "expat": "expat-static", | |
| 47 | + "xml2": "libxml2-dev", | |
| 48 | + "yaml": "yaml-static", | |
| 49 | + "pq": "libpq-dev", | |
| 50 | + "mysqlclient": "mariadb-dev", | |
| 51 | + "gmp": "gmp-dev", | |
| 52 | + "sodium": "libsodium-static", | |
| 53 | + "uv": "libuv-static", | |
| 54 | + "brotlidec": "brotli-static", | |
| 55 | + "brotlienc": "brotli-static", | |
| 56 | +}.toTable | |
| 57 | + | |
| 58 | +func libFromSoname*(soname: string): string = | |
| 59 | + ## `libssl.so.3` → `ssl`; `libpcre2-8.so(.0|)` → `pcre2-8`. | |
| 60 | + var s = soname | |
| 61 | + if not s.startsWith("lib"): return "" | |
| 62 | + s = s[3 .. ^1] | |
| 63 | + let dot = s.find(".so") | |
| 64 | + if dot < 0: return "" | |
| 65 | + s[0 ..< dot] | |
| 66 | + | |
| 67 | +proc linkLibs*(linkcmd: string): seq[string] = | |
| 68 | + ## Every `-lfoo` on the link line, deduplicated, order preserved. | |
| 69 | + var seen: HashSet[string] | |
| 70 | + for token in linkcmd.splitWhitespace(): | |
| 71 | + if token.startsWith("-l") and token.len > 2: | |
| 72 | + let lib = token[2 .. ^1] | |
| 73 | + if lib notin seen: | |
| 74 | + seen.incl lib | |
| 75 | + result.add lib | |
| 76 | + | |
| 77 | +proc dynlibsIn*(text: string): seq[string] = | |
| 78 | + ## Base library names from the dynlib candidate strings Nim emits into C. | |
| 79 | + ## One `loadLib` site spells out every soname it would try, so the same | |
| 80 | + ## library shows up many times; we want it once. | |
| 81 | + var seen: HashSet[string] | |
| 82 | + var i = 0 | |
| 83 | + while true: | |
| 84 | + let start = text.find("\"lib", i) | |
| 85 | + if start < 0: break | |
| 86 | + let stop = text.find('"', start + 1) | |
| 87 | + if stop < 0: break | |
| 88 | + let lib = libFromSoname(text[start + 1 ..< stop]) | |
| 89 | + if lib.len > 0 and lib notin seen: | |
| 90 | + seen.incl lib | |
| 91 | + result.add lib | |
| 92 | + i = stop + 1 | |
| 93 | + | |
| 94 | +proc probe*(source: string, nimArgs: openArray[string] = [], | |
| 95 | + nimExe = "nim", cacheDir = ""): string = | |
| 96 | + ## Run the compile-only probe; returns the nimcache directory. | |
| 97 | + let cache = if cacheDir.len > 0: cacheDir | |
| 98 | + else: getTempDir() / "nimstatic-probe" / source.extractFilename | |
| 99 | + removeDir cache | |
| 100 | + createDir cache | |
| 101 | + var cmd = @[nimExe, "c", "--compileOnly", "--genScript", | |
| 102 | + "--nimcache:" & cache, "--hints:off"] | |
| 103 | + for a in nimArgs: cmd.add a | |
| 104 | + cmd.add source | |
| 105 | + let (output, code) = execCmdEx(cmd.quoteShellCommand) | |
| 106 | + if code != 0: | |
| 107 | + raise newException(OSError, "the probe compile failed:\n" & output.strip()) | |
| 108 | + cache | |
| 109 | + | |
| 110 | +proc inspect*(cache: string, extraMap: Table[string, string] = initTable[string, string]()): Detection = | |
| 111 | + ## Read a nimcache and say which libraries the program needs. | |
| 112 | + result.nimcache = cache | |
| 113 | + var jsonFile = "" | |
| 114 | + for path in walkFiles(cache / "*.json"): | |
| 115 | + jsonFile = path | |
| 116 | + break | |
| 117 | + if jsonFile.len == 0: | |
| 118 | + raise newException(IOError, "no build json in " & cache & | |
| 119 | + " — did the probe compile run?") | |
| 120 | + | |
| 121 | + var linked: seq[string] | |
| 122 | + let data = parseFile(jsonFile) | |
| 123 | + if data.hasKey("linkcmd"): | |
| 124 | + linked = linkLibs(data["linkcmd"].getStr) | |
| 125 | + | |
| 126 | + var dynamic: seq[string] | |
| 127 | + for path in walkFiles(cache / "*.c"): | |
| 128 | + for lib in dynlibsIn(readFile(path)): | |
| 129 | + if lib notin dynamic: | |
| 130 | + dynamic.add lib | |
| 131 | + | |
| 132 | + for (libs, isDyn) in [(linked, false), (dynamic, true)]: | |
| 133 | + for lib in libs: | |
| 134 | + if lib in libcProvided: continue | |
| 135 | + if result.needs.anyIt(it.lib == lib): continue | |
| 136 | + let pkg = if lib in extraMap: extraMap[lib] | |
| 137 | + else: packageFor.getOrDefault(lib, "") | |
| 138 | + result.needs.add Need(lib: lib, dynlib: isDyn, package: pkg) | |
| 139 | + if pkg.len == 0 and lib notin result.unmapped: | |
| 140 | + result.unmapped.add lib | |
| 141 | + | |
| 142 | +proc packages*(d: Detection): seq[string] = | |
| 143 | + for need in d.needs: | |
| 144 | + if need.package.len > 0 and need.package notin result: | |
| 145 | + result.add need.package | |
| 146 | + | |
| 147 | +proc dynlibOverrides*(d: Detection): seq[string] = | |
| 148 | + ## Libraries Nim would dlopen — each needs --dynlibOverride to link instead. | |
| 149 | + for need in d.needs: | |
| 150 | + if need.dynlib and need.package.len > 0: | |
| 151 | + result.add need.lib | |
| 152 | + | |
| 153 | +proc detect*(source: string, nimArgs: openArray[string] = [], | |
| 154 | + nimExe = "nim", extraMap = initTable[string, string]()): Detection = | |
| 155 | + inspect(probe(source, nimArgs, nimExe), extraMap) | |
| new file mode 100644 | |||
| @@ -0,0 +1,155 @@ | |||
| 1 | +## Working out what a Nim program actually links against. | ||
| 2 | +## | ||
| 3 | +## Guessing from `import` lines would be wrong in both directions: a transitive | ||
| 4 | +## import three modules deep still needs its library, and an import behind a | ||
| 5 | +## `when` that never fires does not. So instead we ask the compiler — a | ||
| 6 | +## `--compileOnly --genScript` probe writes a nimcache the real build would have | ||
| 7 | +## produced, and that cache states the answer twice over: | ||
| 8 | +## | ||
| 9 | +## * `<project>.json` carries the link command, so every `-lfoo` is explicit. | ||
| 10 | +## * The generated C contains the dynlib candidate strings Nim will `dlopen` | ||
| 11 | +## at runtime — `"libssl.so(.3|.1.1|…)"` — which never reach the link line | ||
| 12 | +## at all, and are exactly what breaks a static binary. | ||
| 13 | + | ||
| 14 | +import std/[json, os, osproc, sequtils, sets, strutils, tables] | ||
| 15 | + | ||
| 16 | +type | ||
| 17 | + Need* = object | ||
| 18 | + lib*: string ## base name: `ssl`, `crypto`, `pcre` | ||
| 19 | + dynlib*: bool ## dlopen'd at runtime rather than linked | ||
| 20 | + package*: string ## Alpine package, "" when unmapped | ||
| 21 | + | ||
| 22 | + Detection* = object | ||
| 23 | + needs*: seq[Need] | ||
| 24 | + unmapped*: seq[string] | ||
| 25 | + nimcache*: string | ||
| 26 | + | ||
| 27 | +const libcProvided* = ["c", "m", "rt", "dl", "pthread", "util", "resolv", "crypt"] | ||
| 28 | + ## Linked by name on glibc, but part of musl itself — asking Alpine for a | ||
| 29 | + ## package would find nothing, and none is needed. | ||
| 30 | + | ||
| 31 | +const packageFor* = { | ||
| 32 | + "ssl": "openssl-libs-static", | ||
| 33 | + "crypto": "openssl-libs-static", | ||
| 34 | + "z": "zlib-static", | ||
| 35 | + "bz2": "bzip2-static", | ||
| 36 | + "lzma": "xz-static", | ||
| 37 | + "zstd": "zstd-static", | ||
| 38 | + "pcre": "pcre-dev", # Alpine keeps pcre's .a in -dev | ||
| 39 | + "pcre2-8": "pcre2-dev", | ||
| 40 | + "sqlite3": "sqlite-static", | ||
| 41 | + "curl": "curl-static", | ||
| 42 | + "ffi": "libffi-dev", | ||
| 43 | + "readline": "readline-static", | ||
| 44 | + "ncurses": "ncurses-static", | ||
| 45 | + "ncursesw": "ncurses-static", | ||
| 46 | + "expat": "expat-static", | ||
| 47 | + "xml2": "libxml2-dev", | ||
| 48 | + "yaml": "yaml-static", | ||
| 49 | + "pq": "libpq-dev", | ||
| 50 | + "mysqlclient": "mariadb-dev", | ||
| 51 | + "gmp": "gmp-dev", | ||
| 52 | + "sodium": "libsodium-static", | ||
| 53 | + "uv": "libuv-static", | ||
| 54 | + "brotlidec": "brotli-static", | ||
| 55 | + "brotlienc": "brotli-static", | ||
| 56 | +}.toTable | ||
| 57 | + | ||
| 58 | +func libFromSoname*(soname: string): string = | ||
| 59 | + ## `libssl.so.3` → `ssl`; `libpcre2-8.so(.0|)` → `pcre2-8`. | ||
| 60 | + var s = soname | ||
| 61 | + if not s.startsWith("lib"): return "" | ||
| 62 | + s = s[3 .. ^1] | ||
| 63 | + let dot = s.find(".so") | ||
| 64 | + if dot < 0: return "" | ||
| 65 | + s[0 ..< dot] | ||
| 66 | + | ||
| 67 | +proc linkLibs*(linkcmd: string): seq[string] = | ||
| 68 | + ## Every `-lfoo` on the link line, deduplicated, order preserved. | ||
| 69 | + var seen: HashSet[string] | ||
| 70 | + for token in linkcmd.splitWhitespace(): | ||
| 71 | + if token.startsWith("-l") and token.len > 2: | ||
| 72 | + let lib = token[2 .. ^1] | ||
| 73 | + if lib notin seen: | ||
| 74 | + seen.incl lib | ||
| 75 | + result.add lib | ||
| 76 | + | ||
| 77 | +proc dynlibsIn*(text: string): seq[string] = | ||
| 78 | + ## Base library names from the dynlib candidate strings Nim emits into C. | ||
| 79 | + ## One `loadLib` site spells out every soname it would try, so the same | ||
| 80 | + ## library shows up many times; we want it once. | ||
| 81 | + var seen: HashSet[string] | ||
| 82 | + var i = 0 | ||
| 83 | + while true: | ||
| 84 | + let start = text.find("\"lib", i) | ||
| 85 | + if start < 0: break | ||
| 86 | + let stop = text.find('"', start + 1) | ||
| 87 | + if stop < 0: break | ||
| 88 | + let lib = libFromSoname(text[start + 1 ..< stop]) | ||
| 89 | + if lib.len > 0 and lib notin seen: | ||
| 90 | + seen.incl lib | ||
| 91 | + result.add lib | ||
| 92 | + i = stop + 1 | ||
| 93 | + | ||
| 94 | +proc probe*(source: string, nimArgs: openArray[string] = [], | ||
| 95 | + nimExe = "nim", cacheDir = ""): string = | ||
| 96 | + ## Run the compile-only probe; returns the nimcache directory. | ||
| 97 | + let cache = if cacheDir.len > 0: cacheDir | ||
| 98 | + else: getTempDir() / "nimstatic-probe" / source.extractFilename | ||
| 99 | + removeDir cache | ||
| 100 | + createDir cache | ||
| 101 | + var cmd = @[nimExe, "c", "--compileOnly", "--genScript", | ||
| 102 | + "--nimcache:" & cache, "--hints:off"] | ||
| 103 | + for a in nimArgs: cmd.add a | ||
| 104 | + cmd.add source | ||
| 105 | + let (output, code) = execCmdEx(cmd.quoteShellCommand) | ||
| 106 | + if code != 0: | ||
| 107 | + raise newException(OSError, "the probe compile failed:\n" & output.strip()) | ||
| 108 | + cache | ||
| 109 | + | ||
| 110 | +proc inspect*(cache: string, extraMap: Table[string, string] = initTable[string, string]()): Detection = | ||
| 111 | + ## Read a nimcache and say which libraries the program needs. | ||
| 112 | + result.nimcache = cache | ||
| 113 | + var jsonFile = "" | ||
| 114 | + for path in walkFiles(cache / "*.json"): | ||
| 115 | + jsonFile = path | ||
| 116 | + break | ||
| 117 | + if jsonFile.len == 0: | ||
| 118 | + raise newException(IOError, "no build json in " & cache & | ||
| 119 | + " — did the probe compile run?") | ||
| 120 | + | ||
| 121 | + var linked: seq[string] | ||
| 122 | + let data = parseFile(jsonFile) | ||
| 123 | + if data.hasKey("linkcmd"): | ||
| 124 | + linked = linkLibs(data["linkcmd"].getStr) | ||
| 125 | + | ||
| 126 | + var dynamic: seq[string] | ||
| 127 | + for path in walkFiles(cache / "*.c"): | ||
| 128 | + for lib in dynlibsIn(readFile(path)): | ||
| 129 | + if lib notin dynamic: | ||
| 130 | + dynamic.add lib | ||
| 131 | + | ||
| 132 | + for (libs, isDyn) in [(linked, false), (dynamic, true)]: | ||
| 133 | + for lib in libs: | ||
| 134 | + if lib in libcProvided: continue | ||
| 135 | + if result.needs.anyIt(it.lib == lib): continue | ||
| 136 | + let pkg = if lib in extraMap: extraMap[lib] | ||
| 137 | + else: packageFor.getOrDefault(lib, "") | ||
| 138 | + result.needs.add Need(lib: lib, dynlib: isDyn, package: pkg) | ||
| 139 | + if pkg.len == 0 and lib notin result.unmapped: | ||
| 140 | + result.unmapped.add lib | ||
| 141 | + | ||
| 142 | +proc packages*(d: Detection): seq[string] = | ||
| 143 | + for need in d.needs: | ||
| 144 | + if need.package.len > 0 and need.package notin result: | ||
| 145 | + result.add need.package | ||
| 146 | + | ||
| 147 | +proc dynlibOverrides*(d: Detection): seq[string] = | ||
| 148 | + ## Libraries Nim would dlopen — each needs --dynlibOverride to link instead. | ||
| 149 | + for need in d.needs: | ||
| 150 | + if need.dynlib and need.package.len > 0: | ||
| 151 | + result.add need.lib | ||
| 152 | + | ||
| 153 | +proc detect*(source: string, nimArgs: openArray[string] = [], | ||
| 154 | + nimExe = "nim", extraMap = initTable[string, string]()): Detection = | ||
| 155 | + inspect(probe(source, nimArgs, nimExe), extraMap) | ||
added
src/nimstatic/flags.nim +95 -0 | new file mode 100644 | ||
| @@ -0,0 +1,95 @@ | ||
| 1 | +## Turning a sysroot plus a detection into compiler flags. | |
| 2 | +## | |
| 3 | +## Two things beyond paths are needed for a static Nim binary that talks TLS, | |
| 4 | +## and both are silent failures otherwise: | |
| 5 | +## | |
| 6 | +## * `-d:ssl` (and any other dynlib binding) makes Nim `dlopen` the library at | |
| 7 | +## runtime. A static binary cannot, and dies at startup — even on code paths | |
| 8 | +## that never use it. `--dynlibOverride:<lib>` plus the archive on the link | |
| 9 | +## line is the fix. | |
| 10 | +## * OpenSSL 3 removed `SSL_get_peer_certificate`, which Nim's wrapper still | |
| 11 | +## names, so the link fails on one undefined symbol. | |
| 12 | + | |
| 13 | +import std/[algorithm, os, sequtils, strutils] | |
| 14 | +import ./sysroot | |
| 15 | + | |
| 16 | +proc includeDir*(root: string): string = root / "usr" / "include" | |
| 17 | +proc libDir*(root: string): string = root / "usr" / "lib" | |
| 18 | + | |
| 19 | +proc archive*(root, lib: string): string = libDir(root) / ("lib" & lib & ".a") | |
| 20 | +proc hasLib*(root, lib: string): bool = fileExists(archive(root, lib)) | |
| 21 | + | |
| 22 | +func linkOrder(libs: seq[string]): seq[string] = | |
| 23 | + ## A static linker resolves left to right, so a dependent archive has to come | |
| 24 | + ## before the one it draws from: libssl needs libcrypto, not the other way. | |
| 25 | + const first = ["ssl", "crypto"] | |
| 26 | + result = libs.filterIt(it in first) | |
| 27 | + result.sort(proc (a, b: string): int = cmp(first.find(a), first.find(b))) | |
| 28 | + result.add libs.filterIt(it notin first).sorted() | |
| 29 | + | |
| 30 | +proc overridesIn*(root: string): seq[string] = | |
| 31 | + ## Fallback when nothing was detected: assume archives present in the sysroot | |
| 32 | + ## are there to be linked. | |
| 33 | + for lib in ["ssl", "crypto"]: | |
| 34 | + if hasLib(root, lib): result.add lib | |
| 35 | + | |
| 36 | +proc nimFlags*(root: string, overrides: openArray[string] = [], | |
| 37 | + libs: openArray[string] = [], opensslCompat = true): seq[string] = | |
| 38 | + result = @["--passC:-I" & includeDir(root), | |
| 39 | + "--passL:-L" & libDir(root), | |
| 40 | + "--passL:-static"] | |
| 41 | + let present = linkOrder(overrides.toSeq.filterIt(hasLib(root, it))) | |
| 42 | + for lib in present: | |
| 43 | + result.add "--dynlibOverride:" & lib | |
| 44 | + for lib in present: | |
| 45 | + result.add "--passL:" & archive(root, lib) | |
| 46 | + if opensslCompat and "ssl" in present: | |
| 47 | + # Removed in OpenSSL 3.0; Nim's wrapper still names it. | |
| 48 | + result.add "--passC:-DSSL_get_peer_certificate=SSL_get1_peer_certificate" | |
| 49 | + for lib in libs: | |
| 50 | + result.add "--passL:-l" & lib | |
| 51 | + | |
| 52 | +proc ccFlags*(root: string, libs: openArray[string] = []): seq[string] = | |
| 53 | + result = @["-I" & includeDir(root), "-L" & libDir(root), "-static"] | |
| 54 | + for lib in libs: | |
| 55 | + result.add "-l" & lib | |
| 56 | + | |
| 57 | +proc pkgConfigEnv*(root: string): seq[string] = | |
| 58 | + @["PKG_CONFIG_SYSROOT_DIR=" & root, | |
| 59 | + "PKG_CONFIG_LIBDIR=" & libDir(root) / "pkgconfig"] | |
| 60 | + | |
| 61 | +proc nimCfg*(root, cc: string, overrides: openArray[string] = [], | |
| 62 | + libs: openArray[string] = []): string = | |
| 63 | + ## A nim.cfg fragment: cross-compiler plus every flag from `nimFlags`. | |
| 64 | + result = "# Generated by nimstatic — static musl build against " & root & | |
| 65 | + "\n# Regenerate with: nimstatic nimcfg --root " & root & "\n" | |
| 66 | + if cc.len > 0: | |
| 67 | + result.add "--cc:clang\n" | |
| 68 | + result.add "--clang.exe:\"" & cc & "\"\n" | |
| 69 | + result.add "--clang.linkerexe:\"" & cc & "\"\n" | |
| 70 | + for f in nimFlags(root, overrides, libs): | |
| 71 | + result.add f & "\n" | |
| 72 | + | |
| 73 | +const zigccTemplate* = """#!/bin/sh | |
| 74 | +# Generated by nimstatic: zig as a musl cross-compiler. | |
| 75 | +exec zig cc -target @TARGET@ "$@" | |
| 76 | +""" | |
| 77 | + | |
| 78 | +proc zigccScript*(target: string): string = | |
| 79 | + zigccTemplate.replace("@TARGET@", target) | |
| 80 | + | |
| 81 | +proc writeZigCc*(path, target: string) = | |
| 82 | + createDir path.parentDir | |
| 83 | + writeFile(path, zigccScript(target)) | |
| 84 | + setFilePermissions(path, {fpUserRead, fpUserWrite, fpUserExec, | |
| 85 | + fpGroupRead, fpGroupExec, | |
| 86 | + fpOthersRead, fpOthersExec}) | |
| 87 | + | |
| 88 | +proc describe*(root: string): string = | |
| 89 | + let libs = staticLibs(root) | |
| 90 | + if libs.len == 0: | |
| 91 | + return "no static libraries in " & root & "\n" | |
| 92 | + result = $libs.len & " static libraries in " & libDir(root) & ":\n" | |
| 93 | + for l in libs: | |
| 94 | + result.add " " & l.extractFilename & " (" & | |
| 95 | + $(getFileSize(l) div 1024) & " KiB)\n" | |
| new file mode 100644 | |||
| @@ -0,0 +1,95 @@ | |||
| 1 | +## Turning a sysroot plus a detection into compiler flags. | ||
| 2 | +## | ||
| 3 | +## Two things beyond paths are needed for a static Nim binary that talks TLS, | ||
| 4 | +## and both are silent failures otherwise: | ||
| 5 | +## | ||
| 6 | +## * `-d:ssl` (and any other dynlib binding) makes Nim `dlopen` the library at | ||
| 7 | +## runtime. A static binary cannot, and dies at startup — even on code paths | ||
| 8 | +## that never use it. `--dynlibOverride:<lib>` plus the archive on the link | ||
| 9 | +## line is the fix. | ||
| 10 | +## * OpenSSL 3 removed `SSL_get_peer_certificate`, which Nim's wrapper still | ||
| 11 | +## names, so the link fails on one undefined symbol. | ||
| 12 | + | ||
| 13 | +import std/[algorithm, os, sequtils, strutils] | ||
| 14 | +import ./sysroot | ||
| 15 | + | ||
| 16 | +proc includeDir*(root: string): string = root / "usr" / "include" | ||
| 17 | +proc libDir*(root: string): string = root / "usr" / "lib" | ||
| 18 | + | ||
| 19 | +proc archive*(root, lib: string): string = libDir(root) / ("lib" & lib & ".a") | ||
| 20 | +proc hasLib*(root, lib: string): bool = fileExists(archive(root, lib)) | ||
| 21 | + | ||
| 22 | +func linkOrder(libs: seq[string]): seq[string] = | ||
| 23 | + ## A static linker resolves left to right, so a dependent archive has to come | ||
| 24 | + ## before the one it draws from: libssl needs libcrypto, not the other way. | ||
| 25 | + const first = ["ssl", "crypto"] | ||
| 26 | + result = libs.filterIt(it in first) | ||
| 27 | + result.sort(proc (a, b: string): int = cmp(first.find(a), first.find(b))) | ||
| 28 | + result.add libs.filterIt(it notin first).sorted() | ||
| 29 | + | ||
| 30 | +proc overridesIn*(root: string): seq[string] = | ||
| 31 | + ## Fallback when nothing was detected: assume archives present in the sysroot | ||
| 32 | + ## are there to be linked. | ||
| 33 | + for lib in ["ssl", "crypto"]: | ||
| 34 | + if hasLib(root, lib): result.add lib | ||
| 35 | + | ||
| 36 | +proc nimFlags*(root: string, overrides: openArray[string] = [], | ||
| 37 | + libs: openArray[string] = [], opensslCompat = true): seq[string] = | ||
| 38 | + result = @["--passC:-I" & includeDir(root), | ||
| 39 | + "--passL:-L" & libDir(root), | ||
| 40 | + "--passL:-static"] | ||
| 41 | + let present = linkOrder(overrides.toSeq.filterIt(hasLib(root, it))) | ||
| 42 | + for lib in present: | ||
| 43 | + result.add "--dynlibOverride:" & lib | ||
| 44 | + for lib in present: | ||
| 45 | + result.add "--passL:" & archive(root, lib) | ||
| 46 | + if opensslCompat and "ssl" in present: | ||
| 47 | + # Removed in OpenSSL 3.0; Nim's wrapper still names it. | ||
| 48 | + result.add "--passC:-DSSL_get_peer_certificate=SSL_get1_peer_certificate" | ||
| 49 | + for lib in libs: | ||
| 50 | + result.add "--passL:-l" & lib | ||
| 51 | + | ||
| 52 | +proc ccFlags*(root: string, libs: openArray[string] = []): seq[string] = | ||
| 53 | + result = @["-I" & includeDir(root), "-L" & libDir(root), "-static"] | ||
| 54 | + for lib in libs: | ||
| 55 | + result.add "-l" & lib | ||
| 56 | + | ||
| 57 | +proc pkgConfigEnv*(root: string): seq[string] = | ||
| 58 | + @["PKG_CONFIG_SYSROOT_DIR=" & root, | ||
| 59 | + "PKG_CONFIG_LIBDIR=" & libDir(root) / "pkgconfig"] | ||
| 60 | + | ||
| 61 | +proc nimCfg*(root, cc: string, overrides: openArray[string] = [], | ||
| 62 | + libs: openArray[string] = []): string = | ||
| 63 | + ## A nim.cfg fragment: cross-compiler plus every flag from `nimFlags`. | ||
| 64 | + result = "# Generated by nimstatic — static musl build against " & root & | ||
| 65 | + "\n# Regenerate with: nimstatic nimcfg --root " & root & "\n" | ||
| 66 | + if cc.len > 0: | ||
| 67 | + result.add "--cc:clang\n" | ||
| 68 | + result.add "--clang.exe:\"" & cc & "\"\n" | ||
| 69 | + result.add "--clang.linkerexe:\"" & cc & "\"\n" | ||
| 70 | + for f in nimFlags(root, overrides, libs): | ||
| 71 | + result.add f & "\n" | ||
| 72 | + | ||
| 73 | +const zigccTemplate* = """#!/bin/sh | ||
| 74 | +# Generated by nimstatic: zig as a musl cross-compiler. | ||
| 75 | +exec zig cc -target @TARGET@ "$@" | ||
| 76 | +""" | ||
| 77 | + | ||
| 78 | +proc zigccScript*(target: string): string = | ||
| 79 | + zigccTemplate.replace("@TARGET@", target) | ||
| 80 | + | ||
| 81 | +proc writeZigCc*(path, target: string) = | ||
| 82 | + createDir path.parentDir | ||
| 83 | + writeFile(path, zigccScript(target)) | ||
| 84 | + setFilePermissions(path, {fpUserRead, fpUserWrite, fpUserExec, | ||
| 85 | + fpGroupRead, fpGroupExec, | ||
| 86 | + fpOthersRead, fpOthersExec}) | ||
| 87 | + | ||
| 88 | +proc describe*(root: string): string = | ||
| 89 | + let libs = staticLibs(root) | ||
| 90 | + if libs.len == 0: | ||
| 91 | + return "no static libraries in " & root & "\n" | ||
| 92 | + result = $libs.len & " static libraries in " & libDir(root) & ":\n" | ||
| 93 | + for l in libs: | ||
| 94 | + result.add " " & l.extractFilename & " (" & | ||
| 95 | + $(getFileSize(l) div 1024) & " KiB)\n" | ||
renamed
src/nimstatic/index.nim +0 -0 | similarity index 100% | ||
| rename from src/muslkit/index.nim | ||
| rename to src/nimstatic/index.nim |
| similarity index 100% | |||
| rename from src/muslkit/index.nim | |||
| rename to src/nimstatic/index.nim |
renamed
src/nimstatic/repo.nim +2 -2 | similarity index 97% | ||
| rename from src/muslkit/repo.nim | ||
| rename to src/nimstatic/repo.nim | ||
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | ## Talking to an Alpine mirror: index fetch, package download, local caching. |
| 2 | 2 | ## |
| 3 | -## Everything lands in a cache directory first, so a re-run of `muslkit add` is | |
| 3 | +## Everything lands in a cache directory first, so a re-run of `nimstatic add` is | |
| 4 | 4 | ## offline and a sysroot can be rebuilt without touching the network. |
| 5 | 5 | |
| 6 | 6 | import std/[httpclient, os, strutils, times] |
| @@ -24,7 +24,7 @@ proc initRemote*(mirror = defaultMirror, branch = defaultBranch, | ||
| 24 | 24 | cacheDir = "", quiet = false): Remote = |
| 25 | 25 | Remote(mirror: mirror, branch: branch, arch: arch, repos: repos, |
| 26 | 26 | cacheDir: if cacheDir.len > 0: cacheDir |
| 27 | - else: getCacheDir() / "muslkit", | |
| 27 | + else: getCacheDir() / "nimstatic", | |
| 28 | 28 | quiet: quiet) |
| 29 | 29 | |
| 30 | 30 | proc repoUrl*(r: Remote, repo: string): string = |
| similarity index 97% | |||
| rename from src/muslkit/repo.nim | |||
| rename to src/nimstatic/repo.nim | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | ## Talking to an Alpine mirror: index fetch, package download, local caching. | 1 | ## Talking to an Alpine mirror: index fetch, package download, local caching. |
| 2 | ## | 2 | ## |
| 3 | -## Everything lands in a cache directory first, so a re-run of `muslkit add` is | 3 | +## Everything lands in a cache directory first, so a re-run of `nimstatic add` is |
| 4 | ## offline and a sysroot can be rebuilt without touching the network. | 4 | ## offline and a sysroot can be rebuilt without touching the network. |
| 5 | 5 | ||
| 6 | import std/[httpclient, os, strutils, times] | 6 | import std/[httpclient, os, strutils, times] |
| @@ -24,7 +24,7 @@ proc initRemote*(mirror = defaultMirror, branch = defaultBranch, | |||
| 24 | cacheDir = "", quiet = false): Remote = | 24 | cacheDir = "", quiet = false): Remote = |
| 25 | Remote(mirror: mirror, branch: branch, arch: arch, repos: repos, | 25 | Remote(mirror: mirror, branch: branch, arch: arch, repos: repos, |
| 26 | cacheDir: if cacheDir.len > 0: cacheDir | 26 | cacheDir: if cacheDir.len > 0: cacheDir |
| 27 | - else: getCacheDir() / "muslkit", | 27 | + else: getCacheDir() / "nimstatic", |
| 28 | quiet: quiet) | 28 | quiet: quiet) |
| 29 | 29 | ||
| 30 | proc repoUrl*(r: Remote, repo: string): string = | 30 | proc repoUrl*(r: Remote, repo: string): string = |
renamed
src/nimstatic/sysroot.nim +2 -2 | similarity index 93% | ||
| rename from src/muslkit/sysroot.nim | ||
| rename to src/nimstatic/sysroot.nim | ||
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | import std/[algorithm, os, sequtils, strutils] |
| 5 | 5 | import ./index |
| 6 | 6 | |
| 7 | -const manifestPath* = ".muslkit/installed.tsv" | |
| 7 | +const manifestPath* = ".nimstatic/installed.tsv" | |
| 8 | 8 | |
| 9 | 9 | type Installed* = object |
| 10 | 10 | name*, version*, repo*: string |
| @@ -22,7 +22,7 @@ proc readManifest*(sysroot: string): seq[Installed] = | ||
| 22 | 22 | |
| 23 | 23 | proc writeManifest*(sysroot: string, entries: seq[Installed]) = |
| 24 | 24 | createDir manifestFile(sysroot).parentDir |
| 25 | - var lines = @["# name\tversion\trepo — written by muslkit"] | |
| 25 | + var lines = @["# name\tversion\trepo — written by nimstatic"] | |
| 26 | 26 | for e in entries.sortedByIt(it.name): |
| 27 | 27 | lines.add e.name & "\t" & e.version & "\t" & e.repo |
| 28 | 28 | writeFile(manifestFile(sysroot), lines.join("\n") & "\n") |
| similarity index 93% | |||
| rename from src/muslkit/sysroot.nim | |||
| rename to src/nimstatic/sysroot.nim | |||
| @@ -4,7 +4,7 @@ | |||
| 4 | import std/[algorithm, os, sequtils, strutils] | 4 | import std/[algorithm, os, sequtils, strutils] |
| 5 | import ./index | 5 | import ./index |
| 6 | 6 | ||
| 7 | -const manifestPath* = ".muslkit/installed.tsv" | 7 | +const manifestPath* = ".nimstatic/installed.tsv" |
| 8 | 8 | ||
| 9 | type Installed* = object | 9 | type Installed* = object |
| 10 | name*, version*, repo*: string | 10 | name*, version*, repo*: string |
| @@ -22,7 +22,7 @@ proc readManifest*(sysroot: string): seq[Installed] = | |||
| 22 | 22 | ||
| 23 | proc writeManifest*(sysroot: string, entries: seq[Installed]) = | 23 | proc writeManifest*(sysroot: string, entries: seq[Installed]) = |
| 24 | createDir manifestFile(sysroot).parentDir | 24 | createDir manifestFile(sysroot).parentDir |
| 25 | - var lines = @["# name\tversion\trepo — written by muslkit"] | 25 | + var lines = @["# name\tversion\trepo — written by nimstatic"] |
| 26 | for e in entries.sortedByIt(it.name): | 26 | for e in entries.sortedByIt(it.name): |
| 27 | lines.add e.name & "\t" & e.version & "\t" & e.repo | 27 | lines.add e.name & "\t" & e.version & "\t" & e.repo |
| 28 | writeFile(manifestFile(sysroot), lines.join("\n") & "\n") | 28 | writeFile(manifestFile(sysroot), lines.join("\n") & "\n") |
added
tests/test_muslkit +0 -0 | new file mode 100755 | ||
| Binary files /dev/null and b/tests/test_muslkit differ | ||
| new file mode 100755 | |||
| Binary files /dev/null and b/tests/test_muslkit differ | Binary files /dev/null and b/tests/test_muslkit differ | ||
renamed
tests/test_nimstatic.nim +64 -5 | similarity index 67% | ||
| rename from tests/test_muslkit.nim | ||
| rename to tests/test_nimstatic.nim | ||
| @@ -1,5 +1,5 @@ | ||
| 1 | -import std/[os, strutils, tables, unittest] | |
| 2 | -import ../src/muslkit | |
| 1 | +import std/[os, sequtils, strutils, tables, unittest] | |
| 2 | +import ../src/nimstatic | |
| 3 | 3 | |
| 4 | 4 | const sampleIndex = """ |
| 5 | 5 | C:Q1aaa= |
| @@ -110,7 +110,7 @@ suite "resolution": | ||
| 110 | 110 | check a.packages["foo"].repo == "community" |
| 111 | 111 | |
| 112 | 112 | suite "sysroot manifest": |
| 113 | - let root = getTempDir() / "muslkit-test-root" | |
| 113 | + let root = getTempDir() / "nimstatic-test-root" | |
| 114 | 114 | removeDir root |
| 115 | 115 | |
| 116 | 116 | test "record then read back": |
| @@ -139,7 +139,7 @@ suite "sysroot manifest": | ||
| 139 | 139 | removeDir root |
| 140 | 140 | |
| 141 | 141 | suite "flags": |
| 142 | - let root = getTempDir() / "muslkit-test-flags" | |
| 142 | + let root = getTempDir() / "nimstatic-test-flags" | |
| 143 | 143 | removeDir root |
| 144 | 144 | createDir root / "usr" / "lib" |
| 145 | 145 | |
| @@ -152,7 +152,7 @@ suite "flags": | ||
| 152 | 152 | test "openssl archives switch on the static-TLS workarounds": |
| 153 | 153 | for name in ["libssl.a", "libcrypto.a"]: |
| 154 | 154 | writeFile(root / "usr" / "lib" / name, "") |
| 155 | - let f = nimFlags(root, ["z"]).join(" ") | |
| 155 | + let f = nimFlags(root, overridesIn(root), ["z"]).join(" ") | |
| 156 | 156 | check "--dynlibOverride:ssl" in f |
| 157 | 157 | check "--dynlibOverride:crypto" in f |
| 158 | 158 | # OpenSSL 3 dropped the symbol Nim's wrapper still names |
| @@ -165,6 +165,15 @@ suite "flags": | ||
| 165 | 165 | "-static", "-lssl"] |
| 166 | 166 | check pkgConfigEnv(root)[0] == "PKG_CONFIG_SYSROOT_DIR=" & root |
| 167 | 167 | |
| 168 | + test "an override with no archive in the sysroot is dropped": | |
| 169 | + # Asking Nim not to dlopen a library we cannot link would break the build. | |
| 170 | + let f = nimFlags(root, ["ghost"]).join(" ") | |
| 171 | + check "--dynlibOverride:ghost" notin f | |
| 172 | + | |
| 173 | + test "libssl links before libcrypto": | |
| 174 | + let f = nimFlags(root, ["crypto", "ssl"]).join(" ") | |
| 175 | + check f.find("libssl.a") < f.find("libcrypto.a") | |
| 176 | + | |
| 168 | 177 | test "nimcfg carries the compiler and the flags": |
| 169 | 178 | let cfg = nimCfg(root, "/tmp/zigcc") |
| 170 | 179 | check "--cc:clang" in cfg |
| @@ -185,3 +194,53 @@ suite "remote": | ||
| 185 | 194 | "https://dl-cdn.alpinelinux.org/alpine/edge/community/aarch64" |
| 186 | 195 | check apkFile(Pkg(name: "zlib-static", version: "1.3.2-r0")) == |
| 187 | 196 | "zlib-static-1.3.2-r0.apk" |
| 197 | + | |
| 198 | +suite "detection": | |
| 199 | + test "sonames reduce to library names": | |
| 200 | + check libFromSoname("libssl.so.3") == "ssl" | |
| 201 | + check libFromSoname("libpcre2-8.so.0") == "pcre2-8" | |
| 202 | + check libFromSoname("libcrypto.so(.3|.1.1|.10|)") == "crypto" | |
| 203 | + check libFromSoname("notalib") == "" | |
| 204 | + check libFromSoname("libnoso") == "" | |
| 205 | + | |
| 206 | + test "link libraries come off the link command, once each": | |
| 207 | + let cmd = "gcc -o app a.o b.o -pthread -lm -lm -lrt -lpcre -ldl" | |
| 208 | + check linkLibs(cmd) == @["m", "rt", "pcre", "dl"] | |
| 209 | + | |
| 210 | + test "dynlib candidates are found in generated C": | |
| 211 | + let c = """ | |
| 212 | + static char* sslCandidates[] = {"libssl.so.3", "libssl.so.1.1"}; | |
| 213 | + static char* cryptoCandidates[] = {"libcrypto.so(.3|.1.1|)"}; | |
| 214 | + const char* unrelated = "libera chat"; | |
| 215 | + """ | |
| 216 | + check dynlibsIn(c) == @["ssl", "crypto"] | |
| 217 | + | |
| 218 | + test "inspect maps libraries to packages and flags the rest": | |
| 219 | + let cache = getTempDir() / "nimstatic-test-cache" | |
| 220 | + removeDir cache | |
| 221 | + createDir cache | |
| 222 | + writeFile(cache / "app.json", """{"linkcmd": "gcc -o app a.o -lm -lsqlite3"}""") | |
| 223 | + writeFile(cache / "app.c", """char* c[] = {"libssl.so.3", "libmystery.so.1"};""") | |
| 224 | + let d = inspect(cache) | |
| 225 | + | |
| 226 | + # libc's own are never requested | |
| 227 | + check not d.needs.anyIt(it.lib == "m") | |
| 228 | + check d.needs.anyIt(it.lib == "sqlite3" and not it.dynlib) | |
| 229 | + check d.needs.anyIt(it.lib == "ssl" and it.dynlib) | |
| 230 | + check "openssl-libs-static" in packages(d) | |
| 231 | + check "sqlite-static" in packages(d) | |
| 232 | + check d.unmapped == @["mystery"] | |
| 233 | + # only dlopen'd libraries need the override | |
| 234 | + check dynlibOverrides(d) == @["ssl"] | |
| 235 | + removeDir cache | |
| 236 | + | |
| 237 | + test "--map overrides the built-in table": | |
| 238 | + let cache = getTempDir() / "nimstatic-test-cache2" | |
| 239 | + removeDir cache | |
| 240 | + createDir cache | |
| 241 | + writeFile(cache / "app.json", """{"linkcmd": "gcc -o app a.o"}""") | |
| 242 | + writeFile(cache / "app.c", """char* c[] = {"libmystery.so.1"};""") | |
| 243 | + let d = inspect(cache, {"mystery": "mystery-static"}.toTable) | |
| 244 | + check packages(d) == @["mystery-static"] | |
| 245 | + check d.unmapped.len == 0 | |
| 246 | + removeDir cache | |
| similarity index 67% | |||
| rename from tests/test_muslkit.nim | |||
| rename to tests/test_nimstatic.nim | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | -import std/[os, strutils, tables, unittest] | 1 | +import std/[os, sequtils, strutils, tables, unittest] |
| 2 | -import ../src/muslkit | 2 | +import ../src/nimstatic |
| 3 | 3 | ||
| 4 | const sampleIndex = """ | 4 | const sampleIndex = """ |
| 5 | C:Q1aaa= | 5 | C:Q1aaa= |
| @@ -110,7 +110,7 @@ suite "resolution": | |||
| 110 | check a.packages["foo"].repo == "community" | 110 | check a.packages["foo"].repo == "community" |
| 111 | 111 | ||
| 112 | suite "sysroot manifest": | 112 | suite "sysroot manifest": |
| 113 | - let root = getTempDir() / "muslkit-test-root" | 113 | + let root = getTempDir() / "nimstatic-test-root" |
| 114 | removeDir root | 114 | removeDir root |
| 115 | 115 | ||
| 116 | test "record then read back": | 116 | test "record then read back": |
| @@ -139,7 +139,7 @@ suite "sysroot manifest": | |||
| 139 | removeDir root | 139 | removeDir root |
| 140 | 140 | ||
| 141 | suite "flags": | 141 | suite "flags": |
| 142 | - let root = getTempDir() / "muslkit-test-flags" | 142 | + let root = getTempDir() / "nimstatic-test-flags" |
| 143 | removeDir root | 143 | removeDir root |
| 144 | createDir root / "usr" / "lib" | 144 | createDir root / "usr" / "lib" |
| 145 | 145 | ||
| @@ -152,7 +152,7 @@ suite "flags": | |||
| 152 | test "openssl archives switch on the static-TLS workarounds": | 152 | test "openssl archives switch on the static-TLS workarounds": |
| 153 | for name in ["libssl.a", "libcrypto.a"]: | 153 | for name in ["libssl.a", "libcrypto.a"]: |
| 154 | writeFile(root / "usr" / "lib" / name, "") | 154 | writeFile(root / "usr" / "lib" / name, "") |
| 155 | - let f = nimFlags(root, ["z"]).join(" ") | 155 | + let f = nimFlags(root, overridesIn(root), ["z"]).join(" ") |
| 156 | check "--dynlibOverride:ssl" in f | 156 | check "--dynlibOverride:ssl" in f |
| 157 | check "--dynlibOverride:crypto" in f | 157 | check "--dynlibOverride:crypto" in f |
| 158 | # OpenSSL 3 dropped the symbol Nim's wrapper still names | 158 | # OpenSSL 3 dropped the symbol Nim's wrapper still names |
| @@ -165,6 +165,15 @@ suite "flags": | |||
| 165 | "-static", "-lssl"] | 165 | "-static", "-lssl"] |
| 166 | check pkgConfigEnv(root)[0] == "PKG_CONFIG_SYSROOT_DIR=" & root | 166 | check pkgConfigEnv(root)[0] == "PKG_CONFIG_SYSROOT_DIR=" & root |
| 167 | 167 | ||
| 168 | + test "an override with no archive in the sysroot is dropped": | ||
| 169 | + # Asking Nim not to dlopen a library we cannot link would break the build. | ||
| 170 | + let f = nimFlags(root, ["ghost"]).join(" ") | ||
| 171 | + check "--dynlibOverride:ghost" notin f | ||
| 172 | + | ||
| 173 | + test "libssl links before libcrypto": | ||
| 174 | + let f = nimFlags(root, ["crypto", "ssl"]).join(" ") | ||
| 175 | + check f.find("libssl.a") < f.find("libcrypto.a") | ||
| 176 | + | ||
| 168 | test "nimcfg carries the compiler and the flags": | 177 | test "nimcfg carries the compiler and the flags": |
| 169 | let cfg = nimCfg(root, "/tmp/zigcc") | 178 | let cfg = nimCfg(root, "/tmp/zigcc") |
| 170 | check "--cc:clang" in cfg | 179 | check "--cc:clang" in cfg |
| @@ -185,3 +194,53 @@ suite "remote": | |||
| 185 | "https://dl-cdn.alpinelinux.org/alpine/edge/community/aarch64" | 194 | "https://dl-cdn.alpinelinux.org/alpine/edge/community/aarch64" |
| 186 | check apkFile(Pkg(name: "zlib-static", version: "1.3.2-r0")) == | 195 | check apkFile(Pkg(name: "zlib-static", version: "1.3.2-r0")) == |
| 187 | "zlib-static-1.3.2-r0.apk" | 196 | "zlib-static-1.3.2-r0.apk" |
| 197 | + | ||
| 198 | +suite "detection": | ||
| 199 | + test "sonames reduce to library names": | ||
| 200 | + check libFromSoname("libssl.so.3") == "ssl" | ||
| 201 | + check libFromSoname("libpcre2-8.so.0") == "pcre2-8" | ||
| 202 | + check libFromSoname("libcrypto.so(.3|.1.1|.10|)") == "crypto" | ||
| 203 | + check libFromSoname("notalib") == "" | ||
| 204 | + check libFromSoname("libnoso") == "" | ||
| 205 | + | ||
| 206 | + test "link libraries come off the link command, once each": | ||
| 207 | + let cmd = "gcc -o app a.o b.o -pthread -lm -lm -lrt -lpcre -ldl" | ||
| 208 | + check linkLibs(cmd) == @["m", "rt", "pcre", "dl"] | ||
| 209 | + | ||
| 210 | + test "dynlib candidates are found in generated C": | ||
| 211 | + let c = """ | ||
| 212 | + static char* sslCandidates[] = {"libssl.so.3", "libssl.so.1.1"}; | ||
| 213 | + static char* cryptoCandidates[] = {"libcrypto.so(.3|.1.1|)"}; | ||
| 214 | + const char* unrelated = "libera chat"; | ||
| 215 | + """ | ||
| 216 | + check dynlibsIn(c) == @["ssl", "crypto"] | ||
| 217 | + | ||
| 218 | + test "inspect maps libraries to packages and flags the rest": | ||
| 219 | + let cache = getTempDir() / "nimstatic-test-cache" | ||
| 220 | + removeDir cache | ||
| 221 | + createDir cache | ||
| 222 | + writeFile(cache / "app.json", """{"linkcmd": "gcc -o app a.o -lm -lsqlite3"}""") | ||
| 223 | + writeFile(cache / "app.c", """char* c[] = {"libssl.so.3", "libmystery.so.1"};""") | ||
| 224 | + let d = inspect(cache) | ||
| 225 | + | ||
| 226 | + # libc's own are never requested | ||
| 227 | + check not d.needs.anyIt(it.lib == "m") | ||
| 228 | + check d.needs.anyIt(it.lib == "sqlite3" and not it.dynlib) | ||
| 229 | + check d.needs.anyIt(it.lib == "ssl" and it.dynlib) | ||
| 230 | + check "openssl-libs-static" in packages(d) | ||
| 231 | + check "sqlite-static" in packages(d) | ||
| 232 | + check d.unmapped == @["mystery"] | ||
| 233 | + # only dlopen'd libraries need the override | ||
| 234 | + check dynlibOverrides(d) == @["ssl"] | ||
| 235 | + removeDir cache | ||
| 236 | + | ||
| 237 | + test "--map overrides the built-in table": | ||
| 238 | + let cache = getTempDir() / "nimstatic-test-cache2" | ||
| 239 | + removeDir cache | ||
| 240 | + createDir cache | ||
| 241 | + writeFile(cache / "app.json", """{"linkcmd": "gcc -o app a.o"}""") | ||
| 242 | + writeFile(cache / "app.c", """char* c[] = {"libmystery.so.1"};""") | ||
| 243 | + let d = inspect(cache, {"mystery": "mystery-static"}.toTable) | ||
| 244 | + check packages(d) == @["mystery-static"] | ||
| 245 | + check d.unmapped.len == 0 | ||
| 246 | + removeDir cache | ||