## nimstatic — fully static Nim binaries, dependencies and all. ## ## nimstatic foo.nim ## ## It asks the Nim compiler what `foo.nim` links against (including the ## libraries Nim would `dlopen` at runtime, which never touch the link line), ## fetches musl-built static archives for them from Alpine's mirrors, and ## compiles against a sysroot it owns. No apk, no container, no root. import nimstatic/[build, detect, flags, index, repo, sysroot] export build, detect, flags, index, repo, sysroot when isMainModule: import std/[os, sequtils, strutils, tables] const usageText = """ nimstatic — fully static Nim binaries, dependencies and all 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) Build options: -o, --output Binary to write (default: the source's name) -d, --debug Skip -d:release -n, --dry-run Print the build command instead of running it --map Map a library to an Alpine package (repeatable) --pkg Also install this package (repeatable) --cc Compiler to use instead of a generated zig wrapper --nim Nim executable (default: nim) Sysroot options: -r, --root Sysroot (default $XDG_DATA_HOME/nimstatic/sysroot) -b, --branch Alpine branch (default v3.21; `edge` for rolling) -a, --arch Target arch (default x86_64) -m, --mirror Mirror base URL --repo Comma-separated repositories (default main,community) -l, --lib Add -l to emitted flags (repeatable) --no-deps Do not pull dependencies (add only) --refresh Re-fetch the index even if it is fresh -q, --quiet No progress on stderr -h, --help Show help 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 Packages come over HTTPS and are unpacked as-is; nimstatic does not verify Alpine's signatures, so treat a sysroot as build input, not as a trust root. """ type Opts = object root, mirror, branch, arch, cc, output, nimExe: string repos, libs, args, nimArgs, pkgs: seq[string] maps: Table[string, string] noDeps, refresh, quiet, help, debug, dryRun: bool proc defaultRoot(): string = getDataDir() / "nimstatic" / "sysroot" proc die(msg: string) = stderr.writeLine "nimstatic: " & msg quit(1) proc parseOpts(): Opts = ## Hand-rolled so `--root dir`, `--root=dir` and `-r dir` all work, and so ## everything past `--` can be handed to the compiler untouched. result = Opts(root: getEnv("NIMSTATIC_ROOT", defaultRoot()), mirror: defaultMirror, branch: defaultBranch, arch: defaultArch, repos: defaultRepos, nimExe: "nim") let argv = commandLineParams() var i = 0 while i < argv.len: let arg = argv[i] if arg == "--": result.nimArgs = argv[i + 1 .. ^1] break if not arg.startsWith("-") or arg == "-": result.args.add arg inc i continue var key = arg.strip(leading = true, trailing = false, chars = {'-'}) val = "" attached = false for sep in ['=', ':']: let at = key.find(sep) if at >= 0: val = key[at + 1 .. ^1] key = key[0 ..< at] attached = true break proc takeValue(): string = if attached: return val inc i if i >= argv.len: die "option --" & key & " needs a value" argv[i] case key of "r", "root": result.root = takeValue() of "b", "branch": result.branch = takeValue() of "a", "arch": result.arch = takeValue() of "m", "mirror": result.mirror = takeValue() of "repo": result.repos = takeValue().split(',').filterIt(it.len > 0) of "l", "lib": result.libs.add takeValue() of "o", "output": result.output = takeValue() of "cc": result.cc = takeValue() of "nim": result.nimExe = takeValue() of "pkg": result.pkgs.add takeValue() of "map": let m = takeValue().split('=', 1) if m.len != 2: die "--map wants lib=package, got: " & m.join("=") result.maps[m[0]] = m[1] of "d", "debug": result.debug = true of "n", "dry-run": result.dryRun = true of "no-deps": result.noDeps = true of "refresh": result.refresh = true of "q", "quiet": result.quiet = true of "h", "help": result.help = true else: die "unknown option: " & arg inc i proc emit(o: Opts, text: string) = if o.output.len > 0: createDir o.output.parentDir writeFile(o.output, text) if not o.quiet: stderr.writeLine "wrote " & o.output else: stdout.write text proc buildOpts(o: Opts): BuildOpts = BuildOpts(root: o.root, cc: o.cc, output: o.output, nimExe: o.nimExe, target: o.arch & "-linux-musl", nimArgs: o.nimArgs, extraMap: o.maps, extraPackages: o.pkgs, release: not o.debug, dryRun: o.dryRun, quiet: o.quiet) proc cmdBuild(o: Opts, remote: Remote, source: string) = let res = buildOpts(o).build(remote, source) if o.dryRun: echo res.command.quoteShellCommand return if not o.quiet: let size = if fileExists(res.output): getFileSize(res.output) div 1024 else: 0 stderr.writeLine "wrote " & res.output & " (" & $size & " KiB, static)" proc cmdDetect(o: Opts, source: string) = let d = detect(source, o.nimArgs, o.nimExe, o.maps) if d.needs.len == 0: echo "no external libraries needed — plain --passL:-static will do" return echo "library".alignLeft(16), "how".alignLeft(9), "alpine package" for need in d.needs: echo need.lib.alignLeft(16), (if need.dynlib: "dlopen" else: "link").alignLeft(9), (if need.package.len > 0: need.package else: "(unmapped)") if packages(d).len > 0: echo "\nnimstatic add ", packages(d).join(" ") if d.unmapped.len > 0: echo "\nunmapped: ", d.unmapped.join(", "), "\nsearch for one with: nimstatic search ", d.unmapped[0] proc cmdAdd(o: Opts, remote: Remote) = if o.args.len == 0: die "add: name at least one package" let pkgs = remote.fetchIndex(o.refresh).resolve(o.args, not o.noDeps) for pkg in pkgs: unpack(remote.fetchPackage(pkg), o.root) record(o.root, pkgs) if not o.quiet: stderr.writeLine "unpacked " & $pkgs.len & " package(s) into " & o.root proc cmdList(o: Opts) = let entries = readManifest(o.root) if entries.len == 0: stderr.writeLine "nothing installed in " & o.root return for e in entries: echo e.name.alignLeft(32), e.version.alignLeft(20), e.repo proc cmdSearch(o: Opts, remote: Remote) = if o.args.len == 0: die "search: give some text" let idx = remote.fetchIndex(o.refresh) needle = o.args.join(" ").toLowerAscii var hits = 0 for name, pkg in idx.packages: if needle in name.toLowerAscii or needle in pkg.description.toLowerAscii: echo name.alignLeft(34), pkg.version.alignLeft(16), pkg.description inc hits if hits == 0: stderr.writeLine "no matches for " & needle proc cmdShow(o: Opts, remote: Remote) = if o.args.len == 0: die "show: name a package" let idx = remote.fetchIndex(o.refresh) for want in o.args: let name = idx.find(want) if name.len == 0: die "no package provides '" & want & "'" let pkg = idx.packages[name] echo "name: ", pkg.name echo "version: ", pkg.version echo "repository: ", pkg.repo, "/", remote.arch echo "size: ", pkg.size div 1024, " KiB" echo "description: ", pkg.description if pkg.depends.len > 0: echo "depends: ", pkg.depends.join(" ") if pkg.provides.len > 0: echo "provides: ", pkg.provides.join(" ") echo "url: ", remote.repoUrl(pkg.repo), "/", apkFile(pkg) proc main() = let o = parseOpts() if o.help or o.args.len == 0: stdout.write usageText quit(if o.help: 0 else: 1) let head = o.args[0] rest = block: var r = o r.args = o.args[1 .. ^1] r remote = initRemote(o.mirror, o.branch, o.arch, o.repos, quiet = o.quiet) # A .nim path is the whole point, so it needs no subcommand. if head.endsWith(".nim") or fileExists(head): cmdBuild(o, remote, head) return case head of "build": if rest.args.len == 0: die "build: name a .nim file" cmdBuild(rest, remote, rest.args[0]) of "detect": if rest.args.len == 0: die "detect: name a .nim file" cmdDetect(rest, rest.args[0]) of "add": cmdAdd(rest, remote) of "list": cmdList(rest) of "libs": stdout.write describe(rest.root) of "search": cmdSearch(rest, remote) of "show": cmdShow(rest, remote) of "nimflags": echo nimFlags(rest.root, overridesIn(rest.root), rest.libs).join(" ") of "ccflags": echo ccFlags(rest.root, rest.libs).join(" ") of "nimcfg": rest.emit nimCfg(rest.root, rest.cc, overridesIn(rest.root), rest.libs) of "zigcc": let path = if rest.output.len > 0: rest.output else: "zigcc" writeZigCc(path, rest.arch & "-linux-musl") if not rest.quiet: stderr.writeLine "wrote " & path of "env": for kv in pkgConfigEnv(rest.root): echo "export ", kv echo "export NIMSTATIC_ROOT=", rest.root of "path": echo rest.root of "clean": removeDir rest.root if not rest.quiet: stderr.writeLine "removed " & rest.root else: die "unknown command: " & head & " (try --help)" try: main() except CatchableError as e: die e.msg