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

nimstatic.nim · 272 lines · 10.5 KBNim Blame HistoryRaw
temp: raw size probe 159b56c nandi 7h ago1## nimstatic — fully static Nim binaries, dependencies and all.
2##
3## nimstatic foo.nim
4##
5## It asks the Nim compiler what `foo.nim` links against (including the
6## libraries Nim would `dlopen` at runtime, which never touch the link line),
7## fetches musl-built static archives for them from Alpine's mirrors, and
8## compiles against a sysroot it owns. No apk, no container, no root.
9
10import nimstatic/[build, detect, flags, index, repo, sysroot]
11export build, detect, flags, index, repo, sysroot
12
13when isMainModule:
14 import std/[os, sequtils, strutils, tables]
15
16 const usageText = """
17nimstatic — fully static Nim binaries, dependencies and all
18
19Usage:
20 nimstatic <file.nim> [-- <nim args>] Detect, fetch, build static
21 nimstatic detect <file.nim> Show what it needs, change nothing
22 nimstatic add <pkg>... Put packages in the sysroot by hand
23 nimstatic list Show what the sysroot holds
24 nimstatic libs Show the sysroot's static libraries
25 nimstatic search <text> Search Alpine's index
26 nimstatic show <pkg> Index record for one package
27 nimstatic nimflags [-l lib] Print nim flags for the sysroot
28 nimstatic ccflags [-l lib] Print cc/clang flags
29 nimstatic nimcfg [-o file] Write a nim.cfg fragment
30 nimstatic zigcc [-o file] Write a `zig cc -target …-musl` wrapper
31 nimstatic env Shell exports (PKG_CONFIG_*, NIMSTATIC_ROOT)
32 nimstatic path Print the sysroot path
33 nimstatic clean Remove the sysroot (cache is kept)
34
35Build options:
36 -o, --output <path> Binary to write (default: the source's name)
37 -d, --debug Skip -d:release
38 -n, --dry-run Print the build command instead of running it
39 --map <lib=pkg> Map a library to an Alpine package (repeatable)
40 --pkg <name> Also install this package (repeatable)
41 --cc <path> Compiler to use instead of a generated zig wrapper
42 --nim <path> Nim executable (default: nim)
43
44Sysroot options:
45 -r, --root <dir> Sysroot (default $XDG_DATA_HOME/nimstatic/sysroot)
46 -b, --branch <ver> Alpine branch (default v3.21; `edge` for rolling)
47 -a, --arch <arch> Target arch (default x86_64)
48 -m, --mirror <url> Mirror base URL
49 --repo <names> Comma-separated repositories (default main,community)
50 -l, --lib <name> Add -l<name> to emitted flags (repeatable)
51 --no-deps Do not pull dependencies (add only)
52 --refresh Re-fetch the index even if it is fresh
53 -q, --quiet No progress on stderr
54 -h, --help Show help
55
56Everything after `--` goes to the Nim compiler, for both the probe and the
57build, so conditional imports resolve the same way twice:
58
59 nimstatic app.nim -- -d:ssl -d:danger
60
61Packages come over HTTPS and are unpacked as-is; nimstatic does not verify
62Alpine's signatures, so treat a sysroot as build input, not as a trust root.
63"""
64
65 type Opts = object
66 root, mirror, branch, arch, cc, output, nimExe: string
67 repos, libs, args, nimArgs, pkgs: seq[string]
68 maps: Table[string, string]
69 noDeps, refresh, quiet, help, debug, dryRun: bool
70
71 proc defaultRoot(): string = getDataDir() / "nimstatic" / "sysroot"
72
73 proc die(msg: string) =
74 stderr.writeLine "nimstatic: " & msg
75 quit(1)
76
77 proc parseOpts(): Opts =
78 ## Hand-rolled so `--root dir`, `--root=dir` and `-r dir` all work, and so
79 ## everything past `--` can be handed to the compiler untouched.
80 result = Opts(root: getEnv("NIMSTATIC_ROOT", defaultRoot()),
81 mirror: defaultMirror, branch: defaultBranch,
82 arch: defaultArch, repos: defaultRepos, nimExe: "nim")
83 let argv = commandLineParams()
84 var i = 0
85 while i < argv.len:
86 let arg = argv[i]
87 if arg == "--":
88 result.nimArgs = argv[i + 1 .. ^1]
89 break
90 if not arg.startsWith("-") or arg == "-":
91 result.args.add arg
92 inc i
93 continue
94 var
95 key = arg.strip(leading = true, trailing = false, chars = {'-'})
96 val = ""
97 attached = false
98 for sep in ['=', ':']:
99 let at = key.find(sep)
100 if at >= 0:
101 val = key[at + 1 .. ^1]
102 key = key[0 ..< at]
103 attached = true
104 break
105
106 proc takeValue(): string =
107 if attached: return val
108 inc i
109 if i >= argv.len: die "option --" & key & " needs a value"
110 argv[i]
111
112 case key
113 of "r", "root": result.root = takeValue()
114 of "b", "branch": result.branch = takeValue()
115 of "a", "arch": result.arch = takeValue()
116 of "m", "mirror": result.mirror = takeValue()
117 of "repo": result.repos = takeValue().split(',').filterIt(it.len > 0)
118 of "l", "lib": result.libs.add takeValue()
119 of "o", "output": result.output = takeValue()
120 of "cc": result.cc = takeValue()
121 of "nim": result.nimExe = takeValue()
122 of "pkg": result.pkgs.add takeValue()
123 of "map":
124 let m = takeValue().split('=', 1)
125 if m.len != 2: die "--map wants lib=package, got: " & m.join("=")
126 result.maps[m[0]] = m[1]
127 of "d", "debug": result.debug = true
128 of "n", "dry-run": result.dryRun = true
129 of "no-deps": result.noDeps = true
130 of "refresh": result.refresh = true
131 of "q", "quiet": result.quiet = true
132 of "h", "help": result.help = true
133 else: die "unknown option: " & arg
134 inc i
135
136 proc emit(o: Opts, text: string) =
137 if o.output.len > 0:
138 createDir o.output.parentDir
139 writeFile(o.output, text)
140 if not o.quiet: stderr.writeLine "wrote " & o.output
141 else:
142 stdout.write text
143
144 proc buildOpts(o: Opts): BuildOpts =
145 BuildOpts(root: o.root, cc: o.cc, output: o.output, nimExe: o.nimExe,
146 target: o.arch & "-linux-musl", nimArgs: o.nimArgs,
147 extraMap: o.maps, extraPackages: o.pkgs,
148 release: not o.debug, dryRun: o.dryRun, quiet: o.quiet)
149
150 proc cmdBuild(o: Opts, remote: Remote, source: string) =
151 let res = buildOpts(o).build(remote, source)
152 if o.dryRun:
153 echo res.command.quoteShellCommand
154 return
155 if not o.quiet:
156 let size = if fileExists(res.output): getFileSize(res.output) div 1024 else: 0
157 stderr.writeLine "wrote " & res.output & " (" & $size & " KiB, static)"
158
159 proc cmdDetect(o: Opts, source: string) =
160 let d = detect(source, o.nimArgs, o.nimExe, o.maps)
161 if d.needs.len == 0:
162 echo "no external libraries needed — plain --passL:-static will do"
163 return
164 echo "library".alignLeft(16), "how".alignLeft(9), "alpine package"
165 for need in d.needs:
166 echo need.lib.alignLeft(16),
167 (if need.dynlib: "dlopen" else: "link").alignLeft(9),
168 (if need.package.len > 0: need.package else: "(unmapped)")
169 if packages(d).len > 0:
170 echo "\nnimstatic add ", packages(d).join(" ")
171 if d.unmapped.len > 0:
172 echo "\nunmapped: ", d.unmapped.join(", "),
173 "\nsearch for one with: nimstatic search ", d.unmapped[0]
174
175 proc cmdAdd(o: Opts, remote: Remote) =
176 if o.args.len == 0: die "add: name at least one package"
177 let pkgs = remote.fetchIndex(o.refresh).resolve(o.args, not o.noDeps)
178 for pkg in pkgs:
179 unpack(remote.fetchPackage(pkg), o.root)
180 record(o.root, pkgs)
181 if not o.quiet:
182 stderr.writeLine "unpacked " & $pkgs.len & " package(s) into " & o.root
183
184 proc cmdList(o: Opts) =
185 let entries = readManifest(o.root)
186 if entries.len == 0:
187 stderr.writeLine "nothing installed in " & o.root
188 return
189 for e in entries:
190 echo e.name.alignLeft(32), e.version.alignLeft(20), e.repo
191
192 proc cmdSearch(o: Opts, remote: Remote) =
193 if o.args.len == 0: die "search: give some text"
194 let
195 idx = remote.fetchIndex(o.refresh)
196 needle = o.args.join(" ").toLowerAscii
197 var hits = 0
198 for name, pkg in idx.packages:
199 if needle in name.toLowerAscii or needle in pkg.description.toLowerAscii:
200 echo name.alignLeft(34), pkg.version.alignLeft(16), pkg.description
201 inc hits
202 if hits == 0: stderr.writeLine "no matches for " & needle
203
204 proc cmdShow(o: Opts, remote: Remote) =
205 if o.args.len == 0: die "show: name a package"
206 let idx = remote.fetchIndex(o.refresh)
207 for want in o.args:
208 let name = idx.find(want)
209 if name.len == 0: die "no package provides '" & want & "'"
210 let pkg = idx.packages[name]
211 echo "name: ", pkg.name
212 echo "version: ", pkg.version
213 echo "repository: ", pkg.repo, "/", remote.arch
214 echo "size: ", pkg.size div 1024, " KiB"
215 echo "description: ", pkg.description
216 if pkg.depends.len > 0: echo "depends: ", pkg.depends.join(" ")
217 if pkg.provides.len > 0: echo "provides: ", pkg.provides.join(" ")
218 echo "url: ", remote.repoUrl(pkg.repo), "/", apkFile(pkg)
219
220 proc main() =
221 let o = parseOpts()
222 if o.help or o.args.len == 0:
223 stdout.write usageText
224 quit(if o.help: 0 else: 1)
225 let
226 head = o.args[0]
227 rest = block:
228 var r = o
229 r.args = o.args[1 .. ^1]
230 r
231 remote = initRemote(o.mirror, o.branch, o.arch, o.repos, quiet = o.quiet)
232
233 # A .nim path is the whole point, so it needs no subcommand.
234 if head.endsWith(".nim") or fileExists(head):
235 cmdBuild(o, remote, head)
236 return
237
238 case head
239 of "build":
240 if rest.args.len == 0: die "build: name a .nim file"
241 cmdBuild(rest, remote, rest.args[0])
242 of "detect":
243 if rest.args.len == 0: die "detect: name a .nim file"
244 cmdDetect(rest, rest.args[0])
245 of "add": cmdAdd(rest, remote)
246 of "list": cmdList(rest)
247 of "libs": stdout.write describe(rest.root)
248 of "search": cmdSearch(rest, remote)
249 of "show": cmdShow(rest, remote)
250 of "nimflags":
251 echo nimFlags(rest.root, overridesIn(rest.root), rest.libs).join(" ")
252 of "ccflags": echo ccFlags(rest.root, rest.libs).join(" ")
253 of "nimcfg":
254 rest.emit nimCfg(rest.root, rest.cc, overridesIn(rest.root), rest.libs)
255 of "zigcc":
256 let path = if rest.output.len > 0: rest.output else: "zigcc"
257 writeZigCc(path, rest.arch & "-linux-musl")
258 if not rest.quiet: stderr.writeLine "wrote " & path
259 of "env":
260 for kv in pkgConfigEnv(rest.root):
261 echo "export ", kv
262 echo "export NIMSTATIC_ROOT=", rest.root
263 of "path": echo rest.root
264 of "clean":
265 removeDir rest.root
266 if not rest.quiet: stderr.writeLine "removed " & rest.root
267 else: die "unknown command: " & head & " (try --help)"
268
269 try:
270 main()
271 except CatchableError as e:
272 die e.msg