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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
## 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 <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)
Build options:
-o, --output <path> 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 <lib=pkg> Map a library to an Alpine package (repeatable)
--pkg <name> Also install this package (repeatable)
--cc <path> Compiler to use instead of a generated zig wrapper
--nim <path> Nim executable (default: nim)
Sysroot options:
-r, --root <dir> Sysroot (default $XDG_DATA_HOME/nimstatic/sysroot)
-b, --branch <ver> Alpine branch (default v3.21; `edge` for rolling)
-a, --arch <arch> Target arch (default x86_64)
-m, --mirror <url> Mirror base URL
--repo <names> Comma-separated repositories (default main,community)
-l, --lib <name> Add -l<name> 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
|