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

Rename to nimstatic, and detect dependencies from the source 53cd879 · on 63a71590b2153d6d622a758b8a31ba942d041a85 · nandi · 19h ago
nimstatic.nim · 272 lines · 10.5 KBNim Blame HistoryRaw
  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