nandi/nimstaticpublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/nimstatic.git
git clone ssh://git@rickub.com/nandi/nimstatic.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
README: install from the release assets bc5f3d7 · on main · nandi · 24d ago
README.md

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>.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 <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 --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

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

Grab the static binary from the latest release:

curl -sSfLO https://rickub.com/nandi/nimstatic/releases/download/v0.1.5/nimstatic-0.1.5-x86_64-linux
chmod +x nimstatic-0.1.5-x86_64-linux

Each release attaches the binary, an .xz of it, and SHA256SUMS; CI builds
them on a v* tag, and nimstatic builds itself, so the artifact doubles as
proof the tool works on a clean machine. To pin it in a project, use the
DotSlash file in the release notes.

Or from source: nimble install, or 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).