Renamed babel to nimble.

This commit is contained in:
Dominik Picheta 2014-10-11 22:38:44 +01:00
commit adbc30c22e
9 changed files with 216 additions and 190 deletions

29
src/nimblepkg/compat.nim Normal file
View file

@ -0,0 +1,29 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
## This module contains additional code from the development version of
## Nimrod's standard library. These procs are required to be able to compile
## against the last stable release 0.9.4. Once 0.9.6 is release these procs
## will disappear.
import json
when false:
when not (NimrodPatch >= 5):
when not defined(`{}`):
proc `{}`*(node: PJsonNode, key: string): PJsonNode =
## Transverses the node and gets the given value. If any of the
## names does not exist, returns nil
result = node
if isNil(node): return nil
result = result[key]
when not defined(`{}=`):
proc `{}=`*(node: PJsonNode, names: varargs[string], value: PJsonNode) =
## Transverses the node and tries to set the value at the given location
## to `value` If any of the names are missing, they are added
var node = node
for i in 0..(names.len-2):
if isNil(node[names[i]]):
node[names[i]] = newJObject()
node = node[names[i]]
node[names[names.len-1]] = value

45
src/nimblepkg/config.nim Normal file
View file

@ -0,0 +1,45 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
import parsecfg, streams, strutils, os
import tools
type
TConfig* = object
nimbleDir*: string
proc initConfig(): TConfig =
result.nimbleDir = getHomeDir() / ".nimble"
proc parseConfig*(): TConfig =
result = initConfig()
var confFile = getConfigDir() / "nimble" / "nimble.ini"
var f = newFileStream(confFile, fmRead)
if f == nil:
# Try the old deprecated babel.ini
confFile = getConfigDir() / "babel" / "babel.ini"
f = newFileStream(confFile, fmRead)
if f != nil:
echo("[Warning] Using deprecated config file at ", confFile)
if f != nil:
echo("Reading from config file at ", confFile)
var p: TCfgParser
open(p, f, confFile)
while true:
var e = next(p)
case e.kind
of cfgEof:
break
of cfgSectionStart: discard
of cfgKeyValuePair, cfgOption:
case e.key.normalize
of "nimbledir":
result.nimbleDir = e.value
else:
raise newException(ENimble, "Unable to parse config file:" &
" Unknown key: " & e.key)
of cfgError:
raise newException(ENimble, "Unable to parse config file: " & e.msg)
close(p)

216
src/nimblepkg/download.nim Normal file
View file

