diff --git a/src/nimble.nim b/src/nimble.nim index 38fef3a..f8ac7c1 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -13,7 +13,7 @@ from sequtils import toSeq import nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, nimblepkg/download, nimblepkg/config, nimblepkg/common, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, - nimblepkg/cli, nimblepkg/packageinstaller + nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps import nimblepkg/nimscriptsupport @@ -141,61 +141,6 @@ proc copyFilesRec(origDir, currentDir, dest: string, result.incl copyFileD(pkgInfo.mypath, changeRoot(pkgInfo.mypath.splitFile.dir, dest, pkgInfo.mypath)) -proc saveNimbleData(options: Options) = - # TODO: This file should probably be locked. - writeFile(options.getNimbleDir() / "nimbledata.json", - pretty(options.nimbleData)) - -proc addRevDep(options: Options, dep: tuple[name, version: string], - pkg: PackageInfo) = - # let depNameVer = dep.name & '-' & dep.version - if not options.nimbleData["reverseDeps"].hasKey(dep.name): - options.nimbleData["reverseDeps"][dep.name] = newJObject() - if not options.nimbleData["reverseDeps"][dep.name].hasKey(dep.version): - options.nimbleData["reverseDeps"][dep.name][dep.version] = newJArray() - let revDep = %{ "name": %pkg.name, "version": %pkg.specialVersion} - let thisDep = options.nimbleData["reverseDeps"][dep.name][dep.version] - if revDep notin thisDep: - thisDep.add revDep - -proc removeRevDep(options: Options, pkg: PackageInfo) = - ## Removes ``pkg`` from the reverse dependencies of every package. - assert(not pkg.isMinimal) - proc remove(options: Options, pkg: PackageInfo, depTup: PkgTuple, - thisDep: JsonNode) = - 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.specialVersion): - newVal.add revDep - thisDep[ver] = newVal - - for depTup in pkg.requires: - if depTup.name.isURL(): - # We sadly must go through everything in this case... - for key, val in options.nimbleData["reverseDeps"]: - options.remove(pkg, depTup, val) - else: - let thisDep = options.nimbleData{"reverseDeps", depTup.name} - if thisDep.isNil: continue - options.remove(pkg, depTup, thisDep) - - # Clean up empty objects/arrays - var newData = newJObject() - for key, val in options.nimbleData["reverseDeps"]: - if val.len != 0: - var newVal = newJObject() - for ver, elem in val: - if elem.len != 0: - newVal[ver] = elem - if newVal.len != 0: - newData[key] = newVal - options.nimbleData["reverseDeps"] = newData - - saveNimbleData(options) - proc install(packages: seq[PkgTuple], options: Options, doPrompt = true): tuple[deps: seq[PackageInfo], pkg: PackageInfo] @@ -270,7 +215,7 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = # (unsatisfiable dependendencies). # N.B. NimbleData is saved in installFromDir. for i in reverseDeps: - addRevDep(options, i, pkginfo) + addRevDep(options.nimbleData, i, pkginfo) proc buildFromDir(pkgInfo: PackageInfo, paths: seq[string], args: var seq[string]) = @@ -416,6 +361,11 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): raise NimbleQuit(msg: "") + + # Remove reverse deps. + let pkgInfo = getPkgInfo(pkgDestDir, options) + options.nimbleData.removeRevDep(pkgInfo) + removePkgDir(pkgDestDir, options) # Remove any symlinked binaries for bin in pkgInfo.bin: @@ -426,7 +376,6 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, else: removeFile(binDir / bin) - createDir(pkgDestDir) # Copy this package's files based on the preferences specified in PkgInfo. var filesInstalled = initSet[string]() @@ -831,21 +780,20 @@ proc uninstall(options: Options) = for pkg in pkgList: # Check whether any packages depend on the ones the user is trying to # uninstall. - let thisPkgsDep = options.nimbleData["reverseDeps"]{pkg.name}{pkg.specialVersion} - if not thisPkgsDep.isNil: - var reason = "" - if thisPkgsDep.len == 1: - reason = "$1 ($2) depends on it" % [thisPkgsDep[0]["name"].str, - thisPkgsDep[0]["version"].str] - else: - for i in 0 .. 0: + errors.add("Cannot uninstall $1 ($2) because $3" % + [pkgTup.name, pkg.specialVersion, reason]) else: pkgsToDelete.add pkg @@ -869,12 +817,14 @@ proc uninstall(options: Options) = # removeRevDep needs the package dependency info, so we can't just pass # a minimal pkg info. - removeRevDep(options, pkg.toFullInfo(options)) + removeRevDep(options.nimbleData, pkg.toFullInfo(options)) removePkgDir(options.getPkgsDir / (pkg.name & '-' & pkg.specialVersion), options) display("Removed", "$1 ($2)" % [pkg.name, $pkg.specialVersion], Success, HighPriority) + saveNimbleData(options) + proc listTasks(options: Options) = let nimbleFile = findNimbleFile(getCurrentDir(), true) nimscriptsupport.listTasks(nimbleFile, options) diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim new file mode 100644 index 0000000..551ee19 --- /dev/null +++ b/src/nimblepkg/reversedeps.nim @@ -0,0 +1,115 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import os, json + +import options, common, version, download, packageinfo + +proc saveNimbleData*(options: Options) = + # TODO: This file should probably be locked. + writeFile(options.getNimbleDir() / "nimbledata.json", + pretty(options.nimbleData)) + +proc addRevDep*(nimbleData: JsonNode, dep: tuple[name, version: string], + pkg: PackageInfo) = + # Add a record which specifies that `pkg` has a dependency on `dep`, i.e. + # the reverse dependency of `dep` is `pkg`. + if not nimbleData["reverseDeps"].hasKey(dep.name): + nimbleData["reverseDeps"][dep.name] = newJObject() + if not nimbleData["reverseDeps"][dep.name].hasKey(dep.version): + nimbleData["reverseDeps"][dep.name][dep.version] = newJArray() + let revDep = %{ "name": %pkg.name, "version": %pkg.specialVersion} + let thisDep = nimbleData["reverseDeps"][dep.name][dep.version] + if revDep notin thisDep: + thisDep.add revDep + +proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = + ## Removes ``pkg`` from the reverse dependencies of every package. + assert(not pkg.isMinimal) + proc remove(pkg: PackageInfo, depTup: PkgTuple, thisDep: JsonNode) = + 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.specialVersion): + newVal.add revDep + thisDep[ver] = newVal + + for depTup in pkg.requires: + if depTup.name.isURL(): + # We sadly must go through everything in this case... + for key, val in nimbleData["reverseDeps"]: + remove(pkg, depTup, val) + else: + let thisDep = nimbleData{"reverseDeps", depTup.name} + if thisDep.isNil: continue + remove(pkg, depTup, thisDep) + + # Clean up empty objects/arrays + var newData = newJObject() + for key, val in nimbleData["reverseDeps"]: + if val.len != 0: + var newVal = newJObject() + for ver, elem in val: + if elem.len != 0: + newVal[ver] = elem + if newVal.len != 0: + newData[key] = newVal + nimbleData["reverseDeps"] = newData + +proc getRevDeps*(options: Options, pkg: PackageInfo): seq[PkgTuple] = + ## Returns a list of *currently installed* reverse dependencies for `pkg`. + result = @[] + let thisPkgsDep = + options.nimbleData["reverseDeps"]{pkg.name}{pkg.specialVersion} + if not thisPkgsDep.isNil: + let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) + for pkg in thisPkgsDep: + let pkgTup = ( + name: pkg["name"].getStr(), + ver: parseVersionRange(pkg["version"].getStr()) + ) + var pkgInfo: PackageInfo + if not findPkg(pkgList, pkgTup, pkgInfo): + continue + + result.add(pkgTup) + +when isMainModule: + var nimbleData = %{"reverseDeps": newJObject()} + + let nimforum1 = PackageInfo( + isMinimal: false, + name: "nimforum", + specialVersion: "0.1.0", + requires: @[("jester", parseVersionRange("0.1.0")), + ("captcha", parseVersionRange("1.0.0")), + ("auth", parseVersionRange("#head"))] + ) + let nimforum2 = PackageInfo(isMinimal: false, name: "nimforum", specialVersion: "0.2.0") + let play = PackageInfo(isMinimal: false, name: "play", specialVersion: "#head") + + nimbleData.addRevDep(("jester", "0.1.0"), nimforum1) + nimbleData.addRevDep(("jester", "0.1.0"), play) + nimbleData.addRevDep(("captcha", "1.0.0"), nimforum1) + nimbleData.addRevDep(("auth", "#head"), nimforum1) + nimbleData.addRevDep(("captcha", "1.0.0"), nimforum2) + nimbleData.addRevDep(("auth", "#head"), nimforum2) + + doAssert nimbleData["reverseDeps"]["jester"]["0.1.0"].len == 2 + doAssert nimbleData["reverseDeps"]["captcha"]["1.0.0"].len == 2 + doAssert nimbleData["reverseDeps"]["auth"]["#head"].len == 2 + + block: + nimbleData.removeRevDep(nimforum1) + let jester = nimbleData["reverseDeps"]["jester"]["0.1.0"][0] + doAssert jester["name"].getStr() == play.name + doAssert jester["version"].getStr() == play.specialVersion + + let captcha = nimbleData["reverseDeps"]["captcha"]["1.0.0"][0] + doAssert captcha["name"].getStr() == nimforum2.name + doAssert captcha["version"].getStr() == nimforum2.specialVersion + + echo("Everything works!") + diff --git a/tests/revdep/mydep/mydep.nimble b/tests/revdep/mydep/mydep.nimble new file mode 100644 index 0000000..1d5f3a9 --- /dev/null +++ b/tests/revdep/mydep/mydep.nimble @@ -0,0 +1,10 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Random dep" +license = "MIT" + +# Dependencies + +requires "nim >= 0.15.0" \ No newline at end of file diff --git a/tests/revdep/pkgNoDep/pkgA.nimble b/tests/revdep/pkgNoDep/pkgA.nimble new file mode 100644 index 0000000..fd6878a --- /dev/null +++ b/tests/revdep/pkgNoDep/pkgA.nimble @@ -0,0 +1,11 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Correctly structured package A" +license = "MIT" + +# Dependencies + +requires "nim >= 0.15.0" + diff --git a/tests/revdep/pkgWithDep/pkgA.nimble b/tests/revdep/pkgWithDep/pkgA.nimble new file mode 100644 index 0000000..842af28 --- /dev/null +++ b/tests/revdep/pkgWithDep/pkgA.nimble @@ -0,0 +1,11 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Correctly structured package A" +license = "MIT" + +# Dependencies + +requires "nim >= 0.15.0", "mydep" + diff --git a/tests/tester.nim b/tests/tester.nim index 0732177..8496b8d 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -38,6 +38,15 @@ proc execNimble(args: varargs[string]): tuple[output: string, exitCode: int] = result = execCmdEx(quotedArgs.join(" ")) +proc execNimbleYes(args: varargs[string]): tuple[output: string, exitCode: int]= + # issue #6314 + execNimble(@args & "-y") + +template verify(res: (string, int)) = + let r = res + checkpoint r[0] + check r[1] == QuitSuccess + proc processOutput(output: string): seq[string] = output.strip.splitLines().filter((x: string) => (x.len > 0)) @@ -466,6 +475,29 @@ test "can pass args with spaces to Nim (#351)": checkpoint output check exitCode == QuitSuccess +suite "reverse dependencies": + test "basic test": + cd "revdep/mydep": + verify execNimbleYes("install") + + cd "revdep/pkgWithDep": + verify execNimbleYes("install") + + verify execNimbleYes("remove", "pkgA") + verify execNimbleYes("remove", "mydep") + + test "issue #373": + cd "revdep/mydep": + verify execNimbleYes("install") + + cd "revdep/pkgWithDep": + verify execNimbleYes("install") + + cd "revdep/pkgNoDep": + verify execNimbleYes("install") + + verify execNimbleYes("remove", "mydep") + suite "develop feature": test "can reject binary packages": cd "develop/binary":