Many new features: build command and dependency resolution implemented.

This commit is contained in:
Dominik Picheta 2013-06-01 17:04:57 +01:00
commit 5e01fa0342
3 changed files with 251 additions and 64 deletions

231
babel.nim
View file

@ -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()

View file

@ -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 =

View file

@ -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!")