feat: lifetime elision, tooling CI, registry, and LSP locals
Ship the QUALITY_PLAN stretch from ownership through ecosystem: C.1 lifetime elision (bootstrap + selfhost), bux fmt/test/doc CI hooks, stdlib goldens, package registry (bux search/add), and LSP 0.4 position-sensitive locals with inferred let types. Full-tree format pass plus Map/Set remove double-free fix.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
## registry.nim — Bux package registry index (E.1)
|
||||
##
|
||||
## Index format (TOML-ish, one package per [[package]] table):
|
||||
##
|
||||
## [[package]]
|
||||
## name = "greet"
|
||||
## version = "0.1.0"
|
||||
## source = "file:packages/greet" # relative to the registry file
|
||||
## description = "Hello helpers"
|
||||
##
|
||||
## [[package]]
|
||||
## name = "net"
|
||||
## version = "1.2.0"
|
||||
## source = "https://github.com/bux-lang/net.git"
|
||||
##
|
||||
## Lookup order for the index file:
|
||||
## 1. $BUX_REGISTRY (file path)
|
||||
## 2. ~/.bux/registry.toml
|
||||
## 3. <repo>/config/registry.toml next to the compiler / cwd
|
||||
|
||||
import std/[os, strutils, strformat, algorithm]
|
||||
|
||||
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
|
||||
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 findRegistryIndex*(): string =
|
||||
## Locate the registry index file.
|
||||
let env = getEnv("BUX_REGISTRY")
|
||||
if env.len > 0 and 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 = if path.len > 0: path else: findRegistryIndex()
|
||||
result.packages = @[]
|
||||
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}\""
|
||||
Reference in New Issue
Block a user