nimstatic
Fully static Nim binaries, dependencies and all.
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:
<project>.jsoncarries the link command, so every-lfoois 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:sslmakes Nimdlopenlibssl at runtime. A static binary cannot,
and dies at startup withcould not load: libcrypto.so(...)— even on code
paths that never touch the network. Fix:--dynlibOverride:ssl --dynlibOverride:cryptoplus 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 <file.nim> [-- <nim args>] Detect, fetch, build static
nimstatic detect <file.nim> Show what it needs, change nothing
nimstatic add <pkg>... Put packages in the sysroot by hand
nimstatic list Show what the sysroot holds
nimstatic libs Show the sysroot's static libraries
nimstatic search <text> Search Alpine's index
nimstatic show <pkg> 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:
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:
nimstatic app.nim --arch aarch64 -o app-arm64
Requirements
- zig on PATH — used as
zig cc -target x86_64-linux-musl. Pass--ccto
use a musl cross-compiler you already have instead. - tar — an
.apkis concatenated gzip streams, which GNU tar reads.
Using it as a library
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
nimble install
or build in place with nim c -d:ssl -o:nimstatic src/nimstatic.nim.
Tests
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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
|