@ -0,0 +1,216 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
import parseutils, os, osproc, strutils, tables, pegs
import packageinfo, version, tools
type
TDownloadMethod* {.pure.} = enum
Git = "git", Hg = "hg"
proc getSpecificDir(meth: TDownloadMethod): string =
case meth
of TDownloadMethod.Git:
".git"
of TDownloadMethod.Hg:
".hg"
proc doCheckout(meth: TDownloadMethod, downloadDir, branch: string) =
case meth
of TDownloadMethod.Git:
cd downloadDir:
# Force is used here because local changes may appear straight after a
# clone has happened. Like in the case of git on Windows where it
# messes up the damn line endings.
doCmd("git checkout --force " & branch)
of TDownloadMethod.Hg:
cd downloadDir:
doCmd("hg checkout " & branch)
proc doPull(meth: TDownloadMethod, downloadDir: string) =
case meth
of TDownloadMethod.Git:
doCheckout(meth, downloadDir, "master")
cd downloadDir:
doCmd("git pull")
if existsFile(".gitmodules"):
doCmd("git submodule update")
of TDownloadMethod.Hg:
doCheckout(meth, downloadDir, "default")
cd downloadDir:
doCmd("hg pull")
proc doClone(meth: TDownloadMethod, url, downloadDir: string, branch = "", tip = true) =
let branchArg = if branch == "": "" else: "-b " & branch & " "
case meth
of TDownloadMethod.Git:
let depthArg = if tip: "--depth 1 " else: ""
# TODO: Get rid of the annoying 'detached HEAD' message somehow?
doCmd("git clone --recursive " & depthArg & branchArg & url &
" " & downloadDir)
of TDownloadMethod.Hg:
let tipArg = if tip: "-r tip " else: ""
doCmd("hg clone " & tipArg & branchArg & url & " " & downloadDir)
proc getTagsList(dir: string, meth: TDownloadMethod): seq[string] =
cd dir:
var output = execProcess("git tag")
case meth
of TDownloadMethod.Git:
output = execProcess("git tag")
of TDownloadMethod.Hg:
output = execProcess("hg tags")
if output.len > 0:
case meth
of TDownloadMethod.Git:
result = @[]
for i in output.splitLines():
if i == "": continue
result.add(i)
of TDownloadMethod.Hg:
result = @[]
for i in output.splitLines():
if i == "": continue
var tag = ""
discard parseUntil(i, tag, ' ')
if tag != "tip":
result.add(tag)
else:
result = @[]
proc getTagsListRemote*(url: string, meth: TDownloadMethod): seq[string] =
result = @[]
case meth
of TDownloadMethod.Git:
var (output, exitCode) = doCmdEx("git ls-remote --tags " & url)
if exitCode != QuitSuccess:
raise newException(EOS, "Unable to query remote tags for " & url &
". Git returned: " & output)
for i in output.splitLines():
if i == "": continue
let start = i.find("refs/tags/")+"refs/tags/".len
let tag = i[start .. -1]
if not tag.endswith("^{}"): result.add(tag)
of TDownloadMethod.Hg:
# http://stackoverflow.com/questions/2039150/show-tags-for-remote-hg-repository
raise newException(EInvalidValue, "Hg doesn't support remote tag querying.")
proc getVersionList*(tags: seq[string]): TTable[TVersion, string] =
# Returns: TTable of version -> git tag name
result = initTable[TVersion, string]()
for tag in tags:
if tag != "":
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 getDownloadMethod*(meth: string): TDownloadMethod =
case meth
of "git": return TDownloadMethod.Git
of "hg", "mercurial": return TDownloadMethod.Hg
else:
raise newException(ENimble, "Invalid download method: " & meth)
proc getHeadName*(meth: TDownloadMethod): string =
## Returns the name of the download method specific head. i.e. for git
## it's ``head`` for hg it's ``tip``.
case meth
of TDownloadMethod.Git: "head"
of TDownloadMethod.Hg: "tip"
proc checkUrlType*(url: string): TDownloadMethod =
## Determines the download method based on the URL.
if doCmdEx("git ls-remote " & url).exitCode == QuitSuccess:
return TDownloadMethod.Git
elif doCmdEx("hg identify " & url).exitCode == QuitSuccess:
return TDownloadMethod.Hg
else:
raise newException(ENimble, "Unable to identify url.")
proc isURL*(name: string): bool =
name.startsWith(peg" @'://' ")
proc doDownload*(url: string, downloadDir: string, verRange: PVersionRange,
downMethod: TDownloadMethod) =
template getLatestByTag(meth: stmt): stmt {.dirty, immediate.} =
echo("Found tags...")
# Find latest version that fits our ``verRange``.
var latest = findLatest(verRange, versions)
## Note: HEAD is not used when verRange.kind is verAny. This is
## intended behaviour, the latest tagged version will be used in this case.
# If no tagged versions satisfy our range latest.tag will be "".
# We still clone in that scenario because we want to try HEAD in that case.
# https://github.com/nimrod-code/nimble/issues/22
meth
proc verifyClone() =
## Makes sure that the downloaded package's version satisfies the requested
## version range.
let pkginfo = getPkgInfo(downloadDir)
if pkginfo.version.newVersion notin verRange:
raise newException(ENimble,
"Downloaded package's version does not satisfy requested version " &
"range: wanted $1 got $2." %
[$verRange, $pkginfo.version])
removeDir(downloadDir)
if verRange.kind == verSpecial:
# We want a specific commit/branch/tag here.
if verRange.spe == newSpecial(getHeadName(downMethod)):
doClone(downMethod, url, downloadDir) # Grab HEAD.
else:
# We don't know if we got a commit hash or a branch here, and
# we can't clone a specific commit (with depth 1) according to:
# http://stackoverflow.com/a/7198956/492186
doClone(downMethod, url, downloadDir, tip = false)
doCheckout(downMethod, downloadDir, $verRange.spe)
else:
case downMethod
of TDownloadMethod.Git:
# For Git we have to query the repo remotely for its tags. This is
# necessary as cloning with a --depth of 1 removes all tag info.
let versions = getTagsListRemote(url, downMethod).getVersionList()
if versions.len > 0:
getLatestByTag:
echo("Cloning latest tagged version: ", latest.tag)
doClone(downMethod, url, downloadDir, latest.tag)
else:
# If no commits have been tagged on the repo we just clone HEAD.
doClone(downMethod, url, downloadDir) # Grab HEAD.
verifyClone()
of TDownloadMethod.Hg:
doClone(downMethod, url, downloadDir)
let versions = getTagsList(downloadDir, downMethod).getVersionList()
if versions.len > 0:
getLatestByTag:
echo("Switching to latest tagged version: ", latest.tag)
doCheckout(downMethod, downloadDir, latest.tag)
verifyClone()
proc echoPackageVersions*(pkg: TPackage) =
let downMethod = pkg.downloadMethod.getDownloadMethod()
case downMethod
of TDownloadMethod.Git:
try:
let versions = getTagsListRemote(pkg.url, downMethod).getVersionList()
if versions.len > 0:
var vstr = ""
var i = 0
for v in values(versions):
if i != 0:
vstr.add(", ")
vstr.add(v)
i.inc
echo(" versions: " & vstr)
else:
echo(" versions: (No versions tagged in the remote repository)")
except EOS:
echo(getCurrentExceptionMsg())
of TDownloadMethod.Hg:
echo(" versions: (Remote tag retrieval not supported by " & pkg.downloadMethod & ")")

