@@ 85,10 85,13 @@ proc startProcess(cmd: string; cmdArgs: varargs[string]): Process =
# stderr.writeLine(cmd, " ", join(cmdArgs, " "))
startProcess(cmd, args = cmdArgs, options = {poUsePath})
-proc gitLsRemote(url: string; withTags: bool): seq[tuple[tag: string, rev: string]] =
+type GitPair = object
+ `ref`, `rev`: string
+
+proc gitLsRemote(url: string; tagsArg = false): seq[GitPair] =
var line, rev, refer: string
var process =
- if withTags:
+ if tagsArg:
startProcess("git", "ls-remote", "--tags", url)
else:
startProcess("git", "ls-remote", url)
@@ 102,32 105,32 @@ proc gitLsRemote(url: string; withTags: bool): seq[tuple[tag: string, rev: strin
headsTags = "refs/heads/"
if refer.startsWith(refsTags):
refer.removePrefix(refsTags)
- result.add((refer, rev,))
+ result.add GitPair(`ref`: refer, `rev`: rev)
elif refer.startsWith(headsTags):
refer.removePrefix(headsTags)
- result.add((refer, rev,))
+ result.add GitPair(`ref`: refer, `rev`: rev)
stderr.write(process.errorStream.readAll)
close(process)
- if withTags and result.len == 0:
- result = gitLsRemote(url, not withTags)
+ if tagsArg and result.len == 0:
+ result = gitLsRemote(url, not tagsArg)
-proc matchRev(url: string; wanted: VersionRange): tuple[tag: string, rev: string] =
+proc matchRev(url: string; wanted: VersionRange): GitPair =
if wanted.kind == verSpecial:
let special = $wanted.spe
if special[0] == '#':
- result[1] = special[1..special.high]
+ result.rev = special[1..special.high]
else:
quit("unhandled version " & url & " " & $wanted)
else:
let withTags = wanted.kind != verAny
let pairs = gitLsRemote(url, withTags)
var resultVersion: Version
- for (tag, rev) in pairs:
+ for pair in pairs:
try:
- var tagVer = tag.newVersion
+ var tagVer = pair.`ref`.newVersion
if tagVer.withinRange(wanted) and resultVersion < tagVer:
resultVersion = tagVer
- result = (tag, rev)
+ result = pair
except ParseVersionError: discard
if result.rev == "" and pairs.len > 0:
result = pairs[pairs.high]
@@ 144,26 147,16 @@ proc collectMetadata(data: JsonNode) =
quit("no .nimble files found in " & storePath)
packageNames.sort()
data{"packages"} = %packageNames
- var
- nimbleFilePath = findNimbleFile(storePath, true)
- pkg = getPkgInfoFromFile(nimbleFilePath, nimbleOptions)
+ var nimbleFilePath = storePath.findNimbleFile(true)
+ var pkg = getPkgInfoFromFile(nimbleFilePath, nimbleOptions)
data{"srcDir"} = %pkg.srcDir
-proc prefetchGit(uri: Uri; version: VersionRange): JsonNode =
- var
- uri = uri
- subdir = ""
- uri.scheme.removePrefix("git+")
- if uri.query != "":
- if uri.query.startsWith("subdir="):
- subdir = uri.query[7 .. ^1]
- uri.query = ""
- let cloneUrl = $uri
- let (tag, rev) = matchRev(cloneUrl, version)
- var archiveUri = uri
+proc prefetchGit(cloneUrl: string; pair: GitPair; subDir = ""): JsonNode =
+ assert pair.rev != ""
+ var archiveUri = cloneUrl.parseUri
archiveUri.scheme = "https"
archiveUri.path.removeSuffix ".git"
- archiveUri.path = archiveUri.path / "archive" / rev & ".tar.gz"
+ archiveUri.path = archiveUri.path / "archive" / pair.rev & ".tar.gz"
let client = newHttpClient()
defer: close(client)
let
@@ 185,7 178,7 @@ proc prefetchGit(uri: Uri; version: VersionRange): JsonNode =
{
"method": "fetchzip",
"path": storePath,
- "rev": rev,
+ "rev": pair.rev,
"sha256": hash,
"url": archiveUrl
}
@@ 194,7 187,7 @@ proc prefetchGit(uri: Uri; version: VersionRange): JsonNode =
# a Nim attribute not used by the fetcher
else:
stderr.writeLine "fetch of ", archiveUrl, " returned ", resp.code
- var args = @["--quiet", "--fetch-submodules", "--url", cloneUrl, "--rev", rev]
+ var args = @["--quiet", "--fetch-submodules", "--url", cloneUrl, "--rev", pair.rev]
stderr.writeLine "prefetch ", cloneUrl
let dump = execProcess(
"nix-prefetch-git",
@@ 208,10 201,32 @@ proc prefetchGit(uri: Uri; version: VersionRange): JsonNode =
result{"subdir"} = %subdir
# a Nim attribute not used by the fetcher
result{"method"} = %"git"
- if tag != "":
- result{"ref"} = %tag
+ if pair.`ref` != "":
+ result{"ref"} = %pair.`ref`
collectMetadata(result)
+proc prefetchGit(uri: Uri; version: VersionRange): JsonNode =
+ var
+ uri = uri
+ subdir = ""
+ uri.scheme.removePrefix("git+")
+ if uri.query != "":
+ if uri.query.startsWith("subdir="):
+ subdir = uri.query[7 .. ^1]
+ uri.query = ""
+ let cloneUrl = $uri
+ cloneUrl.prefetchGit(cloneUrl.matchRev(version), subdir=subdir)
+
+proc selectGitCommit(url: string): GitPair =
+ let pairs = gitLsRemote(url)
+ stdout.writeLine "select a tag from ", url, ":"
+ for pair in pairs:
+ stdout.writeLine "\t", pair.`ref`, "\t", pair.`rev`
+ let choice = stdin.readLine.strip
+ for pair in pairs:
+ if choice == pair.`ref` or choice == pair.`rev`:
+ return pair
+
proc containsPackageUri(lockAttrs: JsonNode; pkgUrl: string): bool =
for e in lockAttrs.items:
if e{"url"}.getStr == pkgUrl:
@@ 361,9 376,12 @@ proc queueNimbleDependencies(
dependsOn.add %dep.bomRef
queue.addLast dep
-proc getComponent(bom: JsonNode; bomRef: string): JsonNode =
+proc getComponent(bom: JsonNode; bomRef: string; byName = false): JsonNode =
for e in bom{"components"}.elems:
if e{"bom-ref"}.getStr == bomRef: return e
+ if result.isNil and byName:
+ for e in bom{"components"}.elems:
+ if e{"name"}.getStr == bomRef: return e
return newJObject()
proc getDepends(bom: JsonNode; bomRef: string): JsonNode =
@@ 469,16 487,70 @@ proc generateBom(nimblePath: string): JsonNode =
if not result.containsComponent(dep.bomRef):
result.addComponent(dep, queue)
+proc replaceProperty(props: JsonNode; key, val: string): bool =
+ for pair in props.elems:
+ if pair{"name"}.getStr == key:
+ if pair{"value"}.getStr == val:
+ return false
+ else:
+ pair{"value"} = %val
+ return true
+ props.add(%*{ "name": key, "value": val })
+ true
+
+proc updateFodData(bom: JsonNode; bomRef, version: string; fodData: JsonNode): bool =
+ let
+ comp = bom.getComponent(bomRef)
+ props = comp{"properties"}
+ if comp{"version"}.getStr != version:
+ comp{"version"} = %version
+ result = true
+ for (key, val) in fodData.pairs:
+ let vs = val.getStr
+ if vs != "":
+ result = props.replaceProperty("nix:fod:" & key, vs) or result
+
+proc update(bom: JsonNode; bomRef: string; byName: bool): bool =
+ let comp = bom.getComponent(bomRef, byName)
+ assert not comp.isNil
+ var bomRef = comp{"bom-ref"}.getStr
+ assert not bomRef.isNil
+ var suitableRefFound = false
+ let extRefs = comp{"externalReferences"}
+ for extRef in extRefs.elems:
+ if extRef{"type"}.getStr == "vcs":
+ suitableRefFound = true
+ stderr.writeLine "updating ", extRef
+ let url = extRef{"url"}.getStr
+ if not url.parseUri.isGitUrl:
+ stderr.writeLine("assuming ", url, " is a git URL")
+ let gitPair = url.selectGitCommit()
+ let pkgData = url.prefetchGit gitPair
+ result = bom.updateFodData(bomRef, gitPair.`ref`.strip(chars={'v'}), pkgData)
+ if not suitableRefFound:
+ stderr.writeLine "cannot update from \"externalReferences\": ", extRefs
+
type Mode = enum
unspecifiedMode,
lockFileMode,
bomFromNimbleMode,
- listComponentsMode
+ listComponentsMode,
+ updateComponentMode
+
+const nimbleModes = {lockFileMode, bomFromNimbleMode, updateComponentMode}
-const nimbleModes = {lockFileMode, bomFromNimbleMode}
+proc findSbom(path: string): string =
+ result = path
+ if result.dirExists:
+ result = result / "sbom.json"
+ if not result.fileExists:
+ quit("no file to parse at " & result)
+
+proc parseSbom(path: string): JsonNode = path.findSbom.parseFile
proc main =
var
+ componentArgs: seq[string]
packagePath: string
mode = unspecifiedMode
recursive = true
@@ 498,12 570,18 @@ proc main =
mode = lockFileMode
of "list-components", "components", "comps":
mode = listComponentsMode
+ of "update-component", "update":
+ mode = updateComponentMode
else:
if packagePath != "":
quit("no more than a single package path may be specified")
packagePath = key
of cmdShortOption, cmdLongOption:
case key
+ of "component", "comp", "c":
+ if val == "":
+ quit("a component must be specified with " & key & ":")
+ componentArgs.add val
of "recursive", "r":
recursive = true
of "flat":
@@ 520,7 598,7 @@ proc main =
packagePath = getCurrentDir()
if mode in nimbleModes:
- nimbleOptions = parseCmdLine()
+ nimbleOptions = initOptions()
nimbleOptions.nimBin = findExe("nim")
if packagePath.fileExists:
packagePath = packagePath.parentDir
@@ 532,13 610,28 @@ proc main =
of bomFromNimbleMode:
stdout.writeLine generateBom(packagePath).pretty
of listComponentsMode:
- if packagePath.existsDir:
- packagePath = packagePath / "sbom.json"
- if not packagePath.existsFile:
- quit("no file to parse at " & packagePath)
- let bom = packagePath.parseFile()
+ let bom = packagePath.parseSbom()
for comp in bom{"components"}.elems:
stdout.writeLine comp{"bom-ref"}.getStr
+ of updateComponentMode:
+ var updated = false
+ if componentArgs.len == 0:
+ quit("update requires at least one --component:… argument")
+ let
+ sbomPath = packagePath.findSbom()
+ bom = sbomPath.parseFile()
+ for s in componentArgs:
+ if bom.update(s, byName=true):
+ updated = true
+ else:
+ stderr.writeLine "no update to ", s
+ if updated:
+ var text = bom.pretty
+ text.add '\n'
+ sbomPath.writeFile text
+ else:
+ stderr.writeLine "no updates to ", sbomPath
+
of unspecifiedMode:
raiseAssert "mode unspecified"