211 lines
7.2 KiB
Nim
211 lines
7.2 KiB
Nim
# Copyright (C) Dominik Picheta. All rights reserved.
|
|
# BSD License. Look at license.txt for more info.
|
|
import parsecfg, json, streams, strutils, parseutils, os
|
|
import version, common
|
|
type
|
|
TPackageInfo* = object
|
|
mypath*: string ## The path of this .babel file
|
|
name*: string
|
|
version*: string
|
|
author*: string
|
|
description*: string
|
|
license*: string
|
|
skipDirs*: seq[string]
|
|
skipFiles*: seq[string]
|
|
requires*: seq[tuple[name: string, ver: PVersionRange]]
|
|
bin*: seq[string]
|
|
|
|
TPackage* = object
|
|
name*: string
|
|
version*: string
|
|
license*: string
|
|
url*: string
|
|
dvcsTag*: string
|
|
downloadMethod*: string
|
|
tags*: seq[string]
|
|
description*: string
|
|
|
|
proc initPackageInfo(): TPackageInfo =
|
|
result.mypath = ""
|
|
result.name = ""
|
|
result.version = ""
|
|
result.author = ""
|
|
result.description = ""
|
|
result.license = ""
|
|
result.skipDirs = @[]
|
|
result.skipFiles = @[]
|
|
result.requires = @[]
|
|
result.bin = @[]
|
|
|
|
proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) =
|
|
if pkgInfo.name == "":
|
|
quit("Incorrect .babel file: " & path & " does not contain a name field.")
|
|
if pkgInfo.version == "":
|
|
quit("Incorrect .babel file: " & path & " does not contain a version field.")
|
|
if pkgInfo.author == "":
|
|
quit("Incorrect .babel file: " & path & " does not contain an author field.")
|
|
if pkgInfo.description == "":
|
|
quit("Incorrect .babel file: " & path & " does not contain a description field.")
|
|
if pkgInfo.license == "":
|
|
quit("Incorrect .babel file: " & path & " does not contain a license field.")
|
|
|
|
proc parseRequires(req: string): tuple[name: string, ver: PVersionRange] =
|
|
try:
|
|
if ' ' in req:
|
|
var i = skipUntil(req, whitespace)
|
|
result.name = req[0 .. i].strip
|
|
result.ver = parseVersionRange(req[i .. -1])
|
|
else:
|
|
result.name = req.strip
|
|
result.ver = PVersionRange(kind: verAny)
|
|
except EParseVersion:
|
|
quit("Unable to parse dependency version range: " & getCurrentExceptionMsg())
|
|
|
|
proc readPackageInfo*(path: string): TPackageInfo =
|
|
result = initPackageInfo()
|
|
result.mypath = path
|
|
var fs = newFileStream(path, fmRead)
|
|
if fs != nil:
|
|
var p: TCfgParser
|
|
open(p, fs, path)
|
|
var currentSection = ""
|
|
while true:
|
|
var ev = next(p)
|
|
case ev.kind
|
|
of cfgEof:
|
|
break
|
|
of cfgSectionStart:
|
|
currentSection = ev.section
|
|
of cfgKeyValuePair:
|
|
case currentSection.normalize
|
|
of "package":
|
|
case ev.key.normalize
|
|
of "name": result.name = ev.value
|
|
of "version": result.version = ev.value
|
|
of "author": result.author = ev.value
|
|
of "description": result.description = ev.value
|
|
of "license": result.license = ev.value
|
|
of "skipdirs":
|
|
result.skipDirs.add(ev.value.split(','))
|
|
of "skipfiles":
|
|
result.skipFiles.add(ev.value.split(','))
|
|
of "bin":
|
|
result.bin = ev.value.split(',')
|
|
else:
|
|
quit("Invalid field: " & ev.key, QuitFailure)
|
|
of "deps", "dependencies":
|
|
case ev.key.normalize
|
|
of "requires":
|
|
for v in ev.value.split(','):
|
|
result.requires.add(parseRequires(v.strip))
|
|
else:
|
|
quit("Invalid field: " & ev.key, QuitFailure)
|
|
else: quit("Invalid section: " & currentSection, QuitFailure)
|
|
of cfgOption: quit("Invalid package info, should not contain --" & ev.value, QuitFailure)
|
|
of cfgError:
|
|
echo(ev.msg)
|
|
close(p)
|
|
else:
|
|
raise newException(EInvalidValue, "Cannot open package info: " & path)
|
|
validatePackageInfo(result, path)
|
|
|
|
proc optionalField(obj: PJsonNode, name: string): string =
|
|
if existsKey(obj, name):
|
|
if obj[name].kind == JString:
|
|
return obj[name].str
|
|
else:
|
|
quit("Corrupted packages.json file. " & name & " field is of unexpected type.")
|
|
else: return ""
|
|
|
|
proc requiredField(obj: PJsonNode, name: string): string =
|
|
if existsKey(obj, name):
|
|
if obj[name].kind == JString:
|
|
return obj[name].str
|
|
else:
|
|
quit("Corrupted packages.json file. " & name & " field is of unexpected type.")
|
|
else:
|
|
quit("Package in packages.json file does not contain a " & name & " field.")
|
|
|
|
proc getPackage*(pkg: string, packagesPath: string, resPkg: var TPackage): bool =
|
|
let packages = parseFile(packagesPath)
|
|
for p in packages:
|
|
if p["name"].str != pkg: continue
|
|
resPkg.name = pkg
|
|
resPkg.url = p.requiredField("url")
|
|
resPkg.version = p.optionalField("version")
|
|
resPkg.downloadMethod = p.requiredField("method")
|
|
resPkg.dvcsTag = p.optionalField("dvcs-tag")
|
|
resPkg.license = p.requiredField("license")
|
|
resPkg.tags = @[]
|
|
for t in p["tags"]:
|
|
resPkg.tags.add(t.str)
|
|
resPkg.description = p.requiredField("description")
|
|
return true
|
|
return false
|
|
|
|
proc getPackageList*(packagesPath: string): seq[TPackage] =
|
|
result = @[]
|
|
let packages = parseFile(packagesPath)
|
|
for p in packages:
|
|
var pkg: TPackage
|
|
pkg.name = p.requiredField("name")
|
|
pkg.version = p.optionalField("version")
|
|
pkg.url = p.requiredField("url")
|
|
pkg.downloadMethod = p.requiredField("method")
|
|
pkg.dvcsTag = p.optionalField("dvcs-tag")
|
|
pkg.license = p.requiredField("license")
|
|
pkg.tags = @[]
|
|
for t in p["tags"]:
|
|
pkg.tags.add(t.str)
|
|
pkg.description = p.requiredField("description")
|
|
result.add(pkg)
|
|
|
|
proc findBabelFile*(dir: string): string =
|
|
result = ""
|
|
for kind, path in walkDir(dir):
|
|
if kind == pcFile and path.splitFile.ext == ".babel":
|
|
if result != "":
|
|
raise newException(EBabel, "Only one .babel file should be present in " & dir)
|
|
result = path
|
|
|
|
proc getPkgInfo*(dir: string): TPackageInfo =
|
|
## Find the .babel file in ``dir`` and parses it, returning a TPackageInfo.
|
|
let babelFile = findBabelFile(dir)
|
|
if babelFile == "":
|
|
quit("Specified directory does not contain a .babel file.", QuitFailure)
|
|
result = readPackageInfo(babelFile)
|
|
|
|
proc getInstalledPkgs*(libsDir: string): seq[TPackageInfo] =
|
|
## Gets a list of installed packages.
|
|
##
|
|
## ``libsDir`` is in most cases: ~/.babel/libs/
|
|
result = @[]
|
|
for kind, path in walkDir(libsDir):
|
|
if kind == pcDir:
|
|
let babelFile = findBabelFile(path)
|
|
if babelFile != "":
|
|
result.add(readPackageInfo(babelFile))
|
|
else:
|
|
# TODO: Abstract logging.
|
|
echo("WARNING: No .babel file found for ", path)
|
|
|
|
proc findPkg*(pkglist: seq[TPackageInfo],
|
|
dep: tuple[name: string, ver: PVersionRange],
|
|
r: var TPackageInfo): bool =
|
|
## Searches ``pkglist`` for a package of which version is withing the range
|
|
## of ``dep.ver``. ``True`` is returned if a package is found. If multiple
|
|
## packages are found the newest one is returned (the one with the highest
|
|
## version number)
|
|
for pkg in pkglist:
|
|
if pkg.name != dep.name: continue
|
|
if withinRange(newVersion(pkg.version), dep.ver):
|
|
if not result or newVersion(r.version) < newVersion(pkg.version):
|
|
r = pkg
|
|
result = true
|
|
|
|
proc echoPackage*(pkg: TPackage) =
|
|
echo(pkg.name & ":")
|
|
echo(" url: " & pkg.url & " (" & pkg.downloadMethod & ")")
|
|
echo(" tags: " & pkg.tags.join(", "))
|
|
echo(" description: " & pkg.description)
|
|
echo(" license: " & pkg.license)
|