View file

@ -0,0 +1,355 @@
# 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, tools
type
## Tuple containing package name and version range.
TPkgTuple* = tuple[name: string, ver: PVersionRange]
TPackageInfo* = object
mypath*: string ## The path of this .nimble file
name*: string
version*: string
author*: string
description*: string
license*: string
skipDirs*: seq[string]
skipFiles*: seq[string]
skipExt*: seq[string]
installDirs*: seq[string]
installFiles*: seq[string]
installExt*: seq[string]
requires*: seq[TPkgTuple]
bin*: seq[string]
srcDir*: string
backend*: string
TPackage* = object
# Required fields in a package.
name*: string
url*: string # Download location.
license*: string
downloadMethod*: string
description*: string
tags*: seq[string] # Even if empty, always a valid non nil seq. \
# From here on, optional fields set to the emtpy string if not available.
version*: string
dvcsTag*: string
web*: string # Info url for humans.
TMetadata* = object
url*: string
proc initPackageInfo(): TPackageInfo =
result.mypath = ""
result.name = ""
result.version = ""
result.author = ""
result.description = ""
result.license = ""
result.skipDirs = @[]
result.skipFiles = @[]
result.skipExt = @[]
result.installDirs = @[]
result.installFiles = @[]
result.installExt = @[]
result.requires = @[]
result.bin = @[]
result.srcDir = ""
result.backend = "c"
proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) =
if pkgInfo.name == "":
raise newException(ENimble, "Incorrect .nimble file: " & path &
" does not contain a name field.")
if pkgInfo.version == "":
raise newException(ENimble, "Incorrect .nimble file: " & path &
" does not contain a version field.")
if pkgInfo.author == "":
raise newException(ENimble, "Incorrect .nimble file: " & path &
" does not contain an author field.")
if pkgInfo.description == "":
raise newException(ENimble, "Incorrect .nimble file: " & path &
" does not contain a description field.")
if pkgInfo.license == "":
raise newException(ENimble, "Incorrect .nimble file: " & path &
" does not contain a license field.")
if pkgInfo.backend notin ["c", "cc", "objc", "cpp", "js"]:
raise newException(ENimble, "'" & pkgInfo.backend & "' is an invalid backend.")
for c in pkgInfo.version:
if c notin ({'.'} + Digits):
raise newException(ENimble,
"Version may only consist of numbers and the '.' character " &
"but found '" & c & "'.")
proc parseRequires(req: string): TPkgTuple =
try:
if ' ' in req:
var i = skipUntil(req, Whitespace)
result.name = req[0 .. i].strip
result.ver = parseVersionRange(req[i .. -1])
elif '#' in req:
var i = skipUntil(req, {'#'})
result.name = req[0 .. i-1]
result.ver = parseVersionRange(req[i .. -1])
else:
result.name = req.strip
result.ver = PVersionRange(kind: verAny)
except EParseVersion:
raise newException(ENimble, "Unable to parse dependency version range: " &
getCurrentExceptionMsg())
proc multiSplit(s: string): seq[string] =
## Returns ``s`` split by newline and comma characters.
##
## Before returning, all individual entries are stripped of whitespace and
## also empty entries are purged from the list. If after all the cleanups are
## done no entries are found in the list, the proc returns a sequence with
## the original string as the only entry.
result = split(s, {char(0x0A), char(0x0D), ','})
map(result, proc(x: var string) = x = x.strip())
for i in countdown(result.len()-1, 0):
if len(result[i]) < 1:
result.del(i)
# Huh, nothing to return? Return given input.
if len(result) < 1:
return @[s]
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 "srcdir": result.srcDir = ev.value
of "skipdirs":
result.skipDirs.add(ev.value.multiSplit)
of "skipfiles":
result.skipFiles.add(ev.value.multiSplit)
of "skipext":
result.skipExt.add(ev.value.multiSplit)
of "installdirs":
result.installDirs.add(ev.value.multiSplit)
of "installfiles":
result.installFiles.add(ev.value.multiSplit)
of "installext":
result.installExt.add(ev.value.multiSplit)
of "bin":
for i in ev.value.multiSplit:
result.bin.add(i.addFileExt(ExeExt))
of "backend":
result.backend = ev.value.toLower()
case result.backend.normalize
of "javascript": result.backend = "js"
else:
raise newException(ENimble, "Invalid field: " & ev.key)
of "deps", "dependencies":
case ev.key.normalize
of "requires":
for v in ev.value.multiSplit:
result.requires.add(parseRequires(v.strip))
else:
raise newException(ENimble, "Invalid field: " & ev.key)
else: raise newException(ENimble, "Invalid section: " & currentSection)
of cfgOption: raise newException(ENimble, "Invalid package info, should not contain --" & ev.value)
of cfgError:
raise newException(ENimble, "Error parsing .nimble file: " & ev.msg)
close(p)
else:
raise newException(EInvalidValue, "Cannot open package info: " & path)
validatePackageInfo(result, path)
proc optionalField(obj: PJsonNode, name: string, default = ""): string =
## Queries ``obj`` for the optional ``name`` string.
##
## Returns the value of ``name`` if it is a valid string, or aborts execution
## if the field exists but is not of string type. If ``name`` is not present,
## returns ``default``.
if existsKey(obj, name):
if obj[name].kind == JString:
return obj[name].str
else:
raise newException(ENimble, "Corrupted packages.json file. " & name & " field is of unexpected type.")
else: return default
proc requiredField(obj: PJsonNode, name: string): string =
## Queries ``obj`` for the required ``name`` string.
##
## Aborts execution if the field does not exist or is of invalid json type.
result = optionalField(obj, name, nil)
if result == nil:
raise newException(ENimble,
"Package in packages.json file does not contain a " & name & " field.")
proc fromJson(obj: PJSonNode): TPackage =
## Constructs a TPackage object from a JSON node.
##
## Aborts execution if the JSON node doesn't contain the required fields.
result.name = obj.requiredField("name")
result.version = obj.optionalField("version")
result.url = obj.requiredField("url")
result.downloadMethod = obj.requiredField("method")
result.dvcsTag = obj.optionalField("dvcs-tag")
result.license = obj.requiredField("license")
result.tags = @[]
for t in obj["tags"]:
result.tags.add(t.str)
result.description = obj.requiredField("description")
result.web = obj.optionalField("web")
proc readMetadata*(path: string): TMetadata =
## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json``
var bmeta = path / "nimblemeta.json"
if not existsFile(bmeta):
bmeta = path / "babelmeta.json"
if existsFile(bmeta):
echo("WARNING: using deprecated babelmeta.json file in " & path)
if not existsFile(bmeta):
result.url = ""
echo("WARNING: No nimblemeta.json file found in " & path)
return
# TODO: Make this an error.
let cont = readFile(bmeta)
let jsonmeta = parseJson(cont)
result.url = jsonmeta["url"].str
proc getPackage*(pkg: string, packagesPath: string, resPkg: var TPackage): bool =
## Searches ``packagesPath`` file saving into ``resPkg`` the found package.
##
## Pass in ``pkg`` the name of the package you are searching for. As
## convenience the proc returns a boolean specifying if the ``resPkg`` was
## successfully filled with good data.
let packages = parseFile(packagesPath)
for p in packages:
if p["name"].str == pkg:
resPkg = p.fromJson()
return true
proc getPackageList*(packagesPath: string): seq[TPackage] =
## Returns the list of packages found at the specified path.
result = @[]
let packages = parseFile(packagesPath)
for p in packages:
let pkg: TPackage = p.fromJson()
result.add(pkg)
proc findNimbleFile*(dir: string): string =
result = ""
for kind, path in walkDir(dir):
if kind == pcFile and path.splitFile.ext in [".babel", ".nimble"]:
if result != "":
raise newException(ENimble, "Only one .nimble file should be present in " & dir)
result = path
proc getPkgInfo*(dir: string): TPackageInfo =
## Find the .nimble file in ``dir`` and parses it, returning a TPackageInfo.
let nimbleFile = findNimbleFile(dir)
if nimbleFile == "":
raise newException(ENimble, "Specified directory does not contain a .nimble file.")
result = readPackageInfo(nimbleFile)
proc getInstalledPkgs*(libsDir: string): seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]] =
## Gets a list of installed packages.
##
## ``libsDir`` is in most cases: ~/.nimble/pkgs/
result = @[]
for kind, path in walkDir(libsDir):
if kind == pcDir:
let nimbleFile = findNimbleFile(path)
if nimbleFile != "":
let meta = readMetadata(path)
result.add((readPackageInfo(nimbleFile), meta))
else:
# TODO: Abstract logging.
echo("WARNING: No .nimble file found for ", path)
proc findPkg*(pkglist: seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]],
dep: TPkgTuple,
r: var TPackageInfo): bool =
## Searches ``pkglist`` for a package of which version is within 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)
##
## **Note**: dep.name here could be a URL, hence the need for pkglist.meta.
for pkg in pkglist:
if pkg.pkginfo.name.normalize != dep.name.normalize and
pkg.meta.url.normalize != dep.name.normalize: continue
if withinRange(newVersion(pkg.pkginfo.version), dep.ver):
if not result or newVersion(r.version) < newVersion(pkg.pkginfo.version):
r = pkg.pkginfo
result = true
proc findAllPkgs*(pkglist: seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]],
dep: TPkgTuple): seq[TPackageInfo] =
## Searches ``pkglist`` for packages of which version is within the range
## of ``dep.ver``. This is similar to ``findPkg`` but returns multiple
## packages if multiple are found.
result = @[]
for pkg in pkglist:
if pkg.pkginfo.name.normalize != dep.name.normalize and
pkg.meta.url.normalize != dep.name.normalize: continue
if withinRange(newVersion(pkg.pkginfo.version), dep.ver):
result.add pkg.pkginfo
proc getRealDir*(pkgInfo: TPackageInfo): string =
## Returns the ``pkgInfo.srcDir`` or the .mypath directory if package does
## not specify the src dir.
if pkgInfo.srcDir != "":
result = pkgInfo.mypath.splitFile.dir / pkgInfo.srcDir
else:
result = pkgInfo.mypath.splitFile.dir
proc getNameVersion*(pkgpath: string): tuple[name, version: string] =
## Splits ``pkgpath`` in the format ``/home/user/.nimble/pkgs/package-0.1``
## into ``(packagea, 0.1)``
result.name = ""
result.version = ""
let tail = pkgpath.splitPath.tail
if '-' notin tail:
result.name = tail
return
for i in countdown(tail.len-1, 0):
if tail[i] == '-':
result.name = tail[0 .. i-1]
result.version = tail[i+1 .. -1]
break
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)
if pkg.web.len > 0:
echo(" website: " & pkg.web)
proc getDownloadDirName*(pkg: TPackage, verRange: PVersionRange): string =
result = pkg.name
let verSimple = getSimpleString(verRange)
if verSimple != "":
result.add "_"
result.add verSimple
when isMainModule:
doAssert getNameVersion("/home/user/.nimble/libs/packagea-0.1") == ("packagea", "0.1")
doAssert getNameVersion("/home/user/.nimble/libs/package-a-0.1") == ("package-a", "0.1")

