From c6396a3bc3e83c9412299384f433e5da02dd918e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 2 Dec 2012 01:24:47 +0000 Subject: [PATCH] Re-write, mostly works. --- babel.babel | 13 +-- babel.nim | 201 ++++++++++++++++++++++++++++++++++++++++++ installer.nim | 230 ------------------------------------------------ packageinfo.nim | 71 +++++++++++++++ parser.nim | 193 ---------------------------------------- readme.markdown | 17 ++-- version.nim | 172 ------------------------------------ 7 files changed, 285 insertions(+), 612 deletions(-) create mode 100644 babel.nim delete mode 100644 installer.nim create mode 100644 packageinfo.nim delete mode 100644 parser.nim delete mode 100644 version.nim diff --git a/babel.babel b/babel.babel index 0709250..96783a5 100644 --- a/babel.babel +++ b/babel.babel @@ -1,17 +1,10 @@ -; Babel library +; Example babel file [Package] name = "babel" version = "0.1.0" author = "Dominik Picheta" -category = "Distribution" -description = """Babel framework: Specifies a common interface for programmers - to more easily build their applications in portable way.""" +description = """Jester is a web framework inspired by Sinatra.""" [Library] -Depends = "nimrod >= 0.8.10" -ExposedModules = "parser, installer, version" ; No need for .nim -;Files = "babel.babel" +SkipDirs = "tests" -;[Exe] -;Depends = "nimrod >= 0.8.11" -;Exe = "babel" diff --git a/babel.nim b/babel.nim new file mode 100644 index 0000000..dff64bc --- /dev/null +++ b/babel.nim @@ -0,0 +1,201 @@ +import httpclient, parseopt, os, strutils, osproc + +import packageinfo + +type + TActionType = enum + ActionNil, ActionUpdate, ActionInstall + + TAction = object + case typ: TActionType + of ActionNil: nil + of ActionUpdate: + optionalURL: string # Overrides default package list. + of ActionInstall: + optionalName: seq[string] # When this is @[], installs package from current dir. + +const + help = """ +Usage: babel COMMAND + +Commands: + install Installs a list of packages. + update Updates package list. A package list URL can be optionally specificed. +""" + babelVersion = "0.1.0" + defaultPackageURL = "https://github.com/nimrod-code/packages/raw/master/packages.json" + +proc writeHelp() = + echo(help) + quit(QuitSuccess) + +proc writeVersion() = + echo(babelVersion) + quit(QuitSuccess) + +proc parseCmdLine(): TAction = + result.typ = ActionNil + for kind, key, val in getOpt(): + case kind + of cmdArgument: + if result.typ == ActionNil: + case key + of "install": + result.typ = ActionInstall + result.optionalName = @[] + of "update": + result.typ = ActionUpdate + result.optionalURL = "" + else: writeHelp() + else: + case result.typ + of ActionNil: + assert false + of ActionInstall: + result.optionalName.add(key) + of ActionUpdate: + result.optionalURL = key + of cmdLongOption, cmdShortOption: + case key + of "help", "h": writeHelp() + of "version", "v": writeVersion() + of cmdEnd: assert(false) # cannot happen + if result.typ == ActionNil: + writeHelp() + +proc update(url: string = defaultPackageURL) = + echo("Downloading package list from " & url) + downloadFile(url, getHomeDir() / ".babel" / "packages.json") + echo("Done.") + +proc findBabelFile(dir: string): string = + for kind, path in walkDir(dir): + if kind == pcFile and path.splitFile.ext == ".babel": + return path + return "" + +proc copyFileD(fro, to: string) = + echo(fro, " -> ", to) + copyFile(fro, to) + +proc getBabelDir: string = return getHomeDir() / ".babel" + +proc getLibsDir: string = return getBabelDir() / "libs" + +proc samePaths(p1, p2: string): bool = + ## Normalizes path (by adding a trailing slash) and compares. + let cp1 = if not p1.endsWith("/"): p1 & "/" else: p1 + let cp2 = if not p2.endsWith("/"): p2 & "/" else: p2 + return cmpPaths(cp1, cp2) == 0 + +proc changeRoot(origRoot, newRoot, path: string): string = + ## origRoot: /home/dom/ + ## newRoot: /home/test/ + ## path: /home/dom/bar/blah/2/foo.txt + ## Return value -> /home/test/bar/blah/2/foo.txt + if path.startsWith(origRoot): + return newRoot / path[origRoot.len .. -1] + else: + raise newException(EInvalidValue, + "Cannot change root of path: Path does not begin with original root.") + +proc copyFilesRec(origDir, currentDir: string, pkgInfo: TPackageInfo) = + for kind, file in walkDir(currentDir): + if kind == pcDir: + var skip = false + for ignoreDir in pkgInfo.skipDirs: + if samePaths(file, origDir / ignoreDir): + skip = true + break + let thisDir = splitPath(file).tail + assert thisDir != "" + if thisDir[0] == '.': skip = true + if thisDir == "nimcache": skip = true + + if skip: continue + # Create the dir. + createDir(changeRoot(origDir, getLibsDir() / pkgInfo.name, file)) + + copyFilesRec(origDir, file, pkgInfo) + else: + var skip = false + if file.splitFile().name[0] == '.': skip = true + if file.splitFile().ext == "": skip = true + for ignoreFile in pkgInfo.skipFiles: + if samePaths(file, origDir / ignoreFile): + skip = true + break + + if not skip: + copyFileD(file, changeRoot(origDir, getLibsDir() / pkgInfo.name, file)) + +proc installFromDir(dir: string) = + let babelFile = findBabelFile(dir) + if babelFile == "": + quit("Specified directory does not contain a .babel file.", QuitFailure) + var pkgInfo = readPackageInfo(babelFile) + if not existsDir(getLibsDir() / pkgInfo.name): + createDir(getLibsDir() / pkgInfo.name) + else: echo("Warning: Package already exists.") + + # Find main project file. + let nimFile = dir / pkgInfo.name.addFileExt("nim") + let nimrodFile = dir / pkgInfo.name.addFileExt("nimrod") + if existsFile(nimFile) or existsFile(nimrodFile): + if existsFile(nimFile): + copyFileD(nimFile, changeRoot(dir, getLibsDir(), nimFile)) + pkgInfo.skipFiles.add(changeRoot(dir, "", nimFile)) + elif existsFile(nimrodFile): + copyFileD(nimrodFile, changeRoot(dir, getLibsDir(), nimrodFile)) + pkgInfo.skipFiles.add(changeRoot(dir, "", nimrodFile)) + else: + quit("Could not find main package file.", QuitFailure) + + copyFilesRec(dir, dir, pkgInfo) + +proc install(packages: seq[String]) = + if packages == @[]: + installFromDir(getCurrentDir()) + else: + if not existsFile(getBabelDir() / "packages.json"): + quit("Please run babel update.", QuitFailure) + for p in packages: + var pkg: TPackage + if getPackage(p, getBabelDir() / "packages.json", pkg): + let downloadDir = (getTempDir() / "babel" / pkg.name) + case pkg.downloadMethod + of "git": + echo("Executing git...") + removeDir(downloadDir) + let exitCode = execCmd("git clone " & pkg.url & " " & downloadDir) + if exitCode != QuitSuccess: + quit("Execution of git failed.", QuitFailure) + else: quit("Unknown download method: " & pkg.downloadMethod, QuitFailure) + installFromDir(downloadDir) + else: + quit("Package not found.", QuitFailure) + +proc doAction(action: TAction) = + case action.typ + of ActionUpdate: + if action.optionalURL != "": + update(action.optionalURL) + else: + update() + of ActionInstall: + install(action.optionalName) + of ActionNil: + assert false + +when isMainModule: + if not existsDir(getHomeDir() / ".babel"): + createDir(getHomeDir() / ".babel") + if not existsDir(getHomeDir() / ".babel" / "libs"): + createDir(getHomeDir() / ".babel" / "libs") + + parseCmdLine().doAction() + + + + + \ No newline at end of file diff --git a/installer.nim b/installer.nim deleted file mode 100644 index 1f2a41a..0000000 --- a/installer.nim +++ /dev/null @@ -1,230 +0,0 @@ -import parser, version, osproc, strutils, re, os, parseutils, streams - -type - EInstall = object of EBase - - TDepend = tuple[name: String, verRange: PVersionRange] - -var debug* = 1 ## 0 = no messages, 1 - 3 = less to more verbose respectively. - -proc echoD*(s: string, verbosity: int = 1, nl: bool = True) = - if debug >= verbosity: - stdout.write(s & (if nl: "\n" else: "")) - -proc getBabelDir(): string = - when defined(windows): - result = getHomeDir() / "babel" - else: - result = getHomeDir() / ".babel" - -proc getNimVersion(cmd: string = "nimrod"): String = - var output = execProcess(cmd & " -v") - var line = splitLines(output)[0] - - # Thanks Araq :) - var i = 0 - var nimrodVersion = "" - i = skipIgnoreCase(line, "Nimrod Compiler Version ") - if i <= 0: raise newException(EInstall, "Cannot detect Nimrod's version") - i = parseToken(line, nimrodVersion, {'.', '0'..'9'}, i) - if nimrodVersion.len == 0: - raise newException(EInstall, "Cannot detect Nimrod's version") - - return nimrodVersion - -proc compile*(file: string, flags: string = "") = - var args: string = flags & "c " & file - echoD("Compiling " & file & "...") - var code = execShellCmd(findExe("nimrod") & " " & args) - if code != quitSuccess: - raise newException(EInstall, "Compilation failed: Nimrod returned exit code " & - $code) - -proc dependExists(name: string, verRange: PVersionRange): Bool = - if name == "nimrod": - var nimVer = getNimVersion() - if not withinRange(newVersion(nimVer), verRange): - raise newException(EInstall, "Nimrod version doesn't satisfy dependency: " & - nimVer & " " & $verRange) - else: return True - else: - for kind, path in walkDir(getBabelDir() / "packages"): - if kind == pcFile: - var file = path.extractFilename() - if file.startswith(name & "-"): - if verRange.kind != verAny: - var ver = "" - # This will have a dot at the end, it doesn't cause trouble though. - var ret = file.parseToken(ver, digits + {'.'}, name.len() + 1) - if ret != 0: - if withinRange(newVersion(ver), verRange): - return True - else: - raise newException(EInstall, "Invalid .babel file: " & file) - else: return True - - return False - -proc verifyDepends(proj: TProject): seq[TDepend] = - result = @[] - for nameStr, verStr in items(proj.depends): - echoD(" " & nameStr & " " & verStr & "...", 2, False) - var verRange: PVersionRange - if verStr == "": - new(verRange) - verRange.kind = verAny - else: - verRange = parseVersionRange(verStr) - - if not dependExists(nameStr, verRange): - result.add((nameStr, verRange)) - echoD(" FAIL", 2) - else: - echoD(" OK", 2) - -proc createDirDebug(dir: string) = - # Have to check, so that it doesn't echo... - if not existsDir(dir): - echoD("Creating directory " & dir & "...", 3, False) - createDir(dir) - echoD(" Done!", 3) - -proc createDirs(dirs: seq[string]) = - for i in items(dirs): - createDirDebug(i) - -proc copyFileDebug(file, copyTo: string) = - echoD("Copying " & file & " to " & copyTo & "...", 3, False) - copyFile(file, copyTo) - echoD(" Done!", 3) - -proc moveFileDebug(file, moveTo: string) = - echoD("Moving " & file & " to " & moveTo & "...", 3, False) - moveFile(file, moveTo) - echoD(" Done!", 3) - -proc copyFiles(proj: TProject) = - # This will create a $home/.babel and lib/ or bin/. It will also copy all the - # files listed in proj.modules and proj.files and the .babel file. - var babelDir = getBabelDir() - - var dirs = @[babelDir, babelDir / "lib", babelDir / "bin", babelDir / "packages"] - createDirs(dirs) - if proj.library: - # TODO: How will we handle multiple versions? - var projDir = babelDir / "lib" / proj.name # $babel/lib/name - createDirDebug(projDir) - # Copy the files - for i in items(proj.modules): - var file = proj.confDir / i.addFileExt("nim") - var copyTo = projDir / i.addFileExt("nim") - copyFileDebug(file, copyTo) - if proj.files.len > 0: - for i in items(proj.files): - var file = proj.confDir / i - var copyTo = projDir / i - copyFileDebug(file, copyTo) - - elif proj.executable: - var exeSrcFile = proj.confDir / proj.exeFile.addFileExt("nim") - # Compile - compile(exeSrcFile) - - var exeCmpFile = exeSrcFile.changeFileExt(ExeExt) - var copyTo = babelDir / "bin" / proj.exeFile.addFileExt(ExeExt) - copyFileDebug(exeCmpFile, copyTo) - - # Copy the .babel file into the packages folder. - var babelFile = proj.confDir / proj.name.addFileExt("babel") - # addFileExt fails in this situation. Although it's fine without .babel - var copyTo = babelDir / "packages" / - (proj.name & "-" & proj.version) - copyFileDebug(babelFile, copyTo) - - - -proc upgrade(proj: TProject) = - ## This proc moves the files in lib/package into lib/package/lastVersion - ## It then copies the new version into lib/package. - - var babelDir = getBabelDir() - # Find the config for the latest (currently installed) package version in - # packages/ - var latestVersion = "0" - var path = "" - for kind, confPath in walkDir(babelDir / "packages"): - if kind == pcFile: - var file = confPath.extractFilename() - if file.startsWith(proj.name & "-"): - var ver = "" - # This will have a dot at the end, it doesn't cause trouble though. - discard file.parseToken(ver, digits + {'.'}, proj.name.len() + 1) - if ver > latestVersion: - latestVersion = ver - path = confPath - - assert(path != "") - - if proj.library: - echoD("Reading " & path & "...", 3) - var latestConf = parseBabel(path) - var newVerDir = babelDir / "lib" / latestConf.name / latestConf.version - createDirDebug(newVerDir) - # Move the files - for kind, path in walkDir(babelDir / "lib" / latestConf.name): - if kind == pcFile: - moveFileDebug(path, newVerDir / path.extractFilename()) - - # Install the new version - copyFiles(proj) - - -proc install*(name: string, filename: string = "") = - ## Install package by the name of ``name``, filename specifies where to look for it - ## if left as "", the current working directory will be assumed. - # TODO: Add a `debug` variable? If true the status messages get echo-ed, - # vice-versa if false? - var babelFile: TProject - var path = "" - if filename == "": - path = name.addFileExt("babel") - else: - path = filename / name.addFileExt("babel") - - echoD("Reading " & path & "...", 3) - babelFile = parseBabel(path) - - var ret = babelFile.verify() - if ret != "": - raise newException(EInstall, "Verifying the .babel file failed: " & ret) - - var upgrade = False # Specifies whether to upgrade - # Check whether this package is already installed - if dependExists(babelFile.name, newVREq(babelFile.version)): - raise newException(EInstall, - "This package is already installed!") - elif dependExists(babelFile.name, newVREarlier(babelFile.version)): - upgrade = True - - if babelFile.depends.len == 1: - echoD("Verifying 1 dependency...") - else: - echoD("Verifying " & $babelFile.depends.len() & " dependencies...") - var dependsNeeded = babelFile.verifyDepends() - if dependsNeeded.len() > 0: - raise newException(EInstall, "TODO: Download & Install dependencies.") - else: - echoD("All dependencies verified!") - - if not upgrade: - echoD("Installing " & babelFile.name & "-" & babelFile.version & "...") - babelFile.copyFiles() - else: - echoD("Upgrading " & babelFile.name & " to version " & - babelFile.version & "...") - babelFile.upgrade() - - echoD("Package " & babelFile.name & " successfully installed.") - -when isMainModule: - install(paramStr(1)) diff --git a/packageinfo.nim b/packageinfo.nim new file mode 100644 index 0000000..8de5f5d --- /dev/null +++ b/packageinfo.nim @@ -0,0 +1,71 @@ +import parsecfg, json, streams, strutils +type + TPackageInfo* = object + name*: string + version*: string + author*: string + description*: string + library*: bool + skipDirs*: seq[string] + skipFiles*: seq[string] + + TPackage* = object + name*: string + url*: string + downloadMethod*: string + tags*: seq[string] + description*: string + +proc readPackageInfo*(path: string): TPackageInfo = + result.skipDirs = @[] + result.skipFiles = @[] + 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 "library": + case ev.key.normalize + of "skipdirs": + result.skipDirs.add(ev.value.split(',')) + of "skipfiles": + result.skipFiles.add(ev.value.split(',')) + 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: + quit("Cannot open package info: " & path, QuitFailure) + +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["url"].str + resPkg.downloadMethod = p["method"].str + resPkg.tags = @[] + for t in p["tags"]: + resPkg.tags.add(t.str) + resPkg.description = p["description"].str + return true + return false + + + \ No newline at end of file diff --git a/parser.nim b/parser.nim deleted file mode 100644 index 74a7e9b..0000000 --- a/parser.nim +++ /dev/null @@ -1,193 +0,0 @@ -import parsecfg, streams, strutils, os - -type - TProject* = object - name*: String # Req - version*: String # Req - author*: String # Req - category*: String # Req - desc*: String # Req - license*: String - homepage*: String - - library*: bool - depends*: seq[tuple[name, vRange: string]] # Dependencies - modules*: seq[string] # ExtraModules - files*: seq[string] # files - - executable*: bool - exeFile*: string - - unknownFields*: seq[string] # TODO: - - confDir*: string # Directory of the babel file, "" if current work dir. - - EParseErr* = object of EInvalidValue - -proc initProj(): TProject = - result.name = "" - result.version = "" - result.author = "" - result.category = "" - result.desc = "" - result.license = "" - result.homepage = "" - - result.library = False - result.executable = False - result.depends = @[] - result.modules = @[] - result.files = @[] - result.exeFile = "" - - result.unknownFields = @[] - - result.confDir = "" - -proc parseList(s: string): seq[string] = - result = @[] - var many = s.split({',', ';'}) - for i in items(many): - result.add(i.strip()) - -proc skipUntil(s, token: string, o: var string): int = - ## Captures each char until `token` is found, sets `o` to the captured string, - ## and returns the number of characters captured. - var i = 0 - while True: - case s[i] - of '\0': - break - else: - var found = False - for t in 0..len(token)-1: - if s[i+t] == token[t]: - found = True - else: - found = False - break - if found: - break - inc(i) - o = copy(s, 0, i-1) - return i - -proc parseDepends(s: string): seq[tuple[name, vRange: string]] = - result = @[] - var many = s.split({',', ';'}) - for i in items(many): - var stripped = i.strip() - var name = "" - var index = skipUntil(stripped, " ", name) - var vRange = stripped.copy(index) - result.add((name.strip(), vRange.strip())) - -proc parseErr(p: TCfgParser, msg: string) = - raise newException(EParseErr, "(" & $p.getLine() & ", " & - $p.getColumn() & ") " & msg) - -proc parseBabel*(file: string): TProject = - result = initProj() - result.confDir = splitFile(file).dir - - var f = newFileStream(file, fmRead) - if f != nil: - var p: TCfgParser - open(p, f, file) - - var section: String = "" - while true: - var e = next(p) - case e.kind - of cfgEof: - break - of cfgKeyValuePair: - case section - of "package": - case normalize(e.key): - of "name": - result.name = e.value - of "version": - result.version = e.value - of "author": - result.author = e.value - of "category": - result.category = e.value - of "description": - result.desc = e.value - of "homepage": - result.homepage = e.value - of "license": - result.license = e.value - else: - p.parseErr("Unknown key: " & e.key) - of "library": - case normalize(e.key) - of "depends": - result.depends = e.value.parseDepends() - of "files": - result.files = e.value.parseList() - of "exposedmodules": - result.modules = e.value.parseList() - else: - p.parseErr("Unknown key: " & e.key) - of "exe": - case normalize(e.key) - of "depends": - result.depends = e.value.parseDepends() - of "files": - result.files = e.value.parseList() - of "exe": - result.exeFile = e.value - else: - p.parseErr("Unknown key: " & e.key) - - else: - p.parseErr("Unknown section: " & section) - - of cfgSectionStart: - section = normalize(e.section) - case normalize(e.section): - of "library": - result.library = True - of "exe": - result.executable = True - of "package": - nil - else: - p.parseErr("Unknown section: " & section) - - of cfgError: - p.parseErr(e.msg) - - of cfgOption: - p.parseErr("Unknown option: " & e.key) - - close(p) - else: - raise newException(EIO, "Cannot open " & file) - -proc isEmpty(s: string): Bool = return s == "" - -proc verify*(proj: TProject): string = - ## Checks whether the required fields have been specified. - if isEmpty(proj.name) or isEmpty(proj.version) or isEmpty(proj.author) or - isEmpty(proj.category) or isEmpty(proj.desc): - return "Missing required fields." - elif proj.library == false and proj.executable == false: - return "Either a valid Library needs to be specified or a valid Bin." - elif proj.library == true and proj.modules.len() == 0: - return "A valid library needs at least one ExposedModule listed." - # TODO: Rules for Bin. - - return "" - -when isMainModule: - for i in items(parseList("test, asdasd >sda; jsj, kk >>, sd")): - echo(i) - var project = parseBabel("babel.babel") - echo project.library - echo() - var o = "" - echo skipUntil("nimrod >= 0.8.10", " ", o) - echo(o) diff --git a/readme.markdown b/readme.markdown index bbf3676..32a4b40 100644 --- a/readme.markdown +++ b/readme.markdown @@ -3,15 +3,18 @@ Babel is a work in progress package manager for Nimrod. ## Babel's folder structure Babel stores everything that has been installed in ~/.babel on Unix systems and -in your $home/babel on Windows. Libraries are installed in lib/ in folders -which names contain the name of the package, the folders -contain the modules and any other files that the package wished to install. -Applications are installed into bin/. There is also a packages/ directory which -contains all the packages' .babel files. +in your $home/babel on Windows. Libraries are stored in ~/.babel/libs. + +## Libraries +Libraries should contain a ``ProjectName.nim`` file, this file will be copied +to ~/.babel/libs/ProjectName.nim allowing anyone to import it by doing +``import ProjectName``. Any private files should be placed, by convention, in +a ``private`` folder, these are files which the user of your library should not +be using. Every other file and folder will be copied to ~/.babel/libs/ProjectName/. ## Contribution If you would like to help, feel free to fork and make any additions you see fit and then send a pull request. If you have any questions about the project you can ask me directly on github, -ask on the nimrod [forum](http://force7.de/heimdall), or ask on Freenode in -the #nimrod channel. +ask on the nimrod [forum](http://forum.nimrod-code.org), or ask on Freenode in +the #nimrod channel. \ No newline at end of file diff --git a/version.nim b/version.nim deleted file mode 100644 index a61598e..0000000 --- a/version.nim +++ /dev/null @@ -1,172 +0,0 @@ -## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5`` -import strutils -type - TVersion* = distinct string - - TVersionRangeEnum* = enum - verLater, # > V - verEarlier, # < V - verEqLater, # >= V -- Equal or later - verEqEarlier, # <= V -- Equal or earlier - verIntersect, # > V & < V - verEq, # V - verAny # * - - PVersionRange* = ref TVersionRange - TVersionRange* = object - case kind*: TVersionRangeEnum - of verLater, verEarlier, verEqLater, verEqEarlier, verEq: - ver*: TVersion - of verIntersect: - verILeft, verIRight: PVersionRange - of verAny: - nil - - EParseVersion = object of EInvalidValue - -proc newVersion*(ver: string): TVersion = return TVersion(ver) - -proc `$`*(ver: TVersion): String {.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: - if i > sVer.len-1: - return True - elif i > sVer2.len-1: - return False - - var sVerI = parseInt(sVer[i]) - var sVerI2 = parseInt(sVer2[i]) - if sVerI < sVerI2: - return True - elif sVerI == sVerI2: - nil - else: - return False - -proc `==`*(ver: TVersion, ver2: TVersion): Bool {.borrow.} - -proc `<=`*(ver: TVersion, ver2: TVersion): Bool = - return (ver == ver2) or (ver < ver2) - -proc withinRange*(ver: TVersion, ran: PVersionRange): Bool = - case ran.kind - of verLater: - return ver > ran.ver - of verEarlier: - return ver < ran.ver - of verEqLater: - return ver >= ran.ver - of verEqEarlier: - return ver <= ran.ver - of verEq: - return ver == ran.ver - of verIntersect: - return withinRange(ver, ran.verILeft) and withinRange(ver, ran.verIRight) - of verAny: - return True - -proc makeRange*(version: string, op: string): PVersionRange = - new(result) - case op - of ">": - result.kind = verLater - of "<": - result.kind = verEarlier - of ">=": - result.kind = verEqLater - of "<=": - result.kind = verEqEarlier - else: - raise newException(EParseVersion, "Invalid operator: " & op) - result.ver = TVersion(version) - -proc parseVersionRange*(s: string): PVersionRange = - # >= 1.5 & <= 1.8 - new(result) - - var i = 0 - var op = "" - var version = "" - while True: - case s[i] - of '>', '<', '=': - op.add(s[i]) - of '&': - result.kind = verIntersect - result.verILeft = makeRange(version, op) - - # Parse everything after & - # Recursion <3 - result.verIRight = parseVersionRange(copy(s, i + 1)) - - # Disallow more than one verIntersect. It's pointless and could lead to - # major unknown mistakes. - if result.verIRight.kind == verIntersect: - raise newException(EParseVersion, - "Having more than one `&` in a version range is pointless") - - break - - of '0'..'9', '.': - version.add(s[i]) - - of '\0': - result = makeRange(version, op) - break - - of ' ': - nil # Ignore whitespace - - else: - raise newException(EParseVersion, "Unexpected char in version range: " & s[i]) - inc(i) - -proc `$`*(verRange: PVersionRange): String = - case verRange.kind - of verLater: - result = "> " - of verEarlier: - result = "< " - of verEqLater: - result = ">= " - of verEqEarlier: - result = "<= " - of verEq: - result = "" - of verIntersect: - result = $verRange.verILeft & " & " & $verRange.verIRight - of verAny: - return "Any" - - result.add(string(verRange.ver)) - -proc newVRAny*(): PVersionRange = - new(result) - result.kind = verAny - -proc newVREarlier*(ver: String): PVersionRange = - new(result) - result.kind = verEarlier - result.ver = newVersion(ver) - -proc newVREq*(ver: string): PVersionRange = - new(result) - result.kind = verEq - result.ver = newVersion(ver) - -when isMainModule: - assert(newVersion("1.0") < newVersion("1.4")) - assert(newVersion("1.0.1") > newVersion("1.0")) - assert(newVersion("1.0.6") <= newVersion("1.0.6")) - - var inter1 = parseVersionRange(">= 1.0 & <= 1.5 & > 1.1 ") - - assert(not withinRange(newVersion("1.5.1"), inter1)) - assert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1)) - - assert(newVersion("1") == newVersion("1")) - - echo("Everything works! Assuming that you didn't compile without assertions...")