nandi/nimstaticpublic Fork 0
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.

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

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