114
src/nimblepkg/tools.nim Normal file
View file

@ -0,0 +1,114 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
#
# Various miscellaneous utility functions reside here.
import osproc, pegs, strutils, os, parseurl, sets, json
import version, packageinfo
type
ENimble* = object of EBase
proc doCmd*(cmd: string) =
let bin = cmd.split(' ')[0]
if findExe(bin) == "":
raise newException(ENimble, "'" & bin & "' not in PATH.")
let exitCode = execCmd(cmd)
if exitCode != QuitSuccess:
raise newException(ENimble, "Execution failed with exit code " & $exitCode)
proc doCmdEx*(cmd: string): tuple[output: TaintedString, exitCode: int] =
let bin = cmd.split(' ')[0]
if findExe(bin) == "":
raise newException(ENimble, "'" & bin & "' not in PATH.")
return execCmdEx(cmd)
template cd*(dir: string, body: stmt) =
## Sets the current dir to ``dir``, executes ``body`` and restores the
## previous working dir.
let lastDir = getCurrentDir()
setCurrentDir(dir)
body
setCurrentDir(lastDir)
proc getNimBin*: string =
result = "nim"
if findExe("nim") != "": result = "nim"
elif findExe("nimrod") != "": result = "nimrod"
proc getNimrodVersion*: TVersion =
let nimBin = getNimBin()
let vOutput = doCmdEx(nimBin & " -v").output
var matches: array[0..MaxSubpatterns, string]
if vOutput.find(peg"'Version'\s{(\d+\.)+\d}", matches) == -1:
quit("Couldn't find Nim version.", QuitFailure)
newVersion(matches[0])
proc samePaths*(p1, p2: string): bool =
## Normalizes path (by adding a trailing slash) and compares.
var cp1 = if not p1.endsWith("/"): p1 & "/" else: p1
var cp2 = if not p2.endsWith("/"): p2 & "/" else: p2
cp1 = cp1.replace('/', DirSep).replace('\\', DirSep)
cp2 = cp2.replace('/', DirSep).replace('\\', DirSep)
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 copyFileD*(fro, to: string): string =
## Returns the destination (``to``).
echo(fro, " -> ", to)
copyFile(fro, to)
result = to
proc copyDirD*(fro, to: string): seq[string] =
## Returns the filenames of the files in the directory that were copied.
result = @[]
echo("Copying directory: ", fro, " -> ", to)
for path in walkDirRec(fro):
createDir(changeRoot(fro, to, path.splitFile.dir))
result.add copyFileD(path, changeRoot(fro, to, path))
proc getDownloadDirName*(url: string, verRange: PVersionRange): string =
## Creates a directory name based on the specified ``url``
result = ""
let purl = parseUrl(url)
for i in purl.hostname:
case i
of strutils.Letters, strutils.Digits:
result.add i
else: nil
result.add "_"
for i in purl.path:
case i
of strutils.Letters, strutils.Digits:
result.add i
else: nil
let verSimple = getSimpleString(verRange)
if verSimple != "":
result.add "_"
result.add verSimple
proc incl*(s: var TSet[string], v: seq[string] | TSet[string]) =
for i in v:
s.incl i
proc contains*(j: PJsonNode, elem: PJsonNode): bool =
for i in j:
if i == elem:
return true
proc contains*(j: PJsonNode, elem: tuple[key: string, val: PJsonNode]): bool =
for key, val in pairs(j):
if key == elem.key and val == elem.val:
return true

