# nimstatic Fully static Nim binaries, dependencies and all. ```bash nimstatic app.nim ``` That's the whole thing. nimstatic asks the Nim compiler what `app.nim` actually needs, fetches musl-built static libraries for it from Alpine's mirrors, and compiles against a sysroot it owns. No apk, no container, no root, no Nix. ``` $ nimstatic freeqsay.nim -- -d:ssl probing freeqsay.nim … ssl dlopen openssl-libs-static crypto dlopen openssl-libs-static downloading openssl-libs-static 3.3.7-r1 (12734 KiB) building … wrote freeqsay (20687 KiB, static) ``` The result runs anywhere with a Linux kernel — `ldd` says *not a dynamic executable* — and its HTTPS still works. ## How it knows what you need Guessing from `import` lines would be wrong in both directions: a transitive import three modules deep still needs its library, and an import behind a `when` that never fires does not. So nimstatic asks the compiler instead. A `--compileOnly --genScript` probe produces the nimcache your real build would have, and that cache answers twice over: - **`.json`** carries the link command, so every `-lfoo` is explicit. - **The generated C** contains the dynlib candidate strings Nim will `dlopen` at runtime — `"libssl.so(.3|.1.1|…)"`. These never appear on a link line, and they are exactly what breaks a static binary. Libraries musl already provides (`m`, `rt`, `dl`, `pthread`, …) are skipped. Anything else is looked up in a table of Alpine packages; whatever isn't mapped is reported rather than silently dropped: ``` $ nimstatic detect app.nim library how alpine package sqlite3 link sqlite-static ssl dlopen openssl-libs-static mystery dlopen (unmapped) unmapped: mystery search for one with: nimstatic search mystery ``` Then `--map mystery=mystery-static` teaches it, for that build. ## The two things that silently break static Nim Both are handled automatically; they're documented here because they cost everyone an afternoon at least once. - **`-d:ssl` makes Nim `dlopen` libssl at runtime.** A static binary cannot, and dies at startup with `could not load: libcrypto.so(...)` — even on code paths that never touch the network. Fix: `--dynlibOverride:ssl --dynlibOverride:crypto` plus the archives on the link line. nimstatic emits an override only for libraries it actually has an archive for, since an override without one turns a runtime failure into a link failure. - **OpenSSL 3 removed `SSL_get_peer_certificate`,** which Nim's wrapper still names, so the link dies on one undefined symbol. Fix: `-DSSL_get_peer_certificate=SSL_get1_peer_certificate`. A static binary also carries no CA trust store, so set `SSL_CERT_FILE` on the host that runs it. ## Usage ``` nimstatic [-- ] Detect, fetch, build static nimstatic detect Show what it needs, change nothing nimstatic add ... Put packages in the sysroot by hand nimstatic list Show what the sysroot holds nimstatic libs Show the sysroot's static libraries nimstatic search Search Alpine's index nimstatic show Index record for one package nimstatic nimflags [-l lib] Print nim flags for the sysroot nimstatic ccflags [-l lib] Print cc/clang flags nimstatic nimcfg [-o file] Write a nim.cfg fragment nimstatic zigcc [-o file] Write a `zig cc -target …-musl` wrapper nimstatic env Shell exports (PKG_CONFIG_*, NIMSTATIC_ROOT) nimstatic path Print the sysroot path nimstatic clean Remove the sysroot (cache is kept) ``` Everything after `--` goes to the Nim compiler for **both** the probe and the build, so conditional imports resolve the same way twice: ```bash nimstatic app.nim -- -d:ssl -d:danger ``` Build options: `-o/--output`, `-d/--debug` (skip `-d:release`), `-n/--dry-run` (print the command instead of running it), `--map lib=pkg`, `--pkg name`, `--cc path`, `--nim path`. Sysroot options: `-r/--root`, `-b/--branch` (default `v3.21`, `edge` for rolling), `-a/--arch`, `-m/--mirror`, `--repo main,community`, `-l/--lib`, `--no-deps`, `--refresh`, `-q/--quiet`. The sysroot defaults to `$XDG_DATA_HOME/nimstatic/sysroot` and honors `NIMSTATIC_ROOT`. Downloads cache under `$XDG_CACHE_HOME/nimstatic`, so the second build is offline and the index is re-fetched once a day. ## Cross-compiling Same command, one flag — the sysroot, the packages and the zig target all follow `--arch`: ```bash nimstatic app.nim --arch aarch64 -o app-arm64 ``` ## Requirements - **zig** on PATH — used as `zig cc -target x86_64-linux-musl`. Pass `--cc` to use a musl cross-compiler you already have instead. - **tar** — an `.apk` is concatenated gzip streams, which GNU tar reads. ## Using it as a library ```nim import nimstatic let d = detect("app.nim", ["-d:ssl"]) echo packages(d) # @["openssl-libs-static"] echo dynlibOverrides(d) # @["ssl", "crypto"] let remote = initRemote(branch = "edge") for pkg in remote.fetchIndex().resolve(packages(d)): echo pkg.name, " ", pkg.version, " ", remote.fetchPackage(pkg) ``` Modules: `nimstatic/detect` (probe, parse, map), `nimstatic/build` (the one-command path), `nimstatic/index` (APKINDEX parsing, provides and dependency resolution), `nimstatic/repo` (mirror, cache, download, unpack), `nimstatic/sysroot` (manifest, static-lib discovery), `nimstatic/flags` (flag emission). ## Trust Packages come over HTTPS from the mirror and are unpacked as-is. nimstatic checks the size recorded in the index but does **not** verify Alpine's RSA signatures — apk's checksum field covers a package's control segment rather than the file, so a real check means implementing apk's signature format. Treat a sysroot as build input, not as a trust root. If that matters, pin a mirror you run. ## Install ```bash nimble install ``` or build in place with `nim c -d:ssl -o:nimstatic src/nimstatic.nim`. ## Tests ```bash nimble test ``` 25 tests, all offline: soname parsing, link-command parsing, dynlib discovery in generated C, package mapping, APKINDEX parsing, dependency resolution through `so:`/`pkgconfig` provides, manifest round-trips and flag emission (including link order and the dropped-override case).