eb81856565
Ship QUALITY_PLAN sessions 31–33: fetchable package index URLs, showcase app and micro/nexus benchmarks, and debugger-friendly C codegen. - Registry: BUX_REGISTRY accepts http(s) URLs (curl/wget → ~/.bux/cache) - E.2: make test-apps smoke for nexus/boko/simpledb/jwt-pitbul - E.5: benches/micro + C/Nim/Zig twins; make bench-nexus (wrk) - E.4: HIR locs → #line .bux; default -O0 -g; --release -O2; make test-dwarf
240 lines
8.0 KiB
Nim
240 lines
8.0 KiB
Nim
## registry.nim — Bux package registry index (E.1 + HTTP URL)
|
|
##
|
|
## Index format (TOML-ish, one package per [[package]] table):
|
|
##
|
|
## [[package]]
|
|
## name = "greet"
|
|
## version = "0.1.0"
|
|
## source = "file:packages/greet" # relative to the index file
|
|
## description = "Hello helpers"
|
|
##
|
|
## [[package]]
|
|
## name = "net"
|
|
## version = "1.2.0"
|
|
## source = "https://github.com/bux-lang/net.git"
|
|
##
|
|
## Lookup order for the index:
|
|
## 1. $BUX_REGISTRY — local file path **or** http(s):// URL
|
|
## 2. ~/.bux/registry.toml
|
|
## 3. <repo>/config/registry.toml next to the compiler / cwd
|
|
##
|
|
## HTTP indices are downloaded to ~/.bux/cache/registry_http.toml.
|
|
## Relative file:/path: sources in a remote index are resolved against that
|
|
## cache directory — prefer absolute paths or git URLs for remote registries.
|
|
|
|
import std/[os, strutils, strformat, algorithm, osproc]
|
|
|
|
type
|
|
RegistryPackage* = object
|
|
name*: string
|
|
version*: string
|
|
source*: string ## raw source as written in the index
|
|
description*: string
|
|
resolvedPath*: string ## absolute path for file: sources (filled on load)
|
|
|
|
Registry* = object
|
|
path*: string ## index file path (local, or cached path for HTTP)
|
|
sourceUrl*: string ## non-empty when loaded from an HTTP URL
|
|
packages*: seq[RegistryPackage]
|
|
|
|
proc resolvePackageSource(pkg: var RegistryPackage, indexDir: string) =
|
|
if pkg.source.startsWith("file:"):
|
|
var p = pkg.source["file:".len .. ^1]
|
|
if p.startsWith("//"):
|
|
p = p[2 .. ^1]
|
|
if not p.isAbsolute:
|
|
p = indexDir / p
|
|
pkg.resolvedPath = p.absolutePath
|
|
elif pkg.source.startsWith("path:"):
|
|
var p = pkg.source["path:".len .. ^1]
|
|
if not p.isAbsolute:
|
|
p = indexDir / p
|
|
pkg.resolvedPath = p.absolutePath
|
|
pkg.source = "file:" & pkg.resolvedPath
|
|
|
|
proc parseRegistryToml(content, indexPath: string): seq[RegistryPackage] =
|
|
## Minimal parser for repeated [[package]] blocks with string keys.
|
|
result = @[]
|
|
var cur: RegistryPackage
|
|
var inPkg = false
|
|
let indexDir = indexPath.parentDir
|
|
|
|
for raw in content.splitLines():
|
|
let line = raw.strip()
|
|
if line.len == 0 or line.startsWith("#"):
|
|
continue
|
|
if line == "[[package]]" or line == "[[Package]]":
|
|
if inPkg and cur.name.len > 0:
|
|
resolvePackageSource(cur, indexDir)
|
|
result.add(cur)
|
|
cur = RegistryPackage()
|
|
inPkg = true
|
|
continue
|
|
if not inPkg:
|
|
continue
|
|
let eq = line.find('=')
|
|
if eq < 0: continue
|
|
let key = line[0 ..< eq].strip().toLowerAscii()
|
|
var val = line[eq + 1 .. ^1].strip()
|
|
if val.len >= 2 and val[0] == '"' and val[^1] == '"':
|
|
val = val[1 ..< ^1]
|
|
case key
|
|
of "name": cur.name = val
|
|
of "version": cur.version = val
|
|
of "source": cur.source = val
|
|
of "description": cur.description = val
|
|
else: discard
|
|
if inPkg and cur.name.len > 0:
|
|
resolvePackageSource(cur, indexDir)
|
|
result.add(cur)
|
|
|
|
proc isHttpUrl*(s: string): bool =
|
|
s.startsWith("http://") or s.startsWith("https://")
|
|
|
|
proc registryCacheDir*(): string =
|
|
getHomeDir() / ".bux" / "cache"
|
|
|
|
proc fetchRegistryUrl*(url: string): string =
|
|
## Download a remote registry.toml into ~/.bux/cache/.
|
|
## Returns the local cache path on success, or "" on failure.
|
|
## Re-fetches when the URL changes or when $BUX_REGISTRY_REFRESH is set.
|
|
let cacheDir = registryCacheDir()
|
|
try:
|
|
createDir(cacheDir)
|
|
except OSError, IOError:
|
|
return ""
|
|
let cachePath = cacheDir / "registry_http.toml"
|
|
let metaPath = cacheDir / "registry_http.url"
|
|
let force = getEnv("BUX_REGISTRY_REFRESH").len > 0
|
|
var cachedUrl = ""
|
|
if fileExists(metaPath):
|
|
try:
|
|
cachedUrl = readFile(metaPath).strip()
|
|
except CatchableError:
|
|
cachedUrl = ""
|
|
if not force and fileExists(cachePath) and cachedUrl == url:
|
|
return cachePath.absolutePath
|
|
|
|
# Prefer curl; fall back to wget
|
|
var ok = false
|
|
if findExe("curl").len > 0:
|
|
let cmd = &"curl -fsSL --max-time 30 -o {quoteShell(cachePath)} {quoteShell(url)}"
|
|
let (_, code) = execCmdEx(cmd)
|
|
ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0
|
|
elif findExe("wget").len > 0:
|
|
let cmd = &"wget -q -T 30 -O {quoteShell(cachePath)} {quoteShell(url)}"
|
|
let (_, code) = execCmdEx(cmd)
|
|
ok = code == 0 and fileExists(cachePath) and getFileSize(cachePath) > 0
|
|
else:
|
|
return ""
|
|
|
|
if not ok:
|
|
return ""
|
|
try:
|
|
writeFile(metaPath, url & "\n")
|
|
except CatchableError:
|
|
discard
|
|
return cachePath.absolutePath
|
|
|
|
proc findRegistryIndex*(): tuple[path: string, url: string] =
|
|
## Locate the registry index. Returns (localPath, sourceUrl).
|
|
## sourceUrl is non-empty only when the index was (or should be) fetched via HTTP.
|
|
result = ("", "")
|
|
let env = getEnv("BUX_REGISTRY")
|
|
if env.len > 0:
|
|
if isHttpUrl(env):
|
|
let local = fetchRegistryUrl(env)
|
|
if local.len > 0:
|
|
return (local, env)
|
|
return ("", env) # URL set but fetch failed — caller can report
|
|
if fileExists(env):
|
|
return (env.absolutePath, "")
|
|
let homeIdx = getHomeDir() / ".bux" / "registry.toml"
|
|
if fileExists(homeIdx):
|
|
return (homeIdx, "")
|
|
let candidates = @[
|
|
getAppDir() / ".." / "config" / "registry.toml",
|
|
getAppDir() / "config" / "registry.toml",
|
|
getCurrentDir() / "config" / "registry.toml",
|
|
getCurrentDir() / ".." / "config" / "registry.toml",
|
|
]
|
|
for c in candidates:
|
|
if fileExists(c):
|
|
return (c.absolutePath, "")
|
|
return ("", "")
|
|
|
|
proc loadRegistry*(path: string = ""): Registry =
|
|
result.path = ""
|
|
result.sourceUrl = ""
|
|
result.packages = @[]
|
|
if path.len > 0:
|
|
if isHttpUrl(path):
|
|
result.sourceUrl = path
|
|
result.path = fetchRegistryUrl(path)
|
|
else:
|
|
result.path = path
|
|
else:
|
|
let (p, u) = findRegistryIndex()
|
|
result.path = p
|
|
result.sourceUrl = u
|
|
if result.path.len == 0 or not fileExists(result.path):
|
|
return
|
|
try:
|
|
let content = readFile(result.path)
|
|
result.packages = parseRegistryToml(content, result.path)
|
|
except CatchableError:
|
|
result.packages = @[]
|
|
|
|
proc registryLookup*(reg: Registry, name: string, versionReq: string = "*"): RegistryPackage =
|
|
## Find a package by name. versionReq `*` picks the last matching entry
|
|
## (index order; put newest last). Exact version matches preferred.
|
|
result = RegistryPackage()
|
|
var candidates: seq[RegistryPackage] = @[]
|
|
for p in reg.packages:
|
|
if p.name.toLowerAscii() == name.toLowerAscii():
|
|
candidates.add(p)
|
|
if candidates.len == 0:
|
|
return
|
|
if versionReq.len == 0 or versionReq == "*":
|
|
return candidates[^1]
|
|
for p in candidates:
|
|
if p.version == versionReq:
|
|
return p
|
|
# Semver prefix match: "1" matches "1.0.0"
|
|
for p in candidates:
|
|
if p.version.startsWith(versionReq):
|
|
return p
|
|
return candidates[^1]
|
|
|
|
proc registrySearch*(reg: Registry, query: string): seq[RegistryPackage] =
|
|
result = @[]
|
|
let q = query.toLowerAscii()
|
|
for p in reg.packages:
|
|
if q.len == 0 or
|
|
q in p.name.toLowerAscii() or
|
|
q in p.description.toLowerAscii():
|
|
result.add(p)
|
|
result.sort(proc (a, b: RegistryPackage): int =
|
|
cmp(a.name.toLowerAscii(), b.name.toLowerAscii()))
|
|
|
|
proc isGitSource*(source: string): bool =
|
|
source.startsWith("http://") or source.startsWith("https://") or
|
|
source.startsWith("git@") or source.startsWith("git://") or
|
|
source.startsWith("ssh://")
|
|
|
|
proc isFileSource*(source: string): bool =
|
|
source.startsWith("file:") or source.startsWith("path:")
|
|
|
|
proc formatRegistryDepLine*(name: string, pkg: RegistryPackage): string =
|
|
## Produce a bux.toml Dependencies line for a resolved registry package.
|
|
if pkg.resolvedPath.len > 0 and dirExists(pkg.resolvedPath):
|
|
return &"{name} = {{ Path = \"{pkg.resolvedPath}\" }}"
|
|
if isGitSource(pkg.source):
|
|
let ver = if pkg.version.len > 0: pkg.version else: "*"
|
|
return &"{name} = {{ Version = \"{ver}\", Source = \"{pkg.source}\" }}"
|
|
if isFileSource(pkg.source) and pkg.resolvedPath.len > 0:
|
|
return &"{name} = {{ Path = \"{pkg.resolvedPath}\" }}"
|
|
# Fallback: version-only (install will re-resolve)
|
|
let ver = if pkg.version.len > 0: pkg.version else: "*"
|
|
return &"{name} = \"{ver}\""
|