BIN
src/nimblepkg/version Executable file

Binary file not shown.

283
src/nimblepkg/version.nim Normal file
View file

@ -0,0 +1,283 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5``
import strutils, tables, hashes, parseutils
type
TVersion* = distinct string
TSpecial* = distinct string
TVersionRangeEnum* = enum
verLater, # > V
verEarlier, # < V
verEqLater, # >= V -- Equal or later
verEqEarlier, # <= V -- Equal or earlier
verIntersect, # > V & < V
verEq, # V
verAny, # *
verSpecial # #head
PVersionRange* = ref TVersionRange
TVersionRange* = object
case kind*: TVersionRangeEnum
of verLater, verEarlier, verEqLater, verEqEarlier, verEq:
ver*: TVersion
of verSpecial:
spe*: TSpecial
of verIntersect:
verILeft, verIRight: PVersionRange
of verAny:
nil
EParseVersion* = object of EInvalidValue
proc newVersion*(ver: string): TVersion = return TVersion(ver)
proc newSpecial*(spe: string): TSpecial = return TSpecial(spe)
proc `$`*(ver: TVersion): string {.borrow.}
proc hash*(ver: TVersion): THash {.borrow.}
proc `$`*(ver: TSpecial): string {.borrow.}
proc hash*(ver: TSpecial): THash {.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:
return true
elif sVerI == sVerI2:
nil
else:
return false
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 `==`*(spe: TSpecial, spe2: TSpecial): bool =
return ($spe).toLower() == ($spe2).toLower()
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 verSpecial:
return false
of verIntersect:
return withinRange(ver, ran.verILeft) and withinRange(ver, ran.verIRight)
of verAny:
return true
proc withinRange*(spe: TSpecial, ran: PVersionRange): bool =
case ran.kind
of verLater, verEarlier, verEqLater, verEqEarlier, verEq, verIntersect:
return false
of verSpecial:
return spe == ran.spe
of verAny:
return true
proc contains*(ran: PVersionRange, ver: TVersion): bool =
return withinRange(ver, ran)
proc contains*(ran: PVersionRange, spe: TSpecial): bool =
return withinRange(spe, ran)
proc makeRange*(version: string, op: string): PVersionRange =
new(result)
if version == "":
raise newException(EParseVersion, "A version needs to accompany the operator.")
case op
of ">":
result.kind = verLater
of "<":
result.kind = verEarlier
of ">=":
result.kind = verEqLater
of "<=":
result.kind = verEqEarlier
of "":
result.kind = verEq
else:
raise newException(EParseVersion, "Invalid operator: " & op)
result.ver = TVersion(version)
proc parseVersionRange*(s: string): PVersionRange =
# >= 1.5 & <= 1.8
new(result)
if s[0] == '#':
result.kind = verSpecial
result.spe = s[1 .. -1].TSpecial
return
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(substr(s, i + 1))
# Disallow more than one verIntersect. It's pointless and could lead to
# major unpredictable 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 ' ':
# Make sure '0.9 8.03' is not allowed.
if version != "" and i < s.len:
if s[i+1] in {'0'..'9', '.'}:
raise newException(EParseVersion, "Whitespace is not allowed in a version literal.")
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 verSpecial:
return "#" & $verRange.spe
of verIntersect:
return $verRange.verILeft & " & " & $verRange.verIRight
of verAny:
return "any version"
result.add(string(verRange.ver))
proc getSimpleString*(verRange: PVersionRange): string =
## Gets a string with no special symbols and spaces. Used for dir name creation
## in tools.nim
case verRange.kind
of verSpecial:
result = $verRange.spe
of verLater, verEarlier, verEqLater, verEqEarlier, verEq:
result = $verRange.ver
of verIntersect:
result = getSimpleString(verRange.verILeft) & "_" & getSimpleString(verRange.verIRight)
of verAny:
result = ""
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)
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:
doAssert(newVersion("1.0") < newVersion("1.4"))
doAssert(newVersion("1.0.1") > newVersion("1.0"))
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 inter2 = parseVersionRange("1.0")
doAssert(inter2.kind == verEq)
#echo(parseVersionRange(">= 0.8 0.9"))
doAssert(not withinRange(newVersion("1.5.1"), 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.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(newVersion("1.0.3") != newVersion("1.0.2"))
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")
# TODO: Allow these in later versions?
#doAssert newVersion("0.1-rc1") < newVersion("0.2")
#doAssert newVersion("0.1-rc1") < newVersion("0.1")
# Special tests
doAssert newSpecial("ab26sgdt362") != newSpecial("ab26saggdt362")
doAssert newSpecial("ab26saggdt362") == newSpecial("ab26saggdt362")
doAssert newSpecial("head") == newSpecial("HEAD")
doAssert newSpecial("head") == newSpecial("head")
var sp = parseVersionRange("#ab26sgdt362")
doAssert newSpecial("ab26sgdt362") in sp
doAssert newSpecial("ab26saggdt362") notin sp
echo("Everything works!")