Implements babel uninstall.
This commit is contained in:
parent
3f4d8f4857
commit
05d9956662
3 changed files with 148 additions and 14 deletions
141
src/babel.nim
141
src/babel.nim
|
|
@ -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.
|
||||||
|
|
@ -86,9 +91,14 @@ proc parseCmdLine(): TOptions =
|
||||||
of cmdArgument:
|
of cmdArgument:
|
||||||
if result.action.typ == ActionNil:
|
if result.action.typ == ActionNil:
|
||||||
case key
|
case key
|
||||||
of "install":
|
of "install", "path":
|
||||||
result.action.typ = ActionInstall
|
case key
|
||||||
|
of "install":
|
||||||
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue