Changed directory structure.
This commit is contained in:
parent
2129b2c98d
commit
4cd00aee62
8 changed files with 1 additions and 0 deletions
510
src/babel.nim
Normal file
510
src/babel.nim
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils,
|
||||
strtabs, json
|
||||
|
||||
import packageinfo, version, common, tools, download, algorithm
|
||||
|
||||
type
|
||||
TOptions = object
|
||||
forcePrompts: TForcePrompt
|
||||
queryVersions: bool
|
||||
action: TAction
|
||||
|
||||
TActionType = enum
|
||||
ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList,
|
||||
ActionBuild, ActionPath
|
||||
|
||||
TAction = object
|
||||
case typ: TActionType
|
||||
of ActionNil, ActionList, ActionBuild: nil
|
||||
of ActionUpdate:
|
||||
optionalURL: string # Overrides default package list.
|
||||
of ActionInstall, ActionPath:
|
||||
optionalName: seq[string] # \
|
||||
# When this is @[], installs package from current dir.
|
||||
of ActionSearch:
|
||||
search: seq[string] # Search string.
|
||||
|
||||
TForcePrompt = enum
|
||||
DontForcePrompt, ForcePromptYes, ForcePromptNo
|
||||
|
||||
const
|
||||
help = """
|
||||
Usage: babel COMMAND [opts]
|
||||
|
||||
Commands:
|
||||
install [pkgname, ...] Installs a list of packages.
|
||||
build Builds a package.
|
||||
update [url] Updates package list. A package list URL can
|
||||
be optionally specified.
|
||||
search [--ver] pkg/tag Searches for a specified package. Search is
|
||||
performed by tag and by name.
|
||||
list [--ver] Lists all packages.
|
||||
path pkgname ... Shows absolute path to the installed packages
|
||||
specified.
|
||||
|
||||
Options:
|
||||
-h, --help Print this help message.
|
||||
-v, --version Print version information.
|
||||
-y, --accept Accept all interactive prompts.
|
||||
-n, --reject Reject all interactive prompts.
|
||||
--ver Query remote server for package version
|
||||
information when searching or listing packages
|
||||
"""
|
||||
babelVersion = "0.1.0"
|
||||
defaultPackageURL = "https://github.com/nimrod-code/packages/raw/master/packages.json"
|
||||
|
||||
proc writeHelp() =
|
||||
echo(help)
|
||||
quit(QuitSuccess)
|
||||
|
||||
proc writeVersion() =
|
||||
echo("babel v$# compiled at $# $#" % [babelVersion, compileDate, compileTime])
|
||||
quit(QuitSuccess)
|
||||
|
||||
proc parseCmdLine(): TOptions =
|
||||
result.action.typ = ActionNil
|
||||
for kind, key, val in getOpt():
|
||||
case kind
|
||||
of cmdArgument:
|
||||
if result.action.typ == ActionNil:
|
||||
case key
|
||||
of "install":
|
||||
result.action.typ = ActionInstall
|
||||
result.action.optionalName = @[]
|
||||
of "build":
|
||||
result.action.typ = ActionBuild
|
||||
of "update":
|
||||
result.action.typ = ActionUpdate
|
||||
result.action.optionalURL = ""
|
||||
of "search":
|
||||
result.action.typ = ActionSearch
|
||||
result.action.search = @[]
|
||||
of "list":
|
||||
result.action.typ = ActionList
|
||||
of "path":
|
||||
result.action.typ = ActionPath
|
||||
result.action.optionalName = @[]
|
||||
else: writeHelp()
|
||||
else:
|
||||
case result.action.typ
|
||||
of ActionNil:
|
||||
assert false
|
||||
of ActionInstall, ActionPath:
|
||||
result.action.optionalName.add(key)
|
||||
of ActionUpdate:
|
||||
result.action.optionalURL = key
|
||||
of ActionSearch:
|
||||
result.action.search.add(key)
|
||||
of ActionList, ActionBuild:
|
||||
writeHelp()
|
||||
of cmdLongOption, cmdShortOption:
|
||||
case key
|
||||
of "help", "h": writeHelp()
|
||||
of "version", "v": writeVersion()
|
||||
of "accept", "y": result.forcePrompts = ForcePromptYes
|
||||
of "reject", "n": result.forcePrompts = ForcePromptNo
|
||||
of "ver": result.queryVersions = true
|
||||
of cmdEnd: assert(false) # cannot happen
|
||||
if result.action.typ == ActionNil:
|
||||
writeHelp()
|
||||
|
||||
proc prompt(options: TOptions, question: string): bool =
|
||||
## Asks an interactive question and returns the result.
|
||||
##
|
||||
## The proc will return immediately without asking the user if the global
|
||||
## forcePrompts has a value different than DontForcePrompt.
|
||||
case options.forcePrompts
|
||||
of ForcePromptYes:
|
||||
echo(question & " -> [forced yes]")
|
||||
return true
|
||||
of ForcePromptNo:
|
||||
echo(question & " -> [forced no]")
|
||||
return false
|
||||
of DontForcePrompt:
|
||||
echo(question & " [y/N]")
|
||||
let yn = stdin.readLine()
|
||||
case yn.normalize
|
||||
of "y", "yes":
|
||||
return true
|
||||
of "n", "no":
|
||||
return false
|
||||
else:
|
||||
return false
|
||||
|
||||
let babelDir = getHomeDir() / ".babel"
|
||||
let pkgsDir = babelDir / "pkgs"
|
||||
let binDir = babelDir / "bin"
|
||||
|
||||
proc update(url: string = defaultPackageURL) =
|
||||
## Downloads the package list from the specified URL.
|
||||
##
|
||||
## If the download is successful, the global didUpdatePackages is set to
|
||||
## true. Otherwise an exception is raised on error.
|
||||
echo("Downloading package list from " & url)
|
||||
downloadFile(url, babelDir / "packages.json")
|
||||
echo("Done.")
|
||||
|
||||
proc checkInstallFile(pkgInfo: TPackageInfo,
|
||||
origDir, file: string): bool =
|
||||
## Checks whether ``file`` should be installed.
|
||||
## ``True`` means file should be skipped.
|
||||
|
||||
for ignoreFile in pkgInfo.skipFiles:
|
||||
if ignoreFile.endswith("babel"):
|
||||
raise newException(EBabel, ignoreFile & " must be installed.")
|
||||
if samePaths(file, origDir / ignoreFile):
|
||||
result = true
|
||||
break
|
||||
|
||||
for ignoreExt in pkgInfo.skipExt:
|
||||
if file.splitFile.ext == ('.' & ignoreExt):
|
||||
result = true
|
||||
break
|
||||
|
||||
if file.splitFile().name[0] == '.': result = true
|
||||
|
||||
proc checkInstallDir(pkgInfo: TPackageInfo,
|
||||
origDir, dir: string): bool =
|
||||
## Determines whether ``dir`` should be installed.
|
||||
## ``True`` means dir should be skipped.
|
||||
for ignoreDir in pkgInfo.skipDirs:
|
||||
if samePaths(dir, origDir / ignoreDir):
|
||||
result = true
|
||||
break
|
||||
|
||||
let thisDir = splitPath(dir).tail
|
||||
assert thisDir != ""
|
||||
if thisDir[0] == '.': result = true
|
||||
if thisDir == "nimcache": result = true
|
||||
|
||||
proc copyWithExt(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
|
||||
for kind, path in walkDir(currentDir):
|
||||
if kind == pcDir:
|
||||
copyWithExt(origDir, path, dest, pkgInfo)
|
||||
else:
|
||||
for iExt in pkgInfo.installExt:
|
||||
if path.splitFile.ext == ('.' & iExt):
|
||||
createDir(changeRoot(origDir, dest, path).splitFile.dir)
|
||||
copyFileD(path, changeRoot(origDir, dest, path))
|
||||
|
||||
proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
|
||||
## Copies all the required files, skips files specified in the .babel file
|
||||
## (TPackageInfo).
|
||||
let whitelistMode =
|
||||
pkgInfo.installDirs.len != 0 or
|
||||
pkgInfo.installFiles.len != 0 or
|
||||
pkgInfo.installExt.len != 0
|
||||
if whitelistMode:
|
||||
for file in pkgInfo.installFiles:
|
||||
createDir(dest / file.splitFile.dir)
|
||||
copyFileD(origDir / file, dest / file)
|
||||
|
||||
for dir in pkgInfo.installDirs:
|
||||
# TODO: Allow skipping files inside dirs?
|
||||
copyDirD(origDir / dir, dest / dir)
|
||||
|
||||
copyWithExt(origDir, currentDir, dest, pkgInfo)
|
||||
else:
|
||||
for kind, file in walkDir(currentDir):
|
||||
if kind == pcDir:
|
||||
let skip = pkgInfo.checkInstallDir(origDir, file)
|
||||
|
||||
if skip: continue
|
||||
# Create the dir.
|
||||
createDir(changeRoot(origDir, dest, file))
|
||||
|
||||
copyFilesRec(origDir, file, dest, pkgInfo)
|
||||
else:
|
||||
let skip = pkgInfo.checkInstallFile(origDir, file)
|
||||
|
||||
if skip: continue
|
||||
|
||||
copyFileD(file, changeRoot(origDir, dest, file))
|
||||
|
||||
copyFileD(pkgInfo.mypath,
|
||||
changeRoot(pkgInfo.mypath.splitFile.dir, dest, pkgInfo.mypath))
|
||||
|
||||
proc install(packages: seq[tuple[name: string, verRange: PVersionRange]],
|
||||
options: TOptions, doPrompt = true): seq[string] {.discardable.}
|
||||
proc processDeps(pkginfo: TPackageInfo, options: TOptions): seq[string] =
|
||||
## Verifies and installs dependencies.
|
||||
##
|
||||
## Returns the list of paths to pass to the compiler during build phase.
|
||||
result = @[]
|
||||
let pkglist = getInstalledPkgs(pkgsDir)
|
||||
for dep in pkginfo.requires:
|
||||
if dep.name == "nimrod":
|
||||
let nimVer = getNimrodVersion()
|
||||
if not withinRange(nimVer, dep.ver):
|
||||
quit("Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")")
|
||||
else:
|
||||
echo("Looking for ", dep.name, " (", $dep.ver, ")...")
|
||||
var pkg: TPackageInfo
|
||||
if not findPkg(pkglist, dep, pkg):
|
||||
echo("None found, installing...")
|
||||
let paths = install(@[(dep.name, dep.ver)], options)
|
||||
result.add(paths)
|
||||
else:
|
||||
echo("Dependency already satisfied.")
|
||||
result.add(pkg.mypath.splitFile.dir)
|
||||
|
||||
# Check if two packages of the same name (but different version) are listed
|
||||
# in the path.
|
||||
var pkgsInPath: PStringTable = newStringTable(modeCaseSensitive)
|
||||
for p in result:
|
||||
let (name, version) = getNameVersion(p)
|
||||
if pkgsInPath.hasKey(name):
|
||||
raise newException(EBabel,
|
||||
"Cannot satisfy the dependency on $1 $2 and $1 $3" %
|
||||
[name, version, pkgsInPath[name]])
|
||||
pkgsInPath[name] = version
|
||||
|
||||
proc buildFromDir(pkgInfo: TPackageInfo, paths: seq[string]) =
|
||||
## Builds a package as specified by ``pkgInfo``.
|
||||
let realDir = pkgInfo.getRealDir()
|
||||
var args = ""
|
||||
for path in paths: args.add("--path:\"" & path & "\" ")
|
||||
for bin in pkgInfo.bin:
|
||||
echo("Building ", pkginfo.name, "/", bin, " using ", pkgInfo.backend,
|
||||
" backend...")
|
||||
doCmd("nimrod $# -d:release $# \"$#\"" %
|
||||
[pkgInfo.backend, args, realDir / bin.changeFileExt("nim")])
|
||||
|
||||
proc installFromDir(dir: string, latest: bool, options: TOptions, url: string): seq[string] =
|
||||
## Returns where package has been installed to.
|
||||
## The return value of this function is used by
|
||||
## ``processDeps`` to gather a list of paths to pass to the nimrod compiler.
|
||||
var pkgInfo = getPkgInfo(dir)
|
||||
let realDir = pkgInfo.getRealDir()
|
||||
|
||||
let versionStr = (if latest: "" else: '-' & pkgInfo.version)
|
||||
let pkgDestDir = pkgsDir / (pkgInfo.name & versionStr)
|
||||
if existsDir(pkgDestDir):
|
||||
if not options.prompt(pkgInfo.name & versionStr & " already exists. Overwrite?"):
|
||||
quit(QuitSuccess)
|
||||
removeDir(pkgDestDir)
|
||||
# Remove any symlinked binaries
|
||||
for bin in pkgInfo.bin:
|
||||
# TODO: Check that this binary belongs to the package being installed.
|
||||
when defined(windows):
|
||||
removeFile(binDir / bin.changeFileExt("bat"))
|
||||
else:
|
||||
removeFile(binDir / bin)
|
||||
|
||||
echo("Installing ", pkginfo.name, "-", pkginfo.version)
|
||||
|
||||
# Dependencies need to be processed before the creation of the pkg dir.
|
||||
let paths = processDeps(pkginfo, options)
|
||||
|
||||
if pkgInfo.bin.len > 0: buildFromDir(pkgInfo, paths)
|
||||
|
||||
createDir(pkgDestDir)
|
||||
if pkgInfo.bin.len > 0:
|
||||
createDir(binDir)
|
||||
# Copy all binaries and files that are not skipped
|
||||
copyFilesRec(realDir, realDir, pkgDestDir, pkgInfo)
|
||||
# Set file permissions to +x for all binaries built,
|
||||
# and symlink them on *nix OS' to $babelDir/bin/
|
||||
for bin in pkgInfo.bin:
|
||||
if not existsFile(pkgDestDir / bin):
|
||||
copyFileD(realDir / bin, pkgDestDir / bin)
|
||||
|
||||
let currentPerms = getFilePermissions(pkgDestDir / bin)
|
||||
setFilePermissions(pkgDestDir / bin, currentPerms + {fpUserExec})
|
||||
let cleanBin = bin.extractFilename
|
||||
when defined(unix):
|
||||
# TODO: Verify that we are removing an old bin of this package, not
|
||||
# some other package's binary!
|
||||
if existsFile(binDir / bin): removeFile(binDir / cleanBin)
|
||||
echo("Creating symlink: ", pkgDestDir / bin, " -> ", binDir / cleanBin)
|
||||
doCmd("ln -s \"" & pkgDestDir / bin & "\" " & binDir / cleanBin)
|
||||
elif defined(windows):
|
||||
let dest = binDir / cleanBin.changeFileExt("bat")
|
||||
echo("Creating stub: ", pkgDestDir / bin, " -> ", dest)
|
||||
writeFile(dest, "\"" & pkgDestDir / bin & "\" %*\n")
|
||||
else:
|
||||
{.error: "Sorry, your platform is not supported.".}
|
||||
else:
|
||||
copyFilesRec(realDir, realDir, pkgDestDir, pkgInfo)
|
||||
|
||||
# Save a babelmeta.json file.
|
||||
var babelmeta = %{"url": %url}
|
||||
writeFile(pkgDestDir / "babelmeta.json", $babelmeta)
|
||||
|
||||
result = paths # Return the paths to the dependencies of this package.
|
||||
result.add pkgDestDir
|
||||
|
||||
echo(pkgInfo.name & " installed successfully.")
|
||||
|
||||
proc downloadPkg(url: string, verRange: PVersionRange,
|
||||
downMethod: TDownloadMethod): string =
|
||||
let downloadDir = (getTempDir() / "babel" / getDownloadDirName(url, verRange))
|
||||
if not existsDir(getTempDir() / "babel"): createDir(getTempDir() / "babel")
|
||||
echo("Downloading ", url, " into ", downloadDir, " using ", downMethod, "...")
|
||||
doDownload(url, downloadDir, verRange, downMethod)
|
||||
result = downloadDir
|
||||
|
||||
proc downloadPkg(pkg: TPackage, verRange: PVersionRange): string =
|
||||
let downloadDir = (getTempDir() / "babel" / getDownloadDirName(pkg, verRange))
|
||||
if not existsDir(getTempDir() / "babel"): createDir(getTempDir() / "babel")
|
||||
let downMethod = pkg.downloadMethod.getDownloadMethod()
|
||||
echo("Downloading ", pkg.name, " into ", downloadDir, " using ", downMethod, "...")
|
||||
doDownload(pkg.url, downloadDir, verRange, downMethod)
|
||||
result = downloadDir
|
||||
|
||||
proc install(packages: seq[tuple[name: string, verRange: PVersionRange]],
|
||||
options: TOptions, doPrompt = true): seq[string] =
|
||||
if packages == @[]:
|
||||
result = installFromDir(getCurrentDir(), false, options, "")
|
||||
else:
|
||||
# If packages.json is not present ask the user if they want to download it.
|
||||
if not existsFile(babelDir / "packages.json"):
|
||||
if doPrompt and
|
||||
options.prompt("Local packages.json not found, download it from internet?"):
|
||||
update()
|
||||
install(packages, options, false)
|
||||
else:
|
||||
quit("Please run babel update.", QuitFailure)
|
||||
|
||||
# Install each package.
|
||||
for pv in packages:
|
||||
if pv.name.startsWith(peg" @'://' "):
|
||||
let meth = checkUrlType(pv.name)
|
||||
let downloadDir = downloadPkg(pv.name, pv.verRange, meth)
|
||||
result = installFromDir(downloadDir, false, options, pv.name)
|
||||
else:
|
||||
var pkg: TPackage
|
||||
if getPackage(pv.name, babelDir / "packages.json", pkg):
|
||||
let downloadDir = downloadPkg(pkg, pv.verRange)
|
||||
result = installFromDir(downloadDir, false, options, pkg.url)
|
||||
else:
|
||||
# If package is not found give the user a chance to update package.json
|
||||
if doPrompt and
|
||||
options.prompt(pv.name & " not found in local packages.json, check internet for updated packages?"):
|
||||
update()
|
||||
install(@[pv], options, false)
|
||||
else:
|
||||
raise newException(EBabel, "Package not found.")
|
||||
|
||||
proc build(options: TOptions) =
|
||||
var pkgInfo = getPkgInfo(getCurrentDir())
|
||||
let paths = processDeps(pkginfo, options)
|
||||
buildFromDir(pkgInfo, paths)
|
||||
|
||||
proc search(options: TOptions) =
|
||||
## Searches for matches in ``options.action.search``.
|
||||
##
|
||||
## Searches are done in a case insensitive way making all strings lower case.
|
||||
assert options.action.typ == ActionSearch
|
||||
if options.action.search == @[]:
|
||||
raise newException(EBabel, "Please specify a search string.")
|
||||
if not existsFile(babelDir / "packages.json"):
|
||||
raise newException(EBabel, "Please run babel update.")
|
||||
let pkgList = getPackageList(babelDir / "packages.json")
|
||||
var found = false
|
||||
template onFound: stmt =
|
||||
echoPackage(pkg)
|
||||
if options.queryVersions:
|
||||
echoPackageVersions(pkg)
|
||||
echo(" ")
|
||||
found = true
|
||||
break
|
||||
|
||||
for pkg in pkgList:
|
||||
for word in options.action.search:
|
||||
# Search by name.
|
||||
if word.toLower() in pkg.name.toLower():
|
||||
onFound()
|
||||
# Search by tag.
|
||||
for tag in pkg.tags:
|
||||
if word.toLower() in tag.toLower():
|
||||
onFound()
|
||||
|
||||
if not found:
|
||||
echo("No package found.")
|
||||
|
||||
proc list(options: TOptions) =
|
||||
if not existsFile(babelDir / "packages.json"):
|
||||
raise newException(EBabel, "Please run babel update.")
|
||||
let pkgList = getPackageList(babelDir / "packages.json")
|
||||
for pkg in pkgList:
|
||||
echoPackage(pkg)
|
||||
if options.queryVersions:
|
||||
echoPackageVersions(pkg)
|
||||
echo(" ")
|
||||
|
||||
type VersionAndPath = tuple[version: TVersion, path: string]
|
||||
|
||||
proc listPaths(packages: seq[String]) =
|
||||
## Loops over installing packages displaying their installed paths.
|
||||
##
|
||||
## If there are several packages installed, only the last one (the version
|
||||
## listed in the packages.json) will be displayed. If any package name is not
|
||||
## found, the proc displays a missing message and continues through the list,
|
||||
## but at the end quits with a non zero exit error.
|
||||
##
|
||||
## On success the proc returns normally.
|
||||
var errors = 0
|
||||
for name in packages:
|
||||
var installed: seq[VersionAndPath] = @[]
|
||||
# There may be several, list all available ones and sort by version.
|
||||
for file in walkFiles(pkgsDir / name & "-*" / name & ".babel"):
|
||||
var pkgInfo = getPkgInfo(splitFile(file).dir)
|
||||
var v: VersionAndPath
|
||||
v.version = newVersion(pkgInfo.version)
|
||||
v.path = pkgsDir / (pkgInfo.name & '-' & pkgInfo.version)
|
||||
installed.add(v)
|
||||
|
||||
if installed.len > 0:
|
||||
sort(installed, system.cmp[VersionAndPath], Descending)
|
||||
echo installed[0].path
|
||||
else:
|
||||
echo "Warning: Package '" & name & "' not installed"
|
||||
errors += 1
|
||||
if errors > 0:
|
||||
raise newException(EBabel, "At least one of the specified packages was not found")
|
||||
|
||||
proc doAction(options: TOptions) =
|
||||
case options.action.typ
|
||||
of ActionUpdate:
|
||||
if options.action.optionalURL != "":
|
||||
update(options.action.optionalURL)
|
||||
else:
|
||||
update()
|
||||
of ActionInstall:
|
||||
var installList: seq[tuple[name: string, verRange: PVersionRange]] = @[]
|
||||
for name in options.action.optionalName:
|
||||
if '#' in name:
|
||||
let i = find(name, '#')
|
||||
installList.add((name[0 .. i-1], name[i .. -1].parseVersionRange()))
|
||||
else:
|
||||
installList.add((name, PVersionRange(kind: verAny)))
|
||||
|
||||
install(installList, options)
|
||||
of ActionSearch:
|
||||
search(options)
|
||||
of ActionList:
|
||||
list(options)
|
||||
of ActionPath:
|
||||
listPaths(options.action.optionalName)
|
||||
of ActionBuild:
|
||||
build(options)
|
||||
of ActionNil:
|
||||
assert false
|
||||
|
||||
when isMainModule:
|
||||
if not existsDir(babelDir):
|
||||
createDir(babelDir)
|
||||
if not existsDir(pkgsDir):
|
||||
createDir(pkgsDir)
|
||||
|
||||
when defined(release):
|
||||
try:
|
||||
parseCmdLine().doAction()
|
||||
except EBabel:
|
||||
quit("FAILURE: " & getCurrentExceptionMsg())
|
||||
else:
|
||||
parseCmdLine().doAction()
|
||||
1
src/babel.nimrod.cfg
Normal file
1
src/babel.nimrod.cfg
Normal file
|
|
@ -0,0 +1 @@
|
|||
-d:ssl
|
||||
15
src/common.nim
Normal file
15
src/common.nim
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
import os, osproc
|
||||
|
||||
type
|
||||
EBabel* = object of EBase
|
||||
|
||||
proc copyFileD*(fro, to: string) =
|
||||
echo(fro, " -> ", to)
|
||||
copyFile(fro, to)
|
||||
|
||||
proc copyDirD*(fro, to: string) =
|
||||
echo(fro, " -> ", to)
|
||||
copyDir(fro, to)
|
||||
210
src/download.nim
Normal file
210
src/download.nim
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
import parseutils, os, osproc, strutils, tables
|
||||
|
||||
import packageinfo, common, 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")
|
||||
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 " & 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) = execCmdEx("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(EBabel, "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 execCmdEx("git ls-remote " & url).exitCode == QuitSuccess:
|
||||
return TDownloadMethod.Git
|
||||
elif execCmdEx("hg identify " & url).exitCode == QuitSuccess:
|
||||
return TDownloadMethod.Hg
|
||||
else:
|
||||
raise newException(EBabel, "Unable to identify url.")
|
||||
|
||||
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/babel/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(EBabel,
|
||||
"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 & ")")
|
||||
328
src/packageinfo.nim
Normal file
328
src/packageinfo.nim
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
import parsecfg, json, streams, strutils, parseutils, os
|
||||
import version, common
|
||||
type
|
||||
TPackageInfo* = object
|
||||
mypath*: string ## The path of this .babel file
|
||||
name*: string
|
||||
version*: string
|
||||
author*: string
|
||||
description*: string
|
||||
license*: string
|
||||
skipDirs*: seq[string]
|
||||
skipFiles*: seq[string]
|
||||
skipExt*: seq[string]
|
||||
installDirs*: seq[string]
|
||||
installFiles*: seq[string]
|
||||
installExt*: seq[string]
|
||||
requires*: seq[tuple[name: string, ver: PVersionRange]]
|
||||
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(EBabel, "Incorrect .babel file: " & path &
|
||||
" does not contain a name field.")
|
||||
if pkgInfo.version == "":
|
||||
raise newException(EBabel, "Incorrect .babel file: " & path &
|
||||
" does not contain a version field.")
|
||||
if pkgInfo.author == "":
|
||||
raise newException(EBabel, "Incorrect .babel file: " & path &
|
||||
" does not contain an author field.")
|
||||
if pkgInfo.description == "":
|
||||
raise newException(EBabel, "Incorrect .babel file: " & path &
|
||||
" does not contain a description field.")
|
||||
if pkgInfo.license == "":
|
||||
raise newException(EBabel, "Incorrect .babel file: " & path &
|
||||
" does not contain a license field.")
|
||||
if pkgInfo.backend notin ["c", "cc", "objc", "cpp", "js"]:
|
||||
raise newException(EBabel, "'" & pkgInfo.backend & "' is an invalid backend.")
|
||||
for c in pkgInfo.version:
|
||||
if c notin ({'.'} + digits):
|
||||
raise newException(EBabel,
|
||||
"Version may only consist of numbers and the '.' character " &
|
||||
"but found '" & c & "'.")
|
||||
|
||||
proc parseRequires(req: string): tuple[name: string, ver: PVersionRange] =
|
||||
try:
|
||||
if ' ' in req:
|
||||
var i = skipUntil(req, whitespace)
|
||||
result.name = req[0 .. i].strip
|
||||
result.ver = parseVersionRange(req[i .. -1])
|
||||
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(EBabel, "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(EBabel, "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(EBabel, "Invalid field: " & ev.key)
|
||||
else: raise newException(EBabel, "Invalid section: " & currentSection)
|
||||
of cfgOption: raise newException(EBabel, "Invalid package info, should not contain --" & ev.value)
|
||||
of cfgError:
|
||||
raise newException(EBabel, "Error parsing .babel 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(EBabel, "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(EBabel,
|
||||
"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 ``~/.babel/pkgs/pkg-0.1/babelmeta.json``
|
||||
let bmeta = path / "babelmeta.json"
|
||||
if not existsFile(bmeta):
|
||||
result.url = ""
|
||||
echo("WARNING: No babelmeta.json file found in " & path)
|
||||
return
|
||||
# TODO: Make this an error.
|
||||
let cont = readFile(path / "babelmeta.json")
|
||||
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 findBabelFile*(dir: string): string =
|
||||
result = ""
|
||||
for kind, path in walkDir(dir):
|
||||
if kind == pcFile and path.splitFile.ext == ".babel":
|
||||
if result != "":
|
||||
raise newException(EBabel, "Only one .babel file should be present in " & dir)
|
||||
result = path
|
||||
|
||||
proc getPkgInfo*(dir: string): TPackageInfo =
|
||||
## Find the .babel file in ``dir`` and parses it, returning a TPackageInfo.
|
||||
let babelFile = findBabelFile(dir)
|
||||
if babelFile == "":
|
||||
raise newException(EBabel, "Specified directory does not contain a .babel file.")
|
||||
result = readPackageInfo(babelFile)
|
||||
|
||||
proc getInstalledPkgs*(libsDir: string): seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]] =
|
||||
## Gets a list of installed packages.
|
||||
##
|
||||
## ``libsDir`` is in most cases: ~/.babel/pkgs/
|
||||
result = @[]
|
||||
for kind, path in walkDir(libsDir):
|
||||
if kind == pcDir:
|
||||
let babelFile = findBabelFile(path)
|
||||
if babelFile != "":
|
||||
let meta = readMetadata(path)
|
||||
result.add((readPackageInfo(babelFile), meta))
|
||||
else:
|
||||
# TODO: Abstract logging.
|
||||
echo("WARNING: No .babel file found for ", path)
|
||||
|
||||
proc findPkg*(pkglist: seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]],
|
||||
dep: tuple[name: string, ver: PVersionRange],
|
||||
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 != dep.name and pkg.meta.url != dep.name: 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 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/.babel/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)
|
||||
|
||||
when isMainModule:
|
||||
doAssert getNameVersion("/home/user/.babel/libs/packagea-0.1") == ("packagea", "0.1")
|
||||
doAssert getNameVersion("/home/user/.babel/libs/package-a-0.1") == ("package-a", "0.1")
|
||||
76
src/tools.nim
Normal file
76
src/tools.nim
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# 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
|
||||
import version, common, packageinfo
|
||||
|
||||
# TODO: Merge with common.nim?
|
||||
|
||||
proc doCmd*(cmd: string) =
|
||||
let exitCode = execCmd(cmd)
|
||||
if exitCode != QuitSuccess:
|
||||
raise newException(EBabel, "Execution failed with exit code " & $exitCode)
|
||||
|
||||
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 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 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 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 getDownloadDirName*(pkg: TPackage, verRange: PVersionRange): string =
|
||||
result = pkg.name
|
||||
let verSimple = getSimpleString(verRange)
|
||||
if verSimple != "":
|
||||
result.add "_"
|
||||
result.add verSimple
|
||||
284
src/version.nim
Normal file
284
src/version.nim
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
# 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!")
|
||||
Loading…
Add table
Add a link
Reference in a new issue