Implements babel uninstall.

This commit is contained in:
Dominik Picheta 2014-06-22 16:24:16 +01:00
commit 05d9956662
3 changed files with 148 additions and 14 deletions

View file

@ -4,6 +4,8 @@
import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils, import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils,
strtabs, json, algorithm, sets strtabs, json, algorithm, sets
from sequtils import toSeq
import babelpkg/packageinfo, babelpkg/version, babelpkg/tools, babelpkg/download, import babelpkg/packageinfo, babelpkg/version, babelpkg/tools, babelpkg/download,
babelpkg/config babelpkg/config
@ -16,19 +18,21 @@ type
queryVersions: bool queryVersions: bool
action: TAction action: TAction
config: TConfig config: TConfig
babelData: PJsonNode ## Babeldata.json
TActionType = enum TActionType = enum
ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList, ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList,
ActionBuild, ActionPath ActionBuild, ActionPath, ActionUninstall
TAction = object TAction = object
case typ: TActionType case typ: TActionType
of ActionNil, ActionList, ActionBuild: nil of ActionNil, ActionList, ActionBuild: nil
of ActionUpdate: of ActionUpdate:
optionalURL: string # Overrides default package list. optionalURL: string # Overrides default package list.
of ActionInstall, ActionPath: of ActionInstall, ActionPath, ActionUninstall:
optionalName: seq[string] # \ optionalName: seq[string] # \
# When this is @[], installs package from current dir. # When this is @[], installs package from current dir.
packages: seq[TPkgTuple] # Optional only for ActionInstall.
of ActionSearch: of ActionSearch:
search: seq[string] # Search string. search: seq[string] # Search string.
@ -41,6 +45,7 @@ Usage: babel COMMAND [opts]
Commands: Commands:
install [pkgname, ...] Installs a list of packages. install [pkgname, ...] Installs a list of packages.
uninstall [pkgname, ...] Uninstalls a list of packages.
build Builds a package. build Builds a package.
update [url] Updates package list. A package list URL can update [url] Updates package list. A package list URL can
be optionally specified. be optionally specified.
@ -85,10 +90,15 @@ proc parseCmdLine(): TOptions =
case kind case kind
of cmdArgument: of cmdArgument:
if result.action.typ == ActionNil: if result.action.typ == ActionNil:
case key
of "install", "path":
case key case key
of "install": of "install":
result.action.typ = ActionInstall result.action.typ = ActionInstall
of "path":
result.action.typ = ActionPath
result.action.optionalName = @[] result.action.optionalName = @[]
result.action.packages = @[]
of "build": of "build":
result.action.typ = ActionBuild result.action.typ = ActionBuild
of "update": of "update":
@ -99,16 +109,25 @@ proc parseCmdLine(): TOptions =
result.action.search = @[] result.action.search = @[]
of "list": of "list":
result.action.typ = ActionList result.action.typ = ActionList
of "path": of "uninstall", "remove", "delete", "del", "rm":
result.action.typ = ActionPath result.action.typ = ActionUninstall
result.action.packages = @[]
result.action.optionalName = @[] result.action.optionalName = @[]
else: writeHelp() else: writeHelp()
else: else:
case result.action.typ case result.action.typ
of ActionNil: of ActionNil:
assert false assert false
of ActionInstall, ActionPath: of ActionInstall, ActionPath, ActionUninstall:
result.action.optionalName.add(key) result.action.optionalName.add(key)
# Parse pkg@verRange
if '@' in key:
let i = find(key, '@')
let pkgTup = (key[0 .. i-1], key[i+1 .. -1].parseVersionRange())
result.action.packages.add(pkgTup)
else:
result.action.packages.add((key, PVersionRange(kind: verAny)))
of ActionUpdate: of ActionUpdate:
result.action.optionalURL = key result.action.optionalURL = key
of ActionSearch: of ActionSearch:
@ -126,6 +145,13 @@ proc parseCmdLine(): TOptions =
if result.action.typ == ActionNil: if result.action.typ == ActionNil:
writeHelp() writeHelp()
# Load babeldata.json
let babeldataFilename = result.getBabelDir() / "babeldata.json"
if fileExists(babeldataFilename):
result.babelData = parseFile(babeldataFilename)
else:
result.babelData = %{"reverseDeps": newJObject()}
proc prompt(options: TOptions, question: string): bool = proc prompt(options: TOptions, question: string): bool =
## Asks an interactive question and returns the result. ## Asks an interactive question and returns the result.
## ##
@ -262,6 +288,36 @@ proc copyFilesRec(origDir, currentDir, dest: string,
result.incl copyFileD(pkgInfo.mypath, result.incl copyFileD(pkgInfo.mypath,
changeRoot(pkgInfo.mypath.splitFile.dir, dest, pkgInfo.mypath)) changeRoot(pkgInfo.mypath.splitFile.dir, dest, pkgInfo.mypath))
proc addRevDep(options: TOptions, dep: tuple[name, version: string],
pkg: TPackageInfo) =
let depNameVer = dep.name & '-' & dep.version
if not options.babelData["reverseDeps"].hasKey(dep.name):
options.babelData["reverseDeps"][dep.name] = newJObject()
if not options.babelData["reverseDeps"][dep.name].hasKey(dep.version):
options.babelData["reverseDeps"][dep.name][dep.version] = newJArray()
let revDep = %{ "name": %pkg.name, "version": %pkg.version}
let thisDep = options.babelData["reverseDeps"][dep.name][dep.version]
if revDep notin thisDep:
thisDep.add revDep
writeFile(options.getBabelDir() / "babeldata.json", pretty(options.babelData))
proc removeRevDep(options: TOptions, pkg: TPackageInfo) =
## Removes ``pkg`` from the reverse dependencies of every package.
for depTup in pkg.requires:
let thisDep = options.babelData["reverseDeps"][depTup.name]
if thisDep.isNil: continue
for ver, val in thisDep:
if ver.newVersion in depTup.ver:
var newVal = newJArray()
for revDep in val:
if not (revDep["name"].str == pkg.name and
revDep["version"].str == pkg.version):
newVal.add revDep
options.babelData["reverseDeps"][depTup.name][ver] = newVal
writeFile(options.getBabelDir() / "babeldata.json", pretty(options.babelData))
proc install(packages: seq[tuple[name: string, verRange: PVersionRange]], proc install(packages: seq[tuple[name: string, verRange: PVersionRange]],
options: TOptions, doPrompt = true): seq[string] {.discardable.} options: TOptions, doPrompt = true): seq[string] {.discardable.}
proc processDeps(pkginfo: TPackageInfo, options: TOptions): seq[string] = proc processDeps(pkginfo: TPackageInfo, options: TOptions): seq[string] =
@ -282,11 +338,15 @@ proc processDeps(pkginfo: TPackageInfo, options: TOptions): seq[string] =
echo("None found, installing...") echo("None found, installing...")
let paths = install(@[(dep.name, dep.ver)], options) let paths = install(@[(dep.name, dep.ver)], options)
result.add(paths) result.add(paths)
# Look up the pkg info again, for addRevDep.
doAssert findPkg(pkgList, dep, pkg)
else: else:
echo("Dependency already satisfied.") echo("Dependency already satisfied.")
result.add(pkg.mypath.splitFile.dir) result.add(pkg.mypath.splitFile.dir)
# Process the dependencies of this dependency. # Process the dependencies of this dependency.
result.add(processDeps(pkg, options)) result.add(processDeps(pkg, options))
addRevDep(options, (pkg.name, pkg.version), pkginfo)
# Check if two packages of the same name (but different version) are listed # Check if two packages of the same name (but different version) are listed
# in the path. # in the path.
@ -326,9 +386,18 @@ proc removePkgDir(dir: string, options: TOptions) =
"Meta data does not contain required info.") "Meta data does not contain required info.")
for file in babelmeta["files"]: for file in babelmeta["files"]:
removeFile(dir / file.str) removeFile(dir / file.str)
removeFile(dir / "babelmeta.json")
# If there are no files left in the directory, remove the directory.
if toSeq(walkDirRec(dir)).len == 0:
removeDir(dir)
else:
echo("WARNING: Cannot completely remove " & dir &
". Files not installed by babel are present.")
except EOS, EJsonParsingError: except EOS, EJsonParsingError:
echo("Error: Unable to read babelmeta.json: ", getCurrentExceptionMsg()) echo("Error: Unable to read babelmeta.json: ", getCurrentExceptionMsg())
if not options.prompt("Would you like to COMPLETELY overwrite ALL files " & if not options.prompt("Would you like to COMPLETELY remove ALL files " &
"in " & dir & "?"): "in " & dir & "?"):
quit(QuitSuccess) quit(QuitSuccess)
removeDir(dir) removeDir(dir)
@ -354,7 +423,7 @@ proc installFromDir(dir: string, latest: bool, options: TOptions,
let versionStr = (if latest: "" else: '-' & pkgInfo.version) let versionStr = (if latest: "" else: '-' & pkgInfo.version)
let pkgDestDir = pkgsDir / (pkgInfo.name & versionStr) let pkgDestDir = pkgsDir / (pkgInfo.name & versionStr)
if existsDir(pkgDestDir): if existsDir(pkgDestDir) and existsFile(pkgDestDir / "babelmeta.json"):
if not options.prompt(pkgInfo.name & versionStr & " already exists. Overwrite?"): if not options.prompt(pkgInfo.name & versionStr & " already exists. Overwrite?"):
quit(QuitSuccess) quit(QuitSuccess)
removePkgDir(pkgDestDir, options) removePkgDir(pkgDestDir, options)
@ -557,6 +626,56 @@ proc listPaths(options: TOptions) =
if errors > 0: if errors > 0:
raise newException(EBabel, "At least one of the specified packages was not found") raise newException(EBabel, "At least one of the specified packages was not found")
proc uninstall(options: TOptions) =
var pkgsToDelete: seq[TPackageInfo] = @[]
# Do some verification.
for pkgTup in options.action.packages:
echo("Looking for ", pkgTup.name, " (", $pkgTup.ver, ")...")
let pkgList = getInstalledPkgs(options.getPkgsDir())
var pkg: TPackageInfo
if not findPkg(pkglist, pkgTup, pkg):
raise newException(EBabel, "Package not found")
echo("Checking reverse dependencies...")
# Check whether any packages depend on the one the user is trying to uninstall
let thisDep = options.babelData["reverseDeps"][pkgTup.name]
if not thisDep.isNil:
for ver, val in thisDep.pairs:
if ver.newVersion in pkgTup.ver and val.len != 0:
assert val.kind == JArray
var reason = ""
if val.len == 1:
reason = val[0]["name"].str & " (" & val[0]["version"].str &
") depends on it"
else:
for i in 0 .. <val.len:
reason.add val[i]["name"].str & " (" & val[i]["version"].str & ")"
if i != <val.len:
reason.add ", "
reason.add " depend on it"
raise newException(EBabel, "Cannot uninstall " & pkgTup.name &
" because " & reason)
pkgsToDelete.add pkg
var pkgNames = ""
for i in 0 .. <pkgsToDelete.len:
if i != 0: pkgNames.add ", "
let pkg = pkgsToDelete[i]
pkgNames.add pkg.name & " (" & pkg.version & ")"
# Let's confirm that the user wants these packages removed.
if not options.prompt("The following packages will be removed:\n " &
pkgNames & "\nDo you wish to continue?"):
quit(QuitSuccess)
for pkg in pkgsToDelete:
# If we reach this point then the package can be safely removed.
removeRevDep(options, pkg)
removePkgDir(options.getPkgsDir / (pkg.name & '-' & pkg.version), options)
echo("Removed ", pkg.name, " (", $pkg.version, ")")
proc doAction(options: TOptions) = proc doAction(options: TOptions) =
if not existsDir(options.getBabelDir()): if not existsDir(options.getBabelDir()):
createDir(options.getBabelDir()) createDir(options.getBabelDir())
@ -569,13 +688,15 @@ proc doAction(options: TOptions) =
of ActionInstall: of ActionInstall:
var installList: seq[tuple[name: string, verRange: PVersionRange]] = @[] var installList: seq[tuple[name: string, verRange: PVersionRange]] = @[]
for name in options.action.optionalName: for name in options.action.optionalName:
if '#' in name: if '#' in name: # TODO: Change this to allow babel install pkg@>0.1
let i = find(name, '#') let i = find(name, '#')
installList.add((name[0 .. i-1], name[i .. -1].parseVersionRange())) installList.add((name[0 .. i-1], name[i .. -1].parseVersionRange()))
else: else:
installList.add((name, PVersionRange(kind: verAny))) installList.add((name, PVersionRange(kind: verAny)))
install(installList, options) install(installList, options)
of ActionUninstall:
uninstall(options)
of ActionSearch: of ActionSearch:
search(options) search(options)
of ActionList: of ActionList:

View file

@ -3,6 +3,9 @@
import parsecfg, json, streams, strutils, parseutils, os import parsecfg, json, streams, strutils, parseutils, os
import version, tools import version, tools
type type
## Tuple containing package name and version range.
TPkgTuple* = tuple[name: string, ver: PVersionRange]
TPackageInfo* = object TPackageInfo* = object
mypath*: string ## The path of this .babel file mypath*: string ## The path of this .babel file
name*: string name*: string
@ -16,7 +19,7 @@ type
installDirs*: seq[string] installDirs*: seq[string]
installFiles*: seq[string] installFiles*: seq[string]
installExt*: seq[string] installExt*: seq[string]
requires*: seq[tuple[name: string, ver: PVersionRange]] requires*: seq[TPkgTuple]
bin*: seq[string] bin*: seq[string]
srcDir*: string srcDir*: string
backend*: string backend*: string
@ -79,7 +82,7 @@ proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) =
"Version may only consist of numbers and the '.' character " & "Version may only consist of numbers and the '.' character " &
"but found '" & c & "'.") "but found '" & c & "'.")
proc parseRequires(req: string): tuple[name: string, ver: PVersionRange] = proc parseRequires(req: string): TPkgTuple =
try: try:
if ' ' in req: if ' ' in req:
var i = skipUntil(req, whitespace) var i = skipUntil(req, whitespace)
@ -275,7 +278,7 @@ proc getInstalledPkgs*(libsDir: string): seq[tuple[pkginfo: TPackageInfo, meta:
echo("WARNING: No .babel file found for ", path) echo("WARNING: No .babel file found for ", path)
proc findPkg*(pkglist: seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]], proc findPkg*(pkglist: seq[tuple[pkginfo: TPackageInfo, meta: TMetaData]],
dep: tuple[name: string, ver: PVersionRange], dep: TPkgTuple,
r: var TPackageInfo): bool = r: var TPackageInfo): bool =
## Searches ``pkglist`` for a package of which version is within the range ## 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 ## of ``dep.ver``. ``True`` is returned if a package is found. If multiple

View file

@ -2,7 +2,7 @@
# BSD License. Look at license.txt for more info. # BSD License. Look at license.txt for more info.
# #
# Various miscellaneous utility functions reside here. # Various miscellaneous utility functions reside here.
import osproc, pegs, strutils, os, parseurl, sets import osproc, pegs, strutils, os, parseurl, sets, json
import version, packageinfo import version, packageinfo
type type
@ -95,3 +95,13 @@ proc getDownloadDirName*(url: string, verRange: PVersionRange): string =
proc incl*(s: var TSet[string], v: seq[string] | TSet[string]) = proc incl*(s: var TSet[string], v: seq[string] | TSet[string]) =
for i in v: for i in v:
s.incl i 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