Many new features: build command and dependency resolution implemented.
This commit is contained in:
parent
d6be7b84e6
commit
5e01fa0342
3 changed files with 251 additions and 64 deletions
231
babel.nim
231
babel.nim
|
|
@ -1,23 +1,25 @@
|
||||||
# Copyright (C) Dominik Picheta. All rights reserved.
|
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||||
# BSD License. Look at license.txt for more info.
|
# BSD License. Look at license.txt for more info.
|
||||||
|
|
||||||
import httpclient, parseopt, os, strutils, osproc
|
import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils
|
||||||
|
|
||||||
import packageinfo
|
import packageinfo, version
|
||||||
|
|
||||||
type
|
type
|
||||||
TActionType = enum
|
TActionType = enum
|
||||||
ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList
|
ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList, ActionBuild
|
||||||
|
|
||||||
TAction = object
|
TAction = object
|
||||||
case typ: TActionType
|
case typ: TActionType
|
||||||
of ActionNil, ActionList: nil
|
of ActionNil, ActionList, ActionBuild: nil
|
||||||
of ActionUpdate:
|
of ActionUpdate:
|
||||||
optionalURL: string # Overrides default package list.
|
optionalURL: string # Overrides default package list.
|
||||||
of ActionInstall:
|
of ActionInstall:
|
||||||
optionalName: seq[string] # When this is @[], installs package from current dir.
|
optionalName: seq[string] # When this is @[], installs package from current dir.
|
||||||
of ActionSearch:
|
of ActionSearch:
|
||||||
search: seq[string] # Search string.
|
search: seq[string] # Search string.
|
||||||
|
|
||||||
|
EBabel = object of EBase
|
||||||
|
|
||||||
const
|
const
|
||||||
help = """
|
help = """
|
||||||
|
|
@ -25,6 +27,7 @@ Usage: babel COMMAND [opts]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
install Installs a list of packages.
|
install Installs a list of packages.
|
||||||
|
build Builds a package.
|
||||||
update Updates package list. A package list URL can be optionally specificed.
|
update Updates package list. A package list URL can be optionally specificed.
|
||||||
search Searches for a specified package.
|
search Searches for a specified package.
|
||||||
list Lists all packages.
|
list Lists all packages.
|
||||||
|
|
@ -50,6 +53,8 @@ proc parseCmdLine(): TAction =
|
||||||
of "install":
|
of "install":
|
||||||
result.typ = ActionInstall
|
result.typ = ActionInstall
|
||||||
result.optionalName = @[]
|
result.optionalName = @[]
|
||||||
|
of "build":
|
||||||
|
result.typ = ActionBuild
|
||||||
of "update":
|
of "update":
|
||||||
result.typ = ActionUpdate
|
result.typ = ActionUpdate
|
||||||
result.optionalURL = ""
|
result.optionalURL = ""
|
||||||
|
|
@ -69,7 +74,7 @@ proc parseCmdLine(): TAction =
|
||||||
result.optionalURL = key
|
result.optionalURL = key
|
||||||
of ActionSearch:
|
of ActionSearch:
|
||||||
result.search.add(key)
|
result.search.add(key)
|
||||||
of ActionList:
|
of ActionList, ActionBuild:
|
||||||
writeHelp()
|
writeHelp()
|
||||||
of cmdLongOption, cmdShortOption:
|
of cmdLongOption, cmdShortOption:
|
||||||
case key
|
case key
|
||||||
|
|
@ -90,13 +95,21 @@ proc prompt(question: string): bool =
|
||||||
else:
|
else:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
proc getBabelDir: string = return getHomeDir() / ".babel"
|
proc getNimrodVersion: TVersion =
|
||||||
|
let vOutput = execProcess("nimrod -v")
|
||||||
|
var matches: array[0..MaxSubpatterns, string]
|
||||||
|
if vOutput.find(peg"'Version'\s{(\d\.)+\d}", matches) == -1:
|
||||||
|
quit("Couldn't find Nimrod version.", QuitFailure)
|
||||||
|
newVersion(matches[0])
|
||||||
|
|
||||||
proc getLibsDir: string = return getBabelDir() / "libs"
|
let babelDir = getHomeDir() / ".babel"
|
||||||
|
let libsDir = babelDir / "libs"
|
||||||
|
let binDir = babelDir / "bin"
|
||||||
|
let nimVer = getNimrodVersion()
|
||||||
|
|
||||||
proc update(url: string = defaultPackageURL) =
|
proc update(url: string = defaultPackageURL) =
|
||||||
echo("Downloading package list from " & url)
|
echo("Downloading package list from " & url)
|
||||||
downloadFile(url, getBabelDir() / "packages.json")
|
downloadFile(url, babelDir / "packages.json")
|
||||||
echo("Done.")
|
echo("Done.")
|
||||||
|
|
||||||
proc findBabelFile(dir: string): string =
|
proc findBabelFile(dir: string): string =
|
||||||
|
|
@ -127,6 +140,11 @@ proc changeRoot(origRoot, newRoot, path: string): string =
|
||||||
raise newException(EInvalidValue,
|
raise newException(EInvalidValue,
|
||||||
"Cannot change root of path: Path does not begin with original root.")
|
"Cannot change root of path: Path does not begin with original root.")
|
||||||
|
|
||||||
|
proc doCmd(cmd: string) =
|
||||||
|
let exitCode = execCmd(cmd)
|
||||||
|
if exitCode != QuitSuccess:
|
||||||
|
quit("Execution failed with exit code " & $exitCode, QuitFailure)
|
||||||
|
|
||||||
proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
|
proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
|
||||||
for kind, file in walkDir(currentDir):
|
for kind, file in walkDir(currentDir):
|
||||||
if kind == pcDir:
|
if kind == pcDir:
|
||||||
|
|
@ -149,78 +167,189 @@ proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
|
||||||
var skip = false
|
var skip = false
|
||||||
if file.splitFile().name[0] == '.': skip = true
|
if file.splitFile().name[0] == '.': skip = true
|
||||||
for ignoreFile in pkgInfo.skipFiles:
|
for ignoreFile in pkgInfo.skipFiles:
|
||||||
|
if ignoreFile.endswith("babel"):
|
||||||
|
quit(ignoreFile & " must be installed.")
|
||||||
if samePaths(file, origDir / ignoreFile):
|
if samePaths(file, origDir / ignoreFile):
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
|
|
||||||
if not skip:
|
if not skip:
|
||||||
copyFileD(file, changeRoot(origDir, dest, file))
|
copyFileD(file, changeRoot(origDir, dest, file))
|
||||||
|
|
||||||
proc installFromDir(dir: string, latest: bool) =
|
proc getPkgInfo(dir: string): TPackageInfo =
|
||||||
let babelFile = findBabelFile(dir)
|
let babelFile = findBabelFile(dir)
|
||||||
if babelFile == "":
|
if babelFile == "":
|
||||||
quit("Specified directory does not contain a .babel file.", QuitFailure)
|
quit("Specified directory does not contain a .babel file.", QuitFailure)
|
||||||
var pkgInfo = readPackageInfo(babelFile)
|
result = readPackageInfo(babelFile)
|
||||||
|
|
||||||
let pkgDestDir = getLibsDir() / (pkgInfo.name &
|
# TODO: Move to packageinfo.nim
|
||||||
|
|
||||||
|
proc getInstalledPkgs(): seq[tuple[path: string, info: TPackageInfo]] =
|
||||||
|
## Gets a list of installed packages
|
||||||
|
result = @[]
|
||||||
|
for kind, path in walkDir(libsDir):
|
||||||
|
if kind == pcDir:
|
||||||
|
let babelFile = findBabelFile(path)
|
||||||
|
if babelFile != "":
|
||||||
|
result.add((path, readPackageInfo(babelFile)))
|
||||||
|
else:
|
||||||
|
# TODO: Abstract logging.
|
||||||
|
echo("WARNING: No .babel file found for ", path)
|
||||||
|
|
||||||
|
proc findPkg(pkglist: seq[tuple[path: string, info: TPackageInfo]],
|
||||||
|
dep: tuple[name: string, ver: PVersionRange],
|
||||||
|
r: var tuple[path: string, info: TPackageInfo]): bool =
|
||||||
|
for pkg in pkglist:
|
||||||
|
if pkg.info.name != dep.name: continue
|
||||||
|
if withinRange(newVersion(pkg.info.version), dep.ver):
|
||||||
|
if not result or newVersion(r.info.version) < newVersion(pkg.info.version):
|
||||||
|
r = pkg
|
||||||
|
result = true
|
||||||
|
|
||||||
|
proc install(packages: seq[String], verRange: PVersionRange): string {.discardable.}
|
||||||
|
proc processDeps(pkginfo: TPackageInfo): seq[string] =
|
||||||
|
## Verifies and installs dependencies.
|
||||||
|
##
|
||||||
|
## Returns the list of paths to pass to the compiler during build phase.
|
||||||
|
result = @[]
|
||||||
|
let pkglist = getInstalledPkgs()
|
||||||
|
for dep in pkginfo.requires:
|
||||||
|
if dep.name == "nimrod":
|
||||||
|
if not withinRange(nimVer, dep.ver):
|
||||||
|
quit("Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")")
|
||||||
|
else:
|
||||||
|
echo("Looking for ", dep.name, " (", $dep.ver, ")...")
|
||||||
|
var pkg: tuple[path: string, info: TPackageInfo]
|
||||||
|
if not findPkg(pkglist, dep, pkg):
|
||||||
|
let dest = install(@[dep.name], dep.ver)
|
||||||
|
if dest != "":
|
||||||
|
# only add if not a binary package
|
||||||
|
result.add(dest)
|
||||||
|
else:
|
||||||
|
echo("Dependency already satisfied.")
|
||||||
|
if pkg.info.bin.len == 0:
|
||||||
|
result.add(pkg.path)
|
||||||
|
|
||||||
|
proc buildFromDir(dir: string, paths: seq[string]) =
|
||||||
|
## Builds a package which resides in ``dir``
|
||||||
|
var pkgInfo = getPkgInfo(dir)
|
||||||
|
var args = ""
|
||||||
|
for path in paths: args.add("--path:" & path & " ")
|
||||||
|
for bin in pkgInfo.bin:
|
||||||
|
echo("Building ", pkginfo.name, "/", bin, "...")
|
||||||
|
echo(args)
|
||||||
|
doCmd("nimrod c -d:release " & args & dir / bin)
|
||||||
|
|
||||||
|
proc installFromDir(dir: string, latest: bool): string =
|
||||||
|
## Returns where package has been installed to. If package is a binary,
|
||||||
|
## ``""`` is returned.
|
||||||
|
var pkgInfo = getPkgInfo(dir)
|
||||||
|
let pkgDestDir = libsDir / (pkgInfo.name &
|
||||||
(if latest: "" else: '-' & pkgInfo.version))
|
(if latest: "" else: '-' & pkgInfo.version))
|
||||||
if not existsDir(pkgDestDir):
|
if existsDir(pkgDestDir):
|
||||||
createDir(pkgDestDir)
|
|
||||||
else:
|
|
||||||
if not prompt("Package already exists. Overwrite?"):
|
if not prompt("Package already exists. Overwrite?"):
|
||||||
quit(QuitSuccess)
|
quit(QuitSuccess)
|
||||||
removeDir(pkgDestDir)
|
removeDir(pkgDestDir)
|
||||||
createDir(pkgDestDir)
|
|
||||||
|
|
||||||
copyFilesRec(dir, dir, pkgDestDir, pkgInfo)
|
echo("Installing ", pkginfo.name, "-", pkginfo.version)
|
||||||
echo(pkgInfo.name & " installed successfully.")
|
|
||||||
|
# Dependencies need to be processed before the creation of the pkg dir.
|
||||||
proc doCmd(cmd: string) =
|
let paths = processDeps(pkginfo)
|
||||||
let exitCode = execCmd(cmd)
|
|
||||||
if exitCode != QuitSuccess:
|
createDir(pkgDestDir)
|
||||||
quit("Execution failed with exit code " & $exitCode, QuitFailure)
|
if pkgInfo.bin.len > 0:
|
||||||
|
buildFromDir(dir, paths)
|
||||||
|
createDir(binDir)
|
||||||
|
for bin in pkgInfo.bin:
|
||||||
|
copyFileD(dir / bin, binDir / bin)
|
||||||
|
let currentPerms = getFilePermissions(binDir / bin)
|
||||||
|
setFilePermissions(binDir / bin, currentPerms + {fpUserExec})
|
||||||
|
# Copy the .babel to lib/
|
||||||
|
let babelFile = findBabelFile(dir)
|
||||||
|
copyFileD(babelFile, changeRoot(dir, pkgDestDir, babelFile))
|
||||||
|
result = ""
|
||||||
|
else:
|
||||||
|
copyFilesRec(dir, dir, pkgDestDir, pkgInfo)
|
||||||
|
echo(pkgInfo.name & " installed successfully.")
|
||||||
|
result = pkgDestDir
|
||||||
|
|
||||||
proc getDVCSTag(pkg: TPackage): string =
|
proc getDVCSTag(pkg: TPackage): string =
|
||||||
result = pkg.dvcsTag
|
result = pkg.dvcsTag
|
||||||
if result == "":
|
if result == "":
|
||||||
result = pkg.version
|
result = pkg.version
|
||||||
|
|
||||||
proc install(packages: seq[String]) =
|
proc getTagsList(dir: string): seq[string] =
|
||||||
if packages == @[]:
|
let output = execProcess("cd \"" & dir & "\" && git tag")
|
||||||
installFromDir(getCurrentDir(), false)
|
if output.len > 0:
|
||||||
|
result = output.splitLines()
|
||||||
else:
|
else:
|
||||||
if not existsFile(getBabelDir() / "packages.json"):
|
result = @[]
|
||||||
|
|
||||||
|
proc getVersionList(dir: string): TTable[TVersion, string] =
|
||||||
|
# Returns: TTable of version -> git tag name
|
||||||
|
result = initTable[TVersion, string]()
|
||||||
|
let tags = getTagsList(dir)
|
||||||
|
for tag in tags:
|
||||||
|
let i = skipUntil(tag, digits) # skip any chars before the version
|
||||||
|
# TODO: Better checking, tags can have any names. Add warnings and such.
|
||||||
|
result[newVersion(tag[i .. -1])] = tag
|
||||||
|
|
||||||
|
proc install(packages: seq[String], verRange: PVersionRange): string =
|
||||||
|
if packages == @[]:
|
||||||
|
result = installFromDir(getCurrentDir(), false)
|
||||||
|
else:
|
||||||
|
if not existsFile(babelDir / "packages.json"):
|
||||||
quit("Please run babel update.", QuitFailure)
|
quit("Please run babel update.", QuitFailure)
|
||||||
for p in packages:
|
for p in packages:
|
||||||
var pkg: TPackage
|
var pkg: TPackage
|
||||||
if getPackage(p, getBabelDir() / "packages.json", pkg):
|
if getPackage(p, babelDir / "packages.json", pkg):
|
||||||
let downloadDir = (getTempDir() / "babel" / pkg.name)
|
let downloadDir = (getTempDir() / "babel" / pkg.name)
|
||||||
let dvcsTag = getDVCSTag(pkg)
|
#let dvcsTag = getDVCSTag(pkg)
|
||||||
case pkg.downloadMethod
|
case pkg.downloadMethod
|
||||||
of "git":
|
of "git":
|
||||||
echo("Executing git...")
|
echo("Executing git...")
|
||||||
if existsDir(downloadDir / ".git"):
|
if existsDir(downloadDir / ".git"):
|
||||||
doCmd("cd "& downloadDir &" && git pull")
|
doCmd("cd " & downloadDir & " && git pull")
|
||||||
else:
|
else:
|
||||||
removeDir(downloadDir)
|
removeDir(downloadDir)
|
||||||
doCmd("git clone --depth 1 " & pkg.url & " " & downloadDir)
|
doCmd("git clone --depth 1 " & pkg.url & " " & downloadDir)
|
||||||
|
|
||||||
if dvcsTag != "":
|
# TODO: Determine if version is a commit hash, if it is. Move the
|
||||||
doCmd("cd \"" & downloadDir & "\" && git checkout " & dvcsTag)
|
# git repo to ``babelDir/libs``, then babel can simply checkout
|
||||||
|
# the correct hash instead of constantly cloning and copying.
|
||||||
|
let versions = getVersionList(downloadDir)
|
||||||
|
if versions.len > 0:
|
||||||
|
let latest = findLatest(verRange, versions)
|
||||||
|
|
||||||
|
if latest.tag != "":
|
||||||
|
doCmd("cd \"" & downloadDir & "\" && git checkout " & latest.tag)
|
||||||
|
elif verRange.kind != verAny:
|
||||||
|
let pkginfo = getPkgInfo(downloadDir)
|
||||||
|
if pkginfo.version.newVersion notin verRange:
|
||||||
|
raise newException(EBabel,
|
||||||
|
"No versions of " & pkg.name &
|
||||||
|
" exist (this usually means that `git tag` returned nothing)." &
|
||||||
|
"Git HEAD also does not satisfy version range: " & $verRange)
|
||||||
|
# We use GIT HEAD if it satisfies our ver range
|
||||||
|
|
||||||
else: quit("Unknown download method: " & pkg.downloadMethod, QuitFailure)
|
else: quit("Unknown download method: " & pkg.downloadMethod, QuitFailure)
|
||||||
|
|
||||||
installFromDir(downloadDir, dvcsTag == "")
|
result = installFromDir(downloadDir, false)
|
||||||
else:
|
else:
|
||||||
quit("Package not found.", QuitFailure)
|
raise newException(EBabel, "Package not found.")
|
||||||
|
|
||||||
|
proc build =
|
||||||
|
var pkgInfo = getPkgInfo(getCurrentDir())
|
||||||
|
let paths = processDeps(pkginfo)
|
||||||
|
buildFromDir(getCurrentDir(), paths)
|
||||||
|
|
||||||
proc search(action: TAction) =
|
proc search(action: TAction) =
|
||||||
assert action.typ == ActionSearch
|
assert action.typ == ActionSearch
|
||||||
if action.search == @[]:
|
if action.search == @[]:
|
||||||
quit("Please specify a search string.", QuitFailure)
|
quit("Please specify a search string.", QuitFailure)
|
||||||
if not existsFile(getBabelDir() / "packages.json"):
|
if not existsFile(babelDir / "packages.json"):
|
||||||
quit("Please run babel update.", QuitFailure)
|
quit("Please run babel update.", QuitFailure)
|
||||||
let pkgList = getPackageList(getBabelDir() / "packages.json")
|
let pkgList = getPackageList(babelDir / "packages.json")
|
||||||
var notFound = true
|
var notFound = true
|
||||||
for pkg in pkgList:
|
for pkg in pkgList:
|
||||||
for word in action.search:
|
for word in action.search:
|
||||||
|
|
@ -241,9 +370,9 @@ proc search(action: TAction) =
|
||||||
echo("No package found.")
|
echo("No package found.")
|
||||||
|
|
||||||
proc list =
|
proc list =
|
||||||
if not existsFile(getBabelDir() / "packages.json"):
|
if not existsFile(babelDir / "packages.json"):
|
||||||
quit("Please run babel update.", QuitFailure)
|
quit("Please run babel update.", QuitFailure)
|
||||||
let pkgList = getPackageList(getBabelDir() / "packages.json")
|
let pkgList = getPackageList(babelDir / "packages.json")
|
||||||
for pkg in pkgList:
|
for pkg in pkgList:
|
||||||
echoPackage(pkg)
|
echoPackage(pkg)
|
||||||
echo(" ")
|
echo(" ")
|
||||||
|
|
@ -256,22 +385,30 @@ proc doAction(action: TAction) =
|
||||||
else:
|
else:
|
||||||
update()
|
update()
|
||||||
of ActionInstall:
|
of ActionInstall:
|
||||||
install(action.optionalName)
|
# TODO: Allow user to specify version.
|
||||||
|
install(action.optionalName, PVersionRange(kind: verAny))
|
||||||
of ActionSearch:
|
of ActionSearch:
|
||||||
search(action)
|
search(action)
|
||||||
of ActionList:
|
of ActionList:
|
||||||
list()
|
list()
|
||||||
|
of ActionBuild:
|
||||||
|
build()
|
||||||
of ActionNil:
|
of ActionNil:
|
||||||
assert false
|
assert false
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
if not existsDir(getBabelDir()):
|
if not existsDir(babelDir):
|
||||||
createDir(getBabelDir())
|
createDir(babelDir)
|
||||||
if not existsDir(getLibsDir()):
|
if not existsDir(libsDir):
|
||||||
createDir(getLibsDir())
|
createDir(libsDir)
|
||||||
|
|
||||||
parseCmdLine().doAction()
|
|
||||||
|
|
||||||
|
when defined(release):
|
||||||
|
try:
|
||||||
|
parseCmdLine().doAction()
|
||||||
|
except EBabel:
|
||||||
|
quit("FAILURE: " & getCurrentExceptionMsg())
|
||||||
|
else:
|
||||||
|
parseCmdLine().doAction()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type
|
||||||
skipDirs*: seq[string]
|
skipDirs*: seq[string]
|
||||||
skipFiles*: seq[string]
|
skipFiles*: seq[string]
|
||||||
requires*: seq[tuple[name: string, ver: PVersionRange]]
|
requires*: seq[tuple[name: string, ver: PVersionRange]]
|
||||||
|
bin*: seq[string]
|
||||||
|
|
||||||
TPackage* = object
|
TPackage* = object
|
||||||
name*: string
|
name*: string
|
||||||
|
|
@ -32,6 +33,7 @@ proc initPackageInfo(): TPackageInfo =
|
||||||
result.skipDirs = @[]
|
result.skipDirs = @[]
|
||||||
result.skipFiles = @[]
|
result.skipFiles = @[]
|
||||||
result.requires = @[]
|
result.requires = @[]
|
||||||
|
result.bin = @[]
|
||||||
|
|
||||||
proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) =
|
proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) =
|
||||||
if pkgInfo.name == "":
|
if pkgInfo.name == "":
|
||||||
|
|
@ -47,9 +49,13 @@ proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) =
|
||||||
|
|
||||||
proc parseRequires(req: string): tuple[name: string, ver: PVersionRange] =
|
proc parseRequires(req: string): tuple[name: string, ver: PVersionRange] =
|
||||||
try:
|
try:
|
||||||
var i = skipUntil(req, whitespace)
|
if ' ' in req:
|
||||||
result.name = req[0 .. i]
|
var i = skipUntil(req, whitespace)
|
||||||
result.ver = parseVersionRange(req[i .. -1])
|
result.name = req[0 .. i].strip
|
||||||
|
result.ver = parseVersionRange(req[i .. -1])
|
||||||
|
else:
|
||||||
|
result.name = req.strip
|
||||||
|
result.ver = PVersionRange(kind: verAny)
|
||||||
except EParseVersion:
|
except EParseVersion:
|
||||||
quit("Unable to parse dependency version range: " & getCurrentExceptionMsg())
|
quit("Unable to parse dependency version range: " & getCurrentExceptionMsg())
|
||||||
|
|
||||||
|
|
@ -80,12 +86,15 @@ proc readPackageInfo*(path: string): TPackageInfo =
|
||||||
result.skipDirs.add(ev.value.split(','))
|
result.skipDirs.add(ev.value.split(','))
|
||||||
of "skipfiles":
|
of "skipfiles":
|
||||||
result.skipFiles.add(ev.value.split(','))
|
result.skipFiles.add(ev.value.split(','))
|
||||||
|
of "bin":
|
||||||
|
result.bin = ev.value.split(',')
|
||||||
else:
|
else:
|
||||||
quit("Invalid field: " & ev.key, QuitFailure)
|
quit("Invalid field: " & ev.key, QuitFailure)
|
||||||
of "deps", "dependencies":
|
of "deps", "dependencies":
|
||||||
case ev.key.normalize
|
case ev.key.normalize
|
||||||
of "requires":
|
of "requires":
|
||||||
result.requires.add(parseRequires(ev.value))
|
for v in ev.value.split(','):
|
||||||
|
result.requires.add(parseRequires(v.strip))
|
||||||
else:
|
else:
|
||||||
quit("Invalid field: " & ev.key, QuitFailure)
|
quit("Invalid field: " & ev.key, QuitFailure)
|
||||||
else: quit("Invalid section: " & currentSection, QuitFailure)
|
else: quit("Invalid section: " & currentSection, QuitFailure)
|
||||||
|
|
@ -94,7 +103,7 @@ proc readPackageInfo*(path: string): TPackageInfo =
|
||||||
echo(ev.msg)
|
echo(ev.msg)
|
||||||
close(p)
|
close(p)
|
||||||
else:
|
else:
|
||||||
quit("Cannot open package info: " & path, QuitFailure)
|
raise newException(EInvalidValue, "Cannot open package info: " & path)
|
||||||
validatePackageInfo(result, path)
|
validatePackageInfo(result, path)
|
||||||
|
|
||||||
proc optionalField(obj: PJsonNode, name: string): string =
|
proc optionalField(obj: PJsonNode, name: string): string =
|
||||||
|
|
|
||||||
65
version.nim
65
version.nim
|
|
@ -2,7 +2,7 @@
|
||||||
# BSD License. Look at license.txt for more info.
|
# BSD License. Look at license.txt for more info.
|
||||||
|
|
||||||
## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5``
|
## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5``
|
||||||
import strutils
|
import strutils, tables, hashes, parseutils
|
||||||
type
|
type
|
||||||
TVersion* = distinct string
|
TVersion* = distinct string
|
||||||
|
|
||||||
|
|
@ -31,17 +31,18 @@ proc newVersion*(ver: string): TVersion = return TVersion(ver)
|
||||||
|
|
||||||
proc `$`*(ver: TVersion): String {.borrow.}
|
proc `$`*(ver: TVersion): String {.borrow.}
|
||||||
|
|
||||||
|
proc hash*(ver: TVersion): THash {.borrow.}
|
||||||
|
|
||||||
proc `<`*(ver: TVersion, ver2: TVersion): Bool =
|
proc `<`*(ver: TVersion, ver2: TVersion): Bool =
|
||||||
var sVer = string(ver).split('.')
|
var sVer = string(ver).split('.')
|
||||||
var sVer2 = string(ver2).split('.')
|
var sVer2 = string(ver2).split('.')
|
||||||
for i in 0..max(sVer.len, sVer2.len)-1:
|
for i in 0..max(sVer.len, sVer2.len)-1:
|
||||||
if i > sVer.len-1:
|
var sVerI = 0
|
||||||
return True
|
if i < sVer.len:
|
||||||
elif i > sVer2.len-1:
|
discard parseInt(sVer[i], sVerI)
|
||||||
return False
|
var sVerI2 = 0
|
||||||
|
if i < sVer2.len:
|
||||||
var sVerI = parseInt(sVer[i])
|
discard parseInt(sVer2[i], sVerI2)
|
||||||
var sVerI2 = parseInt(sVer2[i])
|
|
||||||
if sVerI < sVerI2:
|
if sVerI < sVerI2:
|
||||||
return True
|
return True
|
||||||
elif sVerI == sVerI2:
|
elif sVerI == sVerI2:
|
||||||
|
|
@ -49,7 +50,20 @@ proc `<`*(ver: TVersion, ver2: TVersion): Bool =
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
proc `==`*(ver: TVersion, ver2: TVersion): Bool {.borrow.}
|
proc `==`*(ver: TVersion, ver2: TVersion): Bool =
|
||||||
|
var sVer = string(ver).split('.')
|
||||||
|
var sVer2 = string(ver2).split('.')
|
||||||
|
for i in 0..max(sVer.len, sVer2.len)-1:
|
||||||
|
var sVerI = 0
|
||||||
|
if i < sVer.len:
|
||||||
|
discard parseInt(sVer[i], sVerI)
|
||||||
|
var sVerI2 = 0
|
||||||
|
if i < sVer2.len:
|
||||||
|
discard parseInt(sVer2[i], sVerI2)
|
||||||
|
if sVerI == sVerI2:
|
||||||
|
result = true
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
proc `<=`*(ver: TVersion, ver2: TVersion): Bool =
|
proc `<=`*(ver: TVersion, ver2: TVersion): Bool =
|
||||||
return (ver == ver2) or (ver < ver2)
|
return (ver == ver2) or (ver < ver2)
|
||||||
|
|
@ -71,6 +85,9 @@ proc withinRange*(ver: TVersion, ran: PVersionRange): Bool =
|
||||||
of verAny:
|
of verAny:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
proc contains*(ran: PVersionRange, ver: TVersion): bool =
|
||||||
|
return withinRange(ver, ran)
|
||||||
|
|
||||||
proc makeRange*(version: string, op: string): PVersionRange =
|
proc makeRange*(version: string, op: string): PVersionRange =
|
||||||
new(result)
|
new(result)
|
||||||
if version == "":
|
if version == "":
|
||||||
|
|
@ -110,7 +127,7 @@ proc parseVersionRange*(s: string): PVersionRange =
|
||||||
result.verIRight = parseVersionRange(substr(s, i + 1))
|
result.verIRight = parseVersionRange(substr(s, i + 1))
|
||||||
|
|
||||||
# Disallow more than one verIntersect. It's pointless and could lead to
|
# Disallow more than one verIntersect. It's pointless and could lead to
|
||||||
# major unknown mistakes.
|
# major unpredictable mistakes.
|
||||||
if result.verIRight.kind == verIntersect:
|
if result.verIRight.kind == verIntersect:
|
||||||
raise newException(EParseVersion,
|
raise newException(EParseVersion,
|
||||||
"Having more than one `&` in a version range is pointless")
|
"Having more than one `&` in a version range is pointless")
|
||||||
|
|
@ -135,7 +152,6 @@ proc parseVersionRange*(s: string): PVersionRange =
|
||||||
inc(i)
|
inc(i)
|
||||||
|
|
||||||
proc `$`*(verRange: PVersionRange): String =
|
proc `$`*(verRange: PVersionRange): String =
|
||||||
echo(verRange.repr())
|
|
||||||
case verRange.kind
|
case verRange.kind
|
||||||
of verLater:
|
of verLater:
|
||||||
result = "> "
|
result = "> "
|
||||||
|
|
@ -150,7 +166,7 @@ proc `$`*(verRange: PVersionRange): String =
|
||||||
of verIntersect:
|
of verIntersect:
|
||||||
return $verRange.verILeft & " & " & $verRange.verIRight
|
return $verRange.verILeft & " & " & $verRange.verIRight
|
||||||
of verAny:
|
of verAny:
|
||||||
return "Any"
|
return "any version"
|
||||||
|
|
||||||
result.add(string(verRange.ver))
|
result.add(string(verRange.ver))
|
||||||
|
|
||||||
|
|
@ -168,10 +184,22 @@ proc newVREq*(ver: string): PVersionRange =
|
||||||
result.kind = verEq
|
result.kind = verEq
|
||||||
result.ver = newVersion(ver)
|
result.ver = newVersion(ver)
|
||||||
|
|
||||||
|
proc findLatest*(verRange: PVersionRange, versions: TTable[TVersion, string]): tuple[ver: TVersion, tag: string] =
|
||||||
|
result = (newVersion(""), "")
|
||||||
|
for ver, tag in versions:
|
||||||
|
if not withinRange(ver, verRange): continue
|
||||||
|
if ver > result.ver:
|
||||||
|
result = (ver, tag)
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
doAssert(newVersion("1.0") < newVersion("1.4"))
|
doAssert(newVersion("1.0") < newVersion("1.4"))
|
||||||
doAssert(newVersion("1.0.1") > newVersion("1.0"))
|
doAssert(newVersion("1.0.1") > newVersion("1.0"))
|
||||||
doAssert(newVersion("1.0.6") <= newVersion("1.0.6"))
|
doAssert(newVersion("1.0.6") <= newVersion("1.0.6"))
|
||||||
|
#doAssert(not withinRange(newVersion("0.1.0"), parseVersionRange("> 0.1")))
|
||||||
|
doAssert(not (newVersion("0.1.0") < newVersion("0.1")))
|
||||||
|
doAssert(not (newVersion("0.1.0") > newVersion("0.1")))
|
||||||
|
doAssert(newVersion("0.1.0") < newVersion("0.1.0.0.1"))
|
||||||
|
doAssert(newVersion("0.1.0") <= newVersion("0.1"))
|
||||||
|
|
||||||
var inter1 = parseVersionRange(">= 1.0 & <= 1.5")
|
var inter1 = parseVersionRange(">= 1.0 & <= 1.5")
|
||||||
var inter2 = parseVersionRange("1.0")
|
var inter2 = parseVersionRange("1.0")
|
||||||
|
|
@ -182,5 +210,18 @@ when isMainModule:
|
||||||
doAssert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1))
|
doAssert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1))
|
||||||
|
|
||||||
doAssert(newVersion("1") == newVersion("1"))
|
doAssert(newVersion("1") == newVersion("1"))
|
||||||
|
doAssert(newVersion("1.0.2.4.6.1.2.123") == newVersion("1.0.2.4.6.1.2.123"))
|
||||||
|
doAssert(newVersion("1.0.2") != newVersion("1.0.2.4.6.1.2.123"))
|
||||||
|
|
||||||
|
doAssert(not (newVersion("") < newVersion("0.0.0")))
|
||||||
|
doAssert(newVersion("") < newVersion("1.0.0"))
|
||||||
|
doAssert(newVersion("") < newVersion("0.1.0"))
|
||||||
|
|
||||||
|
var versions = toTable[TVersion, string]({newVersion("0.1.1"): "v0.1.1", newVersion("0.2.3"): "v0.2.3", newVersion("0.5"): "v0.5"})
|
||||||
|
doAssert findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) == (newVersion("0.2.3"), "v0.2.3")
|
||||||
|
|
||||||
|
|
||||||
|
doAssert newVersion("0.1-rc1") < newVersion("0.2")
|
||||||
|
doAssert newVersion("0.1-rc1") < newVersion("0.1")
|
||||||
|
|
||||||
echo("Everything works!")
|
echo("Everything works!")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue