From 4fb4e2c4bb38037a2a4e97304424c4711a73e992 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Thu, 15 Aug 2019 20:31:01 +0300 Subject: [PATCH 01/73] Add type aliases for some tuples All tuples from the same type which are found in more than one place in the code are type aliased and their usage is replaced with the corresponding alias. Related to nim-lang/nimble#127 --- src/nimble.nim | 13 +++++-------- src/nimblepkg/common.nim | 2 ++ src/nimblepkg/packageinfo.nim | 12 ++++++++---- src/nimblepkg/packageparser.nim | 2 +- src/nimblepkg/reversedeps.nim | 2 +- src/nimblepkg/tools.nim | 5 ++--- tests/nim.cfg | 1 + tests/tester.nim | 10 +++++----- 8 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 tests/nim.cfg diff --git a/src/nimble.nim b/src/nimble.nim index 889506a2..6e01986b 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -46,7 +46,7 @@ proc refresh(options: Options) = proc install(packages: seq[PkgTuple], options: Options, - doPrompt = true): tuple[deps: seq[PackageInfo], pkg: PackageInfo] + doPrompt = true): PackageDependenciesInfo proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = ## Verifies and installs dependencies. ## @@ -58,9 +58,9 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = "dependencies for $1@$2" % [pkginfo.name, pkginfo.specialVersion], priority = HighPriority) - var pkgList {.global.}: seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = @[] + var pkgList {.global.}: seq[PackageInfoAndMetaData] = @[] once: pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - var reverseDeps: seq[tuple[name, version: string]] = @[] + var reverseDeps: seq[PackageReverseDependency] = @[] for dep in pkginfo.requires: if dep.name == "nimrod" or dep.name == "nim": let nimVer = getNimrodVersion(options) @@ -259,10 +259,7 @@ proc vcsRevisionInDir(dir: string): string = discard proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, - url: string): tuple[ - deps: seq[PackageInfo], - pkg: PackageInfo - ] = + url: string): PackageDependenciesInfo = ## Returns where package has been installed to, together with paths ## to the packages this package depends on. ## The return value of this function is used by @@ -428,7 +425,7 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options, proc install(packages: seq[PkgTuple], options: Options, - doPrompt = true): tuple[deps: seq[PackageInfo], pkg: PackageInfo] = + doPrompt = true): PackageDependenciesInfo = if packages == @[]: result = installFromDir(getCurrentDir(), newVRAny(), options, "") else: diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 90ab3502..87073830 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -46,6 +46,8 @@ when not defined(nimscript): ## Same as quit(QuitSuccess), but allows cleanup. NimbleQuit* = ref object of CatchableError + ProcessOutput* = tuple[output: string, exitCode: int] + proc raiseNimbleError*(msg: string, hint = "") = var exc = newException(NimbleError, msg) exc.hint = hint diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 77641535..704b364e 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -31,6 +31,10 @@ type nimbleFilePath*: string packageDir*: string + PackageReverseDependency* = tuple[name, version: string] + PackageDependenciesInfo* = tuple[deps: seq[PackageInfo], pkg: PackageInfo] + PackageInfoAndMetaData* = tuple[pkginfo: PackageInfo, meta: MetaData] + proc initPackageInfo*(path: string): PackageInfo = result.myPath = path result.specialVersion = "" @@ -65,7 +69,7 @@ proc toValidPackageName*(name: string): string = of AllChars - IdentChars - {'-'}: discard else: result.add(c) -proc getNameVersion*(pkgpath: string): tuple[name, version: string] = +proc getNameVersion*(pkgpath: string): PackageReverseDependency = ## Splits ``pkgpath`` in the format ``/home/user/.nimble/pkgs/package-0.1`` ## into ``(packagea, 0.1)`` ## @@ -334,7 +338,7 @@ proc findNimbleFile*(dir: string; error: bool): string = display("Hint:", hintMsg, Warning, HighPriority) proc getInstalledPkgsMin*(libsDir: string, options: Options): - seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = + seq[PackageInfoAndMetaData] = ## Gets a list of installed packages. The resulting package info is ## minimal. This has the advantage that it does not depend on the ## ``packageparser`` module, and so can be used by ``nimscriptwrapper``. @@ -384,7 +388,7 @@ proc resolveAlias*(dep: PkgTuple, options: Options): PkgTuple = # no alias is present. result.name = pkg.name -proc findPkg*(pkglist: seq[tuple[pkgInfo: PackageInfo, meta: MetaData]], +proc findPkg*(pkglist: seq[PackageInfoAndMetaData], dep: PkgTuple, r: var PackageInfo): bool = ## Searches ``pkglist`` for a package of which version is within the range @@ -402,7 +406,7 @@ proc findPkg*(pkglist: seq[tuple[pkgInfo: PackageInfo, meta: MetaData]], r = pkg.pkginfo result = true -proc findAllPkgs*(pkglist: seq[tuple[pkgInfo: PackageInfo, meta: MetaData]], +proc findAllPkgs*(pkglist: seq[PackageInfoAndMetaData], dep: PkgTuple): seq[PackageInfo] = ## Searches ``pkglist`` for packages of which version is within the range ## of ``dep.ver``. This is similar to ``findPkg`` but returns multiple diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 1f957dcf..ff706b9f 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -443,7 +443,7 @@ proc getPkgInfo*(dir: string, options: Options): PackageInfo = return getPkgInfoFromFile(nimbleFile, options) proc getInstalledPkgs*(libsDir: string, options: Options): - seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = + seq[PackageInfoAndMetaData] = ## Gets a list of installed packages. ## ## ``libsDir`` is in most cases: ~/.nimble/pkgs/ diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 45d9940f..7c730dd0 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -10,7 +10,7 @@ proc saveNimbleData*(options: Options) = writeFile(options.getNimbleDir() / "nimbledata.json", pretty(options.nimbleData)) -proc addRevDep*(nimbleData: JsonNode, dep: tuple[name, version: string], +proc addRevDep*(nimbleData: JsonNode, dep: PackageReverseDependency, pkg: PackageInfo) = # Add a record which specifies that `pkg` has a dependency on `dep`, i.e. # the reverse dependency of `dep` is `pkg`. diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 72a8250e..6e70407e 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -3,7 +3,7 @@ # # Various miscellaneous utility functions reside here. import osproc, pegs, strutils, os, uri, sets, json, parseutils -import version, cli, options +import common, version, cli, options from net import SslCVerifyMode, newContext, SslContext proc extractBin(cmd: string): string = @@ -41,8 +41,7 @@ proc doCmd*(cmd: string) = "Execution failed with exit code $1\nCommand: $2\nOutput: $3" % [$exitCode, cmd, output]) -{.warning[Deprecated]: off.} -proc doCmdEx*(cmd: string): tuple[output: TaintedString, exitCode: int] = +proc doCmdEx*(cmd: string): ProcessOutput = let bin = extractBin(cmd) if findExe(bin) == "": raise newException(NimbleError, "'" & bin & "' not in PATH.") diff --git a/tests/nim.cfg b/tests/nim.cfg new file mode 100644 index 00000000..4cdbf076 --- /dev/null +++ b/tests/nim.cfg @@ -0,0 +1 @@ +--path:"../src/" diff --git a/tests/tester.nim b/tests/tester.nim index fb902381..b2cd11c7 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1,6 +1,8 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. + import osproc, unittest, strutils, os, sequtils, sugar, strformat +import nimblepkg/common # TODO: Each test should start off with a clean slate. Currently installed # packages are shared between each test which causes a multitude of issues @@ -28,7 +30,7 @@ template cd*(dir: string, body: untyped) = body setCurrentDir(lastDir) -proc execNimble(args: varargs[string]): tuple[output: string, exitCode: int] = +proc execNimble(args: varargs[string]): ProcessOutput = var quotedArgs = @args quotedArgs.insert("--nimbleDir:" & installDir) quotedArgs.insert(nimblePath) @@ -49,7 +51,7 @@ proc execNimble(args: varargs[string]): tuple[output: string, exitCode: int] = checkpoint(cmd) checkpoint(result.output) -proc execNimbleYes(args: varargs[string]): tuple[output: string, exitCode: int]= +proc execNimbleYes(args: varargs[string]): ProcessOutput = # issue #6314 execNimble(@args & "-y") @@ -288,7 +290,7 @@ suite "nimscript": cd "invalidPackage": let (output, exitCode) = execNimble("check") let lines = output.strip.processOutput() - check(lines[^2].contains("undeclared identifier: 'thisFieldDoesNotExist'")) + check lines.inLines("undeclared identifier: 'thisFieldDoesNotExist'") check exitCode == QuitFailure test "can accept short flags (#329)": @@ -815,7 +817,6 @@ suite "nimble run": check exitCode == QuitSuccess check output.contains("tests$1run$1$2 --test" % [$DirSep, "run".changeFileExt(ExeExt)]) - echo output check output.contains("""Testing `nimble run`: @["--test"]""") test "Nimble options before --": @@ -829,7 +830,6 @@ suite "nimble run": check exitCode == QuitSuccess check output.contains("tests$1run$1$2 --test" % [$DirSep, "run".changeFileExt(ExeExt)]) - echo output check output.contains("""Testing `nimble run`: @["--test"]""") test "Compilation flags before run command": From b72fdc4c466628222be842ddd57442dc33886a75 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sat, 17 Aug 2019 17:49:13 +0300 Subject: [PATCH 02/73] Implement calculation of a package sha1 checksum Related to nim-lang/nimble#127 --- src/nimblepkg/checksum.nim | 49 ++++++++++++++++++++++++++++++++++++++ src/nimblepkg/tools.nim | 10 +++++++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/nimblepkg/checksum.nim diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim new file mode 100644 index 00000000..d9f5304a --- /dev/null +++ b/src/nimblepkg/checksum.nim @@ -0,0 +1,49 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import os, strutils, std/sha1, algorithm +import tools + +proc extractFileList(consoleOutput: string): seq[string] = + result = consoleOutput.splitLines() + discard result.pop() + +proc getPackageFileListFromGit(): seq[string] = + let output = tryDoCmdEx("git ls-files") + extractFileList(string(output)) + +proc getPackageFileListFromMercurial(): seq[string] = + let output = tryDoCmdEx("hg manifest") + extractFileList(string(output)) + +proc getPackageFileListWithoutScm(): seq[string] = + for file in walkDirRec(".", relative = true): + result.add(file) + +proc getPackageFileList(): seq[string] = + if existsDir(".git"): + result = getPackageFileListFromGit() + elif existsDir(".hg"): + result = getPackageFileListFromMercurial() + else: + result = getPackageFileListWithoutScm() + result.sort() + +proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = + checksum.update(fileName) + let file = fileName.open(fmRead) + defer: close(file) + const bufferSize = 8192 + var buffer = newString(bufferSize) + while true: + var bytesRead = readChars(file, buffer, 0, bufferSize) + if bytesRead == 0: break + checksum.update(buffer[0.. Date: Sun, 1 Sep 2019 21:40:56 +0300 Subject: [PATCH 03/73] Add a package sha1 checksum to its dir name - The package sha1 checksum is added to the `PackageInfo` object when it is being initialized either by calculating it or by getting it from the package directory name in the local package cache. - When package is being written in the local cache its sha1 checksum is added to the directory name alongside its name and version. - Reverse dependencies format is changed to contain also the package sha1 checksum. Conversion between the old and the new formats is implemented for backward compatibility. All functionality related to the reverse dependencies is fixed to work properly with the new format. - The indexing of the `nimbledata.json` fields is changed to use values from an enum for their names to avoid typing mistakes. - All unit tests are fixed to pass with the new changes and new unit tests are added where it is needed. Related to nim-lang/nimble#127 --- src/nimble.nim | 9 +- src/nimblepkg/checksum.nim | 6 +- src/nimblepkg/common.nim | 118 +++++---- src/nimblepkg/jsonhelpers.nim | 149 +++++++++++ src/nimblepkg/options.nim | 32 ++- src/nimblepkg/packageinfo.nim | 137 ++++++---- src/nimblepkg/packageparser.nim | 15 +- src/nimblepkg/reversedeps.nim | 258 ++++++++++++++----- src/nimblepkg/tools.nim | 2 +- tests/revdep/nimbleData/new_nimble_data.json | 59 +++++ tests/revdep/nimbleData/old_nimble_data.json | 44 ++++ tests/tester.nim | 81 ++++-- 12 files changed, 700 insertions(+), 210 deletions(-) create mode 100644 src/nimblepkg/jsonhelpers.nim create mode 100644 tests/revdep/nimbleData/new_nimble_data.json create mode 100644 tests/revdep/nimbleData/old_nimble_data.json diff --git a/src/nimble.nim b/src/nimble.nim index 6e01986b..dc5f545c 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -60,7 +60,7 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = var pkgList {.global.}: seq[PackageInfoAndMetaData] = @[] once: pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - var reverseDeps: seq[PackageReverseDependency] = @[] + var reverseDeps: seq[PackageBasicInfo] = @[] for dep in pkginfo.requires: if dep.name == "nimrod" or dep.name == "nim": let nimVer = getNimrodVersion(options) @@ -96,7 +96,7 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = result.add(pkg) # Process the dependencies of this dependency. result.add(processDeps(pkg.toFullInfo(options), options)) - reverseDeps.add((pkg.name, pkg.specialVersion)) + reverseDeps.add((pkg.name, pkg.specialVersion, pkg.checksum)) # Check if two packages of the same name (but different version) are listed # in the path. @@ -220,7 +220,7 @@ proc removePkgDir(dir: string, options: Options) = # Search for an older version of the package we are removing. # So that we can reinstate its symlink. - let (pkgName, _) = getNameVersion(dir) + let (pkgName, _, _) = getNameVersionChecksum(dir) let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) var pkgInfo: PackageInfo if pkgList.findPkg((pkgName, newVRAny()), pkgInfo): @@ -912,8 +912,7 @@ proc uninstall(options: Options) = # removeRevDep needs the package dependency info, so we can't just pass # a minimal pkg info. removeRevDep(options.nimbleData, pkg.toFullInfo(options)) - removePkgDir(options.getPkgsDir / (pkg.name & '-' & pkg.specialVersion), - options) + removePkgDir(pkg.getPkgDest(options), options) display("Removed", "$1 ($2)" % [pkg.name, $pkg.specialVersion], Success, HighPriority) diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index d9f5304a..9260414b 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -21,9 +21,9 @@ proc getPackageFileListWithoutScm(): seq[string] = result.add(file) proc getPackageFileList(): seq[string] = - if existsDir(".git"): + if dirExists(".git"): result = getPackageFileListFromGit() - elif existsDir(".hg"): + elif dirExists(".hg"): result = getPackageFileListFromMercurial() else: result = getPackageFileListWithoutScm() @@ -36,7 +36,7 @@ proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = const bufferSize = 8192 var buffer = newString(bufferSize) while true: - var bytesRead = readChars(file, buffer, 0, bufferSize) + var bytesRead = readChars(file, buffer) if bytesRead == 0: break checksum.update(buffer[0.. 0: pkgChecksum + else: calculatePackageSha1Checksum(fileDir) proc toValidPackageName*(name: string): string = result = "" @@ -69,31 +88,6 @@ proc toValidPackageName*(name: string): string = of AllChars - IdentChars - {'-'}: discard else: result.add(c) -proc getNameVersion*(pkgpath: string): PackageReverseDependency = - ## Splits ``pkgpath`` in the format ``/home/user/.nimble/pkgs/package-0.1`` - ## into ``(packagea, 0.1)`` - ## - ## Also works for file paths like: - ## ``/home/user/.nimble/pkgs/package-0.1/package.nimble`` - if pkgPath.splitFile.ext in [".nimble", ".nimble-link", ".babel"]: - return getNameVersion(pkgPath.splitPath.head) - - result.name = "" - result.version = "" - let tail = pkgpath.splitPath.tail - - const specialSeparator = "-#" - var sepIdx = tail.find(specialSeparator) - if sepIdx == -1: - sepIdx = tail.rfind('-') - - if sepIdx == -1: - result.name = tail - return - - result.name = tail[0 .. sepIdx - 1] - result.version = tail.substr(sepIdx + 1) - proc optionalField(obj: JsonNode, name: string, default = ""): string = ## Queries ``obj`` for the optional ``name`` string. ## @@ -350,11 +344,12 @@ proc getInstalledPkgsMin*(libsDir: string, options: Options): let nimbleFile = findNimbleFile(path, false) if nimbleFile != "": let meta = readMetaData(path) - let (name, version) = getNameVersion(path) + let (name, version, checksum) = getNameVersionChecksum(path) var pkg = initPackageInfo(nimbleFile) pkg.name = name pkg.version = version pkg.specialVersion = version + pkg.checksum = checksum pkg.isMinimal = true pkg.isInstalled = true let nimbleFileDir = nimbleFile.splitFile().dir @@ -553,7 +548,7 @@ proc iterInstallFiles*(realDir: string, pkgInfo: PackageInfo, action(file) proc getPkgDest*(pkgInfo: PackageInfo, options: Options): string = - let versionStr = '-' & pkgInfo.specialVersion + let versionStr = '-' & pkgInfo.specialVersion & '-' & pkgInfo.checksum let pkgDestDir = options.getPkgsDir() / (pkgInfo.name & versionStr) return pkgDestDir @@ -567,20 +562,66 @@ proc hash*(x: PackageInfo): Hash = result = !$h when isMainModule: - doAssert getNameVersion("/home/user/.nimble/libs/packagea-0.1") == - ("packagea", "0.1") - doAssert getNameVersion("/home/user/.nimble/libs/package-a-0.1") == - ("package-a", "0.1") - doAssert getNameVersion("/home/user/.nimble/libs/package-a-0.1/package.nimble") == - ("package-a", "0.1") - doAssert getNameVersion("/home/user/.nimble/libs/package-#head") == - ("package", "#head") - doAssert getNameVersion("/home/user/.nimble/libs/package-#branch-with-dashes") == - ("package", "#branch-with-dashes") - # readPackageInfo (and possibly more) depends on this not raising. - doAssert getNameVersion("/home/user/.nimble/libs/package") == ("package", "") + import unittest - doAssert toValidPackageName("foo__bar") == "foo_bar" - doAssert toValidPackageName("jhbasdh!£$@%#^_&*_()qwe") == "jhbasdh_qwe" + check getNameVersionChecksum( + "/home/user/.nimble/libs/packagea-0.1") == + ("packagea", "0.1", "") - echo("All tests passed!") + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1") == + ("package-a", "0.1", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1/package.nimble") == + ("package-a", "0.1", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#head") == + ("package", "#head", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#branch-with-dashes") == + ("package", "#branch-with-dashes", "") + + # readPackageInfo (and possibly more) depends on this not raising. + check getNameVersionChecksum( + "/home/user/.nimble/libs/package") == + ("package", "", "") + + # Tests with hash sums in the package directory names + + check getNameVersionChecksum( + "/home/user/.nimble/libs/packagea-0.1-" & + "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") == + ("packagea", "0.1", "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1-" & + "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") == + ("package-a", "0.1", "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1-" & + "43e3b1138312656310e93ffcfdd866b2dcce3b35/package.nimble") == + ("package-a", "0.1", "43e3b1138312656310e93ffcfdd866b2dcce3b35") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#head-" & + "efba335dccf2631d7ac2740109142b92beb3b465") == + ("package", "#head", "efba335dccf2631d7ac2740109142b92beb3b465") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#branch-with-dashes-" & + "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") == + ("package", "#branch-with-dashes", "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-" & + "b12e18db49fc60df117e5d8a289c4c2050a272dd") == + ("package", "", "b12e18db49fc60df117e5d8a289c4c2050a272dd") + + check toValidPackageName("foo__bar") == "foo_bar" + check toValidPackageName("jhbasdh!£$@%#^_&*_()qwe") == "jhbasdh_qwe" + + reportUnitTestSuccess() diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index ff706b9f..86690d3b 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -349,7 +349,6 @@ proc readPackageInfo(result: var PackageInfo, nf: NimbleFile, options: Options, return result = initPackageInfo(nf) - let minimalInfo = getNameVersion(nf) validatePackageName(nf.splitFile.name) @@ -365,8 +364,6 @@ proc readPackageInfo(result: var PackageInfo, nf: NimbleFile, options: Options, if not success: if onlyMinimalInfo: - result.name = minimalInfo.name - result.version = minimalInfo.version result.isNimScript = true result.isMinimal = true @@ -396,9 +393,9 @@ proc readPackageInfo(result: var PackageInfo, nf: NimbleFile, options: Options, # The package directory name may include a "special" version # (example #head). If so, it is given higher priority and therefore # overwrites the .nimble file's version. - let version = parseVersionRange(minimalInfo.version) + let version = parseVersionRange(result.version) if version.kind == verSpecial: - result.specialVersion = minimalInfo.version + result.specialVersion = result.version # Apply rules to infer which files should/shouldn't be installed. See #469. inferInstallRules(result, options) @@ -440,7 +437,7 @@ proc getPkgInfoFromFile*(file: NimbleFile, options: Options): PackageInfo = proc getPkgInfo*(dir: string, options: Options): PackageInfo = ## Find the .nimble file in ``dir`` and parses it, returning a PackageInfo. let nimbleFile = findNimbleFile(dir, true) - return getPkgInfoFromFile(nimbleFile, options) + result = getPkgInfoFromFile(nimbleFile, options) proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfoAndMetaData] = @@ -454,8 +451,10 @@ proc getInstalledPkgs*(libsDir: string, options: Options): " this error message, remove $1." proc createErrorMsg(tmplt, path, msg: string): string = - let (name, version) = getNameVersion(path) - return tmplt % [name, version, msg] + let (name, version, checksum) = getNameVersionChecksum(path) + let fullVersion = if checksum.len > 0: version & '-' & checksum + else: version + return tmplt % [name, fullVersion, msg] display("Loading", "list of installed packages", priority = MediumPriority) diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 7c730dd0..05319252 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -3,72 +3,78 @@ import os, json, sets -import options, common, version, download, packageinfo +import options, common, version, download, packageinfo, jsonhelpers proc saveNimbleData*(options: Options) = # TODO: This file should probably be locked. - writeFile(options.getNimbleDir() / "nimbledata.json", + writeFile(options.getNimbleDir() / nimbleDataFile.name, pretty(options.nimbleData)) -proc addRevDep*(nimbleData: JsonNode, dep: PackageReverseDependency, +proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, 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 + + let dependencies = nimbleData.addIfNotExist( + $ndjkRevDep, + dep.name, + dep.version, + dep.checksum, + newJArray(), + ) + + let dependency = %{ + $ndjkRevDepName: %pkg.name, + $ndjkRevDepVersion: %pkg.specialVersion, + $ndjkRevDepChecksum: %pkg.checksum, + } + + if dependency notin dependencies: + dependencies.add(dependency) 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 version, revDepsForVersion in thisDep: + if version.newVersion in depTup.ver: + for checksum, revDepsForChecksum in revDepsForVersion: + var newVal = newJArray() + for rd in revDepsForChecksum: + # if the reverse dependency is different than the package which we + # currently deleting, it will be kept. + if rd[$ndjkRevDepName].str != pkg.name or + rd[$ndjkRevDepVersion].str != pkg.specialVersion or + rd[$ndjkRevDepChecksum].str != pkg.checksum: + newVal.add rd + revDepsForVersion[checksum] = newVal + + let reverseDependencies = nimbleData[$ndjkRevDep] for depTup in pkg.requires: if depTup.name.isURL(): # We sadly must go through everything in this case... - for key, val in nimbleData["reverseDeps"]: + for key, val in reverseDependencies: remove(pkg, depTup, val) else: - let thisDep = nimbleData{"reverseDeps", depTup.name} + let thisDep = nimbleData{$ndjkRevDep, 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 + nimbleData[$ndjkRevDep] = cleanUpEmptyObjects(reverseDependencies) proc getRevDepTups*(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} + let thisPkgsDep = options.nimbleData[$ndjkRevDep]{ + pkg.name}{pkg.specialVersion}{pkg.checksum} 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()) + name: pkg[$ndjkRevDepName].getStr(), + ver: parseVersionRange(pkg[$ndjkRevDepVersion].getStr()) ) var pkgInfo: PackageInfo if not findPkg(pkgList, pkgTup, pkgInfo): @@ -83,7 +89,8 @@ proc getRevDeps*(options: Options, pkg: PackageInfo): HashSet[PackageInfo] = for rdepInfo in findAllPkgs(installedPkgs, rdepTup): result.incl rdepInfo -proc getAllRevDeps*(options: Options, pkg: PackageInfo, result: var HashSet[PackageInfo]) = +proc getAllRevDeps*(options: Options, pkg: PackageInfo, + result: var HashSet[PackageInfo]) = if pkg in result: return @@ -97,7 +104,8 @@ proc getAllRevDeps*(options: Options, pkg: PackageInfo, result: var HashSet[Pack result.incl pkg when isMainModule: - var nimbleData = %{"reverseDeps": newJObject()} + + import unittest let nimforum1 = PackageInfo( isMinimal: false, @@ -105,31 +113,155 @@ when isMainModule: 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 + ("auth", parseVersionRange("#head"))], + checksum: "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2", + ) + + let nimforum2 = PackageInfo( + isMinimal: false, + name: "nimforum", + specialVersion: "0.2.0", + checksum: "B60044137CEA185F287346EBEAB6B3E0895BDA4D", + ) + + let play = PackageInfo( + isMinimal: false, + name: "play", + specialVersion: "#head", + checksum: "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + ) - let captcha = nimbleData["reverseDeps"]["captcha"]["1.0.0"][0] - doAssert captcha["name"].getStr() == nimforum2.name - doAssert captcha["version"].getStr() == nimforum2.specialVersion + proc setupNimbleData(): JsonNode = + result = newNimbleDataNode() - echo("Everything works!") + result.addRevDep( + ("jester", "0.1.0", "1B629F98B23614DF292F176A1681FA439DCC05E2"), + nimforum1) + + result.addRevDep(("jester", "0.1.0", ""), play) + + result.addRevDep( + ("captcha", "1.0.0", "CE128561B06DD106A83638AD415A2A52548F388E"), + nimforum1) + + result.addRevDep( + ("auth", "#head", "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01"), + nimforum1) + + result.addRevDep( + ("captcha", "1.0.0", "CE128561B06DD106A83638AD415A2A52548F388E"), + nimforum2) + + result.addRevDep( + ("auth", "#head", "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01"), + nimforum2) + + proc testAddRevDep() = + + let expectedResult = """{ + "version": "0.1.0", + "reverseDeps": { + "jester": { + "0.1.0": { + "1B629F98B23614DF292F176A1681FA439DCC05E2": [ + { + "name": "nimforum", + "version": "0.1.0", + "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + } + ], + "": [ + { + "name": "play", + "version": "#head", + "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + } + ] + } + }, + "captcha": { + "1.0.0": { + "CE128561B06DD106A83638AD415A2A52548F388E": [ + { + "name": "nimforum", + "version": "0.1.0", + "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + }, + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + } + ] + } + }, + "auth": { + "#head": { + "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ + { + "name": "nimforum", + "version": "0.1.0", + "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + }, + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + } + ] + } + } + } + }""".parseJson() + + let nimbleData = setupNimbleData() + check nimbleData == expectedResult + + proc testRemoveRevDep() = + + let expectedResult = """{ + "version": "0.1.0", + "reverseDeps": { + "jester": { + "0.1.0": { + "": [ + { + "name": "play", + "version": "#head", + "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + } + ] + } + }, + "captcha": { + "1.0.0": { + "CE128561B06DD106A83638AD415A2A52548F388E": [ + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + } + ] + } + }, + "auth": { + "#head": { + "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + } + ] + } + } + } + }""".parseJson() + + let nimbleData = setupNimbleData() + nimbleData.removeRevDep(nimforum1) + check nimbleData == expectedResult + testAddRevDep() + testRemoveRevDep() + reportUnitTestSuccess() diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 00d1cfbd..9febfa99 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -47,7 +47,7 @@ proc doCmdEx*(cmd: string): ProcessOutput = raise newException(NimbleError, "'" & bin & "' not in PATH.") return execCmdEx(cmd) -proc tryDoCmdEx*(cmd: string): TaintedString = +proc tryDoCmdEx*(cmd: string): string = let (output, exitCode) = doCmdEx(cmd) if exitCode != QuitSuccess: raise newException( diff --git a/tests/revdep/nimbleData/new_nimble_data.json b/tests/revdep/nimbleData/new_nimble_data.json new file mode 100644 index 00000000..4a148b89 --- /dev/null +++ b/tests/revdep/nimbleData/new_nimble_data.json @@ -0,0 +1,59 @@ +{ + "version": "0.1.0", + "reverseDeps": { + "stew": { + "0.1.0": { + "": [ + { + "name": "faststreams", + "version": "0.1.0", + "checksum": "" + }, + { + "name": "serialization", + "version": "0.1.0", + "checksum": "" + }, + { + "name": "json_serialization", + "version": "0.1.0", + "checksum": "" + } + ] + } + }, + "faststreams": { + "0.1.0": { + "": [ + { + "name": "serialization", + "version": "0.1.0", + "checksum": "" + } + ] + } + }, + "serialization": { + "0.1.0": { + "": [ + { + "name": "json_serialization", + "version": "0.1.0", + "checksum": "" + } + ] + } + }, + "json_serialization": { + "0.1.0": { + "": [ + { + "name": "chronicles", + "version": "0.6.0", + "checksum": "" + } + ] + } + } + } +} diff --git a/tests/revdep/nimbleData/old_nimble_data.json b/tests/revdep/nimbleData/old_nimble_data.json new file mode 100644 index 00000000..519f9bc6 --- /dev/null +++ b/tests/revdep/nimbleData/old_nimble_data.json @@ -0,0 +1,44 @@ +{ + "reverseDeps": { + "stew": { + "0.1.0": [ + { + "name": "faststreams", + "version": "0.1.0" + }, + { + "name": "serialization", + "version": "0.1.0" + }, + { + "name": "json_serialization", + "version": "0.1.0" + } + ] + }, + "faststreams": { + "0.1.0": [ + { + "name": "serialization", + "version": "0.1.0" + } + ] + }, + "serialization": { + "0.1.0": [ + { + "name": "json_serialization", + "version": "0.1.0" + } + ] + }, + "json_serialization": { + "0.1.0": [ + { + "name": "chronicles", + "version": "0.6.0" + } + ] + } + } +} diff --git a/tests/tester.nim b/tests/tester.nim index b2cd11c7..a36b33f8 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1,8 +1,10 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. +import osproc, unittest, strutils, os, sequtils, sugar, json, std/sha1, + strformat -import osproc, unittest, strutils, os, sequtils, sugar, strformat -import nimblepkg/common +from nimblepkg/common import ProcessOutput +from nimblepkg/options import parseNimbleData # TODO: Each test should start off with a clean slate. Currently installed # packages are shared between each test which causes a multitude of issues @@ -12,6 +14,8 @@ let rootDir = getCurrentDir().parentDir() let nimblePath = rootDir / "src" / addFileExt("nimble", ExeExt) let installDir = rootDir / "tests" / "nimbleDir" let buildTests = rootDir / "buildTests" +let pkgsDir = installDir / "pkgs" + const path = "../src/nimble" const stringNotFound = -1 @@ -87,6 +91,21 @@ proc hasLineStartingWith(lines: seq[string], prefix: string): bool = return true return false +proc getPackageDir*(pkgCacheDir, pkgDirPrefix: string): string = + for kind, dir in walkDir(pkgCacheDir): + if kind != pcDir or not dir.startsWith(pkgCacheDir / pkgDirPrefix): + continue + let pkgChecksumStartIndex = dir.rfind('-') + if pkgChecksumStartIndex == -1: + continue + let pkgChecksum = dir[pkgChecksumStartIndex + 1 .. ^1] + if pkgChecksum.isValidSha1Hash(): + return dir + return "" + +proc packageDirExists(pkgCacheDir, pkgDirPrefix: string): bool = + getPackageDir(pkgCacheDir, pkgDirPrefix).len > 0 + proc safeMoveFile(src, dest: string) = try: moveFile(src, dest) @@ -210,7 +229,8 @@ suite "nimscript": if lines[3].startsWith("Before PkgDir:"): check line.endsWith("tests" / "nimscript") check lines[^1].startsWith("After PkgDir:") - check lines[^1].endsWith("tests" / "nimbleDir" / "pkgs" / "nimscript-0.1.0") + let packageDir = getPackageDir(pkgsDir, "nimscript-0.1.0") + check lines[^1].endsWith(packageDir) test "before/after on build": cd "nimscript": @@ -356,7 +376,7 @@ suite "uninstall": check execNimbleYes("uninstall", "PackageA@0.2", "issue27b").exitCode == QuitSuccess - check(not dirExists(installDir / "pkgs" / "PackageA-0.2.0")) + check(not dirExists(pkgsDir / "PackageA-0.2.0")) suite "nimble dump": beforeSuite() @@ -526,6 +546,20 @@ suite "reverse dependencies": check execNimble("path", "nimboost").exitCode != QuitSuccess check execNimble("path", "nimfp").exitCode != QuitSuccess + test "old format conversion": + const oldNimbleDataFileName = + "./revdep/nimbleData/old_nimble_data.json".normalizedPath + const newNimbleDataFileName = + "./revdep/nimbleData/new_nimble_data.json".normalizedPath + + doAssert fileExists(oldNimbleDataFileName) + doAssert fileExists(newNimbleDataFileName) + + let oldNimbleData = parseNimbleData(oldNimbleDataFileName) + let newNimbleData = parseNimbleData(newNimbleDataFileName) + + doAssert oldNimbleData == newNimbleData + suite "develop feature": beforeSuite() @@ -543,7 +577,8 @@ suite "develop feature": check output.processOutput.inLines("will not be compiled") check exitCode == QuitSuccess - let path = installDir / "pkgs" / "hybrid-#head" / "hybrid.nimble-link" + let packageDir = getPackageDir(pkgsDir, "hybrid-#head") + let path = packageDir / "hybrid.nimble-link" check fileExists(path) let split = readFile(path).processOutput() check split.len == 2 @@ -557,8 +592,8 @@ suite "develop feature": check(not output.processOutput.inLines("will not be compiled")) check exitCode == QuitSuccess - let path = installDir / "pkgs" / "srcdirtest-#head" / - "srcdirtest.nimble-link" + let packageDir = getPackageDir(pkgsDir, "srcdirtest-#head") + let path = packageDir / "srcdirtest.nimble-link" check fileExists(path) let split = readFile(path).processOutput() check split.len == 2 @@ -607,7 +642,8 @@ suite "develop feature": let (_, exitCode) = execNimbleYes("install") check exitCode == QuitSuccess let (output, _) = execNimble("path", "srcdirtest") - check output.strip() == installDir / "pkgs" / "srcdirtest-1.0" + let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") + check output.strip() == packageDir suite "test command": beforeSuite() @@ -734,6 +770,10 @@ suite "Module tests": cd "..": check execCmdEx("nim c -r src/nimblepkg/download").exitCode == QuitSuccess + test "jsonhelpers": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/jsonhelpers").exitCode == QuitSuccess + suite "nimble run": beforeSuite() @@ -996,6 +1036,7 @@ suite "misc tests": "[UnusedImport]", "[Deprecated]", "[XDeclaredButNotUsed]", + "[Spacing]", ] for line in output.splitLines(): @@ -1034,16 +1075,20 @@ suite "issues": check output.contains("before test") check output.contains("after test") - # When building, any newly installed packages should be referenced via the path that they get permanently installed at. test "issue 799": + # When building, any newly installed packages should be referenced via the + # path that they get permanently installed at. + removeDir installDir cd "issue799": let (build_output, build_code) = execNimbleYes("--verbose", "build") check build_code == 0 var build_results = processOutput(build_output) build_results.keepItIf(unindent(it).startsWith("Executing")) + for build_line in build_results: if build_line.contains("issue799"): - let pkg_installed_path = "--path:" & (installDir / "pkgs" / "nimble-#head").quoteShell + let nimble_install_dir = getPackageDir(pkgsDir, "nimble-#head") + let pkg_installed_path = "--path:'" & nimble_install_dir & "'" check build_line.contains(pkg_installed_path) test "issue 793": @@ -1149,8 +1194,10 @@ suite "issues": # Install v1 and check (output, exitCode) = execNimbleYes(["install", "--verbose"]) check exitCode == QuitSuccess - check output.contains "binname-0.1.0" / "binname".addFileExt(ext) - check output.contains "binname-0.1.0" / "binname-2" + check output.contains getPackageDir(pkgsDir, "binname-0.1.0") / + "binname".addFileExt(ext) + check output.contains getPackageDir(pkgsDir, "binname-0.1.0") / + "binname-2" (output, exitCode) = execBin("binname") check exitCode == QuitSuccess @@ -1163,8 +1210,10 @@ suite "issues": # Install v2 and check var (output, exitCode) = execNimbleYes(["install", "--verbose"]) check exitCode == QuitSuccess - check output.contains "binname-0.2.0" / "binname".addFileExt(ext) - check output.contains "binname-0.2.0" / "binname-2" + check output.contains getPackageDir(pkgsDir, "binname-0.2.0") / + "binname".addFileExt(ext) + check output.contains getPackageDir(pkgsDir, "binname-0.2.0") / + "binname-2" (output, exitCode) = execBin("binname") check exitCode == QuitSuccess @@ -1336,7 +1385,7 @@ suite "issues": "https://github.com/nimble-test/packagea.git@#1f9cb289c89"] check execNimbleYes(arguments).exitCode == QuitSuccess # Verify that it was installed correctly. - check dirExists(installDir / "pkgs" / "PackageA-#1f9cb289c89") + check packageDirExists(pkgsDir, "PackageA-#1f9cb289c89") # Remove it so that it doesn't interfere with the uninstall tests. check execNimbleYes("uninstall", "packagea@#1f9cb289c89").exitCode == QuitSuccess @@ -1355,6 +1404,8 @@ suite "issues": check inLines(lines1, "The .nimble file name must match name specified inside") test "issue 113 (uninstallation problems)": + removeDir(installDir) + cd "issue113/c": check execNimbleYes("install").exitCode == QuitSuccess cd "issue113/b": From 09b1368ec1eeb52f1be546a20b4023e3233d1824 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 11 Sep 2019 13:28:29 +0300 Subject: [PATCH 04/73] Deduplicate packages returned by processDeps proc Fix `processDeps` procedure to not return a dependency multiple times when it is a common dependency of more than one package. Related to nim-lang/nimble#127 --- src/nimble.nim | 10 +++++----- src/nimblepkg/common.nim | 10 ++++++---- src/nimblepkg/packageinfo.nim | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index dc5f545c..5cc571aa 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -47,12 +47,12 @@ proc refresh(options: Options) = proc install(packages: seq[PkgTuple], options: Options, doPrompt = true): PackageDependenciesInfo -proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = +proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = ## Verifies and installs dependencies. ## ## Returns the list of PackageInfo (for paths) to pass to the compiler ## during build phase. - result = @[] + assert(not pkginfo.isMinimal, "processDeps needs pkginfo.requires") display("Verifying", "dependencies for $1@$2" % [pkginfo.name, pkginfo.specialVersion], @@ -294,7 +294,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Build before removing an existing package (if one exists). This way # if the build fails then the old package will still be installed. if pkgInfo.bin.len > 0: - let paths = result.deps.map(dep => dep.getRealDir()) + let paths = result.deps.toSeq.map(dep => dep.getRealDir()) let flags = if options.action.typ in {actionInstall, actionPath, actionUninstall, actionDevelop}: options.action.passNimFlags else: @@ -460,7 +460,7 @@ proc install(packages: seq[PkgTuple], proc build(options: Options) = var pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) - let deps = processDeps(pkginfo, options) + let deps = processDeps(pkginfo, options).toSeq let paths = deps.map(dep => dep.getRealDir()) var args = options.getCompilationFlags() buildFromDir(pkgInfo, paths, args, options) @@ -478,7 +478,7 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = var pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) - let deps = processDeps(pkginfo, options) + let deps = processDeps(pkginfo, options).toSeq if not execHook(options, options.action.typ, true): raise newException(NimbleError, "Pre-hook prevented further execution.") diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 99e0c421..ecda16e9 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -79,13 +79,15 @@ proc reportUnitTestSuccess*() = stdout.styledWrite(fgGreen, "All tests passed.") when not declared(initHashSet): - import sets - template initHashSet*[A](initialSize = 64): HashSet[A] = initSet[A](initialSize) when not declared(toHashSet): - import sets - template toHashSet*[A](keys: openArray[A]): HashSet[A] = toSet(keys) + +template add*[A](s: HashSet[A], key: A) = + s.incl(key) + +template add*[A](s: HashSet[A], other: HashSet[A]) = + s.incl(other) diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 5cfefe80..ae9a17e1 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -34,7 +34,7 @@ type packageDir*: string PackageBasicInfo* = tuple[name, version, checksum: string] - PackageDependenciesInfo* = tuple[deps: seq[PackageInfo], pkg: PackageInfo] + PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] PackageInfoAndMetaData* = tuple[pkginfo: PackageInfo, meta: MetaData] proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = From a07ea8ea589abb7d37c0e3a624c7714fb1b72bcd Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 1 Sep 2019 21:29:47 +0300 Subject: [PATCH 05/73] Fix cd template to restore the previous dir The `cd` template from `tools.nim` did not restore the previous directory in the case when return from the function is made in the body. The defer statement is used to fix this. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 2 +- src/nimblepkg/publish.nim | 2 +- src/nimblepkg/tools.nim | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 7819c3b4..d2728b5a 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -59,8 +59,8 @@ proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "", doCmd("hg clone " & tipArg & branchArg & url & " " & downloadDir) proc getTagsList(dir: string, meth: DownloadMethod): seq[string] = + var output: string cd dir: - var output = execProcess("git tag") case meth of DownloadMethod.git: output = execProcess("git tag") diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index eb4395cd..0f5906a8 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -240,4 +240,4 @@ proc publish*(p: PackageInfo, o: Options) = display("Pushing", "to remote of fork.", priority = HighPriority) doCmd("git push https://" & auth.token & "@github.com/" & auth.user & "/packages " & branchName) let prUrl = createPullRequest(auth, p.name, branchName) - display("Success:", "Pull request successful, check at " & prUrl , Success, HighPriority) + display("Success:", "Pull request successful, check at " & prUrl , Success, HighPriority) diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 9febfa99..3e2e2e85 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -60,8 +60,9 @@ template cd*(dir: string, body: untyped) = ## previous working dir. let lastDir = getCurrentDir() setCurrentDir(dir) - body - setCurrentDir(lastDir) + block: + defer: setCurrentDir(lastDir) + body proc getNimrodVersion*(options: Options): Version = let vOutput = doCmdEx(getNimBin(options).quoteShell & " -v").output From 43bdb1a22e939eeae625b3905d52b1e8a94cbd3e Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 11 Sep 2019 18:08:01 +0300 Subject: [PATCH 06/73] Implement saving and loading of a Nimble lock file - Implemented writing of a lock file `nimble.lockfile` on `nimble lock` command executed in some nimble project directory like JSON with the following structure: * version - Version of the lock file format. Currently at 0.1.0. * packages - Compound JSON object of locked to specific version project dependencies. * - The name of the dependency package. * version - The version of the nimble package as in project's `nimble` file. Currently it is used only for human readable reference. * vcsRevision - The VCS revision to which the dependency is locked (currently Git or Mercurial SHA1 commit id). Used to download exactly the same version of the package. * url - URL to the repository from where the package should be downloaded. * downloadMethod - Indicates the download method which should be used to download the repository. Currently only the traditional for Nimble - Git and Mercurial download methods are supported. * dependencies - JSON array of package names to which the package depends. Those packages also must be in the lock file. Their names are used for writing the reverse dependencies of the package in the local Nimble cache's `nimbledata.json` file. * checksum - Compound JSON object which contains at least one type of package content checksum. Currently only SHA1 is supported. * sha1 - SHA1 checksum of the package content. Used to validate that the downloaded package has exactly the same content as that described in the lock file. The checksum is manually computed (it is not being queries from Git or Mercurial) in order to be independent from the download method. - If a lock file `nimble.lockfile` exists, then on performing of all Nimble commands which require searching for dependencies and downloading them in the case they are missing (like `build`, `install`, `develop`), it is read and its content is used to download exactly the same version of the project dependencies by using the URL, download method and VCS revision written in it. The checksum of the downloaded package is compared against the one written in the lock file. In the case the two checksums are not equal then it will be printed error message and the operation will be aborted. Reverse dependencies are added for installed locked dependencies just like for any other package being locally installed. - Important implementation details: * Cloning of a specific Git commit described in the lock file uses a method (described here: https://stackoverflow.com/a/3489576/853791) requiring at least Git version 2.5 from year 2015 and enabled on server side with the configuration variable `uploadpack.allowReachableSHA1InWant`. Currently the feature is supported by both GitHub and BitBucket. Because this feature is old enough and already supported by the most popular public repository hosting services, fall back method is not provided. See `cloneSpecificRevision` procedure in `download.nim` file for more details. - Other changes: * `getVcsRevisionFromDir` procedure is fixed to be able to work with Git and Mercurial repository subdirectories. Previously the top level repository directory where are `.git` and `.hg` subdirectories had been required. * A new file `packageinfotypes.nim` is added and all related to package info operations types from `packageinfo.nim` and `common.nim` files are moved to it. * Enumeration field names with string values are used to index nimble meta data JSON object fields in order to minimize the risk of error by misspelling. * Additional files are added to `.gitignore`. Related to nim-lang/nimble#127 --- .gitignore | 33 +++-- src/nimble.nim | 188 ++++++++++++++++++++--------- src/nimblepkg/checksum.nim | 18 ++- src/nimblepkg/common.nim | 41 ++----- src/nimblepkg/download.nim | 44 +++++-- src/nimblepkg/options.nim | 16 ++- src/nimblepkg/packageinfo.nim | 164 ++++++++++++++++--------- src/nimblepkg/packageinfotypes.nim | 78 ++++++++++++ src/nimblepkg/packageinstaller.nim | 14 +-- src/nimblepkg/packageparser.nim | 3 +- src/nimblepkg/publish.nim | 1 + src/nimblepkg/reversedeps.nim | 12 +- src/nimblepkg/tools.nim | 23 +++- src/nimblepkg/version.nim | 3 +- tests/.gitignore | 24 ++++ tests/tester.nim | 16 ++- 16 files changed, 485 insertions(+), 193 deletions(-) create mode 100644 src/nimblepkg/packageinfotypes.nim diff --git a/.gitignore b/.gitignore index 4661d605..abe509dd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,29 @@ nimcache/ /src/babel /src/nimble +# executables from test and build +/nimble +src/nimblepkg/checksum +src/nimblepkg/cli +src/nimblepkg/common +src/nimblepkg/config +src/nimblepkg/download +src/nimblepkg/init +src/nimblepkg/jsonhelpers +src/nimblepkg/lockfile +src/nimblepkg/nimscriptapi +src/nimblepkg/nimscriptexecutor +src/nimblepkg/nimscriptwrapper +src/nimblepkg/options +src/nimblepkg/packageinfo +src/nimblepkg/packageinfotypes +src/nimblepkg/packageinstaller +src/nimblepkg/packageparser +src/nimblepkg/publish +src/nimblepkg/reversedeps +src/nimblepkg/tools +src/nimblepkg/version + # Windows executables *.exe *.dll @@ -28,13 +51,5 @@ nimcache/ *.orig # Test procedure artifacts +*.nims /buildTests - -# executables from test and build (already gitignored but keeping for documentation) -# /nimble -# src/nimblepkg/cli -# src/nimblepkg/packageinfo -# src/nimblepkg/packageparser -# src/nimblepkg/reversedeps -# src/nimblepkg/version -# src/nimblepkg/download diff --git a/src/nimble.nim b/src/nimble.nim index 5cc571aa..78b432d4 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -11,11 +11,12 @@ from unicode import toLower from sequtils import toSeq from strformat import fmt -import nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, - nimblepkg/download, nimblepkg/config, nimblepkg/common, +import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, + nimblepkg/tools, nimblepkg/download, nimblepkg/config, nimblepkg/common, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, - nimblepkg/nimscriptexecutor, nimblepkg/init + nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, + nimblepkg/checksum import nimblepkg/nimscriptwrapper @@ -46,7 +47,8 @@ proc refresh(options: Options) = proc install(packages: seq[PkgTuple], options: Options, - doPrompt = true): PackageDependenciesInfo + doPrompt, first, fromLockFile: bool): PackageDependenciesInfo + proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = ## Verifies and installs dependencies. ## @@ -82,8 +84,11 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = if not found: display("Installing", $resolvedDep, priority = HighPriority) - let toInstall = @[(resolvedDep.name, resolvedDep.ver)] - let (pkgs, installedPkg) = install(toInstall, options) + let toInstall = @[(resolvedDep.name, resolvedDep.ver, "")] + let (pkgs, installedPkg) = install(toInstall, options, + doPrompt = false, + first = false, + fromLockFile = false) result.add(pkgs) pkg = installedPkg # For addRevDep @@ -197,14 +202,14 @@ proc buildFromDir( proc removePkgDir(dir: string, options: Options) = ## Removes files belonging to the package in ``dir``. try: - var nimblemeta = parseFile(dir / "nimblemeta.json") - if not nimblemeta.hasKey("files"): + var nimblemeta = parseFile(dir / packageMetaDataFileName) + if not nimblemeta.hasKey($pmdjkFiles): raise newException(JsonParsingError, "Meta data does not contain required info.") - for file in nimblemeta["files"]: + for file in nimblemeta[$pmdjkFiles]: removeFile(dir / file.str) - removeFile(dir / "nimblemeta.json") + removeFile(dir / packageMetaDataFileName) # If there are no files left in the directory, remove the directory. if toSeq(walkDirRec(dir)).len == 0: @@ -213,9 +218,9 @@ proc removePkgDir(dir: string, options: Options) = display("Warning:", ("Cannot completely remove $1. Files not installed " & "by nimble are present.") % dir, Warning, HighPriority) - if nimblemeta.hasKey("binaries"): + if nimblemeta.hasKey($pmdjkBinaries): # Remove binaries. - for binary in nimblemeta["binaries"]: + for binary in nimblemeta[$pmdjkBinaries]: removeFile(options.getBinDir() / binary.str) # Search for an older version of the package we are removing. @@ -223,7 +228,7 @@ proc removePkgDir(dir: string, options: Options) = let (pkgName, _, _) = getNameVersionChecksum(dir) let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) var pkgInfo: PackageInfo - if pkgList.findPkg((pkgName, newVRAny()), pkgInfo): + if pkgList.findPkg((pkgName, newVRAny(), ""), pkgInfo): pkgInfo = pkgInfo.toFullInfo(options) for bin, src in pkgInfo.bin: let symlinkDest = pkgInfo.getOutputDir(bin) @@ -241,29 +246,28 @@ proc removePkgDir(dir: string, options: Options) = raise NimbleQuit(msg: "") removeDir(dir) -proc vcsRevisionInDir(dir: string): string = - ## Returns current revision number of HEAD if dir is inside VCS, or nil in - ## case of failure. - var cmd = "" - if dirExists(dir / ".git"): - cmd = "git -C " & quoteShell(dir) & " rev-parse HEAD" - elif dirExists(dir / ".hg"): - cmd = "hg --cwd " & quoteShell(dir) & " id -i" +proc processLockedDependencies(packageInfo: PackageInfo, options: Options): + seq[PackageInfo] - if cmd.len > 0: - try: - let res = doCmdEx(cmd) - if res.exitCode == 0: - result = string(res.output).strip() - except: - discard +proc processAllDependencies(pkgInfo: PackageInfo, options: Options): + seq[PackageInfo] = + if pkgInfo.lockedDependencies.len > 0: + pkgInfo.processLockedDependencies(options) + else: + pkgInfo.processDeps(options).toSeq proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, - url: string): PackageDependenciesInfo = + url: string, first: bool, fromLockFile: bool): + PackageDependenciesInfo = ## Returns where package has been installed to, together with paths ## to the packages this package depends on. ## The return value of this function is used by ## ``processDeps`` to gather a list of paths to pass to the nim compiler. + ## + ## ``first`` + ## True if this is the first level of the indirect recursion. + ## ``fromLockFile`` + ## True if we are installing dependencies from the lock file. # Handle pre-`install` hook. if not options.depsOnly: @@ -282,7 +286,10 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, pkgInfo.specialVersion = $requestedVer.spe # Dependencies need to be processed before the creation of the pkg dir. - result.deps = processDeps(pkgInfo, depsOptions) + if first and pkgInfo.lockedDependencies.len > 0: + result.deps = processLockedDependencies(pkgInfo, depsOptions).toHashSet + elif not fromLockFile: + result.deps = processDeps(pkgInfo, depsOptions) if options.depsOnly: result.pkg = pkgInfo @@ -304,7 +311,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Don't copy artifacts if project local deps mode and "installing" the top level package if not (options.localdeps and options.isInstallingTopLevel(dir)): let pkgDestDir = pkgInfo.getPkgDest(options) - if dirExists(pkgDestDir) and fileExists(pkgDestDir / "nimblemeta.json"): + if fileExists(pkgDestDir / packageMetaDataFileName): let msg = "$1@$2 already exists. Overwrite?" % [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): @@ -367,7 +374,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, for filename in setupBinSymlink(symlinkDest, symlinkFilename, options): binariesInstalled.incl(filename) - let vcsRevision = vcsRevisionInDir(realDir) + let vcsRevision = getVcsRevisionFromDir(dir) # Save a nimblemeta.json file. saveNimbleMeta(pkgDestDir, url, vcsRevision, filesInstalled, @@ -397,6 +404,54 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, cd pkgInfo.myPath.splitFile.dir: discard execHook(options, actionInstall, false) +proc processLockedDependencies(packageInfo: PackageInfo, options: Options): + seq[PackageInfo] = + ## ``` + ## For each dependency in the lock file: + ## Check whether it is already installed and if not: + ## Download it at specific VCS revision. + ## Check whether it has the right checksum and if so: + ## Install it from the download dir. + ## Add record in the reverse dependencies file. + ## Convert its info to PackageInfo and add it to the result. + ## ``` + + let packagesDir = options.getPkgsDir() + + for name, dep in packageInfo.lockedDependencies: + let depDirName = packagesDir / fmt"{name}-{dep.version}-{dep.checksum.sha1}" + + if not depDirName.dirExists: + let (url, metadata) = getUrlData(dep.url) + let version = dep.version.parseVersionRange + let subdir = metadata.getOrDefault("subdir") + + let (downloadDir, _) = downloadPkg( + url, version, dep.downloadMethod.getDownloadMethod, subdir, options, + downloadPath = "", dep.vcsRevision) + + let downloadedPackageChecksum = calculatePackageSha1Checksum(downloadDir) + if downloadedPackageChecksum != dep.checksum.sha1: + raiseChecksumError(name, dep.version, dep.vcsRevision, + downloadedPackageChecksum, dep.checksum.sha1) + + let (_, newlyInstalledPackageInfo) = installFromDir( + downloadDir, version, options, url, first = false, fromLockFile = true) + + for depDepName in dep.dependencies: + let depDep = packageInfo.lockedDependencies[depDepName] + let revDep = (name: depDepName, version: depDep.version, + checksum: depDep.checksum.sha1) + options.nimbleData.addRevDep(revDep, newlyInstalledPackageInfo) + + result.add newlyInstalledPackageInfo + + else: + let nimbleFilePath = findNimbleFile(depDirName, false) + let packageInfo = getInstalledPackageMin( + depDirName, nimbleFilePath).toFullInfo(options) + result.add packageInfo + proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string, Table[string, string]) = @@ -425,32 +480,43 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options, proc install(packages: seq[PkgTuple], options: Options, - doPrompt = true): PackageDependenciesInfo = + doPrompt, first, fromLockFile: bool): PackageDependenciesInfo = + ## ``first`` + ## True if this is the first level of the indirect recursion. + ## ``fromLockFile`` + ## True if we are installing dependencies from the lock file. + if packages == @[]: - result = installFromDir(getCurrentDir(), newVRAny(), options, "") + result = installFromDir(getCurrentDir(), newVRAny(), options, "", first, + fromLockFile) else: # Install each package. for pv in packages: let (meth, url, metadata) = getDownloadInfo(pv, options, doPrompt) let subdir = metadata.getOrDefault("subdir") let (downloadDir, downloadVersion) = - downloadPkg(url, pv.ver, meth, subdir, options) + downloadPkg(url, pv.ver, meth, subdir, options, downloadPath = "", + vcsRevision = "") try: - result = installFromDir(downloadDir, pv.ver, options, url) + result = installFromDir(downloadDir, pv.ver, options, url, first, + fromLockFile) except BuildFailed: # The package failed to build. # Check if we tried building a tagged version of the package. let headVer = getHeadName(meth) - if pv.ver.kind != verSpecial and downloadVersion != headVer: - # If we tried building a tagged version of the package then - # ask the user whether they want to try building #head. + if pv.ver.kind != verSpecial and downloadVersion != headVer and + not fromLockFile: + # If we tried building a tagged version of the package and this is not + # fixed in the lock file version then ask the user whether they want + # to try building #head. let promptResult = doPrompt and options.prompt(("Build failed for '$1@$2', would you" & " like to try installing '$1@#head' (latest unstable)?") % [pv.name, $downloadVersion]) if promptResult: - let toInstall = @[(pv.name, headVer.toVersionRange())] - result = install(toInstall, options, doPrompt) + let toInstall = @[(pv.name, headVer.toVersionRange(), "")] + result = install(toInstall, options, doPrompt, first, + fromLockFile = false) else: raise newException(BuildFailed, "Aborting installation due to build failure") @@ -458,9 +524,10 @@ proc install(packages: seq[PkgTuple], raise proc build(options: Options) = - var pkgInfo = getPkgInfo(getCurrentDir(), options) + let dir = getCurrentDir() + let pkgInfo = getPkgInfo(dir, options) nimScriptHint(pkgInfo) - let deps = processDeps(pkginfo, options).toSeq + let deps = pkgInfo.processAllDependencies(options) let paths = deps.map(dep => dep.getRealDir()) var args = options.getCompilationFlags() buildFromDir(pkgInfo, paths, args, options) @@ -476,10 +543,10 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = raise newException(NimbleError, "Specified file, " & bin & " or " & binDotNim & ", does not exist.") - var pkgInfo = getPkgInfo(getCurrentDir(), options) + let dir = getCurrentDir() + let pkgInfo = getPkgInfo(dir, options) nimScriptHint(pkgInfo) - let deps = processDeps(pkginfo, options).toSeq - + let deps = pkgInfo.processAllDependencies(options) if not execHook(options, options.action.typ, true): raise newException(NimbleError, "Pre-hook prevented further execution.") @@ -592,7 +659,7 @@ proc listPaths(options: Options) = var errors = 0 let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) - for name, version in options.action.packages.items: + for name, version, _ in options.action.packages.items: var installed: seq[VersionAndPath] = @[] # There may be several, list all available ones and sort by version. for x in pkgs.items(): @@ -663,7 +730,7 @@ proc dump(options: Options) = # jsonutils.toJson would work but is only available since 1.3.5, so we # do it manually. j[key] = newJArray() - for (name, ver) in val: + for (name, ver, _) in val: j[key].add %{ "name": % name, # we serialize both: `ver` may be more convenient for tooling @@ -954,17 +1021,17 @@ proc developFromDir(dir: string, options: Options) = optsCopy.startDir = dir optsCopy.nim = options.nim cd dir: - discard processDeps(pkgInfo, optsCopy) + discard pkgInfo.processAllDependencies(optsCopy) else: # Dependencies need to be processed before the creation of the pkg dir. - discard processDeps(pkgInfo, options) + discard pkgInfo.processAllDependencies(options) # Don't link if project local deps mode and "developing" the top level package if not (options.localdeps and options.isInstallingTopLevel(dir)): # This is similar to the code in `installFromDir`, except that we # *consciously* not worry about the package's binaries. let pkgDestDir = pkgInfo.getPkgDest(options) - if dirExists(pkgDestDir) and fileExists(pkgDestDir / "nimblemeta.json"): + if fileExists(pkgDestDir / packageMetaDataFileName): let msg = "$1@$2 already exists. Overwrite?" % [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): @@ -986,7 +1053,7 @@ proc developFromDir(dir: string, options: Options) = writeNimbleLink(nimbleLinkPath, nimbleLink) # Save a nimblemeta.json file. - saveNimbleMeta(pkgDestDir, dir, vcsRevisionInDir(dir), nimbleLinkPath) + saveNimbleMeta(pkgDestDir, dir, getVcsRevisionFromDir(dir), nimbleLinkPath) # Save the nimble data (which might now contain reverse deps added in # processDeps). @@ -1029,7 +1096,8 @@ proc develop(options: Options) = pv.ver var options = options options.forceFullClone = true - discard downloadPkg(url, ver, meth, subdir, options, downloadDir) + discard downloadPkg(url, ver, meth, subdir, options, downloadDir, + vcsRevision = "") developFromDir(downloadDir / subdir, options) proc test(options: Options) = @@ -1113,6 +1181,13 @@ proc check(options: Options) = display("Failure:", "Validation failed", Error, HighPriority) quit(QuitFailure) +proc lock(options: Options) = + let currentDir = getCurrentDir() + let packageInfo = getPkgInfo(currentDir, options) + let dependencies = processDeps(packageInfo, options).toSeq.map( + pkg => pkg.toFullInfo(options)) + writeLockFile(dependencies, options) + proc run(options: Options) = # Verify parameters. var pkgInfo = getPkgInfo(getCurrentDir(), options) @@ -1152,7 +1227,10 @@ proc doAction(options: var Options) = of actionRefresh: refresh(options) of actionInstall: - let (_, pkgInfo) = install(options.action.packages, options) + let (_, pkgInfo) = install(options.action.packages, options, + doPrompt = true, + first = true, + fromLockFile = false) if options.action.packages.len == 0: nimScriptHint(pkgInfo) if pkgInfo.foreignDeps.len > 0: @@ -1191,6 +1269,8 @@ proc doAction(options: var Options) = develop(options) of actionCheck: check(options) + of actionLock: + lock(options) of actionNil: assert false of actionCustom: diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index 9260414b..cdcf4988 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -1,8 +1,22 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, strutils, std/sha1, algorithm -import tools +import os, strutils, std/sha1, algorithm, strformat +import version, tools + +type + ChecksumError* = object of NimbleError + +proc raiseChecksumError*(name, version, vcsRevision, + checksum, expectedChecksum: string) = + var error = newException(ChecksumError, +fmt""" +Downloaded package checksum does not correspond to that in the lock file: + Package: {name}@v{version}@r{vcsRevision} + Checksum: {checksum} + Expected checksum: {expectedChecksum} +""") + raise error proc extractFileList(consoleOutput: string): seq[string] = result = consoleOutput.splitLines() diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index ecda16e9..ed53c4e4 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -4,43 +4,12 @@ # Various miscellaneous common types reside here, to avoid problems with # recursive imports -import sets, tables, terminal +import sets, terminal import version type BuildFailed* = object of NimbleError - PackageInfo* = object - myPath*: string ## The path of this .nimble file - isNimScript*: bool ## Determines if this pkg info was read from a nims file - isMinimal*: bool - isInstalled*: bool ## Determines if the pkg this info belongs to is installed - isLinked*: bool ## Determines if the pkg this info belongs to has been linked via `develop` - nimbleTasks*: HashSet[string] ## All tasks defined in the Nimble file - postHooks*: HashSet[string] ## Useful to know so that Nimble doesn't execHook unnecessarily - preHooks*: HashSet[string] - name*: string - ## The version specified in the .nimble file.Assuming info is non-minimal, - ## it will always be a non-special version such as '0.1.4' - version*: string - specialVersion*: string ## Either `myVersion` or a special version such as #head. - 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[PkgTuple] - bin*: Table[string, string] - binDir*: string - srcDir*: string - backend*: string - foreignDeps*: seq[string] - checksum*: string - ## Same as quit(QuitSuccess), but allows cleanup. NimbleQuit* = ref object of CatchableError @@ -53,9 +22,17 @@ type ndjkRevDepVersion = "version" ndjkRevDepChecksum = "checksum" + PackageMetaDataJsonKeys* = enum + pmdjkUrl = "url" + pmdjkVcsRevision = "vcsRevision" + pmdjkFiles = "files" + pmdjkBinaries = "binaries" + pmdjkIsLink = "isLink" + const nimbleVersion* = "0.13.1" nimbleDataFile* = (name: "nimbledata.json", version: "0.1.0") + packageMetaDataFileName* = "nimblemeta.json" proc raiseNimbleError*(msg: string, hint = "") = var exc = newException(NimbleError, msg) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index d2728b5a..132449e9 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -1,8 +1,8 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import parseutils, os, osproc, strutils, tables, pegs, uri -import packageinfo, packageparser, version, tools, common, options, cli +import parseutils, os, osproc, strutils, tables, pegs, uri, strformat +import packageinfotypes, packageparser, version, tools, common, options, cli from algorithm import SortOrder, sorted from sequtils import toSeq, filterIt, map @@ -156,9 +156,24 @@ proc getUrlData*(url: string): (string, Table[string, string]) = proc isURL*(name: string): bool = name.startsWith(peg" @'://' ") +proc cloneSpecificRevision(downloadMethod: DownloadMethod, + url, downloadDir, vcsRevision: string) = + assert vcsRevision.len > 0 + display("Cloning", "revision: " & vcsRevision, priority = MediumPriority) + case downloadMethod + of DownloadMethod.git: + createDir(downloadDir) + cd downloadDir: + doCmd("git init") + doCmd(fmt"git remote add origin {url}") + doCmd(fmt"git fetch --depth 1 origin {vcsRevision}") + doCmd("git reset --hard FETCH_HEAD") + of DownloadMethod.hg: + doCmd(fmt"hg clone {url} -r {vcsRevision}") + proc doDownload(url: string, downloadDir: string, verRange: VersionRange, - downMethod: DownloadMethod, - options: Options): Version = + downMethod: DownloadMethod, options: Options, + vcsRevision: string): Version = ## Downloads the repository specified by ``url`` using the specified download ## method. ## @@ -177,7 +192,9 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, result = latest.ver removeDir(downloadDir) - if verRange.kind == verSpecial: + if vcsRevision.len > 0: + cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) + elif verRange.kind == verSpecial: # We want a specific commit/branch/tag here. if verRange.spe == getHeadName(downMethod): # Grab HEAD. @@ -223,10 +240,10 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, priority = HighPriority) proc downloadPkg*(url: string, verRange: VersionRange, - downMethod: DownloadMethod, - subdir: string, - options: Options, - downloadPath = ""): (string, Version) = + downMethod: DownloadMethod, + subdir: string, + options: Options, + downloadPath, vcsRevision: string): (string, Version) = ## Downloads the repository as specified by ``url`` and ``verRange`` using ## the download method specified. ## @@ -234,9 +251,14 @@ proc downloadPkg*(url: string, verRange: VersionRange, ## ## Returns the directory where it was downloaded (subdir is appended) and ## the concrete version which was downloaded. + ## + ## ``vcsRevision`` + ## If specified this parameter will cause specific VCS revision to be + ## checked out. + let downloadDir = if downloadPath == "": - (getNimbleTempDir() / getDownloadDirName(url, verRange)) + (getNimbleTempDir() / getDownloadDirName(url, verRange, vcsRevision)) else: downloadPath @@ -261,7 +283,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, priority = HighPriority) result = ( downloadDir / subdir, - doDownload(modUrl, downloadDir, verRange, downMethod, options) + doDownload(modUrl, downloadDir, verRange, downMethod, options, vcsRevision) ) if verRange.kind != verSpecial: diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index cc3e84be..708a91a8 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -6,7 +6,7 @@ import sequtils, sugar import std/options as std_opt from httpclient import Proxy, newProxy -import config, version, common, cli +import config, version, common, cli, packageinfotypes const nimbledeps* = "nimbledeps" @@ -47,11 +47,12 @@ type actionInstall, actionSearch, actionList, actionBuild, actionPath, actionUninstall, actionCompile, actionDoc, actionCustom, actionTasks, actionDevelop, actionCheck, - actionRun + actionLock, actionRun Action* = object case typ*: ActionType - of actionNil, actionList, actionPublish, actionTasks, actionCheck: nil + of actionNil, actionList, actionPublish, actionTasks, actionCheck, + actionLock: nil of actionRefresh: optionalURL*: string # Overrides default package list. of actionInstall, actionPath, actionUninstall, actionDevelop: @@ -130,6 +131,7 @@ Commands: .nimble file, a project directory or the name of an installed package. [--ini, --json] Selects the output format (the default is --ini). + lock Generates or updates a package lock file. Nimble Options: -h, --help Print this help message. @@ -203,6 +205,8 @@ proc parseActionType*(action: string): ActionType = result = actionDevelop of "check": result = actionCheck + of "lock": + result = actionLock else: result = actionCustom @@ -235,7 +239,7 @@ proc initAction*(options: var Options, key: string) = options.action.arguments = @[] options.action.custCompileFlags = @[] options.action.custRunFlags = @[] - of actionPublish, actionList, actionTasks, actionCheck, actionRun, + of actionPublish, actionList, actionTasks, actionCheck, actionRun, actionLock, actionNil: discard proc prompt*(options: Options, question: string): bool = @@ -374,9 +378,9 @@ proc parseArgument*(key: string, result: var Options) = let (pkgName, pkgVer) = (key[0 .. i-1], key[i+1 .. key.len-1]) if pkgVer.len == 0: raise newException(NimbleError, "Version range expected after '@'.") - result.action.packages.add((pkgName, pkgVer.parseVersionRange())) + result.action.packages.add((pkgName, pkgVer.parseVersionRange(), "")) else: - result.action.packages.add((key, VersionRange(kind: verAny))) + result.action.packages.add((key, VersionRange(kind: verAny), "")) of actionRefresh: result.action.optionalURL = key of actionSearch: diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index ae9a17e1..3d6262d8 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -3,39 +3,67 @@ # Stdlib imports import system except TResult -import hashes, json, strutils, os, sets, tables, httpclient +import hashes, json, strutils, os, sets, tables, httpclient, sequtils, sugar from net import SslError from compiler/nimblecmd import getPathVersionChecksum # Local imports -import version, tools, common, options, cli, config, checksum +import version, tools, common, options, cli, config, checksum, + packageinfotypes type - Package* = object ## Definition of package from packages.json. - # 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 empty string if not available. - version*: string - dvcsTag*: string - web*: string # Info url for humans. - alias*: string ## A name of another package, that this package aliases. - - MetaData* = object - url*: string - - NimbleLink* = object - nimbleFilePath*: string - packageDir*: string - - PackageBasicInfo* = tuple[name, version, checksum: string] - PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] - PackageInfoAndMetaData* = tuple[pkginfo: PackageInfo, meta: MetaData] + LockFileJsonKeys = enum + lfjkVersion = "version" + lfjkPackages = "packages" + lfjkPackageVersion = "version" + lfjkPackageVcsRevision = "vcsRevision" + lfjkPackageUrl = "url" + lfjkPackageDownloadMethod = "downloadMethod" + lfjkPackageDependencies = "dependencies" + lfjkPackageChecksum = "checksum" + lfjkPackageChecksumSha1 = "sha1" + +const + lockFile = (name: "nimble.lockfile", version: "0.1.0") + +proc lockFileExists*(dir: string): bool = + fileExists(dir / lockFile.name) + +proc getPackage*(name: string, options: Options): Package + +proc writeLockFile*(packages: seq[PackageInfo], options: Options) = + let packagesJsonNode = newJObject() + for pkgInfo in packages: + let pkg = getPackage(pkgInfo.name, options) + let packageJsonNode = %{ + $lfjkPackageVersion: %pkgInfo.version, + $lfjkPackageVcsRevision: %pkgInfo.vcsRevision, + $lfjkPackageUrl: %pkg.url, + $lfjkPackageDownloadMethod: %pkg.downloadMethod, + $lfjkPackageDependencies: %pkgInfo.requires.map( + pkg => pkg.name).filter(name => name != "nim"), + $lfjkPackageChecksum: %{ + $lfjkPackageChecksumSha1: %pkgInfo.checksum + } + } + packagesJsonNode.add(pkgInfo.name, packageJsonNode) + + let mainJsonNode = %{ + $lfjkVersion: %lockFile.version, + $lfjkPackages: packagesJsonNode + } + + writeFile(lockFile.name, mainJsonNode.pretty) + +proc readLockFile*(dir: string): LockFileDependencies = + let lockFilePath = dir / lockFile.name + let json = parseFile(lockFilePath) + result = json[$lfjkPackages].to(result.typeof) + +proc getLockedDependencies*(dir: string): LockFileDependencies = + if lockFileExists(dir): + result = readLockFile(dir) proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = ## Splits ``pkgpath`` in the format @@ -48,6 +76,28 @@ proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = return getNameVersionChecksum(pkgPath.splitPath.head) getPathVersionChecksum(pkgpath.splitPath.tail) +proc readMetaData*(path: string, silent = false): MetaData = + ## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json`` + var bmeta = path / packageMetaDataFileName + if not fileExists(bmeta) and not silent: + result.url = "" + display("Warning:", "No nimblemeta.json file found in " & path, + Warning, HighPriority) + return + # TODO: Make this an error. + let cont = readFile(bmeta) + let jsonmeta = parseJson(cont) + result.url = jsonmeta[$pmdjkUrl].str + result.vcsRevision = jsonmeta[$pmdjkVcsRevision].str + +proc getVcsRevision(dir: string): string = + # If the directory is under version control get the revision from it. + result = getVcsRevisionFromDir(dir) + if result.len > 0: return + # Otherwise this probably is directory in the local cache and we try to get it + # from the nimble package meta data json file. + result = readMetaData(dir, true).vcsRevision + proc initPackageInfo*(filePath: string): PackageInfo = let (fileDir, fileName, _) = filePath.splitFile let (_, pkgVersion, pkgChecksum) = filePath.getNameVersionChecksum @@ -75,9 +125,11 @@ proc initPackageInfo*(filePath: string): PackageInfo = result.srcDir = "" result.binDir = "" result.backend = "c" + result.lockedDependencies = getLockedDependencies(fileDir) result.checksum = if pkgChecksum.len > 0: pkgChecksum else: calculatePackageSha1Checksum(fileDir) + result.vcsRevision = getVcsRevision(fileDir) proc toValidPackageName*(name: string): string = result = "" @@ -131,19 +183,6 @@ proc fromJson(obj: JSonNode): Package = result.description = obj.requiredField("description") result.web = obj.optionalField("web") -proc readMetaData*(path: string): MetaData = - ## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json`` - var bmeta = path / "nimblemeta.json" - if not fileExists(bmeta): - result.url = "" - display("Warning:", "No nimblemeta.json file found in " & path, - Warning, HighPriority) - return - # TODO: Make this an error. - let cont = readFile(bmeta) - let jsonmeta = parseJson(cont) - result.url = jsonmeta["url"].str - proc readNimbleLink*(nimbleLinkPath: string): NimbleLink = let s = readFile(nimbleLinkPath).splitLines() result.nimbleFilePath = s[0] @@ -287,6 +326,12 @@ proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool = resPkg = resolveAlias(resPkg, options) return true +proc getPackage*(name: string, options: Options): Package = + let success = getPackage(name, options, result) + if not success: + raise newException(NimbleError, + "Cannot find package with name '" & name & "'.") + proc getPackageList*(options: Options): seq[Package] = ## Returns the list of packages found in the downloaded packages.json files. result = @[] @@ -331,6 +376,27 @@ proc findNimbleFile*(dir: string; error: bool): string = display("Warning:", msg, Warning, HighPriority) display("Hint:", hintMsg, Warning, HighPriority) +proc getInstalledPackageMin*(packageDir, nimbleFilePath: string): PackageInfo = + let (name, version, checksum) = getNameVersionChecksum(packageDir) + result = initPackageInfo(nimbleFilePath) + result.name = name + result.version = version + result.specialVersion = version + result.checksum = checksum + result.isMinimal = true + result.isInstalled = true + let nimbleFileDir = nimbleFilePath.splitFile().dir + result.isLinked = cmpPaths(nimbleFileDir, packageDir) != 0 + + # Read the package's 'srcDir' (this is stored in the .nimble-link so + # we can easily grab it) + if result.isLinked: + let nimbleLinkPath = packageDir / name.addFileExt("nimble-link") + let realSrcPath = readNimbleLink(nimbleLinkPath).packageDir + assert realSrcPath.startsWith(nimbleFileDir) + result.srcDir = realSrcPath.replace(nimbleFileDir) + result.srcDir.removePrefix(DirSep) + proc getInstalledPkgsMin*(libsDir: string, options: Options): seq[PackageInfoAndMetaData] = ## Gets a list of installed packages. The resulting package info is @@ -344,25 +410,7 @@ proc getInstalledPkgsMin*(libsDir: string, options: Options): let nimbleFile = findNimbleFile(path, false) if nimbleFile != "": let meta = readMetaData(path) - let (name, version, checksum) = getNameVersionChecksum(path) - var pkg = initPackageInfo(nimbleFile) - pkg.name = name - pkg.version = version - pkg.specialVersion = version - pkg.checksum = checksum - pkg.isMinimal = true - pkg.isInstalled = true - let nimbleFileDir = nimbleFile.splitFile().dir - pkg.isLinked = cmpPaths(nimbleFileDir, path) != 0 - - # Read the package's 'srcDir' (this is stored in the .nimble-link so - # we can easily grab it) - if pkg.isLinked: - let nimbleLinkPath = path / name.addFileExt("nimble-link") - let realSrcPath = readNimbleLink(nimbleLinkPath).packageDir - assert realSrcPath.startsWith(nimbleFileDir) - pkg.srcDir = realSrcPath.replace(nimbleFileDir) - pkg.srcDir.removePrefix(DirSep) + let pkg = getInstalledPackageMin(path, nimbleFile) result.add((pkg, meta)) proc withinRange*(pkgInfo: PackageInfo, verRange: VersionRange): bool = diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim new file mode 100644 index 00000000..87b2e569 --- /dev/null +++ b/src/nimblepkg/packageinfotypes.nim @@ -0,0 +1,78 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import sets, tables +import version + +type + Checksums* = object + sha1*: string + + LockFileDependency* = object + version*: string + vcsRevision*: string + url*: string + downloadMethod*: string + dependencies*: seq[string] + checksum*: Checksums + + LockFileDependencies* = Table[string, LockFileDependency] + + PackageInfo* = object + myPath*: string ## The path of this .nimble file + isNimScript*: bool ## Determines if this pkg info was read from a nims file + isMinimal*: bool + isInstalled*: bool ## Determines if the pkg this info belongs to is installed + isLinked*: bool ## Determines if the pkg this info belongs to has been linked via `develop` + nimbleTasks*: HashSet[string] ## All tasks defined in the Nimble file + postHooks*: HashSet[string] ## Useful to know so that Nimble doesn't execHook unnecessarily + preHooks*: HashSet[string] + name*: string + ## The version specified in the .nimble file.Assuming info is non-minimal, + ## it will always be a non-special version such as '0.1.4' + version*: string + specialVersion*: string ## Either `myVersion` or a special version such as #head. + 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[PkgTuple] + bin*: Table[string, string] + binDir*: string + srcDir*: string + backend*: string + foreignDeps*: seq[string] + lockedDependencies*: LockFileDependencies + checksum*: string + vcsRevision*: string ## This is git or hg commit sha1. + + Package* = object ## Definition of package from packages.json. + # 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 empty string if not available. + version*: string + dvcsTag*: string + web*: string # Info url for humans. + alias*: string ## A name of another package, that this package aliases. + + MetaData* = object + url*: string + vcsRevision*: string + + NimbleLink* = object + nimbleFilePath*: string + packageDir*: string + + PackageBasicInfo* = tuple[name, version, checksum: string] + PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] + PackageInfoAndMetaData* = tuple[pkginfo: PackageInfo, meta: MetaData] diff --git a/src/nimblepkg/packageinstaller.nim b/src/nimblepkg/packageinstaller.nim index 424c782d..4b9634e2 100644 --- a/src/nimblepkg/packageinstaller.nim +++ b/src/nimblepkg/packageinstaller.nim @@ -3,7 +3,7 @@ import os, strutils, sets, json # Local imports -import cli, options, tools +import cli, options, tools, common when defined(windows): import version @@ -88,19 +88,19 @@ proc saveNimbleMeta*(pkgDestDir, url, vcsRevision: string, ## package. ## ## isLink - Determines whether the installed package is a .nimble-link. - var nimblemeta = %{"url": %url} + var nimblemeta = %{$pmdjkUrl: %url} if vcsRevision.len > 0: - nimblemeta["vcsRevision"] = %vcsRevision + nimblemeta[$pmdjkVcsRevision] = %vcsRevision let files = newJArray() - nimblemeta["files"] = files + nimblemeta[$pmdjkFiles] = files for file in filesInstalled: files.add(%changeRoot(pkgDestDir, "", file)) let binaries = newJArray() - nimblemeta["binaries"] = binaries + nimblemeta[$pmdjkBinaries] = binaries for bin in bins: binaries.add(%bin) - nimblemeta["isLink"] = %isLink - writeFile(pkgDestDir / "nimblemeta.json", $nimblemeta) + nimblemeta[$pmdjkIsLink] = %isLink + writeFile(pkgDestDir / packageMetaDataFileName, nimblemeta.pretty) proc saveNimbleMeta*(pkgDestDir, pkgDir, vcsRevision, nimbleLinkPath: string) = ## Overload of saveNimbleMeta for linked (.nimble-link) packages. diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 86690d3b..acbba5e4 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -3,7 +3,8 @@ import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map, toSeq -import version, tools, common, nimscriptwrapper, options, packageinfo, cli +import version, tools, nimscriptwrapper, options, cli, + packageinfo, packageinfotypes ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` ## because it depends on ``nimscriptwrapper`` (``nimscriptwrapper`` also diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index 0f5906a8..0257bcaf 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -6,6 +6,7 @@ import system except TResult import httpclient, strutils, json, os, browsers, times, uri +import version, tools, cli, config, options, packageinfotypes import version, tools, common, cli, config, options {.warning[UnusedImport]: off.} from net import SslCVerifyMode, newContext diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 05319252..5a94ac7a 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -3,7 +3,8 @@ import os, json, sets -import options, common, version, download, packageinfo, jsonhelpers +import common, options, version, download, jsonhelpers, + packageinfotypes, packageinfo proc saveNimbleData*(options: Options) = # TODO: This file should probably be locked. @@ -74,7 +75,8 @@ proc getRevDepTups*(options: Options, pkg: PackageInfo): seq[PkgTuple] = for pkg in thisPkgsDep: let pkgTup = ( name: pkg[$ndjkRevDepName].getStr(), - ver: parseVersionRange(pkg[$ndjkRevDepVersion].getStr()) + ver: parseVersionRange(pkg[$ndjkRevDepVersion].getStr()), + vcsRevision: "" ) var pkgInfo: PackageInfo if not findPkg(pkgList, pkgTup, pkgInfo): @@ -111,9 +113,9 @@ when isMainModule: isMinimal: false, name: "nimforum", specialVersion: "0.1.0", - requires: @[("jester", parseVersionRange("0.1.0")), - ("captcha", parseVersionRange("1.0.0")), - ("auth", parseVersionRange("#head"))], + requires: @[("jester", parseVersionRange("0.1.0"), ""), + ("captcha", parseVersionRange("1.0.0"), ""), + ("auth", parseVersionRange("#head"), "")], checksum: "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2", ) diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 3e2e2e85..06937e82 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -116,7 +116,8 @@ proc createDirD*(dir: string) = display("Creating", "directory $#" % dir, priority = LowPriority) createDir(dir) -proc getDownloadDirName*(uri: string, verRange: VersionRange): string = +proc getDownloadDirName*(uri: string, verRange: VersionRange, + vcsRevision: string): string = ## Creates a directory name based on the specified ``uri`` (url) result = "" let puri = parseUri(uri) @@ -136,6 +137,10 @@ proc getDownloadDirName*(uri: string, verRange: VersionRange): string = if verSimple != "": result.add "_" result.add verSimple + + if vcsRevision.len > 0: + result.add "_" + result.add vcsRevision proc incl*(s: var HashSet[string], v: seq[string] | HashSet[string]) = for i in v: @@ -175,6 +180,22 @@ proc getNimbleUserTempDir*(): string = tmpdir = getTempDir() return tmpdir + +proc getVcsRevisionFromDir*(dir: string): string = + ## Returns current revision number of HEAD if dir is inside VCS, or nil in + ## case of failure. + + template tryToGetRevision(command: string): untyped = + try: + let (output, exitCode) = doCmdEx(command) + if exitCode == QuitSuccess: + return string(output).strip() + except: + discard + + tryToGetRevision("git -C " & quoteShell(dir) & " rev-parse HEAD") + tryToGetRevision("hg --cwd " & quoteShell(dir) & " id -i") + proc newSSLContext*(disabled: bool): SslContext = var sslVerifyMode = CVerifyPeer if disabled: diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index c11052d8..251fbf91 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -3,6 +3,7 @@ ## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5`` import strutils, tables, hashes, parseutils + type Version* = distinct string @@ -31,7 +32,7 @@ type nil ## Tuple containing package name and version range. - PkgTuple* = tuple[name: string, ver: VersionRange] + PkgTuple* = tuple[name: string, ver: VersionRange, vcsRevision: string] ParseVersionError* = object of ValueError NimbleError* = object of CatchableError diff --git a/tests/.gitignore b/tests/.gitignore index a46b1a44..cfa6829e 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -2,6 +2,30 @@ # tester /nimble-test +/buildDir +/binaryPackage/v1/binaryPackage +/binaryPackage/v2/binaryPackage +/develop/dependent/src/dependent +/issue27/issue27 +/issue206/issue/issue206bin +/issue289/issue289 +/issue428/nimbleDir/ +/issue564/issue564/ +/nimbleDir/ +/nimbleVersionDefine/nimbleVersionDefine +/packageStructure/c/c +/packageStructure/y/y +/run/run +/testCommand/testOverride/myTester +/testCommand/testsFail/tests/a +/testCommand/testsFail/tests/b +/testCommand/testsPass/tests/one +/testCommand/testsPass/tests/three +/testCommand/testsPass/tests/two +/nimscript/nimscript +/packageStructure/validBinary/y +/testCommand/testsFail/tests/t2 +/passNimFlags/passNimFlags /issue799/issue799 /issue308515/v1/binname.out /issue308515/v2/binname.out diff --git a/tests/tester.nim b/tests/tester.nim index a36b33f8..1268fd76 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -91,7 +91,7 @@ proc hasLineStartingWith(lines: seq[string], prefix: string): bool = return true return false -proc getPackageDir*(pkgCacheDir, pkgDirPrefix: string): string = +proc getPackageDir(pkgCacheDir, pkgDirPrefix: string): string = for kind, dir in walkDir(pkgCacheDir): if kind != pcDir or not dir.startsWith(pkgCacheDir / pkgDirPrefix): continue @@ -315,7 +315,7 @@ suite "nimscript": test "can accept short flags (#329)": cd "nimscript": - check execNimble("c", "-d:release", "nimscript.nim").exitCode == QuitSuccess + check execNimble("c", "-d:release", "nimscript.nim").exitCode == QuitSuccess" suite "uninstall": beforeSuite() @@ -324,6 +324,9 @@ suite "uninstall": let args = ["install", "https://github.com/nimble-test/packagebin2.git"] check execNimbleYes(args).exitCode == QuitSuccess + proc cannotSatisfyMsg(v1, v2: string): string = + &"Cannot satisfy the dependency on PackageA {v1} and PackageA {v2}" + test "can reject same version dependencies": let (outp, exitCode) = execNimbleYes( "install", "https://github.com/nimble-test/packagebin.git") @@ -331,8 +334,8 @@ suite "uninstall": # stderr output being generated and flushed without first flushing stdout let ls = outp.strip.processOutput() check exitCode != QuitSuccess - check "Cannot satisfy the dependency on PackageA 0.2.0 and PackageA 0.5.0" in - ls[ls.len-1] + check ls.inLines(cannotSatisfyMsg("0.2.0", "0.5.0")) or + ls.inLines(cannotSatisfyMsg("0.5.0", "0.2.0")) test "issue #27": # Install b @@ -1242,8 +1245,9 @@ suite "issues": cd "issue428": # Note: Can't use execNimble because it patches nimbleDir check execCmdEx(nimblePath & " -y --nimbleDir=./nimbleDir install").exitCode == QuitSuccess - check dirExists("nimbleDir/pkgs/dummy-0.1.0") - check(not dirExists("nimbleDir/pkgs/dummy-0.1.0/nimbleDir")) + let pkgDir = getPackageDir("nimbleDir/pkgs", "dummy-0.1.0") + check pkgDir.dirExists + check not (pkgDir / "nimbleDir").dirExists test "issue 399": cd "issue399": From b34e7b45815d6bb63cc157b5dda4003a9f2e5d94 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 4 Mar 2020 20:09:19 +0200 Subject: [PATCH 07/73] Implement topological sort of locked dependencies Dependencies are sorted topologically before writing them into the lock file. When the lock file is loaded the order is preserved by using `OrderedTable` data structure and they will be installed in a such way, that if some dependency must be build before installing, its dependencies will be already installed. If a cycle in the dependency graph is detected then a warning will be printed, but the algorithm will continue, because it is not expected the most of the dependencies to be binary and to require build. Despite the fact that the cyclic dependencies are invalid, there are many cases that they will actually work and it is better to continue with a warning which should be fixed by package developers, instead of breaking the build prematurely. Unit tests for the topological sort are added and the "Module tests" suite in `tester.nim` file is re-factored to use template for minimizing the code duplication. Related to nim-lang/nimble#127 --- .gitignore | 1 + src/nimble.nim | 6 +- src/nimblepkg/checksum.nim | 4 +- src/nimblepkg/common.nim | 2 +- src/nimblepkg/packageinfo.nim | 47 ++++----- src/nimblepkg/packageinfotypes.nim | 2 +- src/nimblepkg/publish.nim | 2 +- src/nimblepkg/tools.nim | 2 +- src/nimblepkg/topologicalsort.nim | 151 +++++++++++++++++++++++++++++ tests/tester.nim | 3 +- 10 files changed, 185 insertions(+), 35 deletions(-) create mode 100644 src/nimblepkg/topologicalsort.nim diff --git a/.gitignore b/.gitignore index abe509dd..c5f72218 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ src/nimblepkg/packageparser src/nimblepkg/publish src/nimblepkg/reversedeps src/nimblepkg/tools +src/nimblepkg/topologicalsort src/nimblepkg/version # Windows executables diff --git a/src/nimble.nim b/src/nimble.nim index 78b432d4..f898afc4 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -16,7 +16,7 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, - nimblepkg/checksum + nimblepkg/checksum, nimblepkg/topologicalsort import nimblepkg/nimscriptwrapper @@ -1186,7 +1186,9 @@ proc lock(options: Options) = let packageInfo = getPkgInfo(currentDir, options) let dependencies = processDeps(packageInfo, options).toSeq.map( pkg => pkg.toFullInfo(options)) - writeLockFile(dependencies, options) + let dependencyGraph = buildDependencyGraph(dependencies, options) + let (topologicalOrder, _) = topologicalSort(dependencyGraph) + writeLockFile(dependencyGraph, topologicalOrder) proc run(options: Options) = # Verify parameters. diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index cdcf4988..8aab9173 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -24,11 +24,11 @@ proc extractFileList(consoleOutput: string): seq[string] = proc getPackageFileListFromGit(): seq[string] = let output = tryDoCmdEx("git ls-files") - extractFileList(string(output)) + extractFileList(output) proc getPackageFileListFromMercurial(): seq[string] = let output = tryDoCmdEx("hg manifest") - extractFileList(string(output)) + extractFileList(output) proc getPackageFileListWithoutScm(): seq[string] = for file in walkDirRec(".", relative = true): diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index ed53c4e4..3c8ad657 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -53,7 +53,7 @@ proc getOutputInfo*(err: ref NimbleError): (string, string) = proc reportUnitTestSuccess*() = if programResult == QuitSuccess: - stdout.styledWrite(fgGreen, "All tests passed.") + stdout.styledWrite(fgGreen, "All tests passed.\n") when not declared(initHashSet): template initHashSet*[A](initialSize = 64): HashSet[A] = diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 3d6262d8..0e9856d3 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -3,7 +3,7 @@ # Stdlib imports import system except TResult -import hashes, json, strutils, os, sets, tables, httpclient, sequtils, sugar +import hashes, json, strutils, os, sets, tables, httpclient from net import SslError from compiler/nimblecmd import getPathVersionChecksum @@ -30,40 +30,35 @@ const proc lockFileExists*(dir: string): bool = fileExists(dir / lockFile.name) -proc getPackage*(name: string, options: Options): Package +proc writeLockFile*(fileName: string, packages: LockFileDependencies, + topologicallySortedOrder: seq[string]) = + ## Saves lock file on the disk in topologically sorted order of the + ## dependencies. -proc writeLockFile*(packages: seq[PackageInfo], options: Options) = let packagesJsonNode = newJObject() - for pkgInfo in packages: - let pkg = getPackage(pkgInfo.name, options) - let packageJsonNode = %{ - $lfjkPackageVersion: %pkgInfo.version, - $lfjkPackageVcsRevision: %pkgInfo.vcsRevision, - $lfjkPackageUrl: %pkg.url, - $lfjkPackageDownloadMethod: %pkg.downloadMethod, - $lfjkPackageDependencies: %pkgInfo.requires.map( - pkg => pkg.name).filter(name => name != "nim"), - $lfjkPackageChecksum: %{ - $lfjkPackageChecksumSha1: %pkgInfo.checksum - } - } - packagesJsonNode.add(pkgInfo.name, packageJsonNode) + for packageName in topologicallySortedOrder: + packagesJsonNode.add packageName, %packages[packageName] let mainJsonNode = %{ - $lfjkVersion: %lockFile.version, - $lfjkPackages: packagesJsonNode - } + $lfjkVersion: %lockFile.version, + $lfjkPackages: packagesJsonNode + } + + writeFile(fileName, mainJsonNode.pretty) - writeFile(lockFile.name, mainJsonNode.pretty) +proc writeLockFile*(packages: LockFileDependencies, + topologicallySortedOrder: seq[string]) = + writeLockFile(lockFile.name, packages, topologicallySortedOrder) -proc readLockFile*(dir: string): LockFileDependencies = - let lockFilePath = dir / lockFile.name - let json = parseFile(lockFilePath) - result = json[$lfjkPackages].to(result.typeof) +proc readLockFile*(filePath: string): LockFileDependencies = + parseFile(filePath)[$lfjkPackages].to(result.typeof) + +proc readLockFileInDir*(dir: string): LockFileDependencies = + readLockFile(dir / lockFile.name) proc getLockedDependencies*(dir: string): LockFileDependencies = if lockFileExists(dir): - result = readLockFile(dir) + result = readLockFileInDir(dir) proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = ## Splits ``pkgpath`` in the format diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 87b2e569..0e069371 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -16,7 +16,7 @@ type dependencies*: seq[string] checksum*: Checksums - LockFileDependencies* = Table[string, LockFileDependency] + LockFileDependencies* = OrderedTable[string, LockFileDependency] PackageInfo* = object myPath*: string ## The path of this .nimble file diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index 0257bcaf..3ab67d95 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -202,7 +202,7 @@ proc publish*(p: PackageInfo, o: Options) = if dirExists(os.getCurrentDir() / ".git"): let (output, exitCode) = doCmdEx("git ls-remote --get-url") if exitCode == 0: - url = output.string.strip + url = output.strip if url.endsWith(".git"): url.setLen(url.len - 4) downloadMethod = "git" let parsed = parseUri(url) diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 06937e82..f8697746 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -189,7 +189,7 @@ proc getVcsRevisionFromDir*(dir: string): string = try: let (output, exitCode) = doCmdEx(command) if exitCode == QuitSuccess: - return string(output).strip() + return output.strip() except: discard diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim new file mode 100644 index 00000000..3f80057e --- /dev/null +++ b/src/nimblepkg/topologicalsort.nim @@ -0,0 +1,151 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import sequtils, sugar, tables, strformat, algorithm +import packageinfotypes, packageinfo, options, cli + +proc buildDependencyGraph*(packages: seq[PackageInfo], options: Options): + LockFileDependencies = + ## Creates records which will be saved to the lock file. + + for pkgInfo in packages: + var package: LockFileDependency + let pkg = getPackage(pkgInfo.name, options) + package.version = pkgInfo.version + package.vcsRevision = pkgInfo.vcsRevision + package.url = pkg.url + package.downloadMethod = pkg.downloadMethod + package.dependencies = pkgInfo.requires.map( + pkg => pkg.name).filter(name => name != "nim") + package.checksum.sha1 = pkgInfo.checksum + result[pkgInfo.name] = package + +proc topologicalSort*(graph: LockFileDependencies): + tuple[order: seq[string], cycles: seq[seq[string]]] = + ## Topologically sorts dependency graph which will be saved to the lock file. + ## + ## Returns tuple containing sequence with the package names in the + ## topologically sorted order and another sequence with detected cyclic + ## dependencies if any (should not be such). Only cycles which don't have + ## edges part of another cycle are being detected (in the order of the + ## visiting). + + type + NodeMark = enum + nmNotMarked + nmTemporary + nmPermanent + + NodeInfo = tuple[mark: NodeMark, cameFrom: string] + NodesInfo = OrderedTable[string, NodeInfo] + + var + order = newSeqOfCap[string](graph.len) + cycles: seq[seq[string]] + nodesInfo: NodesInfo + + proc getCycle(finalNode: string): seq[string] = + var + path = newSeqOfCap[string](graph.len) + previousNode = nodesInfo[finalNode].cameFrom + + path.add finalNode + while previousNode != finalNode: + path.add previousNode + previousNode = nodesInfo[previousNode].cameFrom + + path.add previousNode + path.reverse() + + return path + + proc printNotADagWarning() = + let message = cycles.foldl( + a & "\nCycle detected: " & b.foldl(&"{a} -> {b}"), + "The dependency graph is not a DAG.") + display("Warning", message, Warning, HighPriority) + + for node, _ in graph: + nodesInfo[node] = (mark: nmNotMarked, cameFrom: "") + + proc visit(node: string) = + template nodeInfo: var NodeInfo = nodesInfo[node] + + if nodeInfo.mark == nmPermanent: + return + + if nodeInfo.mark == nmTemporary: + cycles.add getCycle(node) + return + + nodeInfo.mark = nmTemporary + + let neighbors = graph[node].dependencies + for node2 in neighbors: + nodesInfo[node2].cameFrom = node + visit(node2) + + nodeInfo.mark = nmPermanent + order.add node + + for node, nodeInfo in nodesInfo: + if nodeInfo.mark != nmPermanent: + visit(node) + + if cycles.len > 0: + printNotADagWarning() + + return (order, cycles) + +when isMainModule: + import unittest, common + + proc testTopologicalSort() = + + proc testWithoutCycles() = + let + graph = { + "json_serialization": LockFileDependency( + dependencies: @["serialization", "stew"]), + "faststreams": LockFileDependency(dependencies: @["stew"]), + "testutils": LockFileDependency(), + "stew": LockFileDependency(), + "serialization": LockFileDependency( + dependencies: @["faststreams", "stew"]), + "chronicles": LockFileDependency( + dependencies: @["json_serialization", "testutils"]) + }.toOrderedTable + + expectedTopologicallySortedOrder = @[ + "stew", "faststreams", "serialization", "json_serialization", + "testutils", "chronicles"] + expectedCycles: seq[seq[string]] = @[] + + (actualTopologicallySortedOrder, actualCycles) = topologicalSort(graph) + + check actualTopologicallySortedOrder == expectedTopologicallySortedOrder + check actualCycles == expectedCycles + + proc testWithCycles() = + let + graph = { + "A": LockFileDependency(dependencies: @["B", "E"]), + "B": LockFileDependency(dependencies: @["A", "C"]), + "C": LockFileDependency(dependencies: @["D"]), + "D": LockFileDependency(dependencies: @["B"]), + "E": LockFileDependency(dependencies: @["D", "E"]) + }.toOrderedTable + + expectedTopologicallySortedOrder = @["D", "C", "B", "E", "A"] + expectedCycles = @[@["A", "B", "A"], @["B", "C", "D", "B"], @["E", "E"]] + + (actualTopologicallySortedOrder, actualCycles) = topologicalSort(graph) + + check actualTopologicallySortedOrder == expectedTopologicallySortedOrder + check actualCycles == expectedCycles + + testWithoutCycles() + testWithCycles() + + testTopologicalSort() + reportUnitTestSuccess() diff --git a/tests/tester.nim b/tests/tester.nim index 1268fd76..e51cac44 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -315,7 +315,7 @@ suite "nimscript": test "can accept short flags (#329)": cd "nimscript": - check execNimble("c", "-d:release", "nimscript.nim").exitCode == QuitSuccess" + check execNimble("c", "-d:release", "nimscript.nim").exitCode == QuitSuccess suite "uninstall": beforeSuite() @@ -1040,6 +1040,7 @@ suite "misc tests": "[Deprecated]", "[XDeclaredButNotUsed]", "[Spacing]", + "[ConvFromXtoItselfNotNeeded]", ] for line in output.splitLines(): From 6abb4cd9cef8484142a86bc15c1de2096f59441f Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 10 Mar 2020 20:25:42 +0200 Subject: [PATCH 08/73] Move some lock file functionality to separate file Move lock file reading and writing functionality to separate Nim module in `lockfile.nim` file. Related to nim-lang/nimble#127 --- src/nimble.nim | 5 ++- src/nimblepkg/lockfile.nim | 58 ++++++++++++++++++++++++++++++ src/nimblepkg/packageinfo.nim | 50 +------------------------- src/nimblepkg/packageinfotypes.nim | 17 ++------- src/nimblepkg/topologicalsort.nim | 2 +- 5 files changed, 64 insertions(+), 68 deletions(-) create mode 100644 src/nimblepkg/lockfile.nim diff --git a/src/nimble.nim b/src/nimble.nim index f898afc4..e8ebbccb 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -16,9 +16,8 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, - nimblepkg/checksum, nimblepkg/topologicalsort - -import nimblepkg/nimscriptwrapper + nimblepkg/checksum, nimblepkg/topologicalsort, nimblepkg/lockfile, + nimblepkg/nimscriptwrapper proc refresh(options: Options) = ## Downloads the package list from the specified URL. diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim new file mode 100644 index 00000000..73928aa2 --- /dev/null +++ b/src/nimblepkg/lockfile.nim @@ -0,0 +1,58 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import tables, os, json + +type + Checksums* = object + sha1*: string + + LockFileDependency* = object + version*: string + vcsRevision*: string + url*: string + downloadMethod*: string + dependencies*: seq[string] + checksum*: Checksums + + LockFileDependencies* = OrderedTable[string, LockFileDependency] + + LockFileJsonKeys = enum + lfjkVersion = "version" + lfjkPackages = "packages" + +const + lockFile = (name: "nimble.lockfile", version: "0.1.0") + +proc lockFileExists*(dir: string): bool = + fileExists(dir / lockFile.name) + +proc writeLockFile*(fileName: string, packages: LockFileDependencies, + topologicallySortedOrder: seq[string]) = + ## Saves lock file on the disk in topologically sorted order of the + ## dependencies. + + let packagesJsonNode = newJObject() + for packageName in topologicallySortedOrder: + packagesJsonNode.add packageName, %packages[packageName] + + let mainJsonNode = %{ + $lfjkVersion: %lockFile.version, + $lfjkPackages: packagesJsonNode + } + + writeFile(fileName, mainJsonNode.pretty) + +proc writeLockFile*(packages: LockFileDependencies, + topologicallySortedOrder: seq[string]) = + writeLockFile(lockFile.name, packages, topologicallySortedOrder) + +proc readLockFile*(filePath: string): LockFileDependencies = + parseFile(filePath)[$lfjkPackages].to(result.typeof) + +proc readLockFileInDir*(dir: string): LockFileDependencies = + readLockFile(dir / lockFile.name) + +proc getLockedDependencies*(dir: string): LockFileDependencies = + if lockFileExists(dir): + result = readLockFileInDir(dir) diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 0e9856d3..f0674988 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -10,55 +10,7 @@ from compiler/nimblecmd import getPathVersionChecksum # Local imports import version, tools, common, options, cli, config, checksum, - packageinfotypes - -type - LockFileJsonKeys = enum - lfjkVersion = "version" - lfjkPackages = "packages" - lfjkPackageVersion = "version" - lfjkPackageVcsRevision = "vcsRevision" - lfjkPackageUrl = "url" - lfjkPackageDownloadMethod = "downloadMethod" - lfjkPackageDependencies = "dependencies" - lfjkPackageChecksum = "checksum" - lfjkPackageChecksumSha1 = "sha1" - -const - lockFile = (name: "nimble.lockfile", version: "0.1.0") - -proc lockFileExists*(dir: string): bool = - fileExists(dir / lockFile.name) - -proc writeLockFile*(fileName: string, packages: LockFileDependencies, - topologicallySortedOrder: seq[string]) = - ## Saves lock file on the disk in topologically sorted order of the - ## dependencies. - - let packagesJsonNode = newJObject() - for packageName in topologicallySortedOrder: - packagesJsonNode.add packageName, %packages[packageName] - - let mainJsonNode = %{ - $lfjkVersion: %lockFile.version, - $lfjkPackages: packagesJsonNode - } - - writeFile(fileName, mainJsonNode.pretty) - -proc writeLockFile*(packages: LockFileDependencies, - topologicallySortedOrder: seq[string]) = - writeLockFile(lockFile.name, packages, topologicallySortedOrder) - -proc readLockFile*(filePath: string): LockFileDependencies = - parseFile(filePath)[$lfjkPackages].to(result.typeof) - -proc readLockFileInDir*(dir: string): LockFileDependencies = - readLockFile(dir / lockFile.name) - -proc getLockedDependencies*(dir: string): LockFileDependencies = - if lockFileExists(dir): - result = readLockFileInDir(dir) + packageinfotypes, lockfile proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = ## Splits ``pkgpath`` in the format diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 0e069371..0934093a 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -1,23 +1,10 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import sets, tables -import version +import sets +import version, lockfile type - Checksums* = object - sha1*: string - - LockFileDependency* = object - version*: string - vcsRevision*: string - url*: string - downloadMethod*: string - dependencies*: seq[string] - checksum*: Checksums - - LockFileDependencies* = OrderedTable[string, LockFileDependency] - PackageInfo* = object myPath*: string ## The path of this .nimble file isNimScript*: bool ## Determines if this pkg info was read from a nims file diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index 3f80057e..32254ff9 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import sequtils, sugar, tables, strformat, algorithm -import packageinfotypes, packageinfo, options, cli +import packageinfotypes, packageinfo, options, cli, lockfile proc buildDependencyGraph*(packages: seq[PackageInfo], options: Options): LockFileDependencies = From f8708ad7a0adf21592afd1a201e93c17f9798863 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 10 Mar 2020 21:13:30 +0200 Subject: [PATCH 09/73] Move some functionality to separate file Move `nimbledata.json` file saving and loading functionality to the separate Nim module `nimbledata.nim`. Related to nim-lang/nimble#127 --- .gitignore | 1 + src/nimble.nim | 13 +++++---- src/nimblepkg/common.nim | 1 - src/nimblepkg/nimbledata.nim | 44 ++++++++++++++++++++++++++++++ src/nimblepkg/options.nim | 24 ++-------------- src/nimblepkg/packageinfo.nim | 2 +- src/nimblepkg/packageinfotypes.nim | 2 +- src/nimblepkg/packageinstaller.nim | 2 +- src/nimblepkg/reversedeps.nim | 8 ++---- tests/tester.nim | 2 +- 10 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 src/nimblepkg/nimbledata.nim diff --git a/.gitignore b/.gitignore index c5f72218..7afc34f0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ src/nimblepkg/download src/nimblepkg/init src/nimblepkg/jsonhelpers src/nimblepkg/lockfile +src/nimblepkg/nimbledata src/nimblepkg/nimscriptapi src/nimblepkg/nimscriptexecutor src/nimblepkg/nimscriptwrapper diff --git a/src/nimble.nim b/src/nimble.nim index e8ebbccb..604a9d4e 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -17,7 +17,7 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, nimblepkg/checksum, nimblepkg/topologicalsort, nimblepkg/lockfile, - nimblepkg/nimscriptwrapper + nimblepkg/nimscriptwrapper, nimblepkg/nimbledata proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -201,14 +201,14 @@ proc buildFromDir( proc removePkgDir(dir: string, options: Options) = ## Removes files belonging to the package in ``dir``. try: - var nimblemeta = parseFile(dir / packageMetaDataFileName) + var nimblemeta = parseFile(dir / nimbleDataFile.name) if not nimblemeta.hasKey($pmdjkFiles): raise newException(JsonParsingError, "Meta data does not contain required info.") for file in nimblemeta[$pmdjkFiles]: removeFile(dir / file.str) - removeFile(dir / packageMetaDataFileName) + removeFile(dir / nimbleDataFile.name) # If there are no files left in the directory, remove the directory. if toSeq(walkDirRec(dir)).len == 0: @@ -245,6 +245,9 @@ proc removePkgDir(dir: string, options: Options) = raise NimbleQuit(msg: "") removeDir(dir) +proc saveNimbleData(options: Options) = + saveNimbleDataToDir(options.getNimbleDir(), options.nimbleData) + proc processLockedDependencies(packageInfo: PackageInfo, options: Options): seq[PackageInfo] @@ -310,7 +313,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Don't copy artifacts if project local deps mode and "installing" the top level package if not (options.localdeps and options.isInstallingTopLevel(dir)): let pkgDestDir = pkgInfo.getPkgDest(options) - if fileExists(pkgDestDir / packageMetaDataFileName): + if fileExists(pkgDestDir / nimbleDataFile.name): let msg = "$1@$2 already exists. Overwrite?" % [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): @@ -1030,7 +1033,7 @@ proc developFromDir(dir: string, options: Options) = # This is similar to the code in `installFromDir`, except that we # *consciously* not worry about the package's binaries. let pkgDestDir = pkgInfo.getPkgDest(options) - if fileExists(pkgDestDir / packageMetaDataFileName): + if fileExists(pkgDestDir / nimbleDataFile.name): let msg = "$1@$2 already exists. Overwrite?" % [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 3c8ad657..f4f4e682 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -32,7 +32,6 @@ type const nimbleVersion* = "0.13.1" nimbleDataFile* = (name: "nimbledata.json", version: "0.1.0") - packageMetaDataFileName* = "nimblemeta.json" proc raiseNimbleError*(msg: string, hint = "") = var exc = newException(NimbleError, msg) diff --git a/src/nimblepkg/nimbledata.nim b/src/nimblepkg/nimbledata.nim new file mode 100644 index 00000000..e6518443 --- /dev/null +++ b/src/nimblepkg/nimbledata.nim @@ -0,0 +1,44 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import json, os + +type + NimbleDataJsonKeys* = enum + ndjkVersion = "version" + ndjkRevDep = "reverseDeps" + ndjkRevDepName = "name" + ndjkRevDepVersion = "version" + ndjkRevDepChecksum = "checksum" + +const + nimbleDataFile = (name: "nimbledata.json", version: "0.1.0") + +proc saveNimbleData*(filePath: string, nimbleData: JsonNode) = + # TODO: This file should probably be locked. + writeFile(filePath, nimbleData.pretty) + +proc saveNimbleDataToDir*(nimbleDir: string, nimbleData: JsonNode) = + saveNimbleData(nimbleDir / nimbleDataFile.name, nimbleData) + +proc newNimbleDataNode*(): JsonNode = + %{ $ndjkVersion: %nimbleDataFile.version, $ndjkRevDep: newJObject() } + +proc convertToTheNewFormat(nimbleData: JsonNode) = + nimbleData.add($ndjkVersion, %nimbleDataFile.version) + for name, versions in nimbleData[$ndjkRevDep]: + for version, dependencies in versions: + for dependency in dependencies: + dependency.add($ndjkRevDepChecksum, %"") + versions[version] = %{ "": dependencies } + +proc parseNimbleData*(fileName: string): JsonNode = + if fileExists(fileName): + result = parseFile(fileName) + if not result.hasKey($ndjkVersion): + convertToTheNewFormat(result) + else: + result = newNimbleDataNode() + +proc parseNimbleDataFromDir*(dir: string): JsonNode = + parseNimbleData(dir / nimbleDataFile.name) diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 708a91a8..879a8686 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -6,7 +6,7 @@ import sequtils, sugar import std/options as std_opt from httpclient import Proxy, newProxy -import config, version, common, cli, packageinfotypes +import config, version, common, cli, packageinfotypes, nimbledata const nimbledeps* = "nimbledeps" @@ -507,25 +507,6 @@ proc initOptions*(): Options = noColor: not isatty(stdout) ) -proc newNimbleDataNode*(): JsonNode = - %{ $ndjkVersion: %nimbleDataFile.version, $ndjkRevDep: newJObject() } - -proc convertToTheNewFormat(nimbleData: JsonNode) = - nimbleData.add($ndjkVersion, %nimbleDataFile.version) - for name, versions in nimbleData[$ndjkRevDep]: - for version, dependencies in versions: - for dependency in dependencies: - dependency.add($ndjkRevDepChecksum, %"") - versions[version] = %{ "": dependencies } - -proc parseNimbleData*(fileName: string): JsonNode = - if fileExists(fileName): - result = parseFile(fileName) - if not result.hasKey($ndjkVersion): - convertToTheNewFormat(result) - else: - result = newNimbleDataNode() - proc handleUnknownFlags(options: var Options) = if options.action.typ == actionRun: # In addition to flags that come after the command before binary, @@ -583,8 +564,7 @@ proc parseCmdLine*(): Options = # Parse config. result.config = parseConfig() - result.nimbleData = parseNimbleData( - result.getNimbleDir() / nimbleDataFile.name) + result.nimbleData = parseNimbleDataFromDir(result.getNimbleDir()) if result.action.typ == actionNil and not result.showVersion: result.showHelp = true diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index f0674988..58391ba7 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -25,7 +25,7 @@ proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = proc readMetaData*(path: string, silent = false): MetaData = ## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json`` - var bmeta = path / packageMetaDataFileName + var bmeta = path / nimbleDataFile.name if not fileExists(bmeta) and not silent: result.url = "" display("Warning:", "No nimblemeta.json file found in " & path, diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 0934093a..580400d1 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -1,7 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import sets +import sets, tables import version, lockfile type diff --git a/src/nimblepkg/packageinstaller.nim b/src/nimblepkg/packageinstaller.nim index 4b9634e2..758b40e6 100644 --- a/src/nimblepkg/packageinstaller.nim +++ b/src/nimblepkg/packageinstaller.nim @@ -100,7 +100,7 @@ proc saveNimbleMeta*(pkgDestDir, url, vcsRevision: string, for bin in bins: binaries.add(%bin) nimblemeta[$pmdjkIsLink] = %isLink - writeFile(pkgDestDir / packageMetaDataFileName, nimblemeta.pretty) + writeFile(pkgDestDir / nimbleDataFile.name, nimblemeta.pretty) proc saveNimbleMeta*(pkgDestDir, pkgDir, vcsRevision, nimbleLinkPath: string) = ## Overload of saveNimbleMeta for linked (.nimble-link) packages. diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 5a94ac7a..20917be4 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -1,16 +1,11 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, json, sets +import json, sets import common, options, version, download, jsonhelpers, packageinfotypes, packageinfo -proc saveNimbleData*(options: Options) = - # TODO: This file should probably be locked. - writeFile(options.getNimbleDir() / nimbleDataFile.name, - pretty(options.nimbleData)) - proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, pkg: PackageInfo) = # Add a record which specifies that `pkg` has a dependency on `dep`, i.e. @@ -108,6 +103,7 @@ proc getAllRevDeps*(options: Options, pkg: PackageInfo, when isMainModule: import unittest + import nimbledata let nimforum1 = PackageInfo( isMinimal: false, diff --git a/tests/tester.nim b/tests/tester.nim index e51cac44..23d132a6 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -4,7 +4,7 @@ import osproc, unittest, strutils, os, sequtils, sugar, json, std/sha1, strformat from nimblepkg/common import ProcessOutput -from nimblepkg/options import parseNimbleData +from nimblepkg/nimbledata import parseNimbleData # TODO: Each test should start off with a clean slate. Currently installed # packages are shared between each test which causes a multitude of issues From 052fbf4dd3503cd8ba9b92a480405767870492cb Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 11 Mar 2020 20:12:13 +0200 Subject: [PATCH 10/73] Move `NimbleError` type to `common.nim` NimbleError type is moved from `version.nim` to `common.nim` where it belongs more naturally since it is a base type for all Nimble exceptions and should be potentially available to all other Nimble modules even if they do not require version handling functionalities from `version.nim`. Related to nim-lang/nimble#127 --- src/nimblepkg/checksum.nim | 2 +- src/nimblepkg/cli.nim | 2 +- src/nimblepkg/common.nim | 4 +++- src/nimblepkg/config.nim | 2 +- src/nimblepkg/nimscriptexecutor.nim | 3 +-- src/nimblepkg/nimscriptwrapper.nim | 3 +-- src/nimblepkg/packageparser.nim | 2 +- src/nimblepkg/publish.nim | 3 +-- src/nimblepkg/version.nim | 4 +--- 9 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index 8aab9173..f468020c 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import os, strutils, std/sha1, algorithm, strformat -import version, tools +import common, tools type ChecksumError* = object of NimbleError diff --git a/src/nimblepkg/cli.nim b/src/nimblepkg/cli.nim index 6ce27fd7..06cc565d 100644 --- a/src/nimblepkg/cli.nim +++ b/src/nimblepkg/cli.nim @@ -13,7 +13,7 @@ # - Normal for MediumPriority. import terminal, sets, strutils -import version +import common when not declared(initHashSet): import common diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index f4f4e682..c421d7d3 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -5,9 +5,11 @@ # recursive imports import sets, terminal -import version type + NimbleError* = object of CatchableError + hint*: string + BuildFailed* = object of NimbleError ## Same as quit(QuitSuccess), but allows cleanup. diff --git a/src/nimblepkg/config.nim b/src/nimblepkg/config.nim index 32f97248..d2da021f 100644 --- a/src/nimblepkg/config.nim +++ b/src/nimblepkg/config.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import parsecfg, streams, strutils, os, tables, uri -import version, cli +import common, cli type Config* = object diff --git a/src/nimblepkg/nimscriptexecutor.nim b/src/nimblepkg/nimscriptexecutor.nim index 00d8eb74..598adb05 100644 --- a/src/nimblepkg/nimscriptexecutor.nim +++ b/src/nimblepkg/nimscriptexecutor.nim @@ -3,8 +3,7 @@ import os, strutils, sets -import packageparser, common, packageinfo, options, nimscriptwrapper, cli, - version +import packageparser, common, packageinfo, options, nimscriptwrapper, cli proc execHook*(options: Options, hookAction: ActionType, before: bool): bool = ## Returns whether to continue. diff --git a/src/nimblepkg/nimscriptwrapper.nim b/src/nimblepkg/nimscriptwrapper.nim index 80979934..f339121e 100644 --- a/src/nimblepkg/nimscriptwrapper.nim +++ b/src/nimblepkg/nimscriptwrapper.nim @@ -5,8 +5,7 @@ ## scripting language. import hashes, json, os, strutils, tables, times, osproc - -import version, options, cli, tools +import common, options, cli, tools type Flags = TableRef[string, seq[string]] diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index acbba5e4..e54faf34 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -3,7 +3,7 @@ import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map, toSeq -import version, tools, nimscriptwrapper, options, cli, +import common, version, tools, nimscriptwrapper, options, cli, packageinfo, packageinfotypes ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index 3ab67d95..edf97c54 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -6,8 +6,7 @@ import system except TResult import httpclient, strutils, json, os, browsers, times, uri -import version, tools, cli, config, options, packageinfotypes -import version, tools, common, cli, config, options +import common, tools, cli, config, options, packageinfotypes {.warning[UnusedImport]: off.} from net import SslCVerifyMode, newContext diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index 251fbf91..0fcfb62d 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -2,7 +2,7 @@ # 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 +import common, strutils, tables, hashes, parseutils type Version* = distinct string @@ -35,8 +35,6 @@ type PkgTuple* = tuple[name: string, ver: VersionRange, vcsRevision: string] ParseVersionError* = object of ValueError - NimbleError* = object of CatchableError - hint*: string proc `$`*(ver: Version): string {.borrow.} From 45bc9755b73ca9a889a332c34b8da59cacf1a28f Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Thu, 16 Jul 2020 20:36:53 +0300 Subject: [PATCH 11/73] Merge `PackageInfo` and `MetaData` types In order to eliminate some duplications in the data about a package, the `MetaData` object is added as a field of the `PackageInfo` object. Transparent access to the fields of the `metaData` field directly via `PackageInfo` is provided via getter and setter templates. Currently the following duplications are remove: - The `vcsRevision` string field. Priority is given to the VCS revision obtained from the real package directory instead to this written in the package meta data in the case of the linked package. - The `isLinked` boolean field in the `PackageInfo` object is removed and the `isLink` field of the `PackageMetaData` object is used instead. - The `bin` sequence field in the `PackageInfo` object is removed and the `binaries` sequence in the `PackageMetaData` object is used instead. This is possible due to the fact that they should be the same. In the case of Windows links, where two files are being installed, we know how the name of the additional file is produced by the name written in the `bin` field and we can also delete it without saving both names in the meta data. The code for saving and loading of the package meta data is changed to use direct mapping between the Nim structure and the JSON object. Everywhere where it is necessary the package meta data is filled in the `PackageInfo` object after being read from the JSON file. One such place is `removePkgDir` procedure which functionality is split in multiple smaller procedures in order to make it easier for understanding and maintenance. Additionally as a part of the refactoring the code for setting or calculating the package checksum and for getting the package VCS revision is removed from the `initPackageInfo` procedure and moved to other places where it belongs more naturally. Related to nim-lang/nimble#127 --- .gitignore | 1 + src/nimble.nim | 249 +++++++++++++++-------------- src/nimblepkg/checksum.nim | 2 +- src/nimblepkg/common.nim | 7 - src/nimblepkg/options.nim | 2 +- src/nimblepkg/packageinfo.nim | 122 +++++--------- src/nimblepkg/packageinfotypes.nim | 46 +++++- src/nimblepkg/packageinstaller.nim | 45 +----- src/nimblepkg/packagemetadata.nim | 48 ++++++ src/nimblepkg/packageparser.nim | 60 +++---- src/nimblepkg/tools.nim | 11 +- tests/tester.nim | 2 +- 12 files changed, 301 insertions(+), 294 deletions(-) create mode 100644 src/nimblepkg/packagemetadata.nim diff --git a/.gitignore b/.gitignore index 7afc34f0..1c4457de 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ src/nimblepkg/options src/nimblepkg/packageinfo src/nimblepkg/packageinfotypes src/nimblepkg/packageinstaller +src/nimblepkg/packagemetadata src/nimblepkg/packageparser src/nimblepkg/publish src/nimblepkg/reversedeps diff --git a/src/nimble.nim b/src/nimble.nim index 604a9d4e..9703424b 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -3,13 +3,14 @@ import system except TResult -import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc +import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc, + strformat + import std/options as std_opt import strutils except toLower from unicode import toLower from sequtils import toSeq -from strformat import fmt import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, nimblepkg/download, nimblepkg/config, nimblepkg/common, @@ -17,7 +18,8 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, nimblepkg/checksum, nimblepkg/topologicalsort, nimblepkg/lockfile, - nimblepkg/nimscriptwrapper, nimblepkg/nimbledata + nimblepkg/nimscriptwrapper, nimblepkg/nimbledata, + nimblepkg/packagemetadata proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -59,7 +61,7 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = "dependencies for $1@$2" % [pkginfo.name, pkginfo.specialVersion], priority = HighPriority) - var pkgList {.global.}: seq[PackageInfoAndMetaData] = @[] + var pkgList {.global.}: seq[PackageInfo] = @[] once: pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) var reverseDeps: seq[PackageBasicInfo] = @[] for dep in pkginfo.requires: @@ -91,9 +93,10 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = result.add(pkgs) pkg = installedPkg # For addRevDep + fillMetaData(pkg, pkg.getRealDir(), false) # This package has been installed so we add it to our pkgList. - pkgList.add((pkg, readMetaData(pkg.getRealDir()))) + pkgList.add pkg else: display("Info:", "Dependency on $1 already satisfied" % $dep, priority = HighPriority) @@ -130,6 +133,7 @@ proc buildFromDir( let realDir = pkgInfo.getRealDir() pkgDir = pkgInfo.myPath.parentDir() + cd pkgDir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionBuild, true): raise newException(NimbleError, "Pre-hook prevented further execution.") @@ -138,6 +142,7 @@ proc buildFromDir( raise newException(NimbleError, "Nothing to build. Did you specify a module to build using the" & " `bin` key in your .nimble file?") + var binariesBuilt = 0 args = args @@ -157,6 +162,7 @@ proc buildFromDir( if options.isInstallingTopLevel(pkgInfo.myPath.parentDir()): options.getCompilationBinary(pkgInfo).get("") else: "" + for bin, src in pkgInfo.bin: # Check if this is the only binary that we want to build. if binToBuild.len != 0 and binToBuild != bin: @@ -198,56 +204,86 @@ proc buildFromDir( cd pkgDir: # Make sure `execHook` executes the correct .nimble file. discard execHook(options, actionBuild, false) -proc removePkgDir(dir: string, options: Options) = - ## Removes files belonging to the package in ``dir``. - try: - var nimblemeta = parseFile(dir / nimbleDataFile.name) - if not nimblemeta.hasKey($pmdjkFiles): - raise newException(JsonParsingError, - "Meta data does not contain required info.") - for file in nimblemeta[$pmdjkFiles]: - removeFile(dir / file.str) - - removeFile(dir / nimbleDataFile.name) - - # If there are no files left in the directory, remove the directory. - if toSeq(walkDirRec(dir)).len == 0: - removeDir(dir) - else: - display("Warning:", ("Cannot completely remove $1. Files not installed " & - "by nimble are present.") % dir, Warning, HighPriority) - - if nimblemeta.hasKey($pmdjkBinaries): - # Remove binaries. - for binary in nimblemeta[$pmdjkBinaries]: - removeFile(options.getBinDir() / binary.str) - - # Search for an older version of the package we are removing. - # So that we can reinstate its symlink. - let (pkgName, _, _) = getNameVersionChecksum(dir) - let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - var pkgInfo: PackageInfo - if pkgList.findPkg((pkgName, newVRAny(), ""), pkgInfo): - pkgInfo = pkgInfo.toFullInfo(options) - for bin, src in pkgInfo.bin: - let symlinkDest = pkgInfo.getOutputDir(bin) - let symlinkFilename = options.getBinDir() / bin.extractFilename - discard setupBinSymlink(symlinkDest, symlinkFilename, options) - else: - display("Warning:", ("Cannot completely remove $1. Binary symlinks may " & - "have been left over in $2.") % - [dir, options.getBinDir()]) - except OSError, JsonParsingError: - display("Warning", "Unable to read nimblemeta.json: " & - getCurrentExceptionMsg(), Warning, HighPriority) - if not options.prompt("Would you like to COMPLETELY remove ALL files " & - "in " & dir & "?"): - raise NimbleQuit(msg: "") - removeDir(dir) - proc saveNimbleData(options: Options) = saveNimbleDataToDir(options.getNimbleDir(), options.nimbleData) +proc promptRemoveEntirePackageDir(pkgDir: string, options: Options) = + display("Warning", + &"Unable to read {packageMetaDataFileName}: {getCurrentExceptionMsg()}", + Warning, HighPriority) + + if not options.prompt( + &"Would you like to COMPLETELY remove ALL files in {pkgDir}?"): + raise NimbleQuit(msg: "") + +proc removePackageDir(pkgInfo: PackageInfo, pkgDestDir: string) = + for file in pkgInfo.files: + removeFile(pkgDestDir / file) + + removeFile(pkgDestDir / packageMetaDataFileName) + + if pkgDestDir.isEmptyDir(): + removeDir(pkgDestDir) + else: + display("Warning:", &"Cannot completely remove {pkgDestDir}." & + " Files not installed by Nimble are present.", + Warning, HighPriority) + +proc removeBinariesSymlinks(pkgInfo: PackageInfo, binDir: string) = + for bin in pkgInfo.binaries: + when defined(windows): + removeFile(binDir / bin.changeFileExt("cmd")) + removeFile(binDir / bin) + +proc reinstallSymlinksForOlderVersion(pkgDir: string, options: Options) = + let (pkgName, _, _) = getNameVersionChecksum(pkgDir) + let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) + var newPkgInfo: PackageInfo + if pkgList.findPkg((pkgName, newVRAny(), ""), newPkgInfo): + newPkgInfo = newPkgInfo.toFullInfo(options) + for bin in newPkgInfo.binaries: + let symlinkDest = newPkgInfo.getOutputDir(bin) + let symlinkFilename = options.getBinDir() / bin.extractFilename + discard setupBinSymlink(symlinkDest, symlinkFilename, options) + +proc removePackage(pkgInfo: PackageInfo, options: Options) = + var pkgInfo = pkgInfo + let pkgDestDir = pkgInfo.getPkgDest(options) + + if not pkgInfo.hasMetaData: + try: + fillMetaData(pkgInfo, pkgDestDir, true) + except MetaDataError, ValueError: + promptRemoveEntirePackageDir(pkgDestDir, options) + removeDir(pkgDestDir) + + removePackageDir(pkgInfo, pkgDestDir) + removeBinariesSymlinks(pkgInfo, options.getBinDir()) + reinstallSymlinksForOlderVersion(pkgDestDir, options) + options.nimbleData.removeRevDep(pkgInfo) + +proc packageExists(pkgInfo: PackageInfo, options: Options): bool = + let pkgDestDir = pkgInfo.getPkgDest(options) + return fileExists(pkgDestDir / packageMetaDataFileName) + +proc promptOverwriteExistingPackage(pkgInfo: PackageInfo, + options: Options): bool = + let message = "$1@$2 already exists. Overwrite?" % + [pkgInfo.name, pkgInfo.specialVersion] + return options.prompt(message) + +proc removeOldPackage(pkgInfo: PackageInfo, options: Options) = + let pkgDestDir = pkgInfo.getPkgDest(options) + let oldPkgInfo = getPkgInfo(pkgDestDir, options) + removePackage(oldPkgInfo, options) + +proc promptRemovePackageIfExists(pkgInfo: PackageInfo, options: Options): bool = + if packageExists(pkgInfo, options): + if not promptOverwriteExistingPackage(pkgInfo, options): + return false + removeOldPackage(pkgInfo, options) + return true + proc processLockedDependencies(packageInfo: PackageInfo, options: Options): seq[PackageInfo] @@ -310,32 +346,21 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, @[] buildFromDir(pkgInfo, paths, "-d:release" & flags, options) - # Don't copy artifacts if project local deps mode and "installing" the top level package - if not (options.localdeps and options.isInstallingTopLevel(dir)): - let pkgDestDir = pkgInfo.getPkgDest(options) - if fileExists(pkgDestDir / nimbleDataFile.name): - let msg = "$1@$2 already exists. Overwrite?" % - [pkgInfo.name, pkgInfo.specialVersion] - if not options.prompt(msg): - return + let pkgDestDir = pkgInfo.getPkgDest(options) - # Remove reverse deps. - let pkgInfo = getPkgInfo(pkgDestDir, options) - options.nimbleData.removeRevDep(pkgInfo) + # Fill package Meta data + pkgInfo.url = url + pkgInfo.isLink = false - removePkgDir(pkgDestDir, options) - # Remove any symlinked binaries - for bin, src in pkgInfo.bin: - # TODO: Check that this binary belongs to the package being installed. - when defined(windows): - removeFile(binDir / bin.changeFileExt("cmd")) - removeFile(binDir / bin.changeFileExt("")) - else: - removeFile(binDir / bin) + # Don't copy artifacts if project local deps mode and "installing" the top + # level package. + if not (options.localdeps and options.isInstallingTopLevel(dir)): + if not promptRemovePackageIfExists(pkgInfo, options): + return createDir(pkgDestDir) # Copy this package's files based on the preferences specified in PkgInfo. - var filesInstalled = initHashSet[string]() + var filesInstalled: HashSet[string] iterInstallFiles(realDir, pkgInfo, options, proc (file: string) = createDir(changeRoot(realDir, pkgDestDir, file.splitFile.dir)) @@ -348,7 +373,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, pkgInfo.myPath) filesInstalled.incl copyFileD(pkgInfo.myPath, dest) - var binariesInstalled = initHashSet[string]() + var binariesInstalled: HashSet[string] if pkgInfo.bin.len > 0: # Make sure ~/.nimble/bin directory is created. createDir(binDir) @@ -360,6 +385,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, if dirExists(pkgDestDir / bin): bin & ".out" else: bin + if fileExists(pkgDestDir / binDest): display("Warning:", ("Binary '$1' was already installed from source" & " directory. Will be overwritten.") % bin, Warning, @@ -373,33 +399,28 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Set up a symlink. let symlinkDest = pkgDestDir / binDest let symlinkFilename = options.getBinDir() / bin.extractFilename - for filename in setupBinSymlink(symlinkDest, symlinkFilename, options): - binariesInstalled.incl(filename) - - let vcsRevision = getVcsRevisionFromDir(dir) + binariesInstalled.incl( + setupBinSymlink(symlinkDest, symlinkFilename, options)) - # Save a nimblemeta.json file. - saveNimbleMeta(pkgDestDir, url, vcsRevision, filesInstalled, - binariesInstalled) + # Update package path to point to installed directory rather than the temp + # directory. + pkgInfo.myPath = dest + pkgInfo.files = filesInstalled.toSeq + pkgInfo.binaries = binariesInstalled.toSeq - # Save the nimble data (which might now contain reverse deps added in - # processDeps). + saveMetaData(pkgInfo.metaData, pkgDestDir) saveNimbleData(options) - - # update package path to point to installed directory rather than the temp directory - pkgInfo.myPath = dest else: display("Warning:", "Skipped copy in project local deps mode", Warning) pkgInfo.isInstalled = true - # Return the dependencies of this package (mainly for paths). - result.deps.add pkgInfo - result.pkg = pkgInfo - display("Success:", pkgInfo.name & " installed successfully.", Success, HighPriority) + result.deps.incl pkgInfo + result.pkg = pkgInfo + # Run post-install hook now that package is installed. The `execHook` proc # executes the hook defined in the CWD, so we set it to where the package # has been installed. @@ -629,10 +650,10 @@ proc list(options: Options) = proc listInstalled(options: Options) = var h = initOrderedTable[string, seq[string]]() let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) - for x in pkgs.items(): + for pkg in pkgs: let - pName = x.pkginfo.name - pVer = x.pkginfo.specialVersion + pName = pkg.name + pVer = pkg.specialVersion if not h.hasKey(pName): h[pName] = @[] var s = h[pName] add(s, pVer) @@ -664,14 +685,14 @@ proc listPaths(options: Options) = for name, version, _ in options.action.packages.items: var installed: seq[VersionAndPath] = @[] # There may be several, list all available ones and sort by version. - for x in pkgs.items(): + for pkg in pkgs: let - pName = x.pkginfo.name - pVer = x.pkginfo.specialVersion + pName = pkg.name + pVer = pkg.specialVersion if name == pName: var v: VersionAndPath v.version = newVersion(pVer) - v.path = x.pkginfo.getRealDir() + v.path = pkg.getRealDir() installed.add(v) if installed.len > 0: @@ -761,7 +782,7 @@ proc dump(options: Options) = fn "installFiles", p.installFiles fn "installExt", p.installExt fn "requires", p.requires - fn "bin", toSeq(p.bin.keys) + fn "bin", toSeq(p.bin.values) fn "binDir", p.binDir fn "srcDir", p.srcDir fn "backend", p.backend @@ -976,12 +997,8 @@ proc uninstall(options: Options) = raise NimbleQuit(msg: "") for pkg in pkgsToDelete: - # If we reach this point then the package can be safely removed. - - # removeRevDep needs the package dependency info, so we can't just pass - # a minimal pkg info. - removeRevDep(options.nimbleData, pkg.toFullInfo(options)) - removePkgDir(pkg.getPkgDest(options), options) + let pkgInfo = pkg.toFullInfo(options) + removePackage(pkgInfo, options) display("Removed", "$1 ($2)" % [pkg.name, $pkg.specialVersion], Success, HighPriority) @@ -1030,16 +1047,10 @@ proc developFromDir(dir: string, options: Options) = # Don't link if project local deps mode and "developing" the top level package if not (options.localdeps and options.isInstallingTopLevel(dir)): - # This is similar to the code in `installFromDir`, except that we - # *consciously* not worry about the package's binaries. - let pkgDestDir = pkgInfo.getPkgDest(options) - if fileExists(pkgDestDir / nimbleDataFile.name): - let msg = "$1@$2 already exists. Overwrite?" % - [pkgInfo.name, pkgInfo.specialVersion] - if not options.prompt(msg): - raise NimbleQuit(msg: "") - removePkgDir(pkgDestDir, options) + if not promptRemovePackageIfExists(pkgInfo, options): + return + let pkgDestDir = pkgInfo.getPkgDest(options) createDir(pkgDestDir) # The .nimble-link file contains the path to the real .nimble file, # and a secondary path to the source directory of the package. @@ -1054,11 +1065,12 @@ proc developFromDir(dir: string, options: Options) = ) writeNimbleLink(nimbleLinkPath, nimbleLink) - # Save a nimblemeta.json file. - saveNimbleMeta(pkgDestDir, dir, getVcsRevisionFromDir(dir), nimbleLinkPath) + # Fill package meta data + pkgInfo.url = "file://" & dir + pkgInfo.files.add nimbleLinkPath + pkgInfo.isLink = true - # Save the nimble data (which might now contain reverse deps added in - # processDeps). + saveMetaData(pkgInfo.metaData, pkgDestDir) saveNimbleData(options) display("Success:", (pkgInfo.name & " linked successfully to '$1'.") % @@ -1200,7 +1212,7 @@ proc run(options: Options) = if binary.len == 0: raiseNimbleError("Please specify a binary to run") - if binary notin toSeq(pkgInfo.bin.keys): + if binary notin pkgInfo.bin: raiseNimbleError( "Binary '$#' is not defined in '$#' package." % [binary, pkgInfo.name] ) @@ -1213,7 +1225,6 @@ proc run(options: Options) = displayDebug("Executing", cmd) cmd.execCmd.quit - proc doAction(options: var Options) = if options.showHelp: writeHelp() diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index f468020c..3485077d 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -12,7 +12,7 @@ proc raiseChecksumError*(name, version, vcsRevision, var error = newException(ChecksumError, fmt""" Downloaded package checksum does not correspond to that in the lock file: - Package: {name}@v{version}@r{vcsRevision} + Package: {name}@v.{version}@r.{vcsRevision} Checksum: {checksum} Expected checksum: {expectedChecksum} """) diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index c421d7d3..2ecb5376 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -24,13 +24,6 @@ type ndjkRevDepVersion = "version" ndjkRevDepChecksum = "checksum" - PackageMetaDataJsonKeys* = enum - pmdjkUrl = "url" - pmdjkVcsRevision = "vcsRevision" - pmdjkFiles = "files" - pmdjkBinaries = "binaries" - pmdjkIsLink = "isLink" - const nimbleVersion* = "0.13.1" nimbleDataFile* = (name: "nimbledata.json", version: "0.1.0") diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 879a8686..03536df0 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -650,7 +650,7 @@ proc getCompilationBinary*(options: Options, pkgInfo: PackageInfo): Option[strin if optRunFile.get("").len > 0: optRunFile.get() elif pkgInfo.bin.len == 1: - toSeq(pkgInfo.bin.keys)[0] + toSeq(pkgInfo.bin.values)[0] else: "" diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 58391ba7..1196cffe 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -9,8 +9,12 @@ from net import SslError from compiler/nimblecmd import getPathVersionChecksum # Local imports -import version, tools, common, options, cli, config, checksum, - packageinfotypes, lockfile +import version, tools, common, options, cli, config, lockfile, packageinfotypes, + packagemetadata + +proc hasMetaData*(pkgInfo: PackageInfo): bool = + # if the package info has loaded meta data its files list have to be not empty + pkgInfo.files.len > 0 proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = ## Splits ``pkgpath`` in the format @@ -23,60 +27,12 @@ proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = return getNameVersionChecksum(pkgPath.splitPath.head) getPathVersionChecksum(pkgpath.splitPath.tail) -proc readMetaData*(path: string, silent = false): MetaData = - ## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json`` - var bmeta = path / nimbleDataFile.name - if not fileExists(bmeta) and not silent: - result.url = "" - display("Warning:", "No nimblemeta.json file found in " & path, - Warning, HighPriority) - return - # TODO: Make this an error. - let cont = readFile(bmeta) - let jsonmeta = parseJson(cont) - result.url = jsonmeta[$pmdjkUrl].str - result.vcsRevision = jsonmeta[$pmdjkVcsRevision].str - -proc getVcsRevision(dir: string): string = - # If the directory is under version control get the revision from it. - result = getVcsRevisionFromDir(dir) - if result.len > 0: return - # Otherwise this probably is directory in the local cache and we try to get it - # from the nimble package meta data json file. - result = readMetaData(dir, true).vcsRevision - proc initPackageInfo*(filePath: string): PackageInfo = let (fileDir, fileName, _) = filePath.splitFile - let (_, pkgVersion, pkgChecksum) = filePath.getNameVersionChecksum - - result.myPath = filePath - result.specialVersion = "" - result.nimbleTasks.init() - result.preHooks.init() - result.postHooks.init() - # reasonable default: + result.myPath = filePath result.name = fileName - result.version = pkgVersion - result.author = "" - result.description = "" - result.license = "" - result.skipDirs = @[] - result.skipFiles = @[] - result.skipExt = @[] - result.installDirs = @[] - result.installFiles = @[] - result.installExt = @[] - result.requires = @[] - result.foreignDeps = @[] - result.bin = initTable[string, string]() - result.srcDir = "" - result.binDir = "" result.backend = "c" result.lockedDependencies = getLockedDependencies(fileDir) - result.checksum = - if pkgChecksum.len > 0: pkgChecksum - else: calculatePackageSha1Checksum(fileDir) - result.vcsRevision = getVcsRevision(fileDir) proc toValidPackageName*(name: string): string = result = "" @@ -323,29 +279,34 @@ proc findNimbleFile*(dir: string; error: bool): string = display("Warning:", msg, Warning, HighPriority) display("Hint:", hintMsg, Warning, HighPriority) -proc getInstalledPackageMin*(packageDir, nimbleFilePath: string): PackageInfo = - let (name, version, checksum) = getNameVersionChecksum(packageDir) +proc setNameVersionChecksum*(pkgInfo: var PackageInfo, pkgDir: string) = + let (name, version, checksum) = getNameVersionChecksum(pkgDir) + pkgInfo.name = name + if pkgInfo.version.len == 0: + # if there is no previously set version from the `.nimble` file + pkgInfo.version = version + pkgInfo.specialVersion = version + pkgInfo.checksum = checksum + +proc getInstalledPackageMin*(pkgDir, nimbleFilePath: string): PackageInfo = result = initPackageInfo(nimbleFilePath) - result.name = name - result.version = version - result.specialVersion = version - result.checksum = checksum + setNameVersionChecksum(result, pkgDir) result.isMinimal = true result.isInstalled = true - let nimbleFileDir = nimbleFilePath.splitFile().dir - result.isLinked = cmpPaths(nimbleFileDir, packageDir) != 0 - # Read the package's 'srcDir' (this is stored in the .nimble-link so - # we can easily grab it) - if result.isLinked: - let nimbleLinkPath = packageDir / name.addFileExt("nimble-link") + fillMetaData(result, pkgDir, false) + + if result.isLink: + # Read the package's 'srcDir' (this is stored in the .nimble-link so we can + # easily grab it). + let nimbleLinkPath = pkgDir / result.name.addFileExt("nimble-link") let realSrcPath = readNimbleLink(nimbleLinkPath).packageDir + let nimbleFileDir = nimbleFilePath.splitFile().dir assert realSrcPath.startsWith(nimbleFileDir) result.srcDir = realSrcPath.replace(nimbleFileDir) result.srcDir.removePrefix(DirSep) -proc getInstalledPkgsMin*(libsDir: string, options: Options): - seq[PackageInfoAndMetaData] = +proc getInstalledPkgsMin*(libsDir: string, options: Options): seq[PackageInfo] = ## Gets a list of installed packages. The resulting package info is ## minimal. This has the advantage that it does not depend on the ## ``packageparser`` module, and so can be used by ``nimscriptwrapper``. @@ -356,9 +317,8 @@ proc getInstalledPkgsMin*(libsDir: string, options: Options): if kind == pcDir: let nimbleFile = findNimbleFile(path, false) if nimbleFile != "": - let meta = readMetaData(path) let pkg = getInstalledPackageMin(path, nimbleFile) - result.add((pkg, meta)) + result.add pkg proc withinRange*(pkgInfo: PackageInfo, verRange: VersionRange): bool = ## Determines whether the specified package's version is within the @@ -378,9 +338,8 @@ proc resolveAlias*(dep: PkgTuple, options: Options): PkgTuple = # no alias is present. result.name = pkg.name -proc findPkg*(pkglist: seq[PackageInfoAndMetaData], - dep: PkgTuple, - r: var PackageInfo): bool = +proc findPkg*(pkglist: seq[PackageInfo], dep: PkgTuple, + r: var PackageInfo): 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 @@ -388,29 +347,28 @@ proc findPkg*(pkglist: seq[PackageInfoAndMetaData], ## ## **Note**: dep.name here could be a URL, hence the need for pkglist.meta. for pkg in pkglist: - if cmpIgnoreStyle(pkg.pkginfo.name, dep.name) != 0 and - cmpIgnoreStyle(pkg.meta.url, dep.name) != 0: continue - if withinRange(pkg.pkgInfo, dep.ver): - let isNewer = newVersion(r.version) < newVersion(pkg.pkginfo.version) + if cmpIgnoreStyle(pkg.name, dep.name) != 0 and + cmpIgnoreStyle(pkg.url, dep.name) != 0: continue + if withinRange(pkg, dep.ver): + let isNewer = newVersion(r.version) < newVersion(pkg.version) if not result or isNewer: - r = pkg.pkginfo + r = pkg result = true -proc findAllPkgs*(pkglist: seq[PackageInfoAndMetaData], - dep: PkgTuple): seq[PackageInfo] = +proc findAllPkgs*(pkglist: seq[PackageInfo], dep: PkgTuple): seq[PackageInfo] = ## Searches ``pkglist`` for packages of which version is within the range ## of ``dep.ver``. This is similar to ``findPkg`` but returns multiple ## packages if multiple are found. result = @[] for pkg in pkglist: - if cmpIgnoreStyle(pkg.pkgInfo.name, dep.name) != 0 and - cmpIgnoreStyle(pkg.meta.url, dep.name) != 0: continue - if withinRange(pkg.pkgInfo, dep.ver): - result.add pkg.pkginfo + if cmpIgnoreStyle(pkg.name, dep.name) != 0 and + cmpIgnoreStyle(pkg.url, dep.name) != 0: continue + if withinRange(pkg, dep.ver): + result.add pkg proc getRealDir*(pkgInfo: PackageInfo): string = ## Returns the directory containing the package source files. - if pkgInfo.srcDir != "" and (not pkgInfo.isInstalled or pkgInfo.isLinked): + if pkgInfo.srcDir != "" and (not pkgInfo.isInstalled or pkgInfo.isLink): result = pkgInfo.mypath.splitFile.dir / pkgInfo.srcDir else: result = pkgInfo.mypath.splitFile.dir diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 580400d1..79f90b47 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -5,12 +5,18 @@ import sets, tables import version, lockfile type + PackageMetaData* = object + url*: string + vcsRevision*: string + files*: seq[string] + binaries*: seq[string] + isLink*: bool + PackageInfo* = object myPath*: string ## The path of this .nimble file isNimScript*: bool ## Determines if this pkg info was read from a nims file isMinimal*: bool isInstalled*: bool ## Determines if the pkg this info belongs to is installed - isLinked*: bool ## Determines if the pkg this info belongs to has been linked via `develop` nimbleTasks*: HashSet[string] ## All tasks defined in the Nimble file postHooks*: HashSet[string] ## Useful to know so that Nimble doesn't execHook unnecessarily preHooks*: HashSet[string] @@ -36,7 +42,7 @@ type foreignDeps*: seq[string] lockedDependencies*: LockFileDependencies checksum*: string - vcsRevision*: string ## This is git or hg commit sha1. + metaData*: PackageMetaData Package* = object ## Definition of package from packages.json. # Required fields in a package. @@ -52,14 +58,40 @@ type web*: string # Info url for humans. alias*: string ## A name of another package, that this package aliases. - MetaData* = object - url*: string - vcsRevision*: string - NimbleLink* = object nimbleFilePath*: string packageDir*: string PackageBasicInfo* = tuple[name, version, checksum: string] PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] - PackageInfoAndMetaData* = tuple[pkginfo: PackageInfo, meta: MetaData] + +template url*(packageInfo: PackageInfo): untyped = + packageInfo.metaData.url + +template `url=`*(packageInfo: var PackageInfo, urlParam: string) = + packageInfo.metaData.url = urlParam + +template vcsRevision*(packageInfo: PackageInfo): untyped = + packageInfo.metaData.vcsRevision + +template `vcsRevision=`*(packageInfo: var PackageInfo, vcsRevisionParam: string) = + packageInfo.metaData.vcsRevision = vcsRevisionParam + +template files*(packageInfo: PackageInfo): untyped = + packageInfo.metaData.files + +template `files=`*(packageInfo: var PackageInfo, filesParam: seq[string]) = + packageInfo.metaData.files = filesParam + +template binaries*(packageInfo: PackageInfo): untyped = + packageInfo.metaData.binaries + +template `binaries=`*(packageInfo: var PackageInfo, + binariesParam: seq[string]) = + packageInfo.metaData.binaries = binariesParam + +template isLink*(packageInfo: PackageInfo): untyped = + packageInfo.metaData.isLink + +template `isLink=`*(packageInfo: PackageInfo, isLinkParam: bool) = + packageInfo.metaData.isLink = isLinkParam diff --git a/src/nimblepkg/packageinstaller.nim b/src/nimblepkg/packageinstaller.nim index 758b40e6..1837e558 100644 --- a/src/nimblepkg/packageinstaller.nim +++ b/src/nimblepkg/packageinstaller.nim @@ -1,17 +1,13 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, strutils, sets, json +import os, strutils # Local imports -import cli, options, tools, common +import cli, options when defined(windows): - import version - -when not declared(initHashSet) or not declared(toHashSet): import common -when defined(windows): # This is just for Win XP support. # TODO: Drop XP support? from winlean import WINBOOL, DWORD @@ -75,40 +71,3 @@ proc setupBinSymlink*(symlinkDest, symlinkFilename: string, result.add bashDest.extractFilename else: {.error: "Sorry, your platform is not supported.".} - -proc saveNimbleMeta*(pkgDestDir, url, vcsRevision: string, - filesInstalled, bins: HashSet[string], - isLink: bool = false) = - ## Saves the specified data into a ``nimblemeta.json`` file inside - ## ``pkgDestDir``. - ## - ## filesInstalled - A list of absolute paths to files which have been - ## installed. - ## bins - A list of binary filenames which have been installed for this - ## package. - ## - ## isLink - Determines whether the installed package is a .nimble-link. - var nimblemeta = %{$pmdjkUrl: %url} - if vcsRevision.len > 0: - nimblemeta[$pmdjkVcsRevision] = %vcsRevision - let files = newJArray() - nimblemeta[$pmdjkFiles] = files - for file in filesInstalled: - files.add(%changeRoot(pkgDestDir, "", file)) - let binaries = newJArray() - nimblemeta[$pmdjkBinaries] = binaries - for bin in bins: - binaries.add(%bin) - nimblemeta[$pmdjkIsLink] = %isLink - writeFile(pkgDestDir / nimbleDataFile.name, nimblemeta.pretty) - -proc saveNimbleMeta*(pkgDestDir, pkgDir, vcsRevision, nimbleLinkPath: string) = - ## Overload of saveNimbleMeta for linked (.nimble-link) packages. - ## - ## pkgDestDir - The directory where the package has been installed. - ## For example: ~/.nimble/pkgs/jester-#head/ - ## - ## pkgDir - The directory where the original package files are. - ## For example: ~/projects/jester/ - saveNimbleMeta(pkgDestDir, "file://" & pkgDir, vcsRevision, - toHashSet[string]([nimbleLinkPath]), initHashSet[string](), true) diff --git a/src/nimblepkg/packagemetadata.nim b/src/nimblepkg/packagemetadata.nim new file mode 100644 index 00000000..56284d9a --- /dev/null +++ b/src/nimblepkg/packagemetadata.nim @@ -0,0 +1,48 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import json, os, strformat +import common, packageinfotypes, cli, tools + +type + MetaDataError* = object of NimbleError + +const + packageMetaDataFileName* = "nimblemeta.json" + +proc saveMetaData*(metaData: PackageMetaData, dirName: string) = + ## Saves some important data to file in the package installation directory. + var metaDataWithChangedPaths = metaData + for i, file in metaData.files: + metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file) + let json = %metaDataWithChangedPaths + writeFile(dirName / packageMetaDataFileName, json.pretty) + +proc loadMetaData(dirName: string, raiseIfNotFound: bool): PackageMetaData = + ## Returns package meta data read from file in package installation directory + let fileName = dirName / packageMetaDataFileName + if fileExists(fileName): + let json = parseFile(fileName) + result = json.to(result.typeof) + elif raiseIfNotFound: + raise newException(MetaDataError, + &"No {packageMetaDataFileName} file found in {dirName}") + else: + display("Warning:", &"No {packageMetaDataFileName} file found in {dirName}", + Warning, HighPriority) + +proc fillMetaData*(packageInfo: var PackageInfo, dirName: string, + raiseIfNotFound: bool) = + # Save the VCS revision possibly previously obtained in `initPackageInfo` from + # the `.nimble` file directory to not be overridden from this read by meta + # data file in the case the package is in develop mode. + let vcsRevision = packageInfo.vcsRevision + + packageInfo.metaData = loadMetaData(dirName, raiseIfNotFound) + + if packageInfo.isLink: + # If this is a linked package the real VCS revision from the `.nimble` file + # directory obtained in `initPackageInfo` is the actual one, but not this + # written in the package meta data in the time of the linking of the + # package. + packageInfo.vcsRevision = vcsRevision diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index e54faf34..b046488b 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -3,8 +3,8 @@ import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map, toSeq -import common, version, tools, nimscriptwrapper, options, cli, - packageinfo, packageinfotypes +import common, version, tools, nimscriptwrapper, options, cli, packagemetadata, + packageinfo, packageinfotypes, checksum ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` ## because it depends on ``nimscriptwrapper`` (``nimscriptwrapper`` also @@ -100,7 +100,7 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = ## https://github.com/nim-lang/nimble/issues/144 let realDir = pkgInfo.getRealDir() - normalizedBinNames = toSeq(pkgInfo.bin.keys).map( + normalizedBinNames = toSeq(pkgInfo.bin.values).map( (x) => x.changeFileExt("").toLowerAscii() ) correctDir = @@ -326,8 +326,8 @@ proc inferInstallRules(pkgInfo: var PackageInfo, options: Options) = if fileExists(pkgInfo.getRealDir() / pkgInfo.name.addFileExt("nim")): pkgInfo.installFiles.add(pkgInfo.name.addFileExt("nim")) -proc readPackageInfo(result: var PackageInfo, nf: NimbleFile, options: Options, - onlyMinimalInfo=false) = +proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): + PackageInfo = ## Reads package info from the specified Nimble file. ## ## Attempts to read it using the "old" Nimble ini format first, if that @@ -386,17 +386,19 @@ proc readPackageInfo(result: var PackageInfo, nf: NimbleFile, options: Options, " " & exc.msg & "." raise newException(NimbleError, msg) - # By default specialVersion is the same as version. - result.specialVersion = result.version - - # Only attempt to read a special version if `nf` is inside the $nimbleDir. - if nf.startsWith(options.getNimbleDir()): - # The package directory name may include a "special" version - # (example #head). If so, it is given higher priority and therefore - # overwrites the .nimble file's version. - let version = parseVersionRange(result.version) - if version.kind == verSpecial: - result.specialVersion = result.version + let fileDir = nf.splitFile().dir + if not fileDir.startsWith(options.getPkgsDir()): + # If the `.nimble` file is not in the installation directory but in the + # package repository we have to get its VCS revision and to calculate its + # checksum. + result.vcsRevision = getVcsRevisionFromDir(fileDir) + result.checksum = calculatePackageSha1Checksum(fileDir) + # By default specialVersion is the same as version. + result.specialVersion = result.version + else: + # Otherwise we have to get its name, special version and checksum from the + # package directory. + setNameVersionChecksum(result, fileDir) # Apply rules to infer which files should/shouldn't be installed. See #469. inferInstallRules(result, options) @@ -412,7 +414,7 @@ proc readPackageInfo(result: var PackageInfo, nf: NimbleFile, options: Options, proc validate*(file: NimbleFile, options: Options, error: var ValidationError, pkgInfo: var PackageInfo): bool = try: - pkgInfo.readPackageInfo(file, options) + pkgInfo = readPackageInfo(file, options) except ValidationError as exc: error = exc[] return false @@ -424,7 +426,7 @@ proc getPkgInfoFromFile*(file: NimbleFile, options: Options): PackageInfo = ## object. Any validation errors are handled and displayed as warnings. var info: PackageInfo try: - info.readPackageInfo(file, options) + info = readPackageInfo(file, options) except ValidationError: let exc = (ref ValidationError)(getCurrentException()) if exc.warnAll: @@ -440,8 +442,7 @@ proc getPkgInfo*(dir: string, options: Options): PackageInfo = let nimbleFile = findNimbleFile(dir, true) result = getPkgInfoFromFile(nimbleFile, options) -proc getInstalledPkgs*(libsDir: string, options: Options): - seq[PackageInfoAndMetaData] = +proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfo] = ## Gets a list of installed packages. ## ## ``libsDir`` is in most cases: ~/.nimble/pkgs/ @@ -453,7 +454,7 @@ proc getInstalledPkgs*(libsDir: string, options: Options): proc createErrorMsg(tmplt, path, msg: string): string = let (name, version, checksum) = getNameVersionChecksum(path) - let fullVersion = if checksum.len > 0: version & '-' & checksum + let fullVersion = if checksum.len > 0: version & "@c." & checksum else: version return tmplt % [name, fullVersion, msg] @@ -464,10 +465,10 @@ proc getInstalledPkgs*(libsDir: string, options: Options): if kind == pcDir: let nimbleFile = findNimbleFile(path, false) if nimbleFile != "": - let meta = readMetaData(path) var pkg: PackageInfo try: - pkg.readPackageInfo(nimbleFile, options, onlyMinimalInfo=false) + pkg = readPackageInfo(nimbleFile, options, onlyMinimalInfo=false) + fillMetaData(pkg, path, false) except ValidationError: let exc = (ref ValidationError)(getCurrentException()) exc.msg = createErrorMsg(validationErrorMsg, path, exc.msg) @@ -486,20 +487,19 @@ proc getInstalledPkgs*(libsDir: string, options: Options): raise exc pkg.isInstalled = true - pkg.isLinked = - cmpPaths(nimbleFile.splitFile().dir, path) != 0 - result.add((pkg, meta)) + result.add pkg proc isNimScript*(nf: string, options: Options): bool = - var p: PackageInfo - p.readPackageInfo(nf, options) - result = p.isNimScript + readPackageInfo(nf, options).isNimScript proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = if pkg.isMinimal: result = getPkgInfoFromFile(pkg.mypath, options) result.isInstalled = pkg.isInstalled - result.isLinked = pkg.isLinked + result.isLink = pkg.isLink + result.specialVersion = pkg.specialVersion + if pkg.hasMetaData: + result.metaData = pkg.metaData else: return pkg diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index f8697746..72f20452 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -2,7 +2,9 @@ # BSD License. Look at license.txt for more info. # # Various miscellaneous utility functions reside here. -import osproc, pegs, strutils, os, uri, sets, json, parseutils, strformat +import osproc, pegs, strutils, os, uri, sets, json, parseutils, strformat, + sequtils + import common, version, cli, options from net import SslCVerifyMode, newContext, SslContext @@ -182,8 +184,8 @@ proc getNimbleUserTempDir*(): string = proc getVcsRevisionFromDir*(dir: string): string = - ## Returns current revision number of HEAD if dir is inside VCS, or nil in - ## case of failure. + ## Returns current revision number of HEAD if dir is inside VCS, or an empty + ## string in case of failure. template tryToGetRevision(command: string): untyped = try: @@ -196,6 +198,9 @@ proc getVcsRevisionFromDir*(dir: string): string = tryToGetRevision("git -C " & quoteShell(dir) & " rev-parse HEAD") tryToGetRevision("hg --cwd " & quoteShell(dir) & " id -i") +proc isEmptyDir*(dir: string): bool = + toSeq(walkDirRec(dir)).len == 0 + proc newSSLContext*(disabled: bool): SslContext = var sslVerifyMode = CVerifyPeer if disabled: diff --git a/tests/tester.nim b/tests/tester.nim index 23d132a6..d0cb3498 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1002,6 +1002,7 @@ suite "misc tests": check execNimble("recurse").exitCode == QuitSuccess test "picks #head when looking for packages": + removeDir installDir cd "versionClashes" / "aporiaScenario": let (output, exitCode) = execNimbleYes("install", "--verbose") checkpoint output @@ -1064,7 +1065,6 @@ suite "misc tests": test "can list": check execNimble("list").exitCode == QuitSuccess - check execNimble("list", "-i").exitCode == QuitSuccess suite "issues": From f05a9aa8ef9834845a71ba65ae01a3c0c87dda9a Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 13 Mar 2020 16:36:48 +0200 Subject: [PATCH 12/73] Remove `vcsRevision` field from `PkgTuple` tuple The `vcsRevision` string field was mistakenly added to the `PkgTuple` tuple in a previous commit, possibly because it was part of some temporary decision, and it is not used. Related to nim-lang/nimble#127 --- src/nimble.nim | 10 +++++----- src/nimblepkg/options.nim | 4 ++-- src/nimblepkg/reversedeps.nim | 7 +++---- src/nimblepkg/version.nim | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 9703424b..b5eeec57 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -85,7 +85,7 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = if not found: display("Installing", $resolvedDep, priority = HighPriority) - let toInstall = @[(resolvedDep.name, resolvedDep.ver, "")] + let toInstall = @[(resolvedDep.name, resolvedDep.ver)] let (pkgs, installedPkg) = install(toInstall, options, doPrompt = false, first = false, @@ -239,7 +239,7 @@ proc reinstallSymlinksForOlderVersion(pkgDir: string, options: Options) = let (pkgName, _, _) = getNameVersionChecksum(pkgDir) let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) var newPkgInfo: PackageInfo - if pkgList.findPkg((pkgName, newVRAny(), ""), newPkgInfo): + if pkgList.findPkg((pkgName, newVRAny()), newPkgInfo): newPkgInfo = newPkgInfo.toFullInfo(options) for bin in newPkgInfo.binaries: let symlinkDest = newPkgInfo.getOutputDir(bin) @@ -537,7 +537,7 @@ proc install(packages: seq[PkgTuple], " like to try installing '$1@#head' (latest unstable)?") % [pv.name, $downloadVersion]) if promptResult: - let toInstall = @[(pv.name, headVer.toVersionRange(), "")] + let toInstall = @[(pv.name, headVer.toVersionRange())] result = install(toInstall, options, doPrompt, first, fromLockFile = false) else: @@ -682,7 +682,7 @@ proc listPaths(options: Options) = var errors = 0 let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) - for name, version, _ in options.action.packages.items: + for name, version in options.action.packages.items: var installed: seq[VersionAndPath] = @[] # There may be several, list all available ones and sort by version. for pkg in pkgs: @@ -753,7 +753,7 @@ proc dump(options: Options) = # jsonutils.toJson would work but is only available since 1.3.5, so we # do it manually. j[key] = newJArray() - for (name, ver, _) in val: + for (name, ver) in val: j[key].add %{ "name": % name, # we serialize both: `ver` may be more convenient for tooling diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 03536df0..9e7bba49 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -378,9 +378,9 @@ proc parseArgument*(key: string, result: var Options) = let (pkgName, pkgVer) = (key[0 .. i-1], key[i+1 .. key.len-1]) if pkgVer.len == 0: raise newException(NimbleError, "Version range expected after '@'.") - result.action.packages.add((pkgName, pkgVer.parseVersionRange(), "")) + result.action.packages.add((pkgName, pkgVer.parseVersionRange())) else: - result.action.packages.add((key, VersionRange(kind: verAny), "")) + result.action.packages.add((key, VersionRange(kind: verAny))) of actionRefresh: result.action.optionalURL = key of actionSearch: diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 20917be4..96ed22d9 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -71,7 +71,6 @@ proc getRevDepTups*(options: Options, pkg: PackageInfo): seq[PkgTuple] = let pkgTup = ( name: pkg[$ndjkRevDepName].getStr(), ver: parseVersionRange(pkg[$ndjkRevDepVersion].getStr()), - vcsRevision: "" ) var pkgInfo: PackageInfo if not findPkg(pkgList, pkgTup, pkgInfo): @@ -109,9 +108,9 @@ when isMainModule: isMinimal: false, name: "nimforum", specialVersion: "0.1.0", - requires: @[("jester", parseVersionRange("0.1.0"), ""), - ("captcha", parseVersionRange("1.0.0"), ""), - ("auth", parseVersionRange("#head"), "")], + requires: @[("jester", parseVersionRange("0.1.0")), + ("captcha", parseVersionRange("1.0.0")), + ("auth", parseVersionRange("#head"))], checksum: "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2", ) diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index 0fcfb62d..e444b902 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -32,7 +32,7 @@ type nil ## Tuple containing package name and version range. - PkgTuple* = tuple[name: string, ver: VersionRange, vcsRevision: string] + PkgTuple* = tuple[name: string, ver: VersionRange] ParseVersionError* = object of ValueError From d2a6e18eb2e305edfb05f586b0da9a3cc6d41fd9 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 13 Mar 2020 17:51:25 +0200 Subject: [PATCH 13/73] Fix a regression with a package VCS revision There was a regression with the setting of the right VCS revision to the `PackageInfo` object when it is being transfered to a full info object. The regression was caused by the fact that, when the package is installed and it is not a linked package, then the VCS revision cannot be obtained from the `.nimble` file directory which is not under version control, but the revision written in the package meta data should be used. Related to nim-lang/nimble#127 --- src/nimblepkg/packageparser.nim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index b046488b..654c6967 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -500,6 +500,12 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = result.specialVersion = pkg.specialVersion if pkg.hasMetaData: result.metaData = pkg.metaData + if pkg.isInstalled and not pkg.isLink: + # If this is an installed package and it is not a linked package, then + # there should not be a VCS revision read from the package directory and + # this read previously from the meta data should be used. + assert result.vcsRevision.len == 0 + result.vcsRevision = pkg.vcsRevision else: return pkg From 9d24ee3ad1b82bc5e50d167bcdde0058b9060234 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 17 Mar 2020 05:03:20 +0200 Subject: [PATCH 14/73] Add `aliasThis` template Added `aliasThis` template which automatically generates getter and setter template accessors for the fields of an object nested into another object directly via the embedding one. It is used for automatically generating accessors to the fields of the `PackageMetaData` object directly via the `PackageInfo` object. Related to nim-lang/nimble#127 --- .gitignore | 1 + src/nimblepkg/aliasthis.nim | 143 +++++++++++++++++++++++++++++ src/nimblepkg/packageinfotypes.nim | 33 +------ 3 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 src/nimblepkg/aliasthis.nim diff --git a/.gitignore b/.gitignore index 1c4457de..0614c7d6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ nimcache/ # executables from test and build /nimble +src/nimblepkg/aliasthis src/nimblepkg/checksum src/nimblepkg/cli src/nimblepkg/common diff --git a/src/nimblepkg/aliasthis.nim b/src/nimblepkg/aliasthis.nim new file mode 100644 index 00000000..7877b303 --- /dev/null +++ b/src/nimblepkg/aliasthis.nim @@ -0,0 +1,143 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import macros + +template delegateField*(ObjectType: type[object], + objectField, accessor: untyped) = + ## Defines two additional templates for getter and setter of nested object + ## field directly via the embedding object. The name of the nested object + ## field must match the name of the accessor. + ## + ## Sample usage: + ## + ## .. code-block:: nim + ## + ## type + ## Object1 = object + ## field1: int + ## + ## Object2 = object + ## field2: Object1 + ## + ## delegateField(Object2, field2, field1) + ## + ## var obj: Object2 + ## obj.field1 = 42 + ## echo obj.field1 # prints 42 + ## echo obj.field2.field1 # also prints 42 + + type AccessorType = ObjectType.default.objectField.accessor.typeOf + + template accessor*(obj: ObjectType): AccessorType = + obj.objectField.accessor + + template `accessor "="`*(obj: var ObjectType, value: AccessorType) = + obj.objectField.accessor = value + +func fields(Object: type[object]): seq[string] = + ## Collects the names of the fields of an object. + let obj = Object.default + for name, _ in obj.fieldPairs: + result.add name + +macro aliasThisImpl(dotExpression: typed, fields: static seq[string]): untyped = + ## Accepts a dot expressions of an object and an object's field of object type + ## and the names of the fields of the nested object. Iterates them and for + ## each one generates getter and setter template accessors directly via the + ## embedding object. + + dotExpression.expectKind nnkDotExpr + result = newStmtList() + let ObjectType = dotExpression[0] + let objectField = dotExpression[1] + for accessor in fields: + result.add newCall( + "delegateField", ObjectType, objectField, accessor.newIdentNode) + +template aliasThis*(dotExpression: untyped) = + ## Makes fields of an object nested in another object accessible via the + ## embedding one. Currently only Nim's non variant `object` types and only a + ## single level of nesting are supported. + ## + ## Sample usage: + ## + ## .. code-block:: nim + ## + ## type + ## Object1 = object + ## field1: int + ## + ## Object2 = object + ## field2: Object1 + ## + ## aliasThis Object2.field2 + ## + ## var obj: Object2 + ## obj.field1 = 42 + ## echo obj.field1 # prints 42 + ## echo obj.field2.field1 # also prints 42 + + aliasThisImpl(dotExpression, dotExpression.typeOf.fields) + +when isMainModule: + import unittest + import common + + type + Object1 = object + field11: float + field12: seq[int] + field13: int + + Object2 = object + field11: float # intentionally the name is the same as in Object1 + field22: Object1 + + aliasThis(Object2.field22) + + var obj = Object2( + field11: 3.14, + field22: Object1( + field11: 2.718, + field12: @[1, 1, 2, 3, 5, 8], + field13: 42)) + + # check access to the original value in both ways + check obj.field13 == 42 + check obj.field22.field13 == 42 + check obj.field12 == @[1, 1, 2, 3, 5, 8] + check obj.field22.field12 == @[1, 1, 2, 3, 5, 8] + + # check setter via an alias + obj.field13 = -obj.field13 + check obj.field13 == -42 + check obj.field22.field13 == -42 + + # check setter without an alias + obj.field22.field13 = 0 + check obj.field13 == 0 + check obj.field22.field13 == 0 + + # check procedure call via an alias + obj.field12.add 13 + check obj.field12 == @[1, 1, 2, 3, 5, 8, 13] + check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13] + + # check procedure call without an alias + obj.field22.field12.add 21 + check obj.field12 == @[1, 1, 2, 3, 5, 8, 13, 21] + check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13, 21] + + # check that the priority is on the not aliased field + check obj.field11 == 3.14 + # check that the aliased, but shadowed field is still accessible + check obj.field22.field11 == 2.718 + + # check that setting via matching field name does not override + # the shadowed field + obj.field11 = 0 + check obj.field11 == 0 + check obj.field22.field11 == 2.718 + + reportUnitTestSuccess() diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 79f90b47..a1963256 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import sets, tables -import version, lockfile +import version, lockfile, aliasthis type PackageMetaData* = object @@ -65,33 +65,4 @@ type PackageBasicInfo* = tuple[name, version, checksum: string] PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] -template url*(packageInfo: PackageInfo): untyped = - packageInfo.metaData.url - -template `url=`*(packageInfo: var PackageInfo, urlParam: string) = - packageInfo.metaData.url = urlParam - -template vcsRevision*(packageInfo: PackageInfo): untyped = - packageInfo.metaData.vcsRevision - -template `vcsRevision=`*(packageInfo: var PackageInfo, vcsRevisionParam: string) = - packageInfo.metaData.vcsRevision = vcsRevisionParam - -template files*(packageInfo: PackageInfo): untyped = - packageInfo.metaData.files - -template `files=`*(packageInfo: var PackageInfo, filesParam: seq[string]) = - packageInfo.metaData.files = filesParam - -template binaries*(packageInfo: PackageInfo): untyped = - packageInfo.metaData.binaries - -template `binaries=`*(packageInfo: var PackageInfo, - binariesParam: seq[string]) = - packageInfo.metaData.binaries = binariesParam - -template isLink*(packageInfo: PackageInfo): untyped = - packageInfo.metaData.isLink - -template `isLink=`*(packageInfo: PackageInfo, isLinkParam: bool) = - packageInfo.metaData.isLink = isLinkParam +aliasThis PackageInfo.metaData From 97ad89f73de306e5dc4955ed8920fc9786b22491 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 6 Apr 2020 15:56:09 +0300 Subject: [PATCH 15/73] Improve install from a lock file Some code improvements are added: - Install from a lock file now checks whether a meta data file exists in the directory to determine whether the package exists. If the meta data file does not exist, but the directory exists it prompts with a question whether to remove the entire directory and install the package again or to terminate the execution for manual investigation what wrong have happened. - If a lock file already exists on `nimble lock` command, now Nimble prompts whether to override it. - `toFullInfo` procedure is fixed to perform correctly in the case of missing meta data file for a linked package by not overriding the read by real package directory VCS revision string with an empty one. - A warning for inheriting directly from `Exception` object given by newer Nim versions is fixed by using `CatchableError` for base object of Nimble exceptions. Related to nim-lang/nimble#127 --- src/nimble.nim | 21 +++++++++++++++++---- src/nimblepkg/lockfile.nim | 11 ++++++----- src/nimblepkg/packageparser.nim | 16 +++++++++++----- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index b5eeec57..c55ab295 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -208,9 +208,11 @@ proc saveNimbleData(options: Options) = saveNimbleDataToDir(options.getNimbleDir(), options.nimbleData) proc promptRemoveEntirePackageDir(pkgDir: string, options: Options) = - display("Warning", - &"Unable to read {packageMetaDataFileName}: {getCurrentExceptionMsg()}", - Warning, HighPriority) + let exceptionMsg = getCurrentExceptionMsg() + let warningMsgEnd = if exceptionMsg.len > 0: &": {exceptionMsg}" else: "." + let warningMsg = &"Unable to read {packageMetaDataFileName}{warningMsgEnd}" + + display("Warning", warningMsg, Warning, HighPriority) if not options.prompt( &"Would you like to COMPLETELY remove ALL files in {pkgDir}?"): @@ -444,7 +446,11 @@ proc processLockedDependencies(packageInfo: PackageInfo, options: Options): for name, dep in packageInfo.lockedDependencies: let depDirName = packagesDir / fmt"{name}-{dep.version}-{dep.checksum.sha1}" - if not depDirName.dirExists: + if not existsFile(depDirName / packageMetaDataFileName): + if depDirName.existsDir: + promptRemoveEntirePackageDir(depDirName, options) + removeDir(depDirName) + let (url, metadata) = getUrlData(dep.url) let version = dep.version.parseVersionRange let subdir = metadata.getOrDefault("subdir") @@ -1195,8 +1201,15 @@ proc check(options: Options) = display("Failure:", "Validation failed", Error, HighPriority) quit(QuitFailure) +proc promptOverwriteLockFile(options: Options) = + let message = &"{lockFileName} already exists. Overwrite?" + if not options.prompt(message): + raise NimbleQuit(msg: "") + proc lock(options: Options) = let currentDir = getCurrentDir() + if lockFileExists(currentDir): + promptOverwriteLockFile(options) let packageInfo = getPkgInfo(currentDir, options) let dependencies = processDeps(packageInfo, options).toSeq.map( pkg => pkg.toFullInfo(options)) diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim index 73928aa2..fcafb7ad 100644 --- a/src/nimblepkg/lockfile.nim +++ b/src/nimblepkg/lockfile.nim @@ -22,10 +22,11 @@ type lfjkPackages = "packages" const - lockFile = (name: "nimble.lockfile", version: "0.1.0") + lockFileName* = "nimble.lockfile" + lockFileVersion = "0.1.0" proc lockFileExists*(dir: string): bool = - fileExists(dir / lockFile.name) + fileExists(dir / lockFileName) proc writeLockFile*(fileName: string, packages: LockFileDependencies, topologicallySortedOrder: seq[string]) = @@ -37,7 +38,7 @@ proc writeLockFile*(fileName: string, packages: LockFileDependencies, packagesJsonNode.add packageName, %packages[packageName] let mainJsonNode = %{ - $lfjkVersion: %lockFile.version, + $lfjkVersion: %lockFileVersion, $lfjkPackages: packagesJsonNode } @@ -45,13 +46,13 @@ proc writeLockFile*(fileName: string, packages: LockFileDependencies, proc writeLockFile*(packages: LockFileDependencies, topologicallySortedOrder: seq[string]) = - writeLockFile(lockFile.name, packages, topologicallySortedOrder) + writeLockFile(lockFileName, packages, topologicallySortedOrder) proc readLockFile*(filePath: string): LockFileDependencies = parseFile(filePath)[$lfjkPackages].to(result.typeof) proc readLockFileInDir*(dir: string): LockFileDependencies = - readLockFile(dir / lockFile.name) + readLockFile(dir / lockFileName) proc getLockedDependencies*(dir: string): LockFileDependencies = if lockFileExists(dir): diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 654c6967..f0ef0237 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -501,11 +501,17 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = if pkg.hasMetaData: result.metaData = pkg.metaData if pkg.isInstalled and not pkg.isLink: - # If this is an installed package and it is not a linked package, then - # there should not be a VCS revision read from the package directory and - # this read previously from the meta data should be used. - assert result.vcsRevision.len == 0 - result.vcsRevision = pkg.vcsRevision + if result.vcsRevision.len == 0: + # If this is an installed package and it is not a linked package, then + # there should not be a VCS revision read from the package directory and + # this read previously from the meta data should be used. + result.vcsRevision = pkg.vcsRevision + else: + # But if the package meta data file is missing and the package is + # incorrectly identified as not a linked package, then there should be a + # VCS revision read from the real package directory, which should be + # preserved by not overwriting it with an empty revision. + assert pkg.vcsRevision.len == 0 else: return pkg From b9fe6e178e967c405e5668978273616fe437a32b Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sat, 25 Apr 2020 10:58:54 +0300 Subject: [PATCH 16/73] Fix a crash while installing a package In some cases a file name returned by `git ls-files` or `hg manifest` could be an empty directory name and if so trying to open it will result in a crash. This happens for example in the case of a git sub module directory from which no files are being installed. Related to nim-lang/nimble#127 --- src/nimblepkg/checksum.nim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index 3485077d..086b8c6a 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -45,6 +45,12 @@ proc getPackageFileList(): seq[string] = proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = checksum.update(fileName) + if not fileName.existsFile: + # In some cases a file name returned by `git ls-files` or `hg manifest` + # could be an empty directory name and if so trying to open it will result + # in a crash. This happens for example in the case of a git sub module + # directory from which no files are being installed. + return let file = fileName.open(fmRead) defer: close(file) const bufferSize = 8192 From ae9b2c7d31f6db709ee9a80347a9e65efc588915 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 15 Jun 2020 18:27:48 +0300 Subject: [PATCH 17/73] Implement a new package develop mode The new develop mode works with Nimble packages which are in some local file system directory, but not with a link files in the Nimble's cache directory. This allows greater flexibility because the developer could have multiple develop modes of the same package for working on different projects. The develop mode dependencies are written in a JSON file `nimble.develop` which must be placed in the package's top level directory. On dependencies resolution all dependencies coming from it are considered with higher priority compared to the packages in Nimble's cache. The format of the file is the following one: ```json { "version": "0.1.0", # JSON schema version. "includes": [], # Array of paths to included files. "dependencies": [] # Array of paths to packages. } ``` The format for included develop files is the same as the project's develop file, but their validation works slightly different. Validation rules: - The included develop files must be valid. - The packages listed in `dependencies` section must be valid. - The packages listed in `dependencies` section must be dependencies required by the package's `.nimble` file and to be in the required by it version range. Transitive dependencies are not allowed but this must change in the future. - The packages listed in the included develop files are required to be valid Nimble packages, but they are not required to be valid dependencies of the current project. In the last case they are simply ignored. - The develop files of the develop mode dependencies of some package are being followed and processed recursively. Finally only one common set of develop mode dependencies is created. - In the final set of develop mode dependencies it is not allowed to have more than one packages with same name but at different file system paths. If present the validity of the package's develop file is added to the requirements for validity of the package. The interface of the new develop mode is the following one: - `develop ` - Clones a list of packages for development. If executed in a package directory creates a `nimble.develop` file with paths to the cloned packages which are valid dependencies of the current package. - `-p, --path` - Specifies the path whether the packages should be cloned. Only one such option can be given. - `-c, --create [path]` - Creates an empty develop file with name `nimble.develop` in the current directory or if `path` option is present to the given directory with a given name. - `-a, --add path` - Adds a package at given path to the `nimble.develop` file. - `-r, --remove-path path` - Removes a package with a given name from the `nimble.develop` file. - `-n, --remove-name name` - Removes a package with a given name from the `nimble.develop` file. - `-i, --include file` - Includes a develop file into the current directory's one. - `-e, --exclude file` - Excludes a develop file from the current directory's one. The options for manipulation of the develop files could be given only when executing `develop` command from some package's directory and they work only on project's develop file named `nimble.develop` and not on free develop files intended only for inclusion. Because the develop files are user specific and they contain local file system paths they MUST NOT be committed. Current limitations: - In some moment transitive dependencies in the `dependencies` section of the develop file must be allowed because this will allow to use in develop mode some transitive package dependencies without having in develop mode the full dependencies path to them. It was a design mistake that it was not allowed at the beginning. - There is a problem that if a package and it dependencies are listed for cloning for develop, when cloning the package its dependency is being downloaded as package for installation and it is being installed in the Nimble's global cache despite the fact that it is already cloned for develop or it will be cloned for develop very soon. This is not a new problem and it was present also in the old develop mode, but it is nice to fix it at some point. - When used alongside a lock file the develop mode dependencies should be automatically updated appropriately according to the content of the lock file. Additional changes: - Tests for the new develop mode are added. Applicable old tests are retained and modified in appropriate way. - Some of the old tests are re-factored to not depend to the state of the environment left by tests executed before them and to leave the environment in clean state after them. It lefts to be done for all tests in the future. - A distinct `Path` type is added for use in `developfile.nim` module. It treats path strings comparison in a way independent from whether the path is absolute or relative to the current directory. In the future will be good all operations of Nimble with file system paths to be implemented with it. - Some additional helper procedures like `getCacheDir`m `getPkgDest`, `getNameAndVersion`, `getNimbleFileDir`, `to`, `debugTrace` and so on are added. - The functionality for working with the old develop mode via Nimble link files is largely removed and what remains is moved to `nimblelinkfile.nim` module. It will be used only for the migration code. `isLink` field in the package meta data JSON is also removed. - Module `displaymessages.nim` is added. It contains constants and procedures returning some of the messages given by Nimble. The purpose of this is to facilitate testing code by not requiring to repeat the message for which is being checked in the output. - Custom `counttables.nim` module is named which works differently than the standard one. It is used inside the `developfile.nim` module. - Loading and saving of the file `nimbledata.json`, currently containing only the information about packages' reverse dependencies, are moved on Nimble's startup and Nimble's exit respectively in order to minimize the points where they have to be called, which leads to cleaner code. The data from the file is needed for almost all operations and the loading of the file in not a huge overhead. In future the loading can be skipped when executing Nimble commands for which the content of the file is not needed. - Added low priority log messages for loading and saving of `nimbledata.json` file. - Errors handling is reworked to use exceptions `parent` field for exceptions chaining. When some error is cached and a new one have to be raised with some other error message, the old one can be chained to it and its message displayed as `Details:` when the exception message is finally displayed on some upper level of the call stack. - The command line display interface is extended with shortcut procedures for displaying the different display types and two additional display types are added for the messages starting with `Details` and `Hint`. - `packagemetadata.nim` is renames to `packagemetadatafile.nim` and `nimbledata.nim` is renamed to `nimbledatafile.nim` for naming consistency with other Nim modules which work with files. - Disable `ObservableStores` warning for the entire project because of too many false positives. - The usage of `add` procedure for tables is replaced with `[]` because it is deprecated. - To `aliasthis.nim` is added support for tuples. - Copying of `Options` type objects for the `--localdeps` mode is rewritten in more compact way by copying the entire object at once and resetting the fields which should be different. - `briefCopy` procedure for the `Options` type objects is removed as unnecessary. - Additional files are added to tests' directory `.gitignore` file. Related to nim-lang/nimble#127 --- .gitignore | 8 +- config.nims | 4 + src/nimble.nim | 515 +++---- src/nimblepkg/aliasthis.nim | 20 +- src/nimblepkg/checksum.nim | 2 +- src/nimblepkg/cli.nim | 68 +- src/nimblepkg/common.nim | 121 +- src/nimblepkg/config.nim | 19 +- src/nimblepkg/counttables.nim | 96 ++ src/nimblepkg/developfile.nim | 869 ++++++++++++ src/nimblepkg/displaymessages.nim | 124 ++ src/nimblepkg/nimbledata.nim | 44 - src/nimblepkg/nimbledatafile.nim | 79 ++ src/nimblepkg/nimblelinkfile.nim | 17 + src/nimblepkg/nimscriptexecutor.nim | 6 +- src/nimblepkg/options.nim | 104 +- src/nimblepkg/packageinfo.nim | 105 +- src/nimblepkg/packageinfotypes.nim | 29 +- ...gemetadata.nim => packagemetadatafile.nim} | 35 +- src/nimblepkg/packageparser.nim | 55 +- src/nimblepkg/paths.nim | 30 + src/nimblepkg/reversedeps.nim | 505 ++++--- src/nimblepkg/tools.nim | 43 +- src/nimblepkg/topologicalsort.nim | 4 +- tests/.gitignore | 13 +- tests/develop/dependency/dependency.nim | 9 + tests/develop/dependency/dependency.nimble | 7 + tests/develop/dependency2/dependency.nim | 1 + tests/develop/dependency2/dependency.nimble | 7 + tests/develop/dependent/dependent.nimble | 3 +- tests/develop/dependent/src/dependent.nim | 4 +- tests/develop/dependent2/dependent.nimble | 13 + tests/develop/dependent2/src/dependent.nim | 3 + tests/develop/packages.json | 18 + tests/develop/pkg1/pkg1.nim | 5 + tests/develop/pkg1/pkg1.nimble | 8 + tests/develop/pkg2.2/pkg2.nim | 5 + tests/develop/pkg2.2/pkg2.nimble | 6 + tests/develop/pkg2/pkg2.nim | 5 + tests/develop/pkg2/pkg2.nimble | 6 + tests/develop/pkg3.2/pkg3.nim | 2 + tests/develop/pkg3.2/pkg3.nimble | 6 + tests/develop/pkg3/pkg3.nim | 2 + tests/develop/pkg3/pkg3.nimble | 6 + tests/nim.cfg | 1 + tests/tester.nim | 1221 ++++++++++++++--- 46 files changed, 3305 insertions(+), 948 deletions(-) create mode 100644 config.nims create mode 100644 src/nimblepkg/counttables.nim create mode 100644 src/nimblepkg/developfile.nim create mode 100644 src/nimblepkg/displaymessages.nim delete mode 100644 src/nimblepkg/nimbledata.nim create mode 100644 src/nimblepkg/nimbledatafile.nim create mode 100644 src/nimblepkg/nimblelinkfile.nim rename src/nimblepkg/{packagemetadata.nim => packagemetadatafile.nim} (55%) create mode 100644 src/nimblepkg/paths.nim create mode 100644 tests/develop/dependency/dependency.nim create mode 100644 tests/develop/dependency/dependency.nimble create mode 100644 tests/develop/dependency2/dependency.nim create mode 100644 tests/develop/dependency2/dependency.nimble create mode 100644 tests/develop/dependent2/dependent.nimble create mode 100644 tests/develop/dependent2/src/dependent.nim create mode 100644 tests/develop/packages.json create mode 100644 tests/develop/pkg1/pkg1.nim create mode 100644 tests/develop/pkg1/pkg1.nimble create mode 100644 tests/develop/pkg2.2/pkg2.nim create mode 100644 tests/develop/pkg2.2/pkg2.nimble create mode 100644 tests/develop/pkg2/pkg2.nim create mode 100644 tests/develop/pkg2/pkg2.nimble create mode 100644 tests/develop/pkg3.2/pkg3.nim create mode 100644 tests/develop/pkg3.2/pkg3.nimble create mode 100644 tests/develop/pkg3/pkg3.nim create mode 100644 tests/develop/pkg3/pkg3.nimble diff --git a/.gitignore b/.gitignore index 0614c7d6..8855a505 100644 --- a/.gitignore +++ b/.gitignore @@ -20,11 +20,14 @@ src/nimblepkg/checksum src/nimblepkg/cli src/nimblepkg/common src/nimblepkg/config +src/nimblepkg/counttables +src/nimblepkg/developfile src/nimblepkg/download src/nimblepkg/init src/nimblepkg/jsonhelpers src/nimblepkg/lockfile -src/nimblepkg/nimbledata +src/nimblepkg/nimbledatafile +src/nimblepkg/nimblelinkfile src/nimblepkg/nimscriptapi src/nimblepkg/nimscriptexecutor src/nimblepkg/nimscriptwrapper @@ -32,8 +35,9 @@ src/nimblepkg/options src/nimblepkg/packageinfo src/nimblepkg/packageinfotypes src/nimblepkg/packageinstaller -src/nimblepkg/packagemetadata +src/nimblepkg/packagemetadatafile src/nimblepkg/packageparser +src/nimblepkg/paths src/nimblepkg/publish src/nimblepkg/reversedeps src/nimblepkg/tools diff --git a/config.nims b/config.nims new file mode 100644 index 00000000..73fad52f --- /dev/null +++ b/config.nims @@ -0,0 +1,4 @@ +# Disable "ObservableStores" warning for the entire project because it gives +# too many false positives. +switch("warning", "ObservableStores:off") +switch("define", "ssl") diff --git a/src/nimble.nim b/src/nimble.nim index c55ab295..32c59f78 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -18,8 +18,9 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, nimblepkg/checksum, nimblepkg/topologicalsort, nimblepkg/lockfile, - nimblepkg/nimscriptwrapper, nimblepkg/nimbledata, - nimblepkg/packagemetadata + nimblepkg/nimscriptwrapper, nimblepkg/developfile, nimblepkg/paths, + nimblepkg/nimbledatafile, nimblepkg/packagemetadatafile, + nimblepkg/displaymessages proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -46,25 +47,30 @@ proc refresh(options: Options) = for name, list in options.config.packageLists: fetchList(list, options) -proc install(packages: seq[PkgTuple], - options: Options, +proc install(packages: seq[PkgTuple], options: Options, doPrompt, first, fromLockFile: bool): PackageDependenciesInfo -proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = +proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): + HashSet[PackageInfo] = ## Verifies and installs dependencies. ## - ## Returns the list of PackageInfo (for paths) to pass to the compiler + ## Returns set of PackageInfo (for paths) to pass to the compiler ## during build phase. - assert(not pkginfo.isMinimal, "processDeps needs pkginfo.requires") + assert not pkgInfo.isMinimal, + "processFreeDependencies needs pkgInfo.requires" + + var pkgList {.global.}: seq[PackageInfo] = @[] + once: + pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) + pkgList.add processDevelopDependencies(pkgInfo, options) + display("Verifying", - "dependencies for $1@$2" % [pkginfo.name, pkginfo.specialVersion], + "dependencies for $1@$2" % [pkgInfo.name, pkgInfo.specialVersion], priority = HighPriority) - var pkgList {.global.}: seq[PackageInfo] = @[] - once: pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - var reverseDeps: seq[PackageBasicInfo] = @[] - for dep in pkginfo.requires: + var reverseDependencies: seq[PackageBasicInfo] = @[] + for dep in pkgInfo.requires: if dep.name == "nimrod" or dep.name == "nim": let nimVer = getNimrodVersion(options) if not withinRange(nimVer, dep.ver): @@ -80,17 +86,16 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = display("Checking", "for $1" % $dep, priority = MediumPriority) found = findPkg(pkgList, dep, pkg) if found: - display("Warning:", "Installed package $1 should be renamed to $2" % - [dep.name, resolvedDep.name], Warning, HighPriority) + displayWarning(&"Installed package {dep.name} should be renamed to " & + resolvedDep.name) if not found: display("Installing", $resolvedDep, priority = HighPriority) let toInstall = @[(resolvedDep.name, resolvedDep.ver)] - let (pkgs, installedPkg) = install(toInstall, options, - doPrompt = false, - first = false, - fromLockFile = false) - result.add(pkgs) + let (packages, installedPkg) = install(toInstall, options, + doPrompt = false, first = false, fromLockFile = false) + + result.incl packages pkg = installedPkg # For addRevDep fillMetaData(pkg, pkg.getRealDir(), false) @@ -98,12 +103,12 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = # This package has been installed so we add it to our pkgList. pkgList.add pkg else: - display("Info:", "Dependency on $1 already satisfied" % $dep, - priority = HighPriority) - result.add(pkg) + displayInfo(pkgDepsAlreadySatisfiedMsg(dep)) + result.incl pkg # Process the dependencies of this dependency. - result.add(processDeps(pkg.toFullInfo(options), options)) - reverseDeps.add((pkg.name, pkg.specialVersion, pkg.checksum)) + result.incl processFreeDependencies(pkg.toFullInfo(options), options) + if not pkg.isLink: + reverseDependencies.add((pkg.name, pkg.specialVersion, pkg.checksum)) # Check if two packages of the same name (but different version) are listed # in the path. @@ -121,13 +126,11 @@ proc processDeps(pkginfo: PackageInfo, options: Options): HashSet[PackageInfo] = # them added if the above errorenous condition occurs # (unsatisfiable dependendencies). # N.B. NimbleData is saved in installFromDir. - for i in reverseDeps: - addRevDep(options.nimbleData, i, pkginfo) + for i in reverseDependencies: + addRevDep(options.nimbleData, i, pkgInfo) -proc buildFromDir( - pkgInfo: PackageInfo, paths, args: seq[string], - options: Options -) = +proc buildFromDir(pkgInfo: PackageInfo, paths: HashSet[string], + args: seq[string], options: Options) = ## Builds a package as specified by ``pkgInfo``. # Handle pre-`build` hook. let @@ -186,17 +189,12 @@ proc buildFromDir( try: doCmd(cmd) binariesBuilt.inc() - except NimbleError: - let currentExc = (ref NimbleError)(getCurrentException()) - let exc = newException(BuildFailed, "Build failed for package: " & - pkgInfo.name) - let (error, hint) = getOutputInfo(currentExc) - exc.msg.add("\n" & error) - exc.hint = hint - raise exc + except CatchableError as error: + raise buildFailed( + &"Build failed for the package: {pkgInfo.name}", details = error) if binariesBuilt == 0: - raiseNimbleError( + raise nimbleError( "No binaries built, did you specify a valid binary name?" ) @@ -204,9 +202,6 @@ proc buildFromDir( cd pkgDir: # Make sure `execHook` executes the correct .nimble file. discard execHook(options, actionBuild, false) -proc saveNimbleData(options: Options) = - saveNimbleDataToDir(options.getNimbleDir(), options.nimbleData) - proc promptRemoveEntirePackageDir(pkgDir: string, options: Options) = let exceptionMsg = getCurrentExceptionMsg() let warningMsgEnd = if exceptionMsg.len > 0: &": {exceptionMsg}" else: "." @@ -216,20 +211,10 @@ proc promptRemoveEntirePackageDir(pkgDir: string, options: Options) = if not options.prompt( &"Would you like to COMPLETELY remove ALL files in {pkgDir}?"): - raise NimbleQuit(msg: "") + raise nimbleQuit() proc removePackageDir(pkgInfo: PackageInfo, pkgDestDir: string) = - for file in pkgInfo.files: - removeFile(pkgDestDir / file) - - removeFile(pkgDestDir / packageMetaDataFileName) - - if pkgDestDir.isEmptyDir(): - removeDir(pkgDestDir) - else: - display("Warning:", &"Cannot completely remove {pkgDestDir}." & - " Files not installed by Nimble are present.", - Warning, HighPriority) + removePackageDir(pkgInfo.files & packageMetaDataFileName, pkgDestDir) proc removeBinariesSymlinks(pkgInfo: PackageInfo, binDir: string) = for bin in pkgInfo.binaries: @@ -258,7 +243,7 @@ proc removePackage(pkgInfo: PackageInfo, options: Options) = except MetaDataError, ValueError: promptRemoveEntirePackageDir(pkgDestDir, options) removeDir(pkgDestDir) - + removePackageDir(pkgInfo, pkgDestDir) removeBinariesSymlinks(pkgInfo, options.getBinDir()) reinstallSymlinksForOlderVersion(pkgDestDir, options) @@ -286,23 +271,25 @@ proc promptRemovePackageIfExists(pkgInfo: PackageInfo, options: Options): bool = removeOldPackage(pkgInfo, options) return true -proc processLockedDependencies(packageInfo: PackageInfo, options: Options): - seq[PackageInfo] +proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): + HashSet[PackageInfo] proc processAllDependencies(pkgInfo: PackageInfo, options: Options): - seq[PackageInfo] = + HashSet[PackageInfo] = if pkgInfo.lockedDependencies.len > 0: pkgInfo.processLockedDependencies(options) else: - pkgInfo.processDeps(options).toSeq + pkgInfo.processFreeDependencies(options) proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, url: string, first: bool, fromLockFile: bool): PackageDependenciesInfo = ## Returns where package has been installed to, together with paths ## to the packages this package depends on. + ## ## The return value of this function is used by - ## ``processDeps`` to gather a list of paths to pass to the nim compiler. + ## ``processFreeDependencies`` + ## To gather a list of paths to pass to the Nim compiler. ## ## ``first`` ## True if this is the first level of the indirect recursion. @@ -316,6 +303,10 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, raise newException(NimbleError, "Pre-hook prevented further execution.") var pkgInfo = getPkgInfo(dir, options) + # Set the flag that the package is not in develop mode before saving it to the + # reverse dependencies. + pkgInfo.isLink = false + let realDir = pkgInfo.getRealDir() let binDir = options.getBinDir() var depsOptions = options @@ -327,9 +318,9 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Dependencies need to be processed before the creation of the pkg dir. if first and pkgInfo.lockedDependencies.len > 0: - result.deps = processLockedDependencies(pkgInfo, depsOptions).toHashSet + result.deps = pkgInfo.processLockedDependencies(depsOptions) elif not fromLockFile: - result.deps = processDeps(pkgInfo, depsOptions) + result.deps = pkgInfo.processFreeDependencies(depsOptions) if options.depsOnly: result.pkg = pkgInfo @@ -338,15 +329,24 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, display("Installing", "$1@$2" % [pkginfo.name, pkginfo.specialVersion], priority = HighPriority) + let isPackageAlreadyInCache = pkgInfo.packageExists(options) + # Build before removing an existing package (if one exists). This way # if the build fails then the old package will still be installed. + if pkgInfo.bin.len > 0: - let paths = result.deps.toSeq.map(dep => dep.getRealDir()) + let paths = result.deps.map(dep => dep.getRealDir()) let flags = if options.action.typ in {actionInstall, actionPath, actionUninstall, actionDevelop}: options.action.passNimFlags else: @[] - buildFromDir(pkgInfo, paths, "-d:release" & flags, options) + + try: + buildFromDir(pkgInfo, paths, "-d:release" & flags, options) + except CatchableError: + if not isPackageAlreadyInCache: + removeRevDep(options.nimbleData, pkgInfo) + raise let pkgDestDir = pkgInfo.getPkgDest(options) @@ -411,14 +411,12 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, pkgInfo.binaries = binariesInstalled.toSeq saveMetaData(pkgInfo.metaData, pkgDestDir) - saveNimbleData(options) else: display("Warning:", "Skipped copy in project local deps mode", Warning) pkgInfo.isInstalled = true - display("Success:", pkgInfo.name & " installed successfully.", - Success, HighPriority) + displaySuccess(pkgInstalledMsg(pkgInfo.name)) result.deps.incl pkgInfo result.pkg = pkgInfo @@ -429,8 +427,8 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, cd pkgInfo.myPath.splitFile.dir: discard execHook(options, actionInstall, false) -proc processLockedDependencies(packageInfo: PackageInfo, options: Options): - seq[PackageInfo] = +proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): + HashSet[PackageInfo] = ## ``` ## For each dependency in the lock file: ## Check whether it is already installed and if not: @@ -443,11 +441,11 @@ proc processLockedDependencies(packageInfo: PackageInfo, options: Options): let packagesDir = options.getPkgsDir() - for name, dep in packageInfo.lockedDependencies: + for name, dep in pkgInfo.lockedDependencies: let depDirName = packagesDir / fmt"{name}-{dep.version}-{dep.checksum.sha1}" - if not existsFile(depDirName / packageMetaDataFileName): - if depDirName.existsDir: + if not fileExists(depDirName / packageMetaDataFileName): + if depDirName.dirExists: promptRemoveEntirePackageDir(depDirName, options) removeDir(depDirName) @@ -468,18 +466,18 @@ proc processLockedDependencies(packageInfo: PackageInfo, options: Options): downloadDir, version, options, url, first = false, fromLockFile = true) for depDepName in dep.dependencies: - let depDep = packageInfo.lockedDependencies[depDepName] + let depDep = pkgInfo.lockedDependencies[depDepName] let revDep = (name: depDepName, version: depDep.version, checksum: depDep.checksum.sha1) options.nimbleData.addRevDep(revDep, newlyInstalledPackageInfo) - result.add newlyInstalledPackageInfo + result.incl newlyInstalledPackageInfo else: let nimbleFilePath = findNimbleFile(depDirName, false) let packageInfo = getInstalledPackageMin( depDirName, nimbleFilePath).toFullInfo(options) - result.add packageInfo + result.incl packageInfo proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string, @@ -505,10 +503,9 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options, # isn't there) return getDownloadInfo(pv, options, false) else: - raise newException(NimbleError, "Package not found.") + raise newException(NimbleError, pkgNotFoundMsg(pv)) -proc install(packages: seq[PkgTuple], - options: Options, +proc install(packages: seq[PkgTuple], options: Options, doPrompt, first, fromLockFile: bool): PackageDependenciesInfo = ## ``first`` ## True if this is the first level of the indirect recursion. @@ -516,7 +513,12 @@ proc install(packages: seq[PkgTuple], ## True if we are installing dependencies from the lock file. if packages == @[]: - result = installFromDir(getCurrentDir(), newVRAny(), options, "", first, + let currentDir = getCurrentDir() + if currentDir.hasDevelopFile: + displayWarning( + "Installing a package which currently has develop mode dependencies." & + "\nThey will be ignored and installed as normal packages.") + result = installFromDir(currentDir, newVRAny(), options, "", first, fromLockFile) else: # Install each package. @@ -527,9 +529,9 @@ proc install(packages: seq[PkgTuple], downloadPkg(url, pv.ver, meth, subdir, options, downloadPath = "", vcsRevision = "") try: - result = installFromDir(downloadDir, pv.ver, options, url, first, - fromLockFile) - except BuildFailed: + result = installFromDir(downloadDir, pv.ver, options, url, + first, fromLockFile) + except BuildFailed as error: # The package failed to build. # Check if we tried building a tagged version of the package. let headVer = getHeadName(meth) @@ -544,11 +546,11 @@ proc install(packages: seq[PkgTuple], [pv.name, $downloadVersion]) if promptResult: let toInstall = @[(pv.name, headVer.toVersionRange())] - result = install(toInstall, options, doPrompt, first, - fromLockFile = false) + result = install(toInstall, options, doPrompt, first, + fromLockFile = false) else: - raise newException(BuildFailed, - "Aborting installation due to build failure") + raise buildFailed( + "Aborting installation due to build failure.", details = error) else: raise @@ -565,6 +567,7 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = let bin = options.getCompilationBinary(pkgInfo).get("") binDotNim = bin.addFileExt("nim") + if bin == "": raise newException(NimbleError, "You need to specify a file.") @@ -572,10 +575,10 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = raise newException(NimbleError, "Specified file, " & bin & " or " & binDotNim & ", does not exist.") - let dir = getCurrentDir() - let pkgInfo = getPkgInfo(dir, options) + let pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) let deps = pkgInfo.processAllDependencies(options) + if not execHook(options, options.action.typ, true): raise newException(NimbleError, "Pre-hook prevented further execution.") @@ -588,6 +591,7 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = if options.verbosity == SilentPriority: # Hide Nim warnings args.add("--warnings:off") + for option in options.getCompilationFlags(): args.add(option.quoteShell) @@ -603,8 +607,10 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = else: display("Generating", ("documentation for $1 (from package $2) using $3 " & "backend") % [bin, pkgInfo.name, backend], priority = HighPriority) + doCmd(getNimBin(options).quoteShell & " $# --noNimblePath $# $#" % [backend, join(args, " "), bin.quoteShell]) + display("Success:", "Execution finished", Success, HighPriority) # Run the post hook for action if it exists @@ -950,13 +956,28 @@ Please specify a valid SPDX identifier.""", display("Success:", "Package $# created successfully" % [pkgName], Success, HighPriority) -proc uninstall(options: Options) = +proc removePackages(pkgs: HashSet[ReverseDependency], options: var Options) = + for pkg in pkgs: + let pkgInfo = pkg.toPkgInfo(options) + case pkg.kind + of rdkInstalled: + pkgInfo.removePackage(options) + display("Removed", $pkg, Success, HighPriority) + of rdkDevelop: + options.nimbleData.removeRevDep(pkgInfo) + +proc collectNames(pkgs: HashSet[ReverseDependency], + includeDevelopRevDeps: bool): seq[string] = + for pkg in pkgs: + if pkg.kind != rdkDevelop or includeDevelopRevDeps: + result.add $pkg + +proc uninstall(options: var Options) = if options.action.packages.len == 0: raise newException(NimbleError, "Please specify the package(s) to uninstall.") - var pkgsToDelete: HashSet[PackageInfo] - pkgsToDelete.init() + var pkgsToDelete: HashSet[ReverseDependency] # Do some verification. for pkgTup in options.action.packages: display("Looking", "for $1 ($2)" % [pkgTup.name, $pkgTup.ver], @@ -971,154 +992,150 @@ proc uninstall(options: Options) = # Check whether any packages depend on the ones the user is trying to # uninstall. if options.uninstallRevDeps: - getAllRevDeps(options, pkg, pkgsToDelete) + getAllRevDeps(options.nimbleData, pkg.toRevDep, pkgsToDelete) else: - let - revDeps = getRevDeps(options, pkg) - var reason = "" - for revDep in revDeps: - if reason.len != 0: reason.add ", " - reason.add("$1 ($2)" % [revDep.name, revDep.version]) - if reason.len != 0: - reason &= " depend" & (if revDeps.len == 1: "s" else: "") & " on it" - + let revDeps = options.nimbleData.getRevDeps(pkg.toRevDep) if len(revDeps - pkgsToDelete) > 0: - display("Cannot", "uninstall $1 ($2) because $3" % - [pkgTup.name, pkg.specialVersion, reason], Warning, HighPriority) + let pkgs = revDeps.collectNames(true) + displayWarning( + cannotUninstallPkgMsg(pkgTup.name, pkg.specialVersion, pkgs)) else: - pkgsToDelete.incl pkg + pkgsToDelete.incl pkg.toRevDep if pkgsToDelete.len == 0: raise newException(NimbleError, "Failed uninstall - no packages to delete") - var pkgNames = "" - for pkg in pkgsToDelete.items: - if pkgNames.len != 0: pkgNames.add ", " - pkgNames.add("$1 ($2)" % [pkg.name, pkg.specialVersion]) - - # Let's confirm that the user wants these packages removed. - let msg = ("The following packages will be removed:\n $1\n" & - "Do you wish to continue?") % pkgNames - if not options.prompt(msg): - raise NimbleQuit(msg: "") + if not options.prompt(pkgsToDelete.collectNames(false).promptRemovePkgsMsg): + raise nimbleQuit() - for pkg in pkgsToDelete: - let pkgInfo = pkg.toFullInfo(options) - removePackage(pkgInfo, options) - display("Removed", "$1 ($2)" % [pkg.name, $pkg.specialVersion], Success, - HighPriority) - - saveNimbleData(options) + removePackages(pkgsToDelete, options) proc listTasks(options: Options) = let nimbleFile = findNimbleFile(getCurrentDir(), true) nimscriptwrapper.listTasks(nimbleFile, options) -proc developFromDir(dir: string, options: Options) = +proc developFromDir(pkgInfo: PackageInfo, options: Options) = + let dir = pkgInfo.getNimbleFileDir() + + if options.depsOnly: + raise nimbleError("Cannot develop dependencies only.") + cd dir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionDevelop, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") + raise nimbleError("Pre-hook prevented further execution.") - var pkgInfo = getPkgInfo(dir, options) if pkgInfo.bin.len > 0: if "nim" in pkgInfo.skipExt: - raiseNimbleError("Cannot develop packages that are binaries only.") - - display("Warning:", "This package's binaries will not be compiled " & - "nor symlinked for development.", Warning, HighPriority) + raise nimbleError("Cannot develop packages that are binaries only.") - # Overwrite the version to #head always. - pkgInfo.specialVersion = "#head" + displayWarning( + "This package's binaries will not be compiled for development.") if options.developLocaldeps: - var optsCopy: Options - optsCopy.forcePrompts = options.forcePrompts + var optsCopy = options optsCopy.nimbleDir = dir / nimbledeps - createDir(optsCopy.getPkgsDir()) - optsCopy.verbosity = options.verbosity - optsCopy.action = Action(typ: actionDevelop) - optsCopy.config = options.config - optsCopy.nimbleData = %{"reverseDeps": newJObject()} - optsCopy.pkgInfoCache = newTable[string, PackageInfo]() - optsCopy.noColor = options.noColor - optsCopy.disableValidation = options.disableValidation - optsCopy.forceFullClone = options.forceFullClone + optsCopy.nimbleData = newNimbleDataNode() optsCopy.startDir = dir - optsCopy.nim = options.nim + createDir(optsCopy.getPkgsDir()) cd dir: - discard pkgInfo.processAllDependencies(optsCopy) + discard processAllDependencies(pkgInfo, optsCopy) else: # Dependencies need to be processed before the creation of the pkg dir. - discard pkgInfo.processAllDependencies(options) + discard processAllDependencies(pkgInfo, options) - # Don't link if project local deps mode and "developing" the top level package - if not (options.localdeps and options.isInstallingTopLevel(dir)): - if not promptRemovePackageIfExists(pkgInfo, options): - return + displaySuccess(pkgSetupInDevModeMsg(pkgInfo.name, dir)) - let pkgDestDir = pkgInfo.getPkgDest(options) - createDir(pkgDestDir) - # The .nimble-link file contains the path to the real .nimble file, - # and a secondary path to the source directory of the package. - # The secondary path is necessary so that the package's .nimble file doesn't - # need to be read. This will mean that users will need to re-run - # `nimble develop` if they change their `srcDir` but I think it's a worthy - # compromise. - let nimbleLinkPath = pkgDestDir / pkgInfo.name.addFileExt("nimble-link") - let nimbleLink = NimbleLink( - nimbleFilePath: pkgInfo.myPath, - packageDir: pkgInfo.getRealDir() - ) - writeNimbleLink(nimbleLinkPath, nimbleLink) + # Execute the post-develop hook. + cd dir: + discard execHook(options, actionDevelop, false) - # Fill package meta data - pkgInfo.url = "file://" & dir - pkgInfo.files.add nimbleLinkPath - pkgInfo.isLink = true +proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = + let (meth, url, metadata) = getDownloadInfo(pkgTup, options, true) + let subdir = metadata.getOrDefault("subdir") - saveMetaData(pkgInfo.metaData, pkgDestDir) - saveNimbleData(options) + let name = + if isURL(pkgTup.name): + if subdir.len == 0: + parseUri(pkgTup.name).path.splitFile.name + else: + subdir.splitFile.name + else: + pkgTup.name - display("Success:", (pkgInfo.name & " linked successfully to '$1'.") % - dir, Success, HighPriority) - else: - display("Warning:", "Skipping link in project local deps mode", Warning) + let downloadDir = + if options.action.path.isAbsolute: + options.action.path / name + else: + getCurrentDir() / options.action.path / name - # Execute the post-develop hook. - cd dir: - discard execHook(options, actionDevelop, false) + if dirExists(downloadDir): + let msg = "Cannot clone into '$1': directory exists." % downloadDir + let hint = "Remove the directory, or run this command somewhere else." + raise nimbleError(msg, hint) -proc develop(options: Options) = - if options.action.packages == @[]: - developFromDir(getCurrentDir(), options) - else: - # Install each package. - for pv in options.action.packages: - let name = - if isURL(pv.name): - parseUri(pv.name).path.splitPath().tail - else: - pv.name - let downloadDir = getCurrentDir() / name - if dirExists(downloadDir): - let msg = "Cannot clone into '$1': directory exists." % downloadDir - let hint = "Remove the directory, or run this command somewhere else." - raiseNimbleError(msg, hint) - - let (meth, url, metadata) = getDownloadInfo(pv, options, true) - let subdir = metadata.getOrDefault("subdir") + # Download the HEAD and make sure the full history is downloaded. + let ver = + if pkgTup.ver.kind == verAny: + parseVersionRange("#head") + else: + pkgTup.ver - # Download the HEAD and make sure the full history is downloaded. - let ver = - if pv.ver.kind == verAny: - parseVersionRange("#head") - else: - pv.ver - var options = options - options.forceFullClone = true - discard downloadPkg(url, ver, meth, subdir, options, downloadDir, - vcsRevision = "") - developFromDir(downloadDir / subdir, options) + var options = options + options.forceFullClone = true + discard downloadPkg(url, ver, meth, subdir, options, downloadDir, + vcsRevision = "") + + let pkgDir = downloadDir / subdir + var pkgInfo = getPkgInfo(pkgDir, options) + + developFromDir(pkgInfo, options) + + return pkgInfo.getNimbleFileDir + +proc develop(options: var Options) = + let + hasDevActionsAllowedOnlyInPkgDir = options.action.devActions.filterIt( + it[0] != datNewFile).len > 0 + hasPackages = options.action.packages.len > 0 + hasPath = options.action.path.len > 0 + + if not hasPackages and hasPath: + raise nimbleError(pathGivenButNoPkgsToDownloadMsg) + + var currentDirPkgInfo: PackageInfo + + try: + # Check whether the current directory is a package directory. + currentDirPkgInfo = getPkgInfo(getCurrentDir(), options) + except CatchableError as error: + if hasDevActionsAllowedOnlyInPkgDir: + raise nimbleError(developOptionsOutOfPkgDirectoryMsg, details = error) + + var hasDevActions = options.action.devActions.len > 0 + + if currentDirPkgInfo.isLoaded and (not hasPackages) and (not hasDevActions): + developFromDir(currentDirPkgInfo, options) + + var hasError = false + + # Install each package. + for pkgTup in options.action.packages: + try: + let pkgPath = installDevelopPackage(pkgTup, options) + options.action.devActions.add (datAdd, pkgPath.normalizedPath) + hasDevActions = true + except CatchableError as error: + hasError = true + displayError(&"Cannot install package \"{pkgTup}\" for develop.") + displayDetails(error) + + if hasDevActions: + hasError = not updateDevelopFile(currentDirPkgInfo, options) or hasError + + if hasError: + raise nimbleError( + "There are some errors while executing the operation.", + "See the log above for more details.") proc test(options: Options) = ## Executes all tests starting with 't' in the ``tests`` directory. @@ -1141,7 +1158,7 @@ proc test(options: Options) = for file in files: let (_, name, ext) = file.path.splitFile() if ext == ".nim" and name[0] == 't' and file.kind in {pcFile, pcLinkToFile}: - var optsCopy = options.briefClone() + var optsCopy = options optsCopy.action = Action(typ: actionCompile) optsCopy.action.file = file.path optsCopy.action.backend = pkgInfo.backend @@ -1183,35 +1200,26 @@ proc test(options: Options) = return proc check(options: Options) = - ## Validates a package in the current working directory. - let nimbleFile = findNimbleFile(getCurrentDir(), true) - var error: ValidationError - var pkgInfo: PackageInfo - var validationResult = false try: - validationResult = validate(nimbleFile, options, error, pkgInfo) - except: - raiseNimbleError("Could not validate package:\n" & getCurrentExceptionMsg()) - - if validationResult: - display("Success:", pkgInfo.name & " is valid!", Success, HighPriority) - else: - display("Error:", error.msg, Error, HighPriority) - display("Hint:", error.hint, Warning, HighPriority) - display("Failure:", "Validation failed", Error, HighPriority) - quit(QuitFailure) + let pkgInfo = getPkgInfo(getCurrentDir(), options, true) + validateDevelopFile(pkgInfo, options) + displaySuccess(&"The package \"{pkgInfo.name}\" is valid.") + except CatchableError as error: + displayError(error) + display("Failure:", validationFailedMsg, Error, HighPriority) + raise nimbleQuit(QuitFailure) proc promptOverwriteLockFile(options: Options) = let message = &"{lockFileName} already exists. Overwrite?" if not options.prompt(message): - raise NimbleQuit(msg: "") + raise nimbleQuit() proc lock(options: Options) = let currentDir = getCurrentDir() if lockFileExists(currentDir): promptOverwriteLockFile(options) - let packageInfo = getPkgInfo(currentDir, options) - let dependencies = processDeps(packageInfo, options).toSeq.map( + let pkgInfo = getPkgInfo(currentDir, options) + let dependencies = pkgInfo.processFreeDependencies(options).map( pkg => pkg.toFullInfo(options)) let dependencyGraph = buildDependencyGraph(dependencies, options) let (topologicalOrder, _) = topologicalSort(dependencyGraph) @@ -1223,10 +1231,10 @@ proc run(options: Options) = let binary = options.getCompilationBinary(pkgInfo).get("") if binary.len == 0: - raiseNimbleError("Please specify a binary to run") + raise nimbleError("Please specify a binary to run") if binary notin pkgInfo.bin: - raiseNimbleError( + raise nimbleError( "Binary '$#' is not defined in '$#' package." % [binary, pkgInfo.name] ) @@ -1236,7 +1244,9 @@ proc run(options: Options) = let binaryPath = pkgInfo.getOutputDir(binary) let cmd = quoteShellCommand(binaryPath & options.action.runFlags) displayDebug("Executing", cmd) - cmd.execCmd.quit + + let exitCode = cmd.execCmd + raise nimbleQuit(exitCode) proc doAction(options: var Options) = if options.showHelp: @@ -1244,9 +1254,6 @@ proc doAction(options: var Options) = if options.showVersion: writeVersion() - setNimBin(options) - setNimbleDir(options) - if options.action.typ in {actionTasks, actionRun, actionBuild, actionCompile}: # Implicitly disable package validation for these commands. options.disableValidation = true @@ -1319,37 +1326,41 @@ proc doAction(options: var Options) = # fallback logic. test(options) else: - raiseNimbleError(msg = "Could not find task $1 in $2" % - [options.action.command, nimbleFile], - hint = "Run `nimble --help` and/or `nimble tasks` for" & - " a list of possible commands.") + raise nimbleError(msg = "Could not find task $1 in $2" % + [options.action.command, nimbleFile], + hint = "Run `nimble --help` and/or `nimble tasks` for" & + " a list of possible commands.") when isMainModule: - var error = "" - var hint = "" + var exitCode = QuitSuccess var opt: Options try: opt = parseCmdLine() - opt.startDir = getCurrentDir() + opt.setNimBin + opt.setNimbleDir + opt.loadNimbleData opt.doAction() - except NimbleError: - let currentExc = (ref NimbleError)(getCurrentException()) - (error, hint) = getOutputInfo(currentExc) - except NimbleQuit: - discard + except NimbleQuit as quit: + exitCode = quit.exitCode + except CatchableError as error: + exitCode = QuitFailure + displayTip() + displayError(error) finally: try: let folder = getNimbleTempDir() if opt.shouldRemoveTmp(folder): removeDir(folder) - except OSError: - let msg = "Couldn't remove Nimble's temp dir" - display("Warning:", msg, Warning, MediumPriority) + except CatchableError as error: + displayWarning("Couldn't remove Nimble's temp dir") + displayDetails(error) - if error.len > 0: - displayTip() - display("Error:", error, Error, HighPriority) - if hint.len > 0: - display("Hint:", hint, Warning, HighPriority) - quit(1) + try: + saveNimbleData(opt) + except CatchableError as error: + exitCode = QuitFailure + displayError(&"Couldn't save `{nimbleDataFileName}`.") + displayDetails(error) + + quit(exitCode) diff --git a/src/nimblepkg/aliasthis.nim b/src/nimblepkg/aliasthis.nim index 7877b303..ae4a816a 100644 --- a/src/nimblepkg/aliasthis.nim +++ b/src/nimblepkg/aliasthis.nim @@ -4,7 +4,7 @@ import macros template delegateField*(ObjectType: type[object], - objectField, accessor: untyped) = + objectField, accessor: untyped) = ## Defines two additional templates for getter and setter of nested object ## field directly via the embedding object. The name of the nested object ## field must match the name of the accessor. @@ -35,7 +35,7 @@ template delegateField*(ObjectType: type[object], template `accessor "="`*(obj: var ObjectType, value: AccessorType) = obj.objectField.accessor = value -func fields(Object: type[object]): seq[string] = +func fields(Object: type[object | tuple]): seq[string] = ## Collects the names of the fields of an object. let obj = Object.default for name, _ in obj.fieldPairs: @@ -90,18 +90,23 @@ when isMainModule: field12: seq[int] field13: int + Tuple = tuple[tField1: string, tField2: int] + Object2 = object field11: float # intentionally the name is the same as in Object1 field22: Object1 + field23: Tuple aliasThis(Object2.field22) + aliasThis(Object2.field23) var obj = Object2( field11: 3.14, field22: Object1( field11: 2.718, field12: @[1, 1, 2, 3, 5, 8], - field13: 42)) + field13: 42), + field23: ("tuple", 1)) # check access to the original value in both ways check obj.field13 == 42 @@ -140,4 +145,13 @@ when isMainModule: check obj.field11 == 0 check obj.field22.field11 == 2.718 + # check access to tuple fields via an alias + check obj.tField1 == "tuple" + check obj.tField2 == 1 + + # check modification of tuple fields via an alias + obj.tField1 &= " test" + obj.tField2.inc + check obj.field23 == ("tuple test", 2) + reportUnitTestSuccess() diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index 086b8c6a..3855e888 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -45,7 +45,7 @@ proc getPackageFileList(): seq[string] = proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = checksum.update(fileName) - if not fileName.existsFile: + if not fileName.fileExists: # In some cases a file name returned by `git ls-files` or `hg manifest` # could be an empty directory name and if so trying to open it will result # in a crash. This happens for example in the case of a git sub module diff --git a/src/nimblepkg/cli.nim b/src/nimblepkg/cli.nim index 06cc565d..2c8cc808 100644 --- a/src/nimblepkg/cli.nim +++ b/src/nimblepkg/cli.nim @@ -15,9 +15,6 @@ import terminal, sets, strutils import common -when not declared(initHashSet): - import common - type CLI* = ref object level: Priority @@ -33,7 +30,7 @@ type DebugPriority, LowPriority, MediumPriority, HighPriority, SilentPriority DisplayType* = enum - Error, Warning, Message, Success + Error, Warning, Details, Hint, Message, Success ForcePrompt* = enum dontForcePrompt, forcePromptYes, forcePromptNo @@ -41,23 +38,18 @@ type const longestCategory = len("Downloading") foregrounds: array[Error .. Success, ForegroundColor] = - [fgRed, fgYellow, fgCyan, fgGreen] + [fgRed, fgYellow, fgBlue, fgWhite, fgCyan, fgGreen] styles: array[DebugPriority .. HighPriority, set[Style]] = [{styleDim}, {styleDim}, {}, {styleBright}] - proc newCLI(): CLI = result = CLI( level: HighPriority, - warnings: initHashSet[(string, string)](), - suppressionCount: 0, showColor: true, - suppressMessages: false ) var globalCLI = newCLI() - proc calculateCategoryOffset(category: string): int = assert category.len <= longestCategory return longestCategory - category.len @@ -122,6 +114,40 @@ proc display*(category, msg: string, displayType = Message, displayLine(if i == 0: category else: "...", line, displayType, priority) i.inc +proc displayWarning*(message: string, priority = HighPriority) = + display("Warning: ", message, Warning, priority) + +proc displayHint*(message: string, priority = HighPriority) = + display("Hint: ", message, Hint, priority) + +proc displayDetails*(message: string, priority = HighPriority) = + display("Details: ", message, Details, priority) + +proc displaySuccess*(message: string, priority = HighPriority) = + display("Success: ", message, Success, priority) + +proc displayError*(message: string, priority = HighPriority) = + display("Error: ", message, Error, priority) + +proc displayInfo*(message: string, priority = HighPriority) = + display("Info: ", message, Message, priority) + +template defineDisplayMethods(displayMethodName: untyped) {.dirty.} = + method displayMethodName*(error: ref CatchableError, priority = HighPriority) + {.base.} = + displayMethodName(error.msg, priority) + var errorIt = error + if errorIt.parent != nil: + displayDetails((ref CatchableError)(errorIt.parent), priority) + + method displayMethodName*(error: ref NimbleError, priority = HighPriority) = + procCall (ref CatchableError)(error).displayMethodName(priority) + displayHint(error.hint, priority) + +defineDisplayMethods(displayDetails) +defineDisplayMethods(displayError) +defineDisplayMethods(displayWarning) + proc displayDebug*(category, msg: string) = ## Convenience for displaying debug messages. display(category, msg, priority = DebugPriority) @@ -147,7 +173,7 @@ proc prompt*(forcePrompts: ForcePrompt, question: string): bool = display("Prompt:", question & " -> [forced no]", Warning, HighPriority) return false of dontForcePrompt: - displayLine("Prompt:", question & " [y/N]", Warning, HighPriority) + display("Prompt:", question & " [y/N]", Warning, HighPriority) displayCategory("Answer:", Warning, HighPriority) let yn = stdin.readLine() case yn.normalize @@ -273,23 +299,3 @@ proc setShowColor*(val: bool) = proc setSuppressMessages*(val: bool) = globalCLI.suppressMessages = val - -when isMainModule: - display("Reading", "config file at /Users/dom/.config/nimble/nimble.ini", - priority = LowPriority) - - display("Reading", "official package list", - priority = LowPriority) - - display("Downloading", "daemonize v0.0.2 using Git", - priority = HighPriority) - - display("Warning", "dashes in package names will be deprecated", Warning, - priority = HighPriority) - - display("Error", """Unable to read package info for /Users/dom/.nimble/pkgs/nimble-0.7.11 -Reading as ini file failed with: - Invalid section: . -Evaluating as NimScript file failed with: - Users/dom/.nimble/pkgs/nimble-0.7.11/nimble.nimble(3, 23) Error: cannot open 'src/nimblepkg/common'. -""", Error, HighPriority) diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 2ecb5376..aaada43a 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -4,7 +4,8 @@ # Various miscellaneous common types reside here, to avoid problems with # recursive imports -import sets, terminal +import sugar, terminal, macros, hashes, strutils, sets +export sugar.dump type NimbleError* = object of CatchableError @@ -12,53 +13,103 @@ type BuildFailed* = object of NimbleError - ## Same as quit(QuitSuccess), but allows cleanup. - NimbleQuit* = ref object of CatchableError + ## Same as quit(QuitSuccess) or quit(QuitFailure), but allows cleanup. + NimbleQuit* = object of CatchableError + exitCode*: int ProcessOutput* = tuple[output: string, exitCode: int] - NimbleDataJsonKeys* = enum - ndjkVersion = "version" - ndjkRevDep = "reverseDeps" - ndjkRevDepName = "name" - ndjkRevDepVersion = "version" - ndjkRevDepChecksum = "checksum" - const nimbleVersion* = "0.13.1" - nimbleDataFile* = (name: "nimbledata.json", version: "0.1.0") + nimblePackagesDirName* = "pkgs" + nimbleBinariesDirName* = "bin" + +proc newNimbleError[ErrorType](msg: string, hint = "", + details: ref CatchableError = nil): + ref ErrorType = + result = newException(ErrorType, msg, details) + result.hint = hint -proc raiseNimbleError*(msg: string, hint = "") = - var exc = newException(NimbleError, msg) - exc.hint = hint - raise exc +proc nimbleError*(msg: string, hint = "", details: ref CatchableError = nil): + ref NimbleError = + newNimbleError[NimbleError](msg, hint, details) -proc getOutputInfo*(err: ref NimbleError): (string, string) = - var error = "" - var hint = "" - error = err.msg - when not defined(release): - let stackTrace = getStackTrace(err) - error = stackTrace & "\n\n" & error - if not err.isNil: - hint = err.hint +proc buildFailed*(msg: string, hint = "", details: ref CatchableError = nil): + ref BuildFailed = + newNimbleError[BuildFailed](msg, hint, details) - return (error, hint) +proc nimbleQuit*(exitCode = QuitSuccess): ref NimbleQuit = + result = newException(NimbleQuit, "") + result.exitCode = exitCode proc reportUnitTestSuccess*() = if programResult == QuitSuccess: stdout.styledWrite(fgGreen, "All tests passed.\n") -when not declared(initHashSet): - template initHashSet*[A](initialSize = 64): HashSet[A] = - initSet[A](initialSize) +proc hasField(NewType: type[object], fieldName: static string, + FieldType: type): bool {.compiletime.} = + for name, value in fieldPairs(NewType.default): + if name == fieldName and $value.typeOf == $FieldType: + return true + return false + +macro accessField(obj: typed, name: static string): untyped = + newDotExpr(obj, ident(name)) + +proc to*(obj: object, NewType: type[object]): NewType = + ## Creates an object of `NewType` type, with all fields with both same name + ## and type like a field of `obj`, set to the values of the corresponding + ## fields of `obj`. + + # `ResultType` is a bug workaround: "Cannot evaluate at compile time: NewType" + type ResultType = NewType + for name, value in fieldPairs(obj): + when ResultType.hasField(name, value.typeOf): + accessField(result, name) = value + +template newClone*[T: not ref](obj: T): ref T = + ## Creates a garbage collected heap copy of not a reference object. + let result = obj.typeOf.new + result[] = obj + result + +proc dup*[T](obj: T): T = obj + +proc `$`*(p: ptr | ref): string = cast[int](p).toHex + ## Converts the pointer `p` to its hex string representation. + +proc hash*(p: ptr | ref): int = cast[int](p).hash + ## Calculates the has value of the pointer `p`. + +template cd*(dir: string, body: untyped) = + ## Sets the current dir to ``dir``, executes ``body`` and restores the + ## previous working dir. + let lastDir = getCurrentDir() + setCurrentDir(dir) + block: + defer: setCurrentDir(lastDir) + body + +template debugTrace*(): untyped = + block: + let (filename, line, _) = instantiationInfo() + echo "filename = $#; line: $#" % [filename, $line] -when not declared(toHashSet): - template toHashSet*[A](keys: openArray[A]): HashSet[A] = - toSet(keys) +when isMainModule: + import unittest -template add*[A](s: HashSet[A], key: A) = - s.incl(key) + test "to": + type + Foo = object + i: int + f: float + + Bar = object + i: string + f: float + s: string -template add*[A](s: HashSet[A], other: HashSet[A]) = - s.incl(other) + let foo = Foo(i: 42, f: 3.1415) + var bar = to(foo, Bar) + bar.s = "hello" + check bar == Bar(i: "", f: 3.1415, s: "hello") diff --git a/src/nimblepkg/config.nim b/src/nimblepkg/config.nim index d2da021f..7779a1b4 100644 --- a/src/nimblepkg/config.nim +++ b/src/nimblepkg/config.nim @@ -19,24 +19,19 @@ type proc initConfig(): Config = result.nimbleDir = getHomeDir() / ".nimble" - result.httpProxy = initUri() - result.chcp = true result.cloneUsingHttps = true - - result.packageLists = initTable[string, PackageList]() - let defaultPkgList = PackageList(name: "Official", urls: @[ + result.packageLists["official"] = PackageList(name: "Official", urls: @[ "https://github.com/nim-lang/packages/raw/master/packages.json", "https://irclogs.nim-lang.org/packages.json", "https://nim-lang.org/nimble/packages.json" ]) - result.packageLists["official"] = defaultPkgList -proc initPackageList(): PackageList = - result.name = "" - result.urls = @[] - result.path = "" +proc clear(pkgList: var PackageList) = + pkgList.name = "" + pkgList.urls = @[] + pkgList.path = "" proc addCurrentPkgList(config: var Config, currentPackageList: PackageList) = if currentPackageList.name.len > 0: @@ -60,7 +55,7 @@ proc parseConfig*(): Config = var p: CfgParser open(p, f, confFile) var currentSection = "" - var currentPackageList = initPackageList() + var currentPackageList: PackageList while true: var e = next(p) case e.kind @@ -77,7 +72,7 @@ proc parseConfig*(): Config = currentSection = e.section case currentSection.normalize of "packagelist": - currentPackageList = initPackageList() + currentPackageList.clear() else: raise newException(NimbleError, "Unable to parse config file:" & " Unknown section: " & e.key) diff --git a/src/nimblepkg/counttables.nim b/src/nimblepkg/counttables.nim new file mode 100644 index 00000000..9e06cc88 --- /dev/null +++ b/src/nimblepkg/counttables.nim @@ -0,0 +1,96 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import tables, strformat + +type + CountType = uint + CountTable*[K] = distinct Table[K, CountType] + ## Maps a key to some unsigned integer count. + +template withValue[K](t: var CountTable[K], k: K; + value, body1, body2: untyped) = + withValue(Table[K, CountType](t), k, value, body1, body2) + +proc `[]=`[K](t: var CountTable[K], k: K, v: CountType) {.inline.} = + Table[K, CountType](t)[k] = v + +proc del[K](t: var CountTable[K], k: K) {.inline.} = + del(Table[K, CountType](t), k) + +proc getOrDefault[K](t: CountTable[K], k: K): CountType {.inline.} = + getOrDefault(Table[K, CountType](t), k) + +proc inc*[K](t: var CountTable[K], k: K) = + ## Increments the count of key `k` in table `t`. If the key is missing the + ## procedure adds it with a count 1. + t.withValue(k, value) do: + const maxCount = CountType.high + assert value[] < maxCount, + "Cannot increment because the result will exceed the maximum " & + &"possible count value of {maxCount}." + value[].inc() + do: + t[k] = 1 + +proc dec*[K](t: var CountTable[K], k: K): bool {.discardable.} = + ## Decrements the count of key `k` in table `t`. If the count drops to zero + ## the procedure removes the key from the table. + ## + ## Returns `true` in the case the count for the key `k` drops to zero and the + ## key is removed from the table or `false` otherwise. + ## + ## If the key `k` is missing raises a `KeyError` exception. + t.withValue(k, value) do: + value[].dec() + if value[] == 0: + t.del(k) + result = true + do: + raise newException(KeyError, &"The key \"{k}\" is not found.") + +proc count*[K](t: CountTable[K], k: K): CountType = t.getOrDefault(k) + ## Returns the count of the key `k` from the table `t`. If the key is missing + ## returns zero. + +proc `[]`*[K](t: CountTable[K], k: K): CountType = Table[K, CountType](t)[k] + ## Returns the count of the key `k` from the table `t`. If the key is missing + ## raises a `KeyError` exception. + +proc hasKey*[K](t: CountTable[K], k: K): bool = t.count(k) != 0 + ## Checks whether the key `k` is present in the table `t`. + +when isMainModule: + import unittest + import common + + let testKey = 'a' + var t: CountTable[testKey.typeOf] + + proc checkKeyCount[K](t: CountTable[K], k: K, c: CountType) = + check t.count(k) == c + if c != 0: + check t.hasKey(k) + check t[k] == c + else: + check not t.hasKey(k) + expect KeyError, (discard t[k]) + + checkKeyCount(t, testKey, 0) + + t.inc(testKey) + checkKeyCount(t, testKey, 1) + + t.inc(testKey) + checkKeyCount(t, testKey, 2) + + check not t.dec(testKey) + checkKeyCount(t, testKey, 1) + + check t.dec(testKey) + checkKeyCount(t, testKey, 0) + + expect KeyError, t.dec(testKey) + checkKeyCount(t, testKey, 0) + + reportUnitTestSuccess() diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim new file mode 100644 index 00000000..c0647ff3 --- /dev/null +++ b/src/nimblepkg/developfile.nim @@ -0,0 +1,869 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +## This module implements operations required for working with Nimble develop +## files. + +import sets, json, sequtils, os, strformat, tables, hashes, std/jsonutils, + strutils + +import common, cli, packageinfotypes, packageinfo, packageparser, options, + version, counttables, aliasthis, paths, displaymessages + +type + DevelopFileJsonData = object + # The raw data read from the JSON develop file. + includes: OrderedSet[Path] + ## Paths to the included in the current one develop files. + dependencies: OrderedSet[Path] + ## Paths to the dependencies directories. + + DevFileNameToPkgs* = Table[Path, HashSet[ref PackageInfo]] + ## Mapping between a develop file name and a set of packages. + + PkgToDevFileNames* = Table[ref PackageInfo, HashSet[Path]] + ## Mapping between a package and a set of develop files. + + DevelopFileData* {.requiresInit.} = object + ## The raw data read from the JSON develop file plus the metadata. + path: Path + ## The full path to the develop file. + jsonData: DevelopFileJsonData + ## The actual content of the develop file. + nameToPkg: Table[string, ref PackageInfo] + ## The list of packages coming from the current develop file or some of + ## its includes, indexed by package name. + pathToPkg: Table[Path, ref PackageInfo] + ## The list of packages coming from the current develop file or some of + ## its includes, indexed by package path. + devFileNameToPkgs: DevFileNameToPkgs + ## For each develop file contains references to the packages coming from + ## it or some of its includes. It is used to keep information for which + ## packages, the reference count must be decreased when a develop file + ## is removed. + pkgToDevFileNames: PkgToDevFileNames + ## For each package contains the set of names of the develop files where + ## the path to its directory is mentioned. Used for colliding names error + ## reporting when packages with same name but different paths are present. + pkgRefCount: counttables.CountTable[ref PackageInfo] + ## For each package contains the number of times it is included from + ## different develop files. When the reference count drops to zero the + ## package will be removed from all internal meta data structures. + dependentPkg: PackageInfo + ## The `PackageInfo` of the package in the current directory. + ## It can be missing in the case that this is a develop file intended only + ## for inclusion in other develop files and not related to specific + ## package. + options: Options + ## Current run Nimble's options. + + DevelopFileJsonKeys = enum + ## Develop file JSON objects names. + dfjkVersion = "version" + dfjkIncludes = "includes" + dfjkDependencies = "dependencies" + + NameCollisionRecord = tuple[pkgPath, inclFilePath: Path] + ## Describes the path to a package with a name same as the name of another + ## package, and the path to develop files where it is found. + + CollidingNames = Table[string, HashSet[NameCollisionRecord]] + ## Describes Nimble packages names found more than once in a develop file + ## either directly or via its includes but pointing to different paths. + + InvalidPaths = Table[Path, ref CatchableError] + ## Describes an invalid path to a Nimble package or included develop file. + ## Contains the path as a key and the exact error occurred when we had tried + ## to read the package or the develop file at it. + + ErrorsCollection = object + ## Describes the different errors which are possible to occur on loading of + ## a develop file. + collidingNames: CollidingNames + invalidPackages: InvalidPaths + invalidIncludeFiles: InvalidPaths + +# Disable the warning caused by {.requiresInit.} pragma. +{.warning[UnsafeDefault]: off.} +{.warning[ProveInit]: off.} +aliasThis DevelopFileData.jsonData +{.warning[ProveInit]: on.} +{.warning[UnsafeDefault]: on.} + +const + developFileName* = "nimble.develop" + ## The default name of a Nimble's develop file. This must always be the name + ## of develop files which are not only for inclusion but associated with a + ## specific package. + developFileVersion* = "0.1.0" + ## The version of the develop file's JSON schema. + +proc hasDependentPkg(data: DevelopFileData): bool = + ## Checks whether the develop file data `data` has an associated dependent + ## package. + data.dependentPkg.isLoaded + +proc getNimbleFilePath(pkgInfo: PackageInfo): Path = + ## This is a version of `PackageInfo`'s `getNimbleFileDir` procedure returning + ## `Path` type. + pkgInfo.getNimbleFileDir.Path + +proc assertHasDependentPkg(data: DevelopFileData) = + ## Checks whether there is associated dependent package with the `data`. + assert data.hasDependentPkg, + "This procedure must be used only with associated with particular " & + "package develop files." + +proc getPkgDevFilePath(pkg: PackageInfo): Path = + ## Returns the path to the develop file associated with the package `pkg`. + pkg.getNimbleFilePath / developFileName + +proc getDependentPkgDevFilePath(data: DevelopFileData): Path = + ## Returns the path to the develop file of the dependent package associated + ## with `data`. + data.dependentPkg.getPkgDevFilePath + +proc init*(T: type DevelopFileData, options: Options): T = + ## `DevelopFileData` constructor for a free develop file intended for + ## inclusion in packages develop files. + {.warning[ProveInit]: off.} + result.options = options + +proc init*(T: type DevelopFileData, dependentPkg: PackageInfo, + options: Options): T = + ## `DevelopFileData` constructor for develop file associated with a specific + ## package - `dependentPkg`. + result = init(T, options) + result.dependentPkg = dependentPkg + +proc isEmpty*(data: DevelopFileData): bool = + ## Checks whether there is some content (paths to packages directories or + ## includes to other develop files) in the develop file. + data.includes.len == 0 and data.dependencies.len == 0 + +proc save*(data: DevelopFileData, path: Path, writeEmpty, overwrite: bool) = + ## Saves the `data` to a JSON file with path `path`. If the `data` is empty + ## writes an empty JSON file only if `writeEmpty` is `true`. + ## + ## Raises an `IOError` if: + ## - `overwrite` is `false` and the file with path `path` already exists. + ## - for some reason the writing of the file fails. + + if not writeEmpty and data.isEmpty: + return + + let json = %{ + $dfjkVersion: %developFileVersion, + $dfjkIncludes: %data.includes.toSeq, + $dfjkDependencies: %data.dependencies.toSeq, + } + + if path.fileExists and not overwrite: + raise newException(IOError, fileAlreadyExistsMsg($path)) + + writeFile(path, json.pretty) + +template save(data: DevelopFileData, args: varargs[untyped]) = + ## Saves the `data` to a JSON file in in the directory of `data`'s + ## `dependentPkg` Nimble file. Delegates the functionality to the `save` + ## procedure taking path to develop file. + let fileName = data.getDependentPkgDevFilePath + data.save(fileName, args) + +proc hasDevelopFile*(dir: Path): bool = + ## Returns `true` if there is a Nimble develop file with a default name in + ## the directory `dir` or `false` otherwise. + fileExists(dir / developFileName) + +proc hasDevelopFile*(pkg: PackageInfo): bool = + ## Returns `true` if there is a Nimble develop file with a default name in + ## the directory of the package's `pkg` `.nimble` file or `false` otherwise. + pkg.getNimbleFilePath.hasDevelopFile + +proc raiseDependencyNotInRangeError( + dependencyNameAndVersion, dependentNameAndVersion: string, + versionRange: VersionRange) = + ## Raises `DependencyNotInRange` exception. + raise nimbleError( + dependencyNotInRangeErrorMsg( + dependencyNameAndVersion, dependentNameAndVersion, versionRange), + dependencyNotInRangeErrorHint) + +proc raiseNotADependencyError( + dependencyNameAndVersion, dependentNameAndVersion: string) = + ## Raises `NotADependency` exception. + raise nimbleError( + notADependencyErrorMsg(dependencyNameAndVersion, dependentNameAndVersion), + notADependencyErrorHint) + +proc validateDependency(dependencyPkg, dependentPkg: PackageInfo) = + ## Checks whether `dependencyPkg` is a valid dependency of the `dependentPkg`. + ## If it is not, then raises a `NimbleError` or otherwise simply returns. + ## + ## Raises a `NimbleError` if: + ## - the `dependencyPkg` is not a dependency of the `dependentPkg`. + ## - the `dependencyPkg` is a dependency og the `dependentPkg`, but its + ## version is out of the required by `dependentPkg` version range. + + var isNameFound = false + var versionRange = parseVersionRange("") # any version + + for pkg in dependentPkg.requires: + if cmpIgnoreStyle(dependencyPkg.name, pkg.name) == 0: + isNameFound = true + if Version(dependencyPkg.version) in pkg.ver: + # `dependencyPkg` is a valid dependency of `dependentPkg`. + return + else: + # The package with a name `dependencyPkg.name` is found among + # `dependentPkg` dependencies but its version is out of the required + # range. + versionRange = pkg.ver + break + + # If in `dependentPkg` requires clauses is not found a package with a name + # `dependencyPkg.name` or its version is not in the required range, then + # `dependencyPkg` is not a valid dependency of `dependentPkg`. + + let dependencyPkgNameAndVersion = dependencyPkg.getNameAndVersion() + let dependentPkgNameAndVersion = dependentPkg.getNameAndVersion() + + if isNameFound: + raiseDependencyNotInRangeError( + dependencyPkgNameAndVersion, dependentPkgNameAndVersion, versionRange) + else: + raiseNotADependencyError( + dependencyPkgNameAndVersion, dependentPkgNameAndVersion) + +proc validateIncludedDependency(dependencyPkg, dependentPkg: PackageInfo, + requiredVersionRange: VersionRange): + ref CatchableError = + ## Checks whether the `dependencyPkg` version is in required by the + ## `dependentPkg` version range and if not returns a reference to an error + ## object. Otherwise returns `nil`. + + return + if Version(dependencyPkg.version) in requiredVersionRange: nil + else: nimbleError( + dependencyNotInRangeErrorMsg( + dependencyPkg.getNameAndVersion, dependentPkg.getNameAndVersion, + requiredVersionRange), + dependencyNotInRangeErrorHint) + +proc validatePackage(pkgPath: Path, dependentPkg: PackageInfo, + options: Options): + tuple[pkgInfo: PackageInfo, error: ref CatchableError] = + ## By given file system path `pkgPath`, determines whether it points to a + ## valid Nimble package. + ## + ## If a not empty `dependentPkg` argument is given checks whether the package + ## at `pkgPath` is a valid dependency of `dependentPkg`. + ## + ## Returns a tuple containing: + ## - `pkgInfo` - the package info of the package at `pkgPath` in case + ## `pkgPath` directory contains a valid Nimble package. + ## + ## - `error` - a reference to the exception raised in case `pkgPath` is + ## not a valid package directory or the package in `pkgPath` + ## is not a valid dependency of the `dependentPkg`. + + try: + result.pkgInfo = getPkgInfo(string(pkgPath), options, true) + if dependentPkg.isLoaded: + validateDependency(result.pkgInfo, dependentPkg) + except CatchableError as error: + result.error = error + +proc filterAndValidateIncludedPackages(dependentPkg: PackageInfo, + inclFileData: DevelopFileData, + invalidPackages: var InvalidPaths): + seq[ref PackageInfo] = + ## Iterates over `dependentPkg` dependencies and for each one found in the + ## `inclFileData` list of packages checks whether it is in the required + ## version range. If so stores it to the result sequence and otherwise stores + ## an error object in `invalidPackages` sequence for future error reporting. + + # For each dependency of the dependent package. + for pkg in dependentPkg.requires: + # Check whether it is in the loaded from the included develop file + # dependencies. + let inclPkg = inclFileData.nameToPkg.getOrDefault pkg.name + if inclPkg == nil: + # If not then continue. + continue + # Otherwise validate it against the dependent package. + let error = validateIncludedDependency(inclPkg[], dependentPkg, pkg.ver) + if error == nil: + result.add inclPkg + else: + invalidPackages[inclPkg[].getNimbleFilePath] = error + +proc hasErrors(errors: ErrorsCollection): bool = + ## Checks whether there are some errors in the `ErrorsCollection` - `errors`. + errors.collidingNames.len > 0 or errors.invalidPackages.len > 0 or + errors.invalidIncludeFiles.len > 0 + +proc pkgFoundMoreThanOnceMsg*( + pkgName: string, collisions: HashSet[NameCollisionRecord]): string = + result = &"A package with name \"{pkgName}\" is found more than once." + for (pkgPath, inclFilePath) in collisions: + result &= &"\"{pkgPath}\" from file \"{inclFilePath}\"" + +proc getErrorsDetails(errors: ErrorsCollection): string = + ## Constructs a message with details about the collected errors. + + for pkgPath, error in errors.invalidPackages: + result &= invalidPkgMsg($pkgPath) + result &= &"\nReason: {error.msg}\n\n" + + for inclFilePath, error in errors.invalidIncludeFiles: + result &= invalidDevFileMsg($inclFilePath) + result &= &"\nReason: {error.msg}\n\n" + + for pkgName, collisions in errors.collidingNames: + result &= pkgFoundMoreThanOnceMsg(pkgName, collisions) + result &= "\n" + +proc add[K, V](t: var Table[K, HashSet[V]], k: K, v: V) = + ## Adds a value `v` to the hash set corresponding to the key `k` of the table + ## `t` by first inserting the key `k` and a new hash set into the table `t`, + ## if they don't already exist. + t.withValue(k, value) do: + value[].incl(v) + do: + t[k] = [v].toHashSet + +proc add[K, V](t: var Table[K, HashSet[V]], k: K, values: HashSet[V]) = + ## Adds all values from the hash set `values` to the hash set corresponding + ## to the key `k` of the table `t` by first inserting the key `k` and a new + ## hash set into the table `t`, if they don't already exist. + for v in values: t.add(k, v) + +proc del[K, V](t: var Table[K, HashSet[V]], k: K, v: V) = + ## Removed a value `v` from the hash set corresponding to the key `k` of the + ## table `t` and removes the key and the corresponding hash set from the + ## table in the case the hash set becomes empty. Does nothing if the key in + ## not present in the table or the value is not present in the hash set. + + t.withValue(k, value) do: + value[].excl(v) + if value[].len == 0: + t.del(k) + +proc assertHasKey[K, V](t: Table[K, V], k: K) = + ## Asserts that the key `k` is present in the table `t`. + assert t.hasKey(k), + &"At this point the key `{k}` should be present in the table {t}." + +proc addPackage(data: var DevelopFileData, pkgInfo: PackageInfo, + comingFrom: Path, actualComingFrom: HashSet[Path], + collidingNames: var CollidingNames) = + ## Adds a package `pkgInfo` to the `data` internal meta data structures. + ## + ## Other parameters: + ## `comingFrom` - the develop file name which loading causes the + ## package to be included. + ## + ## `actualComingFrom` - the set of actual develop files where the package + ## path is mentioned. + ## + ## `collidingNames` - an output parameters where packages with same name + ## but with different paths are registered for error + ## reporting. + + var pkg = data.nameToPkg.getOrDefault(pkgInfo.name) + if pkg == nil: + # If a package with `pkgInfo.name` is missing add it to the + # `DevelopFileData` internal data structures add it. + pkg = pkgInfo.newClone + data.pkgRefCount.inc(pkg) + data.nameToPkg[pkg[].name] = pkg + data.pathToPkg[pkg[].getNimbleFilePath()] = pkg + data.devFileNameToPkgs.add(comingFrom, pkg) + data.pkgToDevFileNames.add(pkg, actualComingFrom) + else: + # If a package with `pkgInfo.name` is already included check whether it has + # the same path as the package we are trying to include. + let + alreadyIncludedPkgPath = pkg[].getNimbleFilePath() + newPkgPath = pkgInfo.getNimbleFilePath() + + if alreadyIncludedPkgPath == newPkgPath: + # If the paths are the same then increase the reference count of the + # package and register the new develop files from where it is coming. + data.pkgRefCount.inc(pkg) + data.devFileNameToPkgs.add(comingFrom, pkg) + data.pkgToDevFileNames.add(pkg, actualComingFrom) + else: + # But if we already have a package with the same name at different path + # register the name collision which to be reported as error. + assertHasKey(data.pkgToDevFileNames, pkg) + for devFileName in data.pkgToDevFileNames[pkg]: + collidingNames.add(pkg[].name, (alreadyIncludedPkgPath, devFileName)) + for devFileName in actualComingFrom: + collidingNames.add(pkg[].name, (newPkgPath, devFileName)) + +proc values[K, V](t: Table[K, V]): seq[V] = + ## Returns a sequence containing table's `t` values. + result.setLen(t.len) + var i: Natural = 0 + for v in t.values: + result[i] = v + inc(i) + +proc addPackages(lhs: var DevelopFileData, pkgs: seq[ref PackageInfo], + rhsPath: Path, rhsPkgToDevFileNames: PkgToDevFileNames, + collidingNames: var CollidingNames) = + ## Adds packages from `pkgs` sequence to the develop file data `lhs`. + for pkgRef in pkgs: + assertHasKey(rhsPkgToDevFileNames, pkgRef) + lhs.addPackage(pkgRef[], rhsPath, rhsPkgToDevFileNames[pkgRef], + collidingNames) + +proc mergeIncludedDevFileData(lhs: var DevelopFileData, rhs: DevelopFileData, + errors: var ErrorsCollection) = + ## Merges develop file data `rhs` coming from some included develop file into + ## `lhs`. If `lhs` represents develop file data of some package, but not a + ## free develop file, then first filter and validate `rhs` packages against + ## `lhs`'s list of dependencies. + + let pkgs = + if lhs.hasDependentPkg: + filterAndValidateIncludedPackages( + lhs.dependentPkg, rhs, errors.invalidPackages) + else: + rhs.nameToPkg.values + + lhs.addPackages(pkgs, rhs.path, rhs.pkgToDevFileNames, errors.collidingNames) + +proc mergeFollowedDevFileData(lhs: var DevelopFileData, rhs: DevelopFileData, + errors: var ErrorsCollection) = + ## Merges develop file data `rhs` coming from some followed package's develop + ## file into `lhs`. + rhs.assertHasDependentPkg + lhs.addPackages(rhs.nameToPkg.values, rhs.path, rhs.pkgToDevFileNames, + errors.collidingNames) + +proc load(data: var DevelopFileData, path: Path, + silentIfFileNotExists, raiseOnValidationErrors: bool) = + ## Loads data from a develop file at path `path`. + ## + ## If `silentIfFileNotExists` then does nothing in the case the develop file + ## does not exists. + ## + ## If `raiseOnValidationErrors` raises a `NimbleError` in the case some of the + ## contents of the develop file are invalid. + ## + ## Raises if the develop file or some of the included develop files: + ## - cannot be read. + ## - has an invalid JSON schema. + ## - contains a path to some invalid package. + ## - contains paths to multiple packages with the same name. + + var + errors {.global.}: ErrorsCollection + visitedFiles {.global.}: HashSet[Path] + visitedPkgs {.global.}: HashSet[Path] + + data.path = path + + if silentIfFileNotExists and not path.fileExists: + return + + visitedFiles.incl path + if data.hasDependentPkg: + visitedPkgs.incl data.dependentPkg.getNimbleFileDir + + try: + fromJson(data.jsonData, parseFile(path), Joptions(allowExtraKeys: true)) + except ValueError as error: + raise nimbleError(notAValidDevFileJsonMsg($path), details = error) + + for depPath in data.dependencies: + let depPath = if depPath.isAbsolute: + depPath.normalizedPath else: (path.splitFile.dir / depPath).normalizedPath + let (pkgInfo, error) = validatePackage( + depPath, data.dependentPkg, data.options) + if error == nil: + data.addPackage(pkgInfo, path, [path].toHashSet, errors.collidingNames) + else: + errors.invalidPackages[depPath] = error + + for inclPath in data.includes: + let inclPath = inclPath.normalizedPath + if visitedFiles.contains(inclPath): + continue + var inclDevFileData = init(DevelopFileData, data.options) + try: + inclDevFileData.load(inclPath, false, false) + except CatchableError as error: + errors.invalidIncludeFiles[path] = error + continue + data.mergeIncludedDevFileData(inclDevFileData, errors) + + if data.hasDependentPkg: + # If this is a package develop file, but not a free one, for each of the + # package's develop mode dependencies load its develop file if it is not + # already loaded and merge its data to the current develop file's data. + for path, pkg in data.pathToPkg.dup: + if visitedPkgs.contains(path): + continue + var followedPkgDevFileData = init(DevelopFileData, pkg[], data.options) + try: + followedPkgDevFileData.load(pkg[].getPkgDevFilePath, true, false) + except: + # The errors will be accumulated in `errors` global variable and + # reported by the `load` call which initiated the recursive process. + discard + data.mergeFollowedDevFileData(followedPkgDevFileData, errors) + + if raiseOnValidationErrors and errors.hasErrors: + raise nimbleError(failedToLoadFileMsg($path), + details = nimbleError(errors.getErrorsDetails)) + +template load(data: var DevelopFileData, args: varargs[untyped]) = + ## Loads data for the associated with `data`'s `dependentPkg` develop file by + ## searching it in its Nimble file directory. Delegates the functionality to + ## the `load` procedure taking path to develop file. + let fileName = data.getDependentPkgDevFilePath + data.load(fileName, args) + +proc addDevelopPackage(data: var DevelopFileData, pkg: PackageInfo): bool = + ## Adds package `pkg`'s path to the develop file. + ## + ## Returns `true` if: + ## - the path is successfully added to the develop file. + ## - the path is already present in the develop file. + ## (Only a warning in printed in this case.) + ## + ## Returns `false` in the case of error when: + ## - a package with the same name but at different path is already present + ## in the develop file or some of its includes. + ## - the package `pkg` is not a valid dependency of the dependent package. + + let pkgDir = pkg.getNimbleFilePath() + + # Check whether the develop file already contains a package with a name + # `pkg.name` at different path. + if data.nameToPkg.hasKey(pkg.name) and not data.pathToPkg.hasKey(pkgDir): + let otherPath = data.nameToPkg[pkg.name][].getNimbleFilePath() + displayError(pkgAlreadyPresentAtDifferentPathMsg(pkg.name, $otherPath)) + return false + + if data.hasDependentPkg: + # Check whether `pkg` is a valid dependency. + try: + validateDependency(pkg, data.dependentPkg) + except CatchableError as error: + displayError(error) + return false + + # Add `pkg` to the develop file model. + let success = not data.dependencies.containsOrIncl(pkgDir) + + var collidingNames: CollidingNames + addPackage(data, pkg, data.path, [data.path].toHashSet, collidingNames) + assert collidingNames.len == 0, "Must not have the same package name at " & + "path different than already existing one." + + if success: + displaySuccess(pkgAddedInDevModeMsg(pkg.getNameAndVersion, $pkgDir)) + else: + displayWarning(pkgAlreadyInDevModeMsg(pkg.getNameAndVersion, $pkgDir)) + + return true + +proc addDevelopPackage(data: var DevelopFileData, path: Path): bool = + ## Adds path `path` to some package directory to the develop file. + ## + ## Returns `true` if: + ## - the path is successfully added to the develop file. + ## - the path is already present in . + ## (Only a warning in printed in this case.) + ## + ## Returns `false` in the case of error when: + ## - the path in `path` does not point to a valid Nimble package. + ## - a package with the same name but at different path is already present + ## in the develop file or some of its includes. + ## - the package `pkg` is not a valid dependency of the dependent package. + + let (pkgInfo, error) = validatePackage(path, PackageInfo(), data.options) + if error != nil: + displayError(invalidPkgMsg($path)) + displayDetails(error) + return false + + return addDevelopPackage(data, pkgInfo) + +proc addDevelopPackageEx*(data: var DevelopFileData, path: Path) = + ## Adds a package at path `path` to a free develop file intended for inclusion + ## in other packages develop files. + ## + ## Raises if: + ## - the path in `path` does not point to a valid Nimble package. + ## - a package with the same name but at different path is already present + ## in the develop file or some of its includes. + ## - the path is already present in the develop file. + + assert not data.hasDependentPkg, + "This procedure can only be used for free develop files intended " & + "for inclusion in other packages develop files." + + let (pkg, error) = validatePackage(path, PackageInfo(), data.options) + if error != nil: raise error + + # Check whether the develop file already contains a package with a name + # `pkg.name` at different path. + if data.nameToPkg.hasKey(pkg.name) and not data.pathToPkg.hasKey(path): + raise nimbleError( + pkgAlreadyPresentAtDifferentPathMsg(pkg.name, $data.pathToPkg[pkg.name])) + + # Add `pkg` to the develop file model. + let success = not data.dependencies.containsOrIncl(path) + + var collidingNames: CollidingNames + addPackage(data, pkg, data.path, [data.path].toHashSet, collidingNames) + assert collidingNames.len == 0, "Must not have the same package name at " & + "path different than already existing one." + + if not success: + raise nimbleError(pkgAlreadyInDevModeMsg(pkg.getNameAndVersion, $path)) + +proc removePackage(data: var DevelopFileData, pkg: ref PackageInfo, + devFileName: Path) = + ## Decreases the reference count for a package at path `path` and removes the + ## package from the internal meta data structures in case the reference count + ## drops to zero. + + # If the package is found it must be excluded from the develop file mappings + # by using the name of the develop file as result of which manipulation the + # package is being removed. + data.devFileNameToPkgs.del(devFileName, pkg) + data.pkgToDevFileNames.del(pkg, devFileName) + + # Also the reference count of the package should be decreased. + let removed = data.pkgRefCount.dec(pkg) + if not removed: + # If the reference count is not zero no further processing is needed. + return + + # But if the reference count is zero the package should be removed from all + # other meta data structures to free memory for it and its indexes. + data.nameToPkg.del(pkg[].name) + data.pathToPkg.del(pkg[].getNimbleFilePath()) + + # The package `pkg` could already be missing from `pkgToDevFileNames` if it + # is removed with the removal of `devFileName` value, but if it is included + # from some of `devFileName`'s includes it will still be present and we + # should remove it completely to free its memory. + data.pkgToDevFileNames.del(pkg) + +proc removePackage(data: var DevelopFileData, path, devFileName: Path) = + ## Decreases the reference count for a package at path `path` and removes the + ## package from the internal meta data structures in case the reference count + ## drops to zero. + + let pkg = data.pathToPkg.getOrDefault(path) + if pkg == nil: + # If there is no package at path `path` found. + return + + data.removePackage(pkg, devFileName) + +proc removeDevelopPackageByPath(data: var DevelopFileData, path: Path): bool = + ## Removes path `path` to some package directory from the develop file. + ## If the `path` is not present in the develop file prints a warning. + ## + ## Returns `true` if path `path` is successfully removed from the develop file + ## or `false` if there is no such path added in it. + + let success = not data.dependencies.missingOrExcl(path) + + if success: + let nameAndVersion = data.pathToPkg[path][].getNameAndVersion() + data.removePackage(path, data.path) + displaySuccess(pkgRemovedFromDevModeMsg(nameAndVersion, $path)) + else: + displayWarning(pkgPathNotInDevFileMsg($path)) + + return success + +proc removeDevelopPackageByName(data: var DevelopFileData, name: string): bool = + ## Removes path to a package with name `name` from the develop file. + ## If a package with name `name` is not present in the develop file prints a + ## warning. + ## + ## Returns `true` if a package with name `name` is successfully removed from + ## the develop file or `false` if there is no such package added in it. + + let + pkg = data.nameToPkg.getOrDefault(name) + path = if pkg != nil: pkg[].getNimbleFilePath() else: "" + success = not data.dependencies.missingOrExcl(path) + + if success: + data.removePackage(path, data.path) + displaySuccess(pkgRemovedFromDevModeMsg(pkg[].getNameAndVersion, $path)) + else: + displayWarning(pkgNameNotInDevFileMsg(name)) + + return success + +proc includeDevelopFile(data: var DevelopFileData, path: Path): bool = + ## Includes a develop file at path `path` to the current project's develop + ## file. + ## + ## Returns `true` if the develop file at `path` is: + ## - successfully included in the current project's develop file. + ## - already present in the current project's develop file. + ## (Only a warning in printed in this case.) + ## + ## Returns `false` in the case of error when: + ## - the develop file at `path` could not be loaded. + ## - the inclusion of the develop file at `path` causes a packages names + ## collisions with already added from different place packages with + ## the same name, but with different location. + + var inclFileData = init(DevelopFileData, data.options) + try: + inclFileData.load(path, false, true) + except CatchableError as error: + displayError(failedToLoadFileMsg($path)) + displayDetails(error) + return false + + let success = not data.includes.containsOrIncl(path) + + if success: + var errors: ErrorsCollection + data.mergeIncludedDevFileData(inclFileData, errors) + if errors.hasErrors: + displayError(failedToInclInDevFileMsg($path, $data.path)) + displayDetails(errors.getErrorsDetails) + # Revert the inclusion in the case of merge errors. + data.includes.excl(path) + for pkgPath, _ in inclFileData.pathToPkg: + data.removePackage(pkgPath, path) + return false + + displaySuccess(inclInDevFileMsg($path)) + else: + displayWarning(alreadyInclInDevFileMsg($path)) + + return true + +proc excludeDevelopFile(data: var DevelopFileData, path: Path): bool = + ## Excludes a develop file at path `path` from the current project's develop + ## file. If there is no such, then only a warning is printed. + ## + ## Returns `true` if a develop file at path `path` is successfully removed + ## from the current project's develop file or `false` if there is no such + ## file included in the current one. + + let success = not data.includes.missingOrExcl(path) + + if success: + assertHasKey(data.devFileNameToPkgs, path) + + # Copy the references of the packages which should be deleted, because + # deleting from the same hash set which we iterate will not be correct. + var packages = data.devFileNameToPkgs[path].toSeq + + # Try to remove the packages coming from the develop file at path `path` or + # some of its includes by decreasing their reference count and appropriately + # updating all other internal meta data structures. + for pkg in packages: + data.removePackage(pkg, path) + + displaySuccess(exclFromDevFileMsg($path)) + else: + displayWarning(notInclInDevFileMsg($path)) + + return success + +proc createEmptyDevelopFile(path: Path, options: Options): bool = + ## Creates an empty develop file at given path `path` or with a default name + ## in the current directory if there is no path given. + + let + data = init(DevelopFileData, options) + filePath = if path.len == 0: Path(developFileName) else: path + + try: + data.save(filePath, writeEmpty = true, overwrite = false) + except CatchableError as error: + displayError(error) + return false + + displaySuccess(emptyDevFileCreatedMsg($filePath)) + return true + +proc updateDevelopFile*(dependentPkg: PackageInfo, options: Options): bool = + ## Updates a dependent package `dependentPkg`'s develop file with an + ## information from the Nimble's command line. + ## - Adds newly installed develop packages. + ## - Adds packages by path. + ## - Removes packages by path. + ## - Removes packages by name. + ## - Includes other develop files. + ## - Excludes other develop files. + ## + ## Returns `true` if all operations are successful and `false` otherwise. + ## Raises if cannot load an existing develop file. + + assert options.action.typ == actionDevelop, + "This procedure must be called only on develop command." + + var + hasError = false + hasSuccessfulRemoves = false + data = init(DevelopFileData, dependentPkg, options) + + data.load(true, true) + + # Save the develop file at the end of the procedure only in the case when it + # is being loaded without an exception. + defer: data.save(writeEmpty = hasSuccessfulRemoves, overwrite = true) + + for (actionType, argument) in options.action.devActions: + case actionType + of datNewFile: + hasError = not createEmptyDevelopFile(argument, options) or hasError + of datAdd: + if data.hasDependentPkg: + hasError = not data.addDevelopPackage(argument) or hasError + of datRemoveByPath: + if data.hasDependentPkg: + hasSuccessfulRemoves = data.removeDevelopPackageByPath(argument) or + hasSuccessfulRemoves + of datRemoveByName: + if data.hasDependentPkg: + hasSuccessfulRemoves = data.removeDevelopPackageByName(argument) or + hasSuccessfulRemoves + of datInclude: + if data.hasDependentPkg: + hasError = not data.includeDevelopFile(argument) or hasError + of datExclude: + if data.hasDependentPkg: + hasSuccessfulRemoves = data.excludeDevelopFile(argument) or + hasSuccessfulRemoves + + return not hasError + +proc validateDevelopFile*(dependentPkg: PackageInfo, options: Options) = + ## Validates the develop file for the package `dependentPkg` by trying to + ## load it. + var data = init(DevelopFileData, dependentPkg, options) + data.load(true, true) + +proc processDevelopDependencies*(dependentPkg: PackageInfo, options: Options): + seq[PackageInfo] = + ## Returns a sequence with the develop mode dependencies of the `dependentPkg` + ## and all of their develop mode dependencies. + + var data = init(DevelopFileData, dependentPkg, options) + data.load(true, true) + + result = newSeqOfCap[PackageInfo](data.nameToPkg.len) + for _, pkg in data.nameToPkg: + result.add pkg[] diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim new file mode 100644 index 00000000..5a0048e4 --- /dev/null +++ b/src/nimblepkg/displaymessages.nim @@ -0,0 +1,124 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +## This module contains procedures producing some of the displayed by Nimble +## error messages in order to facilitate testing by removing the requirement +## the message to be repeated both in Nimble and the testing code. + +import strformat, sequtils +import version + +const + validationFailedMsg* = "Validation failed." + + pathGivenButNoPkgsToDownloadMsg* = + "Path option is given but there are no given packages for download." + + developOptionsOutOfPkgDirectoryMsg* = + "Options 'add', 'remove', 'include' and 'exclude' cannot be given " & + "when develop is being executed out of a valid package directory." + + dependencyNotInRangeErrorHint* = + "Update the version of the dependency package in its Nimble file or " & + "update its required version range in the dependent's package Nimble file." + + notADependencyErrorHint* = + "Add the dependency package as a requirement to the Nimble file of the " & + "dependent package." + + multiplePathOptionsGivenMsg* = "Multiple path options are given." + +proc fileAlreadyExistsMsg*(path: string): string = + &"Cannot create file \"{path}\" because it already exists." + +proc emptyDevFileCreatedMsg*(path: string): string = + &"An empty develop file \"{path}\" has been created." + +proc pkgSetupInDevModeMsg*(pkgName, pkgPath: string): string = + &"\"{pkgName}\" set up in develop mode successfully to \"{pkgPath}\"." + +proc pkgInstalledMsg*(pkgName: string): string = + &"{pkgName} installed successfully." + +proc pkgNotFoundMsg*(pkg: PkgTuple): string = &"Package {pkg} not found." + +proc pkgDepsAlreadySatisfiedMsg*(dep: PkgTuple): string = + &"Dependency on {dep} already satisfied" + +proc dependencyNotInRangeErrorMsg*( + dependencyNameAndVersion, dependentNameAndVersion: string, + versionRange: VersionRange): string = + ## Returns an error message for `DependencyNotInRange` exception. + &"The dependency package \"{dependencyNameAndVersion}\" version is out of " & + &"the required by the dependent package \"{dependentNameAndVersion}\" " & + &"version range \"{versionRange}\"." + +proc notADependencyErrorMsg*( + dependencyNameAndVersion, dependentNameAndVersion: string): string = + ## Returns an error message for `NotADependency` exception. + &"The package \"{dependencyNameAndVersion}\" is not a dependency of the " & + &"package \"{dependentNameAndVersion}\"." + +proc invalidPkgMsg*(path: string): string = + &"The package at \"{path}\" is invalid." + +proc invalidDevFileMsg*(path: string): string = + &"The develop file \"{path}\" is invalid." + +proc notAValidDevFileJsonMsg*(devFilePath: string): string = + &"The file \"{devFilePath}\" has not a valid develop file JSON schema." + +proc pkgAlreadyPresentAtDifferentPathMsg*(pkgName, otherPath: string): string = + &"A package with a name \"{pkgName}\" at different path \"{otherPath}\" " & + "is already present in the develop file." + +proc pkgAddedInDevModeMsg*(pkg, path: string): string = + &"The package \"{pkg}\" at path \"{path}\" is added as a develop mode " & + "dependency." + +proc pkgAlreadyInDevModeMsg*(pkg, path: string): string = + &"The package \"{pkg}\" at path \"{path}\" is already in develop mode." + +proc pkgRemovedFromDevModeMsg*(pkg, path: string): string = + &"The package \"{pkg}\" at path \"{path}\" is removed from the develop file." + +proc pkgPathNotInDevFileMsg*(path: string): string = + &"The path \"{path}\" is not in the develop file." + +proc pkgNameNotInDevFileMsg*(pkgName: string): string = + &"A package with name \"{pkgName}\" is not in the develop file." + +proc failedToInclInDevFileMsg*(inclFile, devFile: string): string = + &"Failed to include \"{inclFile}\" to \"{devFile}\"" + +proc inclInDevFileMsg*(path: string): string = + &"The develop file \"{path}\" is successfully included into the current " & + "project's develop file." + +proc alreadyInclInDevFileMsg*(path: string): string = + &"The develop file \"{path}\" is already included in the current project's " & + "develop file." + +proc exclFromDevFileMsg*(path: string): string = + &"The develop file \"{path}\" is successfully excluded from the current " & + "project's develop file." + +proc notInclInDevFileMsg*(path: string): string = + &"The develop file \"{path}\" is not included in the current project's " & + "develop file." + +proc failedToLoadFileMsg*(path: string): string = + &"Failed to load \"{path}\"." + +proc cannotUninstallPkgMsg*(pkgName, pkgVersion: string, + deps: seq[string]): string = + assert deps.len > 0, "The sequence must have at least one package." + result = &"Cannot uninstall {pkgName} ({pkgVersion}) because\n" + result &= deps.foldl(a & "\n" & b) + result &= "\ndepend" & (if deps.len == 1: "s" else: "") & " on it" + +proc promptRemovePkgsMsg*(pkgs: seq[string]): string = + assert pkgs.len > 0, "The sequence must have at least one package." + result = "The following packages will be removed:\n" + result &= pkgs.foldl(a & "\n" & b) + result &= "\nDo you wish to continue?" diff --git a/src/nimblepkg/nimbledata.nim b/src/nimblepkg/nimbledata.nim deleted file mode 100644 index e6518443..00000000 --- a/src/nimblepkg/nimbledata.nim +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) Dominik Picheta. All rights reserved. -# BSD License. Look at license.txt for more info. - -import json, os - -type - NimbleDataJsonKeys* = enum - ndjkVersion = "version" - ndjkRevDep = "reverseDeps" - ndjkRevDepName = "name" - ndjkRevDepVersion = "version" - ndjkRevDepChecksum = "checksum" - -const - nimbleDataFile = (name: "nimbledata.json", version: "0.1.0") - -proc saveNimbleData*(filePath: string, nimbleData: JsonNode) = - # TODO: This file should probably be locked. - writeFile(filePath, nimbleData.pretty) - -proc saveNimbleDataToDir*(nimbleDir: string, nimbleData: JsonNode) = - saveNimbleData(nimbleDir / nimbleDataFile.name, nimbleData) - -proc newNimbleDataNode*(): JsonNode = - %{ $ndjkVersion: %nimbleDataFile.version, $ndjkRevDep: newJObject() } - -proc convertToTheNewFormat(nimbleData: JsonNode) = - nimbleData.add($ndjkVersion, %nimbleDataFile.version) - for name, versions in nimbleData[$ndjkRevDep]: - for version, dependencies in versions: - for dependency in dependencies: - dependency.add($ndjkRevDepChecksum, %"") - versions[version] = %{ "": dependencies } - -proc parseNimbleData*(fileName: string): JsonNode = - if fileExists(fileName): - result = parseFile(fileName) - if not result.hasKey($ndjkVersion): - convertToTheNewFormat(result) - else: - result = newNimbleDataNode() - -proc parseNimbleDataFromDir*(dir: string): JsonNode = - parseNimbleData(dir / nimbleDataFile.name) diff --git a/src/nimblepkg/nimbledatafile.nim b/src/nimblepkg/nimbledatafile.nim new file mode 100644 index 00000000..e50cdc91 --- /dev/null +++ b/src/nimblepkg/nimbledatafile.nim @@ -0,0 +1,79 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import json, os, strformat +import common, options, jsonhelpers, version, cli + +type + NimbleDataJsonKeys* = enum + ndjkVersion = "version" + ndjkRevDep = "reverseDeps" + ndjkRevDepName = "name" + ndjkRevDepVersion = "version" + ndjkRevDepChecksum = "checksum" + ndjkRevDepPath = "path" + +const + nimbleDataFileName* = "nimbledata.json" + nimbleDataFileVersion ="0.1.0" + +var isNimbleDataFileLoaded = false + +proc saveNimbleData(filePath: string, nimbleData: JsonNode) = + # TODO: This file should probably be locked. + if isNimbleDataFileLoaded: + writeFile(filePath, nimbleData.pretty) + displayInfo(&"Nimble data file \"{filePath}\" has been saved.", LowPriority) + +proc saveNimbleDataToDir(nimbleDir: string, nimbleData: JsonNode) = + saveNimbleData(nimbleDir / nimbleDataFileName, nimbleData) + +proc saveNimbleData*(options: Options) = + saveNimbleDataToDir(options.getNimbleDir(), options.nimbleData) + +proc newNimbleDataNode*(): JsonNode = + %{ $ndjkVersion: %nimbleDataFileVersion, $ndjkRevDep: newJObject() } + +proc convertToTheNewFormat(nimbleData: JsonNode) = + nimbleData.add($ndjkVersion, %nimbleDataFileVersion) + for name, versions in nimbleData[$ndjkRevDep]: + for version, dependencies in versions: + for dependency in dependencies: + dependency.add($ndjkRevDepChecksum, %"") + versions[version] = %{ "": dependencies } + +proc loadNimbleData*(fileName: string): JsonNode = + result = parseFile(fileName) + if not result.hasKey($ndjkVersion): + convertToTheNewFormat(result) + +proc removeDeadDevelopReverseDeps*(options: var Options) = + template revDeps: var JsonNode = options.nimbleData[$ndjkRevDep] + var hasDeleted = false + for name, versions in revDeps: + for version, hashSums in versions: + for hashSum, dependencies in hashSums: + for dep in dependencies: + if dep.hasKey($ndjkRevDepPath) and + not dep[$ndjkRevDepPath].str.dirExists: + dep.delete($ndjkRevDepPath) + hasDeleted = true + if hasDeleted: + options.nimbleData[$ndjkRevDep] = cleanUpEmptyObjects(revDeps) + +proc loadNimbleData*(options: var Options) = + let + nimbleDir = options.getNimbleDir() + fileName = nimbleDir / nimbleDataFileName + + if fileExists(fileName): + options.nimbleData = loadNimbleData(fileName) + removeDeadDevelopReverseDeps(options) + displayInfo(&"Nimble data file \"{fileName}\" has been loaded.", + LowPriority) + else: + displayWarning(&"Nimble data file \"{fileName}\" is not found.", + LowPriority) + options.nimbleData = newNimbleDataNode() + + isNimbleDataFileLoaded = true diff --git a/src/nimblepkg/nimblelinkfile.nim b/src/nimblepkg/nimblelinkfile.nim new file mode 100644 index 00000000..00614def --- /dev/null +++ b/src/nimblepkg/nimblelinkfile.nim @@ -0,0 +1,17 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import strutils + +type + NimbleLink* = object + nimbleFilePath*: string + packageDir*: string + +const + nimbleLinkFileExt* = ".nimble-link" + +proc readNimbleLink*(nimbleLinkPath: string): NimbleLink = + let s = readFile(nimbleLinkPath).splitLines() + result.nimbleFilePath = s[0] + result.packageDir = s[1] diff --git a/src/nimblepkg/nimscriptexecutor.nim b/src/nimblepkg/nimscriptexecutor.nim index 598adb05..167b079e 100644 --- a/src/nimblepkg/nimscriptexecutor.nim +++ b/src/nimblepkg/nimscriptexecutor.nim @@ -42,8 +42,8 @@ proc execCustom*(nimbleFile: string, options: Options, execResult = execTask(nimbleFile, options.action.command, options) if not execResult.success: - raiseNimbleError(msg = "Failed to execute task $1 in $2" % - [options.action.command, nimbleFile]) + raise nimbleError(msg = "Failed to execute task $1 in $2" % + [options.action.command, nimbleFile]) if execResult.command.normalize == "nop": display("Warning:", "Using `setCommand 'nop'` is not necessary.", Warning, @@ -57,7 +57,7 @@ proc execCustom*(nimbleFile: string, options: Options, proc getOptionsForCommand*(execResult: ExecutionResult, options: Options): Options = ## Creates an Options object for the requested command. - var newOptions = options.briefClone() + var newOptions = options parseCommand(execResult.command, newOptions) for arg in execResult.arguments: parseArgument(arg, newOptions) diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 9e7bba49..42d7c411 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -6,7 +6,7 @@ import sequtils, sugar import std/options as std_opt from httpclient import Proxy, newProxy -import config, version, common, cli, packageinfotypes, nimbledata +import config, version, common, cli, packageinfotypes, displaymessages const nimbledeps* = "nimbledeps" @@ -49,6 +49,11 @@ type actionDoc, actionCustom, actionTasks, actionDevelop, actionCheck, actionLock, actionRun + DevelopActionType* = enum + datNewFile, datAdd, datRemoveByPath, datRemoveByName, datInclude, datExclude + + DevelopAction* = tuple[actionType: DevelopActionType, argument: string] + Action* = object case typ*: ActionType of actionNil, actionList, actionPublish, actionTasks, actionCheck, @@ -59,6 +64,8 @@ type packages*: seq[PkgTuple] # Optional only for actionInstall # and actionDevelop. passNimFlags*: seq[string] + devActions*: seq[DevelopAction] + path*: string of actionSearch: search*: seq[string] # Search string. of actionInit, actionDump: @@ -84,11 +91,28 @@ Usage: nimble [nimbleopts] COMMAND [cmdopts] Commands: install [pkgname, ...] Installs a list of packages. - [-d, --depsOnly] Installs only dependencies of the package. - [opts, ...] Passes options to the Nim compiler. - develop [pkgname, ...] Clones a list of packages for development. - Symlinks the cloned packages or any package - in the current working directory. + [-d, --depsOnly] Install only dependencies. + [-p, --passNim] Forward specified flag to compiler. + develop [pkgname, ...] Clones a list of packages for development. If + executed in a package directory creates a + `nimble.develop` file with paths to the cloned + packages. + [-p, --path path] Specifies the path whether the packages should + be cloned. + [-c, --create [path]] Creates an empty develop file with name + `nimble.develop` in the current directory or + if path is present to the given directory with + a given name. + [-a, --add path] Adds a package at given path to the + `nimble.develop` file. + [-r, --remove-path path] Removes a package at given path from the + `nimble.develop` file. + [-n, --remove-name name] Removes a package with a given name from + the `nimble.develop` file. + [-i, --include file] Includes a develop file into the current + directory's one. + [-e, --exclude file] Excludes a develop file from the current + directory's one. check Verifies the validity of a package in the current working directory. init [pkgname] Initializes a new Nimble project in the @@ -158,7 +182,7 @@ const noHookActions* = {actionCheck} proc writeHelp*(quit=true) = echo(help) if quit: - raise NimbleQuit(msg: "") + raise nimbleQuit() proc writeVersion*() = echo("nimble v$# compiled at $# $#" % @@ -169,7 +193,7 @@ proc writeVersion*() = else: {.warning: "Couldn't determine GIT hash: " & execResult[0].} echo "git hash: couldn't determine git hash" - raise NimbleQuit(msg: "") + raise nimbleQuit() proc parseActionType*(action: string): ActionType = case action.normalize() @@ -215,32 +239,18 @@ proc initAction*(options: var Options, key: string) = ## `key`. let keyNorm = key.normalize() case options.action.typ - of actionInstall, actionPath, actionDevelop, actionUninstall: - options.action.packages = @[] - options.action.passNimFlags = @[] of actionCompile, actionDoc, actionBuild: - options.action.compileOptions = @[] - options.action.file = "" - if keyNorm == "c" or keyNorm == "compile": options.action.backend = "" - else: options.action.backend = keyNorm - of actionInit: - options.action.projName = "" - options.action.vcsOption = "" + if keyNorm != "c" and keyNorm != "compile": + options.action.backend = keyNorm of actionDump: - options.action.projName = "" - options.action.vcsOption = "" options.forcePrompts = forcePromptYes - of actionRefresh: - options.action.optionalURL = "" - of actionSearch: - options.action.search = @[] of actionCustom: options.action.command = key options.action.arguments = @[] options.action.custCompileFlags = @[] options.action.custRunFlags = @[] - of actionPublish, actionList, actionTasks, actionCheck, actionRun, actionLock, - actionNil: discard + else: + discard proc prompt*(options: Options, question: string): bool = ## Asks an interactive question and returns the result. @@ -267,10 +277,10 @@ proc getNimbleDir*(options: Options): string = return options.nimbleDir proc getPkgsDir*(options: Options): string = - options.getNimbleDir() / "pkgs" + options.getNimbleDir() / nimblePackagesDirName proc getBinDir*(options: Options): string = - options.getNimbleDir() / "bin" + options.getNimbleDir() / nimbleBinariesDirName proc setNimbleDir*(options: var Options) = var @@ -492,6 +502,27 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = # Set run flags for custom task result.action.custRunFlags.add(getFlagString(kind, flag, val)) + of actionDevelop: + case f + of "c", "create": + result.action.devActions.add (datNewFile, val.normalizedPath) + of "a", "add": + result.action.devActions.add (datAdd, val.normalizedPath) + of "r", "remove-path": + result.action.devActions.add (datRemoveByPath, val.normalizedPath) + of "n", "remove-name": + result.action.devActions.add (datRemoveByName, val) + of "i", "include": + result.action.devActions.add (datInclude, val.normalizedPath) + of "e", "exclude": + result.action.devActions.add (datExclude, val.normalizedPath) + of "p", "path": + if result.action.path.len == 0: + result.action.path = val.normalizedPath + else: + raise nimbleError(multiplePathOptionsGivenMsg) + else: + wasFlagHandled = false else: wasFlagHandled = false @@ -504,7 +535,8 @@ proc initOptions*(): Options = action: Action(typ: actionNil), pkgInfoCache: newTable[string, PackageInfo](), verbosity: HighPriority, - noColor: not isatty(stdout) + noColor: not isatty(stdout), + startDir: getCurrentDir(), ) proc handleUnknownFlags(options: var Options) = @@ -564,8 +596,6 @@ proc parseCmdLine*(): Options = # Parse config. result.config = parseConfig() - result.nimbleData = parseNimbleDataFromDir(result.getNimbleDir()) - if result.action.typ == actionNil and not result.showVersion: result.showHelp = true @@ -604,18 +634,6 @@ proc getProxy*(options: Options): Proxy = else: return nil -proc briefClone*(options: Options): Options = - ## Clones the few important fields and creates a new Options object. - var newOptions = initOptions() - newOptions.config = options.config - newOptions.nimbleData = options.nimbleData - newOptions.nimbleDir = options.nimbleDir - newOptions.forcePrompts = options.forcePrompts - newOptions.pkgInfoCache = options.pkgInfoCache - newOptions.verbosity = options.verbosity - newOptions.nim = options.nim - return newOptions - proc shouldRemoveTmp*(options: Options, file: string): bool = result = true if options.verbosity <= DebugPriority: diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 1196cffe..ec128809 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -3,39 +3,28 @@ # Stdlib imports import system except TResult -import hashes, json, strutils, os, sets, tables, httpclient +import hashes, json, strutils, os, sets, tables, httpclient, strformat from net import SslError -from compiler/nimblecmd import getPathVersionChecksum - # Local imports import version, tools, common, options, cli, config, lockfile, packageinfotypes, - packagemetadata + packagemetadatafile + +proc isLoaded*(pkgInfo: PackageInfo): bool = + return pkgInfo.myPath.len > 0 proc hasMetaData*(pkgInfo: PackageInfo): bool = # if the package info has loaded meta data its files list have to be not empty pkgInfo.files.len > 0 -proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = - ## Splits ``pkgpath`` in the format - ## ``/home/user/.nimble/pkgs/package-0.1-febadeaea2345e777f0f6f8433f7f0a52edd5d1b`` - ## into ``("packagea", "0.1", "febadeaea2345e777f0f6f8433f7f0a52edd5d1b")`` - ## - ## Also works for file paths like: - ## ``/home/user/.nimble/pkgs/package-0.1-febadeaea2345e777f0f6f8433f7f0a52edd5d1b/package.nimble`` - if pkgPath.splitFile.ext in [".nimble", ".nimble-link", ".babel"]: - return getNameVersionChecksum(pkgPath.splitPath.head) - getPathVersionChecksum(pkgpath.splitPath.tail) - proc initPackageInfo*(filePath: string): PackageInfo = let (fileDir, fileName, _) = filePath.splitFile - result.myPath = filePath + result.myPath = filePath result.name = fileName result.backend = "c" result.lockedDependencies = getLockedDependencies(fileDir) proc toValidPackageName*(name: string): string = - result = "" for c in name: case c of '_', '-': @@ -86,15 +75,6 @@ proc fromJson(obj: JSonNode): Package = result.description = obj.requiredField("description") result.web = obj.optionalField("web") -proc readNimbleLink*(nimbleLinkPath: string): NimbleLink = - let s = readFile(nimbleLinkPath).splitLines() - result.nimbleFilePath = s[0] - result.packageDir = s[1] - -proc writeNimbleLink*(nimbleLinkPath: string, contents: NimbleLink) = - let c = contents.nimbleFilePath & "\n" & contents.packageDir - writeFile(nimbleLinkPath, c) - proc needsRefresh*(options: Options): bool = ## Determines whether a ``nimble refresh`` is needed. ## @@ -145,7 +125,7 @@ proc fetchList*(list: PackageList, options: Options) = client.downloadFile(url, tempPath) except SslError: let message = "Failed to verify the SSL certificate for " & url - raiseNimbleError(message, "Use --noSSLCheck to ignore this error.") + raise nimbleError(message, "Use --noSSLCheck to ignore this error.") except: let message = "Could not download: " & getCurrentExceptionMsg() @@ -237,8 +217,7 @@ proc getPackage*(name: string, options: Options): Package = proc getPackageList*(options: Options): seq[Package] = ## Returns the list of packages found in the downloaded packages.json files. - result = @[] - var namesAdded = initHashSet[string]() + var namesAdded: HashSet[string] for name, list in options.config.packageLists: let packages = readPackageList(name, options) for p in packages: @@ -248,13 +227,12 @@ proc getPackageList*(options: Options): seq[Package] = namesAdded.incl(pkg.name) proc findNimbleFile*(dir: string; error: bool): string = - result = "" var hits = 0 for kind, path in walkDir(dir): if kind in {pcFile, pcLinkToFile}: let ext = path.splitFile.ext case ext - of ".babel", ".nimble", ".nimble-link": + of ".babel", ".nimble": result = path inc hits else: discard @@ -266,18 +244,7 @@ proc findNimbleFile*(dir: string; error: bool): string = raise newException(NimbleError, "Could not find a file with a .nimble extension inside the specified directory: $1" % dir) else: - display("Warning:", "No .nimble or .nimble-link file found for " & - dir, Warning, HighPriority) - - if result.splitFile.ext == ".nimble-link": - # Return the path of the real .nimble file. - result = readNimbleLink(result).nimbleFilePath - if not fileExists(result): - let msg = "The .nimble-link file is pointing to a missing file: " & result - let hintMsg = - "Remove '$1' or restore the file it points to." % dir - display("Warning:", msg, Warning, HighPriority) - display("Hint:", hintMsg, Warning, HighPriority) + displayWarning(&"No .nimble file found for {dir}") proc setNameVersionChecksum*(pkgInfo: var PackageInfo, pkgDir: string) = let (name, version, checksum) = getNameVersionChecksum(pkgDir) @@ -293,19 +260,8 @@ proc getInstalledPackageMin*(pkgDir, nimbleFilePath: string): PackageInfo = setNameVersionChecksum(result, pkgDir) result.isMinimal = true result.isInstalled = true - fillMetaData(result, pkgDir, false) - if result.isLink: - # Read the package's 'srcDir' (this is stored in the .nimble-link so we can - # easily grab it). - let nimbleLinkPath = pkgDir / result.name.addFileExt("nimble-link") - let realSrcPath = readNimbleLink(nimbleLinkPath).packageDir - let nimbleFileDir = nimbleFilePath.splitFile().dir - assert realSrcPath.startsWith(nimbleFileDir) - result.srcDir = realSrcPath.replace(nimbleFileDir) - result.srcDir.removePrefix(DirSep) - proc getInstalledPkgsMin*(libsDir: string, options: Options): seq[PackageInfo] = ## Gets a list of installed packages. The resulting package info is ## minimal. This has the advantage that it does not depend on the @@ -343,15 +299,23 @@ proc findPkg*(pkglist: seq[PackageInfo], dep: PkgTuple, ## 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) + ## version number). If there is a package in develop mode indicated by its + ## `isLink` field being `true`, it should be only one for given package name, + ## and if its version is in the required range, it will be treated with the + ## highest priority. ## ## **Note**: dep.name here could be a URL, hence the need for pkglist.meta. for pkg in pkglist: if cmpIgnoreStyle(pkg.name, dep.name) != 0 and cmpIgnoreStyle(pkg.url, dep.name) != 0: continue + assert not (pkg.isLink and r.isLink), + "Should not happen the list to contain " & + "the same package in develop mode twice." if withinRange(pkg, dep.ver): let isNewer = newVersion(r.version) < newVersion(pkg.version) - if not result or isNewer: + # If `pkg.isLink` this is a develop mode package and develop mode packages + # are always with higher priority than installed packages. + if not result or isNewer or pkg.isLink: r = pkg result = true @@ -366,17 +330,20 @@ proc findAllPkgs*(pkglist: seq[PackageInfo], dep: PkgTuple): seq[PackageInfo] = if withinRange(pkg, dep.ver): result.add pkg +proc getNimbleFileDir*(pkgInfo: PackageInfo): string = + pkgInfo.myPath.splitFile.dir + proc getRealDir*(pkgInfo: PackageInfo): string = ## Returns the directory containing the package source files. if pkgInfo.srcDir != "" and (not pkgInfo.isInstalled or pkgInfo.isLink): - result = pkgInfo.mypath.splitFile.dir / pkgInfo.srcDir + result = pkgInfo.getNimbleFileDir() / pkgInfo.srcDir else: - result = pkgInfo.mypath.splitFile.dir + result = pkgInfo.getNimbleFileDir() proc getOutputDir*(pkgInfo: PackageInfo, bin: string): string = ## Returns a binary output dir for the package. if pkgInfo.binDir != "": - result = pkgInfo.mypath.splitFile.dir / pkgInfo.binDir / bin + result = pkgInfo.getNimbleFileDir() / pkgInfo.binDir / bin else: result = pkgInfo.mypath.splitFile.dir / bin if bin.len != 0 and dirExists(result): @@ -469,7 +436,7 @@ proc iterInstallFiles*(realDir: string, pkgInfo: PackageInfo, if options.prompt("Missing file " & src & ". Continue?"): continue else: - raise NimbleQuit(msg: "") + raise nimbleQuit() action(src) @@ -480,7 +447,7 @@ proc iterInstallFiles*(realDir: string, pkgInfo: PackageInfo, if options.prompt("Missing directory " & src & ". Continue?"): continue else: - raise NimbleQuit(msg: "") + raise nimbleQuit() iterFilesInDir(src, action) @@ -500,20 +467,26 @@ proc iterInstallFiles*(realDir: string, pkgInfo: PackageInfo, action(file) +proc getCacheDir*(pkgInfo: PackageBasicInfo): string = + &"{pkgInfo.name}-{pkgInfo.version}-{pkgInfo.checksum}" + +proc getPkgDest*(pkgInfo: PackageBasicInfo, options: Options): string = + options.getPkgsDir() / pkgInfo.getCacheDir() + proc getPkgDest*(pkgInfo: PackageInfo, options: Options): string = - let versionStr = '-' & pkgInfo.specialVersion & '-' & pkgInfo.checksum - let pkgDestDir = options.getPkgsDir() / (pkgInfo.name & versionStr) - return pkgDestDir + pkgInfo.basicInfo.getPkgDest(options) proc `==`*(pkg1: PackageInfo, pkg2: PackageInfo): bool = - if pkg1.name == pkg2.name and pkg1.myPath == pkg2.myPath: - return true + pkg1.myPath == pkg2.myPath proc hash*(x: PackageInfo): Hash = var h: Hash = 0 h = h !& hash(x.myPath) result = !$h +proc getNameAndVersion*(pkgInfo: PackageInfo): string = + &"{pkgInfo.name}@{pkgInfo.version}" + when isMainModule: import unittest diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index a1963256..0028fc54 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -5,12 +5,26 @@ import sets, tables import version, lockfile, aliasthis type - PackageMetaData* = object + PackageMetaDataBase* {.inheritable.} = object url*: string vcsRevision*: string files*: seq[string] binaries*: seq[string] + + PackageMetaDataV1* = object of PackageMetaDataBase + isLink*: bool + + PackageMetaDataV2* = object of PackageMetaDataBase + specialVersion*: string + + PackageMetaData* = object of PackageMetaDataBase isLink*: bool + specialVersion*: string + + PackageBasicInfo* = tuple + name: string + version: string + checksum: string PackageInfo* = object myPath*: string ## The path of this .nimble file @@ -20,11 +34,6 @@ type nimbleTasks*: HashSet[string] ## All tasks defined in the Nimble file postHooks*: HashSet[string] ## Useful to know so that Nimble doesn't execHook unnecessarily preHooks*: HashSet[string] - name*: string - ## The version specified in the .nimble file.Assuming info is non-minimal, - ## it will always be a non-special version such as '0.1.4' - version*: string - specialVersion*: string ## Either `myVersion` or a special version such as #head. author*: string description*: string license*: string @@ -40,8 +49,8 @@ type srcDir*: string backend*: string foreignDeps*: seq[string] + basicInfo*: PackageBasicInfo lockedDependencies*: LockFileDependencies - checksum*: string metaData*: PackageMetaData Package* = object ## Definition of package from packages.json. @@ -58,11 +67,7 @@ type web*: string # Info url for humans. alias*: string ## A name of another package, that this package aliases. - NimbleLink* = object - nimbleFilePath*: string - packageDir*: string - - PackageBasicInfo* = tuple[name, version, checksum: string] PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] aliasThis PackageInfo.metaData +aliasThis PackageInfo.basicInfo diff --git a/src/nimblepkg/packagemetadata.nim b/src/nimblepkg/packagemetadatafile.nim similarity index 55% rename from src/nimblepkg/packagemetadata.nim rename to src/nimblepkg/packagemetadatafile.nim index 56284d9a..70eb8427 100644 --- a/src/nimblepkg/packagemetadata.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -7,42 +7,41 @@ import common, packageinfotypes, cli, tools type MetaDataError* = object of NimbleError + PackageMetaDataJsonKeys = enum + pmdjkVersion = "version" + pmdjkMetaData = "metaData" + const packageMetaDataFileName* = "nimblemeta.json" + packageMetaDataFileVersion = "0.1.0" proc saveMetaData*(metaData: PackageMetaData, dirName: string) = ## Saves some important data to file in the package installation directory. - var metaDataWithChangedPaths = metaData + var metaDataWithChangedPaths = to(metaData, PackageMetaDataV2) for i, file in metaData.files: metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file) - let json = %metaDataWithChangedPaths + let json = %{ + $pmdjkVersion: %packageMetaDataFileVersion, + $pmdjkMetaData: %metaDataWithChangedPaths } writeFile(dirName / packageMetaDataFileName, json.pretty) -proc loadMetaData(dirName: string, raiseIfNotFound: bool): PackageMetaData = +proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData = ## Returns package meta data read from file in package installation directory let fileName = dirName / packageMetaDataFileName if fileExists(fileName): let json = parseFile(fileName) - result = json.to(result.typeof) + if not json.hasKey($pmdjkVersion): + result = to(json.to(PackageMetaDataV1), PackageMetaData) + let (_, specialVersion, _) = getNameVersionChecksum(dirName) + result.specialVersion = specialVersion + else: + result = to(json[$pmdjkMetaData].to(PackageMetaDataV2), PackageMetaData) elif raiseIfNotFound: raise newException(MetaDataError, &"No {packageMetaDataFileName} file found in {dirName}") else: - display("Warning:", &"No {packageMetaDataFileName} file found in {dirName}", - Warning, HighPriority) + displayWarning(&"No {packageMetaDataFileName} file found in {dirName}") proc fillMetaData*(packageInfo: var PackageInfo, dirName: string, raiseIfNotFound: bool) = - # Save the VCS revision possibly previously obtained in `initPackageInfo` from - # the `.nimble` file directory to not be overridden from this read by meta - # data file in the case the package is in develop mode. - let vcsRevision = packageInfo.vcsRevision - packageInfo.metaData = loadMetaData(dirName, raiseIfNotFound) - - if packageInfo.isLink: - # If this is a linked package the real VCS revision from the `.nimble` file - # directory obtained in `initPackageInfo` is the actual one, but not this - # written in the package meta data in the time of the linking of the - # package. - packageInfo.vcsRevision = vcsRevision diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index f0ef0237..57162599 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -3,8 +3,8 @@ import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map, toSeq -import common, version, tools, nimscriptwrapper, options, cli, packagemetadata, - packageinfo, packageinfotypes, checksum +import common, version, tools, nimscriptwrapper, options, cli, + packagemetadatafile, packageinfo, packageinfotypes, checksum ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` ## because it depends on ``nimscriptwrapper`` (``nimscriptwrapper`` also @@ -350,6 +350,7 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): return result = initPackageInfo(nf) + result.isLink = not nf.startsWith(options.getPkgsDir) validatePackageName(nf.splitFile.name) @@ -367,11 +368,6 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): if onlyMinimalInfo: result.isNimScript = true result.isMinimal = true - - # It's possible this proc will receive a .nimble-link file eventually, - # I added this assert to hopefully make this error clear for everyone. - let msg = "No version detected. Received nimble-link?" - assert result.version.len > 0, msg else: try: readPackageInfoFromNims(nf, options, result) @@ -411,17 +407,8 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): validateVersion(result.version) validatePackageInfo(result, options) -proc validate*(file: NimbleFile, options: Options, - error: var ValidationError, pkgInfo: var PackageInfo): bool = - try: - pkgInfo = readPackageInfo(file, options) - except ValidationError as exc: - error = exc[] - return false - - return true - -proc getPkgInfoFromFile*(file: NimbleFile, options: Options): PackageInfo = +proc getPkgInfoFromFile*(file: NimbleFile, options: Options, + forValidation = false): PackageInfo = ## Reads the specified .nimble file and returns its data as a PackageInfo ## object. Any validation errors are handled and displayed as warnings. var info: PackageInfo @@ -429,7 +416,7 @@ proc getPkgInfoFromFile*(file: NimbleFile, options: Options): PackageInfo = info = readPackageInfo(file, options) except ValidationError: let exc = (ref ValidationError)(getCurrentException()) - if exc.warnAll: + if exc.warnAll and not forValidation: display("Warning:", exc.msg, Warning, HighPriority) display("Hint:", exc.hint, Warning, HighPriority) else: @@ -437,10 +424,11 @@ proc getPkgInfoFromFile*(file: NimbleFile, options: Options): PackageInfo = finally: result = info -proc getPkgInfo*(dir: string, options: Options): PackageInfo = +proc getPkgInfo*(dir: string, options: Options, forValidation = false): + PackageInfo = ## Find the .nimble file in ``dir`` and parses it, returning a PackageInfo. let nimbleFile = findNimbleFile(dir, true) - result = getPkgInfoFromFile(nimbleFile, options) + result = getPkgInfoFromFile(nimbleFile, options, forValidation) proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfo] = ## Gets a list of installed packages. @@ -496,22 +484,21 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = if pkg.isMinimal: result = getPkgInfoFromFile(pkg.mypath, options) result.isInstalled = pkg.isInstalled + # The `isLink` data from the meta data file is with priority because of the + # old format develop packages. result.isLink = pkg.isLink result.specialVersion = pkg.specialVersion - if pkg.hasMetaData: + + assert not (pkg.isInstalled and pkg.isLink), + "A package must not be simultaneously installed and linked." + + if result.isInstalled: + assert result.vcsRevision.len == 0, + "Should not have a VCS revision read from package directory for " & + "installed packages." + + # For installed packages use already read meta data. result.metaData = pkg.metaData - if pkg.isInstalled and not pkg.isLink: - if result.vcsRevision.len == 0: - # If this is an installed package and it is not a linked package, then - # there should not be a VCS revision read from the package directory and - # this read previously from the meta data should be used. - result.vcsRevision = pkg.vcsRevision - else: - # But if the package meta data file is missing and the package is - # incorrectly identified as not a linked package, then there should be a - # VCS revision read from the real package directory, which should be - # preserved by not overwriting it with an empty revision. - assert pkg.vcsRevision.len == 0 else: return pkg diff --git a/src/nimblepkg/paths.nim b/src/nimblepkg/paths.nim new file mode 100644 index 00000000..eb1daf15 --- /dev/null +++ b/src/nimblepkg/paths.nim @@ -0,0 +1,30 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +## This module implements operations with file system paths in a way independent +## of weather the path is absolute or relative to the current directory. + +import os, json, hashes + +type Path* = distinct string + +converter toPath*(path: string): Path = Path(path) + +proc `%`*(path: Path): JsonNode {.borrow.} +proc `$`*(path: Path): string {.borrow.} + +proc isAbsolute*(path: Path): bool {.borrow.} +proc splitFile*(path: Path): tuple[dir, name, ext: Path] {.borrow.} +proc splitPath*(path: Path): tuple[head, tail: Path] {.borrow.} +proc normalizedPath*(path: Path): Path {.borrow.} +proc dirExists*(dirname: Path): bool {.borrow.} +proc fileExists*(filename: Path): bool {.borrow.} +proc parseFile*(filename: Path): JsonNode {.borrow.} +proc `/`*(head, tail: Path): Path {.borrow.} +proc writeFile*(filename: Path, content: string) {.borrow.} +proc len*(path: Path): int {.borrow.} + +proc hash*(path: Path): Hash = hash(absolutePath(string(path))) + +proc `==`*(lhs, rhs: Path): bool = + absolutePath(string(lhs)) == absolutePath(string(rhs)) diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 96ed22d9..2738feb1 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -1,10 +1,48 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import json, sets - -import common, options, version, download, jsonhelpers, - packageinfotypes, packageinfo +import json, sets, os, hashes, unicode + +import options, version, download, jsonhelpers, nimbledatafile, + packageinfotypes, packageinfo, packageparser + +type + ReverseDependencyKind* = enum + rdkInstalled, + rdkDevelop, + + ReverseDependency* = object + ## Represents a reverse dependency info containing name, version and + ## checksum for the installed packages or the path to the package directory + ## for the reverse dependencies. + case kind*: ReverseDependencyKind + of rdkInstalled: + pkgInfo*: PackageBasicInfo + of rdkDevelop: + pkgPath*: string + +proc hash*(revDep: ReverseDependency): Hash = + case revDep.kind + of rdkInstalled: + result = revDep.pkgInfo.getCacheDir.hash + of rdkDevelop: + result = revDep.pkgPath.hash + +proc `==`*(lhs, rhs: ReverseDependency): bool = + if lhs.kind != rhs.kind: + return false + case lhs.kind: + of rdkInstalled: + return lhs.pkgInfo == rhs.pkgInfo + of rdkDevelop: + return lhs.pkgPath == rhs.pkgPath + +proc `$`*(revDep: ReverseDependency): string = + case revDep.kind + of rdkInstalled: + result = revDep.pkgInfo.getCacheDir + of rdkDevelop: + result = revDep.pkgPath proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, pkg: PackageInfo) = @@ -13,23 +51,26 @@ proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, let dependencies = nimbleData.addIfNotExist( $ndjkRevDep, - dep.name, + dep.name.toLower, dep.version, dep.checksum, - newJArray(), - ) + newJArray()) - let dependency = %{ - $ndjkRevDepName: %pkg.name, - $ndjkRevDepVersion: %pkg.specialVersion, - $ndjkRevDepChecksum: %pkg.checksum, - } + var dependency: JsonNode + if not pkg.isLink: + dependency = %{ + $ndjkRevDepName: %pkg.name.toLower, + $ndjkRevDepVersion: %pkg.version, + $ndjkRevDepChecksum: %pkg.checksum} + else: + dependency = %{ $ndjkRevDepPath: %pkg.getNimbleFileDir().absolutePath } if dependency notin dependencies: dependencies.add(dependency) 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) = @@ -38,13 +79,26 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = for checksum, revDepsForChecksum in revDepsForVersion: var newVal = newJArray() for rd in revDepsForChecksum: - # if the reverse dependency is different than the package which we + # If the reverse dependency is different than the package which we # currently deleting, it will be kept. - if rd[$ndjkRevDepName].str != pkg.name or - rd[$ndjkRevDepVersion].str != pkg.specialVersion or - rd[$ndjkRevDepChecksum].str != pkg.checksum: + if rd.hasKey($ndjkRevDepPath): + # This is a develop mode reverse dependency. + if rd[$ndjkRevDepPath].str != pkg.getNimbleFileDir: + # It is compared by its directory path. + newVal.add rd + elif rd[$ndjkRevDepChecksum].str != pkg.checksum: + # For the reverse dependencies added since the introduction of the + # new format comparison of the checksums is specific enough. newVal.add rd - revDepsForVersion[checksum] = newVal + else: + # But if the both checksums are not present, those are converted + # from the old format packages and they must be compared by the + # `name` and `specialVersion` fields. + if rd[$ndjkRevDepChecksum].str.len == 0 and pkg.checksum.len == 0: + if rd[$ndjkRevDepName].str != pkg.name.toLower or + rd[$ndjkRevDepVersion].str != pkg.specialVersion: + newVal.add rd + revDepsForVersion[checksum] = newVal let reverseDependencies = nimbleData[$ndjkRevDep] @@ -54,211 +108,244 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = for key, val in reverseDependencies: remove(pkg, depTup, val) else: - let thisDep = nimbleData{$ndjkRevDep, depTup.name} + let thisDep = nimbleData{$ndjkRevDep, depTup.name.toLower} if thisDep.isNil: continue remove(pkg, depTup, thisDep) nimbleData[$ndjkRevDep] = cleanUpEmptyObjects(reverseDependencies) -proc getRevDepTups*(options: Options, pkg: PackageInfo): seq[PkgTuple] = - ## Returns a list of *currently installed* reverse dependencies for `pkg`. - result = @[] - let thisPkgsDep = options.nimbleData[$ndjkRevDep]{ - pkg.name}{pkg.specialVersion}{pkg.checksum} - if not thisPkgsDep.isNil: - let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - for pkg in thisPkgsDep: - let pkgTup = ( - name: pkg[$ndjkRevDepName].getStr(), - ver: parseVersionRange(pkg[$ndjkRevDepVersion].getStr()), - ) - var pkgInfo: PackageInfo - if not findPkg(pkgList, pkgTup, pkgInfo): - continue - - result.add(pkgTup) - -proc getRevDeps*(options: Options, pkg: PackageInfo): HashSet[PackageInfo] = - result.init() - let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) - for rdepTup in getRevDepTups(options, pkg): - for rdepInfo in findAllPkgs(installedPkgs, rdepTup): - result.incl rdepInfo - -proc getAllRevDeps*(options: Options, pkg: PackageInfo, - result: var HashSet[PackageInfo]) = - if pkg in result: +proc getRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency): + HashSet[ReverseDependency] = + ## Returns a list of *currently installed* or *develop mode* reverse + ## dependencies for `pkg`. + + if pkg.kind == rdkDevelop: return - let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) - for rdepTup in getRevDepTups(options, pkg): - for rdepInfo in findAllPkgs(installedPkgs, rdepTup): - if rdepInfo in result: - continue + let reverseDependencies = nimbleData[$ndjkRevDep]{ + pkg.pkgInfo.name.toLower}{pkg.pkgInfo.version}{pkg.pkgInfo.checksum} - getAllRevDeps(options, rdepInfo, result) + if reverseDependencies.isNil: + return + + for revDep in reverseDependencies: + if revDep.hasKey($ndjkRevDepPath): + # This is a develop mode package. + let path = revDep[$ndjkRevDepPath].str + result.incl ReverseDependency(kind: rdkDevelop, pkgPath: path) + else: + # This is an installed package. + let pkgBasicInfo = (name: revDep[$ndjkRevDepName].str, + version: revDep[$ndjkRevDepVersion].str, + checksum: revDep[$ndjkRevDepChecksum].str) + result.incl ReverseDependency(kind: rdkInstalled, pkgInfo: pkgBasicInfo) + +proc toPkgInfo*(revDep: ReverseDependency, options: Options): PackageInfo = + case revDep.kind + of rdkInstalled: + let pkgDir = revDep.pkgInfo.getPkgDest(options) + result = getPkgInfo(pkgDir, options) + of rdkDevelop: + result = getPkgInfo(revDep.pkgPath, options) + +proc toRevDep*(pkg: PackageInfo): ReverseDependency = + if not pkg.isLink: + result = ReverseDependency( + kind: rdkInstalled, + pkgInfo: (pkg.name, pkg.version, pkg.checksum)) + else: + result = ReverseDependency( + kind: rdkDevelop, + pkgPath: pkg.getNimbleFileDir) + +proc getAllRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency, + result: var HashSet[ReverseDependency]) = result.incl pkg + let revDeps = getRevDeps(nimbleData, pkg) + for revDep in revDeps: + if revDep in result: continue + getAllRevDeps(nimbleData, revDep, result) when isMainModule: - import unittest - import nimbledata - - 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"))], - checksum: "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2", - ) - - let nimforum2 = PackageInfo( - isMinimal: false, - name: "nimforum", - specialVersion: "0.2.0", - checksum: "B60044137CEA185F287346EBEAB6B3E0895BDA4D", - ) - - let play = PackageInfo( - isMinimal: false, - name: "play", - specialVersion: "#head", - checksum: "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" - ) - - proc setupNimbleData(): JsonNode = - result = newNimbleDataNode() - - result.addRevDep( - ("jester", "0.1.0", "1B629F98B23614DF292F176A1681FA439DCC05E2"), - nimforum1) - - result.addRevDep(("jester", "0.1.0", ""), play) - - result.addRevDep( - ("captcha", "1.0.0", "CE128561B06DD106A83638AD415A2A52548F388E"), - nimforum1) - - result.addRevDep( - ("auth", "#head", "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01"), - nimforum1) - - result.addRevDep( - ("captcha", "1.0.0", "CE128561B06DD106A83638AD415A2A52548F388E"), - nimforum2) - - result.addRevDep( - ("auth", "#head", "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01"), - nimforum2) - - proc testAddRevDep() = - - let expectedResult = """{ - "version": "0.1.0", - "reverseDeps": { - "jester": { - "0.1.0": { - "1B629F98B23614DF292F176A1681FA439DCC05E2": [ - { - "name": "nimforum", - "version": "0.1.0", - "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" - } - ], - "": [ - { - "name": "play", - "version": "#head", - "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" - } - ] - } - }, - "captcha": { - "1.0.0": { - "CE128561B06DD106A83638AD415A2A52548F388E": [ - { - "name": "nimforum", - "version": "0.1.0", - "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" - }, - { - "name": "nimforum", - "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" - } - ] - } - }, - "auth": { - "#head": { - "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ - { - "name": "nimforum", - "version": "0.1.0", - "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" - }, - { - "name": "nimforum", - "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" - } - ] + + let + nimforum1 = PackageInfo( + basicInfo: + ("nimforum", "0.1.0", "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2"), + requires: @[("jester", parseVersionRange("0.1.0")), + ("captcha", parseVersionRange("1.0.0")), + ("auth", parseVersionRange("#head"))]) + + nimforum1RevDep = nimforum1.toRevDep + + nimforum2 = PackageInfo(basicInfo: + ("nimforum", "0.2.0", "B60044137CEA185F287346EBEAB6B3E0895BDA4D")) + + nimforum2RevDep = nimforum2.toRevDep + + play = PackageInfo( + basicInfo: ("play", "2.0.1", "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9")) + + nimforumDevelop = PackageInfo( + myPath: "/some/absolute/system/path/nimforum/nimforum.nimble", + metaData: PackageMetaData(isLink: true), + requires: @[("captcha", parseVersionRange("1.0.0"))]) + + nimforumDevelopRevDep = nimforumDevelop.toRevDep + + jester = PackageInfo(basicInfo: + ("jester", "0.1.0", "1B629F98B23614DF292F176A1681FA439DCC05E2")) + + jesterWithoutSha1 = PackageInfo(basicInfo: ("jester", "0.1.0", "")) + + captcha = PackageInfo(basicInfo: + ("captcha", "1.0.0", "CE128561B06DD106A83638AD415A2A52548F388E")) + + captchaRevDep = captcha.toRevDep + + auth = PackageInfo( + basicInfo: ("auth", "#head", "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01")) + + authRevDep = auth.toRevDep + + suite "reverse dependencies": + setup: + var nimbleData = newNimbleDataNode() + nimbleData.addRevDep(jester.basicInfo, nimforum1) + nimbleData.addRevDep(jesterWithoutSha1.basicInfo, play) + nimbleData.addRevDep(captcha.basicInfo, nimforum1) + nimbleData.addRevDep(captcha.basicInfo, nimforum2) + nimbleData.addRevDep(captcha.basicInfo, nimforumDevelop) + nimbleData.addRevDep(auth.basicInfo, nimforum1) + nimbleData.addRevDep(auth.basicInfo, nimforum2) + nimbleData.addRevDep(auth.basicInfo, captcha) + + test "addRevDep": + let expectedResult = """{ + "version": "0.1.0", + "reverseDeps": { + "jester": { + "0.1.0": { + "1B629F98B23614DF292F176A1681FA439DCC05E2": [ + { + "name": "nimforum", + "version": "0.1.0", + "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + } + ], + "": [ + { + "name": "play", + "version": "2.0.1", + "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + } + ] + } + }, + "captcha": { + "1.0.0": { + "CE128561B06DD106A83638AD415A2A52548F388E": [ + { + "name": "nimforum", + "version": "0.1.0", + "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + }, + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + }, + { + "path": "/some/absolute/system/path/nimforum" + } + ] + } + }, + "auth": { + "#head": { + "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ + { + "name": "nimforum", + "version": "0.1.0", + "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + }, + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + }, + { + "name": "captcha", + "version": "1.0.0", + "checksum": "CE128561B06DD106A83638AD415A2A52548F388E" + } + ] + } } } - } - }""".parseJson() - - let nimbleData = setupNimbleData() - check nimbleData == expectedResult - - proc testRemoveRevDep() = - - let expectedResult = """{ - "version": "0.1.0", - "reverseDeps": { - "jester": { - "0.1.0": { - "": [ - { - "name": "play", - "version": "#head", - "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" - } - ] - } - }, - "captcha": { - "1.0.0": { - "CE128561B06DD106A83638AD415A2A52548F388E": [ - { - "name": "nimforum", - "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" - } - ] - } - }, - "auth": { - "#head": { - "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ - { - "name": "nimforum", - "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" - } - ] + }""".parseJson() + + check nimbleData == expectedResult + + test "removeRevDep": + let expectedResult = """{ + "version": "0.1.0", + "reverseDeps": { + "jester": { + "0.1.0": { + "": [ + { + "name": "play", + "version": "2.0.1", + "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + } + ] + } + }, + "captcha": { + "1.0.0": { + "CE128561B06DD106A83638AD415A2A52548F388E": [ + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + } + ] + } + }, + "auth": { + "#head": { + "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ + { + "name": "nimforum", + "version": "0.2.0", + "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + }, + { + "name": "captcha", + "version": "1.0.0", + "checksum": "CE128561B06DD106A83638AD415A2A52548F388E" + } + ] + } } } - } - }""".parseJson() - - let nimbleData = setupNimbleData() - nimbleData.removeRevDep(nimforum1) - check nimbleData == expectedResult - - testAddRevDep() - testRemoveRevDep() - reportUnitTestSuccess() + }""".parseJson() + + nimbleData.removeRevDep(nimforum1) + nimbleData.removeRevDep(nimforumDevelop) + check nimbleData == expectedResult + + test "getRevDeps": + check nimbleData.getRevDeps(nimforumDevelopRevDep) == + HashSet[ReverseDependency]() + check nimbleData.getRevDeps(captchaRevDep) == + [nimforum1RevDep, nimforum2RevDep, nimforumDevelopRevDep].toHashSet + + test "getAllRevDeps": + var revDeps: HashSet[ReverseDependency] + nimbleData.getAllRevDeps(authRevDep, revDeps) + check revDeps == [authRevDep, nimforum1RevDep, nimforum2RevDep, + nimforumDevelopRevDep, captchaRevDep].toHashSet + \ No newline at end of file diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 72f20452..c41fbe2d 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -4,10 +4,11 @@ # Various miscellaneous utility functions reside here. import osproc, pegs, strutils, os, uri, sets, json, parseutils, strformat, sequtils - -import common, version, cli, options from net import SslCVerifyMode, newContext, SslContext +import version, cli, common, packageinfotypes, options +from compiler/nimblecmd import getPathVersionChecksum + proc extractBin(cmd: string): string = if cmd[0] == '"': return cmd.captureBetween('"') @@ -57,14 +58,10 @@ proc tryDoCmdEx*(cmd: string): string = fmt"Execution of '{cmd}' failed with an exit code {exitCode}") return output -template cd*(dir: string, body: untyped) = - ## Sets the current dir to ``dir``, executes ``body`` and restores the - ## previous working dir. - let lastDir = getCurrentDir() - setCurrentDir(dir) - block: - defer: setCurrentDir(lastDir) - body +proc getNimBin*: string = + result = "nim" + if findExe("nim") != "": result = findExe("nim") + elif findExe("nimrod") != "": result = findExe("nimrod") proc getNimrodVersion*(options: Options): Version = let vOutput = doCmdEx(getNimBin(options).quoteShell & " -v").output @@ -121,7 +118,6 @@ proc createDirD*(dir: string) = proc getDownloadDirName*(uri: string, verRange: VersionRange, vcsRevision: string): string = ## Creates a directory name based on the specified ``uri`` (url) - result = "" let puri = parseUri(uri) for i in puri.hostname: case i @@ -201,6 +197,31 @@ proc getVcsRevisionFromDir*(dir: string): string = proc isEmptyDir*(dir: string): bool = toSeq(walkDirRec(dir)).len == 0 +proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = + ## Splits ``pkgpath`` in the format + ## ``/home/user/.nimble/pkgs/package-0.1-febadeaea2345e777f0f6f8433f7f0a52edd5d1b`` + ## into ``("packagea", "0.1", "febadeaea2345e777f0f6f8433f7f0a52edd5d1b")`` + ## + ## Also works for file paths like: + ## ``/home/user/.nimble/pkgs/package-0.1-febadeaea2345e777f0f6f8433f7f0a52edd5d1b/package.nimble`` + if pkgPath.splitFile.ext in [".nimble", ".babel"]: + return getNameVersionChecksum(pkgPath.splitPath.head) + getPathVersionChecksum(pkgpath.splitPath.tail) + +proc removePackageDir*(files: seq[string], dir: string, reportSuccess = false) = + for file in files: + removeFile(dir / file) + + if dir.isEmptyDir(): + removeDir(dir) + if reportSuccess: + displaySuccess(&"The directory \"{dir}\" has been removed.", + MediumPriority) + else: + displayWarning( + &"Cannot completely remove the directory \"{dir}\".\n" & + "Files not installed by Nimble are present.") + proc newSSLContext*(disabled: bool): SslContext = var sslVerifyMode = CVerifyPeer if disabled: diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index 32254ff9..7beaf1c7 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -1,10 +1,10 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import sequtils, sugar, tables, strformat, algorithm +import sequtils, sugar, tables, strformat, algorithm, sets import packageinfotypes, packageinfo, options, cli, lockfile -proc buildDependencyGraph*(packages: seq[PackageInfo], options: Options): +proc buildDependencyGraph*(packages: HashSet[PackageInfo], options: Options): LockFileDependencies = ## Creates records which will be saved to the lock file. diff --git a/tests/.gitignore b/tests/.gitignore index cfa6829e..acf3fe7c 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -5,6 +5,7 @@ /buildDir /binaryPackage/v1/binaryPackage /binaryPackage/v2/binaryPackage +/develop/dependent/dependent /develop/dependent/src/dependent /issue27/issue27 /issue206/issue/issue206bin @@ -13,6 +14,7 @@ /issue564/issue564/ /nimbleDir/ /nimbleVersionDefine/nimbleVersionDefine +/nimbleVersionDefine/src/nimbleVersionDefine /packageStructure/c/c /packageStructure/y/y /run/run @@ -27,12 +29,17 @@ /testCommand/testsFail/tests/t2 /passNimFlags/passNimFlags /issue799/issue799 +/issue308515/v1/binname-2 /issue308515/v1/binname.out +/issue308515/v2/binname-2 /issue308515/v2/binname.out +/issue399/tools/ +/issue727/def +/issue727/src/abc +/issue793/issue793 +/issue793/src/htmldocs/ +/localdeps/localdeps /localdeps/nimbledeps/ -/multi/ -/packagea/ - # occurs on multiple levels nimbleDir/ diff --git a/tests/develop/dependency/dependency.nim b/tests/develop/dependency/dependency.nim new file mode 100644 index 00000000..7e716e07 --- /dev/null +++ b/tests/develop/dependency/dependency.nim @@ -0,0 +1,9 @@ +import packagea + +proc test*(): string = + when defined(windows): + $PackageA.test(6, 9) + elif defined(unix): + $packagea.test(6, 9) + else: + {.error: "Sorry, your platform is not supported.".} diff --git a/tests/develop/dependency/dependency.nimble b/tests/develop/dependency/dependency.nimble new file mode 100644 index 00000000..85b986c3 --- /dev/null +++ b/tests/develop/dependency/dependency.nimble @@ -0,0 +1,7 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "test dependency" +license = "MIT" + +# Dependencies +requires "nim >= 1.2.4", "packagea" diff --git a/tests/develop/dependency2/dependency.nim b/tests/develop/dependency2/dependency.nim new file mode 100644 index 00000000..561c3348 --- /dev/null +++ b/tests/develop/dependency2/dependency.nim @@ -0,0 +1 @@ +proc test*(): string = "15" diff --git a/tests/develop/dependency2/dependency.nimble b/tests/develop/dependency2/dependency.nimble new file mode 100644 index 00000000..25591227 --- /dev/null +++ b/tests/develop/dependency2/dependency.nimble @@ -0,0 +1,7 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "test dependency" +license = "MIT" + +# Dependencies +requires "nim >= 1.2.4" \ No newline at end of file diff --git a/tests/develop/dependent/dependent.nimble b/tests/develop/dependent/dependent.nimble index 7bab4b7c..c73ce83e 100644 --- a/tests/develop/dependent/dependent.nimble +++ b/tests/develop/dependent/dependent.nimble @@ -5,8 +5,9 @@ author = "Dominik Picheta" description = "dependent" license = "MIT" +bin = @["dependent"] srcDir = "src" # Dependencies -requires "nim >= 0.16.0", "srcdirtest" +requires "nim >= 0.16.0", "dependency" diff --git a/tests/develop/dependent/src/dependent.nim b/tests/develop/dependent/src/dependent.nim index 37231bae..88b253f6 100644 --- a/tests/develop/dependent/src/dependent.nim +++ b/tests/develop/dependent/src/dependent.nim @@ -1,3 +1,3 @@ -import srcdirtest +import dependency -doAssert foo() == "correct" \ No newline at end of file +doAssert test() == "15" diff --git a/tests/develop/dependent2/dependent.nimble b/tests/develop/dependent2/dependent.nimble new file mode 100644 index 00000000..1611d119 --- /dev/null +++ b/tests/develop/dependent2/dependent.nimble @@ -0,0 +1,13 @@ +# Package + +version = "1.0" +author = "Dominik Picheta" +description = "dependent" +license = "MIT" + +bin = @["dependent"] +srcDir = "src" + +# Dependencies + +requires "nim >= 0.16.0", "dependency >= 0.2.0" diff --git a/tests/develop/dependent2/src/dependent.nim b/tests/develop/dependent2/src/dependent.nim new file mode 100644 index 00000000..88b253f6 --- /dev/null +++ b/tests/develop/dependent2/src/dependent.nim @@ -0,0 +1,3 @@ +import dependency + +doAssert test() == "15" diff --git a/tests/develop/packages.json b/tests/develop/packages.json new file mode 100644 index 00000000..f201c08f --- /dev/null +++ b/tests/develop/packages.json @@ -0,0 +1,18 @@ +[ + { + "name": "packagea", + "url": "https://github.com/nimble-test/packagea.git", + "method": "git", + "tags": [ "test" ], + "description": "A test package.", + "license": "MIT" + }, + { + "name": "packageb", + "url": "https://github.com/nimble-test/packageb.git", + "method": "git", + "tags": [ "test" ], + "description": "A test package.", + "license": "MIT" + } +] diff --git a/tests/develop/pkg1/pkg1.nim b/tests/develop/pkg1/pkg1.nim new file mode 100644 index 00000000..4cf7db86 --- /dev/null +++ b/tests/develop/pkg1/pkg1.nim @@ -0,0 +1,5 @@ +import pkg2 + +proc foo*() = + echo "pkg1" + pkg2.foo() diff --git a/tests/develop/pkg1/pkg1.nimble b/tests/develop/pkg1/pkg1.nimble new file mode 100644 index 00000000..aad1b396 --- /dev/null +++ b/tests/develop/pkg1/pkg1.nimble @@ -0,0 +1,8 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "test package" +license = "MIT" + +bin = @["pkg1"] + +requires "nim >= 1.2.6", "pkg2", "pkg3" diff --git a/tests/develop/pkg2.2/pkg2.nim b/tests/develop/pkg2.2/pkg2.nim new file mode 100644 index 00000000..3cc477eb --- /dev/null +++ b/tests/develop/pkg2.2/pkg2.nim @@ -0,0 +1,5 @@ +import pkg3 + +proc foo*() = + echo "pkg2" + pkg3.foo() diff --git a/tests/develop/pkg2.2/pkg2.nimble b/tests/develop/pkg2.2/pkg2.nimble new file mode 100644 index 00000000..7b568439 --- /dev/null +++ b/tests/develop/pkg2.2/pkg2.nimble @@ -0,0 +1,6 @@ +version = "0.2.0" +author = "Ivan Bobev" +description = "test package" +license = "MIT" + +requires "nim >= 1.2.6" diff --git a/tests/develop/pkg2/pkg2.nim b/tests/develop/pkg2/pkg2.nim new file mode 100644 index 00000000..3cc477eb --- /dev/null +++ b/tests/develop/pkg2/pkg2.nim @@ -0,0 +1,5 @@ +import pkg3 + +proc foo*() = + echo "pkg2" + pkg3.foo() diff --git a/tests/develop/pkg2/pkg2.nimble b/tests/develop/pkg2/pkg2.nimble new file mode 100644 index 00000000..b2811442 --- /dev/null +++ b/tests/develop/pkg2/pkg2.nimble @@ -0,0 +1,6 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "test package" +license = "MIT" + +requires "nim >= 1.2.6", "pkg3" diff --git a/tests/develop/pkg3.2/pkg3.nim b/tests/develop/pkg3.2/pkg3.nim new file mode 100644 index 00000000..f25bce3a --- /dev/null +++ b/tests/develop/pkg3.2/pkg3.nim @@ -0,0 +1,2 @@ +proc foo*() = + echo "pkg1" diff --git a/tests/develop/pkg3.2/pkg3.nimble b/tests/develop/pkg3.2/pkg3.nimble new file mode 100644 index 00000000..7b568439 --- /dev/null +++ b/tests/develop/pkg3.2/pkg3.nimble @@ -0,0 +1,6 @@ +version = "0.2.0" +author = "Ivan Bobev" +description = "test package" +license = "MIT" + +requires "nim >= 1.2.6" diff --git a/tests/develop/pkg3/pkg3.nim b/tests/develop/pkg3/pkg3.nim new file mode 100644 index 00000000..f25bce3a --- /dev/null +++ b/tests/develop/pkg3/pkg3.nim @@ -0,0 +1,2 @@ +proc foo*() = + echo "pkg1" diff --git a/tests/develop/pkg3/pkg3.nimble b/tests/develop/pkg3/pkg3.nimble new file mode 100644 index 00000000..ad7b5f95 --- /dev/null +++ b/tests/develop/pkg3/pkg3.nimble @@ -0,0 +1,6 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "test package" +license = "MIT" + +requires "nim >= 1.2.6" diff --git a/tests/nim.cfg b/tests/nim.cfg index 4cdbf076..1887b8b0 100644 --- a/tests/nim.cfg +++ b/tests/nim.cfg @@ -1 +1,2 @@ +--path:"$nim/" --path:"../src/" diff --git a/tests/tester.nim b/tests/tester.nim index d0cb3498..97df3bec 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1,38 +1,42 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. import osproc, unittest, strutils, os, sequtils, sugar, json, std/sha1, - strformat + strformat, macros, sets -from nimblepkg/common import ProcessOutput -from nimblepkg/nimbledata import parseNimbleData +import nimblepkg/common, nimblepkg/displaymessages, nimblepkg/paths + +from nimblepkg/developfile import + developFileName, developFileVersion, pkgFoundMoreThanOnceMsg +from nimblepkg/nimbledatafile import + loadNimbleData, nimbleDataFileName, NimbleDataJsonKeys +from nimblepkg/version import VersionRange, parseVersionRange # TODO: Each test should start off with a clean slate. Currently installed # packages are shared between each test which causes a multitude of issues # and is really fragile. -let rootDir = getCurrentDir().parentDir() -let nimblePath = rootDir / "src" / addFileExt("nimble", ExeExt) -let installDir = rootDir / "tests" / "nimbleDir" -let buildTests = rootDir / "buildTests" -let pkgsDir = installDir / "pkgs" - -const path = "../src/nimble" -const stringNotFound = -1 +const + stringNotFound = -1 + pkgAUrl = "https://github.com/nimble-test/packagea.git" + pkgBUrl = "https://github.com/nimble-test/packageb.git" + pkgBinUrl = "https://github.com/nimble-test/packagebin.git" + pkgBin2Url = "https://github.com/nimble-test/packagebin2.git" + pkgMultiUrl = "https://github.com/nimble-test/multi" + pkgMultiAlphaUrl = &"{pkgMultiUrl}?subdir=alpha" + pkgMultiBetaUrl = &"{pkgMultiUrl}?subdir=beta" + +let + rootDir = getCurrentDir().parentDir() + nimblePath = rootDir / "src" / addFileExt("nimble", ExeExt) + installDir = rootDir / "tests" / "nimbleDir" + buildTests = rootDir / "buildTests" + pkgsDir = installDir / nimblePackagesDirName # Set env var to propagate nimble binary path putEnv("NIMBLE_TEST_BINARY_PATH", nimblePath) # Always recompile. -doAssert execCmdEx("nim c -d:danger " & path).exitCode == QuitSuccess - -template cd*(dir: string, body: untyped) = - ## Sets the current dir to ``dir``, executes ``body`` and restores the - ## previous working dir. - block: - let lastDir = getCurrentDir() - setCurrentDir(dir) - body - setCurrentDir(lastDir) +doAssert execCmdEx("nim c -d:danger " & nimblePath).exitCode == QuitSuccess proc execNimble(args: varargs[string]): ProcessOutput = var quotedArgs = @args @@ -81,9 +85,31 @@ proc processOutput(output: string): seq[string] = ) ) -proc inLines(lines: seq[string], line: string): bool = - for i in lines: - if line.normalize in i.normalize: return true +macro defineInLinesProc(procName, extraLine: untyped): untyped = + var LinesType = quote do: seq[string] + if extraLine[0].kind != nnkDiscardStmt: + LinesType = newTree(nnkVarTy, LinesType) + + let linesParam = ident("lines") + let linesLoopCounter = ident("i") + + result = quote do: + proc `procName`(`linesParam`: `LinesType`, msg: string): bool = + let msgLines = msg.splitLines + for msgLine in msgLines: + let msgLine = msgLine.normalize + var msgLineFound = false + for `linesLoopCounter`, line in `linesParam`: + if msgLine in line.normalize: + msgLineFound = true + `extraLine` + break + if not msgLineFound: + return false + return true + +defineInLinesProc(inLines): discard +defineInLinesProc(inLinesOrdered): lines = lines[i + 1 .. ^1] proc hasLineStartingWith(lines: seq[string], prefix: string): bool = for line in lines: @@ -91,7 +117,7 @@ proc hasLineStartingWith(lines: seq[string], prefix: string): bool = return true return false -proc getPackageDir(pkgCacheDir, pkgDirPrefix: string): string = +proc getPackageDir(pkgCacheDir, pkgDirPrefix: string, fullPath = true): string = for kind, dir in walkDir(pkgCacheDir): if kind != pcDir or not dir.startsWith(pkgCacheDir / pkgDirPrefix): continue @@ -100,7 +126,7 @@ proc getPackageDir(pkgCacheDir, pkgDirPrefix: string): string = continue let pkgChecksum = dir[pkgChecksumStartIndex + 1 .. ^1] if pkgChecksum.isValidSha1Hash(): - return dir + return if fullPath: dir else: dir.splitPath.tail return "" proc packageDirExists(pkgCacheDir, pkgDirPrefix: string): bool = @@ -138,6 +164,39 @@ proc beforeSuite() = removeDir(installDir) createDir(installDir) +template usePackageListFile(fileName: string, body: untyped) = + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + path = "$1" + """.unindent % (fileName).replace("\\", "\\\\")) + check execNimble(["refresh"]).exitCode == QuitSuccess + body + +template cleanFile(fileName: string) = + removeFile fileName + defer: removeFile fileName + +macro cleanFiles(fileNames: varargs[string]) = + result = newStmtList() + for fn in fileNames: + result.add quote do: cleanFile(`fn`) + +template cleanDir(dirName: string) = + removeDir dirName + defer: removeDir dirName + +template createTempDir(dirName: string) = + createDir dirName + defer: removeDir dirName + +template cdCleanDir(dirName: string, body: untyped) = + cleanDir dirName + createDir dirName + cd dirName: + body + suite "nimble refresh": beforeSuite() @@ -230,7 +289,7 @@ suite "nimscript": check line.endsWith("tests" / "nimscript") check lines[^1].startsWith("After PkgDir:") let packageDir = getPackageDir(pkgsDir, "nimscript-0.1.0") - check lines[^1].endsWith(packageDir) + check lines[^1].strip(leading = false).endsWith(packageDir) test "before/after on build": cd "nimscript": @@ -243,14 +302,14 @@ suite "nimscript": test "can execute nimscript tasks": cd "nimscript": - let (output, exitCode) = execNimble("--verbose", "work") + let (output, exitCode) = execNimble("work") let lines = output.strip.processOutput() check exitCode == QuitSuccess check lines[^1] == "10" test "can use nimscript's setCommand": cd "nimscript": - let (output, exitCode) = execNimble("--verbose", "cTest") + let (output, exitCode) = execNimble("cTest") let lines = output.strip.processOutput() check exitCode == QuitSuccess check "Execution finished".normalize in lines[^1].normalize @@ -321,15 +380,14 @@ suite "uninstall": beforeSuite() test "can install packagebin2": - let args = ["install", "https://github.com/nimble-test/packagebin2.git"] + let args = ["install", pkgBin2Url] check execNimbleYes(args).exitCode == QuitSuccess proc cannotSatisfyMsg(v1, v2: string): string = &"Cannot satisfy the dependency on PackageA {v1} and PackageA {v2}" test "can reject same version dependencies": - let (outp, exitCode) = execNimbleYes( - "install", "https://github.com/nimble-test/packagebin.git") + let (outp, exitCode) = execNimbleYes("install", pkgBinUrl) # We look at the error output here to avoid out-of-order problems caused by # stderr output being generated and flushed without first flushing stdout let ls = outp.strip.processOutput() @@ -337,25 +395,37 @@ suite "uninstall": check ls.inLines(cannotSatisfyMsg("0.2.0", "0.5.0")) or ls.inLines(cannotSatisfyMsg("0.5.0", "0.2.0")) - test "issue #27": + proc setupIssue27Packages() = # Install b cd "issue27/b": check execNimbleYes("install").exitCode == QuitSuccess - # Install a cd "issue27/a": check execNimbleYes("install").exitCode == QuitSuccess - cd "issue27": check execNimbleYes("install").exitCode == QuitSuccess + test "issue #27": + setupIssue27Packages() + test "can uninstall": + # setup test environment + cleanDir(installDir) + setupIssue27Packages() + check execNimbleYes("install", &"{pkgAUrl}@0.2").exitCode == QuitSuccess + check execNimbleYes("install", &"{pkgAUrl}@0.5").exitCode == QuitSuccess + check execNimbleYes("install", &"{pkgAUrl}@0.6").exitCode == QuitSuccess + check execNimbleYes("install", pkgBin2Url).exitCode == QuitSuccess + check execNimbleYes("install", pkgBUrl).exitCode == QuitSuccess + cd "nimscript": check execNimbleYes("install").exitCode == QuitSuccess + block: let (outp, exitCode) = execNimbleYes("uninstall", "issue27b") - - let ls = outp.strip.processOutput() check exitCode != QuitSuccess - check inLines(ls, "Cannot uninstall issue27b (0.1.0) because issue27a (0.1.0) depends") + var ls = outp.strip.processOutput() + let pkg27ADir = getPackageDir(pkgsDir, "issue27a-0.1.0", false) + let expectedMsg = cannotUninstallPkgMsg("issue27b", "0.1.0", @[pkg27ADir]) + check ls.inLinesOrdered(expectedMsg) check execNimbleYes("uninstall", "issue27").exitCode == QuitSuccess check execNimbleYes("uninstall", "issue27a").exitCode == QuitSuccess @@ -366,8 +436,16 @@ suite "uninstall": let (outp, exitCode) = execNimbleYes("uninstall", "PackageA") check exitCode != QuitSuccess let ls = outp.processOutput() - check inLines(ls, "Cannot uninstall PackageA (0.2.0)") - check inLines(ls, "Cannot uninstall PackageA (0.6.0)") + let + pkgBin2Dir = getPackageDir(pkgsDir, "packagebin2-0.1.0", false) + pkgBDir = getPackageDir(pkgsDir, "packageb-0.1.0", false) + expectedMsgForPkgA0dot6 = cannotUninstallPkgMsg( + "PackageA", "0.6.0", @[pkgBin2Dir]) + expectedMsgForPkgA0dot2 = cannotUninstallPkgMsg( + "PackageA", "0.2.0", @[pkgBDir]) + check ls.inLines(expectedMsgForPkgA0dot6) + check ls.inLines(expectedMsgForPkgA0dot2) + check execNimbleYes("uninstall", "PackageBin2").exitCode == QuitSuccess # Case insensitive @@ -379,7 +457,7 @@ suite "uninstall": check execNimbleYes("uninstall", "PackageA@0.2", "issue27b").exitCode == QuitSuccess - check(not dirExists(pkgsDir / "PackageA-0.2.0")) + check(not dirExists(installDir / "pkgs" / "PackageA-0.2.0")) suite "nimble dump": beforeSuite() @@ -558,95 +636,915 @@ suite "reverse dependencies": doAssert fileExists(oldNimbleDataFileName) doAssert fileExists(newNimbleDataFileName) - let oldNimbleData = parseNimbleData(oldNimbleDataFileName) - let newNimbleData = parseNimbleData(newNimbleDataFileName) + let oldNimbleData = loadNimbleData(oldNimbleDataFileName) + let newNimbleData = loadNimbleData(newNimbleDataFileName) doAssert oldNimbleData == newNimbleData suite "develop feature": - beforeSuite() + proc filesList(filesNames: seq[string]): string = + for fileName in filesNames: + result.addQuoted fileName + result.add ',' + + proc developFile(includes: seq[string], dependencies: seq[string]): string = + result = """{"version":"$#","includes":[$#],"dependencies":[$#]}""" % + [developFileVersion, filesList(includes), filesList(dependencies)] + + const + pkgListFileName = "packages.json" + dependentPkgName = "dependent" + dependentPkgVersion = "1.0" + dependentPkgNameAndVersion = &"{dependentPkgName}@{dependentPkgVersion}" + dependentPkgPath = "develop/dependent".normalizedPath + includeFileName = "included.develop" + pkgAName = "packagea" + pkgBName = "packageb" + pkgSrcDirTestName = "srcdirtest" + pkgHybridName = "hybrid" + depPath = "../dependency".normalizedPath + depName = "dependency" + depVersion = "0.1.0" + depNameAndVersion = &"{depName}@{depVersion}" + dep2Path = "../dependency2".normalizedPath + emptyDevelopFileContent = developFile(@[], @[]) + + let anyVersion = parseVersionRange("") + + test "can develop from dir with srcDir": + cd &"develop/{pkgSrcDirTestName}": + let (output, exitCode) = execNimble("develop") + check exitCode == QuitSuccess + let lines = output.processOutput + check not lines.inLines("will not be compiled") + check lines.inLines(pkgSetupInDevModeMsg( + pkgSrcDirTestName, getCurrentDir())) + + test "can git clone for develop": + cdCleanDir installDir: + let (output, exitCode) = execNimble("develop", pkgAUrl) + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg(pkgAName, installDir / pkgAName)) + + test "can develop from package name": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + let (output, exitCode) = execNimble("develop", pkgBName) + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(pkgInstalledMsg(pkgAName)) + check lines.inLinesOrdered( + pkgSetupInDevModeMsg(pkgBName, installDir / pkgBName)) + + test "can develop list of packages": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + let (output, exitCode) = execNimble( + "develop", pkgAName, pkgBName) + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg( + pkgAName, installDir / pkgAName)) + check lines.inLinesOrdered(pkgInstalledMsg(pkgAName)) + check lines.inLinesOrdered(pkgSetupInDevModeMsg( + pkgBName, installDir / pkgBName)) + + test "cannot remove package with develop reverse dependency": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + check execNimble("develop", pkgBName).exitCode == QuitSuccess + let (output, exitCode) = execNimble("remove", pkgAName) + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + cannotUninstallPkgMsg(pkgAName, "0.2.0", @[installDir / pkgBName])) test "can reject binary packages": cd "develop/binary": let (output, exitCode) = execNimble("develop") - checkpoint output check output.processOutput.inLines("cannot develop packages") check exitCode == QuitFailure test "can develop hybrid": - cd "develop/hybrid": + cd &"develop/{pkgHybridName}": let (output, exitCode) = execNimble("develop") - checkpoint output - check output.processOutput.inLines("will not be compiled") check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered("will not be compiled") + check lines.inLinesOrdered( + pkgSetupInDevModeMsg(pkgHybridName, getCurrentDir())) - let packageDir = getPackageDir(pkgsDir, "hybrid-#head") - let path = packageDir / "hybrid.nimble-link" - check fileExists(path) - let split = readFile(path).processOutput() - check split.len == 2 - check split[0].endsWith("develop" / "hybrid" / "hybrid.nimble") - check split[1].endsWith("develop" / "hybrid") + test "can specify different absolute clone dir": + let otherDir = installDir / "./some/other/dir" + cleanDir otherDir + let (output, exitCode) = execNimble( + "develop", &"-p:{otherDir}", pkgAUrl) + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg(pkgAName, otherDir / pkgAName)) - test "can develop with srcDir": - cd "develop/srcdirtest": - let (output, exitCode) = execNimbleYes("develop") - checkpoint output - check(not output.processOutput.inLines("will not be compiled")) + test "can specify different relative clone dir": + const otherDir = "./some/other/dir" + cdCleanDir installDir: + let (output, exitCode) = execNimble( + "develop", &"-p:{otherDir}", pkgAUrl) check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg(pkgAName, installDir / otherDir / pkgAName)) - let packageDir = getPackageDir(pkgsDir, "srcdirtest-#head") - let path = packageDir / "srcdirtest.nimble-link" - check fileExists(path) - let split = readFile(path).processOutput() - check split.len == 2 - check split[0].endsWith("develop" / "srcdirtest" / "srcdirtest.nimble") - check split[1].endsWith("develop" / "srcdirtest" / "src") + test "do not allow multiple path options": + let + developDir = installDir / "./some/dir" + anotherDevelopDir = installDir / "./some/other/dir" + defer: + # cleanup in the case of test failure + removeDir developDir + removeDir anotherDevelopDir + let (output, exitCode) = execNimble( + "develop", &"-p:{developDir}", &"-p:{anotherDevelopDir}", pkgAUrl) + check exitCode == QuitFailure + check output.processOutput.inLines("Multiple path options are given") + check not developDir.dirExists + check not anotherDevelopDir.dirExists - cd "develop/dependent": - let (output, exitCode) = execNimbleYes("c", "-r", "src" / "dependent.nim") - checkpoint output - check(output.processOutput.inLines("hello")) - check exitCode == QuitSuccess + test "do not allow path option without packages to download": + let developDir = installDir / "./some/dir" + let (output, exitCode) = execNimble("develop", &"-p:{developDir}") + check exitCode == QuitFailure + check output.processOutput.inLines(pathGivenButNoPkgsToDownloadMsg) + check not developDir.dirExists - test "can uninstall linked package": - cd "develop/srcdirtest": - let (_, exitCode) = execNimbleYes("develop") - check exitCode == QuitSuccess + test "do not allow add/remove options out of package directory": + cleanFile developFileName + let (output, exitCode) = execNimble("develop", "-a:./develop/dependency/") + check exitCode == QuitFailure + check output.processOutput.inLines(developOptionsOutOfPkgDirectoryMsg) - let (output, exitCode) = execNimbleYes("uninstall", "srcdirtest") - checkpoint(output) - check exitCode == QuitSuccess - check(not output.processOutput.inLines("warning")) + test "cannot load invalid develop file": + cd dependentPkgPath: + cleanFile developFileName + writeFile(developFileName, "this is not a develop file") + let (output, exitCode) = execNimble("check") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + notAValidDevFileJsonMsg(getCurrentDir() / developFileName)) + check lines.inLinesOrdered(validationFailedMsg) + + test "add downloaded package to the develop file": + cleanDir installDir + cd "develop/dependency": + usePackageListFile &"../{pkgListFileName}": + cleanFile developFileName + let + (output, exitCode) = execNimble( + "develop", &"-p:{installDir}", pkgAName) + pkgAAbsPath = installDir / pkgAName + developFileContent = developFile(@[], @[pkgAAbsPath]) + check exitCode == QuitSuccess + check parseFile(developFileName) == parseJson(developFileContent) + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) + + test "cannot add not a dependency downloaded package to the develop file": + cleanDir installDir + cd "develop/dependency": + usePackageListFile &"../{pkgListFileName}": + cleanFile developFileName + let + (output, exitCode) = execNimble( + "develop", &"-p:{installDir}", pkgAName, pkgBName) + pkgAAbsPath = installDir / pkgAName + pkgBAbsPath = installDir / pkgBName + developFileContent = developFile(@[], @[pkgAAbsPath]) + check exitCode == QuitFailure + check parseFile(developFileName) == parseJson(developFileContent) + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgBName, pkgBAbsPath)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) + check lines.inLinesOrdered( + notADependencyErrorMsg(&"{pkgBName}@0.2.0", depNameAndVersion)) + + test "add package to develop file": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + cleanFiles developFileName, dependentPkgName.addFileExt(ExeExt) + var (output, exitCode) = execNimble("develop", &"-a:{depPath}") + check exitCode == QuitSuccess + check developFileName.fileExists + check output.processOutput.inLines( + pkgAddedInDevModeMsg(depNameAndVersion, depPath)) + const expectedDevelopFile = developFile(@[], @[depPath]) + check parseFile(developFileName) == parseJson(expectedDevelopFile) + (output, exitCode) = execNimble("run") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) + + test "warning on attempt to add the same package twice": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-a:{depPath}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgAlreadyInDevModeMsg(depNameAndVersion, depPath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "cannot add invalid package to develop file": + cd dependentPkgPath: + cleanFile developFileName + const invalidPkgDir = "../invalidPkg".normalizedPath + createTempDir invalidPkgDir + let (output, exitCode) = execNimble("develop", &"-a:{invalidPkgDir}") + check exitCode == QuitFailure + check output.processOutput.inLines(invalidPkgMsg(invalidPkgDir)) + check not developFileName.fileExists - test "can git clone for develop": - let cloneDir = installDir / "developTmp" - createDir(cloneDir) - cd cloneDir: - let url = "https://github.com/nimble-test/packagea.git" - let (_, exitCode) = execNimbleYes("develop", url) - check exitCode == QuitSuccess + test "cannot add not a dependency to develop file": + cd dependentPkgPath: + cleanFile developFileName + let (output, exitCode) = execNimble("develop", "-a:../srcdirtest/") + check exitCode == QuitFailure + check output.processOutput.inLines( + notADependencyErrorMsg(&"{pkgSrcDirTestName}@1.0", "dependent@1.0")) + check not developFileName.fileExists + + test "cannot add two packages with the same name to develop file": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-a:{dep2Path}") + check exitCode == QuitFailure + check output.processOutput.inLines( + pkgAlreadyPresentAtDifferentPathMsg(depName, depPath.absolutePath)) + check parseFile(developFileName) == parseJson(developFileContent) - test "nimble path points to develop": - cd "develop/srcdirtest": - var (output, exitCode) = execNimble("develop") - checkpoint output - check exitCode == QuitSuccess + test "found two packages with the same name in the develop file": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile( + @[], @[depPath, dep2Path]) + writeFile(developFileName, developFileContent) - (output, exitCode) = execNimble("path", "srcdirtest") + let + (output, exitCode) = execNimble("check") + developFilePath = getCurrentDir() / developFileName + + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(failedToLoadFileMsg(developFilePath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, + [(depPath.absolutePath.Path, developFilePath.Path), + (dep2Path.absolutePath.Path, developFilePath.Path)].toHashSet)) + check lines.inLinesOrdered(validationFailedMsg) + + test "remove package from develop file by path": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-r:{depPath}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "warning on attempt to remove not existing package path": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-r:{dep2Path}") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgPathNotInDevFileMsg(dep2Path)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "remove package from develop file by name": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-n:{depName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath.absolutePath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "warning on attempt to remove not existing package name": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + const notExistingPkgName = "dependency2" + let (output, exitCode) = execNimble("develop", &"-n:{notExistingPkgName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgNameNotInDevFileMsg(notExistingPkgName)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "include develop file": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + cleanFiles developFileName, includeFileName, + dependentPkgName.addFileExt(ExeExt) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitSuccess + check developFileName.fileExists + check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) + const expectedDevelopFile = developFile(@[includeFileName], @[]) + check parseFile(developFileName) == parseJson(expectedDevelopFile) + (output, exitCode) = execNimble("run") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) + + test "warning on attempt to include already included develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + alreadyInclInDevFileMsg(includeFileName)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "cannot include invalid develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + writeFile(includeFileName, """{"some": "json"}""") + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitFailure + check not developFileName.fileExists + check output.processOutput.inLines(failedToLoadFileMsg(includeFileName)) + + test "cannot load a develop file with an invalid include file in it": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("check") + check exitCode == QuitFailure + let developFilePath = getCurrentDir() / developFileName + var lines = output.processOutput() + check lines.inLinesOrdered(failedToLoadFileMsg(developFilePath)) + check lines.inLinesOrdered(invalidDevFileMsg(developFilePath)) + check lines.inLinesOrdered(&"cannot read from file: {includeFileName}") + check lines.inLinesOrdered(validationFailedMsg) + + test "can include file pointing to the same package": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + cleanFiles developFileName, includeFileName, + dependentPkgName.addFileExt(ExeExt) + const fileContent = developFile(@[], @[depPath]) + writeFile(developFileName, fileContent) + writeFile(includeFileName, fileContent) + var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) + const expectedFileContent = developFile( + @[includeFileName], @[depPath]) + check parseFile(developFileName) == parseJson(expectedFileContent) + (output, exitCode) = execNimble("run") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) + + test "cannot include conflicting develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[dep2Path]) + writeFile(includeFileName, includeFileContent) + + let + (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + developFilePath = getCurrentDir() / developFileName + + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + failedToInclInDevFileMsg(includeFileName, developFilePath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, + [(depPath.absolutePath.Path, developFilePath.Path), + (dep2Path.Path, includeFileName.Path)].toHashSet)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "validate included dependencies version": + cd &"{dependentPkgPath}2": + cleanFiles developFileName, includeFileName + const includeFileContent = developFile(@[], @[dep2Path]) + writeFile(includeFileName, includeFileContent) + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitFailure + var lines = output.processOutput + let developFilePath = getCurrentDir() / developFileName + check lines.inLinesOrdered( + failedToInclInDevFileMsg(includeFileName, developFilePath)) + check lines.inLinesOrdered(invalidPkgMsg(dep2Path)) + check lines.inLinesOrdered(dependencyNotInRangeErrorMsg( + depNameAndVersion, dependentPkgNameAndVersion, + parseVersionRange(">= 0.2.0"))) + check not developFileName.fileExists + + test "exclude develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + let (output, exitCode) = execNimble("develop", &"-e:{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines(exclFromDevFileMsg(includeFileName)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "warning on attempt to exclude not included develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + let (output, exitCode) = execNimble("develop", &"-e:../{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + notInclInDevFileMsg((&"../{includeFileName}").normalizedPath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "relative paths in the develop file and absolute from the command line": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile( + @[includeFileName], @[depPath]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + + let + includeFileAbsolutePath = includeFileName.absolutePath + dependencyPkgAbsolutePath = "../dependency".absolutePath + (output, exitCode) = execNimble("develop", + &"-e:{includeFileAbsolutePath}", &"-r:{dependencyPkgAbsolutePath}") - checkpoint output check exitCode == QuitSuccess - check output.strip() == getCurrentDir() / "src" + var lines = output.processOutput + check lines.inLinesOrdered(exclFromDevFileMsg(includeFileAbsolutePath)) + check lines.inLinesOrdered( + pkgRemovedFromDevModeMsg(depNameAndVersion, dependencyPkgAbsolutePath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "absolute paths in the develop file and relative from the command line": + cd dependentPkgPath: + let + currentDir = getCurrentDir() + includeFileAbsPath = currentDir / includeFileName + dependencyAbsPath = currentDir / depPath + developFileContent = developFile( + @[includeFileAbsPath], @[dependencyAbsPath]) + includeFileContent = developFile(@[], @[depPath]) + + cleanFiles developFileName, includeFileName + writeFile(developFileName, developFileContent) + writeFile(includeFileName, includeFileContent) + + let (output, exitCode) = execNimble("develop", + &"-e:{includeFileName}", &"-r:{depPath}") + + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(exclFromDevFileMsg(includeFileName)) + check lines.inLinesOrdered( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "uninstall package with develop reverse dependencies": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + const developFileContent = developFile(@[], @[depPath]) + cleanFiles developFileName, "dependent" + writeFile(developFileName, developFileContent) + + block checkSuccessfulInstallAndReverseDependencyAddedToNimbleData: + let + (_, exitCode) = execNimble("install") + nimbleData = parseFile(installDir / nimbleDataFileName) + packageDir = getPackageDir(pkgsDir, "PackageA-0.5.0") + checksum = packageDir[packageDir.rfind('-') + 1 .. ^1] + devRevDepPath = nimbleData{$ndjkRevDep}{pkgAName}{"0.5.0"}{ + checksum}{0}{$ndjkRevDepPath} + depAbsPath = getCurrentDir() / depPath + + check exitCode == QuitSuccess + check not devRevDepPath.isNil + check devRevDepPath.str == depAbsPath + + block checkSuccessfulUninstallAndRemovalFromNimbleData: + let + (_, exitCode) = execNimble("uninstall", "-i", pkgAName, "-y") + nimbleData = parseFile(installDir / nimbleDataFileName) + + check exitCode == QuitSuccess + check not nimbleData[$ndjkRevDep].hasKey(pkgAName) + + test "follow develop dependency's develop file": + cd "develop": + const pkg1DevFilePath = "pkg1" / developFileName + const pkg2DevFilePath = "pkg2" / developFileName + cleanFiles pkg1DevFilePath, pkg2DevFilePath + const pkg1DevFileContent = developFile(@[], @["../pkg2"]) + writeFile(pkg1DevFilePath, pkg1DevFileContent) + const pkg2DevFileContent = developFile(@[], @["../pkg3"]) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (_, exitCode) = execNimble("run", "-n") + check exitCode == QuitSuccess + test "version clash from followed develop file": + cd "develop": + const pkg1DevFilePath = "pkg1" / developFileName + const pkg2DevFilePath = "pkg2" / developFileName + cleanFiles pkg1DevFilePath, pkg2DevFilePath + const pkg1DevFileContent = developFile(@[], @["../pkg2", "../pkg3"]) + writeFile(pkg1DevFilePath, pkg1DevFileContent) + const pkg2DevFileContent = developFile(@[], @["../pkg3.2"]) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + + let + currentDir = getCurrentDir() + pkg1DevFileAbsPath = currentDir / pkg1DevFilePath + pkg2DevFileAbsPath = currentDir / pkg2DevFilePath + pkg3AbsPath = currentDir / "pkg3" + pkg32AbsPath = currentDir / "pkg3.2" + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(failedToLoadFileMsg(pkg1DevFileAbsPath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", + [(pkg3AbsPath.Path, pkg1DevFileAbsPath.Path), + (pkg32AbsPath.Path, pkg2DevFileAbsPath.Path)].toHashSet)) + + test "relative include paths are followed from the file's directory": + cd dependentPkgPath: + const includeFilePath = &"../{includeFileName}" + cleanFiles includeFilePath, developFileName, dependentPkgName.addFileExt(ExeExt) + const developFileContent = developFile(@[includeFilePath], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @["./dependency2/"]) + writeFile(includeFilePath, includeFileContent) + let (_, errorCode) = execNimble("run", "-n") + check errorCode == QuitSuccess + + test "filter not used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>| pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | nimble.develop | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | + # +--------------------------+ includes | + # v + # +---------------+ + # | develop.json | + # +---------------+ + # | + # dependency | + # v + # +---------------------+ + # | pkg3 | + # +---------------------+ + # | version = "0.2.0" | + # +---------------------+ + + # Here the build must fail because "pkg3" coming from develop file included + # in "pkg2"'s develop file is not a dependency of "pkg2" itself and it must + # be filtered. In this way "pkg1"'s dependency to "pkg3" is not satisfied. + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2.2" / developFileName + freeDevFileName = "develop.json" + pkg1DevFileContent = developFile(@[], @["../pkg2.2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFileName}"], @[]) + freeDevFileContent = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFileName + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFileName, freeDevFileContent) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) + check lines.inLinesOrdered(pkgNotFoundMsg(("pkg3", anyVersion))) + + test "do not filter used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>+ pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | requires "pkg3" | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | nimble.develop | + # +--------------------------+ +--------------------------+ + # | + # includes | + # v + # +---------------+ + # | develop.json | + # +---------------+ + # | + # dependency | + # v + # +---------------------+ + # | pkg3 | + # +---------------------+ + # | version = "0.2.0" | + # +---------------------+ + + # Here the build must pass because "pkg3" coming form develop file included + # in "pkg2"'s develop file is a dependency of "pkg2" and it will be used, + # in this way satisfying also "pkg1"'s requirements. + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2" / developFileName + freeDevFileName = "develop.json" + pkg1DevFileContent = developFile(@[], @["../pkg2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFileName}"], @[]) + freeDevFileContent = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFileName + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFileName, freeDevFileContent) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + + test "no version clash with filtered not used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>| pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | nimble.develop | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | + # +--------------------------+ includes | + # | v + # includes | +---------------+ + # v | develop2.json | + # +---------------+ +-------+-------+ + # | develop1.json | | + # +---------------+ dependency | + # | v + # dependency | +---------------------+ + # v | pkg3 | + # +-------------------+ +---------------------+ + # | pkg3 | | version = "0.2.0" | + # +-------------------+ +---------------------+ + # | version = "0.1.0" | + # +-------------------+ + + # Here the build must pass because only the version of "pkg3" included via + # "develop1.json" must be taken into account, since "pkg2" does not depend + # on "pkg3" and the version coming from "develop2.json" must be filtered. + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2.2" / developFileName + freeDevFile1Name = "develop1.json" + freeDevFile2Name = "develop2.json" + pkg1DevFileContent = developFile( + @[&"../{freeDevFile1Name}"], @["../pkg2.2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) + freeDevFile1Content = developFile(@[], @["./pkg3"]) + freeDevFile2Content = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, + freeDevFile1Name, freeDevFile2Name + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFile1Name, freeDevFile1Content) + writeFile(freeDevFile2Name, freeDevFile2Content) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + + test "version clash with used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>| pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | requires "pkg3" | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | nimble.develop | + # +--------------------------+ +--------------------------+ + # | | + # includes | includes | + # v v + # +-------+-------+ +---------------+ + # | develop1.json | | develop2.json | + # +-------+-------+ +---------------+ + # | | + # dependency | dependency | + # v v + # +-------------------+ +---------------------+ + # | pkg3 | | pkg3 | + # +-------------------+ +---------------------+ + # | version = "0.1.0" | | version = "0.2.0" | + # +-------------------+ +---------------------+ + + # Here the build must fail because since "pkg3" is dependency of both "pkg1" + # and "pkg2", both versions coming from "develop1.json" and "develop2.json" + # must be taken into account, but they are different." + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2" / developFileName + freeDevFile1Name = "develop1.json" + freeDevFile2Name = "develop2.json" + pkg1DevFileContent = developFile( + @[&"../{freeDevFile1Name}"], @["../pkg2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) + freeDevFile1Content = developFile(@[], @["./pkg3"]) + freeDevFile2Content = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, + freeDevFile1Name, freeDevFile2Name + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFile1Name, freeDevFile1Content) + writeFile(freeDevFile2Name, freeDevFile2Content) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(failedToLoadFileMsg( + getCurrentDir() / developFileName)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", + [("../pkg3".Path, (&"../{freeDevFile1Name}").Path), + ("../pkg3.2".Path, (&"../{freeDevFile2Name}").Path)].toHashSet)) + + test "create an empty develop file with default name in the current dir": + cd dependentPkgPath: + cleanFile developFileName + let (output, errorCode) = execNimble("develop", "-c") + check errorCode == QuitSuccess + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + check output.processOutput.inLines( + emptyDevFileCreatedMsg(developFileName)) + + test "create an empty develop file in some dir": + cleanDir installDir + let filePath = installDir / "develop.json" + cleanFile filePath + createDir installDir + let (output, errorCode) = execNimble("develop", &"-c:{filePath}") + check errorCode == QuitSuccess + check parseFile(filePath) == parseJson(emptyDevelopFileContent) + check output.processOutput.inLines(emptyDevFileCreatedMsg(filePath)) + + test "try to create an empty develop file with already existing name": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let + filePath = getCurrentDir() / developFileName + (output, errorCode) = execNimble("develop", &"-c:{filePath}") + check errorCode == QuitFailure + check output.processOutput.inLines(fileAlreadyExistsMsg(filePath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "try to create an empty develop file in not existing dir": + let filePath = installDir / "some/not/existing/dir/develop.json" + cleanFile filePath + let (output, errorCode) = execNimble("develop", &"-c:{filePath}") + check errorCode == QuitFailure + check output.processOutput.inLines(&"cannot open: {filePath}") + + test "partial success when some operations in single command failed": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + const + dep2DevelopFilePath = dep2Path / developFileName + includeFileContent = developFile(@[], @[dep2Path]) + invalidInclFilePath = "/some/not/existing/file/path".normalizedPath + + cleanFiles developFileName, includeFileName, dep2DevelopFilePath + writeFile(includeFileName, includeFileContent) + + let + developFilePath = getCurrentDir() / developFileName + (output, errorCode) = execNimble("develop", &"-p:{installDir}", + pkgAName, # fail because not a direct dependency + "-c", # success + &"-a:{depPath}", # success + &"-a:{dep2Path}", # fail because of names collision + &"-i:{includeFileName}", # fail because of names collision + &"-n:{depName}", # success + &"-c:{developFilePath}", # fail because the file already exists + &"-a:{dep2Path}", # success + &"-i:{includeFileName}", # success + &"-i:{invalidInclFilePath}", # fail + &"-c:{dep2DevelopFilePath}") # success + + check errorCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg( + pkgAName, installDir / pkgAName)) + check lines.inLinesOrdered(emptyDevFileCreatedMsg(developFileName)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(depNameAndVersion, depPath)) + check lines.inLinesOrdered( + pkgAlreadyPresentAtDifferentPathMsg(depName, depPath)) + check lines.inLinesOrdered( + failedToInclInDevFileMsg(includeFileName, developFilePath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, + [(depPath.Path, developFilePath.Path), + (dep2Path.Path, includeFileName.Path)].toHashSet)) + check lines.inLinesOrdered( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check lines.inLinesOrdered(fileAlreadyExistsMsg(developFilePath)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(depNameAndVersion, dep2Path)) + check lines.inLinesOrdered(inclInDevFileMsg(includeFileName)) + check lines.inLinesOrdered(failedToLoadFileMsg(invalidInclFilePath)) + check lines.inLinesOrdered(emptyDevFileCreatedMsg(dep2DevelopFilePath)) + check parseFile(dep2DevelopFilePath) == + parseJson(emptyDevelopFileContent) + check lines.inLinesOrdered(notADependencyErrorMsg( + &"{pkgAName}@0.6.0", dependentPkgNameAndVersion)) + const expectedDevelopFileContent = developFile( + @[includeFileName], @[dep2Path]) + check parseFile(developFileName) == + parseJson(expectedDevelopFileContent) + +suite "path command": test "can get correct path for srcDir (#531)": - check execNimbleYes("uninstall", "srcdirtest").exitCode == QuitSuccess cd "develop/srcdirtest": let (_, exitCode) = execNimbleYes("install") check exitCode == QuitSuccess let (output, _) = execNimble("path", "srcdirtest") let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") - check output.strip() == packageDir + check output.strip() == packageDir + + # test "nimble path points to develop": + # cd "develop/srcdirtest": + # var (output, exitCode) = execNimble("develop") + # checkpoint output + # check exitCode == QuitSuccess + + # (output, exitCode) = execNimble("path", "srcdirtest") + + # checkpoint output + # check exitCode == QuitSuccess + # check output.strip() == getCurrentDir() / "src" suite "test command": beforeSuite() @@ -697,25 +1595,25 @@ suite "check command": let (outp, exitCode) = execNimble("check") check exitCode == QuitSuccess check outp.processOutput.inLines("success") - check outp.processOutput.inLines("binaryPackage is valid") + check outp.processOutput.inLines("\"binaryPackage\" is valid") cd "packageStructure/a": let (outp, exitCode) = execNimble("check") check exitCode == QuitSuccess check outp.processOutput.inLines("success") - check outp.processOutput.inLines("a is valid") + check outp.processOutput.inLines("\"a\" is valid") cd "packageStructure/b": let (outp, exitCode) = execNimble("check") check exitCode == QuitSuccess check outp.processOutput.inLines("success") - check outp.processOutput.inLines("b is valid") + check outp.processOutput.inLines("\"b\" is valid") cd "packageStructure/c": let (outp, exitCode) = execNimble("check") check exitCode == QuitSuccess check outp.processOutput.inLines("success") - check outp.processOutput.inLines("c is valid") + check outp.processOutput.inLines("\"c\" is valid") test "can fail package": cd "packageStructure/x": @@ -730,52 +1628,38 @@ suite "multi": test "can install package from git subdir": var - args = @["install", "https://github.com/nimble-test/multi?subdir=alpha"] + args = @["install", pkgMultiAlphaUrl] (output, exitCode) = execNimbleYes(args) check exitCode == QuitSuccess # Issue 785 - args.add @["https://github.com/nimble-test/multi?subdir=beta", "-n"] + args.add @[pkgMultiBetaUrl, "-n"] (output, exitCode) = execNimble(args) check exitCode == QuitSuccess check output.contains("forced no") check output.contains("beta installed successfully") test "can develop package from git subdir": - removeDir("multi") - let args = ["develop", "https://github.com/nimble-test/multi?subdir=beta"] - check execNimbleYes(args).exitCode == QuitSuccess + cleanDir "beta" + check execNimbleYes("develop", pkgMultiBetaUrl).exitCode == QuitSuccess suite "Module tests": - beforeSuite() - - test "version": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/version").exitCode == QuitSuccess - - test "reversedeps": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/reversedeps").exitCode == QuitSuccess - - test "packageparser": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/packageparser").exitCode == QuitSuccess - - test "packageinfo": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/packageinfo").exitCode == QuitSuccess - - test "cli": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/cli").exitCode == QuitSuccess - - test "download": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/download").exitCode == QuitSuccess - - test "jsonhelpers": - cd "..": - check execCmdEx("nim c -r src/nimblepkg/jsonhelpers").exitCode == QuitSuccess + template moduleTest(moduleName: string) = + test moduleName: + cd "..": + check execCmdEx("nim c -r src/nimblepkg/" & moduleName). + exitCode == QuitSuccess + + moduleTest "aliasthis" + moduleTest "common" + moduleTest "download" + moduleTest "jsonhelpers" + moduleTest "packageinfo" + moduleTest "packageparser" + moduleTest "paths" + moduleTest "reversedeps" + moduleTest "topologicalsort" + moduleTest "version" suite "nimble run": beforeSuite() @@ -946,7 +1830,7 @@ suite "project local deps mode": test "nimbledeps exists": cd "localdeps": - removeDir("nimbledeps") + cleanDir("nimbledeps") createDir("nimbledeps") let (output, exitCode) = execCmdEx(nimblePath & " install -y") check exitCode == QuitSuccess @@ -955,15 +1839,16 @@ suite "project local deps mode": test "--localdeps flag": cd "localdeps": - removeDir("nimbledeps") + cleanDir("nimbledeps") let (output, exitCode) = execCmdEx(nimblePath & " install -y -l") check exitCode == QuitSuccess check output.contains("project local deps mode") check output.contains("Succeeded") test "localdeps develop": - removeDir("packagea") - let (_, exitCode) = execCmdEx(nimblePath & " develop https://github.com/nimble-test/packagea --localdeps -y") + cleanDir("packagea") + let (_, exitCode) = execCmdEx(nimblePath & + &" develop {pkgAUrl} --localdeps -y") check exitCode == QuitSuccess check dirExists("packagea" / "nimbledeps") check not dirExists("nimbledeps") @@ -972,9 +1857,7 @@ suite "misc tests": beforeSuite() test "depsOnly + flag order test": - let (output, exitCode) = execNimbleYes( - "--depsOnly", "install", "https://github.com/nimble-test/packagebin2" - ) + let (output, exitCode) = execNimbleYes("--depsOnly", "install", pkgBin2Url) check(not output.contains("Success: packagebin2 installed successfully.")) check exitCode == QuitSuccess @@ -1033,7 +1916,7 @@ suite "misc tests": proc execBuild(fileName: string): tuple[output: string, exitCode: int] = result = execCmdEx( - fmt"nim c -o:{buildDir/fileName.splitFile.name} {fileName}") + &"nim c -o:{buildDir/fileName.splitFile.name} {fileName}") proc checkOutput(output: string): uint = const warningsToCheck = [ @@ -1082,18 +1965,21 @@ suite "issues": test "issue 799": # When building, any newly installed packages should be referenced via the # path that they get permanently installed at. - removeDir installDir + cleanDir installDir cd "issue799": - let (build_output, build_code) = execNimbleYes("--verbose", "build") - check build_code == 0 - var build_results = processOutput(build_output) - build_results.keepItIf(unindent(it).startsWith("Executing")) + let (output, exitCode) = execNimbleYes("build") + check exitCode == QuitSuccess + var lines = output.processOutput + lines.keepItIf(unindent(it).startsWith("Executing")) - for build_line in build_results: - if build_line.contains("issue799"): - let nimble_install_dir = getPackageDir(pkgsDir, "nimble-#head") - let pkg_installed_path = "--path:'" & nimble_install_dir & "'" - check build_line.contains(pkg_installed_path) + for line in lines: + if line.contains("issue799"): + let nimbleInstallDir = getPackageDir( + pkgsDir, &"nimble-{nimbleVersion}") + dump(nimbleInstallDir) + let pkgInstalledPath = "--path:'" & nimble_install_dir & "'" + dump(pkgInstalledPath) + check line.contains(pkgInstalledPath) test "issue 793": cd "issue793": @@ -1245,10 +2131,13 @@ suite "issues": test "issue #428": cd "issue428": # Note: Can't use execNimble because it patches nimbleDir - check execCmdEx(nimblePath & " -y --nimbleDir=./nimbleDir install").exitCode == QuitSuccess - let pkgDir = getPackageDir("nimbleDir/pkgs", "dummy-0.1.0") - check pkgDir.dirExists - check not (pkgDir / "nimbleDir").dirExists + let (_, exitCode) = execCmdEx( + nimblePath & " -y --nimbleDir=./nimbleDir install") + check exitCode == QuitSuccess + let dummyPkgDir = getPackageDir( + "nimbleDir" / nimblePackagesDirName, "dummy-0.1.0") + check dummyPkgDir.dirExists + check not (dummyPkgDir / "nimbleDir").dirExists test "issue 399": cd "issue399": @@ -1320,6 +2209,7 @@ suite "issues": check execNimble("tasks").exitCode == QuitSuccess test "can build with #head and versioned package (#289)": + cleanDir(installDir) cd "issue289": check execNimbleYes("install").exitCode == QuitSuccess @@ -1386,11 +2276,11 @@ suite "issues": assert false test "issue 129 (installing commit hash)": - let arguments = @["install", - "https://github.com/nimble-test/packagea.git@#1f9cb289c89"] + cleanDir(installDir) + let arguments = @["install", &"{pkgAUrl}@#1f9cb289c89"] check execNimbleYes(arguments).exitCode == QuitSuccess # Verify that it was installed correctly. - check packageDirExists(pkgsDir, "PackageA-#1f9cb289c89") + check packageDirExists(pkgsDir, "PackageA-0.6.0") # Remove it so that it doesn't interfere with the uninstall tests. check execNimbleYes("uninstall", "packagea@#1f9cb289c89").exitCode == QuitSuccess @@ -1409,7 +2299,7 @@ suite "issues": check inLines(lines1, "The .nimble file name must match name specified inside") test "issue 113 (uninstallation problems)": - removeDir(installDir) + cleanDir(installDir) cd "issue113/c": check execNimbleYes("install").exitCode == QuitSuccess @@ -1419,10 +2309,13 @@ suite "issues": check execNimbleYes("install").exitCode == QuitSuccess # Try to remove c. - let (output, exitCode) = execNimbleYes(["remove", "c"]) - let lines = output.strip.processOutput() + let + (output, exitCode) = execNimbleYes(["remove", "c"]) + lines = output.strip.processOutput() + pkgBInstallDir = getPackageDir(pkgsDir, "b-0.1.0").splitPath.tail + check exitCode != QuitSuccess - check inLines(lines, "cannot uninstall c (0.1.0) because b (0.1.0) depends on it") + check lines.inLines(cannotUninstallPkgMsg("c", "0.1.0", @[pkgBInstallDir])) check execNimbleYes(["remove", "a"]).exitCode == QuitSuccess check execNimbleYes(["remove", "b"]).exitCode == QuitSuccess From ec4e0fdf9660eaa9ad18de82d09f78fa14218509 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 2 Oct 2020 17:37:35 +0300 Subject: [PATCH 18/73] Redesign exceptions raising and handling mechanics All errors thrown from Nimble are put in a single hierarchy inheriting from `NimbleError` which on its turn inherits from `CatchableError`. Factory methods for each exception type are defined named after the name of the exception type but with a lowercase first latter and exceptions are raised only with: ```nim raise errorName() ``` `NimbleQuit` error is changed to inherit from `Defect` instead from `CatchableError` as workaround to avoid accidentally handling it by some `CatchableError` handler. Related to nim-lang/nimble#127 --- src/nimble.nim | 48 ++++++++++---------- src/nimblepkg/checksum.nim | 8 ++-- src/nimblepkg/cli.nim | 2 +- src/nimblepkg/common.nim | 14 +++--- src/nimblepkg/config.nim | 12 ++--- src/nimblepkg/developfile.nim | 2 +- src/nimblepkg/download.nim | 14 +++--- src/nimblepkg/nimscriptexecutor.nim | 2 +- src/nimblepkg/nimscriptwrapper.nim | 4 +- src/nimblepkg/options.nim | 19 ++++---- src/nimblepkg/packageinfo.nim | 18 ++++---- src/nimblepkg/packageinstaller.nim | 2 +- src/nimblepkg/packagemetadatafile.nim | 6 ++- src/nimblepkg/packageparser.nim | 63 ++++++++++++--------------- src/nimblepkg/publish.nim | 10 ++--- src/nimblepkg/tools.nim | 17 ++++---- src/nimblepkg/version.nim | 27 ++++++------ 17 files changed, 130 insertions(+), 138 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 32c59f78..d619db5d 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -39,7 +39,7 @@ proc refresh(options: Options) = else: if parameter notin options.config.packageLists: let msg = "Package list with the specified name not found." - raise newException(NimbleError, msg) + raise nimbleError(msg) fetchList(options.config.packageLists[parameter], options) else: @@ -75,7 +75,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): let nimVer = getNimrodVersion(options) if not withinRange(nimVer, dep.ver): let msg = "Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")" - raise newException(NimbleError, msg) + raise nimbleError(msg) else: let resolvedDep = dep.resolveAlias(options) display("Checking", "for $1" % $resolvedDep, priority = MediumPriority) @@ -117,7 +117,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): let currentVer = pkgInfo.getConcreteVersion(options) if pkgsInPath.hasKey(pkgInfo.name) and pkgsInPath[pkgInfo.name] != currentVer: - raise newException(NimbleError, + raise nimbleError( "Cannot satisfy the dependency on $1 $2 and $1 $3" % [pkgInfo.name, currentVer, pkgsInPath[pkgInfo.name]]) pkgsInPath[pkgInfo.name] = currentVer @@ -139,10 +139,10 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: HashSet[string], cd pkgDir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionBuild, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") + raise nimbleError("Pre-hook prevented further execution.") if pkgInfo.bin.len == 0: - raise newException(NimbleError, + raise nimbleError( "Nothing to build. Did you specify a module to build using the" & " `bin` key in your .nimble file?") @@ -300,7 +300,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, if not options.depsOnly: cd dir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionInstall, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") + raise nimbleError("Pre-hook prevented further execution.") var pkgInfo = getPkgInfo(dir, options) # Set the flag that the package is not in develop mode before saving it to the @@ -459,8 +459,8 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): let downloadedPackageChecksum = calculatePackageSha1Checksum(downloadDir) if downloadedPackageChecksum != dep.checksum.sha1: - raiseChecksumError(name, dep.version, dep.vcsRevision, - downloadedPackageChecksum, dep.checksum.sha1) + raise checksumError(name, dep.version, dep.vcsRevision, + downloadedPackageChecksum, dep.checksum.sha1) let (_, newlyInstalledPackageInfo) = installFromDir( downloadDir, version, options, url, first = false, fromLockFile = true) @@ -503,7 +503,7 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options, # isn't there) return getDownloadInfo(pv, options, false) else: - raise newException(NimbleError, pkgNotFoundMsg(pv)) + raise nimbleError(pkgNotFoundMsg(pv)) proc install(packages: seq[PkgTuple], options: Options, doPrompt, first, fromLockFile: bool): PackageDependenciesInfo = @@ -569,10 +569,10 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = binDotNim = bin.addFileExt("nim") if bin == "": - raise newException(NimbleError, "You need to specify a file.") + raise nimbleError("You need to specify a file.") if not (fileExists(bin) or fileExists(binDotNim)): - raise newException(NimbleError, + raise nimbleError( "Specified file, " & bin & " or " & binDotNim & ", does not exist.") let pkgInfo = getPkgInfo(getCurrentDir(), options) @@ -580,7 +580,7 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = let deps = pkgInfo.processAllDependencies(options) if not execHook(options, options.action.typ, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") + raise nimbleError("Pre-hook prevented further execution.") var args = @["-d:NimblePkgVersion=" & pkgInfo.version] for dep in deps: @@ -622,9 +622,9 @@ proc search(options: Options) = ## Searches are done in a case insensitive way making all strings lower case. assert options.action.typ == actionSearch if options.action.search == @[]: - raise newException(NimbleError, "Please specify a search string.") + raise nimbleError("Please specify a search string.") if needsRefresh(options): - raise newException(NimbleError, "Please run nimble refresh.") + raise nimbleError("Please run nimble refresh.") let pkgList = getPackageList(options) var found = false template onFound {.dirty.} = @@ -651,7 +651,7 @@ proc search(options: Options) = proc list(options: Options) = if needsRefresh(options): - raise newException(NimbleError, "Please run nimble refresh.") + raise nimbleError("Please run nimble refresh.") let pkgList = getPackageList(options) for pkg in pkgList: echoPackage(pkg) @@ -690,7 +690,7 @@ proc listPaths(options: Options) = assert options.action.typ == actionPath if options.action.packages.len == 0: - raise newException(NimbleError, "A package name needs to be specified") + raise nimbleError("A package name needs to be specified") var errors = 0 let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) @@ -716,7 +716,7 @@ proc listPaths(options: Options) = MediumPriority) errors += 1 if errors > 0: - raise newException(NimbleError, + raise nimbleError( "At least one of the specified packages was not found") proc join(x: seq[PkgTuple]; y: string): string = @@ -743,7 +743,7 @@ proc getPackageByPattern(pattern: string, options: Options): PackageInfo = let identTuple = parseRequires(pattern) var skeletonInfo: PackageInfo if not findPkg(packages, identTuple, skeletonInfo): - raise newException(NimbleError, + raise nimbleError( "Specified package not found" ) result = getPkgInfoFromFile(skeletonInfo.myPath, options) @@ -806,7 +806,7 @@ proc init(options: Options) = # Check whether the vcs is installed. let vcsBin = options.action.vcsOption if vcsBin != "" and findExe(vcsBin, true) == "": - raise newException(NimbleError, "Please install git or mercurial first") + raise nimbleError("Please install git or mercurial first") # Determine the package name. let pkgName = @@ -829,7 +829,7 @@ proc init(options: Options) = if fileExists(nimbleFile): let errMsg = "Nimble file already exists: $#" % nimbleFile - raise newException(NimbleError, errMsg) + raise nimbleError(errMsg) if options.forcePrompts != forcePromptYes: display( @@ -974,7 +974,7 @@ proc collectNames(pkgs: HashSet[ReverseDependency], proc uninstall(options: var Options) = if options.action.packages.len == 0: - raise newException(NimbleError, + raise nimbleError( "Please specify the package(s) to uninstall.") var pkgsToDelete: HashSet[ReverseDependency] @@ -985,7 +985,7 @@ proc uninstall(options: var Options) = let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) var pkgList = findAllPkgs(installedPkgs, pkgTup) if pkgList.len == 0: - raise newException(NimbleError, "Package not found") + raise nimbleError("Package not found") display("Checking", "reverse dependencies", priority = HighPriority) for pkg in pkgList: @@ -1003,7 +1003,7 @@ proc uninstall(options: var Options) = pkgsToDelete.incl pkg.toRevDep if pkgsToDelete.len == 0: - raise newException(NimbleError, "Failed uninstall - no packages to delete") + raise nimbleError("Failed uninstall - no packages to delete") if not options.prompt(pkgsToDelete.collectNames(false).promptRemovePkgsMsg): raise nimbleQuit() @@ -1151,7 +1151,7 @@ proc test(options: Options) = return if not execHook(options, actionCustom, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") + raise nimbleError("Pre-hook prevented further execution.") files.sort((a, b) => cmp(a.path, b.path)) diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index 3855e888..7099f23f 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -7,16 +7,14 @@ import common, tools type ChecksumError* = object of NimbleError -proc raiseChecksumError*(name, version, vcsRevision, - checksum, expectedChecksum: string) = - var error = newException(ChecksumError, -fmt""" +proc checksumError*(name, version, vcsRevision, checksum, + expectedChecksum: string): ref ChecksumError = + result = newNimbleError[ChecksumError](fmt""" Downloaded package checksum does not correspond to that in the lock file: Package: {name}@v.{version}@r.{vcsRevision} Checksum: {checksum} Expected checksum: {expectedChecksum} """) - raise error proc extractFileList(consoleOutput: string): seq[string] = result = consoleOutput.splitLines() diff --git a/src/nimblepkg/cli.nim b/src/nimblepkg/cli.nim index 2c8cc808..b69cd43d 100644 --- a/src/nimblepkg/cli.nim +++ b/src/nimblepkg/cli.nim @@ -256,7 +256,7 @@ proc promptListInteractive(question: string, args: openarray[string]): string = break of '\3': showCursor(stdout) - raise newException(NimbleError, "Keyboard interrupt") + raise nimbleError("Keyboard interrupt") else: discard # Erase all lines of the selection diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index aaada43a..845bfaac 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -14,7 +14,9 @@ type BuildFailed* = object of NimbleError ## Same as quit(QuitSuccess) or quit(QuitFailure), but allows cleanup. - NimbleQuit* = object of CatchableError + ## Inheriting from `Defect` is workaround to avoid accidental catching of + ## `NimbleQuit` by `CatchableError` handlers. + NimbleQuit* = object of Defect exitCode*: int ProcessOutput* = tuple[output: string, exitCode: int] @@ -24,19 +26,19 @@ const nimblePackagesDirName* = "pkgs" nimbleBinariesDirName* = "bin" -proc newNimbleError[ErrorType](msg: string, hint = "", - details: ref CatchableError = nil): +proc newNimbleError*[ErrorType](msg: string, hint = "", + details: ref CatchableError = nil): ref ErrorType = result = newException(ErrorType, msg, details) result.hint = hint -proc nimbleError*(msg: string, hint = "", details: ref CatchableError = nil): +proc nimbleError*(msg: string, hint = "", details: ref CatchableError = nil): ref NimbleError = newNimbleError[NimbleError](msg, hint, details) -proc buildFailed*(msg: string, hint = "", details: ref CatchableError = nil): +proc buildFailed*(msg: string, details: ref CatchableError = nil): ref BuildFailed = - newNimbleError[BuildFailed](msg, hint, details) + newNimbleError[BuildFailed](msg) proc nimbleQuit*(exitCode = QuitSuccess): ref NimbleQuit = result = newException(NimbleQuit, "") diff --git a/src/nimblepkg/config.nim b/src/nimblepkg/config.nim index 7779a1b4..850f26f8 100644 --- a/src/nimblepkg/config.nim +++ b/src/nimblepkg/config.nim @@ -62,9 +62,9 @@ proc parseConfig*(): Config = of cfgEof: if currentSection.len > 0: if currentPackageList.urls.len == 0 and currentPackageList.path == "": - raise newException(NimbleError, "Package list '$1' requires either url or path" % currentPackageList.name) + raise nimbleError("Package list '$1' requires either url or path" % currentPackageList.name) if currentPackageList.urls.len > 0 and currentPackageList.path != "": - raise newException(NimbleError, "Attempted to specify `url` and `path` for the same package list '$1'" % currentPackageList.name) + raise nimbleError("Attempted to specify `url` and `path` for the same package list '$1'" % currentPackageList.name) addCurrentPkgList(result, currentPackageList) break of cfgSectionStart: @@ -74,7 +74,7 @@ proc parseConfig*(): Config = of "packagelist": currentPackageList.clear() else: - raise newException(NimbleError, "Unable to parse config file:" & + raise nimbleError("Unable to parse config file:" & " Unknown section: " & e.key) of cfgKeyValuePair, cfgOption: case e.key.normalize @@ -102,7 +102,7 @@ proc parseConfig*(): Config = case currentSection.normalize of "packagelist": if currentPackageList.path != "": - raise newException(NimbleError, "Attempted to specify more than one `path` for the same package list.") + raise nimbleError("Attempted to specify more than one `path` for the same package list.") else: currentPackageList.path = e.value else: assert false @@ -110,8 +110,8 @@ proc parseConfig*(): Config = # Not relevant anymore but leaving in for legacy ini files discard else: - raise newException(NimbleError, "Unable to parse config file:" & + raise nimbleError("Unable to parse config file:" & " Unknown key: " & e.key) of cfgError: - raise newException(NimbleError, "Unable to parse config file: " & e.msg) + raise nimbleError("Unable to parse config file: " & e.msg) close(p) diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index c0647ff3..bef86c70 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -159,7 +159,7 @@ proc save*(data: DevelopFileData, path: Path, writeEmpty, overwrite: bool) = } if path.fileExists and not overwrite: - raise newException(IOError, fileAlreadyExistsMsg($path)) + raise nimbleError(fileAlreadyExistsMsg($path)) writeFile(path, json.pretty) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 132449e9..87fef073 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -90,8 +90,8 @@ proc getTagsListRemote*(url: string, meth: DownloadMethod): seq[string] = of DownloadMethod.git: var (output, exitCode) = doCmdEx("git ls-remote --tags " & url.quoteShell()) if exitCode != QuitSuccess: - raise newException(OSError, "Unable to query remote tags for " & url & - " Git returned: " & output) + raise nimbleError("Unable to query remote tags for " & url & + ". Git returned: " & output) for i in output.splitLines(): let refStart = i.find("refs/tags/") # git outputs warnings, empty lines, etc @@ -102,7 +102,7 @@ proc getTagsListRemote*(url: string, meth: DownloadMethod): seq[string] = of DownloadMethod.hg: # http://stackoverflow.com/questions/2039150/show-tags-for-remote-hg-repository - raise newException(ValueError, "Hg doesn't support remote tag querying.") + raise nimbleError("Hg doesn't support remote tag querying.") proc getVersionList*(tags: seq[string]): OrderedTable[Version, string] = ## Return an ordered table of Version -> git tag label. Ordering is @@ -125,7 +125,7 @@ proc getDownloadMethod*(meth: string): DownloadMethod = of "git": return DownloadMethod.git of "hg", "mercurial": return DownloadMethod.hg else: - raise newException(NimbleError, "Invalid download method: " & meth) + raise nimbleError("Invalid download method: " & meth) proc getHeadName*(meth: DownloadMethod): Version = ## Returns the name of the download method specific head. i.e. for git @@ -141,7 +141,7 @@ proc checkUrlType*(url: string): DownloadMethod = elif doCmdEx("hg identify " & url.quoteShell()).exitCode == QuitSuccess: return DownloadMethod.hg else: - raise newException(NimbleError, "Unable to identify url: " & url) + raise nimbleError("Unable to identify url: " & url) proc getUrlData*(url: string): (string, Table[string, string]) = var uri = parseUri(url) @@ -291,7 +291,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, ## version range. let pkginfo = getPkgInfo(result[0], options) if pkginfo.version.newVersion notin verRange: - raise newException(NimbleError, + raise nimbleError( "Downloaded package's version does not satisfy requested version " & "range: wanted $1 got $2." % [$verRange, $pkginfo.version]) @@ -307,7 +307,7 @@ proc echoPackageVersions*(pkg: Package) = echo(" versions: " & join(sortedVersions, ", ")) else: echo(" versions: (No versions tagged in the remote repository)") - except OSError: + except CatchableError: echo(getCurrentExceptionMsg()) of DownloadMethod.hg: echo(" versions: (Remote tag retrieval not supported by " & diff --git a/src/nimblepkg/nimscriptexecutor.nim b/src/nimblepkg/nimscriptexecutor.nim index 167b079e..a29de3cb 100644 --- a/src/nimblepkg/nimscriptexecutor.nim +++ b/src/nimblepkg/nimscriptexecutor.nim @@ -35,7 +35,7 @@ proc execCustom*(nimbleFile: string, options: Options, ## Executes the custom command using the nimscript backend. if not execHook(options, actionCustom, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") + raise nimbleError("Pre-hook prevented further execution.") if not nimbleFile.isNimScript(options): writeHelp() diff --git a/src/nimblepkg/nimscriptwrapper.nim b/src/nimblepkg/nimscriptwrapper.nim index f339121e..6f46de93 100644 --- a/src/nimblepkg/nimscriptwrapper.nim +++ b/src/nimblepkg/nimscriptwrapper.nim @@ -139,7 +139,7 @@ proc getIniFile*(scriptName: string, options: Options): string = result.writeFile(output) stdout.writeExecutionOutput() else: - raise newException(NimbleError, stdout & "\nprintPkgInfo() failed") + raise nimbleError(stdout & "\nprintPkgInfo() failed") proc execScript( scriptName, actionName: string, options: Options, isHook: bool @@ -157,7 +157,7 @@ proc execScript( stdout else: "Exception raised during nimble script execution" - raise newException(NimbleError, errMsg) + raise nimbleError(errMsg) let j = diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 42d7c411..b568b446 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -342,14 +342,14 @@ proc setNimBin*(options: var Options) = if pnim.len != 0: options.nim = pnim else: - raise newException(NimbleError, + raise nimbleError( "Unable to find `$1` in $PATH" % options.nim) elif not options.nim.isAbsolute(): # Relative path options.nim = expandTilde(options.nim).absolutePath() if not fileExists(options.nim): - raise newException(NimbleError, "Unable to find `$1`" % options.nim) + raise nimbleError("Unable to find `$1`" % options.nim) else: # Search PATH let pnim = findExe("nim") @@ -362,7 +362,7 @@ proc setNimBin*(options: var Options) = if options.nim.len == 0: # Nim not found in PATH - raise newException(NimbleError, + raise nimbleError( "Unable to find `nim` binary - add to $PATH or use `--nim`") proc getNimBin*(options: Options): string = @@ -387,7 +387,7 @@ proc parseArgument*(key: string, result: var Options) = let i = find(key, '@') let (pkgName, pkgVer) = (key[0 .. i-1], key[i+1 .. key.len-1]) if pkgVer.len == 0: - raise newException(NimbleError, "Version range expected after '@'.") + raise nimbleError("Version range expected after '@'.") result.action.packages.add((pkgName, pkgVer.parseVersionRange())) else: result.action.packages.add((key, VersionRange(kind: verAny))) @@ -397,9 +397,8 @@ proc parseArgument*(key: string, result: var Options) = result.action.search.add(key) of actionInit, actionDump: if result.action.projName != "": - raise newException( - NimbleError, "Can only perform this action on one package at a time." - ) + raise nimbleError( + "Can only perform this action on one package at a time.") result.action.projName = key of actionCompile, actionDoc: result.action.file = key @@ -564,10 +563,8 @@ proc handleUnknownFlags(options: var Options) = # Any unhandled flags? if options.unknownFlags.len > 0: let flag = options.unknownFlags[0] - raise newException( - NimbleError, - "Unknown option: " & getFlagString(flag[0], flag[1], flag[2]) - ) + raise nimbleError("Unknown option: " & + getFlagString(flag[0], flag[1], flag[2])) proc parseCmdLine*(): Options = result = initOptions() diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index ec128809..40c6435b 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -42,7 +42,7 @@ proc optionalField(obj: JsonNode, name: string, default = ""): string = if obj[name].kind == JString: return obj[name].str else: - raise newException(NimbleError, "Corrupted packages.json file. " & name & + raise nimbleError("Corrupted packages.json file. " & name & " field is of unexpected type.") else: return default @@ -52,7 +52,7 @@ proc requiredField(obj: JsonNode, name: string): string = ## Aborts execution if the field does not exist or is of invalid json type. result = optionalField(obj, name) if result.len == 0: - raise newException(NimbleError, + raise nimbleError( "Package in packages.json file does not contain a " & name & " field.") proc fromJson(obj: JSonNode): Package = @@ -152,7 +152,7 @@ proc fetchList*(list: PackageList, options: Options) = display("Success", "Package list copied.", Success, HighPriority) if lastError.len != 0: - raise newException(NimbleError, "Refresh failed\n" & lastError) + raise nimbleError("Refresh failed\n" & lastError) if copyFromPath.len > 0: copyFile(copyFromPath, @@ -188,7 +188,7 @@ proc resolveAlias(pkg: Package, options: Options): Package = display("Warning:", "The $1 package has been renamed to $2" % [pkg.name, pkg.alias], Warning, HighPriority) if not getPackage(pkg.alias, options, result): - raise newException(NimbleError, "Alias for package not found: " & + raise nimbleError("Alias for package not found: " & pkg.alias) proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool = @@ -212,7 +212,7 @@ proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool = proc getPackage*(name: string, options: Options): Package = let success = getPackage(name, options, result) if not success: - raise newException(NimbleError, + raise nimbleError( "Cannot find package with name '" & name & "'.") proc getPackageList*(options: Options): seq[Package] = @@ -237,12 +237,12 @@ proc findNimbleFile*(dir: string; error: bool): string = inc hits else: discard if hits >= 2: - raise newException(NimbleError, + raise nimbleError( "Only one .nimble file should be present in " & dir) elif hits == 0: if error: - raise newException(NimbleError, - "Could not find a file with a .nimble extension inside the specified directory: $1" % dir) + raise nimbleError( + "Specified directory ($1) does not contain a .nimble file." % dir) else: displayWarning(&"No .nimble file found for {dir}") @@ -375,7 +375,7 @@ proc checkInstallFile(pkgInfo: PackageInfo, for ignoreFile in pkgInfo.skipFiles: if ignoreFile.endswith("nimble"): - raise newException(NimbleError, ignoreFile & " must be installed.") + raise nimbleError(ignoreFile & " must be installed.") if samePaths(file, origDir / ignoreFile): result = true break diff --git a/src/nimblepkg/packageinstaller.nim b/src/nimblepkg/packageinstaller.nim index 1837e558..f7c8f0da 100644 --- a/src/nimblepkg/packageinstaller.nim +++ b/src/nimblepkg/packageinstaller.nim @@ -47,7 +47,7 @@ proc setupBinSymlink*(symlinkDest, symlinkFilename: string, var osver = OSVERSIONINFO() osver.dwOSVersionInfoSize = cast[DWORD](sizeof(OSVERSIONINFO)) if GetVersionExA(osver) == WINBOOL(0): - raise newException(NimbleError, + raise nimbleError( "Can't detect OS version: GetVersionExA call failed") let fixChcp = osver.dwMajorVersion <= 5 diff --git a/src/nimblepkg/packagemetadatafile.nim b/src/nimblepkg/packagemetadatafile.nim index 70eb8427..0fd38969 100644 --- a/src/nimblepkg/packagemetadatafile.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -15,6 +15,9 @@ const packageMetaDataFileName* = "nimblemeta.json" packageMetaDataFileVersion = "0.1.0" +proc metaDataError(msg: string): ref MetaDataError = + newNimbleError[MetaDataError](msg) + proc saveMetaData*(metaData: PackageMetaData, dirName: string) = ## Saves some important data to file in the package installation directory. var metaDataWithChangedPaths = to(metaData, PackageMetaDataV2) @@ -37,8 +40,7 @@ proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData = else: result = to(json[$pmdjkMetaData].to(PackageMetaDataV2), PackageMetaData) elif raiseIfNotFound: - raise newException(MetaDataError, - &"No {packageMetaDataFileName} file found in {dirName}") + raise metaDataError(&"No {packageMetaDataFileName} file found in {dirName}") else: displayWarning(&"No {packageMetaDataFileName} file found in {dirName}") diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 57162599..37394073 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -42,16 +42,11 @@ const reservedNames = [ "LPT9", ] -proc newValidationError(msg: string, warnInstalled: bool, - hint: string, warnAll: bool): ref ValidationError = - result = newException(ValidationError, msg) +proc validationError(msg: string, warnInstalled: bool, hint = "", + warnAll = false): ref ValidationError = + result = newNimbleError[ValidationError](msg, hint) result.warnInstalled = warnInstalled result.warnAll = warnAll - result.hint = hint - -proc raiseNewValidationError(msg: string, warnInstalled: bool, - hint: string = "", warnAll = false) = - raise newValidationError(msg, warnInstalled, hint, warnAll) proc validatePackageName*(name: string) = ## Raises an error if specified package name contains invalid characters. @@ -61,7 +56,7 @@ proc validatePackageName*(name: string) = if name.len == 0: return if name[0] in {'0'..'9'}: - raiseNewValidationError(name & + raise validationError(name & "\"$1\" is an invalid package name: cannot begin with $2" % [name, $name[0]], true) @@ -70,27 +65,27 @@ proc validatePackageName*(name: string) = case c of '_': if prevWasUnderscore: - raiseNewValidationError( + raise validationError( "$1 is an invalid package name: cannot contain \"__\"" % name, true) prevWasUnderscore = true of AllChars - IdentChars: - raiseNewValidationError( + raise validationError( "$1 is an invalid package name: cannot contain '$2'" % [name, $c], true) else: prevWasUnderscore = false if name.endsWith("pkg"): - raiseNewValidationError("\"$1\" is an invalid package name: cannot end" & - " with \"pkg\"" % name, false) + raise validationError("\"$1\" is an invalid package name: cannot end" & + " with \"pkg\"" % name, false) if name.toUpperAscii() in reservedNames: - raiseNewValidationError( + raise validationError( "\"$1\" is an invalid package name: reserved name" % name, false) proc validateVersion*(ver: string) = for c in ver: if c notin ({'.'} + Digits): - raiseNewValidationError( + raise validationError( "Version may only consist of numbers and the '.' character " & "but found '" & c & "'.", false) @@ -140,7 +135,7 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = "by adding `skipFiles = @[\"$3\"]` to the .nimble file. See " & "https://github.com/nim-lang/nimble#libraries for more info.") % [pkgInfo.name & ext, correctDir & DirSep, file & ext, pkgInfo.name] - raiseNewValidationError(msg, true, hint, true) + raise validationError(msg, true, hint, true) else: assert(not pkgInfo.isMinimal) # On Windows `pkgInfo.bin` has a .exe extension, so we need to normalize. @@ -156,36 +151,36 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = "to '$3'. Otherwise, prevent its installation " & "by adding `skipDirs = @[\"$1\"]` to the .nimble file.") % [dir, pkgInfo.name, correctDir] - raiseNewValidationError(msg, true, hint, true) + raise validationError(msg, true, hint, true) iterInstallFiles(realDir, pkgInfo, options, onFile) proc validatePackageInfo(pkgInfo: PackageInfo, options: Options) = let path = pkgInfo.myPath if pkgInfo.name == "": - raiseNewValidationError("Incorrect .nimble file: " & path & - " does not contain a name field.", false) + raise validationError("Incorrect .nimble file: " & path & + " does not contain a name field.", false) if pkgInfo.name.normalize != path.splitFile.name.normalize: - raiseNewValidationError( + raise validationError( "The .nimble file name must match name specified inside " & path, true) if pkgInfo.version == "": - raiseNewValidationError("Incorrect .nimble file: " & path & + raise validationError("Incorrect .nimble file: " & path & " does not contain a version field.", false) if not pkgInfo.isMinimal: if pkgInfo.author == "": - raiseNewValidationError("Incorrect .nimble file: " & path & + raise validationError("Incorrect .nimble file: " & path & " does not contain an author field.", false) if pkgInfo.description == "": - raiseNewValidationError("Incorrect .nimble file: " & path & + raise validationError("Incorrect .nimble file: " & path & " does not contain a description field.", false) if pkgInfo.license == "": - raiseNewValidationError("Incorrect .nimble file: " & path & + raise validationError("Incorrect .nimble file: " & path & " does not contain a license field.", false) if pkgInfo.backend notin ["c", "cc", "objc", "cpp", "js"]: - raiseNewValidationError("'" & pkgInfo.backend & + raise validationError("'" & pkgInfo.backend & "' is an invalid backend.", false) validatePackageStructure(pkginfo, options) @@ -261,7 +256,7 @@ proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = let spl = i.split('=', 1) (spl[0], spl[1]) if src.splitFile().ext == ".nim": - raise newException(NimbleError, "`bin` entry should not be a source file: " & src) + raise nimbleError("`bin` entry should not be a source file: " & src) if result.backend == "js": bin = bin.addFileExt(".js") else: @@ -282,22 +277,22 @@ proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = for i in ev.value.multiSplit: result.postHooks.incl(i.normalize) else: - raise newException(NimbleError, "Invalid field: " & ev.key) + raise nimbleError("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(NimbleError, "Invalid field: " & ev.key) - else: raise newException(NimbleError, + raise nimbleError("Invalid field: " & ev.key) + else: raise nimbleError( "Invalid section: " & currentSection) - of cfgOption: raise newException(NimbleError, + of cfgOption: raise nimbleError( "Invalid package info, should not contain --" & ev.value) of cfgError: - raise newException(NimbleError, "Error parsing .nimble file: " & ev.msg) + raise nimbleError("Error parsing .nimble file: " & ev.msg) else: - raise newException(ValueError, "Cannot open package info: " & path) + raise nimbleError("Cannot open package info: " & path) proc readPackageInfoFromNims(scriptName: string, options: Options, result: var PackageInfo) = @@ -380,7 +375,7 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): " " & iniError.msg & ".\n" & " Evaluating as NimScript file failed with: \n" & " " & exc.msg & "." - raise newException(NimbleError, msg) + raise nimbleError(msg) let fileDir = nf.splitFile().dir if not fileDir.startsWith(options.getPkgsDir()): @@ -470,7 +465,7 @@ proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfo] = except: let tmplt = readErrorMsg & "\nMore info: $3" let msg = createErrorMsg(tmplt, path, getCurrentException().msg) - var exc = newException(NimbleError, msg) + var exc = nimbleError(msg) exc.hint = hintMsg % path raise exc diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index edf97c54..d5c3674d 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -23,7 +23,7 @@ const defaultBranch = "master" # Default branch on https://github.com/nim-lang/packages proc userAborted() = - raise newException(NimbleError, "User aborted the process.") + raise nimbleError("User aborted the process.") proc createHeaders(a: Auth) = a.http.headers = newHttpHeaders({ @@ -95,7 +95,7 @@ proc createFork(a: Auth) = try: discard a.http.postContent(ReposUrl & "nim-lang/packages/forks") except HttpRequestError: - raise newException(NimbleError, "Unable to create fork. Access token" & + raise nimbleError("Unable to create fork. Access token" & " might not have enough permissions.") proc createPullRequest(a: Auth, packageName, branch: string): string = @@ -187,11 +187,11 @@ proc publish*(p: PackageInfo, o: Options) = doCmd("git push https://" & auth.token & "@github.com/" & auth.user & "/packages " & defaultBranch) if not dirExists(pkgsDir): - raise newException(NimbleError, + raise nimbleError( "Cannot find nimble-packages-fork git repository. Cloning failed.") if not fileExists(pkgsDir / "packages.json"): - raise newException(NimbleError, + raise nimbleError( "No packages file found in cloned fork.") # We need to do this **before** the cd: @@ -220,7 +220,7 @@ proc publish*(p: PackageInfo, o: Options) = downloadMethod = "hg" # TODO: Retrieve URL from hg. else: - raise newException(NimbleError, + raise nimbleError( "No .git nor .hg directory found. Stopping.") if url.len == 0: diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index c41fbe2d..678a9bf2 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -20,7 +20,7 @@ proc doCmd*(cmd: string) = bin = extractBin(cmd) isNim = bin.extractFilename().startsWith("nim") if findExe(bin) == "": - raise newException(NimbleError, "'" & bin & "' not in PATH.") + raise nimbleError("'" & bin & "' not in PATH.") # To keep output in sequence stdout.flushFile() @@ -32,7 +32,7 @@ proc doCmd*(cmd: string) = display("Executing", cmd, priority = MediumPriority) let exitCode = execCmd(cmd) if exitCode != QuitSuccess: - raise newException(NimbleError, + raise nimbleError( "Execution failed with exit code $1\nCommand: $2" % [$exitCode, cmd]) else: @@ -40,22 +40,21 @@ proc doCmd*(cmd: string) = let (output, exitCode) = execCmdEx(cmd) displayDebug("Output", output) if exitCode != QuitSuccess: - raise newException(NimbleError, + raise nimbleError( "Execution failed with exit code $1\nCommand: $2\nOutput: $3" % [$exitCode, cmd, output]) proc doCmdEx*(cmd: string): ProcessOutput = let bin = extractBin(cmd) if findExe(bin) == "": - raise newException(NimbleError, "'" & bin & "' not in PATH.") + raise nimbleError("'" & bin & "' not in PATH.") return execCmdEx(cmd) proc tryDoCmdEx*(cmd: string): string = let (output, exitCode) = doCmdEx(cmd) if exitCode != QuitSuccess: - raise newException( - NimbleError, - fmt"Execution of '{cmd}' failed with an exit code {exitCode}") + raise nimbleError( + &"Execution of '{cmd}' failed with an exit code {exitCode}") return output proc getNimBin*: string = @@ -67,7 +66,7 @@ proc getNimrodVersion*(options: Options): Version = let vOutput = doCmdEx(getNimBin(options).quoteShell & " -v").output var matches: array[0..MaxSubpatterns, string] if vOutput.find(peg"'Version'\s{(\d+\.)+\d+}", matches) == -1: - raise newException(NimbleError, "Couldn't find Nim version.") + raise nimbleError("Couldn't find Nim version.") newVersion(matches[0]) proc samePaths*(p1, p2: string): bool = @@ -94,7 +93,7 @@ proc changeRoot*(origRoot, newRoot, path: string): string = if path.startsWith(origRoot) or path.samePaths(origRoot): return newRoot / path.substr(origRoot.len, path.len-1) else: - raise newException(ValueError, + raise nimbleError( "Cannot change root of path: Path does not begin with original root.") proc copyFileD*(fro, to: string): string = diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index e444b902..520e006a 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -34,10 +34,12 @@ type ## Tuple containing package name and version range. PkgTuple* = tuple[name: string, ver: VersionRange] - ParseVersionError* = object of ValueError + ParseVersionError* = object of NimbleError -proc `$`*(ver: Version): string {.borrow.} +proc parseVersionError*(msg: string): ref ParseVersionError = + result = newNimbleError[ParseVersionError](msg) +proc `$`*(ver: Version): string {.borrow.} proc hash*(ver: Version): Hash {.borrow.} proc newVersion*(ver: string): Version = @@ -168,8 +170,7 @@ proc getNextIncompatibleVersion(version: string, semver: bool): string = proc makeRange*(version: string, op: string): VersionRange = if version == "": - raise newException(ParseVersionError, - "A version needs to accompany the operator.") + raise parseVersionError("A version needs to accompany the operator.") case op of ">": result = VersionRange(kind: verLater) @@ -189,7 +190,7 @@ proc makeRange*(version: string, op: string): VersionRange = result.verIRight = makeRange(excludedVersion, "<") return else: - raise newException(ParseVersionError, "Invalid operator: " & op) + raise parseVersionError("Invalid operator: " & op) result.ver = Version(version) proc parseVersionRange*(s: string): VersionRange = @@ -221,9 +222,8 @@ proc parseVersionRange*(s: string): VersionRange = # Disallow more than one verIntersect. It's pointless and could lead to # major unpredictable mistakes. if result.verIRight.kind == verIntersect: - raise newException(ParseVersionError, - "Having more than one `&` in a version range is pointless") - + raise parseVersionError( + "Having more than one `&` in a version range is pointless") return of '0'..'9', '.': version.add(s[i]) @@ -232,12 +232,11 @@ proc parseVersionRange*(s: string): VersionRange = # Make sure '0.9 8.03' is not allowed. if version != "" and i < s.len - 1: if s[i+1] in {'0'..'9', '.'}: - raise newException(ParseVersionError, - "Whitespace is not allowed in a version literal.") - + raise parseVersionError( + "Whitespace is not allowed in a version literal.") else: - raise newException(ParseVersionError, - "Unexpected char in version range '" & s & "': " & s[i]) + raise parseVersionError( + "Unexpected char in version range '" & s & "': " & s[i]) inc(i) result = makeRange(version, op) @@ -265,7 +264,7 @@ proc parseRequires*(req: string): PkgTuple = result.name = req.strip result.ver = VersionRange(kind: verAny) except ParseVersionError: - raise newException(NimbleError, + raise nimbleError( "Unable to parse dependency version range: " & getCurrentExceptionMsg()) proc `$`*(verRange: VersionRange): string = From 1c9f73b21b0013da134b8acca302e8dbb6233597 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 10 Nov 2020 11:29:04 +0200 Subject: [PATCH 19/73] Make all module tests to use with unittest module All Nimble module tests are re-factored to use standard library `unittest` module. Additionally `counttables.nim` module is added to `Module tests` suite in `tester.nim` to be executed with other modules tests and `paths.nim` module which currently have no tests is removed from the suite. Related nim-lang/nimble#127 --- src/nimblepkg/aliasthis.nim | 103 ++++++++------- src/nimblepkg/common.nim | 11 +- src/nimblepkg/counttables.nim | 26 ++-- src/nimblepkg/download.nim | 51 ++++---- src/nimblepkg/jsonhelpers.nim | 73 +++++------ src/nimblepkg/packageinfo.nim | 123 +++++++++--------- src/nimblepkg/packageparser.nim | 13 +- src/nimblepkg/topologicalsort.nim | 14 +- src/nimblepkg/version.nim | 204 ++++++++++++++++-------------- tests/tester.nim | 2 +- 10 files changed, 303 insertions(+), 317 deletions(-) diff --git a/src/nimblepkg/aliasthis.nim b/src/nimblepkg/aliasthis.nim index ae4a816a..f53d03ee 100644 --- a/src/nimblepkg/aliasthis.nim +++ b/src/nimblepkg/aliasthis.nim @@ -82,7 +82,6 @@ template aliasThis*(dotExpression: untyped) = when isMainModule: import unittest - import common type Object1 = object @@ -100,7 +99,7 @@ when isMainModule: aliasThis(Object2.field22) aliasThis(Object2.field23) - var obj = Object2( + const objPrototype = Object2( field11: 3.14, field22: Object1( field11: 2.718, @@ -108,50 +107,56 @@ when isMainModule: field13: 42), field23: ("tuple", 1)) - # check access to the original value in both ways - check obj.field13 == 42 - check obj.field22.field13 == 42 - check obj.field12 == @[1, 1, 2, 3, 5, 8] - check obj.field22.field12 == @[1, 1, 2, 3, 5, 8] - - # check setter via an alias - obj.field13 = -obj.field13 - check obj.field13 == -42 - check obj.field22.field13 == -42 - - # check setter without an alias - obj.field22.field13 = 0 - check obj.field13 == 0 - check obj.field22.field13 == 0 - - # check procedure call via an alias - obj.field12.add 13 - check obj.field12 == @[1, 1, 2, 3, 5, 8, 13] - check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13] - - # check procedure call without an alias - obj.field22.field12.add 21 - check obj.field12 == @[1, 1, 2, 3, 5, 8, 13, 21] - check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13, 21] - - # check that the priority is on the not aliased field - check obj.field11 == 3.14 - # check that the aliased, but shadowed field is still accessible - check obj.field22.field11 == 2.718 - - # check that setting via matching field name does not override - # the shadowed field - obj.field11 = 0 - check obj.field11 == 0 - check obj.field22.field11 == 2.718 - - # check access to tuple fields via an alias - check obj.tField1 == "tuple" - check obj.tField2 == 1 - - # check modification of tuple fields via an alias - obj.tField1 &= " test" - obj.tField2.inc - check obj.field23 == ("tuple test", 2) - - reportUnitTestSuccess() + suite "aliasThis for objects": + setup: + var obj = objPrototype + + test "access to the original value in both ways": + check obj.field13 == 42 + check obj.field22.field13 == 42 + check obj.field12 == @[1, 1, 2, 3, 5, 8] + check obj.field22.field12 == @[1, 1, 2, 3, 5, 8] + + test "setter via an alias": + obj.field13 = -obj.field13 + check obj.field13 == -42 + check obj.field22.field13 == -42 + + test "setter without an alias": + obj.field22.field13 = 0 + check obj.field13 == 0 + check obj.field22.field13 == 0 + + test "procedure call via an alias": + obj.field12.add 13 + check obj.field12 == @[1, 1, 2, 3, 5, 8, 13] + check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13] + + test "procedure call without an alias": + obj.field22.field12.add 13 + check obj.field12 == @[1, 1, 2, 3, 5, 8, 13] + check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13] + + test "the priority is on the not aliased field": + check obj.field11 == 3.14 + + test "the aliased, but shadowed field is still accessible": + check obj.field22.field11 == 2.718 + + test "setting via matching field name does not override the shadowed field": + obj.field11 = 0 + check obj.field11 == 0 + check obj.field22.field11 == 2.718 + + suite "aliasThis for tuples": + setup: + var obj = objPrototype + + test "access to tuple fields via an alias": + check obj.tField1 == "tuple" + check obj.tField2 == 1 + + test "modification of tuple fields via an alias": + obj.tField1 &= " test" + obj.tField2.inc + check obj.field23 == ("tuple test", 2) diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 845bfaac..03db1007 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -4,7 +4,7 @@ # Various miscellaneous common types reside here, to avoid problems with # recursive imports -import sugar, terminal, macros, hashes, strutils, sets +import sugar, macros, hashes, strutils, sets export sugar.dump type @@ -44,10 +44,6 @@ proc nimbleQuit*(exitCode = QuitSuccess): ref NimbleQuit = result = newException(NimbleQuit, "") result.exitCode = exitCode -proc reportUnitTestSuccess*() = - if programResult == QuitSuccess: - stdout.styledWrite(fgGreen, "All tests passed.\n") - proc hasField(NewType: type[object], fieldName: static string, FieldType: type): bool {.compiletime.} = for name, value in fieldPairs(NewType.default): @@ -92,11 +88,6 @@ template cd*(dir: string, body: untyped) = defer: setCurrentDir(lastDir) body -template debugTrace*(): untyped = - block: - let (filename, line, _) = instantiationInfo() - echo "filename = $#; line: $#" % [filename, $line] - when isMainModule: import unittest diff --git a/src/nimblepkg/counttables.nim b/src/nimblepkg/counttables.nim index 9e06cc88..a47e31b2 100644 --- a/src/nimblepkg/counttables.nim +++ b/src/nimblepkg/counttables.nim @@ -62,7 +62,6 @@ proc hasKey*[K](t: CountTable[K], k: K): bool = t.count(k) != 0 when isMainModule: import unittest - import common let testKey = 'a' var t: CountTable[testKey.typeOf] @@ -76,21 +75,20 @@ when isMainModule: check not t.hasKey(k) expect KeyError, (discard t[k]) - checkKeyCount(t, testKey, 0) + test "key count": + checkKeyCount(t, testKey, 0) - t.inc(testKey) - checkKeyCount(t, testKey, 1) + t.inc(testKey) + checkKeyCount(t, testKey, 1) - t.inc(testKey) - checkKeyCount(t, testKey, 2) + t.inc(testKey) + checkKeyCount(t, testKey, 2) - check not t.dec(testKey) - checkKeyCount(t, testKey, 1) + check not t.dec(testKey) + checkKeyCount(t, testKey, 1) - check t.dec(testKey) - checkKeyCount(t, testKey, 0) + check t.dec(testKey) + checkKeyCount(t, testKey, 0) - expect KeyError, t.dec(testKey) - checkKeyCount(t, testKey, 0) - - reportUnitTestSuccess() + expect KeyError, t.dec(testKey) + checkKeyCount(t, testKey, 0) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 87fef073..1721a807 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -314,30 +314,27 @@ proc echoPackageVersions*(pkg: Package) = pkg.downloadMethod & ")") when isMainModule: - # Test version sorting - block: - let data = @["v9.0.0-taeyeon", "v9.0.1-jessica", "v9.2.0-sunny", - "v9.4.0-tiffany", "v9.4.2-hyoyeon"] - let expected = toOrderedTable[Version, string]({ - newVersion("9.4.2-hyoyeon"): "v9.4.2-hyoyeon", - newVersion("9.4.0-tiffany"): "v9.4.0-tiffany", - newVersion("9.2.0-sunny"): "v9.2.0-sunny", - newVersion("9.0.1-jessica"): "v9.0.1-jessica", - newVersion("9.0.0-taeyeon"): "v9.0.0-taeyeon" - }) - doAssert expected == getVersionList(data) - - - block: - let data2 = @["v0.1.0", "v0.1.1", "v0.2.0", - "0.4.0", "v0.4.2"] - let expected2 = toOrderedTable[Version, string]({ - newVersion("0.4.2"): "v0.4.2", - newVersion("0.4.0"): "0.4.0", - newVersion("0.2.0"): "v0.2.0", - newVersion("0.1.1"): "v0.1.1", - newVersion("0.1.0"): "v0.1.0", - }) - doAssert expected2 == getVersionList(data2) - - echo("Everything works!") + import unittest + + suite "version sorting": + test "pre-release versions": + let data = @["v9.0.0-taeyeon", "v9.0.1-jessica", "v9.2.0-sunny", + "v9.4.0-tiffany", "v9.4.2-hyoyeon"] + let expected = toOrderedTable[Version, string]({ + newVersion("9.4.2-hyoyeon"): "v9.4.2-hyoyeon", + newVersion("9.4.0-tiffany"): "v9.4.0-tiffany", + newVersion("9.2.0-sunny"): "v9.2.0-sunny", + newVersion("9.0.1-jessica"): "v9.0.1-jessica", + newVersion("9.0.0-taeyeon"): "v9.0.0-taeyeon"}) + check getVersionList(data) == expected + + test "release versions": + let data = @["v0.1.0", "v0.1.1", "v0.2.0", + "0.4.0", "v0.4.2"] + let expected = toOrderedTable[Version, string]({ + newVersion("0.4.2"): "v0.4.2", + newVersion("0.4.0"): "0.4.0", + newVersion("0.2.0"): "v0.2.0", + newVersion("0.1.1"): "v0.1.1", + newVersion("0.1.0"): "v0.1.0",}) + check getVersionList(data) == expected diff --git a/src/nimblepkg/jsonhelpers.nim b/src/nimblepkg/jsonhelpers.nim index 27733dcb..03c97b41 100644 --- a/src/nimblepkg/jsonhelpers.nim +++ b/src/nimblepkg/jsonhelpers.nim @@ -45,28 +45,24 @@ proc cleanUpEmptyObjects*(obj: JsonNode): JsonNode = result = obj when isMainModule: - import unittest - from common import reportUnitTestSuccess - - proc testNewJObjectIfKeyNotExists() = - proc test(testedJson, key, expectedResult: string) = + test "bewJObjectIfKeyNotExists": + proc testProc(testedJson, key, expectedResult: string) = let testedJson = parseJson(testedJson) let expectedResult = parseJson(expectedResult) let actualResult = newJObjectIfKeyNotExists(testedJson, key) check actualResult == expectedResult - test("{}", "key", "{}") - test("{ \"key1\": \"value1\", \"key2\": {} }", "key3", "{}") - test("{ \"key1\": \"value1\", \"key2\": {} }", "key1", "\"value1\"") - test("{ \"key1\": \"value1\", \"key2\": { \"key3\": [ 2, 3, 5] } }", "key2", - "{ \"key3\": [ 2, 3, 5] }") - - proc testAddIfNotExist() = + testProc("{}", "key", "{}") + testProc("{ \"key1\": \"value1\", \"key2\": {} }", "key3", "{}") + testProc("{ \"key1\": \"value1\", \"key2\": {} }", "key1", "\"value1\"") + testProc("{ \"key1\": \"value1\", \"key2\": { \"key3\": [ 2, 3, 5] } }", + "key2", "{ \"key3\": [ 2, 3, 5] }") - proc test(testedJson: string, keys: varargs[string], - jsonToAdd, expectedResult, expectedEndObject: string) = + test "addIfNotExist": + proc testProc(testedJson: string, keys: varargs[string], + jsonToAdd, expectedResult, expectedEndObject: string) = let expectedResult = parseJson(expectedResult) let jsonToAdd = parseJson(jsonToAdd) let actualResult = parseJson(testedJson) @@ -75,36 +71,36 @@ when isMainModule: check actualResult == expectedResult check addedOrOldNode == expectedEndObject - test("{}", "key", "[]", "{ \"key\": [] }", "[]") - test("{}", "key1", "key2", "{}", "{ \"key1\": { \"key2\": {} } }", "{}") - test("{ \"key\": {} }", "key", "[]", "{ \"key\": {} }", "{}") - test("{ \"key1\": { \"key2\": {} } }", "key1", "key2", "[1, 2, 3]", - "{ \"key1\": { \"key2\": {} } }", "{}") - test("{ \"key1\": {}, \"key2\": {} }", "key2", "key3", "{ \"key4\": [1] }", - "{ \"key1\": {}, \"key2\": { \"key3\": { \"key4\": [1] } } }", - "{ \"key4\": [1] }") - - proc testCleanUpEmptyObjects() = - - proc test(testedJson, expectedJson: string) = + testProc("{}", "key", "[]", "{ \"key\": [] }", "[]") + testProc("{}", "key1", "key2", "{}", "{ \"key1\": { \"key2\": {} } }", "{}") + testProc("{ \"key\": {} }", "key", "[]", "{ \"key\": {} }", "{}") + testProc("{ \"key1\": { \"key2\": {} } }", "key1", "key2", "[1, 2, 3]", + "{ \"key1\": { \"key2\": {} } }", "{}") + testProc("{ \"key1\": {}, \"key2\": {} }", "key2", "key3", + "{ \"key4\": [1] }", + "{ \"key1\": {}, \"key2\": { \"key3\": { \"key4\": [1] } } }", + "{ \"key4\": [1] }") + + test "cleanUpEmptyObjects": + proc testProc(testedJson, expectedJson: string) = let testedJsonNode = parseJson(testedJson) let expectedResult = parseJson(expectedJson) let actualResult = cleanUpEmptyObjects(testedJsonNode) check actualResult == expectedResult - test("{}", "{}") - test("[]", "[]") - test("{ \"key\": \"value\" }", "{ \"key\": \"value\" }") - test("[ 3, 1415 ]", "[ 3, 1415 ]") + testProc("{}", "{}") + testProc("[]", "[]") + testProc("{ \"key\": \"value\" }", "{ \"key\": \"value\" }") + testProc("[ 3, 1415 ]", "[ 3, 1415 ]") - test("{ \"key\": [ \"value1\", \"value2\" ] }", - "{ \"key\": [ \"value1\", \"value2\" ] }") + testProc("{ \"key\": [ \"value1\", \"value2\" ] }", + "{ \"key\": [ \"value1\", \"value2\" ] }") - test("{ \"key\": {} }", "{}") - test("[ [], [] ]", "[]") - test("[ { \"key1\": [ { \"key1.1\": [] } ] }, { \"key2\": [] } ]", "[]") + testProc("{ \"key\": {} }", "{}") + testProc("[ [], [] ]", "[]") + testProc("[ { \"key1\": [ { \"key1.1\": [] } ] }, { \"key2\": [] } ]", "[]") - test(""" { + testProc(""" { "key1": { "key1.1": "value1.1", "key1.2": "value1.2" @@ -142,8 +138,3 @@ when isMainModule: ], "key5": 5 }""") - - testNewJObjectIfKeyNotExists() - testAddIfNotExist() - testCleanUpEmptyObjects() - reportUnitTestSuccess() diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 40c6435b..593fed58 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -490,64 +490,65 @@ proc getNameAndVersion*(pkgInfo: PackageInfo): string = when isMainModule: import unittest - check getNameVersionChecksum( - "/home/user/.nimble/libs/packagea-0.1") == - ("packagea", "0.1", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1") == - ("package-a", "0.1", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1/package.nimble") == - ("package-a", "0.1", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#head") == - ("package", "#head", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#branch-with-dashes") == - ("package", "#branch-with-dashes", "") - - # readPackageInfo (and possibly more) depends on this not raising. - check getNameVersionChecksum( - "/home/user/.nimble/libs/package") == - ("package", "", "") - - # Tests with hash sums in the package directory names - - check getNameVersionChecksum( - "/home/user/.nimble/libs/packagea-0.1-" & - "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") == - ("packagea", "0.1", "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1-" & - "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") == - ("package-a", "0.1", "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1-" & - "43e3b1138312656310e93ffcfdd866b2dcce3b35/package.nimble") == - ("package-a", "0.1", "43e3b1138312656310e93ffcfdd866b2dcce3b35") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#head-" & - "efba335dccf2631d7ac2740109142b92beb3b465") == - ("package", "#head", "efba335dccf2631d7ac2740109142b92beb3b465") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#branch-with-dashes-" & - "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") == - ("package", "#branch-with-dashes", "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-" & - "b12e18db49fc60df117e5d8a289c4c2050a272dd") == - ("package", "", "b12e18db49fc60df117e5d8a289c4c2050a272dd") - - check toValidPackageName("foo__bar") == "foo_bar" - check toValidPackageName("jhbasdh!£$@%#^_&*_()qwe") == "jhbasdh_qwe" - - reportUnitTestSuccess() + suite "getNameVersionCheksum": + test "directory names without sha1 hashes": + check getNameVersionChecksum( + "/home/user/.nimble/libs/packagea-0.1") == + ("packagea", "0.1", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1") == + ("package-a", "0.1", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1/package.nimble") == + ("package-a", "0.1", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#head") == + ("package", "#head", "") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#branch-with-dashes") == + ("package", "#branch-with-dashes", "") + + # readPackageInfo (and possibly more) depends on this not raising. + check getNameVersionChecksum( + "/home/user/.nimble/libs/package") == + ("package", "", "") + + test "directory names with sha1 hashes": + check getNameVersionChecksum( + "/home/user/.nimble/libs/packagea-0.1-" & + "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") == + ("packagea", "0.1", "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1-" & + "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") == + ("package-a", "0.1", "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1-" & + "43e3b1138312656310e93ffcfdd866b2dcce3b35/package.nimble") == + ("package-a", "0.1", "43e3b1138312656310e93ffcfdd866b2dcce3b35") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#head-" & + "efba335dccf2631d7ac2740109142b92beb3b465") == + ("package", "#head", "efba335dccf2631d7ac2740109142b92beb3b465") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#branch-with-dashes-" & + "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") == + ("package", "#branch-with-dashes", + "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-" & + "b12e18db49fc60df117e5d8a289c4c2050a272dd") == + ("package", "", "b12e18db49fc60df117e5d8a289c4c2050a272dd") + + test "toValidPackageName": + check toValidPackageName("foo__bar") == "foo_bar" + check toValidPackageName("jhbasdh!£$@%#^_&*_()qwe") == "jhbasdh_qwe" diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 37394073..0cf0199d 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -507,12 +507,9 @@ proc getConcreteVersion*(pkgInfo: PackageInfo, options: Options): string = assert(not newVersion(result).isSpecial) when isMainModule: - validatePackageName("foo_bar") - validatePackageName("f_oo_b_a_r") - try: - validatePackageName("foo__bar") - assert false - except NimbleError: - assert true + import unittest - echo("Everything passed!") + test "validatePackageName": + validatePackageName("foo_bar") + validatePackageName("f_oo_b_a_r") + expect NimbleError, validatePackageName("foo__bar") diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index 7beaf1c7..69d95e24 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -98,11 +98,11 @@ proc topologicalSort*(graph: LockFileDependencies): return (order, cycles) when isMainModule: - import unittest, common + import unittest - proc testTopologicalSort() = + suite "topological sort": - proc testWithoutCycles() = + test "graph without cycles": let graph = { "json_serialization": LockFileDependency( @@ -126,7 +126,7 @@ when isMainModule: check actualTopologicallySortedOrder == expectedTopologicallySortedOrder check actualCycles == expectedCycles - proc testWithCycles() = + test "graph with cycles": let graph = { "A": LockFileDependency(dependencies: @["B", "E"]), @@ -143,9 +143,3 @@ when isMainModule: check actualTopologicallySortedOrder == expectedTopologicallySortedOrder check actualCycles == expectedCycles - - testWithoutCycles() - testWithCycles() - - testTopologicalSort() - reportUnitTestSuccess() diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index 520e006a..ad2efdfe 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -329,99 +329,111 @@ proc `$`*(dep: PkgTuple): string = return dep.name & "@" & $dep.ver 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") - doAssert(inter1.kind == verIntersect) - var inter2 = parseVersionRange("1.0") - doAssert(inter2.kind == verEq) - doAssert(parseVersionRange("== 3.4.2") == parseVersionRange("3.4.2")) - - 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 = toOrderedTable[Version, string]({ - newVersion("0.0.1"): "v0.0.1", - newVersion("0.0.2"): "v0.0.2", - newVersion("0.1.1"): "v0.1.1", - newVersion("0.2.2"): "v0.2.2", - newVersion("0.2.3"): "v0.2.3", - newVersion("0.5"): "v0.5", - newVersion("1.2"): "v1.2", - newVersion("2.2.2"): "v2.2.2", - newVersion("2.2.3"): "v2.2.3", - newVersion("2.3.2"): "v2.3.2", - newVersion("3.2"): "v3.2", - newVersion("3.3.2"): "v3.3.2" - }) - doAssert findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) == - (newVersion("0.2.3"), "v0.2.3") - doAssert findLatest(parseVersionRange("^= 0.1"), versions) == - (newVersion("0.1.1"), "v0.1.1") - doAssert findLatest(parseVersionRange("^= 0"), versions) == - (newVersion("0.5"), "v0.5") - doAssert findLatest(parseVersionRange("~= 2"), versions) == - (newVersion("2.3.2"), "v2.3.2") - doAssert findLatest(parseVersionRange("^= 0.0.1"), versions) == - (newVersion("0.0.1"), "v0.0.1") - doAssert findLatest(parseVersionRange("^= 2.2.2"), versions) == - (newVersion("2.3.2"), "v2.3.2") - doAssert findLatest(parseVersionRange("^= 2.1.1.1"), versions) == - (newVersion("2.3.2"), "v2.3.2") - doAssert findLatest(parseVersionRange("~= 2.2"), versions) == - (newVersion("2.3.2"), "v2.3.2") - doAssert findLatest(parseVersionRange("~= 0.2.2"), 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 newVersion("#ab26sgdt362") != newVersion("#qwersaggdt362") - doAssert newVersion("#ab26saggdt362") == newVersion("#ab26saggdt362") - doAssert newVersion("#head") == newVersion("#HEAD") - doAssert newVersion("#head") == newVersion("#head") - - var sp = parseVersionRange("#ab26sgdt362") - doAssert newVersion("#ab26sgdt362") in sp - doAssert newVersion("#ab26saggdt362") notin sp - - doAssert newVersion("#head") in parseVersionRange("#head") - - # We assume that #head > 0.1.0, in practice this shouldn't be a problem. - doAssert(newVersion("#head") > newVersion("0.1.0")) - doAssert(not(newVersion("#head") > newVersion("#head"))) - doAssert(withinRange(newVersion("#head"), parseVersionRange(">= 0.5.0"))) - doAssert newVersion("#a111") < newVersion("#head") - # We assume that all other special versions are not higher than a normal - # version. - doAssert newVersion("#a111") < newVersion("1.1") - - # An empty version range should give verAny - doAssert parseVersionRange("").kind == verAny - - # toVersionRange tests - doAssert toVersionRange(newVersion("#head")).kind == verSpecial - doAssert toVersionRange(newVersion("0.2.0")).kind == verEq - - # Something raised on IRC - doAssert newVersion("1") == newVersion("1.0") - - echo("Everything works!") + import unittest + + suite "version": + setup: + let versionRange1 {.used.} = parseVersionRange(">= 1.0 & <= 1.5") + let versionRange2 {.used.} = parseVersionRange("1.0") + + test "versions comparison": + check newVersion("1.0") < newVersion("1.4") + check newVersion("1.0.1") > newVersion("1.0") + check newVersion("1.0.6") <= newVersion("1.0.6") + check not (newVersion("0.1.0") < newVersion("0.1")) + check not (newVersion("0.1.0") > newVersion("0.1")) + check newVersion("0.1.0") < newVersion("0.1.0.0.1") + check newVersion("0.1.0") <= newVersion("0.1") + check newVersion("1") == newVersion("1") + check newVersion("1.0.2.4.6.1.2.123") == newVersion("1.0.2.4.6.1.2.123") + check newVersion("1.0.2") != newVersion("1.0.2.4.6.1.2.123") + check newVersion("1.0.3") != newVersion("1.0.2") + check newVersion("1") == newVersion("1.0") + + test "version comparison with empty version": + check not (newVersion("") < newVersion("0.0.0")) + check newVersion("") < newVersion("1.0.0") + check newVersion("") < newVersion("0.1.0") + + test "comparison of Nimble special versions": + check newVersion("#ab26sgdt362") != newVersion("#qwersaggdt362") + check newVersion("#ab26saggdt362") == newVersion("#ab26saggdt362") + check newVersion("#head") == newVersion("#HEAD") + check newVersion("#head") == newVersion("#head") + + test "#head is bigger than any other version": + check newVersion("#head") > newVersion("0.1.0") + check not (newVersion("#head") > newVersion("#head")) + check withinRange(newVersion("#head"), parseVersionRange(">= 0.5.0")) + check newVersion("#a111") < newVersion("#head") + + test "all special versions except #head are smaller than normal versions": + doAssert newVersion("#a111") < newVersion("1.1") + + # TODO: Allow these in later versions? + test "comparison of semantic versions with release candidate tags in them": + skip() + # check newVersion("0.1-rc1") < newVersion("0.2") + # check newVersion("0.1-rc1") < newVersion("0.1") + + test "parse version range": + check parseVersionRange("== 3.4.2") == parseVersionRange("3.4.2") + + test "correct version range kinds": + check versionRange1.kind == verIntersect + check versionRange2.kind == verEq + # An empty version range should give verAny + doAssert parseVersionRange("").kind == verAny + + test "version is within range": + let version1 = newVersion("0.1.0") + let version2 = newVersion("1.5.1") + let version3 = newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12") + let versionRange = parseVersionRange("> 0.1") + check not withinRange(version1, versionRange) + check not withinRange(version2, versionRange1) + check withinRange(version3, versionRange1) + + test "in and notin operators": + let versionRange = parseVersionRange("#ab26sgdt362") + check newVersion("#ab26sgdt362") in versionRange + check newVersion("#ab26saggdt362") notin versionRange + check newVersion("#head") in parseVersionRange("#head") + + test "find latest version": + let versions = toOrderedTable[Version, string]({ + newVersion("0.0.1"): "v0.0.1", + newVersion("0.0.2"): "v0.0.2", + newVersion("0.1.1"): "v0.1.1", + newVersion("0.2.2"): "v0.2.2", + newVersion("0.2.3"): "v0.2.3", + newVersion("0.5"): "v0.5", + newVersion("1.2"): "v1.2", + newVersion("2.2.2"): "v2.2.2", + newVersion("2.2.3"): "v2.2.3", + newVersion("2.3.2"): "v2.3.2", + newVersion("3.2"): "v3.2", + newVersion("3.3.2"): "v3.3.2" + }) + check findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) == + (newVersion("0.2.3"), "v0.2.3") + check findLatest(parseVersionRange("^= 0.1"), versions) == + (newVersion("0.1.1"), "v0.1.1") + check findLatest(parseVersionRange("^= 0"), versions) == + (newVersion("0.5"), "v0.5") + check findLatest(parseVersionRange("~= 2"), versions) == + (newVersion("2.3.2"), "v2.3.2") + check findLatest(parseVersionRange("^= 0.0.1"), versions) == + (newVersion("0.0.1"), "v0.0.1") + check findLatest(parseVersionRange("^= 2.2.2"), versions) == + (newVersion("2.3.2"), "v2.3.2") + check findLatest(parseVersionRange("^= 2.1.1.1"), versions) == + (newVersion("2.3.2"), "v2.3.2") + check findLatest(parseVersionRange("~= 2.2"), versions) == + (newVersion("2.3.2"), "v2.3.2") + check findLatest(parseVersionRange("~= 0.2.2"), versions) == + (newVersion("0.2.3"), "v0.2.3") + + test "convert version to version range": + check toVersionRange(newVersion("#head")).kind == verSpecial + check toVersionRange(newVersion("0.2.0")).kind == verEq diff --git a/tests/tester.nim b/tests/tester.nim index 97df3bec..fc1bc953 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1652,11 +1652,11 @@ suite "Module tests": moduleTest "aliasthis" moduleTest "common" + moduleTest "counttables" moduleTest "download" moduleTest "jsonhelpers" moduleTest "packageinfo" moduleTest "packageparser" - moduleTest "paths" moduleTest "reversedeps" moduleTest "topologicalsort" moduleTest "version" From b494335868daba8191f4879addf1cddff43745e4 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 11 Nov 2020 07:30:28 +0200 Subject: [PATCH 20/73] Add unit tests for paths module Add unit tests for two procedures in paths module which are different for `Path` distinct type from the procedures from the `os` module working with strings. Those are `hash` and `==`. Also add execution of paths module tests to the "Modules test" suite in `tester.nim`. Related to nim-lang/nimble#127 --- src/nimblepkg/paths.nim | 12 ++++++++++++ tests/tester.nim | 1 + 2 files changed, 13 insertions(+) diff --git a/src/nimblepkg/paths.nim b/src/nimblepkg/paths.nim index eb1daf15..4f4a08c5 100644 --- a/src/nimblepkg/paths.nim +++ b/src/nimblepkg/paths.nim @@ -28,3 +28,15 @@ proc hash*(path: Path): Hash = hash(absolutePath(string(path))) proc `==`*(lhs, rhs: Path): bool = absolutePath(string(lhs)) == absolutePath(string(rhs)) + +when isMainModule: + import unittest + + const testDir: Path = "some/relative/path/" + let absolutePathToTestDir: Path = getCurrentDir() / testDir + + test "hashing": + check hash(testDir) == hash(absolutePathToTestDir) + + test "equals": + check testDir == absolutePathToTestDir diff --git a/tests/tester.nim b/tests/tester.nim index fc1bc953..290801a4 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1657,6 +1657,7 @@ suite "Module tests": moduleTest "jsonhelpers" moduleTest "packageinfo" moduleTest "packageparser" + moduleTest "paths" moduleTest "reversedeps" moduleTest "topologicalsort" moduleTest "version" From f22cd0b6e5930e50d76a0b847dafb22d02f44196 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 11 Nov 2020 18:23:16 +0200 Subject: [PATCH 21/73] Use distinct type for sha1 hash values Use distinct type for sha1 hash values in all places in code where sha1 hash values are used. Those are Git and Mercurial commit hashes and package checksums. The new distinct type can only be created outside from its module with a special `init` method which does validation before setting the value. Additional changes: - `path` command tests are disabled because it is currently not working correctly and its functionality has to be redesigned. - tests for the `getNameVersionChacksum` procedure are moved to the `tools` module where is the procedure itself. Related to nim-lang/nimble#127 --- .gitignore | 1 + src/nimble.nim | 36 ++++---- src/nimblepkg/checksum.nim | 13 +-- src/nimblepkg/common.nim | 2 + src/nimblepkg/developfile.nim | 4 +- src/nimblepkg/download.nim | 18 ++-- src/nimblepkg/lockfile.nim | 13 ++- src/nimblepkg/packageinfo.nim | 67 ++------------- src/nimblepkg/packageinfotypes.nim | 10 ++- src/nimblepkg/packagemetadatafile.nim | 12 ++- src/nimblepkg/packageparser.nim | 10 +-- src/nimblepkg/pkgnameversionchecksum | Bin 0 -> 337192 bytes src/nimblepkg/reversedeps.nim | 119 +++++++++++++++----------- src/nimblepkg/sha1hashes.nim | 86 +++++++++++++++++++ src/nimblepkg/tools.nim | 91 ++++++++++++++++++-- src/nimblepkg/topologicalsort.nim | 51 ++++++----- tests/tester.nim | 50 ++++++----- 17 files changed, 377 insertions(+), 206 deletions(-) create mode 100755 src/nimblepkg/pkgnameversionchecksum create mode 100644 src/nimblepkg/sha1hashes.nim diff --git a/.gitignore b/.gitignore index 8855a505..4ece31a9 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ src/nimblepkg/packageparser src/nimblepkg/paths src/nimblepkg/publish src/nimblepkg/reversedeps +src/nimblepkg/sha1hashes src/nimblepkg/tools src/nimblepkg/topologicalsort src/nimblepkg/version diff --git a/src/nimble.nim b/src/nimble.nim index d619db5d..25b1b543 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -4,7 +4,7 @@ import system except TResult import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc, - strformat + strformat, sequtils import std/options as std_opt @@ -20,7 +20,7 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/checksum, nimblepkg/topologicalsort, nimblepkg/lockfile, nimblepkg/nimscriptwrapper, nimblepkg/developfile, nimblepkg/paths, nimblepkg/nimbledatafile, nimblepkg/packagemetadatafile, - nimblepkg/displaymessages + nimblepkg/displaymessages, nimblepkg/sha1hashes proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -47,6 +47,14 @@ proc refresh(options: Options) = for name, list in options.config.packageLists: fetchList(list, options) +proc initPkgList(pkgInfo: PackageInfo, options: Options): seq[PackageInfo] = + let + installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) + developPkgs = processDevelopDependencies(pkgInfo, options) + {.warning[ProveInit]: off.} + result = concat(installedPkgs, developPkgs) + {.warning[ProveInit]: on.} + proc install(packages: seq[PkgTuple], options: Options, doPrompt, first, fromLockFile: bool): PackageDependenciesInfo @@ -61,9 +69,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): "processFreeDependencies needs pkgInfo.requires" var pkgList {.global.}: seq[PackageInfo] = @[] - once: - pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - pkgList.add processDevelopDependencies(pkgInfo, options) + once: pkgList = initPkgList(pkgInfo, options) display("Verifying", "dependencies for $1@$2" % [pkgInfo.name, pkgInfo.specialVersion], @@ -79,7 +85,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): else: let resolvedDep = dep.resolveAlias(options) display("Checking", "for $1" % $resolvedDep, priority = MediumPriority) - var pkg: PackageInfo + var pkg = initPackageInfo() var found = findPkg(pkgList, resolvedDep, pkg) # Check if the original name exists. if not found and resolvedDep.name != dep.name: @@ -225,7 +231,7 @@ proc removeBinariesSymlinks(pkgInfo: PackageInfo, binDir: string) = proc reinstallSymlinksForOlderVersion(pkgDir: string, options: Options) = let (pkgName, _, _) = getNameVersionChecksum(pkgDir) let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) - var newPkgInfo: PackageInfo + var newPkgInfo = initPackageInfo() if pkgList.findPkg((pkgName, newVRAny()), newPkgInfo): newPkgInfo = newPkgInfo.toFullInfo(options) for bin in newPkgInfo.binaries: @@ -442,7 +448,7 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): let packagesDir = options.getPkgsDir() for name, dep in pkgInfo.lockedDependencies: - let depDirName = packagesDir / fmt"{name}-{dep.version}-{dep.checksum.sha1}" + let depDirName = packagesDir / &"{name}-{dep.version}-{dep.checksums.sha1}" if not fileExists(depDirName / packageMetaDataFileName): if depDirName.dirExists: @@ -458,9 +464,9 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): downloadPath = "", dep.vcsRevision) let downloadedPackageChecksum = calculatePackageSha1Checksum(downloadDir) - if downloadedPackageChecksum != dep.checksum.sha1: + if downloadedPackageChecksum != dep.checksums.sha1: raise checksumError(name, dep.version, dep.vcsRevision, - downloadedPackageChecksum, dep.checksum.sha1) + downloadedPackageChecksum, dep.checksums.sha1) let (_, newlyInstalledPackageInfo) = installFromDir( downloadDir, version, options, url, first = false, fromLockFile = true) @@ -468,7 +474,7 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): for depDepName in dep.dependencies: let depDep = pkgInfo.lockedDependencies[depDepName] let revDep = (name: depDepName, version: depDep.version, - checksum: depDep.checksum.sha1) + checksum: depDep.checksums.sha1) options.nimbleData.addRevDep(revDep, newlyInstalledPackageInfo) result.incl newlyInstalledPackageInfo @@ -527,7 +533,7 @@ proc install(packages: seq[PkgTuple], options: Options, let subdir = metadata.getOrDefault("subdir") let (downloadDir, downloadVersion) = downloadPkg(url, pv.ver, meth, subdir, options, downloadPath = "", - vcsRevision = "") + vcsRevision = notSetSha1Hash) try: result = installFromDir(downloadDir, pv.ver, options, url, first, fromLockFile) @@ -741,7 +747,7 @@ proc getPackageByPattern(pattern: string, options: Options): PackageInfo = # Last resort - attempt to read as package identifier let packages = getInstalledPkgsMin(options.getPkgsDir(), options) let identTuple = parseRequires(pattern) - var skeletonInfo: PackageInfo + var skeletonInfo = initPackageInfo() if not findPkg(packages, identTuple, skeletonInfo): raise nimbleError( "Specified package not found" @@ -1083,7 +1089,7 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = var options = options options.forceFullClone = true discard downloadPkg(url, ver, meth, subdir, options, downloadDir, - vcsRevision = "") + vcsRevision = notSetSha1Hash) let pkgDir = downloadDir / subdir var pkgInfo = getPkgInfo(pkgDir, options) @@ -1102,7 +1108,7 @@ proc develop(options: var Options) = if not hasPackages and hasPath: raise nimbleError(pathGivenButNoPkgsToDownloadMsg) - var currentDirPkgInfo: PackageInfo + var currentDirPkgInfo = initPackageInfo() try: # Check whether the current directory is a package directory. diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksum.nim index 7099f23f..b8bf152b 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksum.nim @@ -2,14 +2,15 @@ # BSD License. Look at license.txt for more info. import os, strutils, std/sha1, algorithm, strformat -import common, tools +import common, tools, sha1hashes type ChecksumError* = object of NimbleError -proc checksumError*(name, version, vcsRevision, checksum, - expectedChecksum: string): ref ChecksumError = - result = newNimbleError[ChecksumError](fmt""" +proc checksumError*(name, version: string, + vcsRevision, checksum, expectedChecksum: Sha1Hash): + ref ChecksumError = + result = newNimbleError[ChecksumError](&""" Downloaded package checksum does not correspond to that in the lock file: Package: {name}@v.{version}@r.{vcsRevision} Checksum: {checksum} @@ -58,10 +59,10 @@ proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = if bytesRead == 0: break checksum.update(buffer[0.. 0 - display("Cloning", "revision: " & vcsRevision, priority = MediumPriority) + url, downloadDir: string, vcsRevision: Sha1Hash) = + assert vcsRevision != notSetSha1Hash + display("Cloning", "revision: " & $vcsRevision, priority = MediumPriority) case downloadMethod of DownloadMethod.git: createDir(downloadDir) @@ -173,7 +176,7 @@ proc cloneSpecificRevision(downloadMethod: DownloadMethod, proc doDownload(url: string, downloadDir: string, verRange: VersionRange, downMethod: DownloadMethod, options: Options, - vcsRevision: string): Version = + vcsRevision: Sha1Hash): Version = ## Downloads the repository specified by ``url`` using the specified download ## method. ## @@ -192,7 +195,7 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, result = latest.ver removeDir(downloadDir) - if vcsRevision.len > 0: + if vcsRevision != notSetSha1Hash: cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) elif verRange.kind == verSpecial: # We want a specific commit/branch/tag here. @@ -243,7 +246,8 @@ proc downloadPkg*(url: string, verRange: VersionRange, downMethod: DownloadMethod, subdir: string, options: Options, - downloadPath, vcsRevision: string): (string, Version) = + downloadPath: string, + vcsRevision: Sha1Hash): (string, Version) = ## Downloads the repository as specified by ``url`` and ``verRange`` using ## the download method specified. ## diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim index fcafb7ad..bfa139ad 100644 --- a/src/nimblepkg/lockfile.nim +++ b/src/nimblepkg/lockfile.nim @@ -2,18 +2,19 @@ # BSD License. Look at license.txt for more info. import tables, os, json +import sha1hashes type Checksums* = object - sha1*: string + sha1*: Sha1Hash LockFileDependency* = object version*: string - vcsRevision*: string + vcsRevision*: Sha1Hash url*: string downloadMethod*: string dependencies*: seq[string] - checksum*: Checksums + checksums*: Checksums LockFileDependencies* = OrderedTable[string, LockFileDependency] @@ -49,7 +50,11 @@ proc writeLockFile*(packages: LockFileDependencies, writeLockFile(lockFileName, packages, topologicallySortedOrder) proc readLockFile*(filePath: string): LockFileDependencies = - parseFile(filePath)[$lfjkPackages].to(result.typeof) + {.warning[UnsafeDefault]: off.} + {.warning[ProveInit]: off.} + result = parseFile(filePath)[$lfjkPackages].to(result.typeof) + {.warning[ProveInit]: on.} + {.warning[UnsafeDefault]: on.} proc readLockFileInDir*(dir: string): LockFileDependencies = readLockFile(dir / lockFileName) diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 593fed58..07f676eb 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -8,7 +8,12 @@ from net import SslError # Local imports import version, tools, common, options, cli, config, lockfile, packageinfotypes, - packagemetadatafile + packagemetadatafile, sha1hashes + +proc initPackageInfo*(): PackageInfo = + result = PackageInfo( + basicInfo: ("", "", notSetSha1Hash), + metaData: initPackageMetaData()) proc isLoaded*(pkgInfo: PackageInfo): bool = return pkgInfo.myPath.len > 0 @@ -18,6 +23,7 @@ proc hasMetaData*(pkgInfo: PackageInfo): bool = pkgInfo.files.len > 0 proc initPackageInfo*(filePath: string): PackageInfo = + result = initPackageInfo() let (fileDir, fileName, _) = filePath.splitFile result.myPath = filePath result.name = fileName @@ -490,65 +496,6 @@ proc getNameAndVersion*(pkgInfo: PackageInfo): string = when isMainModule: import unittest - suite "getNameVersionCheksum": - test "directory names without sha1 hashes": - check getNameVersionChecksum( - "/home/user/.nimble/libs/packagea-0.1") == - ("packagea", "0.1", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1") == - ("package-a", "0.1", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1/package.nimble") == - ("package-a", "0.1", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#head") == - ("package", "#head", "") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#branch-with-dashes") == - ("package", "#branch-with-dashes", "") - - # readPackageInfo (and possibly more) depends on this not raising. - check getNameVersionChecksum( - "/home/user/.nimble/libs/package") == - ("package", "", "") - - test "directory names with sha1 hashes": - check getNameVersionChecksum( - "/home/user/.nimble/libs/packagea-0.1-" & - "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") == - ("packagea", "0.1", "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1-" & - "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") == - ("package-a", "0.1", "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-a-0.1-" & - "43e3b1138312656310e93ffcfdd866b2dcce3b35/package.nimble") == - ("package-a", "0.1", "43e3b1138312656310e93ffcfdd866b2dcce3b35") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#head-" & - "efba335dccf2631d7ac2740109142b92beb3b465") == - ("package", "#head", "efba335dccf2631d7ac2740109142b92beb3b465") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-#branch-with-dashes-" & - "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") == - ("package", "#branch-with-dashes", - "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") - - check getNameVersionChecksum( - "/home/user/.nimble/libs/package-" & - "b12e18db49fc60df117e5d8a289c4c2050a272dd") == - ("package", "", "b12e18db49fc60df117e5d8a289c4c2050a272dd") - test "toValidPackageName": check toValidPackageName("foo__bar") == "foo_bar" check toValidPackageName("jhbasdh!£$@%#^_&*_()qwe") == "jhbasdh_qwe" diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 0028fc54..cb18f975 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -2,12 +2,12 @@ # BSD License. Look at license.txt for more info. import sets, tables -import version, lockfile, aliasthis +import version, lockfile, aliasthis, sha1hashes type PackageMetaDataBase* {.inheritable.} = object url*: string - vcsRevision*: string + vcsRevision*: Sha1Hash files*: seq[string] binaries*: seq[string] @@ -24,7 +24,7 @@ type PackageBasicInfo* = tuple name: string version: string - checksum: string + checksum: Sha1Hash PackageInfo* = object myPath*: string ## The path of this .nimble file @@ -69,5 +69,9 @@ type PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] +{.warning[UnsafeDefault]: off.} +{.warning[ProveInit]: off.} aliasThis PackageInfo.metaData aliasThis PackageInfo.basicInfo +{.warning[ProveInit]: on.} +{.warning[UnsafeDefault]: on.} diff --git a/src/nimblepkg/packagemetadatafile.nim b/src/nimblepkg/packagemetadatafile.nim index 0fd38969..068bc94d 100644 --- a/src/nimblepkg/packagemetadatafile.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import json, os, strformat -import common, packageinfotypes, cli, tools +import common, packageinfotypes, cli, tools, sha1hashes type MetaDataError* = object of NimbleError @@ -15,12 +15,17 @@ const packageMetaDataFileName* = "nimblemeta.json" packageMetaDataFileVersion = "0.1.0" +proc initPackageMetaData*(): PackageMetaData = + result = PackageMetaData(vcsRevision: notSetSha1Hash) + proc metaDataError(msg: string): ref MetaDataError = newNimbleError[MetaDataError](msg) proc saveMetaData*(metaData: PackageMetaData, dirName: string) = ## Saves some important data to file in the package installation directory. + {.warning[ProveInit]: off.} var metaDataWithChangedPaths = to(metaData, PackageMetaDataV2) + {.warning[ProveInit]: on.} for i, file in metaData.files: metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file) let json = %{ @@ -30,15 +35,20 @@ proc saveMetaData*(metaData: PackageMetaData, dirName: string) = proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData = ## Returns package meta data read from file in package installation directory + result = initPackageMetaData() let fileName = dirName / packageMetaDataFileName if fileExists(fileName): let json = parseFile(fileName) if not json.hasKey($pmdjkVersion): + {.warning[ProveInit]: off.} result = to(json.to(PackageMetaDataV1), PackageMetaData) + {.warning[ProveInit]: on.} let (_, specialVersion, _) = getNameVersionChecksum(dirName) result.specialVersion = specialVersion else: + {.warning[ProveInit]: off.} result = to(json[$pmdjkMetaData].to(PackageMetaDataV2), PackageMetaData) + {.warning[ProveInit]: on.} elif raiseIfNotFound: raise metaDataError(&"No {packageMetaDataFileName} file found in {dirName}") else: diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 0cf0199d..f20c5ef7 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -3,7 +3,7 @@ import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map, toSeq -import common, version, tools, nimscriptwrapper, options, cli, +import common, version, tools, nimscriptwrapper, options, cli, sha1hashes, packagemetadatafile, packageinfo, packageinfotypes, checksum ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` @@ -406,7 +406,7 @@ proc getPkgInfoFromFile*(file: NimbleFile, options: Options, forValidation = false): PackageInfo = ## Reads the specified .nimble file and returns its data as a PackageInfo ## object. Any validation errors are handled and displayed as warnings. - var info: PackageInfo + var info = initPackageInfo() try: info = readPackageInfo(file, options) except ValidationError: @@ -437,7 +437,7 @@ proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfo] = proc createErrorMsg(tmplt, path, msg: string): string = let (name, version, checksum) = getNameVersionChecksum(path) - let fullVersion = if checksum.len > 0: version & "@c." & checksum + let fullVersion = if checksum != notSetSha1Hash: version & "@c." & $checksum else: version return tmplt % [name, fullVersion, msg] @@ -448,7 +448,7 @@ proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfo] = if kind == pcDir: let nimbleFile = findNimbleFile(path, false) if nimbleFile != "": - var pkg: PackageInfo + var pkg = initPackageInfo() try: pkg = readPackageInfo(nimbleFile, options, onlyMinimalInfo=false) fillMetaData(pkg, path, false) @@ -488,7 +488,7 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = "A package must not be simultaneously installed and linked." if result.isInstalled: - assert result.vcsRevision.len == 0, + assert result.vcsRevision == notSetSha1Hash, "Should not have a VCS revision read from package directory for " & "installed packages." diff --git a/src/nimblepkg/pkgnameversionchecksum b/src/nimblepkg/pkgnameversionchecksum new file mode 100755 index 0000000000000000000000000000000000000000..e73513e1ba4e590ec5f2e4757c085867a8ead8ee GIT binary patch literal 337192 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%0AXC*!p3wbgIbjIJLbVe(kB}}Yb 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 literal 0 HcmV?d00001 diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 2738feb1..f29474dd 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -3,7 +3,7 @@ import json, sets, os, hashes, unicode -import options, version, download, jsonhelpers, nimbledatafile, +import options, version, download, jsonhelpers, nimbledatafile, sha1hashes, packageinfotypes, packageinfo, packageparser type @@ -53,7 +53,7 @@ proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, $ndjkRevDep, dep.name.toLower, dep.version, - dep.checksum, + $dep.checksum, newJArray()) var dependency: JsonNode @@ -86,7 +86,7 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = if rd[$ndjkRevDepPath].str != pkg.getNimbleFileDir: # It is compared by its directory path. newVal.add rd - elif rd[$ndjkRevDepChecksum].str != pkg.checksum: + elif rd[$ndjkRevDepChecksum].str != $pkg.checksum: # For the reverse dependencies added since the introduction of the # new format comparison of the checksums is specific enough. newVal.add rd @@ -94,7 +94,8 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = # But if the both checksums are not present, those are converted # from the old format packages and they must be compared by the # `name` and `specialVersion` fields. - if rd[$ndjkRevDepChecksum].str.len == 0 and pkg.checksum.len == 0: + if rd[$ndjkRevDepChecksum].str.len == 0 and + pkg.checksum == notSetSha1Hash: if rd[$ndjkRevDepName].str != pkg.name.toLower or rd[$ndjkRevDepVersion].str != pkg.specialVersion: newVal.add rd @@ -123,7 +124,7 @@ proc getRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency): return let reverseDependencies = nimbleData[$ndjkRevDep]{ - pkg.pkgInfo.name.toLower}{pkg.pkgInfo.version}{pkg.pkgInfo.checksum} + pkg.pkgInfo.name.toLower}{pkg.pkgInfo.version}{$pkg.pkgInfo.checksum} if reverseDependencies.isNil: return @@ -135,9 +136,10 @@ proc getRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency): result.incl ReverseDependency(kind: rdkDevelop, pkgPath: path) else: # This is an installed package. - let pkgBasicInfo = (name: revDep[$ndjkRevDepName].str, - version: revDep[$ndjkRevDepVersion].str, - checksum: revDep[$ndjkRevDepChecksum].str) + let pkgBasicInfo = + (name: revDep[$ndjkRevDepName].str, + version: revDep[$ndjkRevDepVersion].str, + checksum: revDep[$ndjkRevDepChecksum].str.initSha1Hash) result.incl ReverseDependency(kind: rdkInstalled, pkgInfo: pkgBasicInfo) proc toPkgInfo*(revDep: ReverseDependency, options: Options): PackageInfo = @@ -167,46 +169,61 @@ proc getAllRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency, getAllRevDeps(nimbleData, revDep, result) when isMainModule: - import unittest + import unittest, sequtils - let - nimforum1 = PackageInfo( - basicInfo: - ("nimforum", "0.1.0", "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2"), - requires: @[("jester", parseVersionRange("0.1.0")), - ("captcha", parseVersionRange("1.0.0")), - ("auth", parseVersionRange("#head"))]) + type + RequiresSeq = seq[tuple[name, versionRange: string]] - nimforum1RevDep = nimforum1.toRevDep + proc initMetaData(isLink: bool): PackageMetaData = + result = PackageMetaData( + vcsRevision: notSetSha1Hash, + isLink: isLink) - nimforum2 = PackageInfo(basicInfo: - ("nimforum", "0.2.0", "B60044137CEA185F287346EBEAB6B3E0895BDA4D")) + proc parseRequires(requires: RequiresSeq): seq[PkgTuple] = + requires.mapIt((it.name, it.versionRange.parseVersionRange)) - nimforum2RevDep = nimforum2.toRevDep + proc initPackageInfo(path: string, requires: RequiresSeq = @[]): PackageInfo = + result = PackageInfo( + myPath: path, + requires: requires.parseRequires, + metaData: initMetaData(true)) + + proc initPackageInfo(name, version, checksum: string, + requires: RequiresSeq = @[]): PackageInfo = + result = PackageInfo( + basicInfo: (name, version, checksum.initSha1Hash), + requires: requires.parseRequires, + metaData: initMetaData(false)) + + let + nimforum1 = initPackageInfo( + "nimforum", "0.1.0", "46a96c3f2b0ecb3d3f7bd71e12200ed401e9b9f2", + @[("jester", "0.1.0"), ("captcha", "1.0.0"), ("auth", "#head")]) + nimforum1RevDep = nimforum1.toRevDep - play = PackageInfo( - basicInfo: ("play", "2.0.1", "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9")) + nimforum2 = initPackageInfo( + "nimforum", "0.2.0", "b60044137cea185f287346ebeab6b3e0895bda4d") + nimforum2RevDep = nimforum2.toRevDep - nimforumDevelop = PackageInfo( - myPath: "/some/absolute/system/path/nimforum/nimforum.nimble", - metaData: PackageMetaData(isLink: true), - requires: @[("captcha", parseVersionRange("1.0.0"))]) + play = initPackageInfo( + "play", "2.0.1", "8a54cca572977ed0cc73b9bf783e9dfa6b6f2bf9") + nimforumDevelop = initPackageInfo( + "/some/absolute/system/path/nimforum/nimforum.nimble", + @[("captcha", "1.0.0")]) nimforumDevelopRevDep = nimforumDevelop.toRevDep - jester = PackageInfo(basicInfo: - ("jester", "0.1.0", "1B629F98B23614DF292F176A1681FA439DCC05E2")) - - jesterWithoutSha1 = PackageInfo(basicInfo: ("jester", "0.1.0", "")) + jester = initPackageInfo( + "jester", "0.1.0", "1b629f98b23614df292f176a1681fa439dcc05e2") - captcha = PackageInfo(basicInfo: - ("captcha", "1.0.0", "CE128561B06DD106A83638AD415A2A52548F388E")) + jesterWithoutSha1 = initPackageInfo("jester", "0.1.0", "") + captcha = initPackageInfo( + "captcha", "1.0.0", "ce128561b06dd106a83638ad415a2a52548f388e") captchaRevDep = captcha.toRevDep - auth = PackageInfo( - basicInfo: ("auth", "#head", "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01")) - + auth = initPackageInfo( + "auth", "#head", "c81545df8a559e3da7d38d125e0eaf2b4478cd01") authRevDep = auth.toRevDep suite "reverse dependencies": @@ -227,34 +244,34 @@ when isMainModule: "reverseDeps": { "jester": { "0.1.0": { - "1B629F98B23614DF292F176A1681FA439DCC05E2": [ + "1b629f98b23614df292f176a1681fa439dcc05e2": [ { "name": "nimforum", "version": "0.1.0", - "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + "checksum": "46a96c3f2b0ecb3d3f7bd71e12200ed401e9b9f2" } ], "": [ { "name": "play", "version": "2.0.1", - "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + "checksum": "8a54cca572977ed0cc73b9bf783e9dfa6b6f2bf9" } ] } }, "captcha": { "1.0.0": { - "CE128561B06DD106A83638AD415A2A52548F388E": [ + "ce128561b06dd106a83638ad415a2a52548f388e": [ { "name": "nimforum", "version": "0.1.0", - "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + "checksum": "46a96c3f2b0ecb3d3f7bd71e12200ed401e9b9f2" }, { "name": "nimforum", "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + "checksum": "b60044137cea185f287346ebeab6b3e0895bda4d" }, { "path": "/some/absolute/system/path/nimforum" @@ -264,21 +281,21 @@ when isMainModule: }, "auth": { "#head": { - "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ + "c81545df8a559e3da7d38d125e0eaf2b4478cd01": [ { "name": "nimforum", "version": "0.1.0", - "checksum": "46A96C3F2B0ECB3D3F7BD71E12200ED401E9B9F2" + "checksum": "46a96c3f2b0ecb3d3f7bd71e12200ed401e9b9f2" }, { "name": "nimforum", "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + "checksum": "b60044137cea185f287346ebeab6b3e0895bda4d" }, { "name": "captcha", "version": "1.0.0", - "checksum": "CE128561B06DD106A83638AD415A2A52548F388E" + "checksum": "ce128561b06dd106a83638ad415a2a52548f388e" } ] } @@ -298,34 +315,34 @@ when isMainModule: { "name": "play", "version": "2.0.1", - "checksum": "8A54CCA572977ED0CC73B9BF783E9DFA6B6F2BF9" + "checksum": "8a54cca572977ed0cc73b9bf783e9dfa6b6f2bf9" } ] } }, "captcha": { "1.0.0": { - "CE128561B06DD106A83638AD415A2A52548F388E": [ + "ce128561b06dd106a83638ad415a2a52548f388e": [ { "name": "nimforum", "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + "checksum": "b60044137cea185f287346ebeab6b3e0895bda4d" } ] } }, "auth": { "#head": { - "C81545DF8A559E3DA7D38D125E0EAF2B4478CD01": [ + "c81545df8a559e3da7d38d125e0eaf2b4478cd01": [ { "name": "nimforum", "version": "0.2.0", - "checksum": "B60044137CEA185F287346EBEAB6B3E0895BDA4D" + "checksum": "b60044137cea185f287346ebeab6b3e0895bda4d" }, { "name": "captcha", "version": "1.0.0", - "checksum": "CE128561B06DD106A83638AD415A2A52548F388E" + "checksum": "ce128561b06dd106a83638ad415a2a52548f388e" } ] } diff --git a/src/nimblepkg/sha1hashes.nim b/src/nimblepkg/sha1hashes.nim new file mode 100644 index 00000000..bcc8691d --- /dev/null +++ b/src/nimblepkg/sha1hashes.nim @@ -0,0 +1,86 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import strformat, strutils, json +import common + +type + InvalidSha1HashError* = object of NimbleError + ## Represents an error caused by invalid value of a sha1 hash. + + Sha1Hash* {.requiresInit.} = object + ## Type representing a sha1 hash value. It can only be created by special + ## procedure which validates the input. + hashValue: string + +template `$`*(sha1Hash: Sha1Hash): string = sha1Hash.hashValue +template `%`*(sha1Hash: Sha1Hash): JsonNode = %sha1Hash.hashValue +template `==`*(lhs, rhs: Sha1Hash): bool = lhs.hashValue == rhs.hashValue + +proc invalidSha1Hash(value: string): ref InvalidSha1HashError = + ## Creates a new exception object for an invalid sha1 hash value. + result = newNimbleError[InvalidSha1HashError]( + &"The string '{value}' does not represent a valid sha1 hash value.") + +proc validateSha1Hash(value: string): bool = + ## Checks whether given string is a valid sha1 hash value. Only lower case + ## hexadecimal digits are accepted. + if value.len == 0: + # Empty string is used as a special value for not set sha1 hash. + return true + if value.len != 40: + # Valid sha1 hash must be exactly 40 characters long. + return false + for c in value: + if c notin {'0' .. '9', 'a'..'f'}: + # All characters of valid sha1 hash must be hexadecimal digits with lower + # case letters for digits representing numbers between 10 and 15 + # ('a' to 'f'). + return false + return true + +proc initSha1Hash*(value: string): Sha1Hash = + ## Creates a new `Sha1Hash` object from a string by making all latin letters + ## lower case and validating the transformed value. In the case the supplied + ## string is not a valid sha1 hash value then raises an `InvalidSha1HashError` + ## exception. + let value = value.toLowerAscii + if not validateSha1Hash(value): + raise invalidSha1Hash(value) + return Sha1Hash(hashValue: value) + +const + notSetSha1Hash* = initSha1Hash("") + +proc initFromJson*(dst: var Sha1Hash, jsonNode: JsonNode, + jsonPath: var string) = + case jsonNode.kind + of JNull: dst = notSetSha1Hash + of JObject: dst = initSha1Hash(jsonNode["hashValue"].str) + of JString: dst = initSha1Hash(jsonNode.str) + else: + assert false, + "The `jsonNode` must have one of {JNull, JObject, JString} kinds." + +when isMainModule: + import unittest + + test "validate sha1": + check validateSha1Hash("") + check not validateSha1Hash("9") + check not validateSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358g7") + check not validateSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b") + check not validateSha1Hash("99345CE680CD3E48ACDB9AB4212E4BD9BF9358B7") + check validateSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b7") + + test "init sha1": + check initSha1Hash("") == notSetSha1Hash + expect InvalidSha1HashError: discard initSha1Hash("9") + expect InvalidSha1HashError: + discard initSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358g7") + expect InvalidSha1HashError: + discard initSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b") + check $initSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b7") == + "99345ce680cd3e48acdb9ab4212e4bd9bf9358b7" + check $initSha1Hash("99345CE680CD3E48ACDB9AB4212E4BD9BF9358B7") == + "99345ce680cd3e48acdb9ab4212e4bd9bf9358b7" diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 678a9bf2..4ac70643 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -6,7 +6,7 @@ import osproc, pegs, strutils, os, uri, sets, json, parseutils, strformat, sequtils from net import SslCVerifyMode, newContext, SslContext -import version, cli, common, packageinfotypes, options +import version, cli, common, packageinfotypes, options, sha1hashes from compiler/nimblecmd import getPathVersionChecksum proc extractBin(cmd: string): string = @@ -115,7 +115,7 @@ proc createDirD*(dir: string) = createDir(dir) proc getDownloadDirName*(uri: string, verRange: VersionRange, - vcsRevision: string): string = + vcsRevision: Sha1Hash): string = ## Creates a directory name based on the specified ``uri`` (url) let puri = parseUri(uri) for i in puri.hostname: @@ -135,9 +135,9 @@ proc getDownloadDirName*(uri: string, verRange: VersionRange, result.add "_" result.add verSimple - if vcsRevision.len > 0: + if vcsRevision != notSetSha1Hash: result.add "_" - result.add vcsRevision + result.add $vcsRevision proc incl*(s: var HashSet[string], v: seq[string] | HashSet[string]) = for i in v: @@ -178,7 +178,7 @@ proc getNimbleUserTempDir*(): string = return tmpdir -proc getVcsRevisionFromDir*(dir: string): string = +proc getVcsRevisionFromDir*(dir: string): Sha1Hash = ## Returns current revision number of HEAD if dir is inside VCS, or an empty ## string in case of failure. @@ -186,10 +186,11 @@ proc getVcsRevisionFromDir*(dir: string): string = try: let (output, exitCode) = doCmdEx(command) if exitCode == QuitSuccess: - return output.strip() + return initSha1Hash(output.strip()) except: discard + result = notSetSha1Hash tryToGetRevision("git -C " & quoteShell(dir) & " rev-parse HEAD") tryToGetRevision("hg --cwd " & quoteShell(dir) & " id -i") @@ -203,9 +204,18 @@ proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = ## ## Also works for file paths like: ## ``/home/user/.nimble/pkgs/package-0.1-febadeaea2345e777f0f6f8433f7f0a52edd5d1b/package.nimble`` + if pkgPath.splitFile.ext in [".nimble", ".babel"]: return getNameVersionChecksum(pkgPath.splitPath.head) - getPathVersionChecksum(pkgpath.splitPath.tail) + + let (name, version, checksum) = getPathVersionChecksum(pkgPath.splitPath.tail) + let sha1Checksum = + try: + initSha1Hash(checksum) + except InvalidSha1HashError: + notSetSha1Hash + + return (name, version, sha1Checksum) proc removePackageDir*(files: seq[string], dir: string, reportSuccess = false) = for file in files: @@ -227,3 +237,70 @@ proc newSSLContext*(disabled: bool): SslContext = display("Warning:", "disabling SSL certificate checking", Warning) sslVerifyMode = CVerifyNone return newContext(verifyMode = sslVerifyMode) + +when isMainModule: + import unittest + + suite "getNameVersionCheksum": + test "directory names without sha1 hashes": + check getNameVersionChecksum( + "/home/user/.nimble/libs/packagea-0.1") == + ("packagea", "0.1", notSetSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1") == + ("package-a", "0.1", notSetSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1/package.nimble") == + ("package-a", "0.1", notSetSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#head") == + ("package", "#head", notSetSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#branch-with-dashes") == + ("package", "#branch-with-dashes", notSetSha1Hash) + + # readPackageInfo (and possibly more) depends on this not raising. + check getNameVersionChecksum( + "/home/user/.nimble/libs/package") == + ("package", "", notSetSha1Hash) + + test "directory names with sha1 hashes": + check getNameVersionChecksum( + "/home/user/.nimble/libs/packagea-0.1-" & + "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") == + ("packagea", "0.1", + "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82".initSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1-" & + "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") == + ("package-a", "0.1", + "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6".initSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-a-0.1-" & + "43e3b1138312656310e93ffcfdd866b2dcce3b35/package.nimble") == + ("package-a", "0.1", + "43e3b1138312656310e93ffcfdd866b2dcce3b35".initSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#head-" & + "efba335dccf2631d7ac2740109142b92beb3b465") == + ("package", "#head", + "efba335dccf2631d7ac2740109142b92beb3b465".initSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-#branch-with-dashes-" & + "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") == + ("package", "#branch-with-dashes", + "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c".initSha1Hash) + + check getNameVersionChecksum( + "/home/user/.nimble/libs/package-" & + "b12e18db49fc60df117e5d8a289c4c2050a272dd") == + ("package", "", + "b12e18db49fc60df117e5d8a289c4c2050a272dd".initSha1Hash) diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index 69d95e24..6f748b6e 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -9,16 +9,15 @@ proc buildDependencyGraph*(packages: HashSet[PackageInfo], options: Options): ## Creates records which will be saved to the lock file. for pkgInfo in packages: - var package: LockFileDependency let pkg = getPackage(pkgInfo.name, options) - package.version = pkgInfo.version - package.vcsRevision = pkgInfo.vcsRevision - package.url = pkg.url - package.downloadMethod = pkg.downloadMethod - package.dependencies = pkgInfo.requires.map( - pkg => pkg.name).filter(name => name != "nim") - package.checksum.sha1 = pkgInfo.checksum - result[pkgInfo.name] = package + result[pkgInfo.name] = LockFileDependency( + version: pkgInfo.version, + vcsRevision: pkgInfo.vcsRevision, + url: pkg.url, + downloadMethod: pkg.downloadMethod, + dependencies: pkgInfo.requires.map( + pkg => pkg.name).filter(name => name != "nim"), + checksums: Checksums(sha1: pkgInfo.checksum)) proc topologicalSort*(graph: LockFileDependencies): tuple[order: seq[string], cycles: seq[seq[string]]] = @@ -99,21 +98,27 @@ proc topologicalSort*(graph: LockFileDependencies): when isMainModule: import unittest + from sha1hashes import notSetSha1Hash + + proc initLockFileDependency(deps: seq[string] = @[]): LockFileDependency = + result = LockFileDependency( + vcsRevision: notSetSha1Hash, + dependencies: deps, + checksums: Checksums(sha1: notSetSha1Hash)) suite "topological sort": test "graph without cycles": let graph = { - "json_serialization": LockFileDependency( - dependencies: @["serialization", "stew"]), - "faststreams": LockFileDependency(dependencies: @["stew"]), - "testutils": LockFileDependency(), - "stew": LockFileDependency(), - "serialization": LockFileDependency( - dependencies: @["faststreams", "stew"]), - "chronicles": LockFileDependency( - dependencies: @["json_serialization", "testutils"]) + "json_serialization": initLockFileDependency( + @["serialization", "stew"]), + "faststreams": initLockFileDependency(@["stew"]), + "testutils": initLockFileDependency(), + "stew": initLockFileDependency(), + "serialization": initLockFileDependency(@["faststreams", "stew"]), + "chronicles": initLockFileDependency( + @["json_serialization", "testutils"]) }.toOrderedTable expectedTopologicallySortedOrder = @[ @@ -129,11 +134,11 @@ when isMainModule: test "graph with cycles": let graph = { - "A": LockFileDependency(dependencies: @["B", "E"]), - "B": LockFileDependency(dependencies: @["A", "C"]), - "C": LockFileDependency(dependencies: @["D"]), - "D": LockFileDependency(dependencies: @["B"]), - "E": LockFileDependency(dependencies: @["D", "E"]) + "A": initLockFileDependency(@["B", "E"]), + "B": initLockFileDependency(@["A", "C"]), + "C": initLockFileDependency(@["D"]), + "D": initLockFileDependency(@["B"]), + "E": initLockFileDependency(@["D", "E"]) }.toOrderedTable expectedTopologicallySortedOrder = @["D", "C", "B", "E", "A"] diff --git a/tests/tester.nim b/tests/tester.nim index 290801a4..137a5977 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1525,26 +1525,26 @@ suite "develop feature": check parseFile(developFileName) == parseJson(expectedDevelopFileContent) -suite "path command": - test "can get correct path for srcDir (#531)": - cd "develop/srcdirtest": - let (_, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - let (output, _) = execNimble("path", "srcdirtest") - let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") - check output.strip() == packageDir - - # test "nimble path points to develop": - # cd "develop/srcdirtest": - # var (output, exitCode) = execNimble("develop") - # checkpoint output - # check exitCode == QuitSuccess - - # (output, exitCode) = execNimble("path", "srcdirtest") - - # checkpoint output - # check exitCode == QuitSuccess - # check output.strip() == getCurrentDir() / "src" +# suite "path command": +# test "can get correct path for srcDir (#531)": +# cd "develop/srcdirtest": +# let (_, exitCode) = execNimbleYes("install") +# check exitCode == QuitSuccess +# let (output, _) = execNimble("path", "srcdirtest") +# let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") +# check output.strip() == packageDir + +# test "nimble path points to develop": +# cd "develop/srcdirtest": +# var (output, exitCode) = execNimble("develop") +# checkpoint output +# check exitCode == QuitSuccess + +# (output, exitCode) = execNimble("path", "srcdirtest") + +# checkpoint output +# check exitCode == QuitSuccess +# check output.strip() == getCurrentDir() / "src" suite "test command": beforeSuite() @@ -1659,6 +1659,8 @@ suite "Module tests": moduleTest "packageparser" moduleTest "paths" moduleTest "reversedeps" + moduleTest "sha1hashes" + moduleTest "tools" moduleTest "topologicalsort" moduleTest "version" @@ -1925,6 +1927,8 @@ suite "misc tests": "[Deprecated]", "[XDeclaredButNotUsed]", "[Spacing]", + "[ProveInit]", + "[UnsafeDefault]", "[ConvFromXtoItselfNotNeeded]", ] @@ -2132,11 +2136,13 @@ suite "issues": test "issue #428": cd "issue428": # Note: Can't use execNimble because it patches nimbleDir + const localNimbleDir = "./nimbleDir" + cleanDir localNimbleDir let (_, exitCode) = execCmdEx( - nimblePath & " -y --nimbleDir=./nimbleDir install") + &"{nimblePath} -y --nimbleDir={localNimbleDir} install") check exitCode == QuitSuccess let dummyPkgDir = getPackageDir( - "nimbleDir" / nimblePackagesDirName, "dummy-0.1.0") + localNimbleDir / nimblePackagesDirName, "dummy-0.1.0") check dummyPkgDir.dirExists check not (dummyPkgDir / "nimbleDir").dirExists From 393b44592daa654061d5dfcbbd065f94e599225f Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 30 Nov 2020 20:37:57 +0200 Subject: [PATCH 22/73] Split tests in separate files by suite The tests from the `tester.nim` module are split into separate files for each of the test suites in order to achieve better maintainability of the code and faster compilation when only one of the suites is modified. All new modules are imported from the `tester.nim` module and the common code between them is moved in a new `testscommon.nim` module. Related to nim-lang/Nimble#127 --- tests/.gitignore | 21 +- tests/tcheckcommand.nim | 42 + tests/tdevelopfeature.nim | 887 +++++++++++++ tests/tester.nim | 2383 +--------------------------------- tests/testscommon.nim | 190 +++ tests/tissues.nim | 390 ++++++ tests/tlocaldeps.nim | 34 + tests/tmisctests.nim | 106 ++ tests/tmoduletests.nim | 28 + tests/tmultipkgs.nim | 25 + tests/tnimbledump.nim | 85 ++ tests/tnimblerefresh.nim | 78 ++ tests/tnimbletasks.nim | 34 + tests/tnimscript.nim | 115 ++ tests/tpathcommand.nim | 29 + tests/treversedeps.nim | 84 ++ tests/truncommand.nim | 164 +++ tests/ttestcommand.nim | 47 + tests/ttwobinaryversions.nim | 35 + tests/tuninstall.nim | 92 ++ 20 files changed, 2501 insertions(+), 2368 deletions(-) create mode 100644 tests/tcheckcommand.nim create mode 100644 tests/tdevelopfeature.nim create mode 100644 tests/testscommon.nim create mode 100644 tests/tissues.nim create mode 100644 tests/tlocaldeps.nim create mode 100644 tests/tmisctests.nim create mode 100644 tests/tmoduletests.nim create mode 100644 tests/tmultipkgs.nim create mode 100644 tests/tnimbledump.nim create mode 100644 tests/tnimblerefresh.nim create mode 100644 tests/tnimbletasks.nim create mode 100644 tests/tnimscript.nim create mode 100644 tests/tpathcommand.nim create mode 100644 tests/treversedeps.nim create mode 100644 tests/truncommand.nim create mode 100644 tests/ttestcommand.nim create mode 100644 tests/ttwobinaryversions.nim create mode 100644 tests/tuninstall.nim diff --git a/tests/.gitignore b/tests/.gitignore index acf3fe7c..a142ead6 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,6 +1,21 @@ -# ideally this shouldn't even be needed and all generated files would go under ../buildTests/ - -# tester +tcheckcommand +tdevelopfeature +tester +testscommon +tissues +tlocaldeps +tmisctests +tmoduletests +tmultipkgs +tnimbledump +tnimblerefresh +tnimscript +tpathcommand +treversedeps +truncommand +ttestcommand +ttwobinaryversions +tuninstall /nimble-test /buildDir /binaryPackage/v1/binaryPackage diff --git a/tests/tcheckcommand.nim b/tests/tcheckcommand.nim new file mode 100644 index 00000000..4c3de36f --- /dev/null +++ b/tests/tcheckcommand.nim @@ -0,0 +1,42 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os +import testscommon +from nimblepkg/common import cd + +suite "check command": + test "can succeed package": + cd "binaryPackage/v1": + let (outp, exitCode) = execNimble("check") + check exitCode == QuitSuccess + check outp.processOutput.inLines("success") + check outp.processOutput.inLines("\"binaryPackage\" is valid") + + cd "packageStructure/a": + let (outp, exitCode) = execNimble("check") + check exitCode == QuitSuccess + check outp.processOutput.inLines("success") + check outp.processOutput.inLines("\"a\" is valid") + + cd "packageStructure/b": + let (outp, exitCode) = execNimble("check") + check exitCode == QuitSuccess + check outp.processOutput.inLines("success") + check outp.processOutput.inLines("\"b\" is valid") + + cd "packageStructure/c": + let (outp, exitCode) = execNimble("check") + check exitCode == QuitSuccess + check outp.processOutput.inLines("success") + check outp.processOutput.inLines("\"c\" is valid") + + test "can fail package": + cd "packageStructure/x": + let (outp, exitCode) = execNimble("check") + check exitCode == QuitFailure + check outp.processOutput.inLines("failure") + check outp.processOutput.inLines("validation failed") + check outp.processOutput.inLines("package 'x' has an incorrect structure") diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim new file mode 100644 index 00000000..d78d33aa --- /dev/null +++ b/tests/tdevelopfeature.nim @@ -0,0 +1,887 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils, strformat, json, sets +import testscommon, nimblepkg/displaymessages, nimblepkg/paths + +from nimblepkg/common import cd +from nimblepkg/developfile import developFileName, pkgFoundMoreThanOnceMsg +from nimblepkg/version import parseVersionRange +from nimblepkg/nimbledatafile import nimbleDataFileName, NimbleDataJsonKeys + +suite "develop feature": + const + pkgListFileName = "packages.json" + dependentPkgName = "dependent" + dependentPkgVersion = "1.0" + dependentPkgNameAndVersion = &"{dependentPkgName}@{dependentPkgVersion}" + dependentPkgPath = "develop/dependent".normalizedPath + includeFileName = "included.develop" + pkgAName = "packagea" + pkgBName = "packageb" + pkgSrcDirTestName = "srcdirtest" + pkgHybridName = "hybrid" + depPath = "../dependency".normalizedPath + depName = "dependency" + depVersion = "0.1.0" + depNameAndVersion = &"{depName}@{depVersion}" + dep2Path = "../dependency2".normalizedPath + emptyDevelopFileContent = developFile(@[], @[]) + + let anyVersion = parseVersionRange("") + + test "can develop from dir with srcDir": + cd &"develop/{pkgSrcDirTestName}": + let (output, exitCode) = execNimble("develop") + check exitCode == QuitSuccess + let lines = output.processOutput + check not lines.inLines("will not be compiled") + check lines.inLines(pkgSetupInDevModeMsg( + pkgSrcDirTestName, getCurrentDir())) + + test "can git clone for develop": + cdCleanDir installDir: + let (output, exitCode) = execNimble("develop", pkgAUrl) + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg(pkgAName, installDir / pkgAName)) + + test "can develop from package name": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + let (output, exitCode) = execNimble("develop", pkgBName) + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(pkgInstalledMsg(pkgAName)) + check lines.inLinesOrdered( + pkgSetupInDevModeMsg(pkgBName, installDir / pkgBName)) + + test "can develop list of packages": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + let (output, exitCode) = execNimble( + "develop", pkgAName, pkgBName) + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg( + pkgAName, installDir / pkgAName)) + check lines.inLinesOrdered(pkgInstalledMsg(pkgAName)) + check lines.inLinesOrdered(pkgSetupInDevModeMsg( + pkgBName, installDir / pkgBName)) + + test "cannot remove package with develop reverse dependency": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + check execNimble("develop", pkgBName).exitCode == QuitSuccess + let (output, exitCode) = execNimble("remove", pkgAName) + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + cannotUninstallPkgMsg(pkgAName, "0.2.0", @[installDir / pkgBName])) + + test "can reject binary packages": + cd "develop/binary": + let (output, exitCode) = execNimble("develop") + check output.processOutput.inLines("cannot develop packages") + check exitCode == QuitFailure + + test "can develop hybrid": + cd &"develop/{pkgHybridName}": + let (output, exitCode) = execNimble("develop") + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered("will not be compiled") + check lines.inLinesOrdered( + pkgSetupInDevModeMsg(pkgHybridName, getCurrentDir())) + + test "can specify different absolute clone dir": + let otherDir = installDir / "./some/other/dir" + cleanDir otherDir + let (output, exitCode) = execNimble( + "develop", &"-p:{otherDir}", pkgAUrl) + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg(pkgAName, otherDir / pkgAName)) + + test "can specify different relative clone dir": + const otherDir = "./some/other/dir" + cdCleanDir installDir: + let (output, exitCode) = execNimble( + "develop", &"-p:{otherDir}", pkgAUrl) + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg(pkgAName, installDir / otherDir / pkgAName)) + + test "do not allow multiple path options": + let + developDir = installDir / "./some/dir" + anotherDevelopDir = installDir / "./some/other/dir" + defer: + # cleanup in the case of test failure + removeDir developDir + removeDir anotherDevelopDir + let (output, exitCode) = execNimble( + "develop", &"-p:{developDir}", &"-p:{anotherDevelopDir}", pkgAUrl) + check exitCode == QuitFailure + check output.processOutput.inLines("Multiple path options are given") + check not developDir.dirExists + check not anotherDevelopDir.dirExists + + test "do not allow path option without packages to download": + let developDir = installDir / "./some/dir" + let (output, exitCode) = execNimble("develop", &"-p:{developDir}") + check exitCode == QuitFailure + check output.processOutput.inLines(pathGivenButNoPkgsToDownloadMsg) + check not developDir.dirExists + + test "do not allow add/remove options out of package directory": + cleanFile developFileName + let (output, exitCode) = execNimble("develop", "-a:./develop/dependency/") + check exitCode == QuitFailure + check output.processOutput.inLines(developOptionsOutOfPkgDirectoryMsg) + + test "cannot load invalid develop file": + cd dependentPkgPath: + cleanFile developFileName + writeFile(developFileName, "this is not a develop file") + let (output, exitCode) = execNimble("check") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + notAValidDevFileJsonMsg(getCurrentDir() / developFileName)) + check lines.inLinesOrdered(validationFailedMsg) + + test "add downloaded package to the develop file": + cleanDir installDir + cd "develop/dependency": + usePackageListFile &"../{pkgListFileName}": + cleanFile developFileName + let + (output, exitCode) = execNimble( + "develop", &"-p:{installDir}", pkgAName) + pkgAAbsPath = installDir / pkgAName + developFileContent = developFile(@[], @[pkgAAbsPath]) + check exitCode == QuitSuccess + check parseFile(developFileName) == parseJson(developFileContent) + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) + + test "cannot add not a dependency downloaded package to the develop file": + cleanDir installDir + cd "develop/dependency": + usePackageListFile &"../{pkgListFileName}": + cleanFile developFileName + let + (output, exitCode) = execNimble( + "develop", &"-p:{installDir}", pkgAName, pkgBName) + pkgAAbsPath = installDir / pkgAName + pkgBAbsPath = installDir / pkgBName + developFileContent = developFile(@[], @[pkgAAbsPath]) + check exitCode == QuitFailure + check parseFile(developFileName) == parseJson(developFileContent) + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgBName, pkgBAbsPath)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) + check lines.inLinesOrdered( + notADependencyErrorMsg(&"{pkgBName}@0.2.0", depNameAndVersion)) + + test "add package to develop file": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + cleanFiles developFileName, dependentPkgName.addFileExt(ExeExt) + var (output, exitCode) = execNimble("develop", &"-a:{depPath}") + check exitCode == QuitSuccess + check developFileName.fileExists + check output.processOutput.inLines( + pkgAddedInDevModeMsg(depNameAndVersion, depPath)) + const expectedDevelopFile = developFile(@[], @[depPath]) + check parseFile(developFileName) == parseJson(expectedDevelopFile) + (output, exitCode) = execNimble("run") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) + + test "warning on attempt to add the same package twice": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-a:{depPath}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgAlreadyInDevModeMsg(depNameAndVersion, depPath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "cannot add invalid package to develop file": + cd dependentPkgPath: + cleanFile developFileName + const invalidPkgDir = "../invalidPkg".normalizedPath + createTempDir invalidPkgDir + let (output, exitCode) = execNimble("develop", &"-a:{invalidPkgDir}") + check exitCode == QuitFailure + check output.processOutput.inLines(invalidPkgMsg(invalidPkgDir)) + check not developFileName.fileExists + + test "cannot add not a dependency to develop file": + cd dependentPkgPath: + cleanFile developFileName + let (output, exitCode) = execNimble("develop", "-a:../srcdirtest/") + check exitCode == QuitFailure + check output.processOutput.inLines( + notADependencyErrorMsg(&"{pkgSrcDirTestName}@1.0", "dependent@1.0")) + check not developFileName.fileExists + + test "cannot add two packages with the same name to develop file": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-a:{dep2Path}") + check exitCode == QuitFailure + check output.processOutput.inLines( + pkgAlreadyPresentAtDifferentPathMsg(depName, depPath.absolutePath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "found two packages with the same name in the develop file": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile( + @[], @[depPath, dep2Path]) + writeFile(developFileName, developFileContent) + + let + (output, exitCode) = execNimble("check") + developFilePath = getCurrentDir() / developFileName + + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(failedToLoadFileMsg(developFilePath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, + [(depPath.absolutePath.Path, developFilePath.Path), + (dep2Path.absolutePath.Path, developFilePath.Path)].toHashSet)) + check lines.inLinesOrdered(validationFailedMsg) + + test "remove package from develop file by path": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-r:{depPath}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "warning on attempt to remove not existing package path": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-r:{dep2Path}") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgPathNotInDevFileMsg(dep2Path)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "remove package from develop file by name": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("develop", &"-n:{depName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath.absolutePath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "warning on attempt to remove not existing package name": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + const notExistingPkgName = "dependency2" + let (output, exitCode) = execNimble("develop", &"-n:{notExistingPkgName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgNameNotInDevFileMsg(notExistingPkgName)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "include develop file": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + cleanFiles developFileName, includeFileName, + dependentPkgName.addFileExt(ExeExt) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitSuccess + check developFileName.fileExists + check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) + const expectedDevelopFile = developFile(@[includeFileName], @[]) + check parseFile(developFileName) == parseJson(expectedDevelopFile) + (output, exitCode) = execNimble("run") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) + + test "warning on attempt to include already included develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + alreadyInclInDevFileMsg(includeFileName)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "cannot include invalid develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + writeFile(includeFileName, """{"some": "json"}""") + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitFailure + check not developFileName.fileExists + check output.processOutput.inLines(failedToLoadFileMsg(includeFileName)) + + test "cannot load a develop file with an invalid include file in it": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + let (output, exitCode) = execNimble("check") + check exitCode == QuitFailure + let developFilePath = getCurrentDir() / developFileName + var lines = output.processOutput() + check lines.inLinesOrdered(failedToLoadFileMsg(developFilePath)) + check lines.inLinesOrdered(invalidDevFileMsg(developFilePath)) + check lines.inLinesOrdered(&"cannot read from file: {includeFileName}") + check lines.inLinesOrdered(validationFailedMsg) + + test "can include file pointing to the same package": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + cleanFiles developFileName, includeFileName, + dependentPkgName.addFileExt(ExeExt) + const fileContent = developFile(@[], @[depPath]) + writeFile(developFileName, fileContent) + writeFile(includeFileName, fileContent) + var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) + const expectedFileContent = developFile( + @[includeFileName], @[depPath]) + check parseFile(developFileName) == parseJson(expectedFileContent) + (output, exitCode) = execNimble("run") + check exitCode == QuitSuccess + check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) + + test "cannot include conflicting develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[dep2Path]) + writeFile(includeFileName, includeFileContent) + + let + (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + developFilePath = getCurrentDir() / developFileName + + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + failedToInclInDevFileMsg(includeFileName, developFilePath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, + [(depPath.absolutePath.Path, developFilePath.Path), + (dep2Path.Path, includeFileName.Path)].toHashSet)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "validate included dependencies version": + cd &"{dependentPkgPath}2": + cleanFiles developFileName, includeFileName + const includeFileContent = developFile(@[], @[dep2Path]) + writeFile(includeFileName, includeFileContent) + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") + check exitCode == QuitFailure + var lines = output.processOutput + let developFilePath = getCurrentDir() / developFileName + check lines.inLinesOrdered( + failedToInclInDevFileMsg(includeFileName, developFilePath)) + check lines.inLinesOrdered(invalidPkgMsg(dep2Path)) + check lines.inLinesOrdered(dependencyNotInRangeErrorMsg( + depNameAndVersion, dependentPkgNameAndVersion, + parseVersionRange(">= 0.2.0"))) + check not developFileName.fileExists + + test "exclude develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + let (output, exitCode) = execNimble("develop", &"-e:{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines(exclFromDevFileMsg(includeFileName)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "warning on attempt to exclude not included develop file": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile(@[includeFileName], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + let (output, exitCode) = execNimble("develop", &"-e:../{includeFileName}") + check exitCode == QuitSuccess + check output.processOutput.inLines( + notInclInDevFileMsg((&"../{includeFileName}").normalizedPath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "relative paths in the develop file and absolute from the command line": + cd dependentPkgPath: + cleanFiles developFileName, includeFileName + const developFileContent = developFile( + @[includeFileName], @[depPath]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @[depPath]) + writeFile(includeFileName, includeFileContent) + + let + includeFileAbsolutePath = includeFileName.absolutePath + dependencyPkgAbsolutePath = "../dependency".absolutePath + (output, exitCode) = execNimble("develop", + &"-e:{includeFileAbsolutePath}", &"-r:{dependencyPkgAbsolutePath}") + + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(exclFromDevFileMsg(includeFileAbsolutePath)) + check lines.inLinesOrdered( + pkgRemovedFromDevModeMsg(depNameAndVersion, dependencyPkgAbsolutePath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "absolute paths in the develop file and relative from the command line": + cd dependentPkgPath: + let + currentDir = getCurrentDir() + includeFileAbsPath = currentDir / includeFileName + dependencyAbsPath = currentDir / depPath + developFileContent = developFile( + @[includeFileAbsPath], @[dependencyAbsPath]) + includeFileContent = developFile(@[], @[depPath]) + + cleanFiles developFileName, includeFileName + writeFile(developFileName, developFileContent) + writeFile(includeFileName, includeFileContent) + + let (output, exitCode) = execNimble("develop", + &"-e:{includeFileName}", &"-r:{depPath}") + + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(exclFromDevFileMsg(includeFileName)) + check lines.inLinesOrdered( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + + test "uninstall package with develop reverse dependencies": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + const developFileContent = developFile(@[], @[depPath]) + cleanFiles developFileName, "dependent" + writeFile(developFileName, developFileContent) + + block checkSuccessfulInstallAndReverseDependencyAddedToNimbleData: + let + (_, exitCode) = execNimble("install") + nimbleData = parseFile(installDir / nimbleDataFileName) + packageDir = getPackageDir(pkgsDir, "PackageA-0.5.0") + checksum = packageDir[packageDir.rfind('-') + 1 .. ^1] + devRevDepPath = nimbleData{$ndjkRevDep}{pkgAName}{"0.5.0"}{ + checksum}{0}{$ndjkRevDepPath} + depAbsPath = getCurrentDir() / depPath + + check exitCode == QuitSuccess + check not devRevDepPath.isNil + check devRevDepPath.str == depAbsPath + + block checkSuccessfulUninstallAndRemovalFromNimbleData: + let + (_, exitCode) = execNimble("uninstall", "-i", pkgAName, "-y") + nimbleData = parseFile(installDir / nimbleDataFileName) + + check exitCode == QuitSuccess + check not nimbleData[$ndjkRevDep].hasKey(pkgAName) + + test "follow develop dependency's develop file": + cd "develop": + const pkg1DevFilePath = "pkg1" / developFileName + const pkg2DevFilePath = "pkg2" / developFileName + cleanFiles pkg1DevFilePath, pkg2DevFilePath + const pkg1DevFileContent = developFile(@[], @["../pkg2"]) + writeFile(pkg1DevFilePath, pkg1DevFileContent) + const pkg2DevFileContent = developFile(@[], @["../pkg3"]) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (_, exitCode) = execNimble("run", "-n") + check exitCode == QuitSuccess + + test "version clash from followed develop file": + cd "develop": + const pkg1DevFilePath = "pkg1" / developFileName + const pkg2DevFilePath = "pkg2" / developFileName + cleanFiles pkg1DevFilePath, pkg2DevFilePath + const pkg1DevFileContent = developFile(@[], @["../pkg2", "../pkg3"]) + writeFile(pkg1DevFilePath, pkg1DevFileContent) + const pkg2DevFileContent = developFile(@[], @["../pkg3.2"]) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + + let + currentDir = getCurrentDir() + pkg1DevFileAbsPath = currentDir / pkg1DevFilePath + pkg2DevFileAbsPath = currentDir / pkg2DevFilePath + pkg3AbsPath = currentDir / "pkg3" + pkg32AbsPath = currentDir / "pkg3.2" + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(failedToLoadFileMsg(pkg1DevFileAbsPath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", + [(pkg3AbsPath.Path, pkg1DevFileAbsPath.Path), + (pkg32AbsPath.Path, pkg2DevFileAbsPath.Path)].toHashSet)) + + test "relative include paths are followed from the file's directory": + cd dependentPkgPath: + const includeFilePath = &"../{includeFileName}" + cleanFiles includeFilePath, developFileName, dependentPkgName.addFileExt(ExeExt) + const developFileContent = developFile(@[includeFilePath], @[]) + writeFile(developFileName, developFileContent) + const includeFileContent = developFile(@[], @["./dependency2/"]) + writeFile(includeFilePath, includeFileContent) + let (_, errorCode) = execNimble("run", "-n") + check errorCode == QuitSuccess + + test "filter not used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>| pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | nimble.develop | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | + # +--------------------------+ includes | + # v + # +---------------+ + # | develop.json | + # +---------------+ + # | + # dependency | + # v + # +---------------------+ + # | pkg3 | + # +---------------------+ + # | version = "0.2.0" | + # +---------------------+ + + # Here the build must fail because "pkg3" coming from develop file included + # in "pkg2"'s develop file is not a dependency of "pkg2" itself and it must + # be filtered. In this way "pkg1"'s dependency to "pkg3" is not satisfied. + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2.2" / developFileName + freeDevFileName = "develop.json" + pkg1DevFileContent = developFile(@[], @["../pkg2.2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFileName}"], @[]) + freeDevFileContent = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFileName + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFileName, freeDevFileContent) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) + check lines.inLinesOrdered(pkgNotFoundMsg(("pkg3", anyVersion))) + + test "do not filter used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>+ pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | requires "pkg3" | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | nimble.develop | + # +--------------------------+ +--------------------------+ + # | + # includes | + # v + # +---------------+ + # | develop.json | + # +---------------+ + # | + # dependency | + # v + # +---------------------+ + # | pkg3 | + # +---------------------+ + # | version = "0.2.0" | + # +---------------------+ + + # Here the build must pass because "pkg3" coming form develop file included + # in "pkg2"'s develop file is a dependency of "pkg2" and it will be used, + # in this way satisfying also "pkg1"'s requirements. + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2" / developFileName + freeDevFileName = "develop.json" + pkg1DevFileContent = developFile(@[], @["../pkg2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFileName}"], @[]) + freeDevFileContent = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFileName + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFileName, freeDevFileContent) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + + test "no version clash with filtered not used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>| pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | nimble.develop | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | + # +--------------------------+ includes | + # | v + # includes | +---------------+ + # v | develop2.json | + # +---------------+ +-------+-------+ + # | develop1.json | | + # +---------------+ dependency | + # | v + # dependency | +---------------------+ + # v | pkg3 | + # +-------------------+ +---------------------+ + # | pkg3 | | version = "0.2.0" | + # +-------------------+ +---------------------+ + # | version = "0.1.0" | + # +-------------------+ + + # Here the build must pass because only the version of "pkg3" included via + # "develop1.json" must be taken into account, since "pkg2" does not depend + # on "pkg3" and the version coming from "develop2.json" must be filtered. + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2.2" / developFileName + freeDevFile1Name = "develop1.json" + freeDevFile2Name = "develop2.json" + pkg1DevFileContent = developFile( + @[&"../{freeDevFile1Name}"], @["../pkg2.2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) + freeDevFile1Content = developFile(@[], @["./pkg3"]) + freeDevFile2Content = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, + freeDevFile1Name, freeDevFile2Name + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFile1Name, freeDevFile1Content) + writeFile(freeDevFile2Name, freeDevFile2Content) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + + test "version clash with used included develop dependencies": + # +--------------------------+ +--------------------------+ + # | pkg1 | +------------>| pkg2 | + # +--------------------------+ | dependency +--------------------------+ + # | requires "pkg2", "pkg3" | | | requires "pkg3" | + # +--------------------------+ | +--------------------------+ + # | nimble.develop |--+ | nimble.develop | + # +--------------------------+ +--------------------------+ + # | | + # includes | includes | + # v v + # +-------+-------+ +---------------+ + # | develop1.json | | develop2.json | + # +-------+-------+ +---------------+ + # | | + # dependency | dependency | + # v v + # +-------------------+ +---------------------+ + # | pkg3 | | pkg3 | + # +-------------------+ +---------------------+ + # | version = "0.1.0" | | version = "0.2.0" | + # +-------------------+ +---------------------+ + + # Here the build must fail because since "pkg3" is dependency of both "pkg1" + # and "pkg2", both versions coming from "develop1.json" and "develop2.json" + # must be taken into account, but they are different." + + cd "develop": + const + pkg1DevFilePath = "pkg1" / developFileName + pkg2DevFilePath = "pkg2" / developFileName + freeDevFile1Name = "develop1.json" + freeDevFile2Name = "develop2.json" + pkg1DevFileContent = developFile( + @[&"../{freeDevFile1Name}"], @["../pkg2"]) + pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) + freeDevFile1Content = developFile(@[], @["./pkg3"]) + freeDevFile2Content = developFile(@[], @["./pkg3.2"]) + + cleanFiles pkg1DevFilePath, pkg2DevFilePath, + freeDevFile1Name, freeDevFile2Name + writeFile(pkg1DevFilePath, pkg1DevFileContent) + writeFile(pkg2DevFilePath, pkg2DevFileContent) + writeFile(freeDevFile1Name, freeDevFile1Content) + writeFile(freeDevFile2Name, freeDevFile2Content) + + cd "pkg1": + cleanFile "pkg1".addFileExt(ExeExt) + let (output, exitCode) = execNimble("run", "-n") + check exitCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(failedToLoadFileMsg( + getCurrentDir() / developFileName)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", + [("../pkg3".Path, (&"../{freeDevFile1Name}").Path), + ("../pkg3.2".Path, (&"../{freeDevFile2Name}").Path)].toHashSet)) + + test "create an empty develop file with default name in the current dir": + cd dependentPkgPath: + cleanFile developFileName + let (output, errorCode) = execNimble("develop", "-c") + check errorCode == QuitSuccess + check parseFile(developFileName) == parseJson(emptyDevelopFileContent) + check output.processOutput.inLines( + emptyDevFileCreatedMsg(developFileName)) + + test "create an empty develop file in some dir": + cleanDir installDir + let filePath = installDir / "develop.json" + cleanFile filePath + createDir installDir + let (output, errorCode) = execNimble("develop", &"-c:{filePath}") + check errorCode == QuitSuccess + check parseFile(filePath) == parseJson(emptyDevelopFileContent) + check output.processOutput.inLines(emptyDevFileCreatedMsg(filePath)) + + test "try to create an empty develop file with already existing name": + cd dependentPkgPath: + cleanFile developFileName + const developFileContent = developFile(@[], @[depPath]) + writeFile(developFileName, developFileContent) + let + filePath = getCurrentDir() / developFileName + (output, errorCode) = execNimble("develop", &"-c:{filePath}") + check errorCode == QuitFailure + check output.processOutput.inLines(fileAlreadyExistsMsg(filePath)) + check parseFile(developFileName) == parseJson(developFileContent) + + test "try to create an empty develop file in not existing dir": + let filePath = installDir / "some/not/existing/dir/develop.json" + cleanFile filePath + let (output, errorCode) = execNimble("develop", &"-c:{filePath}") + check errorCode == QuitFailure + check output.processOutput.inLines(&"cannot open: {filePath}") + + test "partial success when some operations in single command failed": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + const + dep2DevelopFilePath = dep2Path / developFileName + includeFileContent = developFile(@[], @[dep2Path]) + invalidInclFilePath = "/some/not/existing/file/path".normalizedPath + + cleanFiles developFileName, includeFileName, dep2DevelopFilePath + writeFile(includeFileName, includeFileContent) + + let + developFilePath = getCurrentDir() / developFileName + (output, errorCode) = execNimble("develop", &"-p:{installDir}", + pkgAName, # fail because not a direct dependency + "-c", # success + &"-a:{depPath}", # success + &"-a:{dep2Path}", # fail because of names collision + &"-i:{includeFileName}", # fail because of names collision + &"-n:{depName}", # success + &"-c:{developFilePath}", # fail because the file already exists + &"-a:{dep2Path}", # success + &"-i:{includeFileName}", # success + &"-i:{invalidInclFilePath}", # fail + &"-c:{dep2DevelopFilePath}") # success + + check errorCode == QuitFailure + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg( + pkgAName, installDir / pkgAName)) + check lines.inLinesOrdered(emptyDevFileCreatedMsg(developFileName)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(depNameAndVersion, depPath)) + check lines.inLinesOrdered( + pkgAlreadyPresentAtDifferentPathMsg(depName, depPath)) + check lines.inLinesOrdered( + failedToInclInDevFileMsg(includeFileName, developFilePath)) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, + [(depPath.Path, developFilePath.Path), + (dep2Path.Path, includeFileName.Path)].toHashSet)) + check lines.inLinesOrdered( + pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check lines.inLinesOrdered(fileAlreadyExistsMsg(developFilePath)) + check lines.inLinesOrdered( + pkgAddedInDevModeMsg(depNameAndVersion, dep2Path)) + check lines.inLinesOrdered(inclInDevFileMsg(includeFileName)) + check lines.inLinesOrdered(failedToLoadFileMsg(invalidInclFilePath)) + check lines.inLinesOrdered(emptyDevFileCreatedMsg(dep2DevelopFilePath)) + check parseFile(dep2DevelopFilePath) == + parseJson(emptyDevelopFileContent) + check lines.inLinesOrdered(notADependencyErrorMsg( + &"{pkgAName}@0.6.0", dependentPkgNameAndVersion)) + const expectedDevelopFileContent = developFile( + @[includeFileName], @[dep2Path]) + check parseFile(developFileName) == + parseJson(expectedDevelopFileContent) diff --git a/tests/tester.nim b/tests/tester.nim index 137a5977..b80d5bf7 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1,2368 +1,21 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import osproc, unittest, strutils, os, sequtils, sugar, json, std/sha1, - strformat, macros, sets -import nimblepkg/common, nimblepkg/displaymessages, nimblepkg/paths - -from nimblepkg/developfile import - developFileName, developFileVersion, pkgFoundMoreThanOnceMsg -from nimblepkg/nimbledatafile import - loadNimbleData, nimbleDataFileName, NimbleDataJsonKeys -from nimblepkg/version import VersionRange, parseVersionRange - -# TODO: Each test should start off with a clean slate. Currently installed -# packages are shared between each test which causes a multitude of issues -# and is really fragile. - -const - stringNotFound = -1 - pkgAUrl = "https://github.com/nimble-test/packagea.git" - pkgBUrl = "https://github.com/nimble-test/packageb.git" - pkgBinUrl = "https://github.com/nimble-test/packagebin.git" - pkgBin2Url = "https://github.com/nimble-test/packagebin2.git" - pkgMultiUrl = "https://github.com/nimble-test/multi" - pkgMultiAlphaUrl = &"{pkgMultiUrl}?subdir=alpha" - pkgMultiBetaUrl = &"{pkgMultiUrl}?subdir=beta" - -let - rootDir = getCurrentDir().parentDir() - nimblePath = rootDir / "src" / addFileExt("nimble", ExeExt) - installDir = rootDir / "tests" / "nimbleDir" - buildTests = rootDir / "buildTests" - pkgsDir = installDir / nimblePackagesDirName - -# Set env var to propagate nimble binary path -putEnv("NIMBLE_TEST_BINARY_PATH", nimblePath) - -# Always recompile. -doAssert execCmdEx("nim c -d:danger " & nimblePath).exitCode == QuitSuccess - -proc execNimble(args: varargs[string]): ProcessOutput = - var quotedArgs = @args - quotedArgs.insert("--nimbleDir:" & installDir) - quotedArgs.insert(nimblePath) - quotedArgs = quotedArgs.map((x: string) => x.quoteShell) - - let path {.used.} = getCurrentDir().parentDir() / "src" - - var cmd = - when not defined(windows): - "PATH=" & path & ":$PATH " & quotedArgs.join(" ") - else: - quotedArgs.join(" ") - when defined(macosx): - # TODO: Yeah, this is really specific to my machine but for my own sanity... - cmd = "DYLD_LIBRARY_PATH=/usr/local/opt/openssl@1.1/lib " & cmd - - result = execCmdEx(cmd) - checkpoint(cmd) - checkpoint(result.output) - -proc execNimbleYes(args: varargs[string]): ProcessOutput = - # issue #6314 - execNimble(@args & "-y") - -proc execBin(name: string): tuple[output: string, exitCode: int] = - var - cmd = installDir / "bin" / name - - when defined(windows): - cmd = "cmd /c " & cmd & ".cmd" - - result = execCmdEx(cmd) - -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 and - "Using env var NIM_LIB_PREFIX" notin x - ) - ) - -macro defineInLinesProc(procName, extraLine: untyped): untyped = - var LinesType = quote do: seq[string] - if extraLine[0].kind != nnkDiscardStmt: - LinesType = newTree(nnkVarTy, LinesType) - - let linesParam = ident("lines") - let linesLoopCounter = ident("i") - - result = quote do: - proc `procName`(`linesParam`: `LinesType`, msg: string): bool = - let msgLines = msg.splitLines - for msgLine in msgLines: - let msgLine = msgLine.normalize - var msgLineFound = false - for `linesLoopCounter`, line in `linesParam`: - if msgLine in line.normalize: - msgLineFound = true - `extraLine` - break - if not msgLineFound: - return false - return true - -defineInLinesProc(inLines): discard -defineInLinesProc(inLinesOrdered): lines = lines[i + 1 .. ^1] - -proc hasLineStartingWith(lines: seq[string], prefix: string): bool = - for line in lines: - if line.strip(trailing = false).startsWith(prefix): - return true - return false - -proc getPackageDir(pkgCacheDir, pkgDirPrefix: string, fullPath = true): string = - for kind, dir in walkDir(pkgCacheDir): - if kind != pcDir or not dir.startsWith(pkgCacheDir / pkgDirPrefix): - continue - let pkgChecksumStartIndex = dir.rfind('-') - if pkgChecksumStartIndex == -1: - continue - let pkgChecksum = dir[pkgChecksumStartIndex + 1 .. ^1] - if pkgChecksum.isValidSha1Hash(): - return if fullPath: dir else: dir.splitPath.tail - return "" - -proc packageDirExists(pkgCacheDir, pkgDirPrefix: string): bool = - getPackageDir(pkgCacheDir, pkgDirPrefix).len > 0 - -proc safeMoveFile(src, dest: string) = - try: - moveFile(src, dest) - except OSError: - copyFile(src, dest) - removeFile(src) - -template testRefresh(body: untyped) = - # Backup current config - let configFile {.inject.} = getConfigDir() / "nimble" / "nimble.ini" - let configBakFile = getConfigDir() / "nimble" / "nimble.ini.bak" - if fileExists(configFile): - safeMoveFile(configFile, configBakFile) - - # Ensure config dir exists - createDir(getConfigDir() / "nimble") - - body - - # Restore config - if fileExists(configBakFile): - safeMoveFile(configBakFile, configFile) - else: - # If the old config doesn't exist, we should still get rid of this new - # config to not screw up the other tests. - removeFile(configFile) - -proc beforeSuite() = - # Clear nimble dir. - removeDir(installDir) - createDir(installDir) - -template usePackageListFile(fileName: string, body: untyped) = - testRefresh(): - writeFile(configFile, """ - [PackageList] - name = "local" - path = "$1" - """.unindent % (fileName).replace("\\", "\\\\")) - check execNimble(["refresh"]).exitCode == QuitSuccess - body - -template cleanFile(fileName: string) = - removeFile fileName - defer: removeFile fileName - -macro cleanFiles(fileNames: varargs[string]) = - result = newStmtList() - for fn in fileNames: - result.add quote do: cleanFile(`fn`) - -template cleanDir(dirName: string) = - removeDir dirName - defer: removeDir dirName - -template createTempDir(dirName: string) = - createDir dirName - defer: removeDir dirName - -template cdCleanDir(dirName: string, body: untyped) = - cleanDir dirName - createDir dirName - cd dirName: - body - -suite "nimble refresh": - beforeSuite() - - test "can refresh with default urls": - let (output, exitCode) = execNimble(["refresh"]) - checkpoint(output) - check exitCode == QuitSuccess - - test "can refresh with custom urls": - testRefresh(): - writeFile(configFile, """ - [PackageList] - name = "official" - url = "https://google.com" - url = "https://google.com/404" - url = "https://irclogs.nim-lang.org/packages.json" - url = "https://nim-lang.org/nimble/packages.json" - url = "https://github.com/nim-lang/packages/raw/master/packages.json" - """.unindent) - - let (output, exitCode) = execNimble(["refresh", "--verbose"]) - checkpoint(output) - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - check inLines(lines, "config file at") - check inLines(lines, "official package list") - check inLines(lines, "https://google.com") - check inLines(lines, "packages.json file is invalid") - check inLines(lines, "404 not found") - check inLines(lines, "Package list downloaded.") - - test "can refresh with local package list": - testRefresh(): - writeFile(configFile, """ - [PackageList] - name = "local" - path = "$1" - """.unindent % (getCurrentDir() / "issue368" / "packages.json").replace("\\", "\\\\")) - let (output, exitCode) = execNimble(["refresh", "--verbose"]) - let lines = output.strip.processOutput() - check inLines(lines, "config file at") - check inLines(lines, "Copying") - check inLines(lines, "Package list copied.") - check exitCode == QuitSuccess - - test "package list source required": - testRefresh(): - writeFile(configFile, """ - [PackageList] - name = "local" - """) - let (output, exitCode) = execNimble(["refresh", "--verbose"]) - let lines = output.strip.processOutput() - check inLines(lines, "config file at") - check inLines(lines, "Package list 'local' requires either url or path") - check exitCode == QuitFailure - - test "package list can only have one source": - testRefresh(): - writeFile(configFile, """ - [PackageList] - name = "local" - path = "$1" - url = "http://nim-lang.org/nimble/packages.json" - """) - let (output, exitCode) = execNimble(["refresh", "--verbose"]) - let lines = output.strip.processOutput() - check inLines(lines, "config file at") - check inLines(lines, "Attempted to specify `url` and `path` for the same package list 'local'") - check exitCode == QuitFailure - -suite "nimscript": - beforeSuite() - - test "can install nimscript package": - cd "nimscript": - let - nim = findExe("nim").relativePath(base = getCurrentDir()) - check execNimbleYes(["install", "--nim:" & nim]).exitCode == QuitSuccess - - test "before/after install pkg dirs are correct": - cd "nimscript": - let (output, exitCode) = execNimbleYes(["install", "--nim:nim"]) - check exitCode == QuitSuccess - check output.contains("Before build") - check output.contains("After build") - let lines = output.strip.processOutput() - for line in lines: - if lines[3].startsWith("Before PkgDir:"): - check line.endsWith("tests" / "nimscript") - check lines[^1].startsWith("After PkgDir:") - let packageDir = getPackageDir(pkgsDir, "nimscript-0.1.0") - check lines[^1].strip(leading = false).endsWith(packageDir) - - test "before/after on build": - cd "nimscript": - let (output, exitCode) = execNimble([ - "build", "--nim:" & findExe("nim"), "--silent"]) - check exitCode == QuitSuccess - check output.contains("Before build") - check output.contains("After build") - check not output.contains("Verifying") - - test "can execute nimscript tasks": - cd "nimscript": - let (output, exitCode) = execNimble("work") - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - check lines[^1] == "10" - - test "can use nimscript's setCommand": - cd "nimscript": - let (output, exitCode) = execNimble("cTest") - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - check "Execution finished".normalize in lines[^1].normalize - - test "can use nimscript's setCommand with flags": - cd "nimscript": - let (output, exitCode) = execNimble("--debug", "cr") - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - check inLines(lines, "Hello World") - - test "can use nimscript with repeated flags (issue #329)": - cd "nimscript": - let (output, exitCode) = execNimble("--debug", "repeated") - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - var found = false - for line in lines: - if line.contains("--define:foo"): - found = true - check found == true - - test "can list nimscript tasks": - cd "nimscript": - let (output, exitCode) = execNimble("tasks") - check "work".normalize in output.normalize - check "test description".normalize in output.normalize - check exitCode == QuitSuccess - - test "can use pre/post hooks": - cd "nimscript": - let (output, exitCode) = execNimble("hooks") - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - check inLines(lines, "First") - check inLines(lines, "middle") - check inLines(lines, "last") - - test "pre hook can prevent action": - cd "nimscript": - let (output, exitCode) = execNimble("hooks2") - let lines = output.strip.processOutput() - check exitCode == QuitFailure - check(not inLines(lines, "Shouldn't happen")) - check inLines(lines, "Hook prevented further execution") - - test "nimble script api": - cd "nimscript": - let (output, exitCode) = execNimble("api") - let lines = output.strip.processOutput() - check exitCode == QuitSuccess - check inLines(lines, "thisDirCT: " & getCurrentDir()) - check inLines(lines, "PKG_DIR: " & getCurrentDir()) - check inLines(lines, "thisDir: " & getCurrentDir()) - - test "nimscript evaluation error message": - cd "invalidPackage": - let (output, exitCode) = execNimble("check") - let lines = output.strip.processOutput() - check lines.inLines("undeclared identifier: 'thisFieldDoesNotExist'") - check exitCode == QuitFailure - - test "can accept short flags (#329)": - cd "nimscript": - check execNimble("c", "-d:release", "nimscript.nim").exitCode == QuitSuccess - -suite "uninstall": - beforeSuite() - - test "can install packagebin2": - let args = ["install", pkgBin2Url] - check execNimbleYes(args).exitCode == QuitSuccess - - proc cannotSatisfyMsg(v1, v2: string): string = - &"Cannot satisfy the dependency on PackageA {v1} and PackageA {v2}" - - test "can reject same version dependencies": - let (outp, exitCode) = execNimbleYes("install", pkgBinUrl) - # We look at the error output here to avoid out-of-order problems caused by - # stderr output being generated and flushed without first flushing stdout - let ls = outp.strip.processOutput() - check exitCode != QuitSuccess - check ls.inLines(cannotSatisfyMsg("0.2.0", "0.5.0")) or - ls.inLines(cannotSatisfyMsg("0.5.0", "0.2.0")) - - proc setupIssue27Packages() = - # Install b - cd "issue27/b": - check execNimbleYes("install").exitCode == QuitSuccess - # Install a - cd "issue27/a": - check execNimbleYes("install").exitCode == QuitSuccess - cd "issue27": - check execNimbleYes("install").exitCode == QuitSuccess - - test "issue #27": - setupIssue27Packages() - - test "can uninstall": - # setup test environment - cleanDir(installDir) - setupIssue27Packages() - check execNimbleYes("install", &"{pkgAUrl}@0.2").exitCode == QuitSuccess - check execNimbleYes("install", &"{pkgAUrl}@0.5").exitCode == QuitSuccess - check execNimbleYes("install", &"{pkgAUrl}@0.6").exitCode == QuitSuccess - check execNimbleYes("install", pkgBin2Url).exitCode == QuitSuccess - check execNimbleYes("install", pkgBUrl).exitCode == QuitSuccess - cd "nimscript": check execNimbleYes("install").exitCode == QuitSuccess - - block: - let (outp, exitCode) = execNimbleYes("uninstall", "issue27b") - check exitCode != QuitSuccess - var ls = outp.strip.processOutput() - let pkg27ADir = getPackageDir(pkgsDir, "issue27a-0.1.0", false) - let expectedMsg = cannotUninstallPkgMsg("issue27b", "0.1.0", @[pkg27ADir]) - check ls.inLinesOrdered(expectedMsg) - - check execNimbleYes("uninstall", "issue27").exitCode == QuitSuccess - check execNimbleYes("uninstall", "issue27a").exitCode == QuitSuccess - - # Remove Package* - check execNimbleYes("uninstall", "PackageA@0.5").exitCode == QuitSuccess - - let (outp, exitCode) = execNimbleYes("uninstall", "PackageA") - check exitCode != QuitSuccess - let ls = outp.processOutput() - let - pkgBin2Dir = getPackageDir(pkgsDir, "packagebin2-0.1.0", false) - pkgBDir = getPackageDir(pkgsDir, "packageb-0.1.0", false) - expectedMsgForPkgA0dot6 = cannotUninstallPkgMsg( - "PackageA", "0.6.0", @[pkgBin2Dir]) - expectedMsgForPkgA0dot2 = cannotUninstallPkgMsg( - "PackageA", "0.2.0", @[pkgBDir]) - check ls.inLines(expectedMsgForPkgA0dot6) - check ls.inLines(expectedMsgForPkgA0dot2) - - check execNimbleYes("uninstall", "PackageBin2").exitCode == QuitSuccess - - # Case insensitive - check execNimbleYes("uninstall", "packagea").exitCode == QuitSuccess - check execNimbleYes("uninstall", "PackageA").exitCode != QuitSuccess - - # Remove the rest of the installed packages. - check execNimbleYes("uninstall", "PackageB").exitCode == QuitSuccess - - check execNimbleYes("uninstall", "PackageA@0.2", "issue27b").exitCode == - QuitSuccess - check(not dirExists(installDir / "pkgs" / "PackageA-0.2.0")) - -suite "nimble dump": - beforeSuite() - - test "can dump for current project": - cd "testdump": - let (outp, exitCode) = execNimble("dump") - check: exitCode == 0 - check: outp.processOutput.inLines("desc: \"Test package for dump command\"") - - test "can dump for project directory": - let (outp, exitCode) = execNimble("dump", "testdump") - check: exitCode == 0 - check: outp.processOutput.inLines("desc: \"Test package for dump command\"") - - test "can dump for project file": - let (outp, exitCode) = execNimble("dump", "testdump" / "testdump.nimble") - check: exitCode == 0 - check: outp.processOutput.inLines("desc: \"Test package for dump command\"") - - test "can dump for installed package": - cd "testdump": - check: execNimbleYes("install").exitCode == 0 - defer: - discard execNimbleYes("remove", "testdump") - - # Otherwise we might find subdirectory instead - cd "..": - let (outp, exitCode) = execNimble("dump", "testdump") - check: exitCode == 0 - check: outp.processOutput.inLines("desc: \"Test package for dump command\"") - - test "can dump when explicitly asking for INI format": - const outpExpected = """ -name: "testdump" -version: "0.1.0" -author: "nigredo-tori" -desc: "Test package for dump command" -license: "BSD" -skipDirs: "" -skipFiles: "" -skipExt: "" -installDirs: "" -installFiles: "" -installExt: "" -requires: "" -bin: "" -binDir: "" -srcDir: "" -backend: "c" -""" - let (outp, exitCode) = execNimble("dump", "--ini", "testdump") - check: exitCode == 0 - check: outp == outpExpected - - test "can dump in JSON format": - const outpExpected = """ -{ - "name": "testdump", - "version": "0.1.0", - "author": "nigredo-tori", - "desc": "Test package for dump command", - "license": "BSD", - "skipDirs": [], - "skipFiles": [], - "skipExt": [], - "installDirs": [], - "installFiles": [], - "installExt": [], - "requires": [], - "bin": [], - "binDir": "", - "srcDir": "", - "backend": "c" -} -""" - let (outp, exitCode) = execNimble("dump", "--json", "testdump") - check: exitCode == 0 - check: outp == outpExpected - -suite "can handle two binary versions": - beforeSuite() - - setup: - cd "binaryPackage/v1": - check execNimbleYes("install").exitCode == QuitSuccess - - cd "binaryPackage/v2": - check execNimbleYes("install").exitCode == QuitSuccess - - test "can execute v2": - let (output, exitCode) = execBin("binaryPackage") - check exitCode == QuitSuccess - check output.strip() == "v2" - - test "can update symlink to earlier version after removal": - check execNimbleYes("remove", "binaryPackage@2.0").exitCode==QuitSuccess - - let (output, exitCode) = execBin("binaryPackage") - check exitCode == QuitSuccess - check output.strip() == "v1" - - test "can keep symlink version after earlier version removal": - check execNimbleYes("remove", "binaryPackage@1.0").exitCode==QuitSuccess - - let (output, exitCode) = execBin("binaryPackage") - check exitCode == QuitSuccess - check output.strip() == "v2" - -suite "reverse dependencies": - beforeSuite() - - test "basic test": - cd "revdep/mydep": - verify execNimbleYes("install") - - cd "revdep/pkgWithDep": - verify execNimbleYes("install") - - verify execNimbleYes("remove", "pkgA") - verify execNimbleYes("remove", "mydep") - - test "revdep fail test": - cd "revdep/mydep": - verify execNimbleYes("install") - - cd "revdep/pkgWithDep": - verify execNimbleYes("install") - - let (output, exitCode) = execNimble("uninstall", "mydep") - checkpoint output - check output.processOutput.inLines("cannot uninstall mydep") - check exitCode == QuitFailure - - test "revdep -i test": - cd "revdep/mydep": - verify execNimbleYes("install") - - cd "revdep/pkgWithDep": - verify execNimbleYes("install") - - verify execNimbleYes("remove", "mydep", "-i") - - 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") - - test "remove skips packages with revDeps (#504)": - check execNimbleYes("--debug", "install", "nimboost@0.5.5", "nimfp@0.4.4").exitCode == QuitSuccess - - var (output, exitCode) = execNimble("uninstall", "nimboost", "nimfp", "-n") - var lines = output.strip.processOutput() - check inLines(lines, "Cannot uninstall nimboost") - - (output, exitCode) = execNimbleYes("uninstall", "nimfp", "nimboost") - lines = output.strip.processOutput() - check (not inLines(lines, "Cannot uninstall nimboost")) - - check execNimble("path", "nimboost").exitCode != QuitSuccess - check execNimble("path", "nimfp").exitCode != QuitSuccess - - test "old format conversion": - const oldNimbleDataFileName = - "./revdep/nimbleData/old_nimble_data.json".normalizedPath - const newNimbleDataFileName = - "./revdep/nimbleData/new_nimble_data.json".normalizedPath - - doAssert fileExists(oldNimbleDataFileName) - doAssert fileExists(newNimbleDataFileName) - - let oldNimbleData = loadNimbleData(oldNimbleDataFileName) - let newNimbleData = loadNimbleData(newNimbleDataFileName) - - doAssert oldNimbleData == newNimbleData - -suite "develop feature": - proc filesList(filesNames: seq[string]): string = - for fileName in filesNames: - result.addQuoted fileName - result.add ',' - - proc developFile(includes: seq[string], dependencies: seq[string]): string = - result = """{"version":"$#","includes":[$#],"dependencies":[$#]}""" % - [developFileVersion, filesList(includes), filesList(dependencies)] - - const - pkgListFileName = "packages.json" - dependentPkgName = "dependent" - dependentPkgVersion = "1.0" - dependentPkgNameAndVersion = &"{dependentPkgName}@{dependentPkgVersion}" - dependentPkgPath = "develop/dependent".normalizedPath - includeFileName = "included.develop" - pkgAName = "packagea" - pkgBName = "packageb" - pkgSrcDirTestName = "srcdirtest" - pkgHybridName = "hybrid" - depPath = "../dependency".normalizedPath - depName = "dependency" - depVersion = "0.1.0" - depNameAndVersion = &"{depName}@{depVersion}" - dep2Path = "../dependency2".normalizedPath - emptyDevelopFileContent = developFile(@[], @[]) - - let anyVersion = parseVersionRange("") - - test "can develop from dir with srcDir": - cd &"develop/{pkgSrcDirTestName}": - let (output, exitCode) = execNimble("develop") - check exitCode == QuitSuccess - let lines = output.processOutput - check not lines.inLines("will not be compiled") - check lines.inLines(pkgSetupInDevModeMsg( - pkgSrcDirTestName, getCurrentDir())) - - test "can git clone for develop": - cdCleanDir installDir: - let (output, exitCode) = execNimble("develop", pkgAUrl) - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgSetupInDevModeMsg(pkgAName, installDir / pkgAName)) - - test "can develop from package name": - cdCleanDir installDir: - usePackageListFile &"../develop/{pkgListFileName}": - let (output, exitCode) = execNimble("develop", pkgBName) - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered(pkgInstalledMsg(pkgAName)) - check lines.inLinesOrdered( - pkgSetupInDevModeMsg(pkgBName, installDir / pkgBName)) - - test "can develop list of packages": - cdCleanDir installDir: - usePackageListFile &"../develop/{pkgListFileName}": - let (output, exitCode) = execNimble( - "develop", pkgAName, pkgBName) - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered(pkgSetupInDevModeMsg( - pkgAName, installDir / pkgAName)) - check lines.inLinesOrdered(pkgInstalledMsg(pkgAName)) - check lines.inLinesOrdered(pkgSetupInDevModeMsg( - pkgBName, installDir / pkgBName)) - - test "cannot remove package with develop reverse dependency": - cdCleanDir installDir: - usePackageListFile &"../develop/{pkgListFileName}": - check execNimble("develop", pkgBName).exitCode == QuitSuccess - let (output, exitCode) = execNimble("remove", pkgAName) - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered( - cannotUninstallPkgMsg(pkgAName, "0.2.0", @[installDir / pkgBName])) - - test "can reject binary packages": - cd "develop/binary": - let (output, exitCode) = execNimble("develop") - check output.processOutput.inLines("cannot develop packages") - check exitCode == QuitFailure - - test "can develop hybrid": - cd &"develop/{pkgHybridName}": - let (output, exitCode) = execNimble("develop") - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered("will not be compiled") - check lines.inLinesOrdered( - pkgSetupInDevModeMsg(pkgHybridName, getCurrentDir())) - - test "can specify different absolute clone dir": - let otherDir = installDir / "./some/other/dir" - cleanDir otherDir - let (output, exitCode) = execNimble( - "develop", &"-p:{otherDir}", pkgAUrl) - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgSetupInDevModeMsg(pkgAName, otherDir / pkgAName)) - - test "can specify different relative clone dir": - const otherDir = "./some/other/dir" - cdCleanDir installDir: - let (output, exitCode) = execNimble( - "develop", &"-p:{otherDir}", pkgAUrl) - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgSetupInDevModeMsg(pkgAName, installDir / otherDir / pkgAName)) - - test "do not allow multiple path options": - let - developDir = installDir / "./some/dir" - anotherDevelopDir = installDir / "./some/other/dir" - defer: - # cleanup in the case of test failure - removeDir developDir - removeDir anotherDevelopDir - let (output, exitCode) = execNimble( - "develop", &"-p:{developDir}", &"-p:{anotherDevelopDir}", pkgAUrl) - check exitCode == QuitFailure - check output.processOutput.inLines("Multiple path options are given") - check not developDir.dirExists - check not anotherDevelopDir.dirExists - - test "do not allow path option without packages to download": - let developDir = installDir / "./some/dir" - let (output, exitCode) = execNimble("develop", &"-p:{developDir}") - check exitCode == QuitFailure - check output.processOutput.inLines(pathGivenButNoPkgsToDownloadMsg) - check not developDir.dirExists - - test "do not allow add/remove options out of package directory": - cleanFile developFileName - let (output, exitCode) = execNimble("develop", "-a:./develop/dependency/") - check exitCode == QuitFailure - check output.processOutput.inLines(developOptionsOutOfPkgDirectoryMsg) - - test "cannot load invalid develop file": - cd dependentPkgPath: - cleanFile developFileName - writeFile(developFileName, "this is not a develop file") - let (output, exitCode) = execNimble("check") - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered( - notAValidDevFileJsonMsg(getCurrentDir() / developFileName)) - check lines.inLinesOrdered(validationFailedMsg) - - test "add downloaded package to the develop file": - cleanDir installDir - cd "develop/dependency": - usePackageListFile &"../{pkgListFileName}": - cleanFile developFileName - let - (output, exitCode) = execNimble( - "develop", &"-p:{installDir}", pkgAName) - pkgAAbsPath = installDir / pkgAName - developFileContent = developFile(@[], @[pkgAAbsPath]) - check exitCode == QuitSuccess - check parseFile(developFileName) == parseJson(developFileContent) - var lines = output.processOutput - check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) - - test "cannot add not a dependency downloaded package to the develop file": - cleanDir installDir - cd "develop/dependency": - usePackageListFile &"../{pkgListFileName}": - cleanFile developFileName - let - (output, exitCode) = execNimble( - "develop", &"-p:{installDir}", pkgAName, pkgBName) - pkgAAbsPath = installDir / pkgAName - pkgBAbsPath = installDir / pkgBName - developFileContent = developFile(@[], @[pkgAAbsPath]) - check exitCode == QuitFailure - check parseFile(developFileName) == parseJson(developFileContent) - var lines = output.processOutput - check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) - check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgBName, pkgBAbsPath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) - check lines.inLinesOrdered( - notADependencyErrorMsg(&"{pkgBName}@0.2.0", depNameAndVersion)) - - test "add package to develop file": - cleanDir installDir - cd dependentPkgPath: - usePackageListFile &"../{pkgListFileName}": - cleanFiles developFileName, dependentPkgName.addFileExt(ExeExt) - var (output, exitCode) = execNimble("develop", &"-a:{depPath}") - check exitCode == QuitSuccess - check developFileName.fileExists - check output.processOutput.inLines( - pkgAddedInDevModeMsg(depNameAndVersion, depPath)) - const expectedDevelopFile = developFile(@[], @[depPath]) - check parseFile(developFileName) == parseJson(expectedDevelopFile) - (output, exitCode) = execNimble("run") - check exitCode == QuitSuccess - check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) - - test "warning on attempt to add the same package twice": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let (output, exitCode) = execNimble("develop", &"-a:{depPath}") - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgAlreadyInDevModeMsg(depNameAndVersion, depPath)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "cannot add invalid package to develop file": - cd dependentPkgPath: - cleanFile developFileName - const invalidPkgDir = "../invalidPkg".normalizedPath - createTempDir invalidPkgDir - let (output, exitCode) = execNimble("develop", &"-a:{invalidPkgDir}") - check exitCode == QuitFailure - check output.processOutput.inLines(invalidPkgMsg(invalidPkgDir)) - check not developFileName.fileExists - - test "cannot add not a dependency to develop file": - cd dependentPkgPath: - cleanFile developFileName - let (output, exitCode) = execNimble("develop", "-a:../srcdirtest/") - check exitCode == QuitFailure - check output.processOutput.inLines( - notADependencyErrorMsg(&"{pkgSrcDirTestName}@1.0", "dependent@1.0")) - check not developFileName.fileExists - - test "cannot add two packages with the same name to develop file": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let (output, exitCode) = execNimble("develop", &"-a:{dep2Path}") - check exitCode == QuitFailure - check output.processOutput.inLines( - pkgAlreadyPresentAtDifferentPathMsg(depName, depPath.absolutePath)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "found two packages with the same name in the develop file": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile( - @[], @[depPath, dep2Path]) - writeFile(developFileName, developFileContent) - - let - (output, exitCode) = execNimble("check") - developFilePath = getCurrentDir() / developFileName - - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered(failedToLoadFileMsg(developFilePath)) - check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, - [(depPath.absolutePath.Path, developFilePath.Path), - (dep2Path.absolutePath.Path, developFilePath.Path)].toHashSet)) - check lines.inLinesOrdered(validationFailedMsg) - - test "remove package from develop file by path": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let (output, exitCode) = execNimble("develop", &"-r:{depPath}") - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - - test "warning on attempt to remove not existing package path": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let (output, exitCode) = execNimble("develop", &"-r:{dep2Path}") - check exitCode == QuitSuccess - check output.processOutput.inLines(pkgPathNotInDevFileMsg(dep2Path)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "remove package from develop file by name": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let (output, exitCode) = execNimble("develop", &"-n:{depName}") - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath.absolutePath)) - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - - test "warning on attempt to remove not existing package name": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - const notExistingPkgName = "dependency2" - let (output, exitCode) = execNimble("develop", &"-n:{notExistingPkgName}") - check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgNameNotInDevFileMsg(notExistingPkgName)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "include develop file": - cleanDir installDir - cd dependentPkgPath: - usePackageListFile &"../{pkgListFileName}": - cleanFiles developFileName, includeFileName, - dependentPkgName.addFileExt(ExeExt) - const includeFileContent = developFile(@[], @[depPath]) - writeFile(includeFileName, includeFileContent) - var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - check exitCode == QuitSuccess - check developFileName.fileExists - check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) - const expectedDevelopFile = developFile(@[includeFileName], @[]) - check parseFile(developFileName) == parseJson(expectedDevelopFile) - (output, exitCode) = execNimble("run") - check exitCode == QuitSuccess - check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) - - test "warning on attempt to include already included develop file": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - const developFileContent = developFile(@[includeFileName], @[]) - writeFile(developFileName, developFileContent) - const includeFileContent = developFile(@[], @[depPath]) - writeFile(includeFileName, includeFileContent) - - let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - check exitCode == QuitSuccess - check output.processOutput.inLines( - alreadyInclInDevFileMsg(includeFileName)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "cannot include invalid develop file": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - writeFile(includeFileName, """{"some": "json"}""") - let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - check exitCode == QuitFailure - check not developFileName.fileExists - check output.processOutput.inLines(failedToLoadFileMsg(includeFileName)) - - test "cannot load a develop file with an invalid include file in it": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - const developFileContent = developFile(@[includeFileName], @[]) - writeFile(developFileName, developFileContent) - let (output, exitCode) = execNimble("check") - check exitCode == QuitFailure - let developFilePath = getCurrentDir() / developFileName - var lines = output.processOutput() - check lines.inLinesOrdered(failedToLoadFileMsg(developFilePath)) - check lines.inLinesOrdered(invalidDevFileMsg(developFilePath)) - check lines.inLinesOrdered(&"cannot read from file: {includeFileName}") - check lines.inLinesOrdered(validationFailedMsg) - - test "can include file pointing to the same package": - cleanDir installDir - cd dependentPkgPath: - usePackageListFile &"../{pkgListFileName}": - cleanFiles developFileName, includeFileName, - dependentPkgName.addFileExt(ExeExt) - const fileContent = developFile(@[], @[depPath]) - writeFile(developFileName, fileContent) - writeFile(includeFileName, fileContent) - var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - check exitCode == QuitSuccess - check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) - const expectedFileContent = developFile( - @[includeFileName], @[depPath]) - check parseFile(developFileName) == parseJson(expectedFileContent) - (output, exitCode) = execNimble("run") - check exitCode == QuitSuccess - check output.processOutput.inLines(pkgInstalledMsg(pkgAName)) - - test "cannot include conflicting develop file": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - const includeFileContent = developFile(@[], @[dep2Path]) - writeFile(includeFileName, includeFileContent) - - let - (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - developFilePath = getCurrentDir() / developFileName - - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered( - failedToInclInDevFileMsg(includeFileName, developFilePath)) - check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, - [(depPath.absolutePath.Path, developFilePath.Path), - (dep2Path.Path, includeFileName.Path)].toHashSet)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "validate included dependencies version": - cd &"{dependentPkgPath}2": - cleanFiles developFileName, includeFileName - const includeFileContent = developFile(@[], @[dep2Path]) - writeFile(includeFileName, includeFileContent) - let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - check exitCode == QuitFailure - var lines = output.processOutput - let developFilePath = getCurrentDir() / developFileName - check lines.inLinesOrdered( - failedToInclInDevFileMsg(includeFileName, developFilePath)) - check lines.inLinesOrdered(invalidPkgMsg(dep2Path)) - check lines.inLinesOrdered(dependencyNotInRangeErrorMsg( - depNameAndVersion, dependentPkgNameAndVersion, - parseVersionRange(">= 0.2.0"))) - check not developFileName.fileExists - - test "exclude develop file": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - const developFileContent = developFile(@[includeFileName], @[]) - writeFile(developFileName, developFileContent) - const includeFileContent = developFile(@[], @[depPath]) - writeFile(includeFileName, includeFileContent) - let (output, exitCode) = execNimble("develop", &"-e:{includeFileName}") - check exitCode == QuitSuccess - check output.processOutput.inLines(exclFromDevFileMsg(includeFileName)) - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - - test "warning on attempt to exclude not included develop file": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - const developFileContent = developFile(@[includeFileName], @[]) - writeFile(developFileName, developFileContent) - const includeFileContent = developFile(@[], @[depPath]) - writeFile(includeFileName, includeFileContent) - let (output, exitCode) = execNimble("develop", &"-e:../{includeFileName}") - check exitCode == QuitSuccess - check output.processOutput.inLines( - notInclInDevFileMsg((&"../{includeFileName}").normalizedPath)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "relative paths in the develop file and absolute from the command line": - cd dependentPkgPath: - cleanFiles developFileName, includeFileName - const developFileContent = developFile( - @[includeFileName], @[depPath]) - writeFile(developFileName, developFileContent) - const includeFileContent = developFile(@[], @[depPath]) - writeFile(includeFileName, includeFileContent) - - let - includeFileAbsolutePath = includeFileName.absolutePath - dependencyPkgAbsolutePath = "../dependency".absolutePath - (output, exitCode) = execNimble("develop", - &"-e:{includeFileAbsolutePath}", &"-r:{dependencyPkgAbsolutePath}") - - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered(exclFromDevFileMsg(includeFileAbsolutePath)) - check lines.inLinesOrdered( - pkgRemovedFromDevModeMsg(depNameAndVersion, dependencyPkgAbsolutePath)) - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - - test "absolute paths in the develop file and relative from the command line": - cd dependentPkgPath: - let - currentDir = getCurrentDir() - includeFileAbsPath = currentDir / includeFileName - dependencyAbsPath = currentDir / depPath - developFileContent = developFile( - @[includeFileAbsPath], @[dependencyAbsPath]) - includeFileContent = developFile(@[], @[depPath]) - - cleanFiles developFileName, includeFileName - writeFile(developFileName, developFileContent) - writeFile(includeFileName, includeFileContent) - - let (output, exitCode) = execNimble("develop", - &"-e:{includeFileName}", &"-r:{depPath}") - - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered(exclFromDevFileMsg(includeFileName)) - check lines.inLinesOrdered( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - - test "uninstall package with develop reverse dependencies": - cleanDir installDir - cd dependentPkgPath: - usePackageListFile &"../{pkgListFileName}": - const developFileContent = developFile(@[], @[depPath]) - cleanFiles developFileName, "dependent" - writeFile(developFileName, developFileContent) - - block checkSuccessfulInstallAndReverseDependencyAddedToNimbleData: - let - (_, exitCode) = execNimble("install") - nimbleData = parseFile(installDir / nimbleDataFileName) - packageDir = getPackageDir(pkgsDir, "PackageA-0.5.0") - checksum = packageDir[packageDir.rfind('-') + 1 .. ^1] - devRevDepPath = nimbleData{$ndjkRevDep}{pkgAName}{"0.5.0"}{ - checksum}{0}{$ndjkRevDepPath} - depAbsPath = getCurrentDir() / depPath - - check exitCode == QuitSuccess - check not devRevDepPath.isNil - check devRevDepPath.str == depAbsPath - - block checkSuccessfulUninstallAndRemovalFromNimbleData: - let - (_, exitCode) = execNimble("uninstall", "-i", pkgAName, "-y") - nimbleData = parseFile(installDir / nimbleDataFileName) - - check exitCode == QuitSuccess - check not nimbleData[$ndjkRevDep].hasKey(pkgAName) - - test "follow develop dependency's develop file": - cd "develop": - const pkg1DevFilePath = "pkg1" / developFileName - const pkg2DevFilePath = "pkg2" / developFileName - cleanFiles pkg1DevFilePath, pkg2DevFilePath - const pkg1DevFileContent = developFile(@[], @["../pkg2"]) - writeFile(pkg1DevFilePath, pkg1DevFileContent) - const pkg2DevFileContent = developFile(@[], @["../pkg3"]) - writeFile(pkg2DevFilePath, pkg2DevFileContent) - cd "pkg1": - cleanFile "pkg1".addFileExt(ExeExt) - let (_, exitCode) = execNimble("run", "-n") - check exitCode == QuitSuccess - - test "version clash from followed develop file": - cd "develop": - const pkg1DevFilePath = "pkg1" / developFileName - const pkg2DevFilePath = "pkg2" / developFileName - cleanFiles pkg1DevFilePath, pkg2DevFilePath - const pkg1DevFileContent = developFile(@[], @["../pkg2", "../pkg3"]) - writeFile(pkg1DevFilePath, pkg1DevFileContent) - const pkg2DevFileContent = developFile(@[], @["../pkg3.2"]) - writeFile(pkg2DevFilePath, pkg2DevFileContent) - - let - currentDir = getCurrentDir() - pkg1DevFileAbsPath = currentDir / pkg1DevFilePath - pkg2DevFileAbsPath = currentDir / pkg2DevFilePath - pkg3AbsPath = currentDir / "pkg3" - pkg32AbsPath = currentDir / "pkg3.2" - - cd "pkg1": - cleanFile "pkg1".addFileExt(ExeExt) - let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered(failedToLoadFileMsg(pkg1DevFileAbsPath)) - check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", - [(pkg3AbsPath.Path, pkg1DevFileAbsPath.Path), - (pkg32AbsPath.Path, pkg2DevFileAbsPath.Path)].toHashSet)) - - test "relative include paths are followed from the file's directory": - cd dependentPkgPath: - const includeFilePath = &"../{includeFileName}" - cleanFiles includeFilePath, developFileName, dependentPkgName.addFileExt(ExeExt) - const developFileContent = developFile(@[includeFilePath], @[]) - writeFile(developFileName, developFileContent) - const includeFileContent = developFile(@[], @["./dependency2/"]) - writeFile(includeFilePath, includeFileContent) - let (_, errorCode) = execNimble("run", "-n") - check errorCode == QuitSuccess - - test "filter not used included develop dependencies": - # +--------------------------+ +--------------------------+ - # | pkg1 | +------------>| pkg2 | - # +--------------------------+ | dependency +--------------------------+ - # | requires "pkg2", "pkg3" | | | nimble.develop | - # +--------------------------+ | +--------------------------+ - # | nimble.develop |--+ | - # +--------------------------+ includes | - # v - # +---------------+ - # | develop.json | - # +---------------+ - # | - # dependency | - # v - # +---------------------+ - # | pkg3 | - # +---------------------+ - # | version = "0.2.0" | - # +---------------------+ - - # Here the build must fail because "pkg3" coming from develop file included - # in "pkg2"'s develop file is not a dependency of "pkg2" itself and it must - # be filtered. In this way "pkg1"'s dependency to "pkg3" is not satisfied. - - cd "develop": - const - pkg1DevFilePath = "pkg1" / developFileName - pkg2DevFilePath = "pkg2.2" / developFileName - freeDevFileName = "develop.json" - pkg1DevFileContent = developFile(@[], @["../pkg2.2"]) - pkg2DevFileContent = developFile(@[&"../{freeDevFileName}"], @[]) - freeDevFileContent = developFile(@[], @["./pkg3.2"]) - - cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFileName - writeFile(pkg1DevFilePath, pkg1DevFileContent) - writeFile(pkg2DevFilePath, pkg2DevFileContent) - writeFile(freeDevFileName, freeDevFileContent) - - cd "pkg1": - cleanFile "pkg1".addFileExt(ExeExt) - let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) - check lines.inLinesOrdered(pkgNotFoundMsg(("pkg3", anyVersion))) - - test "do not filter used included develop dependencies": - # +--------------------------+ +--------------------------+ - # | pkg1 | +------------>+ pkg2 | - # +--------------------------+ | dependency +--------------------------+ - # | requires "pkg2", "pkg3" | | | requires "pkg3" | - # +--------------------------+ | +--------------------------+ - # | nimble.develop |--+ | nimble.develop | - # +--------------------------+ +--------------------------+ - # | - # includes | - # v - # +---------------+ - # | develop.json | - # +---------------+ - # | - # dependency | - # v - # +---------------------+ - # | pkg3 | - # +---------------------+ - # | version = "0.2.0" | - # +---------------------+ - - # Here the build must pass because "pkg3" coming form develop file included - # in "pkg2"'s develop file is a dependency of "pkg2" and it will be used, - # in this way satisfying also "pkg1"'s requirements. - - cd "develop": - const - pkg1DevFilePath = "pkg1" / developFileName - pkg2DevFilePath = "pkg2" / developFileName - freeDevFileName = "develop.json" - pkg1DevFileContent = developFile(@[], @["../pkg2"]) - pkg2DevFileContent = developFile(@[&"../{freeDevFileName}"], @[]) - freeDevFileContent = developFile(@[], @["./pkg3.2"]) - - cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFileName - writeFile(pkg1DevFilePath, pkg1DevFileContent) - writeFile(pkg2DevFilePath, pkg2DevFileContent) - writeFile(freeDevFileName, freeDevFileContent) - - cd "pkg1": - cleanFile "pkg1".addFileExt(ExeExt) - let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) - - test "no version clash with filtered not used included develop dependencies": - # +--------------------------+ +--------------------------+ - # | pkg1 | +------------>| pkg2 | - # +--------------------------+ | dependency +--------------------------+ - # | requires "pkg2", "pkg3" | | | nimble.develop | - # +--------------------------+ | +--------------------------+ - # | nimble.develop |--+ | - # +--------------------------+ includes | - # | v - # includes | +---------------+ - # v | develop2.json | - # +---------------+ +-------+-------+ - # | develop1.json | | - # +---------------+ dependency | - # | v - # dependency | +---------------------+ - # v | pkg3 | - # +-------------------+ +---------------------+ - # | pkg3 | | version = "0.2.0" | - # +-------------------+ +---------------------+ - # | version = "0.1.0" | - # +-------------------+ - - # Here the build must pass because only the version of "pkg3" included via - # "develop1.json" must be taken into account, since "pkg2" does not depend - # on "pkg3" and the version coming from "develop2.json" must be filtered. - - cd "develop": - const - pkg1DevFilePath = "pkg1" / developFileName - pkg2DevFilePath = "pkg2.2" / developFileName - freeDevFile1Name = "develop1.json" - freeDevFile2Name = "develop2.json" - pkg1DevFileContent = developFile( - @[&"../{freeDevFile1Name}"], @["../pkg2.2"]) - pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) - freeDevFile1Content = developFile(@[], @["./pkg3"]) - freeDevFile2Content = developFile(@[], @["./pkg3.2"]) - - cleanFiles pkg1DevFilePath, pkg2DevFilePath, - freeDevFile1Name, freeDevFile2Name - writeFile(pkg1DevFilePath, pkg1DevFileContent) - writeFile(pkg2DevFilePath, pkg2DevFileContent) - writeFile(freeDevFile1Name, freeDevFile1Content) - writeFile(freeDevFile2Name, freeDevFile2Content) - - cd "pkg1": - cleanFile "pkg1".addFileExt(ExeExt) - let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitSuccess - var lines = output.processOutput - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) - - test "version clash with used included develop dependencies": - # +--------------------------+ +--------------------------+ - # | pkg1 | +------------>| pkg2 | - # +--------------------------+ | dependency +--------------------------+ - # | requires "pkg2", "pkg3" | | | requires "pkg3" | - # +--------------------------+ | +--------------------------+ - # | nimble.develop |--+ | nimble.develop | - # +--------------------------+ +--------------------------+ - # | | - # includes | includes | - # v v - # +-------+-------+ +---------------+ - # | develop1.json | | develop2.json | - # +-------+-------+ +---------------+ - # | | - # dependency | dependency | - # v v - # +-------------------+ +---------------------+ - # | pkg3 | | pkg3 | - # +-------------------+ +---------------------+ - # | version = "0.1.0" | | version = "0.2.0" | - # +-------------------+ +---------------------+ - - # Here the build must fail because since "pkg3" is dependency of both "pkg1" - # and "pkg2", both versions coming from "develop1.json" and "develop2.json" - # must be taken into account, but they are different." - - cd "develop": - const - pkg1DevFilePath = "pkg1" / developFileName - pkg2DevFilePath = "pkg2" / developFileName - freeDevFile1Name = "develop1.json" - freeDevFile2Name = "develop2.json" - pkg1DevFileContent = developFile( - @[&"../{freeDevFile1Name}"], @["../pkg2"]) - pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) - freeDevFile1Content = developFile(@[], @["./pkg3"]) - freeDevFile2Content = developFile(@[], @["./pkg3.2"]) - - cleanFiles pkg1DevFilePath, pkg2DevFilePath, - freeDevFile1Name, freeDevFile2Name - writeFile(pkg1DevFilePath, pkg1DevFileContent) - writeFile(pkg2DevFilePath, pkg2DevFileContent) - writeFile(freeDevFile1Name, freeDevFile1Content) - writeFile(freeDevFile2Name, freeDevFile2Content) - - cd "pkg1": - cleanFile "pkg1".addFileExt(ExeExt) - let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered(failedToLoadFileMsg( - getCurrentDir() / developFileName)) - check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", - [("../pkg3".Path, (&"../{freeDevFile1Name}").Path), - ("../pkg3.2".Path, (&"../{freeDevFile2Name}").Path)].toHashSet)) - - test "create an empty develop file with default name in the current dir": - cd dependentPkgPath: - cleanFile developFileName - let (output, errorCode) = execNimble("develop", "-c") - check errorCode == QuitSuccess - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - check output.processOutput.inLines( - emptyDevFileCreatedMsg(developFileName)) - - test "create an empty develop file in some dir": - cleanDir installDir - let filePath = installDir / "develop.json" - cleanFile filePath - createDir installDir - let (output, errorCode) = execNimble("develop", &"-c:{filePath}") - check errorCode == QuitSuccess - check parseFile(filePath) == parseJson(emptyDevelopFileContent) - check output.processOutput.inLines(emptyDevFileCreatedMsg(filePath)) - - test "try to create an empty develop file with already existing name": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let - filePath = getCurrentDir() / developFileName - (output, errorCode) = execNimble("develop", &"-c:{filePath}") - check errorCode == QuitFailure - check output.processOutput.inLines(fileAlreadyExistsMsg(filePath)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "try to create an empty develop file in not existing dir": - let filePath = installDir / "some/not/existing/dir/develop.json" - cleanFile filePath - let (output, errorCode) = execNimble("develop", &"-c:{filePath}") - check errorCode == QuitFailure - check output.processOutput.inLines(&"cannot open: {filePath}") - - test "partial success when some operations in single command failed": - cleanDir installDir - cd dependentPkgPath: - usePackageListFile &"../{pkgListFileName}": - const - dep2DevelopFilePath = dep2Path / developFileName - includeFileContent = developFile(@[], @[dep2Path]) - invalidInclFilePath = "/some/not/existing/file/path".normalizedPath - - cleanFiles developFileName, includeFileName, dep2DevelopFilePath - writeFile(includeFileName, includeFileContent) - - let - developFilePath = getCurrentDir() / developFileName - (output, errorCode) = execNimble("develop", &"-p:{installDir}", - pkgAName, # fail because not a direct dependency - "-c", # success - &"-a:{depPath}", # success - &"-a:{dep2Path}", # fail because of names collision - &"-i:{includeFileName}", # fail because of names collision - &"-n:{depName}", # success - &"-c:{developFilePath}", # fail because the file already exists - &"-a:{dep2Path}", # success - &"-i:{includeFileName}", # success - &"-i:{invalidInclFilePath}", # fail - &"-c:{dep2DevelopFilePath}") # success - - check errorCode == QuitFailure - var lines = output.processOutput - check lines.inLinesOrdered(pkgSetupInDevModeMsg( - pkgAName, installDir / pkgAName)) - check lines.inLinesOrdered(emptyDevFileCreatedMsg(developFileName)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(depNameAndVersion, depPath)) - check lines.inLinesOrdered( - pkgAlreadyPresentAtDifferentPathMsg(depName, depPath)) - check lines.inLinesOrdered( - failedToInclInDevFileMsg(includeFileName, developFilePath)) - check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, - [(depPath.Path, developFilePath.Path), - (dep2Path.Path, includeFileName.Path)].toHashSet)) - check lines.inLinesOrdered( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) - check lines.inLinesOrdered(fileAlreadyExistsMsg(developFilePath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(depNameAndVersion, dep2Path)) - check lines.inLinesOrdered(inclInDevFileMsg(includeFileName)) - check lines.inLinesOrdered(failedToLoadFileMsg(invalidInclFilePath)) - check lines.inLinesOrdered(emptyDevFileCreatedMsg(dep2DevelopFilePath)) - check parseFile(dep2DevelopFilePath) == - parseJson(emptyDevelopFileContent) - check lines.inLinesOrdered(notADependencyErrorMsg( - &"{pkgAName}@0.6.0", dependentPkgNameAndVersion)) - const expectedDevelopFileContent = developFile( - @[includeFileName], @[dep2Path]) - check parseFile(developFileName) == - parseJson(expectedDevelopFileContent) - -# suite "path command": -# test "can get correct path for srcDir (#531)": -# cd "develop/srcdirtest": -# let (_, exitCode) = execNimbleYes("install") -# check exitCode == QuitSuccess -# let (output, _) = execNimble("path", "srcdirtest") -# let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") -# check output.strip() == packageDir - -# test "nimble path points to develop": -# cd "develop/srcdirtest": -# var (output, exitCode) = execNimble("develop") -# checkpoint output -# check exitCode == QuitSuccess - -# (output, exitCode) = execNimble("path", "srcdirtest") - -# checkpoint output -# check exitCode == QuitSuccess -# check output.strip() == getCurrentDir() / "src" - -suite "test command": - beforeSuite() - - test "Runs passing unit tests": - cd "testCommand/testsPass": - # Pass flags to test #726, #757 - let (outp, exitCode) = execNimble("test", "-d:CUSTOM") - check exitCode == QuitSuccess - check outp.processOutput.inLines("First test") - check outp.processOutput.inLines("Second test") - check outp.processOutput.inLines("Third test") - check outp.processOutput.inLines("Executing my func") - - test "Runs failing unit tests": - cd "testCommand/testsFail": - let (outp, exitCode) = execNimble("test") - check exitCode == QuitFailure - check outp.processOutput.inLines("First test") - check outp.processOutput.inLines("Failing Second test") - check(not outp.processOutput.inLines("Third test")) - - test "test command can be overriden": - cd "testCommand/testOverride": - let (outp, exitCode) = execNimble("-d:CUSTOM", "test", "--runflag") - check exitCode == QuitSuccess - check outp.processOutput.inLines("overriden") - check outp.processOutput.inLines("true") - - test "certain files are ignored": - cd "testCommand/testsIgnore": - let (outp, exitCode) = execNimble("test") - check exitCode == QuitSuccess - check(not outp.processOutput.inLines("Should be ignored")) - check outp.processOutput.inLines("First test") - - test "CWD is root of package": - cd "testCommand/testsCWD": - let (outp, exitCode) = execNimble("test") - check exitCode == QuitSuccess - check outp.processOutput.inLines(getCurrentDir()) - -suite "check command": - beforeSuite() - - test "can succeed package": - cd "binaryPackage/v1": - let (outp, exitCode) = execNimble("check") - check exitCode == QuitSuccess - check outp.processOutput.inLines("success") - check outp.processOutput.inLines("\"binaryPackage\" is valid") - - cd "packageStructure/a": - let (outp, exitCode) = execNimble("check") - check exitCode == QuitSuccess - check outp.processOutput.inLines("success") - check outp.processOutput.inLines("\"a\" is valid") - - cd "packageStructure/b": - let (outp, exitCode) = execNimble("check") - check exitCode == QuitSuccess - check outp.processOutput.inLines("success") - check outp.processOutput.inLines("\"b\" is valid") - - cd "packageStructure/c": - let (outp, exitCode) = execNimble("check") - check exitCode == QuitSuccess - check outp.processOutput.inLines("success") - check outp.processOutput.inLines("\"c\" is valid") - - test "can fail package": - cd "packageStructure/x": - let (outp, exitCode) = execNimble("check") - check exitCode == QuitFailure - check outp.processOutput.inLines("failure") - check outp.processOutput.inLines("validation failed") - check outp.processOutput.inLines("package 'x' has an incorrect structure") - -suite "multi": - beforeSuite() - - test "can install package from git subdir": - var - args = @["install", pkgMultiAlphaUrl] - (output, exitCode) = execNimbleYes(args) - check exitCode == QuitSuccess - - # Issue 785 - args.add @[pkgMultiBetaUrl, "-n"] - (output, exitCode) = execNimble(args) - check exitCode == QuitSuccess - check output.contains("forced no") - check output.contains("beta installed successfully") - - test "can develop package from git subdir": - cleanDir "beta" - check execNimbleYes("develop", pkgMultiBetaUrl).exitCode == QuitSuccess - -suite "Module tests": - template moduleTest(moduleName: string) = - test moduleName: - cd "..": - check execCmdEx("nim c -r src/nimblepkg/" & moduleName). - exitCode == QuitSuccess - - moduleTest "aliasthis" - moduleTest "common" - moduleTest "counttables" - moduleTest "download" - moduleTest "jsonhelpers" - moduleTest "packageinfo" - moduleTest "packageparser" - moduleTest "paths" - moduleTest "reversedeps" - moduleTest "sha1hashes" - moduleTest "tools" - moduleTest "topologicalsort" - moduleTest "version" - -suite "nimble run": - beforeSuite() - - test "Invalid binary": - cd "run": - let (output, exitCode) = execNimble( - "--debug", # Flag to enable debug verbosity in Nimble - "run", # Run command invokation - "blahblah", # The command to run - ) - check exitCode == QuitFailure - check output.contains("Binary '$1' is not defined in 'run' package." % - "blahblah".changeFileExt(ExeExt)) - - test "Parameters passed to executable": - cd "run": - let (output, exitCode) = execNimble( - "--debug", # Flag to enable debug verbosity in Nimble - "run", # Run command invokation - "run", # The command to run - "--test", # First argument passed to the executed command - "check" # Second argument passed to the executed command. - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test check" % - [$DirSep, "run".changeFileExt(ExeExt)]) - check output.contains("""Testing `nimble run`: @["--test", "check"]""") - - test "Parameters not passed to single executable": - cd "run": - let (output, exitCode) = execNimble( - "--debug", # Flag to enable debug verbosity in Nimble - "run", # Run command invokation - "--", # Separator for arguments - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - check output.contains("""Testing `nimble run`: @["--test"]""") - - test "Parameters passed to single executable": - cd "run": - let (output, exitCode) = execNimble( - "--debug", # Flag to enable debug verbosity in Nimble - "run", # Run command invokation - "--", # Flag to set run file to "" before next argument - "--test", # First argument passed to the executed command - "check" # Second argument passed to the executed command. - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test check" % - [$DirSep, "run".changeFileExt(ExeExt)]) - check output.contains("""Testing `nimble run`: @["--test", "check"]""") - - test "Executable output is shown even when not debugging": - cd "run": - let (output, exitCode) = - execNimble("run", "run", "--option1", "arg1") - check exitCode == QuitSuccess - check output.contains("""Testing `nimble run`: @["--option1", "arg1"]""") - - test "Quotes and whitespace are well handled": - cd "run": - let (output, exitCode) = execNimble( - "run", "run", "\"", "\'", "\t", "arg with spaces" - ) - check exitCode == QuitSuccess - check output.contains( - """Testing `nimble run`: @["\"", "\'", "\t", "arg with spaces"]""" - ) - - test "Nimble options before executable name": - cd "run": - let (output, exitCode) = execNimble( - "run", # Run command invokation - "--debug", # Flag to enable debug verbosity in Nimble - "run", # The executable to run - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - check output.contains("""Testing `nimble run`: @["--test"]""") - - test "Nimble options before --": - cd "run": - let (output, exitCode) = execNimble( - "run", # Run command invokation - "--debug", # Flag to enable debug verbosity in Nimble - "--", # Separator for arguments - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - check output.contains("""Testing `nimble run`: @["--test"]""") - - test "Compilation flags before run command": - cd "run": - let (output, exitCode) = execNimble( - "-d:sayWhee", # Compile flag to define a conditional symbol - "run", # Run command invokation - "--debug", # Flag to enable debug verbosity in Nimble - "--", # Separator for arguments - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - echo output - check output.contains("""Testing `nimble run`: @["--test"]""") - check output.contains("""Whee!""") - - test "Compilation flags before executable name": - cd "run": - let (output, exitCode) = execNimble( - "--debug", # Flag to enable debug verbosity in Nimble - "run", # Run command invokation - "-d:sayWhee", # Compile flag to define a conditional symbol - "run", # The executable to run - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - echo output - check output.contains("""Testing `nimble run`: @["--test"]""") - check output.contains("""Whee!""") - - test "Compilation flags before --": - cd "run": - let (output, exitCode) = execNimble( - "run", # Run command invokation - "-d:sayWhee", # Compile flag to define a conditional symbol - "--debug", # Flag to enable debug verbosity in Nimble - "--", # Separator for arguments - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - echo output - check output.contains("""Testing `nimble run`: @["--test"]""") - check output.contains("""Whee!""") - - test "Order of compilation flags before and after run command": - cd "run": - let (output, exitCode) = execNimble( - "-d:compileFlagBeforeRunCommand", # Compile flag to define a conditional symbol - "run", # Run command invokation - "-d:sayWhee", # Compile flag to define a conditional symbol - "--debug", # Flag to enable debug verbosity in Nimble - "--", # Separator for arguments - "--test" # First argument passed to the executed command - ) - check exitCode == QuitSuccess - check output.contains("-d:compileFlagBeforeRunCommand -d:sayWhee") - check output.contains("tests$1run$1$2 --test" % - [$DirSep, "run".changeFileExt(ExeExt)]) - echo output - check output.contains("""Testing `nimble run`: @["--test"]""") - check output.contains("""Whee!""") - -suite "project local deps mode": - beforeSuite() - - test "nimbledeps exists": - cd "localdeps": - cleanDir("nimbledeps") - createDir("nimbledeps") - let (output, exitCode) = execCmdEx(nimblePath & " install -y") - check exitCode == QuitSuccess - check output.contains("project local deps mode") - check output.contains("Succeeded") - - test "--localdeps flag": - cd "localdeps": - cleanDir("nimbledeps") - let (output, exitCode) = execCmdEx(nimblePath & " install -y -l") - check exitCode == QuitSuccess - check output.contains("project local deps mode") - check output.contains("Succeeded") - - test "localdeps develop": - cleanDir("packagea") - let (_, exitCode) = execCmdEx(nimblePath & - &" develop {pkgAUrl} --localdeps -y") - check exitCode == QuitSuccess - check dirExists("packagea" / "nimbledeps") - check not dirExists("nimbledeps") - -suite "misc tests": - beforeSuite() - - test "depsOnly + flag order test": - let (output, exitCode) = execNimbleYes("--depsOnly", "install", pkgBin2Url) - check(not output.contains("Success: packagebin2 installed successfully.")) - check exitCode == QuitSuccess - - test "caching of nims and ini detects changes": - cd "caching": - var (output, exitCode) = execNimble("dump") - check output.contains("0.1.0") - let - nfile = "caching.nimble" - writeFile(nfile, readFile(nfile).replace("0.1.0", "0.2.0")) - (output, exitCode) = execNimble("dump") - check output.contains("0.2.0") - writeFile(nfile, readFile(nfile).replace("0.2.0", "0.1.0")) - - # Verify cached .nims runs project dir specific commands correctly - (output, exitCode) = execNimble("testpath") - check exitCode == QuitSuccess - check output.contains("imported") - check output.contains("tests/caching") - check output.contains("copied") - check output.contains("removed") - - test "tasks can be called recursively": - cd "recursive": - check execNimble("recurse").exitCode == QuitSuccess - - test "picks #head when looking for packages": - removeDir installDir - cd "versionClashes" / "aporiaScenario": - let (output, exitCode) = execNimbleYes("install", "--verbose") - checkpoint output - check exitCode == QuitSuccess - check execNimbleYes("remove", "aporiascenario").exitCode == QuitSuccess - check execNimbleYes("remove", "packagea").exitCode == QuitSuccess - - test "pass options to the compiler with `nimble install`": - cd "passNimFlags": - check execNimble("install", "--passNim:-d:passNimIsWorking").exitCode == QuitSuccess - - test "NimbleVersion is defined": - cd "nimbleVersionDefine": - let (output, exitCode) = execNimble("c", "-r", "src/nimbleVersionDefine.nim") - check output.contains("0.1.0") - check exitCode == QuitSuccess - - let (output2, exitCode2) = execNimble("run", "nimbleVersionDefine") - check output2.contains("0.1.0") - check exitCode2 == QuitSuccess - - test "compilation without warnings": - const buildDir = "./buildDir/" - const filesToBuild = [ - "../src/nimble.nim", - "./tester.nim", - ] - - proc execBuild(fileName: string): tuple[output: string, exitCode: int] = - result = execCmdEx( - &"nim c -o:{buildDir/fileName.splitFile.name} {fileName}") - - proc checkOutput(output: string): uint = - const warningsToCheck = [ - "[UnusedImport]", - "[Deprecated]", - "[XDeclaredButNotUsed]", - "[Spacing]", - "[ProveInit]", - "[UnsafeDefault]", - "[ConvFromXtoItselfNotNeeded]", - ] - - for line in output.splitLines(): - for warning in warningsToCheck: - if line.find(warning) != stringNotFound: - once: checkpoint("Detected warnings:") - checkpoint(line) - inc(result) - - removeDir(buildDir) - - var linesWithWarningsCount: uint = 0 - for file in filesToBuild: - let (output, exitCode) = execBuild(file) - check exitCode == QuitSuccess - linesWithWarningsCount += checkOutput(output) - check linesWithWarningsCount == 0 - - test "can update": - check execNimble("update").exitCode == QuitSuccess - - test "can list": - check execNimble("list").exitCode == QuitSuccess - check execNimble("list", "-i").exitCode == QuitSuccess - -suite "issues": - beforeSuite() - - test "issue 801": - cd "issue801": - let (output, exitCode) = execNimbleYes("--debug", "test") - check exitCode == QuitSuccess - - # Verify hooks work - check output.contains("before test") - check output.contains("after test") - - test "issue 799": - # When building, any newly installed packages should be referenced via the - # path that they get permanently installed at. - cleanDir installDir - cd "issue799": - let (output, exitCode) = execNimbleYes("build") - check exitCode == QuitSuccess - var lines = output.processOutput - lines.keepItIf(unindent(it).startsWith("Executing")) - - for line in lines: - if line.contains("issue799"): - let nimbleInstallDir = getPackageDir( - pkgsDir, &"nimble-{nimbleVersion}") - dump(nimbleInstallDir) - let pkgInstalledPath = "--path:'" & nimble_install_dir & "'" - dump(pkgInstalledPath) - check line.contains(pkgInstalledPath) - - test "issue 793": - cd "issue793": - var (output, exitCode) = execNimble("build") - check exitCode == QuitSuccess - check output.contains("before build") - check output.contains("after build") - - # Issue 776 - (output, exitCode) = execNimble("doc", "src/issue793") - check output.contains("before doc") - check output.contains("after doc") - - test "issue 727": - cd "issue727": - var (output, exitCode) = execNimbleYes("--debug", "c", "src/abc") - check exitCode == QuitSuccess - check fileExists(buildTests / "abc".addFileExt(ExeExt)) - check not fileExists("src/def".addFileExt(ExeExt)) - check not fileExists(buildTests / "def".addFileExt(ExeExt)) - - (output, exitCode) = execNimbleYes("--debug", "uninstall", "-i", "timezones") - check exitCode == QuitSuccess - - (output, exitCode) = execNimbleYes("--debug", "run", "def") - check exitCode == QuitSuccess - check output.contains("def727") - check not fileExists("abc".addFileExt(ExeExt)) - check fileExists("def".addFileExt(ExeExt)) - - (output, exitCode) = execNimbleYes("--debug", "uninstall", "-i", "timezones") - check exitCode == QuitSuccess - - test "issue 708": - cd "issue708": - # TODO: We need a way to filter out compiler messages from the messages - # written by our nimble scripts. - let (output, exitCode) = execNimbleYes("install", "--verbose") - check exitCode == QuitSuccess - let lines = output.strip.processOutput() - check(inLines(lines, "hello")) - check(inLines(lines, "hello2")) - - test "do not install single dependency multiple times (#678)": - # for the test to be correct, the tested package and its dependencies must not - # exist in the local cache - removeDir("nimbleDir") - cd "issue678": - testRefresh(): - writeFile(configFile, """ - [PackageList] - name = "local" - path = "$1" - """.unindent % (getCurrentDir() / "packages.json").replace("\\", "\\\\")) - check execNimble(["refresh"]).exitCode == QuitSuccess - let (output, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - let index = output.find("issue678_dependency_1@0.1.0 already exists") - check index == stringNotFound - - test "Passing command line arguments to a task (#633)": - cd "issue633": - let (output, exitCode) = execNimble("testTask", "--testTask") - check exitCode == QuitSuccess - check output.contains("Got it") - - test "error if `bin` is a source file (#597)": - cd "issue597": - let (output, exitCode) = execNimble("build") - check exitCode != QuitSuccess - check output.contains("entry should not be a source file: test.nim") - - test "init does not overwrite existing files (#581)": - createDir("issue581/src") - cd "issue581": - const Src = "echo \"OK\"" - writeFile("src/issue581.nim", Src) - check execNimbleYes("init").exitCode == QuitSuccess - check readFile("src/issue581.nim") == Src - removeDir("issue581") - - test "issue 564": - cd "issue564": - let (_, exitCode) = execNimble("build") - check exitCode == QuitSuccess - - test "issues #280 and #524": - check execNimbleYes("install", "https://github.com/nimble-test/issue280and524.git").exitCode == 0 - - test "issues #308 and #515": - let - ext = when defined(Windows): ExeExt else: "out" - cd "issue308515" / "v1": - var (output, exitCode) = execNimble(["run", "binname", "--silent"]) - check exitCode == QuitSuccess - check output.contains "binname" - - (output, exitCode) = execNimble(["run", "binname-2", "--silent"]) - check exitCode == QuitSuccess - check output.contains "binname-2" - - # Install v1 and check - (output, exitCode) = execNimbleYes(["install", "--verbose"]) - check exitCode == QuitSuccess - check output.contains getPackageDir(pkgsDir, "binname-0.1.0") / - "binname".addFileExt(ext) - check output.contains getPackageDir(pkgsDir, "binname-0.1.0") / - "binname-2" - - (output, exitCode) = execBin("binname") - check exitCode == QuitSuccess - check output.contains "binname 0.1.0" - (output, exitCode) = execBin("binname-2") - check exitCode == QuitSuccess - check output.contains "binname-2 0.1.0" - - cd "issue308515" / "v2": - # Install v2 and check - var (output, exitCode) = execNimbleYes(["install", "--verbose"]) - check exitCode == QuitSuccess - check output.contains getPackageDir(pkgsDir, "binname-0.2.0") / - "binname".addFileExt(ext) - check output.contains getPackageDir(pkgsDir, "binname-0.2.0") / - "binname-2" - - (output, exitCode) = execBin("binname") - check exitCode == QuitSuccess - check output.contains "binname 0.2.0" - (output, exitCode) = execBin("binname-2") - check exitCode == QuitSuccess - check output.contains "binname-2 0.2.0" - - # Uninstall and check v1 back - (output, exitCode) = execNimbleYes("uninstall", "binname@0.2.0") - check exitCode == QuitSuccess - - (output, exitCode) = execBin("binname") - check exitCode == QuitSuccess - check output.contains "binname 0.1.0" - (output, exitCode) = execBin("binname-2") - check exitCode == QuitSuccess - check output.contains "binname-2 0.1.0" - - test "issue 432": - cd "issue432": - check execNimbleYes("install", "--depsOnly").exitCode == QuitSuccess - check execNimbleYes("install", "--depsOnly").exitCode == QuitSuccess - - test "issue #428": - cd "issue428": - # Note: Can't use execNimble because it patches nimbleDir - const localNimbleDir = "./nimbleDir" - cleanDir localNimbleDir - let (_, exitCode) = execCmdEx( - &"{nimblePath} -y --nimbleDir={localNimbleDir} install") - check exitCode == QuitSuccess - let dummyPkgDir = getPackageDir( - localNimbleDir / nimblePackagesDirName, "dummy-0.1.0") - check dummyPkgDir.dirExists - check not (dummyPkgDir / "nimbleDir").dirExists - - test "issue 399": - cd "issue399": - var (output, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - - (output, exitCode) = execBin("subbin") - check exitCode == QuitSuccess - check output.contains("subbin-1") - - test "can pass args with spaces to Nim (#351)": - cd "binaryPackage/v2": - let (output, exitCode) = execCmdEx(nimblePath & - " c -r" & - " -d:myVar=\"string with spaces\"" & - " binaryPackage") - checkpoint output - check exitCode == QuitSuccess - - test "issue #349": - let reservedNames = [ - "CON", - "PRN", - "AUX", - "NUL", - "COM1", - "COM2", - "COM3", - "COM4", - "COM5", - "COM6", - "COM7", - "COM8", - "COM9", - "LPT1", - "LPT2", - "LPT3", - "LPT4", - "LPT5", - "LPT6", - "LPT7", - "LPT8", - "LPT9", - ] - - proc checkName(name: string) = - let (outp, code) = execNimbleYes("init", name) - let msg = outp.strip.processOutput() - check code == QuitFailure - check inLines(msg, - "\"$1\" is an invalid package name: reserved name" % name) - try: - removeFile(name.changeFileExt("nimble")) - removeDir("src") - removeDir("tests") - except OSError: - discard - - for reserved in reservedNames: - checkName(reserved.toUpperAscii()) - checkName(reserved.toLowerAscii()) - - test "issue #338": - cd "issue338": - check execNimbleYes("install").exitCode == QuitSuccess - - test "can distinguish package reading in nimbleDir vs. other dirs (#304)": - cd "issue304" / "package-test": - check execNimble("tasks").exitCode == QuitSuccess - - test "can build with #head and versioned package (#289)": - cleanDir(installDir) - cd "issue289": - check execNimbleYes("install").exitCode == QuitSuccess - - check execNimbleYes(["uninstall", "issue289"]).exitCode == QuitSuccess - check execNimbleYes(["uninstall", "packagea"]).exitCode == QuitSuccess - - test "issue #206": - cd "issue206": - var (output, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - (output, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - - test "can install diamond deps (#184)": - cd "diamond_deps": - cd "d": - check execNimbleYes("install").exitCode == 0 - cd "c": - check execNimbleYes("install").exitCode == 0 - cd "b": - check execNimbleYes("install").exitCode == 0 - cd "a": - # TODO: This doesn't really test anything. But I couldn't quite - # reproduce #184. - let (output, exitCode) = execNimbleYes("install") - checkpoint(output) - check exitCode == 0 - - test "can validate package structure (#144)": - # Test that no warnings are produced for correctly structured packages. - for package in ["a", "b", "c", "validBinary", "softened"]: - cd "packageStructure/" & package: - let (output, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - let lines = output.strip.processOutput() - check(not lines.hasLineStartingWith("Warning:")) - - # Test that warnings are produced for the incorrectly structured packages. - for package in ["x", "y", "z"]: - cd "packageStructure/" & package: - let (output, exitCode) = execNimbleYes("install") - check exitCode == QuitSuccess - let lines = output.strip.processOutput() - checkpoint(output) - case package - of "x": - check lines.hasLineStartingWith( - "Warning: Package 'x' has an incorrect structure. It should" & - " contain a single directory hierarchy for source files," & - " named 'x', but file 'foobar.nim' is in a directory named" & - " 'incorrect' instead.") - of "y": - check lines.hasLineStartingWith( - "Warning: Package 'y' has an incorrect structure. It should" & - " contain a single directory hierarchy for source files," & - " named 'ypkg', but file 'foobar.nim' is in a directory named" & - " 'yWrong' instead.") - of "z": - check lines.hasLineStartingWith( - "Warning: Package 'z' has an incorrect structure. The top level" & - " of the package source directory should contain at most one module," & - " named 'z.nim', but a file named 'incorrect.nim' was found.") - else: - assert false - - test "issue 129 (installing commit hash)": - cleanDir(installDir) - let arguments = @["install", &"{pkgAUrl}@#1f9cb289c89"] - check execNimbleYes(arguments).exitCode == QuitSuccess - # Verify that it was installed correctly. - check packageDirExists(pkgsDir, "PackageA-0.6.0") - # Remove it so that it doesn't interfere with the uninstall tests. - check execNimbleYes("uninstall", "packagea@#1f9cb289c89").exitCode == - QuitSuccess - - test "issue #126": - cd "issue126/a": - let (output, exitCode) = execNimbleYes("install") - let lines = output.strip.processOutput() - check exitCode != QuitSuccess # TODO - check inLines(lines, "issue-126 is an invalid package name: cannot contain '-'") - - cd "issue126/b": - let (output1, exitCode1) = execNimbleYes("install") - let lines1 = output1.strip.processOutput() - check exitCode1 != QuitSuccess - check inLines(lines1, "The .nimble file name must match name specified inside") - - test "issue 113 (uninstallation problems)": - cleanDir(installDir) - - cd "issue113/c": - check execNimbleYes("install").exitCode == QuitSuccess - cd "issue113/b": - check execNimbleYes("install").exitCode == QuitSuccess - cd "issue113/a": - check execNimbleYes("install").exitCode == QuitSuccess - - # Try to remove c. - let - (output, exitCode) = execNimbleYes(["remove", "c"]) - lines = output.strip.processOutput() - pkgBInstallDir = getPackageDir(pkgsDir, "b-0.1.0").splitPath.tail - - check exitCode != QuitSuccess - check lines.inLines(cannotUninstallPkgMsg("c", "0.1.0", @[pkgBInstallDir])) - - check execNimbleYes(["remove", "a"]).exitCode == QuitSuccess - check execNimbleYes(["remove", "b"]).exitCode == QuitSuccess - - cd "issue113/buildfail": - check execNimbleYes("install").exitCode != QuitSuccess - - check execNimbleYes(["remove", "c"]).exitCode == QuitSuccess - - test "issue #108": - cd "issue108": - let (output, exitCode) = execNimble("build") - let lines = output.strip.processOutput() - check exitCode != QuitSuccess - check inLines(lines, "Nothing to build") - -suite "nimble tasks": - beforeSuite() - - test "can list tasks even with no tasks defined in nimble file": - cd "tasks/empty": - let (_, exitCode) = execNimble("tasks") - check exitCode == QuitSuccess - - test "tasks with no descriptions are correctly displayed": - cd "tasks/nodesc": - let (output, exitCode) = execNimble("tasks") - check output.contains("nodesc") - check exitCode == QuitSuccess - - test "task descriptions are correctly aligned to longer name": - cd "tasks/max": - let (output, exitCode) = execNimble("tasks") - check output.contains("task1 Description1") - check output.contains("very_long_task This is a task with a long name") - check output.contains("aaa A task with a small name") - check exitCode == QuitSuccess - - test "task descriptions are correctly aligned to minimum (10 chars)": - cd "tasks/min": - let (output, exitCode) = execNimble("tasks") - check output.contains("a Description for a") - check exitCode == QuitSuccess +# import suites +import tnimblerefresh +import tnimscript +import tuninstall +import tnimbledump +import tnimbletasks +import ttwobinaryversions +import treversedeps +import tdevelopfeature +import tpathcommand +import ttestcommand +import tcheckcommand +import tmultipkgs +import tmoduletests +import truncommand +import tlocaldeps +import tmisctests +import tissues diff --git a/tests/testscommon.nim b/tests/testscommon.nim new file mode 100644 index 00000000..020945ba --- /dev/null +++ b/tests/testscommon.nim @@ -0,0 +1,190 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +import sequtils, strutils, strformat, os, osproc, sugar, unittest, macros, + std/sha1 + +from nimblepkg/common import cd, nimblePackagesDirName, ProcessOutput +from nimblepkg/developfile import developFileVersion + +const + stringNotFound* = -1 + pkgAUrl* = "https://github.com/nimble-test/packagea.git" + pkgBUrl* = "https://github.com/nimble-test/packageb.git" + pkgBinUrl* = "https://github.com/nimble-test/packagebin.git" + pkgBin2Url* = "https://github.com/nimble-test/packagebin2.git" + pkgMultiUrl = "https://github.com/nimble-test/multi" + pkgMultiAlphaUrl* = &"{pkgMultiUrl}?subdir=alpha" + pkgMultiBetaUrl* = &"{pkgMultiUrl}?subdir=beta" + +let + rootDir = getCurrentDir().parentDir + nimblePath* = rootDir / "src" / addFileExt("nimble", ExeExt) + installDir* = rootDir / "tests" / "nimbleDir" + buildTests* = rootDir / "buildTests" + pkgsDir* = installDir / nimblePackagesDirName + +proc execNimble*(args: varargs[string]): ProcessOutput = + var quotedArgs = @args + quotedArgs.insert("--nimbleDir:" & installDir) + quotedArgs.insert(nimblePath) + quotedArgs = quotedArgs.map((x: string) => x.quoteShell) + + let path {.used.} = getCurrentDir().parentDir() / "src" + + var cmd = + when not defined(windows): + "PATH=" & path & ":$PATH " & quotedArgs.join(" ") + else: + quotedArgs.join(" ") + when defined(macosx): + # TODO: Yeah, this is really specific to my machine but for my own sanity... + cmd = "DYLD_LIBRARY_PATH=/usr/local/opt/openssl@1.1/lib " & cmd + + result = execCmdEx(cmd) + checkpoint(cmd) + checkpoint(result.output) + +proc execNimbleYes*(args: varargs[string]): ProcessOutput = + # issue #6314 + execNimble(@args & "-y") + +proc execBin*(name: string): tuple[output: string, exitCode: int] = + var + cmd = installDir / "bin" / name + + when defined(windows): + cmd = "cmd /c " & cmd & ".cmd" + + result = execCmdEx(cmd) + +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 and + "Using env var NIM_LIB_PREFIX" notin x + ) + ) + +macro defineInLinesProc(procName, extraLine: untyped): untyped = + var LinesType = quote do: seq[string] + if extraLine[0].kind != nnkDiscardStmt: + LinesType = newTree(nnkVarTy, LinesType) + + let linesParam = ident("lines") + let linesLoopCounter = ident("i") + + result = quote do: + proc `procName`*(`linesParam`: `LinesType`, msg: string): bool = + let msgLines = msg.splitLines + for msgLine in msgLines: + let msgLine = msgLine.normalize + var msgLineFound = false + for `linesLoopCounter`, line in `linesParam`: + if msgLine in line.normalize: + msgLineFound = true + `extraLine` + break + if not msgLineFound: + return false + return true + +defineInLinesProc(inLines): discard +defineInLinesProc(inLinesOrdered): lines = lines[i + 1 .. ^1] + +proc hasLineStartingWith*(lines: seq[string], prefix: string): bool = + for line in lines: + if line.strip(trailing = false).startsWith(prefix): + return true + return false + +proc getPackageDir*(pkgCacheDir, pkgDirPrefix: string, fullPath = true): string = + for kind, dir in walkDir(pkgCacheDir): + if kind != pcDir or not dir.startsWith(pkgCacheDir / pkgDirPrefix): + continue + let pkgChecksumStartIndex = dir.rfind('-') + if pkgChecksumStartIndex == -1: + continue + let pkgChecksum = dir[pkgChecksumStartIndex + 1 .. ^1] + if pkgChecksum.isValidSha1Hash(): + return if fullPath: dir else: dir.splitPath.tail + return "" + +proc packageDirExists*(pkgCacheDir, pkgDirPrefix: string): bool = + getPackageDir(pkgCacheDir, pkgDirPrefix).len > 0 + +proc safeMoveFile(src, dest: string) = + try: + moveFile(src, dest) + except OSError: + copyFile(src, dest) + removeFile(src) + +template testRefresh*(body: untyped) = + # Backup current config + let configFile {.inject.} = getConfigDir() / "nimble" / "nimble.ini" + let configBakFile = getConfigDir() / "nimble" / "nimble.ini.bak" + if fileExists(configFile): + safeMoveFile(configFile, configBakFile) + + # Ensure config dir exists + createDir(getConfigDir() / "nimble") + + body + + # Restore config + if fileExists(configBakFile): + safeMoveFile(configBakFile, configFile) + +template usePackageListFile*(fileName: string, body: untyped) = + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + path = "$1" + """.unindent % (fileName).replace("\\", "\\\\")) + check execNimble(["refresh"]).exitCode == QuitSuccess + body + +template cleanFile*(fileName: string) = + removeFile fileName + defer: removeFile fileName + +macro cleanFiles*(fileNames: varargs[string]) = + result = newStmtList() + for fn in fileNames: + result.add quote do: cleanFile(`fn`) + +template cleanDir*(dirName: string) = + removeDir dirName + defer: removeDir dirName + +template createTempDir*(dirName: string) = + createDir dirName + defer: removeDir dirName + +template cdCleanDir*(dirName: string, body: untyped) = + cleanDir dirName + createDir dirName + cd dirName: + body + +proc filesList(filesNames: seq[string]): string = + for fileName in filesNames: + result.addQuoted fileName + result.add ',' + +proc developFile*(includes: seq[string], dependencies: seq[string]): string = + result = """{"version":"$#","includes":[$#],"dependencies":[$#]}""" % + [developFileVersion, filesList(includes), filesList(dependencies)] + +# Set env var to propagate nimble binary path +putEnv("NIMBLE_TEST_BINARY_PATH", nimblePath) + +# Always recompile. +doAssert execCmdEx("nim c " & nimblePath).exitCode == QuitSuccess diff --git a/tests/tissues.nim b/tests/tissues.nim new file mode 100644 index 00000000..790dab20 --- /dev/null +++ b/tests/tissues.nim @@ -0,0 +1,390 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, osproc, strutils, sequtils, strformat +import testscommon +from nimblepkg/common import cd, nimbleVersion, nimblePackagesDirName +from nimblepkg/displaymessages import cannotUninstallPkgMsg + +suite "issues": + test "issue 801": + cd "issue801": + let (output, exitCode) = execNimbleYes("test") + check exitCode == QuitSuccess + + # Verify hooks work + check output.contains("before test") + check output.contains("after test") + + test "issue 799": + # When building, any newly installed packages should be referenced via the + # path that they get permanently installed at. + cleanDir installDir + cd "issue799": + let (output, exitCode) = execNimbleYes("build") + check exitCode == QuitSuccess + var lines = output.processOutput + lines.keepItIf(unindent(it).startsWith("Executing")) + + for line in lines: + if line.contains("issue799"): + let nimbleInstallDir = getPackageDir( + pkgsDir, &"nimble-{nimbleVersion}") + let pkgInstalledPath = "--path:'" & nimble_install_dir & "'" + check line.contains(pkgInstalledPath) + + test "issue 793": + cd "issue793": + var (output, exitCode) = execNimble("build") + check exitCode == QuitSuccess + check output.contains("before build") + check output.contains("after build") + + # Issue 776 + (output, exitCode) = execNimble("doc", "src/issue793") + check output.contains("before doc") + check output.contains("after doc") + + test "issue 727": + cd "issue727": + var (output, exitCode) = execNimbleYes("c", "src/abc") + check exitCode == QuitSuccess + check fileExists(buildTests / "abc".addFileExt(ExeExt)) + check not fileExists("src/def".addFileExt(ExeExt)) + check not fileExists(buildTests / "def".addFileExt(ExeExt)) + + (output, exitCode) = execNimbleYes("uninstall", "-i", "timezones") + check exitCode == QuitSuccess + + (output, exitCode) = execNimbleYes("run", "def") + check exitCode == QuitSuccess + check output.contains("def727") + check not fileExists("abc".addFileExt(ExeExt)) + check fileExists("def".addFileExt(ExeExt)) + + (output, exitCode) = execNimbleYes("uninstall", "-i", "timezones") + check exitCode == QuitSuccess + + test "issue 708": + cd "issue708": + # TODO: We need a way to filter out compiler messages from the messages + # written by our nimble scripts. + let (output, exitCode) = execNimbleYes("install", "--verbose") + check exitCode == QuitSuccess + let lines = output.strip.processOutput() + check(inLines(lines, "hello")) + check(inLines(lines, "hello2")) + + test "do not install single dependency multiple times (#678)": + # for the test to be correct, the tested package and its dependencies must not + # exist in the local cache + removeDir("nimbleDir") + cd "issue678": + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + path = "$1" + """.unindent % (getCurrentDir() / "packages.json").replace("\\", "\\\\")) + check execNimble(["refresh"]).exitCode == QuitSuccess + let (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + let index = output.find("issue678_dependency_1@0.1.0 already exists") + check index == stringNotFound + + test "Passing command line arguments to a task (#633)": + cd "issue633": + let (output, exitCode) = execNimble("testTask", "--testTask") + check exitCode == QuitSuccess + check output.contains("Got it") + + test "error if `bin` is a source file (#597)": + cd "issue597": + var (output, exitCode) = execNimble("build") + check exitCode != QuitSuccess + check output.contains("entry should not be a source file: test.nim") + + test "init does not overwrite existing files (#581)": + createDir("issue581/src") + cd "issue581": + const Src = "echo \"OK\"" + writeFile("src/issue581.nim", Src) + check execNimbleYes("init").exitCode == QuitSuccess + check readFile("src/issue581.nim") == Src + removeDir("issue581") + + test "issue 564": + cd "issue564": + let (_, exitCode) = execNimble("build") + check exitCode == QuitSuccess + + test "issues #280 and #524": + check execNimbleYes("install", + "https://github.com/nimble-test/issue280and524.git").exitCode == 0 + + test "issues #308 and #515": + let + ext = when defined(Windows): ExeExt else: "out" + cd "issue308515" / "v1": + var (output, exitCode) = execNimble(["run", "binname", "--silent"]) + check exitCode == QuitSuccess + check output.contains "binname" + + (output, exitCode) = execNimble(["run", "binname-2", "--silent"]) + check exitCode == QuitSuccess + check output.contains "binname-2" + + # Install v1 and check + (output, exitCode) = execNimbleYes(["install", "--verbose"]) + check exitCode == QuitSuccess + check output.contains getPackageDir(pkgsDir, "binname-0.1.0") / + "binname".addFileExt(ext) + check output.contains getPackageDir(pkgsDir, "binname-0.1.0") / + "binname-2" + + (output, exitCode) = execBin("binname") + check exitCode == QuitSuccess + check output.contains "binname 0.1.0" + (output, exitCode) = execBin("binname-2") + check exitCode == QuitSuccess + check output.contains "binname-2 0.1.0" + + cd "issue308515" / "v2": + # Install v2 and check + var (output, exitCode) = execNimbleYes(["install", "--verbose"]) + check exitCode == QuitSuccess + check output.contains getPackageDir(pkgsDir, "binname-0.2.0") / + "binname".addFileExt(ext) + check output.contains getPackageDir(pkgsDir, "binname-0.2.0") / + "binname-2" + + (output, exitCode) = execBin("binname") + check exitCode == QuitSuccess + check output.contains "binname 0.2.0" + (output, exitCode) = execBin("binname-2") + check exitCode == QuitSuccess + check output.contains "binname-2 0.2.0" + + # Uninstall and check v1 back + (output, exitCode) = execNimbleYes("uninstall", "binname@0.2.0") + check exitCode == QuitSuccess + + (output, exitCode) = execBin("binname") + check exitCode == QuitSuccess + check output.contains "binname 0.1.0" + (output, exitCode) = execBin("binname-2") + check exitCode == QuitSuccess + check output.contains "binname-2 0.1.0" + + test "issue 432": + cd "issue432": + check execNimbleYes("install", "--depsOnly").exitCode == QuitSuccess + check execNimbleYes("install", "--depsOnly").exitCode == QuitSuccess + + test "issue #428": + cd "issue428": + # Note: Can't use execNimble because it patches nimbleDir + const localNimbleDir = "./nimbleDir" + cleanDir localNimbleDir + let (_, exitCode) = execCmdEx( + &"{nimblePath} -y --nimbleDir={localNimbleDir} install") + check exitCode == QuitSuccess + let dummyPkgDir = getPackageDir( + localNimbleDir / nimblePackagesDirName, "dummy-0.1.0") + check dummyPkgDir.dirExists + check not (dummyPkgDir / "nimbleDir").dirExists + + test "issue 399": + cd "issue399": + var (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + + (output, exitCode) = execBin("subbin") + check exitCode == QuitSuccess + check output.contains("subbin-1") + + test "can pass args with spaces to Nim (#351)": + cd "binaryPackage/v2": + let (output, exitCode) = execCmdEx(nimblePath & + " c -r" & + " -d:myVar=\"string with spaces\"" & + " binaryPackage") + checkpoint output + check exitCode == QuitSuccess + + test "issue #349": + let reservedNames = [ + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + ] + + proc checkName(name: string) = + let (outp, code) = execNimbleYes("init", name) + let msg = outp.strip.processOutput() + check code == QuitFailure + check inLines(msg, + "\"$1\" is an invalid package name: reserved name" % name) + try: + removeFile(name.changeFileExt("nimble")) + removeDir("src") + removeDir("tests") + except OSError: + discard + + for reserved in reservedNames: + checkName(reserved.toUpperAscii()) + checkName(reserved.toLowerAscii()) + + test "issue #338": + cd "issue338": + check execNimbleYes("install").exitCode == QuitSuccess + + test "can distinguish package reading in nimbleDir vs. other dirs (#304)": + cd "issue304" / "package-test": + check execNimble("tasks").exitCode == QuitSuccess + + test "can build with #head and versioned package (#289)": + cleanDir(installDir) + cd "issue289": + check execNimbleYes("install").exitCode == QuitSuccess + + check execNimbleYes(["uninstall", "issue289"]).exitCode == QuitSuccess + check execNimbleYes(["uninstall", "packagea"]).exitCode == QuitSuccess + + test "issue #206": + cd "issue206": + var (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + + test "can install diamond deps (#184)": + cd "diamond_deps": + cd "d": + check execNimbleYes("install").exitCode == 0 + cd "c": + check execNimbleYes("install").exitCode == 0 + cd "b": + check execNimbleYes("install").exitCode == 0 + cd "a": + # TODO: This doesn't really test anything. But I couldn't quite + # reproduce #184. + let (output, exitCode) = execNimbleYes("install") + checkpoint(output) + check exitCode == 0 + + test "can validate package structure (#144)": + # Test that no warnings are produced for correctly structured packages. + for package in ["a", "b", "c", "validBinary", "softened"]: + cd "packageStructure/" & package: + let (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + let lines = output.strip.processOutput() + check(not lines.hasLineStartingWith("Warning:")) + + # Test that warnings are produced for the incorrectly structured packages. + for package in ["x", "y", "z"]: + cd "packageStructure/" & package: + let (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + let lines = output.strip.processOutput() + checkpoint(output) + case package + of "x": + check lines.hasLineStartingWith( + "Warning: Package 'x' has an incorrect structure. It should" & + " contain a single directory hierarchy for source files," & + " named 'x', but file 'foobar.nim' is in a directory named" & + " 'incorrect' instead.") + of "y": + check lines.hasLineStartingWith( + "Warning: Package 'y' has an incorrect structure. It should" & + " contain a single directory hierarchy for source files," & + " named 'ypkg', but file 'foobar.nim' is in a directory named" & + " 'yWrong' instead.") + of "z": + check lines.hasLineStartingWith( + "Warning: Package 'z' has an incorrect structure. The top level" & + " of the package source directory should contain at most one module," & + " named 'z.nim', but a file named 'incorrect.nim' was found.") + else: + assert false + + test "issue 129 (installing commit hash)": + cleanDir(installDir) + let arguments = @["install", &"{pkgAUrl}@#1f9cb289c89"] + check execNimbleYes(arguments).exitCode == QuitSuccess + # Verify that it was installed correctly. + check packageDirExists(pkgsDir, "PackageA-0.6.0") + # Remove it so that it doesn't interfere with the uninstall tests. + check execNimbleYes("uninstall", "packagea@#1f9cb289c89").exitCode == + QuitSuccess + + test "issue #126": + cd "issue126/a": + let (output, exitCode) = execNimbleYes("install") + let lines = output.strip.processOutput() + check exitCode != QuitSuccess # TODO + check inLines(lines, "issue-126 is an invalid package name: cannot contain '-'") + + cd "issue126/b": + let (output1, exitCode1) = execNimbleYes("install") + let lines1 = output1.strip.processOutput() + check exitCode1 != QuitSuccess + check inLines(lines1, "The .nimble file name must match name specified inside") + + test "issue 113 (uninstallation problems)": + cleanDir(installDir) + + cd "issue113/c": + check execNimbleYes("install").exitCode == QuitSuccess + cd "issue113/b": + check execNimbleYes("install").exitCode == QuitSuccess + cd "issue113/a": + check execNimbleYes("install").exitCode == QuitSuccess + + # Try to remove c. + let + (output, exitCode) = execNimbleYes(["remove", "c"]) + lines = output.strip.processOutput() + pkgBInstallDir = getPackageDir(pkgsDir, "b-0.1.0").splitPath.tail + + check exitCode != QuitSuccess + check lines.inLines(cannotUninstallPkgMsg("c", "0.1.0", @[pkgBInstallDir])) + + check execNimbleYes(["remove", "a"]).exitCode == QuitSuccess + check execNimbleYes(["remove", "b"]).exitCode == QuitSuccess + + cd "issue113/buildfail": + check execNimbleYes("install").exitCode != QuitSuccess + + check execNimbleYes(["remove", "c"]).exitCode == QuitSuccess + + test "issue #108": + cd "issue108": + let (output, exitCode) = execNimble("build") + let lines = output.strip.processOutput() + check exitCode != QuitSuccess + check inLines(lines, "Nothing to build") diff --git a/tests/tlocaldeps.nim b/tests/tlocaldeps.nim new file mode 100644 index 00000000..d8048222 --- /dev/null +++ b/tests/tlocaldeps.nim @@ -0,0 +1,34 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, osproc, strutils, strformat +import testscommon +from nimblepkg/common import cd + +suite "project local deps mode": + test "nimbledeps exists": + cd "localdeps": + cleanDir("nimbledeps") + createDir("nimbledeps") + let (output, exitCode) = execCmdEx(nimblePath & " install -y") + check exitCode == QuitSuccess + check output.contains("project local deps mode") + check output.contains("Succeeded") + + test "--localdeps flag": + cd "localdeps": + cleanDir("nimbledeps") + let (output, exitCode) = execCmdEx(nimblePath & " install -y -l") + check exitCode == QuitSuccess + check output.contains("project local deps mode") + check output.contains("Succeeded") + + test "localdeps develop": + cleanDir("packagea") + let (_, exitCode) = execCmdEx(nimblePath & + &" develop {pkgAUrl} --localdeps -y") + check exitCode == QuitSuccess + check dirExists("packagea" / "nimbledeps") + check not dirExists("nimbledeps") diff --git a/tests/tmisctests.nim b/tests/tmisctests.nim new file mode 100644 index 00000000..bd11ec5b --- /dev/null +++ b/tests/tmisctests.nim @@ -0,0 +1,106 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, osproc, strutils, strformat +import testscommon +from nimblepkg/common import cd, nimbleVersion, nimblePackagesDirName + +suite "misc tests": + test "depsOnly + flag order test": + let (output, exitCode) = execNimbleYes("--depsOnly", "install", pkgBin2Url) + check(not output.contains("Success: packagebin2 installed successfully.")) + check exitCode == QuitSuccess + + test "caching of nims and ini detects changes": + cd "caching": + var (output, exitCode) = execNimble("dump") + check output.contains("0.1.0") + let + nfile = "caching.nimble" + writeFile(nfile, readFile(nfile).replace("0.1.0", "0.2.0")) + (output, exitCode) = execNimble("dump") + check output.contains("0.2.0") + writeFile(nfile, readFile(nfile).replace("0.2.0", "0.1.0")) + + # Verify cached .nims runs project dir specific commands correctly + (output, exitCode) = execNimble("testpath") + check exitCode == QuitSuccess + check output.contains("imported") + check output.contains("tests/caching") + check output.contains("copied") + check output.contains("removed") + + test "tasks can be called recursively": + cd "recursive": + check execNimble("recurse").exitCode == QuitSuccess + + test "picks #head when looking for packages": + removeDir installDir + cd "versionClashes" / "aporiaScenario": + let (output, exitCode) = execNimbleYes("install", "--verbose") + checkpoint output + check exitCode == QuitSuccess + check execNimbleYes("remove", "aporiascenario").exitCode == QuitSuccess + check execNimbleYes("remove", "packagea").exitCode == QuitSuccess + + test "pass options to the compiler with `nimble install`": + cd "passNimFlags": + let (_, exitCode) = execNimble("install", "--passNim:-d:passNimIsWorking") + check exitCode == QuitSuccess + + test "NimbleVersion is defined": + cd "nimbleVersionDefine": + let (output, exitCode) = execNimble("c", "-r", "src/nimbleVersionDefine.nim") + check output.contains("0.1.0") + check exitCode == QuitSuccess + + let (output2, exitCode2) = execNimble("run", "nimbleVersionDefine") + check output2.contains("0.1.0") + check exitCode2 == QuitSuccess + + test "compilation without warnings": + const buildDir = "./buildDir/" + const filesToBuild = [ + "../src/nimble.nim", + #"../src/nimblepkg/nimscriptapi.nim", + "./tester.nim", + ] + + proc execBuild(fileName: string): tuple[output: string, exitCode: int] = + result = execCmdEx( + &"nim c -o:{buildDir/fileName.splitFile.name} {fileName}") + + proc checkOutput(output: string): uint = + const warningsToCheck = [ + "[UnusedImport]", + "[Deprecated]", + "[XDeclaredButNotUsed]", + "[Spacing]", + "[ProveInit]", + "[UnsafeDefault]", + ] + + for line in output.splitLines(): + for warning in warningsToCheck: + if line.find(warning) != stringNotFound: + once: checkpoint("Detected warnings:") + checkpoint(line) + inc(result) + + removeDir(buildDir) + + var linesWithWarningsCount: uint = 0 + for file in filesToBuild: + let (output, exitCode) = execBuild(file) + check exitCode == QuitSuccess + linesWithWarningsCount += checkOutput(output) + check linesWithWarningsCount == 0 + + test "can update": + check execNimble("update").exitCode == QuitSuccess + + test "can list": + check execNimble("list").exitCode == QuitSuccess + check execNimble("list", "-i").exitCode == QuitSuccess diff --git a/tests/tmoduletests.nim b/tests/tmoduletests.nim new file mode 100644 index 00000000..fd2369b3 --- /dev/null +++ b/tests/tmoduletests.nim @@ -0,0 +1,28 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, osproc +from nimblepkg/common import cd + +suite "Module tests": + template moduleTest(moduleName: string) = + test moduleName: + cd "..": + check execCmdEx("nim c -r src/nimblepkg/" & moduleName). + exitCode == QuitSuccess + + moduleTest "aliasthis" + moduleTest "common" + moduleTest "counttables" + moduleTest "download" + moduleTest "jsonhelpers" + moduleTest "packageinfo" + moduleTest "packageparser" + moduleTest "paths" + moduleTest "reversedeps" + moduleTest "sha1hashes" + moduleTest "tools" + moduleTest "topologicalsort" + moduleTest "version" diff --git a/tests/tmultipkgs.nim b/tests/tmultipkgs.nim new file mode 100644 index 00000000..abcee9a1 --- /dev/null +++ b/tests/tmultipkgs.nim @@ -0,0 +1,25 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, strutils +import testscommon + +suite "multi": + test "can install package from git subdir": + var + args = @["install", pkgMultiAlphaUrl] + (output, exitCode) = execNimbleYes(args) + check exitCode == QuitSuccess + + # Issue 785 + args.add @[pkgMultiBetaUrl, "-n"] + (output, exitCode) = execNimble(args) + check exitCode == QuitSuccess + check output.contains("forced no") + check output.contains("beta installed successfully") + + test "can develop package from git subdir": + cleanDir "beta" + check execNimbleYes("develop", pkgMultiBetaUrl).exitCode == QuitSuccess diff --git a/tests/tnimbledump.nim b/tests/tnimbledump.nim new file mode 100644 index 00000000..9e1fab61 --- /dev/null +++ b/tests/tnimbledump.nim @@ -0,0 +1,85 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os +import testscommon +from nimblepkg/common import cd + +suite "nimble dump": + test "can dump for current project": + cd "testdump": + let (outp, exitCode) = execNimble("dump") + check: exitCode == 0 + check: outp.processOutput.inLines("desc: \"Test package for dump command\"") + + test "can dump for project directory": + let (outp, exitCode) = execNimble("dump", "testdump") + check: exitCode == 0 + check: outp.processOutput.inLines("desc: \"Test package for dump command\"") + + test "can dump for project file": + let (outp, exitCode) = execNimble("dump", "testdump" / "testdump.nimble") + check: exitCode == 0 + check: outp.processOutput.inLines("desc: \"Test package for dump command\"") + + test "can dump for installed package": + cd "testdump": + check: execNimbleYes("install").exitCode == 0 + defer: + discard execNimbleYes("remove", "testdump") + + # Otherwise we might find subdirectory instead + cd "..": + let (outp, exitCode) = execNimble("dump", "testdump") + check: exitCode == 0 + check: outp.processOutput.inLines("desc: \"Test package for dump command\"") + + test "can dump when explicitly asking for INI format": + const outpExpected = """ +name: "testdump" +version: "0.1.0" +author: "nigredo-tori" +desc: "Test package for dump command" +license: "BSD" +skipDirs: "" +skipFiles: "" +skipExt: "" +installDirs: "" +installFiles: "" +installExt: "" +requires: "" +bin: "" +binDir: "" +srcDir: "" +backend: "c" +""" + let (outp, exitCode) = execNimble("dump", "--ini", "testdump") + check: exitCode == 0 + check: outp == outpExpected + + test "can dump in JSON format": + const outpExpected = """ +{ + "name": "testdump", + "version": "0.1.0", + "author": "nigredo-tori", + "desc": "Test package for dump command", + "license": "BSD", + "skipDirs": [], + "skipFiles": [], + "skipExt": [], + "installDirs": [], + "installFiles": [], + "installExt": [], + "requires": [], + "bin": [], + "binDir": "", + "srcDir": "", + "backend": "c" +} +""" + let (outp, exitCode) = execNimble("dump", "--json", "testdump") + check: exitCode == 0 + check: outp == outpExpected diff --git a/tests/tnimblerefresh.nim b/tests/tnimblerefresh.nim new file mode 100644 index 00000000..187c8601 --- /dev/null +++ b/tests/tnimblerefresh.nim @@ -0,0 +1,78 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils +import testscommon + +suite "nimble refresh": + test "can refresh with default urls": + let (output, exitCode) = execNimble(["refresh"]) + checkpoint(output) + check exitCode == QuitSuccess + + test "can refresh with custom urls": + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "official" + url = "http://google.com" + url = "http://google.com/404" + url = "http://irclogs.nim-lang.org/packages.json" + url = "http://nim-lang.org/nimble/packages.json" + url = "https://github.com/nim-lang/packages/raw/master/packages.json" + """.unindent) + + let (output, exitCode) = execNimble(["refresh", "--verbose"]) + checkpoint(output) + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "config file at") + check inLines(lines, "official package list") + check inLines(lines, "http://google.com") + check inLines(lines, "packages.json file is invalid") + check inLines(lines, "404 not found") + check inLines(lines, "Package list downloaded.") + + test "can refresh with local package list": + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + path = "$1" + """.unindent % (getCurrentDir() / "issue368" / "packages.json").replace( + "\\", "\\\\")) + let (output, exitCode) = execNimble(["refresh", "--verbose"]) + let lines = output.strip.processOutput() + check inLines(lines, "config file at") + check inLines(lines, "Copying") + check inLines(lines, "Package list copied.") + check exitCode == QuitSuccess + + test "package list source required": + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + """) + let (output, exitCode) = execNimble(["refresh", "--verbose"]) + let lines = output.strip.processOutput() + check inLines(lines, "config file at") + check inLines(lines, "Package list 'local' requires either url or path") + check exitCode == QuitFailure + + test "package list can only have one source": + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + path = "$1" + url = "http://nim-lang.org/nimble/packages.json" + """) + let (output, exitCode) = execNimble(["refresh", "--verbose"]) + let lines = output.strip.processOutput() + check inLines(lines, "config file at") + check inLines(lines, "Attempted to specify `url` and `path` for the " & + "same package list 'local'") + check exitCode == QuitFailure diff --git a/tests/tnimbletasks.nim b/tests/tnimbletasks.nim new file mode 100644 index 00000000..7466459a --- /dev/null +++ b/tests/tnimbletasks.nim @@ -0,0 +1,34 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, strutils, os +import testscommon +from nimblepkg/common import cd + +suite "nimble tasks": + test "can list tasks even with no tasks defined in nimble file": + cd "tasks/empty": + let (_, exitCode) = execNimble("tasks") + check exitCode == QuitSuccess + + test "tasks with no descriptions are correctly displayed": + cd "tasks/nodesc": + let (output, exitCode) = execNimble("tasks") + check output.contains("nodesc") + check exitCode == QuitSuccess + + test "task descriptions are correctly aligned to longer name": + cd "tasks/max": + let (output, exitCode) = execNimble("tasks") + check output.contains("task1 Description1") + check output.contains("very_long_task This is a task with a long name") + check output.contains("aaa A task with a small name") + check exitCode == QuitSuccess + + test "task descriptions are correctly aligned to minimum (10 chars)": + cd "tasks/min": + let (output, exitCode) = execNimble("tasks") + check output.contains("a Description for a") + check exitCode == QuitSuccess diff --git a/tests/tnimscript.nim b/tests/tnimscript.nim new file mode 100644 index 00000000..26f79934 --- /dev/null +++ b/tests/tnimscript.nim @@ -0,0 +1,115 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils +import testscommon +from nimblepkg/common import cd + +suite "nimscript": + test "can install nimscript package": + cd "nimscript": + let + nim = findExe("nim").relativePath(base = getCurrentDir()) + check execNimbleYes(["install", "--nim:" & nim]).exitCode == QuitSuccess + + test "before/after install pkg dirs are correct": + cd "nimscript": + let (output, exitCode) = execNimbleYes(["install", "--nim:nim"]) + check exitCode == QuitSuccess + check output.contains("Before build") + check output.contains("After build") + let lines = output.strip.processOutput() + for line in lines: + if lines[3].startsWith("Before PkgDir:"): + check line.endsWith("tests" / "nimscript") + check lines[^1].startsWith("After PkgDir:") + let packageDir = getPackageDir(pkgsDir, "nimscript-0.1.0") + check lines[^1].endsWith(packageDir) + + test "before/after on build": + cd "nimscript": + let (output, exitCode) = execNimble([ + "build", "--nim:" & findExe("nim"), "--silent"]) + check exitCode == QuitSuccess + check output.contains("Before build") + check output.contains("After build") + check not output.contains("Verifying") + + test "can execute nimscript tasks": + cd "nimscript": + let (output, exitCode) = execNimble("work") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check lines[^1] == "10" + + test "can use nimscript's setCommand": + cd "nimscript": + let (output, exitCode) = execNimble("cTest") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check "Execution finished".normalize in lines[^1].normalize + + test "can use nimscript's setCommand with flags": + cd "nimscript": + let (output, exitCode) = execNimble("--debug", "cr") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "Hello World") + + test "can use nimscript with repeated flags (issue #329)": + cd "nimscript": + let (output, exitCode) = execNimble("--debug", "repeated") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + var found = false + for line in lines: + if line.contains("--define:foo"): + found = true + check found == true + + test "can list nimscript tasks": + cd "nimscript": + let (output, exitCode) = execNimble("tasks") + check "work".normalize in output.normalize + check "test description".normalize in output.normalize + check exitCode == QuitSuccess + + test "can use pre/post hooks": + cd "nimscript": + let (output, exitCode) = execNimble("hooks") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "First") + check inLines(lines, "middle") + check inLines(lines, "last") + + test "pre hook can prevent action": + cd "nimscript": + let (output, exitCode) = execNimble("hooks2") + let lines = output.strip.processOutput() + check exitCode == QuitFailure + check(not inLines(lines, "Shouldn't happen")) + check inLines(lines, "Hook prevented further execution") + + test "nimble script api": + cd "nimscript": + let (output, exitCode) = execNimble("api") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "PKG_DIR: " & getCurrentDir()) + check inLines(lines, "thisDir: " & getCurrentDir()) + + test "nimscript evaluation error message": + cd "invalidPackage": + let (output, exitCode) = execNimble("check") + let lines = output.strip.processOutput() + check(lines.inLines( + "undeclared identifier: 'thisFieldDoesNotExist'")) + check exitCode == QuitFailure + + test "can accept short flags (#329)": + cd "nimscript": + let (_, exitCode) = execNimble("c", "-d:release", "nimscript.nim") + check exitCode == QuitSuccess diff --git a/tests/tpathcommand.nim b/tests/tpathcommand.nim new file mode 100644 index 00000000..e98cbf8b --- /dev/null +++ b/tests/tpathcommand.nim @@ -0,0 +1,29 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils +import testscommon +from nimblepkg/common import cd + +suite "path command": + test "can get correct path for srcDir (#531)": + cd "develop/srcdirtest": + let (_, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + let (output, _) = execNimble("path", "srcdirtest") + let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") + check output.strip() == packageDir + + # test "nimble path points to develop": + # cd "develop/srcdirtest": + # var (output, exitCode) = execNimble("develop") + # checkpoint output + # check exitCode == QuitSuccess + + # (output, exitCode) = execNimble("path", "srcdirtest") + + # checkpoint output + # check exitCode == QuitSuccess + # check output.strip() == getCurrentDir() / "src" diff --git a/tests/treversedeps.nim b/tests/treversedeps.nim new file mode 100644 index 00000000..ece25f6b --- /dev/null +++ b/tests/treversedeps.nim @@ -0,0 +1,84 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils, json +import testscommon + +from nimblepkg/common import cd +from nimblepkg/nimbledatafile import loadNimbleData + +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 "revdep fail test": + cd "revdep/mydep": + verify execNimbleYes("install") + + cd "revdep/pkgWithDep": + verify execNimbleYes("install") + + let (output, exitCode) = execNimble("uninstall", "mydep") + checkpoint output + check output.processOutput.inLines("cannot uninstall mydep") + check exitCode == QuitFailure + + test "revdep -i test": + cd "revdep/mydep": + verify execNimbleYes("install") + + cd "revdep/pkgWithDep": + verify execNimbleYes("install") + + verify execNimbleYes("remove", "mydep", "-i") + + 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") + + test "remove skips packages with revDeps (#504)": + var (output, exitCode) = execNimbleYes( + "install", "nimboost@0.5.5", "nimfp@0.4.4") + check exitCode == QuitSuccess + + (output, exitCode) = execNimble("uninstall", "nimboost", "nimfp", "-n") + var lines = output.strip.processOutput() + check inLines(lines, "Cannot uninstall nimboost") + + (output, exitCode) = execNimbleYes("uninstall", "nimfp", "nimboost") + lines = output.strip.processOutput() + check (not inLines(lines, "Cannot uninstall nimboost")) + + check execNimble("path", "nimboost").exitCode != QuitSuccess + check execNimble("path", "nimfp").exitCode != QuitSuccess + + test "old format conversion": + const oldNimbleDataFileName = + "./revdep/nimbleData/old_nimble_data.json".normalizedPath + const newNimbleDataFileName = + "./revdep/nimbleData/new_nimble_data.json".normalizedPath + + doAssert fileExists(oldNimbleDataFileName) + doAssert fileExists(newNimbleDataFileName) + + let oldNimbleData = loadNimbleData(oldNimbleDataFileName) + let newNimbleData = loadNimbleData(newNimbleDataFileName) + + doAssert oldNimbleData == newNimbleData diff --git a/tests/truncommand.nim b/tests/truncommand.nim new file mode 100644 index 00000000..8a613576 --- /dev/null +++ b/tests/truncommand.nim @@ -0,0 +1,164 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils +import testscommon +from nimblepkg/common import cd + +suite "nimble run": + test "Invalid binary": + cd "run": + let (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "blahblah", # The command to run + ) + check exitCode == QuitFailure + check output.contains("Binary '$1' is not defined in 'run' package." % + "blahblah".changeFileExt(ExeExt)) + + test "Parameters passed to executable": + cd "run": + let (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "run", # The command to run + "--test", # First argument passed to the executed command + "check" # Second argument passed to the executed command. + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test check" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test", "check"]""") + + test "Parameters not passed to single executable": + cd "run": + let (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invocation + "--", # Separator for arguments + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + + test "Parameters passed to single executable": + cd "run": + let (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "--", # Flag to set run file to "" before next argument + "--test", # First argument passed to the executed command + "check" # Second argument passed to the executed command. + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test check" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test", "check"]""") + + test "Executable output is shown even when not debugging": + cd "run": + let (output, exitCode) = + execNimble("run", "run", "--option1", "arg1") + check exitCode == QuitSuccess + check output.contains("""Testing `nimble run`: @["--option1", "arg1"]""") + + test "Quotes and whitespace are well handled": + cd "run": + let (output, exitCode) = execNimble( + "run", "run", "\"", "\'", "\t", "arg with spaces") + check exitCode == QuitSuccess + check output.contains( + """Testing `nimble run`: @["\"", "\'", "\t", "arg with spaces"]""") + + test "Nimble options before executable name": + cd "run": + let (output, exitCode) = execNimble( + "run", # Run command invokation + "--debug", # Flag to enable debug verbosity in Nimble + "run", # The executable to run + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + + test "Nimble options flags before --": + cd "run": + let (output, exitCode) = execNimble( + "run", # Run command invokation + "--debug", # Flag to enable debug verbosity in Nimble + "--", # Separator for arguments + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + + test "Compilation flags before run command": + cd "run": + let (output, exitCode) = execNimble( + "-d:sayWhee", # Compile flag to define a conditional symbol + "run", # Run command invokation + "--debug", # Flag to enable debug verbosity in Nimble + "--", # Separator for arguments + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + check output.contains("""Whee!""") + + test "Compilation flags before executable name": + cd "run": + let (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "-d:sayWhee", # Compile flag to define a conditional symbol + "run", # The executable to run + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + check output.contains("""Whee!""") + + test "Compilation flags before --": + cd "run": + let (output, exitCode) = execNimble( + "run", # Run command invokation + "-d:sayWhee", # Compile flag to define a conditional symbol + "--debug", # Flag to enable debug verbosity in Nimble + "--", # Separator for arguments + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + check output.contains("""Whee!""") + + test "Order of compilation flags before and after run command": + cd "run": + let (output, exitCode) = execNimble( + "-d:compileFlagBeforeRunCommand", # Compile flag to define a conditional symbol + "run", # Run command invokation + "-d:sayWhee", # Compile flag to define a conditional symbol + "--debug", # Flag to enable debug verbosity in Nimble + "--", # Separator for arguments + "--test" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("-d:compileFlagBeforeRunCommand -d:sayWhee") + check output.contains("tests$1run$1$2 --test" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--test"]""") + check output.contains("""Whee!""") diff --git a/tests/ttestcommand.nim b/tests/ttestcommand.nim new file mode 100644 index 00000000..dc8dd1d9 --- /dev/null +++ b/tests/ttestcommand.nim @@ -0,0 +1,47 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os +import testscommon +from nimblepkg/common import cd + +suite "test command": + test "Runs passing unit tests": + cd "testCommand/testsPass": + # Pass flags to test #726, #757 + let (outp, exitCode) = execNimble("test", "-d:CUSTOM") + check exitCode == QuitSuccess + check outp.processOutput.inLines("First test") + check outp.processOutput.inLines("Second test") + check outp.processOutput.inLines("Third test") + check outp.processOutput.inLines("Executing my func") + + test "Runs failing unit tests": + cd "testCommand/testsFail": + let (outp, exitCode) = execNimble("test") + check exitCode == QuitFailure + check outp.processOutput.inLines("First test") + check outp.processOutput.inLines("Failing Second test") + check(not outp.processOutput.inLines("Third test")) + + test "test command can be overriden": + cd "testCommand/testOverride": + let (outp, exitCode) = execNimble("-d:CUSTOM", "test", "--runflag") + check exitCode == QuitSuccess + check outp.processOutput.inLines("overriden") + check outp.processOutput.inLines("true") + + test "certain files are ignored": + cd "testCommand/testsIgnore": + let (outp, exitCode) = execNimble("test") + check exitCode == QuitSuccess + check(not outp.processOutput.inLines("Should be ignored")) + check outp.processOutput.inLines("First test") + + test "CWD is root of package": + cd "testCommand/testsCWD": + let (outp, exitCode) = execNimble("test") + check exitCode == QuitSuccess + check outp.processOutput.inLines(getCurrentDir()) diff --git a/tests/ttwobinaryversions.nim b/tests/ttwobinaryversions.nim new file mode 100644 index 00000000..5b669d3e --- /dev/null +++ b/tests/ttwobinaryversions.nim @@ -0,0 +1,35 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils +import testscommon +from nimblepkg/common import cd + +suite "can handle two binary versions": + setup: + cd "binaryPackage/v1": + check execNimbleYes("install").exitCode == QuitSuccess + + cd "binaryPackage/v2": + check execNimbleYes("install").exitCode == QuitSuccess + + test "can execute v2": + let (output, exitCode) = execBin("binaryPackage") + check exitCode == QuitSuccess + check output.strip() == "v2" + + test "can update symlink to earlier version after removal": + check execNimbleYes("remove", "binaryPackage@2.0").exitCode==QuitSuccess + + let (output, exitCode) = execBin("binaryPackage") + check exitCode == QuitSuccess + check output.strip() == "v1" + + test "can keep symlink version after earlier version removal": + check execNimbleYes("remove", "binaryPackage@1.0").exitCode==QuitSuccess + + let (output, exitCode) = execBin("binaryPackage") + check exitCode == QuitSuccess + check output.strip() == "v2" diff --git a/tests/tuninstall.nim b/tests/tuninstall.nim new file mode 100644 index 00000000..4b481399 --- /dev/null +++ b/tests/tuninstall.nim @@ -0,0 +1,92 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, strutils, os, strformat +import testscommon + +from nimblepkg/displaymessages import cannotUninstallPkgMsg +from nimblepkg/common import cd + +suite "uninstall": + test "can install packagebin2": + cleanDir(installDir) + let args = ["install", pkgBin2Url] + check execNimbleYes(args).exitCode == QuitSuccess + + test "can reject same version dependencies": + cleanDir(installDir) + let (outp, exitCode) = execNimbleYes("install", pkgBinUrl) + # We look at the error output here to avoid out-of-order problems caused by + # stderr output being generated and flushed without first flushing stdout + let ls = outp.strip.processOutput() + check exitCode != QuitSuccess + check "Cannot satisfy the dependency on PackageA 0.2.0 and PackageA 0.5.0" in + ls[ls.len-1] + + proc setupIssue27Packages() = + # Install b + cd "issue27/b": + check execNimbleYes("install").exitCode == QuitSuccess + # Install a + cd "issue27/a": + check execNimbleYes("install").exitCode == QuitSuccess + cd "issue27": + check execNimbleYes("install").exitCode == QuitSuccess + + test "issue #27": + setupIssue27Packages() + + test "can uninstall": + # setup test environment + cleanDir(installDir) + setupIssue27Packages() + check execNimbleYes("install", &"{pkgAUrl}@0.2").exitCode == QuitSuccess + check execNimbleYes("install", &"{pkgAUrl}@0.5").exitCode == QuitSuccess + check execNimbleYes("install", &"{pkgAUrl}@0.6").exitCode == QuitSuccess + check execNimbleYes("install", pkgBin2Url).exitCode == QuitSuccess + check execNimbleYes("install", pkgBUrl).exitCode == QuitSuccess + cd "nimscript": check execNimbleYes("install").exitCode == QuitSuccess + + block: + let (outp, exitCode) = execNimbleYes("uninstall", "issue27b") + check exitCode != QuitSuccess + var ls = outp.strip.processOutput() + let pkg27ADir = getPackageDir(pkgsDir, "issue27a-0.1.0", false) + let expectedMsg = cannotUninstallPkgMsg("issue27b", "0.1.0", @[pkg27ADir]) + check ls.inLinesOrdered(expectedMsg) + + check execNimbleYes("uninstall", "issue27").exitCode == QuitSuccess + check execNimbleYes("uninstall", "issue27a").exitCode == QuitSuccess + + # Remove Package* + check execNimbleYes("uninstall", "PackageA@0.5").exitCode == QuitSuccess + + let (outp, exitCode) = execNimbleYes("uninstall", "PackageA") + check exitCode != QuitSuccess + let ls = outp.processOutput() + let + pkgBin2Dir = getPackageDir(pkgsDir, "packagebin2-0.1.0", false) + pkgBDir = getPackageDir(pkgsDir, "packageb-0.1.0", false) + expectedMsgForPkgA0dot6 = cannotUninstallPkgMsg( + "PackageA", "0.6.0", @[pkgBin2Dir]) + expectedMsgForPkgA0dot2 = cannotUninstallPkgMsg( + "PackageA", "0.2.0", @[pkgBDir]) + check ls.inLines(expectedMsgForPkgA0dot6) + check ls.inLines(expectedMsgForPkgA0dot2) + + check execNimbleYes("uninstall", "PackageBin2").exitCode == QuitSuccess + + # Case insensitive + check execNimbleYes("uninstall", "packagea").exitCode == QuitSuccess + check execNimbleYes("uninstall", "PackageA").exitCode != QuitSuccess + + # Remove the rest of the installed packages. + check execNimbleYes("uninstall", "PackageB").exitCode == QuitSuccess + + check execNimbleYes("uninstall", "PackageA@0.2", "issue27b").exitCode == + QuitSuccess + check(not dirExists(installDir / "pkgs" / "PackageA-0.2.0")) + + check execNimbleYes("uninstall", "nimscript").exitCode == QuitSuccess From eea6bc06cd6b7256ce5087615be7923f9ee58623 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 30 Nov 2020 21:35:04 +0200 Subject: [PATCH 23/73] Implement sync operation for develop dependencies Sync operation which updates the develop mode dependencies to the version specified in the lock file is implemented. If the specified revision is not found locally tries to fetch it from the configured remotes. If it is present on multiple branches tries to stay on the current one and if cannot prefers local branches rather than remote tracking ones. If found on more than one branch gives the user a choice whether to switch. Sync operation will also download non develop mode dependencies' versions described in the lock file if they are not already present in the Nimble cache. If `-l, --list-only` option is given than only list develop mode dependencies which working copies are out of sync without actually syncing them and without downloading missing non develop mode dependencies. **Important implementation details:** To be able to determine whether a working copy of develop mode dependency needs to be synced, locked again or merged with or re-based on some other branch a special sync file is kept is the VCS directory (.git or .hg) of the current package. It keeps a record for every develop mode dependency for its current working copy revision during the last `lock`, `sync` or `develop` operation. The name of the file is `.nimble.sync`. **Additional changes:** - Nimble package directories which are not under version control, with not clean working copies or with not pushed VCS revisions cannot be locked. - Implement updating of already existing lock file. The update is being executed by the following algorithm: * Add newly added dependencies. * Remove missing dependencies. * Update develop mode dependencies to the newly locked version. * Leave installed dependencies to the previously locked version. - On `check` command the develop mode dependencies are being validated against the lock file. The following reasons for validation failure are possible. * The package directory is not under version control. * The package working copy directory is not in clean state. * Current VCS revision is no pushed on any remote. * The working copy needs sync. This happens when the VCS revision written in the sync file is equal to the current working copy revision, but the revision in the lock file is different from them. * The working copy needs lock. This happens when the lock file VCS revision is equal to the VCS revision from the sync file but the current working copy VCS revision is different. * The working copy needs merge or re-base. This happens when lock file, sync file and current working copy VCS revisions are all different. - Implemented caching of loaded develop file to avoid multiple loads when information for them is queried from different places in the code. Related to nim-lnag/nimble#127 --- .gitignore | 4 +- src/nimble.nim | 397 +++++-- src/nimblepkg/aliasthis.nim | 4 +- src/nimblepkg/{checksum.nim => checksums.nim} | 49 +- src/nimblepkg/common.nim | 14 + src/nimblepkg/developfile.nim | 494 +++++--- src/nimblepkg/displaymessages.nim | 11 + src/nimblepkg/download.nim | 22 +- src/nimblepkg/lockfile.nim | 38 +- src/nimblepkg/options.nim | 21 +- src/nimblepkg/packageinfo.nim | 19 +- src/nimblepkg/packageinfotypes.nim | 22 +- src/nimblepkg/packageparser.nim | 8 +- src/nimblepkg/paths.nim | 3 + src/nimblepkg/sha1hashes.nim | 36 +- src/nimblepkg/syncfile.nim | 109 ++ src/nimblepkg/tools.nim | 22 +- src/nimblepkg/topologicalsort.nim | 34 +- src/nimblepkg/vcstools.nim | 1023 +++++++++++++++++ tests/.gitignore | 1 + tests/tester.nim | 27 +- tests/testscommon.nim | 4 + tests/tlockfile.nim | 488 ++++++++ tests/tuninstall.nim | 7 +- 24 files changed, 2501 insertions(+), 356 deletions(-) rename src/nimblepkg/{checksum.nim => checksums.nim} (52%) create mode 100644 src/nimblepkg/syncfile.nim create mode 100644 src/nimblepkg/vcstools.nim create mode 100644 tests/tlockfile.nim diff --git a/.gitignore b/.gitignore index 4ece31a9..44834188 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ nimcache/ # executables from test and build /nimble src/nimblepkg/aliasthis -src/nimblepkg/checksum +src/nimblepkg/checksums src/nimblepkg/cli src/nimblepkg/common src/nimblepkg/config @@ -41,8 +41,10 @@ src/nimblepkg/paths src/nimblepkg/publish src/nimblepkg/reversedeps src/nimblepkg/sha1hashes +src/nimblepkg/syncfile src/nimblepkg/tools src/nimblepkg/topologicalsort +src/nimblepkg/vcstools src/nimblepkg/version # Windows executables diff --git a/src/nimble.nim b/src/nimble.nim index 25b1b543..59c0e7dd 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -17,10 +17,11 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, - nimblepkg/checksum, nimblepkg/topologicalsort, nimblepkg/lockfile, + nimblepkg/checksums, nimblepkg/topologicalsort, nimblepkg/lockfile, nimblepkg/nimscriptwrapper, nimblepkg/developfile, nimblepkg/paths, nimblepkg/nimbledatafile, nimblepkg/packagemetadatafile, - nimblepkg/displaymessages, nimblepkg/sha1hashes + nimblepkg/displaymessages, nimblepkg/sha1hashes, nimblepkg/syncfile, + nimblepkg/vcstools proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -282,7 +283,7 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): proc processAllDependencies(pkgInfo: PackageInfo, options: Options): HashSet[PackageInfo] = - if pkgInfo.lockedDependencies.len > 0: + if pkgInfo.lockedDeps.len > 0: pkgInfo.processLockedDependencies(options) else: pkgInfo.processFreeDependencies(options) @@ -323,7 +324,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, pkgInfo.specialVersion = $requestedVer.spe # Dependencies need to be processed before the creation of the pkg dir. - if first and pkgInfo.lockedDependencies.len > 0: + if first and pkgInfo.lockedDeps.len > 0: result.deps = pkgInfo.processLockedDependencies(depsOptions) elif not fromLockFile: result.deps = pkgInfo.processFreeDependencies(depsOptions) @@ -433,57 +434,67 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, cd pkgInfo.myPath.splitFile.dir: discard execHook(options, actionInstall, false) -proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): - HashSet[PackageInfo] = - ## ``` - ## For each dependency in the lock file: - ## Check whether it is already installed and if not: - ## Download it at specific VCS revision. - ## Check whether it has the right checksum and if so: - ## Install it from the download dir. - ## Add record in the reverse dependencies file. - ## Convert its info to PackageInfo and add it to the result. - ## ``` +proc getLockedDep(pkgInfo: PackageInfo, name: string, dep: LockFileDep, + options: Options): PackageInfo = + ## Returns the package info for dependency package `dep` with name `name` from + ## the lock file of the package `pkgInfo` by searching for it in the local + ## Nimble cache and downloading it from its repository if the version with the + ## required checksum is not found locally. let packagesDir = options.getPkgsDir() + let depDirName = packagesDir / &"{name}-{dep.version}-{dep.checksums.sha1}" - for name, dep in pkgInfo.lockedDependencies: - let depDirName = packagesDir / &"{name}-{dep.version}-{dep.checksums.sha1}" + if not fileExists(depDirName / packageMetaDataFileName): + if depDirName.dirExists: + promptRemoveEntirePackageDir(depDirName, options) + removeDir(depDirName) - if not fileExists(depDirName / packageMetaDataFileName): - if depDirName.dirExists: - promptRemoveEntirePackageDir(depDirName, options) - removeDir(depDirName) + let (url, metadata) = getUrlData(dep.url) + let version = dep.version.parseVersionRange + let subdir = metadata.getOrDefault("subdir") - let (url, metadata) = getUrlData(dep.url) - let version = dep.version.parseVersionRange - let subdir = metadata.getOrDefault("subdir") + let (downloadDir, _) = downloadPkg( + url, version, dep.downloadMethod, subdir, options, + downloadPath = "", dep.vcsRevision) - let (downloadDir, _) = downloadPkg( - url, version, dep.downloadMethod.getDownloadMethod, subdir, options, - downloadPath = "", dep.vcsRevision) + let downloadedPackageChecksum = calculateDirSha1Checksum(downloadDir) + if downloadedPackageChecksum != dep.checksums.sha1: + raise checksumError(name, dep.version, dep.vcsRevision, + downloadedPackageChecksum, dep.checksums.sha1) - let downloadedPackageChecksum = calculatePackageSha1Checksum(downloadDir) - if downloadedPackageChecksum != dep.checksums.sha1: - raise checksumError(name, dep.version, dep.vcsRevision, - downloadedPackageChecksum, dep.checksums.sha1) + let (_, newlyInstalledPackageInfo) = installFromDir( + downloadDir, version, options, url, first = false, fromLockFile = true) - let (_, newlyInstalledPackageInfo) = installFromDir( - downloadDir, version, options, url, first = false, fromLockFile = true) + for depDepName in dep.dependencies: + let depDep = pkgInfo.lockedDeps[depDepName] + let revDep = (name: depDepName, version: depDep.version, + checksum: depDep.checksums.sha1) + options.nimbleData.addRevDep(revDep, newlyInstalledPackageInfo) - for depDepName in dep.dependencies: - let depDep = pkgInfo.lockedDependencies[depDepName] - let revDep = (name: depDepName, version: depDep.version, - checksum: depDep.checksums.sha1) - options.nimbleData.addRevDep(revDep, newlyInstalledPackageInfo) + return newlyInstalledPackageInfo + else: + let nimbleFilePath = findNimbleFile(depDirName, false) + let packageInfo = getInstalledPackageMin( + depDirName, nimbleFilePath).toFullInfo(options) + return packageInfo - result.incl newlyInstalledPackageInfo +proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): + HashSet[PackageInfo] = + # Returns a hash set with `PackageInfo` of all packages from the lock file of + # the package `pkgInfo` by getting the info for develop mode dependencies from + # their local file system directories and other packages from the Nimble + # cache. If a package with required checksum is missing from the local cache + # installs it by downloading it from its repository. + + let developModeDeps = getDevelopDependencies(pkgInfo, options) + for name, dep in pkgInfo.lockedDeps: + let depPkg = + if developModeDeps.hasKey(name): + developModeDeps[name][] + else: + getLockedDep(pkgInfo, name, dep, options) - else: - let nimbleFilePath = findNimbleFile(depDirName, false) - let packageInfo = getInstalledPackageMin( - depDirName, nimbleFilePath).toFullInfo(options) - result.incl packageInfo + result.incl depPkg proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string, @@ -495,7 +506,7 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options, var pkg: Package if getPackage(pv.name, options, pkg): let (url, metadata) = getUrlData(pkg.url) - return (pkg.downloadMethod.getDownloadMethod(), url, metadata) + return (pkg.downloadMethod, url, metadata) else: # If package is not found give the user a chance to refresh # package.json @@ -520,7 +531,7 @@ proc install(packages: seq[PkgTuple], options: Options, if packages == @[]: let currentDir = getCurrentDir() - if currentDir.hasDevelopFile: + if currentDir.developFileExists: displayWarning( "Installing a package which currently has develop mode dependencies." & "\nThey will be ignored and installed as normal packages.") @@ -1098,17 +1109,22 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = return pkgInfo.getNimbleFileDir +proc updateSyncFile(dependentPkg: PackageInfo, options: Options) + proc develop(options: var Options) = let - hasDevActionsAllowedOnlyInPkgDir = options.action.devActions.filterIt( - it[0] != datNewFile).len > 0 hasPackages = options.action.packages.len > 0 hasPath = options.action.path.len > 0 + hasDevActions = options.action.devActions.len > 0 if not hasPackages and hasPath: raise nimbleError(pathGivenButNoPkgsToDownloadMsg) - var currentDirPkgInfo = initPackageInfo() + var + currentDirPkgInfo = initPackageInfo() + hasError = false + hasDevActionsAllowedOnlyInPkgDir = options.action.devActions.filterIt( + it[0] != datNewFile).len > 0 try: # Check whether the current directory is a package directory. @@ -1117,26 +1133,28 @@ proc develop(options: var Options) = if hasDevActionsAllowedOnlyInPkgDir: raise nimbleError(developOptionsOutOfPkgDirectoryMsg, details = error) - var hasDevActions = options.action.devActions.len > 0 - if currentDirPkgInfo.isLoaded and (not hasPackages) and (not hasDevActions): developFromDir(currentDirPkgInfo, options) - var hasError = false - # Install each package. for pkgTup in options.action.packages: try: let pkgPath = installDevelopPackage(pkgTup, options) - options.action.devActions.add (datAdd, pkgPath.normalizedPath) - hasDevActions = true + if currentDirPkgInfo.isLoaded: + options.action.devActions.add (datAdd, pkgPath.normalizedPath) + hasDevActionsAllowedOnlyInPkgDir = true except CatchableError as error: hasError = true displayError(&"Cannot install package \"{pkgTup}\" for develop.") displayDetails(error) - if hasDevActions: + if hasDevActionsAllowedOnlyInPkgDir: hasError = not updateDevelopFile(currentDirPkgInfo, options) or hasError + else: + hasError = not executeDevActionsAllowedOutsidePkgDir(options) or hasError + + if currentDirPkgInfo.isLoaded: + updateSyncFile(currentDirPkgInfo, options) if hasError: raise nimbleError( @@ -1215,21 +1233,270 @@ proc check(options: Options) = display("Failure:", validationFailedMsg, Error, HighPriority) raise nimbleQuit(QuitFailure) -proc promptOverwriteLockFile(options: Options) = - let message = &"{lockFileName} already exists. Overwrite?" - if not options.prompt(message): - raise nimbleQuit() +proc updateSyncFile(dependentPkg: PackageInfo, options: Options) = + # Updates the sync file with the current VCS revisions of develop mode + # dependencies of the package `dependentPkg`. + + let developDeps = processDevelopDependencies(dependentPkg, options).toHashSet + let syncFile = getSyncFile(dependentPkg) + + # Remove old data from the sync file + syncFile.clear + + # Add all current develop packages' VCS revisions to the sync file. + for dep in developDeps: + syncFile.setDepVcsRevision(dep.name, dep.vcsRevision) + + syncFile.save + +proc validateDevModeDepsWorkingCopiesBeforeLock( + pkgInfo: PackageInfo, options: Options) = + ## Validates that the develop mode dependencies states are suitable for + ## locking. They must be under version control, their working copies must be + ## in a clean state and their current VCS revision must be present on some of + ## the configured remotes. + + var errors: ValidationErrors + findValidationErrorsOfDevDepsWithLockFile(pkgInfo, options, errors) + + # Those validation errors are not errors in the context of generating a lock + # file. + const notAnErrorSet = { + vekWorkingCopyNeedsSync, + vekWorkingCopyNeedsLock, + vekWorkingCopyNeedsMerge, + } + + # Remove not errors from the errors set. + for name, error in common.dup(errors): + if error.kind in notAnErrorSet: + errors.del name + + if errors.len > 0: + raise validationErrors(errors) + +proc mergeLockedDependencies*(pkgInfo: PackageInfo, newDeps: LockFileDeps, + options: Options): LockFileDeps = + ## Updates the lock file data of already generated lock file with the data + ## from a new lock operation. + + result = pkgInfo.lockedDeps + let developDeps = pkgInfo.getDevelopDependencies(options) + + for name, dep in newDeps: + if result.hasKey(name): + # If the dependency is already present in the old lock file + if developDeps.hasKey(name): + # and it is a develop mode dependency update it with the newly locked + # version, + result[name] = dep + else: + # but if it is installed dependency just leave it at the current + # version. + discard + else: + # If the dependency is missing from the old develop file add it. + result[name] = dep + + # Clean dependencies which are missing from the newly locked list. + let deps = result + for name, dep in deps: + if not newDeps.hasKey(name): + result.del name + +proc displayLockOperationStart(dir: string): bool = + ## Displays a proper log message for starting generating or updating the lock + ## file of a package in directory `dir`. + + var doesLockFileExist = dir.lockFileExists + let msg = if doesLockFileExist: + updatingTheLockFileMsg + else: + generatingTheLockFileMsg + displayInfo(msg) + return doesLockFileExist + +proc displayLockOperationFinish(didLockFileExist: bool) = + ## Displays a proper log message for finished generation or update of a lock + ## file. + + let msg = if didLockFileExist: + lockFileIsUpdatedMsg + else: + lockFileIsGeneratedMsg + displaySuccess(msg) proc lock(options: Options) = + ## Generates a lock file for the package in the current directory or updates + ## it if it already exists. + let currentDir = getCurrentDir() - if lockFileExists(currentDir): - promptOverwriteLockFile(options) let pkgInfo = getPkgInfo(currentDir, options) + + let doesLockFileExist = displayLockOperationStart(currentDir) + validateDevModeDepsWorkingCopiesBeforeLock(pkgInfo, options) + let dependencies = pkgInfo.processFreeDependencies(options).map( pkg => pkg.toFullInfo(options)) - let dependencyGraph = buildDependencyGraph(dependencies, options) + var dependencyGraph = buildDependencyGraph(dependencies, options) + + if currentDir.lockFileExists: + # If we already have a lock file, merge its data with the newly generated + # one. + # + # IMPORTANT TODO: + # To do this properly, an SMT solver is needed, but anyway, it seems that + # currently Nimble does not check properly for `require` clauses + # satisfaction between all packages, but just greedily picks the best + # matching version of dependencies for the currently processed package. + + dependencyGraph = mergeLockedDependencies(pkgInfo, dependencyGraph, options) + let (topologicalOrder, _) = topologicalSort(dependencyGraph) writeLockFile(dependencyGraph, topologicalOrder) + updateSyncFile(pkgInfo, options) + displayLockOperationFinish(doesLockFileExist) + +proc syncWorkingCopy(name: string, path: Path, dependentPkg: PackageInfo, + options: Options) = + ## Syncs a working copy of a develop mode dependency of package `dependentPkg` + ## with name `name` at path `path` with the revision from the lock file of + ## `dependentPkg`. + + displayInfo(&"Syncing working copy of package \"{name}\" at \"{path}\"...") + + let lockedDeps = dependentPkg.lockedDeps + assert lockedDeps.hasKey(name), + &"Package \"{name}\" must be present in the lock file." + + let vcsRevision = lockedDeps[name].vcsRevision + assert vcsRevision != path.getVcsRevision, + "If here the working copy VCS revision must be different from the " & + "revision written in the lock file." + + try: + if not isVcsRevisionPresentOnSomeBranch(path, vcsRevision): + # If the searched revision is not present on some local branch retrieve + # changes sets from the remote branch corresponding to the local one. + let (remote, branch) = getCorrespondingRemoteAndBranch(path) + retrieveRemoteChangeSets(path, remote, branch) + + if not isVcsRevisionPresentOnSomeBranch(path, vcsRevision): + # If the revision is still not found retrieve all remote change sets. + retrieveRemoteChangeSets(path) + + let + currentBranch = getCurrentBranch(path) + localBranches = getBranchesOnWhichVcsRevisionIsPresent( + path, vcsRevision, btLocal) + remoteTrackingBranches = getBranchesOnWhichVcsRevisionIsPresent( + path, vcsRevision, btRemoteTracking) + allBranches = localBranches + remoteTrackingBranches + + var targetBranch = + if allBranches.len == 0: + # Te revision is not found on any branch. + "" + elif localBranches.len == 1: + # If the revision is present on only one local branch switch to it. + localBranches.toSeq[0] + elif localBranches.contains(currentBranch): + # If the current branch is among the local branches on which the + # revision is found we have to stay to it. + currentBranch + elif remoteTrackingBranches.len == 1: + # If the revision is found on only one remote tracking branch we have to + # fast forward merge it to a corresponding local branch and to switch to + # it. + remoteTrackingBranches.toSeq[0] + elif (let (hasBranch, branchName) = hasCorrespondingRemoteBranch( + path, remoteTrackingBranches); hasBranch): + # If the current branch has corresponding remote tracking branch on + # which the revision is found we have to get the name of the remote + # tracking branch in order to try to fast forward merge it to the local + # branch. + branchName + else: + # If the revision is found on several branches, but nighter of them is + # the current one or a remote tracking branch corresponding to the + # current one then give the user a choice to which branch to switch. + options.promptList( + &"The revision \"{vcsRevision}\" is found on multiple branches.\n" & + "Choose a branch to switch to:", + allBranches.toSeq.toOpenArray(0, allBranches.len - 1)) + + if path.getVcsType == vcsTypeGit and + remoteTrackingBranches.contains(targetBranch): + # If the target branch is a remote tracking branch get all local branches + # which track it. + let localBranches = getLocalBranchesTrackingRemoteBranch( + path, targetBranch) + let localBranch = + if localBranches.len == 0: + # There is no local branch tracking the remote branch and we have to + # get a name for a new branch. + getLocalBranchName(path, targetBranch) + elif localBranches.len == 1: + # There is only one local branch tracking the remote branch. + localBranches[0] + else: + # If there are multiple local branches which track the remote branch + # then give the user a choice to which to try to fast forward merge + # the remote branch. + options.promptList("Choose local branch where to try to fast " & + &"forward merge \"{targetBranch}\":", + localBranches.toOpenArray(0, localBranches.len - 1)) + fastForwardMerge(path, targetBranch, localBranch) + targetBranch = localBranch + + if targetBranch != "": + if targetBranch != currentBranch: + switchBranch(path, targetBranch) + if path.getVcsRevision != vcsRevision: + setCurrentBranchToVcsRevision(path, vcsRevision) + else: + # If the revision is not found on any branch try to set the package + # working copy to it in detached state. If the revision is completely + # missing the operation will fail with exception. + setWorkingCopyToVcsRevision(path, vcsRevision) + + displayInfo(pkgWorkingCopyIsSyncedMsg(name, $path)) + except CatchableError as error: + displayError(&"Working copy of package \"{name}\" at path \"{path}\" " & + "cannot be synced.") + displayDetails(error.msg) + +proc sync(options: Options) = + # Syncs working copies of the develop mode dependencies of the current + # directory package with the revision data from the lock file. + + let currentDir = getCurrentDir() + let pkgInfo = getPkgInfo(currentDir, options) + + if not pkgInfo.areLockedDepsLoaded: + raise nimbleError("Cannot execute `sync` when lock file is missing.") + + if not options.action.listOnly: + # On `sync` we also want to update Nimble cache with the dependencies' + # versions from the lock file. + discard processLockedDependencies(pkgInfo, options) + + var errors: ValidationErrors + findValidationErrorsOfDevDepsWithLockFile(pkgInfo, options, errors) + + for name, error in common.dup(errors): + if error.kind == vekWorkingCopyNeedsSync: + if not options.action.listOnly: + syncWorkingCopy(name, error.path, pkgInfo, options) + else: + displayInfo(pkgWorkingCopyNeedsSyncingMsg(name, $error.path)) + # Remove sync errors because we are doing sync. + errors.del name + + updateSyncFile(pkgInfo, options) + + if errors.len > 0: + raise validationErrors(errors) proc run(options: Options) = # Verify parameters. @@ -1312,6 +1579,8 @@ proc doAction(options: var Options) = check(options) of actionLock: lock(options) + of actionSync: + sync(options) of actionNil: assert false of actionCustom: @@ -1366,7 +1635,7 @@ when isMainModule: saveNimbleData(opt) except CatchableError as error: exitCode = QuitFailure - displayError(&"Couldn't save `{nimbleDataFileName}`.") + displayError(&"Couldn't save \"{nimbleDataFileName}\".") displayDetails(error) quit(exitCode) diff --git a/src/nimblepkg/aliasthis.nim b/src/nimblepkg/aliasthis.nim index f53d03ee..caed40bb 100644 --- a/src/nimblepkg/aliasthis.nim +++ b/src/nimblepkg/aliasthis.nim @@ -77,8 +77,10 @@ template aliasThis*(dotExpression: untyped) = ## obj.field1 = 42 ## echo obj.field1 # prints 42 ## echo obj.field2.field1 # also prints 42 - + + {.warning[UnsafeDefault]: off.} aliasThisImpl(dotExpression, dotExpression.typeOf.fields) + {.warning[UnsafeDefault]: on.} when isMainModule: import unittest diff --git a/src/nimblepkg/checksum.nim b/src/nimblepkg/checksums.nim similarity index 52% rename from src/nimblepkg/checksum.nim rename to src/nimblepkg/checksums.nim index b8bf152b..edeb1169 100644 --- a/src/nimblepkg/checksum.nim +++ b/src/nimblepkg/checksums.nim @@ -1,8 +1,8 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, strutils, std/sha1, algorithm, strformat -import common, tools, sha1hashes +import os, std/sha1, strformat +import common, sha1hashes, vcstools, paths type ChecksumError* = object of NimbleError @@ -17,31 +17,6 @@ Downloaded package checksum does not correspond to that in the lock file: Expected checksum: {expectedChecksum} """) -proc extractFileList(consoleOutput: string): seq[string] = - result = consoleOutput.splitLines() - discard result.pop() - -proc getPackageFileListFromGit(): seq[string] = - let output = tryDoCmdEx("git ls-files") - extractFileList(output) - -proc getPackageFileListFromMercurial(): seq[string] = - let output = tryDoCmdEx("hg manifest") - extractFileList(output) - -proc getPackageFileListWithoutScm(): seq[string] = - for file in walkDirRec(".", relative = true): - result.add(file) - -proc getPackageFileList(): seq[string] = - if dirExists(".git"): - result = getPackageFileListFromGit() - elif dirExists(".hg"): - result = getPackageFileListFromMercurial() - else: - result = getPackageFileListWithoutScm() - result.sort() - proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = checksum.update(fileName) if not fileName.fileExists: @@ -59,10 +34,16 @@ proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = if bytesRead == 0: break checksum.update(buffer[0.. 0, "Must have validation errors." + +proc getValidationErrorMessage*(name: string, error: ValidationError): string = + ## By given validation error `error` constructs a validation error message for + ## given develop mode dependency package with name `name`. + &"Package \"{name}\" at \"{error.path}\" {error.kind}.\n" + +proc getValidationErrorsMessage*(errors: ValidationErrors): string = + ## Constructs an error message reporting develop mode dependencies validation + ## errors. + + errors.assertHasValidationErrors + result = "Some of package's develop mode dependencies are invalid.\n" + for name, error in errors: + result &= getValidationErrorMessage(name, error) + +proc allAreSet(errorFlags: set[ValidationErrorKind]): bool = + ## Checks whether all possible validation error flags are set. + cast[uint](errorFlags) == uint(2'd ^ ValidationErrorKind.enumLen - 1) + +proc getValidationsErrorsHint(errors: ValidationErrors): string = + ## Constructs a hint message for resolving develop mode dependencies + ## validation errors. + + errors.assertHasValidationErrors + var errorFlags: ValidationErrorFlags + + for _, error in errors: + case error.kind: + of vekDirIsNotUnderVersionControl, vekWorkingCopyIsNotClean, + vekVcsRevisionIsNotPushed: + if error.kind notin errorFlags: + result &= + "When you are using a lock file Nimble requires develop mode " & + "dependencies to be under version control, all local changes to be " & + "committed and pushed on some remote, and lock file to be updated.\n" + of vekWorkingCopyNeedsSync: + if error.kind notin errorFlags: + result &= + "You have to call `nimble sync` to synchronize your develop mode " & + "dependencies working copies with the latest lock file.\n" + of vekWorkingCopyNeedsLock: + if error.kind notin errorFlags: + result &= + "You have to call `nimble lock` to update your lock file with the " & + "latest versions of your develop mode dependencies working copies.\n" + of vekWorkingCopyNeedsMerge: + if error.kind notin errorFlags: + result &= + "You have to merge or rebase working copies of your develop mode " & + "dependencies which have conflicts with remote changes." + + errorFlags.incl error.kind + if errorFlags.allAreSet: break + +proc pkgDirIsNotUnderVersionControl(depPkg: PackageInfo): bool = + ## Checks whether a develop mode dependency package directory is under version + ## control. + depPkg.getNimbleFileDir.getVcsType == vcsTypeNone + +proc workingCopyIsNotClean(depPkg: PackageInfo): bool = + ## Checks whether a working copy directory of a develop mode dependency + ## package is clean. Untracked files are not considered. + not depPkg.getNimbleFileDir.isWorkingCopyClean + +proc vcsRevisionIsNotPushed(depPkg: PackageInfo): bool = + ## Checks whether current VCS revision of the working copy directory of a + ## develop mode dependency package is pushed on some remote. + not depPkg.getNimbleFileDir.isVcsRevisionPresentOnSomeRemote( + depPkg.vcsRevision) + +proc workingCopyNeeds*(dependencyPkg, dependentPkg: PackageInfo, + options: Options): NeedsOperation = + ## Be getting in consideration the information from the develop mode + ## dependency working copy directory, the lock file and the sync file + ## determines what kind of operation is needed to resolve the conflicts + ## if any. + + let + lockFileVcsRev = dependentPkg.lockedDeps.getOrDefault( + dependencyPkg.name, notSetLockFileDep).vcsRevision + syncFile = getSyncFile(dependentPkg) + syncFileVcsRev = syncFile.getDepVcsRevision(dependencyPkg.name) + workingCopyVcsRev = getVcsRevision(dependencyPkg.getNimbleFileDir) + + if lockFileVcsRev == syncFileVcsRev and syncFileVcsRev == workingCopyVcsRev: + # When all revisions are matching nothing have to be done. + return needsNone + + if lockFileVcsRev == syncFileVcsRev and syncFileVcsRev != workingCopyVcsRev: + # When lock file and sync file revisions are matching, but working copy + # revision is different, then most probably there are local changes and + # `nimble lock` is needed. + return needsLock + + if lockFileVcsRev != syncFileVcsRev and syncFileVcsRev == workingCopyVcsRev: + # When lock file revision is different from sync file revision, but sync + # file revision is equal to working copy revision then most probably we have + # `pull` executed but we forgot to call `nimble sync`. + return needsSync + + if lockFileVcsRev == workingCopyVcsRev and + workingCopyVcsRev != syncFileVcsRev: + # When lock file revision is equal to working copy revision, but they are + # different from sync file revision, most probably this is because of + # damaged sync file. Everything is Ok, because the sync file will be + # rewritten on the next `nimble lock` or `nimble sync` command. + return needsNone + + if lockFileVcsRev != syncFileVcsRev and + lockFileVcsRev != workingCopyVcsRev and + syncFileVcsRev != workingCopyVcsRev: + # When all revisions are different from one another this indicates that + # there are local changes which are conflicting with remote changes. The + # user have to resolve them manually by merging or rebasing. + return needsMerge + + assert false, "Here all cases are covered and the program " & + "flow must not reach this assert." + + return needsNone + +template addError(error: ValidationErrorKind) = + errors[depPkg.name] = ValidationError( + path: depPkg.getNimbleFileDir, kind: error) + +proc findValidationErrorsOfDevDepsWithLockFile*( + dependentPkg: PackageInfo, options: Options, + errors: var ValidationErrors) = + ## Collects validation errors for the develop mode dependencies with the + ## content of the lock file by getting in consideration the information from + ## the sync file. In the case of discrepancy, gives a useful advice what have + ## to be done to resolve the conflicts for the not matching packages. + + dependentPkg.assertIsLoaded + + let developDependencies = processDevelopDependencies(dependentPkg, options) + + for depPkg in developDependencies: + if depPkg.pkgDirIsNotUnderVersionControl: + addError(vekDirIsNotUnderVersionControl) + elif depPkg.workingCopyIsNotClean: + addError(vekWorkingCopyIsNotClean) + elif depPkg.vcsRevisionIsNotPushed: + addError(vekVcsRevisionIsNotPushed) + elif depPkg.workingCopyNeeds(dependentPkg, options) == needsSync: + addError(vekWorkingCopyNeedsSync) + elif depPkg.workingCopyNeeds(dependentPkg, options) == needsLock: + addError(vekWorkingCopyNeedsLock) + elif depPkg.workingCopyNeeds(dependentPkg, options) == needsMerge: + addError(vekWorkingCopyNeedsMerge) + +proc validationErrors*(errors: ValidationErrors): ref NimbleError = + result = nimbleError( + msg = errors.getValidationErrorsMessage, + hint = errors.getValidationsErrorsHint) + +proc validateDevelopFileAgainstLockFile( + dependentPkg: PackageInfo, options: Options) = + ## Does validation of the develop file dependencies against the data written + ## in the lock file. + + var errors: ValidationErrors + + findValidationErrorsOfDevDepsWithLockFile(dependentPkg, options, errors) + if errors.len > 0: + raise validationErrors(errors) + +proc validateDevelopFile*(dependentPkg: PackageInfo, options: Options) = + ## The procedure is used in the Nimble's `check` command to transitively + ## validate the contents of the develop files. + + discard load(dependentPkg, options, true, true) + if dependentPkg.areLockedDepsLoaded: + validateDevelopFileAgainstLockFile(dependentPkg, options) diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 5a0048e4..1942b0b9 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -28,6 +28,11 @@ const multiplePathOptionsGivenMsg* = "Multiple path options are given." + updatingTheLockFileMsg* = "Updating the lock file..." + generatingTheLockFileMsg* = "Generating the lock file..." + lockFileIsUpdatedMsg* = "The lock file is updated." + lockFileIsGeneratedMsg* = "The lock file is generated." + proc fileAlreadyExistsMsg*(path: string): string = &"Cannot create file \"{path}\" because it already exists." @@ -122,3 +127,9 @@ proc promptRemovePkgsMsg*(pkgs: seq[string]): string = result = "The following packages will be removed:\n" result &= pkgs.foldl(a & "\n" & b) result &= "\nDo you wish to continue?" + +proc pkgWorkingCopyNeedsSyncingMsg*(pkgName, pkgPath: string): string = + &"Package \"{pkgName}\" working copy at path \"{pkgPath}\" needs syncing." + +proc pkgWorkingCopyIsSyncedMsg*(pkgName, pkgPath: string): string = + &"Working copy of package \"{pkgName}\" at \"{pkgPath}\" is synced." diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 3a6da7f3..63ee5f39 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -9,17 +9,6 @@ from sequtils import toSeq, filterIt, map import packageinfotypes, packageparser, version, tools, common, options, cli, sha1hashes -type - DownloadMethod* {.pure.} = enum - git = "git", hg = "hg" - -proc getSpecificDir(meth: DownloadMethod): string {.used.} = - case meth - of DownloadMethod.git: - ".git" - of DownloadMethod.hg: - ".hg" - proc doCheckout(meth: DownloadMethod, downloadDir, branch: string) = case meth of DownloadMethod.git: @@ -123,13 +112,6 @@ proc getVersionList*(tags: seq[string]): OrderedTable[Version, string] = SortOrder.Descending) result = toOrderedTable[Version, string](taggedVers) -proc getDownloadMethod*(meth: string): DownloadMethod = - case meth - of "git": return DownloadMethod.git - of "hg", "mercurial": return DownloadMethod.hg - else: - raise nimbleError("Invalid download method: " & meth) - proc getHeadName*(meth: DownloadMethod): Version = ## Returns the name of the download method specific head. i.e. for git ## it's ``head`` for hg it's ``tip``. @@ -301,7 +283,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, [$verRange, $pkginfo.version]) proc echoPackageVersions*(pkg: Package) = - let downMethod = pkg.downloadMethod.getDownloadMethod() + let downMethod = pkg.downloadMethod case downMethod of DownloadMethod.git: try: @@ -315,7 +297,7 @@ proc echoPackageVersions*(pkg: Package) = echo(getCurrentExceptionMsg()) of DownloadMethod.hg: echo(" versions: (Remote tag retrieval not supported by " & - pkg.downloadMethod & ")") + $pkg.downloadMethod & ")") when isMainModule: import unittest diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim index bfa139ad..feb5536e 100644 --- a/src/nimblepkg/lockfile.nim +++ b/src/nimblepkg/lockfile.nim @@ -2,34 +2,30 @@ # BSD License. Look at license.txt for more info. import tables, os, json -import sha1hashes +import sha1hashes, packageinfotypes type - Checksums* = object - sha1*: Sha1Hash - - LockFileDependency* = object - version*: string - vcsRevision*: Sha1Hash - url*: string - downloadMethod*: string - dependencies*: seq[string] - checksums*: Checksums - - LockFileDependencies* = OrderedTable[string, LockFileDependency] - - LockFileJsonKeys = enum + LockFileJsonKeys* = enum lfjkVersion = "version" lfjkPackages = "packages" + lfjkPkgVcsRevision = "vcsRevision" const - lockFileName* = "nimble.lockfile" + lockFileName* = "nimble.lock" lockFileVersion = "0.1.0" +proc initLockFileDep(): LockFileDep = + result = LockFileDep( + vcsRevision: notSetSha1Hash, + checksums: Checksums(sha1: notSetSha1Hash)) + +const + notSetLockFileDep* = initLockFileDep() + proc lockFileExists*(dir: string): bool = fileExists(dir / lockFileName) -proc writeLockFile*(fileName: string, packages: LockFileDependencies, +proc writeLockFile*(fileName: string, packages: LockFileDeps, topologicallySortedOrder: seq[string]) = ## Saves lock file on the disk in topologically sorted order of the ## dependencies. @@ -45,20 +41,20 @@ proc writeLockFile*(fileName: string, packages: LockFileDependencies, writeFile(fileName, mainJsonNode.pretty) -proc writeLockFile*(packages: LockFileDependencies, +proc writeLockFile*(packages: LockFileDeps, topologicallySortedOrder: seq[string]) = writeLockFile(lockFileName, packages, topologicallySortedOrder) -proc readLockFile*(filePath: string): LockFileDependencies = +proc readLockFile*(filePath: string): LockFileDeps = {.warning[UnsafeDefault]: off.} {.warning[ProveInit]: off.} result = parseFile(filePath)[$lfjkPackages].to(result.typeof) {.warning[ProveInit]: on.} {.warning[UnsafeDefault]: on.} -proc readLockFileInDir*(dir: string): LockFileDependencies = +proc readLockFileInDir*(dir: string): LockFileDeps = readLockFile(dir / lockFileName) -proc getLockedDependencies*(dir: string): LockFileDependencies = +proc getLockedDependencies*(dir: string): LockFileDeps = if lockFileExists(dir): result = readLockFileInDir(dir) diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index b568b446..4505c026 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -44,10 +44,9 @@ type ActionType* = enum actionNil, actionRefresh, actionInit, actionDump, actionPublish, - actionInstall, actionSearch, - actionList, actionBuild, actionPath, actionUninstall, actionCompile, - actionDoc, actionCustom, actionTasks, actionDevelop, actionCheck, - actionLock, actionRun + actionInstall, actionSearch, actionList, actionBuild, actionPath, + actionUninstall, actionCompile, actionDoc, actionCustom, actionTasks, + actionDevelop, actionCheck, actionLock, actionRun, actionSync DevelopActionType* = enum datNewFile, datAdd, datRemoveByPath, datRemoveByName, datInclude, datExclude @@ -58,6 +57,8 @@ type case typ*: ActionType of actionNil, actionList, actionPublish, actionTasks, actionCheck, actionLock: nil + of actionSync: + listOnly*: bool of actionRefresh: optionalURL*: string # Overrides default package list. of actionInstall, actionPath, actionUninstall, actionDevelop: @@ -156,6 +157,10 @@ Commands: the name of an installed package. [--ini, --json] Selects the output format (the default is --ini). lock Generates or updates a package lock file. + sync Synchronizes develop mode dependencies with + the content of the lock file. + [-l, --list-only] Only lists the packages which are not synced + without actually performing the sync operation. Nimble Options: -h, --help Print this help message. @@ -231,6 +236,8 @@ proc parseActionType*(action: string): ActionType = result = actionCheck of "lock": result = actionLock + of "sync": + result = actionSync else: result = actionCustom @@ -522,6 +529,12 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = raise nimbleError(multiplePathOptionsGivenMsg) else: wasFlagHandled = false + of actionSync: + case f + of "l", "list-only": + result.action.listOnly = true + else: + wasFlagHandled = false else: wasFlagHandled = false diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 07f676eb..91dd78a3 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -18,6 +18,12 @@ proc initPackageInfo*(): PackageInfo = proc isLoaded*(pkgInfo: PackageInfo): bool = return pkgInfo.myPath.len > 0 +proc assertIsLoaded*(pkgInfo: PackageInfo) = + assert pkgInfo.isLoaded, "The package info must be loaded." + +proc areLockedDepsLoaded*(pkgInfo: PackageInfo): bool = + pkgInfo.lockedDeps.len > 0 + proc hasMetaData*(pkgInfo: PackageInfo): bool = # if the package info has loaded meta data its files list have to be not empty pkgInfo.files.len > 0 @@ -28,7 +34,7 @@ proc initPackageInfo*(filePath: string): PackageInfo = result.myPath = filePath result.name = fileName result.backend = "c" - result.lockedDependencies = getLockedDependencies(fileDir) + result.lockedDeps = getLockedDependencies(fileDir) proc toValidPackageName*(name: string): string = for c in name: @@ -61,6 +67,13 @@ proc requiredField(obj: JsonNode, name: string): string = raise nimbleError( "Package in packages.json file does not contain a " & name & " field.") +proc parseDownloadMethod*(meth: string): DownloadMethod = + case meth + of "git": return DownloadMethod.git + of "hg", "mercurial": return DownloadMethod.hg + else: + raise nimbleError("Invalid download method: " & meth) + proc fromJson(obj: JSonNode): Package = ## Constructs a Package object from a JSON node. ## @@ -72,7 +85,7 @@ proc fromJson(obj: JSonNode): Package = result.alias = "" result.version = obj.optionalField("version") result.url = obj.requiredField("url") - result.downloadMethod = obj.requiredField("method") + result.downloadMethod = obj.requiredField("method").parseDownloadMethod result.dvcsTag = obj.optionalField("dvcs-tag") result.license = obj.requiredField("license") result.tags = @[] @@ -360,7 +373,7 @@ proc echoPackage*(pkg: Package) = if pkg.alias.len > 0: echo(" Alias for ", pkg.alias) else: - echo(" url: " & pkg.url & " (" & pkg.downloadMethod & ")") + echo(" url: " & pkg.url & " (" & $pkg.downloadMethod & ")") echo(" tags: " & pkg.tags.join(", ")) echo(" description: " & pkg.description) echo(" license: " & pkg.license) diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index cb18f975..713af6cc 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -2,9 +2,25 @@ # BSD License. Look at license.txt for more info. import sets, tables -import version, lockfile, aliasthis, sha1hashes +import version, aliasthis, sha1hashes type + DownloadMethod* {.pure.} = enum + git = "git", hg = "hg" + + Checksums* = object + sha1*: Sha1Hash + + LockFileDep* = object + version*: string + vcsRevision*: Sha1Hash + url*: string + downloadMethod*: DownloadMethod + dependencies*: seq[string] + checksums*: Checksums + + LockFileDeps* = OrderedTable[string, LockFileDep] + PackageMetaDataBase* {.inheritable.} = object url*: string vcsRevision*: Sha1Hash @@ -50,7 +66,7 @@ type backend*: string foreignDeps*: seq[string] basicInfo*: PackageBasicInfo - lockedDependencies*: LockFileDependencies + lockedDeps*: LockFileDeps metaData*: PackageMetaData Package* = object ## Definition of package from packages.json. @@ -58,7 +74,7 @@ type name*: string url*: string # Download location. license*: string - downloadMethod*: string + downloadMethod*: DownloadMethod description*: string tags*: seq[string] # Even if empty, always a valid non nil seq. \ # From here on, optional fields set to the empty string if not available. diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index f20c5ef7..5c438dd8 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -4,7 +4,8 @@ import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map, toSeq import common, version, tools, nimscriptwrapper, options, cli, sha1hashes, - packagemetadatafile, packageinfo, packageinfotypes, checksum + packagemetadatafile, packageinfo, packageinfotypes, checksums, vcstools, + paths ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` ## because it depends on ``nimscriptwrapper`` (``nimscriptwrapper`` also @@ -185,7 +186,6 @@ proc validatePackageInfo(pkgInfo: PackageInfo, options: Options) = validatePackageStructure(pkginfo, options) - proc nimScriptHint*(pkgInfo: PackageInfo) = if not pkgInfo.isNimScript: display("Warning:", "The .nimble file for this project could make use of " & @@ -382,8 +382,8 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): # If the `.nimble` file is not in the installation directory but in the # package repository we have to get its VCS revision and to calculate its # checksum. - result.vcsRevision = getVcsRevisionFromDir(fileDir) - result.checksum = calculatePackageSha1Checksum(fileDir) + result.vcsRevision = getVcsRevision(fileDir.Path) + result.checksum = calculateDirSha1Checksum(fileDir) # By default specialVersion is the same as version. result.specialVersion = result.version else: diff --git a/src/nimblepkg/paths.nim b/src/nimblepkg/paths.nim index 4f4a08c5..9b773ba2 100644 --- a/src/nimblepkg/paths.nim +++ b/src/nimblepkg/paths.nim @@ -23,6 +23,9 @@ proc parseFile*(filename: Path): JsonNode {.borrow.} proc `/`*(head, tail: Path): Path {.borrow.} proc writeFile*(filename: Path, content: string) {.borrow.} proc len*(path: Path): int {.borrow.} +proc isRootDir*(path: Path): bool {.borrow.} +proc parentDir*(path: Path): Path {.borrow.} +proc quoteShell*(s: Path): Path {.borrow.} proc hash*(path: Path): Hash = hash(absolutePath(string(path))) diff --git a/src/nimblepkg/sha1hashes.nim b/src/nimblepkg/sha1hashes.nim index bcc8691d..c776b3e3 100644 --- a/src/nimblepkg/sha1hashes.nim +++ b/src/nimblepkg/sha1hashes.nim @@ -1,7 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import strformat, strutils, json +import strformat, strutils, json, std/sha1, hashes import common type @@ -13,29 +13,28 @@ type ## procedure which validates the input. hashValue: string +const + notSetSha1Hash* = Sha1Hash(hashValue: "") + template `$`*(sha1Hash: Sha1Hash): string = sha1Hash.hashValue template `%`*(sha1Hash: Sha1Hash): JsonNode = %sha1Hash.hashValue template `==`*(lhs, rhs: Sha1Hash): bool = lhs.hashValue == rhs.hashValue +template hash*(sha1Hash: Sha1Hash): Hash = sha1Hash.hashValue.hash proc invalidSha1Hash(value: string): ref InvalidSha1HashError = ## Creates a new exception object for an invalid sha1 hash value. result = newNimbleError[InvalidSha1HashError]( &"The string '{value}' does not represent a valid sha1 hash value.") -proc validateSha1Hash(value: string): bool = +proc isValidSha1Hash(value: string): bool = ## Checks whether given string is a valid sha1 hash value. Only lower case ## hexadecimal digits are accepted. - if value.len == 0: - # Empty string is used as a special value for not set sha1 hash. - return true if value.len != 40: - # Valid sha1 hash must be exactly 40 characters long. + # A valid sha1 hash should be 40 characters long string. return false for c in value: if c notin {'0' .. '9', 'a'..'f'}: - # All characters of valid sha1 hash must be hexadecimal digits with lower - # case letters for digits representing numbers between 10 and 15 - # ('a' to 'f'). + # It also should contain only lower case hexadecimal digits. return false return true @@ -44,14 +43,13 @@ proc initSha1Hash*(value: string): Sha1Hash = ## lower case and validating the transformed value. In the case the supplied ## string is not a valid sha1 hash value then raises an `InvalidSha1HashError` ## exception. + if value == "": + return notSetSha1Hash let value = value.toLowerAscii - if not validateSha1Hash(value): + if not isValidSha1Hash(value): raise invalidSha1Hash(value) return Sha1Hash(hashValue: value) -const - notSetSha1Hash* = initSha1Hash("") - proc initFromJson*(dst: var Sha1Hash, jsonNode: JsonNode, jsonPath: var string) = case jsonNode.kind @@ -66,12 +64,12 @@ when isMainModule: import unittest test "validate sha1": - check validateSha1Hash("") - check not validateSha1Hash("9") - check not validateSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358g7") - check not validateSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b") - check not validateSha1Hash("99345CE680CD3E48ACDB9AB4212E4BD9BF9358B7") - check validateSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b7") + check not isValidSha1Hash("") + check not isValidSha1Hash("9") + check not isValidSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358g7") + check not isValidSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b") + check not isValidSha1Hash("99345CE680CD3E48ACDB9AB4212E4BD9BF9358B7") + check isValidSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b7") test "init sha1": check initSha1Hash("") == notSetSha1Hash diff --git a/src/nimblepkg/syncfile.nim b/src/nimblepkg/syncfile.nim new file mode 100644 index 00000000..7e0015d4 --- /dev/null +++ b/src/nimblepkg/syncfile.nim @@ -0,0 +1,109 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +## This module implement operations on a special `sync` file which is being kept +## in the hidden special VCS directory. It is used to keep the revisions of the +## package's develop mode dependencies at the time when the last `lock` or +## `sync` operation had been performed. The file is used to determine whether a +## new `lock` or `sync` command or a VCS `merge` or `rebase` command is needed +## when there is a conflict between the data written in it and the data from the +## lock file and from the working copy. + +import tables, json, os +import common, sha1hashes, paths, vcstools, packageinfotypes + +type + SyncFileData = Table[string, Sha1Hash] + # Maps develop mode dependency name to the VCS revision it has in the time + # of the last `lock` or `sync` operation or when it is added as a develop + # mode dependency if there is no such operations after that moment. + + SyncFile = object + path: Path + data: SyncFileData + + SyncFileJsonKeys = enum + ## Represents the keys for the `sync` file Json objects. + lsfjkVersion = "version" + lsfjkData = "data" + +const + syncFileExt = ".nimble.sync" + syncFileVersion = "0.1.0" + +proc getPkgDir(pkgInfo: PackageInfo): string = + pkgInfo.myPath.splitFile.dir + +proc getSyncFilePath(pkgInfo: PackageInfo): Path = + ## Returns a path to the sync file for package `pkgInfo`. + + let (vcsType, vcsSpecialDirPath) = + # Do not use `pkgInfo.getNimbleFileDir` in order to avoid circular + # dependencies. + getVcsTypeAndSpecialDirPath(pkgInfo.getPkgDir) + + if vcsType == vcsTypeNone: + # The directory is not under version control, and we have not a place where + # to hide the sync file. + raise nimbleError( + msg = "Sync file require current working directory to be under some " & + "supported type of version control.", + hint = "Put package's working directory under version control.") + + return vcsSpecialDirPath / (pkgInfo.name & syncFileExt).Path + +proc load(syncFile: ref SyncFile, path: Path) = + ## Loads a sync file. + + syncFile.path = path + if not path.fileExists: + return + + {.warning[UnsafeDefault]: off.} + {.warning[ProveInit]: off.} + syncFile.data = parseFile(path)[$lsfjkData].to(SyncFileData) + {.warning[ProveInit]: on.} + {.warning[UnsafeDefault]: on.} + +proc getSyncFile*(pkgInfo: PackageInfo): ref SyncFile = + # Returns a reference to the sync file data of the current working directory + # package `pkgInfo`. + + assert pkgInfo.getPkgDir == getCurrentDir(): + "The package `pkgInfo` must be the current working directory package." + + var syncFile {.global.}: ref SyncFile + once: + syncFile.new + let path = getSyncFilePath(pkgInfo) + syncFile.load(path) + return syncFile + +proc save*(syncFile: ref SyncFile) = + ## Saves a sync file. + + let jsonNode = %{ + $lsfjkVersion: %syncFileVersion, + $lsfjkData: %syncFile.data, + } + + writeFile(syncFile.path, jsonNode.pretty) + +proc getDepVcsRevision*(syncFile: ref SyncFile, depName: string): Sha1Hash = + ## Returns the revision written in the sync file for develop mode dependency + ## `depName`. + syncFile.data.getOrDefault(depName, notSetSha1Hash) + +proc setDepVcsRevision*(syncFile: ref SyncFile, depName: string, + vcsRevision: Sha1Hash) = + ## Sets the revision in the sync file for the develop mode dependency + ## `depName` to be equal to `vcsRevision`. + + syncFile.data[depName] = vcsRevision + +proc clear*(syncFile: ref SyncFile) = + ## Clears all the data from the sync file. + + {.warning[UnsafeDefault]: off.} + syncFile.data.clear + {.warning[UnsafeDefault]: on.} diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 4ac70643..a30f658b 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -50,11 +50,12 @@ proc doCmdEx*(cmd: string): ProcessOutput = raise nimbleError("'" & bin & "' not in PATH.") return execCmdEx(cmd) -proc tryDoCmdEx*(cmd: string): string = +proc tryDoCmdEx*(cmd: string): string {.discardable.} = let (output, exitCode) = doCmdEx(cmd) if exitCode != QuitSuccess: raise nimbleError( - &"Execution of '{cmd}' failed with an exit code {exitCode}") + &"Execution of '{cmd}' failed with an exit code {exitCode}.\n" & + &"Details: {output}") return output proc getNimBin*: string = @@ -177,23 +178,6 @@ proc getNimbleUserTempDir*(): string = tmpdir = getTempDir() return tmpdir - -proc getVcsRevisionFromDir*(dir: string): Sha1Hash = - ## Returns current revision number of HEAD if dir is inside VCS, or an empty - ## string in case of failure. - - template tryToGetRevision(command: string): untyped = - try: - let (output, exitCode) = doCmdEx(command) - if exitCode == QuitSuccess: - return initSha1Hash(output.strip()) - except: - discard - - result = notSetSha1Hash - tryToGetRevision("git -C " & quoteShell(dir) & " rev-parse HEAD") - tryToGetRevision("hg --cwd " & quoteShell(dir) & " id -i") - proc isEmptyDir*(dir: string): bool = toSeq(walkDirRec(dir)).len == 0 diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index 6f748b6e..8aa4297a 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -2,15 +2,15 @@ # BSD License. Look at license.txt for more info. import sequtils, sugar, tables, strformat, algorithm, sets -import packageinfotypes, packageinfo, options, cli, lockfile +import packageinfotypes, packageinfo, options, cli proc buildDependencyGraph*(packages: HashSet[PackageInfo], options: Options): - LockFileDependencies = + LockFileDeps = ## Creates records which will be saved to the lock file. for pkgInfo in packages: let pkg = getPackage(pkgInfo.name, options) - result[pkgInfo.name] = LockFileDependency( + result[pkgInfo.name] = LockFileDep( version: pkgInfo.version, vcsRevision: pkgInfo.vcsRevision, url: pkg.url, @@ -19,7 +19,7 @@ proc buildDependencyGraph*(packages: HashSet[PackageInfo], options: Options): pkg => pkg.name).filter(name => name != "nim"), checksums: Checksums(sha1: pkgInfo.checksum)) -proc topologicalSort*(graph: LockFileDependencies): +proc topologicalSort*(graph: LockFileDeps): tuple[order: seq[string], cycles: seq[seq[string]]] = ## Topologically sorts dependency graph which will be saved to the lock file. ## @@ -100,8 +100,8 @@ when isMainModule: import unittest from sha1hashes import notSetSha1Hash - proc initLockFileDependency(deps: seq[string] = @[]): LockFileDependency = - result = LockFileDependency( + proc initLockFileDep(deps: seq[string] = @[]): LockFileDep = + result = LockFileDep( vcsRevision: notSetSha1Hash, dependencies: deps, checksums: Checksums(sha1: notSetSha1Hash)) @@ -111,13 +111,13 @@ when isMainModule: test "graph without cycles": let graph = { - "json_serialization": initLockFileDependency( + "json_serialization": initLockFileDep( @["serialization", "stew"]), - "faststreams": initLockFileDependency(@["stew"]), - "testutils": initLockFileDependency(), - "stew": initLockFileDependency(), - "serialization": initLockFileDependency(@["faststreams", "stew"]), - "chronicles": initLockFileDependency( + "faststreams": initLockFileDep(@["stew"]), + "testutils": initLockFileDep(), + "stew": initLockFileDep(), + "serialization": initLockFileDep(@["faststreams", "stew"]), + "chronicles": initLockFileDep( @["json_serialization", "testutils"]) }.toOrderedTable @@ -134,11 +134,11 @@ when isMainModule: test "graph with cycles": let graph = { - "A": initLockFileDependency(@["B", "E"]), - "B": initLockFileDependency(@["A", "C"]), - "C": initLockFileDependency(@["D"]), - "D": initLockFileDependency(@["B"]), - "E": initLockFileDependency(@["D", "E"]) + "A": initLockFileDep(@["B", "E"]), + "B": initLockFileDep(@["A", "C"]), + "C": initLockFileDep(@["D"]), + "D": initLockFileDep(@["B"]), + "E": initLockFileDep(@["D", "E"]) }.toOrderedTable expectedTopologicallySortedOrder = @["D", "C", "B", "E", "A"] diff --git a/src/nimblepkg/vcstools.nim b/src/nimblepkg/vcstools.nim new file mode 100644 index 00000000..cbc55f84 --- /dev/null +++ b/src/nimblepkg/vcstools.nim @@ -0,0 +1,1023 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +## This module implements some operations which use version control system +## tools like Git and Mercurial on which Nimble depends. + +import tables, strutils, strformat, os, sets +import common, paths, tools, sha1hashes + +type + VcsType* = enum + ## This type represents a marker for the type of VCS under which is some + ## file system directory. + vcsTypeNone = "none" + vcsTypeGit = "git" + vcsTypeHg = "hg" + + VcsTypeAndSpecialDirPath = tuple[vcsType: VcsType, path: Path] + ## Represents a cache entry for the directory VCS type and VCS special + ## directory path used by `getVcsTypeAndSpecialDirPath` procedure. + +const + noVcsSpecialDir = "" + gitSpecialDir = ".git" + hgSpecialDir = ".hg" + + noVcsDefaultBranch = "" + gitDefaultBranch = "master" + hgDefaultBranch = "default" + + noVcsDefaultRemote = "" + gitDefaultRemote = "origin" + hgDefaultRemote = "default" + +proc getVcsSpecialDir*(vcsType: VcsType): string = + ## Returns a special dir for given VCS type or an empty string for + ## `vcsTypeNone`. + return case vcsType + of vcsTypeNone: noVcsSpecialDir + of vcsTypeGit: gitSpecialDir + of vcsTypeHg: hgSpecialDir + +proc getVcsDefaultBranchName*(vcsType: VcsType): string = + ## Returns the name of the default branch for given VCS. + return case vcsType + of vcsTypeNone: noVcsDefaultBranch + of vcsTypeGit: gitDefaultBranch + of vcsTypeHg: hgDefaultBranch + +proc getVcsDefaultRemoteName*(vcsType: VcsType): string = + return case vcsType + of vcsTypeNone: noVcsDefaultRemote + of vcsTypeGit: gitDefaultRemote + of vcsTypeHg: hgDefaultRemote + +proc dirDoesNotExistErrorMsg(dir: Path): string = + &"The directory \"{dir}\" does not exist." + +proc hasVcsSubDir*(dir: Path): VcsType = + ## Checks whether a directory has a special subdirectory for some supported + ## kind of VCS. + if (dir / gitSpecialDir.Path).dirExists: + result = vcsTypeGit + elif (dir / hgSpecialDir.Path).dirExists: + result = vcsTypeHg + else: + result = vcsTypeNone + +proc getVcsTypeAndSpecialDirPath*(dir: Path): VcsTypeAndSpecialDirPath = + ## By given directory `dir` gets the type of VCS under which is it by + ## traversing the parent directories until some specific directory like + ## `.git`, `.hg` or the root of the file system is found. Additionally it + ## returns the path to the VCS special directory if the directory `dir is + ## under some supported VCS type. + ## + ## The procedure uses a in memory cache to bypass multiple checks for the same + ## directory in single run of Nimble. + ## + ## Raises a `NimbleError` in the case the directory `dir` does not exist. + + var cache {.global.}: Table[Path, VcsTypeAndSpecialDirPath] + if cache.hasKey(dir): + return cache[dir] + + if not dir.dirExists: + raise nimbleError(dirDoesNotExistErrorMsg(dir)) + + var + dirIter = dir + vcsType = vcsTypeNone + + while not dirIter.isRootDir: + vcsType = hasVcsSubDir(dirIter) + if vcsType != vcsTypeNone: + break + dirIter = dirIter.parentDir + + if vcsType == vcsTypeNone: + vcsType = hasVcsSubDir(dirIter) + else: + dirIter = dirIter / vcsType.getVcsSpecialDir.Path + + result = (vcsType, dirIter) + cache[dir] = result + +proc getVcsType*(dir: Path): VcsType = + ## Returns VCS type of the given directory. + ## Raises a `NimbleError` in the case the directory `dir` does not exist. + dir.getVcsTypeAndSpecialDirPath.vcsType + +proc git(path: Path): string = + ## Returns string for Git call at specific path `path`. + &"git -C {path.quoteShell}" + +proc hg(path: Path): string = + ## Returns string for Mercurial call at specific path `path`. + &"hg --cwd {path.quoteShell}" + +proc dirInNotUnderSourceControlErrorMsg*(dir: Path): string = + &"The directory \"{dir}\" is not under source control." + +template doVcsCmdImpl(dir: Path, gitCmd, hgCmd: string, + doCmd, noVcsAction: untyped): untyped = + ## This is a helper template for executing Git or Mercurial external command + ## `gitCmd` or `hgCmd` in the directory `dir` according to the type of version + ## control applied to the directory via some procedure `doCmd` or executing + ## some other action `noVcsAction` in the case it is not under some of the + ## supported VCS types. + + case getVcsType(dir) + of vcsTypeGit: + `doCmd`(git(dir) & " " & gitCmd) + of vcsTypeHg: + `doCmd`(hg(dir) & " " & hgCmd) + of vcsTypeNone: + `noVcsAction` + +template doVcsCmdImpl(dir: Path, gitCmd, hgCmd: string, + doCmd: untyped): untyped = + ## This is a helper template for executing Git or Mercurial external command + ## `gitCmd` or `hgCmd` in the directory `dir` according to the type of version + ## control applied to the directory via some procedure `doCmd` or raising a + ## `NimbleError` in the case it is not under some of the supported VCS types. + + doVcsCmdImpl(dir, gitCmd, hgCmd, doCmd): + raise nimbleError(dirInNotUnderSourceControlErrorMsg(dir)) + +template doVcsCmd(dir: Path, gitCmd, hgCmd: string): untyped = + ## This is a helper template for executing Git or Mercurial external command + ## `gitCmd` or `hgCmd` in the directory `dir` according to the type of version + ## control applied to the directory via `doCmdEx` procedure or raising a + ## `NimbleError` in the case it is not under some of the supported VCS types. + + doVcsCmdImpl(dir, gitCmd, hgCmd): doCmdEx + +template tryDoVcsCmd(dir: Path, gitCmd, hgCmd: string, + noVcsAction: untyped): untyped = + ## This is a helper template for executing Git or Mercurial external command + ## `gitCmd` or `hgCmd` in the directory `dir` according to the type of version + ## control applied to the directory via `tryDoCmdEx` procedure or executing + ## some other action `noVcsAction` in the case it is not under some of the + ## supported VCS types. + + doVcsCmdImpl(dir, gitCmd, hgCmd): + tryDoCmdEx + do: + `noVcsAction` + +template tryDoVcsCmd(dir: Path, gitCmd, hgCmd: string): untyped = + ## This is a helper template for executing Git or Mercurial external command + ## `gitCmd` or `hgCmd` in the directory `dir` according to the type of version + ## control applied to the directory via `tryDoCmdEx` procedure or raising a + ## `NimbleError` in the case it is not under some of the supported VCS types. + + doVcsCmdImpl(dir, gitCmd, hgCmd): tryDoCmdEx + +proc getVcsRevision*(dir: Path): Sha1Hash = + ## Returns current revision number if the directory `dir` is under version + ## control, or an invalid Sha1 checksum otherwise. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - there is no vcsRevisions in the repository. + + let vcsRevision = tryDoVcsCmd(dir, + gitCmd = "rev-parse HEAD", + hgCmd = "id -i --debug", + noVcsAction = $notSetSha1Hash) + + return initSha1Hash(vcsRevision.strip(chars = Whitespace + {'+'})) + +proc getPackageFileListWithoutVcs(dir: Path): seq[string] = + ## Recursively walks the directory `dir` and returns a list of files in it and + ## its subdirectories. + for file in walkDirRec($dir, relative = true): + result.add file + +proc getPackageFileList*(dir: Path): seq[string] = + ## Retrieves a sequence of file names from the directory `dir` and its + ## sub-directories by trying to get it from Git, Mercurial or directly from + ## the file system if both fail. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + + const noVcsOutput = "/" + + let output = tryDoVcsCmd(dir, + gitCmd = "ls-files", + hgCmd = "manifest", + noVcsAction = noVcsOutput) + + return + if output != noVcsOutput: + output.strip.splitLines + else: + dir.getPackageFileListWithoutVcs + +proc isWorkingCopyClean*(path: Path): bool = + ## Checks whether a repository at path `path` has a clean working copy. Do + ## not consider untracked and ignored files. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + let output = tryDoVcsCmd(path, + gitCmd = "status --untracked-files=no --porcelain", + hgCmd = "status -q --color=off") + return output.strip.len == 0 + +proc getRemotesNames*(path: Path): seq[string] = + ## Retrieves a sequence with the names of the set remotes for the repository + ## at path `path`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + let output = tryDoVcsCmd(path, + gitCmd = "remote", + hgCmd = "paths -q").strip + + if output.len > 0: + result = output.splitLines + +proc getRemotePushUrl*(path: Path, remoteName: string): string = + ## Retrieves a push URL for the remote with name `remoteName` set in + ## repository at path `repositoryPath`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + result = tryDoVcsCmd(path, + gitCmd = &"remote get-url --push {remoteName}", + hgCmd = &"paths {remoteName}") + + return result.strip + +proc getRemotesPushUrls*(path: Path): seq[string] = + ## Retrieves a sequence with the push URLs of the set remotes for the + ## repository at path `path`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + let remotesNames = path.getRemotesNames + result = newSeqOfCap[string](remotesNames.len) + for remote in remotesNames: + result.add getRemotePushUrl(path, remote) + +proc isVcsRevisionPresentOnSomeRemote*( + path: Path, vcsRevision: Sha1Hash): bool = + ## Checks whether a VCS revision `vcsRevision` is present on some of the + ## remotes of the repository at path `path`. + ## + ## Raises a `NimbleError` if: + ## - the external source control tool is not found. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + # Note: When `--depth=1` is missing the git command returns success even the + # revision is present only locally, but when it is present dispute being + # `--dry-run` the code below for some reason corrupts the working copy of the + # Git repository living it in a grafted state and for this reason another + # solution must be found. It seems like a bug in Git. + + # for remotePushUrl in path.getRemotesPushUrls: + # let + # remotePushUrl = remotePushUrl.quoteShell + # (_, exitCode) = doVcsCmd(path, + # gitCmd = &"fetch {remotePushUrl} {vcsRevision} -q --dry-run", + # hgCmd = &"pull {remotePushUrl} -r {vcsRevision} -q") + # if exitCode == QuitSuccess: + # return true + + let vcsType = path.getVcsType + if vcsType == vcsTypeGit: + for remotePushUrl in path.getRemotesPushUrls: + let + remotePushUrl = remotePushUrl.quoteShell + (_, fetchCmdExitCode) = doCmdEx(&"{git(path)} fetch {remotePushUrl}") + if fetchCmdExitCode == QuitFailure: + continue + + let (branchCmdOutput, branchCmdExitCode) = doCmdEx( + &"{git(path)} branch -r --contains {vcsRevision}") + if branchCmdExitCode == QuitSuccess and branchCmdOutput.len > 0: + return true + elif vcsType == vcsTypeHg: + for remotePushUrl in path.getRemotesPushUrls: + let + remotePushUrl = remotePushUrl.quoteShell + (_, exitCode) = doCmdEx( + &"{hg(path)} pull {remotePushUrl} -r {vcsRevision} -q") + if exitCode == QuitSuccess: + return true + else: + raise nimbleError(dirInNotUnderSourceControlErrorMsg(path)) + +proc getCurrentBranch*(path: Path): string = + ## Get the name of the current branch for the VCS repository at path `path`. + ## Returns an empty string in the case the repository is in a detached state. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + result = tryDoVcsCmd(path, + gitCmd = "branch --show-current", + hgCmd = "branch") + + return result.strip + +type + BranchType* = enum + ## Determines the branch type which to be queried. + btLocal, btRemoteTracking, btBoth + +proc getBranchesOnWhichVcsRevisionIsPresent*( + path: Path, vcsRevision: Sha1Hash, branchType = btBoth): HashSet[string] = + ## Returns a set of the names of all branches which contain revision + ## `vcsRevision` for a repository at path `path`. If the VCS system is Git + ## `branchType` determines which branches to be returned: local branches, + ## remote tracking branches or both. The parameter has no effect for + ## Mercurial repositories. + ## + ## Note: In Mercurial a revision is present always only on a single branch. + ## For this reason we are searching for all branches where the revision is + ## found as an ancestor of some revision of the branch. + ## + ## Raises a `NimbleError` if: + ## - the external source control tool is not found. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + let + branchTypeParam = case branchType + of btLocal: "" + of btRemoteTracking: "-r" + of btBoth: "-a" + (output, errorCode) = doVcsCmd(path, + gitCmd = &"branch {branchTypeParam} --no-color --contains {vcsRevision}", + hgCmd = &"log -r {vcsRevision}:: -T '{{branch}}\\n'") + + if errorCode != QuitSuccess or output.len == 0: + # If the VCS revision is not found in any local branch Git exits with + # failure, but Mercurial exits with success and an empty output. In both + # cases we are returning an empty set. + return + + let vcsType = path.getVcsType + for line in output.strip.splitLines: + var line = line.strip(chars = Whitespace + {'*'}) + if vcsType == vcsTypeGit and branchType == btBoth: + # For "git branch -a" remote branches are starting with "remotes" which + # have to be removed for uniformity with "git branch -r". + const prefix = "remotes/" + if line.startsWith(prefix): + line = line[prefix.len .. ^1] + result.incl line + +proc isVcsRevisionPresentOnSomeBranch*(path: Path, vcsRevision: Sha1Hash): + bool = + ## Checks whether a given VCS revision `vcsRevision` is found on any local + ## branch of the repository at path `path`. + ## + ## Raises a `NimbleError` if: + ## - the external source control tool is not found. + ## - the directory does not exist. + ## - the directory is not under supported VCS type + + getBranchesOnWhichVcsRevisionIsPresent(path, vcsRevision).len > 0 + +proc isVcsRevisionPresentOnBranch*( + path: Path, vcsRevision: Sha1Hash, branchName: string): bool = + ## Checks whether a given VCS revision `vcsRevision` is present on a branch + ## with name `branchName` in a repository at path `path`. Returns `true` if + ## so or `false` if either the branch don't exist or it does not contain the + ## revision. + ## + ## Raises a `NimbleError` if: + ## - the external source control tool is not found. + ## - the directory does not exist. + ## - the directory is not under supported VCS type + + let branches = getBranchesOnWhichVcsRevisionIsPresent(path, vcsRevision) + return branches.contains(branchName) + +proc retrieveRemoteChangeSets*(path: Path, remoteName, branchName: string) = + ## Retrieve remote `remoteName` and branch `branchName` change sets for the + ## repository at path `path`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + discard tryDoVcsCmd(path, + gitCmd = &"fetch {remoteName} {branchName}", + hgCmd = &"pull {remoteName} -b {branchName}") + +proc retrieveRemoteChangeSets*(path: Path, remoteName: string) = + ## Retrieve remote `remoteName` change sets for the repository at path `path`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + discard tryDoVcsCmd(path, + gitCmd = &"fetch {remoteName}", + hgCmd = &"pull {remoteName}") + +proc retrieveRemoteChangeSets*(path: Path) = + ## Retrieves all change sets for the repository at path `path` from every + ## remote and every branch. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + for remote in getRemotesNames(path): + retrieveRemoteChangeSets(path, remote) + +proc setWorkingCopyToVcsRevision*(path: Path, vcsRevision: Sha1Hash) = + ## Sets working copy of a repository at path `path` to have active a + ## particular VCS revision `vcsRevision`. + ## + ## Note: This is a detached state in the case of Git or a revision's branch + ## in the case of Mercurial. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + discard tryDoVcsCmd(path, + gitCmd = &"checkout {vcsRevision}", + hgCmd = &"update {vcsRevision}") + +proc setCurrentBranchToVcsRevision*(path: Path, vcsRevision: Sha1Hash) = + ## Changes the current VCS revision for repository at path `path`. + ## + ## - For Git sets a current branch HEAD to point to the given VCS revision. + ## + ## - For Mercurial just updates the working copy to the given VCS revision, + ## because in Mercurial the branch is part of the commit meta data. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + discard tryDoVcsCmd(path, + gitCmd = &"reset --hard {vcsRevision}", + hgCmd = &"update {vcsRevision}") + +proc switchBranch*(path: Path, branchName: string) = + ## Switches the current working copy at path `path` branch to `branchName`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + + discard tryDoVcsCmd(path, + gitCmd = &"checkout {branchName}", + hgCmd = &"update {branchName}") + +proc getCorrespondingRemoteAndBranch*(path: Path): + tuple[remote, branch: string] = + ## Gets the name of the remote and the remote branch which current branch of + ## repository at path `path` tracks. If there is no such returns the default + ## remote and current branch names. + ## + ## Note: For Mercurial there is no such thing like remote tracing branch and + ## this procedure always returns the default remote and current branch name. + ## + ## Raises a `NimbleError` if: + ## - the external source control tool is not found. + ## - the directory does not exist. + ## - the directory is not under supported VCS type + + var + output: string + exitCode: int + + let vcsType = path.getVcsType + case vcsType + of vcsTypeGit: + (output, exitCode) = doCmdEx(git(path) & + " rev-parse --abbrev-ref --symbolic-full-name @{u}") + of vcsTypeHg: + (output, exitCode) = ("", QuitFailure) + of vcsTypeNone: + raise nimbleError(dirInNotUnderSourceControlErrorMsg(path)) + + if exitCode == QuitSuccess: + # Separate the remote name from the branch name. + let remotes = path.getRemotesNames + let output = output.strip + for remote in remotes: + if output.startsWith(remote): + return (remote, output[remote.len + 1 .. ^1]) + else: + return (vcsType.getVcsDefaultRemoteName, path.getCurrentBranch) + +proc hasCorrespondingRemoteBranch*(path: Path, remoteBranches: HashSet[string]): + tuple[hasBranch: bool, branchName: string] = + # If the directory at path `path` is a Git repository and its current branch + # has corresponding remote tracking branch in the provided set + # `remoteBranches` returns `true` and the name of the branch or `false` and an + # empty string otherwise. + + if path.getVcsType != vcsTypeGit: + return (false, "") + var (output, exitCode) = doCmdEx(git(path) & + " rev-parse --abbrev-ref --symbolic-full-name @{u}") + output = output.strip + result.hasBranch = exitCode == QuitSuccess and output in remoteBranches + if result.hasBranch: + result.branchName = output + +proc assertIsGitRepository(path: Path) = + assert path.getVcsType == vcsTypeGit, + "This procedure makes sense only for a Git repositories." + +proc getLocalBranchesTrackingRemoteBranch*(path: Path, remoteBranch: string): + seq[string] = + ## By given path to a Git repository and a remote tracking branch name + ## returns a sequence with all local branches which track the remote branch. + path.assertIsGitRepository + let output = tryDoCmdEx(git(path) & + &" for-each-ref --format=\"%(if:equals={remoteBranch})%(upstream:short)%" & + "(then)%(refname:short)%(end)\" refs/heads").strip + if output.len > 0: + output.split('\n') + else: + @[] + +proc getLocalBranchName*(path: Path, remoteBranch: string): string = + ## By given path to a Git repository and name of a remote branch returns a new + ## name which to be used for a local branch name which consists of the name + ## of the remote branch without a remote name prefix. For example: + ## + ## * "origin/master" -> "master" + ## * "upstream/feature/lock-file" -> "feature/lock-file" + + path.assertIsGitRepository + let remotes = path.getRemotesNames + for remote in remotes: + if remoteBranch.startsWith(remote): + return remoteBranch[remote.len + 1 .. ^1] + +proc fastForwardMerge*(path: Path, remoteBranch, localBranch: string) = + ## Tries to fast forward merge a remote branch `remoteBranch` to a local + ## branch `localBranch` in a Git repository at path `path`. + path.assertIsGitRepository + let currentBranch = path.getCurrentBranch + tryDoCmdEx(&"{git(path)} checkout --detach") + tryDoCmdEx(&"{git(path)} fetch . {remoteBranch}:{localBranch}") + if currentBranch.len > 0: + tryDoCmdEx(&"{git(path)} checkout {currentBranch}") + +when isMainModule: + import unittest, std/sha1, sequtils, os + + type + NameToVcsRevision = OrderedTable[string, Sha1Hash] + ## Maps some user supplied string id to VCS commit revision id. + + const + tempDir = getTempDir() + testGitDir = tempDir / "testGitDir" + testHgDir = tempDir / "testHgDir" + testNoVcsDir = tempDir / "testNoVcsDir" + testSubDir = "./testSubDir" + testFile = "test.txt" + testFile2 = "test2.txt" + testFileContent = "This is a test file.\n" + testSubDirFile = testSubDir / testFile + testRemotes: seq[tuple[name, url: string]] = @[ + ("origin", "testRemote1Dir"), + ("other", "testRemote2Dir"), + ("upstream", "testRemote3Dir")] + noSuchVcsRevisionSha1 = initSha1Hash( + "ffffffffffffffffffffffffffffffffffffffff") + newBranchName = "new-branch" + remoteNewBranch = &"{testRemotes[1].name}/{newBranchName}" + newBranchFileName = "test2.txt" + newBranchFileContent = "This is a new branch file content." + testRemoteCommitFile = "remote.txt" + + var nameToVcsRevision: NameToVcsRevision + + proc getMercurialPathsDesc(): string = + result = "[paths]\n" + for remote in testRemotes: + result &= &"{remote.name} = {remote.url.absolutePath}\n" + + proc initRepo(vcsType: VcsType) = tryDoCmdEx(&"{vcsType} init") + + proc collectFiles(files: varargs[string]): string = + for file in files: result &= file & " " + + proc addFiles(vcsType: VcsType, files: varargs[string]) = + tryDoCmdEx(&"{vcsType} add {collectFiles(files)}") + + proc revertAddFiles(vcsType: VcsType, files: varargs[string]) = + let files = collectFiles(files) + case vcsType + of vcsTypeGit: + tryDoCmdEx(&"git reset HEAD -- {files}") + of vcsTypeHg: + tryDoCmdEx(&"hg revert {files}") + of vcsTypeNone: + assert false, "Must not enter here." + + proc commit(vcsType: VcsType, name: string) = + # Use user supplied name for the commit as commit message. + tryDoCmdEx(&"{vcsType} commit -m {name}") + nameToVcsRevision[name] = getVcsRevision(".") + + proc addRemotes(vcsType: VcsType) = + case vcsType + of vcsTypeGit: + for remote in testRemotes: + tryDoCmdEx(&"git remote add {remote.name} {remote.url}") + of vcsTypeHg: + writeFile(".hg/hgrc", getMercurialPathsDesc()) + of vcsTypeNone: + assert false, "VCS type must not be 'vcsTypeNone'." + + proc setupRemoteRepo(vcsType: VcsType, remoteUrl: string) = + createDir remoteUrl + tryDoCmdEx(&"{vcsType} init {remoteUrl}") + if vcsType == vcsTypeGit: + cd remoteUrl: + tryDoCmdEx("git config receive.denyCurrentBranch ignore") + + proc setupRemoteRepos(vcsType: VcsType) = + for remote in testRemotes: + setupRemoteRepo(vcsType, remote.url) + + proc switchBranch(vcsType: VcsType, branchName: string) = + let command = case vcsType + of vcsTypeGit: "checkout" + of vcsTypeHg: "update" + of vcsTypeNone: + assert false, "Must not enter here."; "" + + tryDoCmdEx(&"{vcsType} {command} {branchName}") + + proc createCommitOnRemote(vcsType: VcsType, remoteName, remoteUrl: string) = + cd remoteUrl: + writeFile(testRemoteCommitFile, "") + addFiles(vcsType, testRemoteCommitFile) + commit(vcsType, remoteName) + + proc createCommitOnTestRemotes(vcsType: VcsType) = + for remote in testRemotes: + createCommitOnRemote(vcsType, remote.name, remote.url) + + proc setupNewBranch(vcsType: VcsType, branchName: string) = + tryDoCmdEx(&"{vcsType} branch {branchName}") + + proc pushToRemote(vcsType: VcsType, remoteName: string) = + tryDoCmdEx(&"{vcsType} push {remoteName}") + + proc pushToTestRemotes(vcsType: VcsType) = + for remote in testRemotes: + pushToRemote(vcsType, remote.name) + + proc createTestFiles() = + writeFile(testFile, testFileContent) + createDir testSubDir + writeFile(testSubDirFile, "") + + proc commitInTheNewBranch(vcsType: VcsType, name: string) = + writeFile(newBranchFileName, newBranchFileContent) + addFiles(vcsType, newBranchFileName) + commit(vcsType, name) + + proc getExpectedLocalBranchesForVcsType(vcsType: VcsType): HashSet[string] = + let defaultBranchName = vcsType.getVcsDefaultBranchName + result = [defaultBranchName, newBranchName].toHashSet + + proc getExpectedRemoteTrackingBranchesForVcsType(vcsType: VcsType): + HashSet[string] = + if vcsType == vcsTypeGit: + for remote in testRemotes: + result.incl &"{remote.name}/{vcsType.getVcsDefaultBranchName}" + else: + result = vcsType.getExpectedLocalBranchesForVcsType + + proc getExpectedBranchesForVcsType(vcsType: VcsType): HashSet[string] = + result = vcsType.getExpectedLocalBranchesForVcsType + result = result + vcsType.getExpectedRemoteTrackingBranchesForVcsType + + proc createNewBranchOnTestRemotes(vcsType: VcsType) = + for remote in testRemotes: + cd remote.url: + setupNewBranch(vcsType, newBranchName) + + proc setupSuite(vcsType: VcsType, vcsTestDir: string) = + cdNewDir vcsTestDir: + initRepo(vcsType) + createTestFiles() + addFiles(vcsType, testFile, testSubDirFile) + commit(vcsType, vcsType.getVcsDefaultBranchName) + addRemotes(vcsType) + setupRemoteRepos(vcsType) + pushToTestRemotes(vcsType) + createCommitOnTestRemotes(vcsType) + createNewBranchOnTestRemotes(vcsType) + setupNewBranch(vcsType, newBranchName) + if vcsType == vcsTypeHg: + # Mercurial requires to have a commit for the new branch before + # switching to it. + commitInTheNewBranch(vcsType, newBranchName) + switchBranch(vcsType, newBranchName) + defer: + # Restore the main branch on scope exit. + switchBranch(vcsType, getVcsDefaultBranchName(vcsType)) + if vcsType != vcsTypeHg: + # In the case of Mercurial at this point the commit is already done. + commitInTheNewBranch(vcsType, newBranchName) + + template installRemoteTrackingBranch(testDir: string): untyped {.dirty.} = + # A hack for `testDir` to be available in `&` macro. + let td = testDir + # Fetch remote branch + tryDoCmdEx(&"git -C {td} fetch {testRemotes[1].name} {newBranchName}") + defer: + # Delete the newly fetched remote branch on scope exit to clean the + # state of the repo. + tryDoCmdEx( + &"git -C {td} branch -dr {testRemotes[1].name}/{newBranchName}") + + # Tell the current branch to track it + tryDoCmdEx( + &"git -C {td} branch -u {testRemotes[1].name}/{newBranchName}") + + proc setupNoVcsSuite() = + cdNewDir testNoVcsDir: + createTestFiles() + + proc tearDownSuite(dir: string) = + removeDir dir + + template suiteTestCode(vcsType: VcsType, testDir: string, + remoteUrlPath: untyped) {.dirty.} = + assert vcsType != vcsTypeNone, + "The type of the VCS must not be 'vcsTypeNone'" + + setupSuite(vcsType, testDir) + + test "getVcsTypeAndSpecialDirPath": + let expectedVcsSpecialDirPath = testDir.Path / getVcsSpecialDir(vcsType) + check getVcsTypeAndSpecialDirPath(testDir) == + (vcsType, expectedVcsSpecialDirPath) + check getVcsTypeAndSpecialDirPath(testDir / testSubDir) == + (vcsType, expectedVcsSpecialDirPath) + + test "getVcsRevision": + check isValidSha1Hash($getVcsRevision(testDir)) + + test "getPackageFileList": + check getPackageFileList(testDir) == @[testFile, testSubDirFile] + + test "isWorkingCopyClean": + check isWorkingCopyClean(testDir) + cd testDir: + # Make working copy state not clean. + writeFile(testFile2, "") + addFiles(vcsType, testFile2) + defer: + # Restore previous state on scope exit. + cd testDir: + revertAddFiles(vcsType, testFile2) + removeFile testFile2 + check not isWorkingCopyClean(testDir) + + test "getRemotesNames": + check getRemotesNames(testDir) == testRemotes.mapIt(it.name) + for remote in testRemotes: + # Test for empty list when there are not set remotes. + check getRemotesNames(testDir/remote.url) == newSeq[string]() + + test "getRemotePushUrl": + for remote in testRemotes: + check getRemotePushUrl(testDir, remote.name) == remoteUrlPath + + test "getRemotesPushUrls": + var remoteUrls: seq[string] + for remote in testRemotes: + # Test for empty list when there are not set remotes. + check getRemotesPushUrls(testDir/remote.url) == newSeq[string]() + remoteUrls.add remoteUrlPath + check getRemotesPushUrls(testDir) == remoteUrls + + test "isVcsRevisionPresentOnSomeRemote": + let vcsRevision = getVcsRevision(testDir) + check isVcsRevisionPresentOnSomeRemote(testDir, vcsRevision) + check not isVcsRevisionPresentOnSomeRemote(testDir, noSuchVcsRevisionSha1) + + test "getCurrentBranch": + let vcsDefaultBranchName = getVcsDefaultBranchName(vcsType) + check getCurrentBranch(testDir) == vcsDefaultBranchName + cd testDir: + switchBranch(vcsType, newBranchName) + defer: switchBranch(vcsType, vcsDefaultBranchName) + check getCurrentBranch(".") == newBranchName + check getCurrentBranch(testDir) == vcsDefaultBranchName + + test "getBranchesOnWhichVcsRevisionIsPresent": + let vcsRevision = getVcsRevision(testDir) + + check getBranchesOnWhichVcsRevisionIsPresent( + testDir, vcsRevision, btBoth) == + vcsType.getExpectedBranchesForVcsType + + check getBranchesOnWhichVcsRevisionIsPresent( + testDir, vcsRevision, btLocal) == + vcsType.getExpectedLocalBranchesForVcsType + + check getBranchesOnWhichVcsRevisionIsPresent( + testDir, vcsRevision, btRemoteTracking) == + vcsType.getExpectedRemoteTrackingBranchesForVcsType + + check getBranchesOnWhichVcsRevisionIsPresent( + testDir, noSuchVcsRevisionSha1) == HashSet[string]() + + test "isVcsRevisionPresentOnSomeBranch": + check isVcsRevisionPresentOnSomeBranch( + testDir, getVcsRevision(testDir)) + check not isVcsRevisionPresentOnSomeBranch( + testDir, noSuchVcsRevisionSha1) + + test "isVcsRevisionPresentOnBranch": + let vcsRevision = getVcsRevision(testDir) + let branchName = getCurrentBranch(testDir) + check isVcsRevisionPresentOnBranch(testDir, vcsRevision, branchName) + check not isVcsRevisionPresentOnBranch( + testDir, noSuchVcsRevisionSha1, branchName) + check not isVcsRevisionPresentOnBranch( + testDir, vcsRevision, "not-existing-branch") + + test "retrieveRemoteChangeSets (for single remote and branch)": + let remoteName = testRemotes[2].name + let remoteVcsRevision = nameToVcsRevision[remoteName] + check not isVcsRevisionPresentOnSomeBranch(testDir, remoteVcsRevision) + retrieveRemoteChangeSets(testDir, remoteName, + vcsType.getVcsDefaultBranchName) + check isVcsRevisionPresentOnSomeBranch(testDir, remoteVcsRevision) + + test "retrieveRemoteChangeSets (for single remote)": + let remoteName = testRemotes[0].name + let remoteVcsRevision = nameToVcsRevision[remoteName] + check not isVcsRevisionPresentOnSomeBranch(testDir, remoteVcsRevision) + retrieveRemoteChangeSets(testDir, remoteName) + check isVcsRevisionPresentOnSomeBranch(testDir, remoteVcsRevision) + + test "retrieveRemoteChangeSets (for all remotes and branches)": + let remoteVcsRevision = nameToVcsRevision[testRemotes[1].name] + check not isVcsRevisionPresentOnSomeBranch(testDir, remoteVcsRevision) + retrieveRemoteChangeSets(testDir) + check isVcsRevisionPresentOnSomeBranch(testDir, remoteVcsRevision) + + test "setWorkingCopyToVcsRevision": + let oldRevision = getVcsRevision(testDir) + let changeRevision = nameToVcsRevision[newBranchName] + setWorkingCopyToVcsRevision(testDir, changeRevision) + defer: + # Restore the repository state at scope exit. + cd testDir: switchBranch(vcsType, getVcsDefaultBranchName(vcsType)) + let newRevision = getVcsRevision(testDir) + check newRevision != oldRevision + check newRevision == changeRevision + + test "setCurrentBranchToVcsRevision": + let oldRevision = getVcsRevision(testDir) + let changeRevision = nameToVcsRevision[newBranchName] + let branchName = getCurrentBranch(testDir) + setCurrentBranchToVcsRevision(testDir, changeRevision) + defer: + # Restore the repository state at scope exit. + setCurrentBranchToVcsRevision(testDir, oldRevision) + check getVcsRevision(testDir) == oldRevision + let newRevision = getVcsRevision(testDir) + # Check that the revision is actually changed, + check newRevision != oldRevision + # to the intended one. + check newRevision == changeRevision + case vcsType + of vcsTypeGit: + # In the case of Git test that the branch is not changed, + check getCurrentBranch(testDir) == branchName + of vcsTypeHg: + # but for Mercurial the branch name is part of the commit meta data + # and it will be changed. + check getCurrentBranch(testDir) == newBranchName + of vcsTypeNone: + assert false, "Must not enter here." + + test "switchBranch": + switchBranch(testDir, newBranchName) + defer: + # Restore the repository state at scope exit. + cd testDir: switchBranch(vcsType, getVcsDefaultBranchName(vcsType)) + check getCurrentBranch(testDir) == newBranchName + expect NimbleError: switchBranch(testDir, "not-existing-branch") + + test "getCorrespondingRemoteAndBranch": + let (remote, branch) = testDir.getCorrespondingRemoteAndBranch + # There is no setup remote tracking branch and the default remote name for + # the VCS type and current branch name are returned. + check remote == vcsType.getVcsDefaultRemoteName + check branch == testDir.getCurrentBranch + + if vcsType == vcsTypeGit: + testDir.installRemoteTrackingBranch + let (remote, branch) = testDir.getCorrespondingRemoteAndBranch + check remote == testRemotes[1].name + check branch == newBranchName + + test "hasCorrespondingRemoteBranch": + if vcsType == vcsTypeGit: + testDir.installRemoteTrackingBranch + let remoteTrackingBranches = getBranchesOnWhichVcsRevisionIsPresent( + testDir, testDir.getVcsRevision, btRemoteTracking) + check testDir.hasCorrespondingRemoteBranch(remoteTrackingBranches) == + (true, remoteNewBranch) + else: + check testDir.hasCorrespondingRemoteBranch(HashSet[string]()) == + (false, "") + + test "getLocalBranchesTrackingRemoteBranch": + if vcsType == vcsTypeGit: + testDir.installRemoteTrackingBranch + check testDir.getLocalBranchesTrackingRemoteBranch("not-existing") == + newSeqOfCap[string](0) + check testDir.getLocalBranchesTrackingRemoteBranch(remoteNewBranch) == + @[vcsType.getVcsDefaultBranchName] + else: + skip() + + test "getLocalBranchName": + if vcsType == vcsTypeGit: + check testDir.getLocalBranchName(remoteNewBranch) == newBranchName + else: + skip() + + test "fastForwardMerge": + if vcsType == vcsTypeGit: + const testBranchName = "test-branch" + cd testDir: + vcsType.setupNewBranch(testBranchName) + vcsType.switchBranch(testBranchName) + testDir.installRemoteTrackingBranch + cd testDir: vcsType.switchBranch(vcsType.getVcsDefaultBranchName) + testDir.fastForwardMerge(remoteNewBranch, testBranchName) + expect NimbleError: + testDir.fastForwardMerge(remoteNewBranch, newBranchName) + else: + skip() + + tearDownSuite(testDir) + + suite "Git": + suiteTestCode(vcsTypeGit, testGitDir): remote.url + + suite "Mercurial": + suiteTestCode(vcsTypeHg, testHgDir): + (testHgDir / remote.url).absolutePath + + suite "no version control": + setupNoVcsSuite() + + test "getVcsTypeAndSpecialDirPath": + const rootDir = when defined(windows): '\\' else: '/' + let (vcsType, specialDirPath) = getVcsTypeAndSpecialDirPath(testNoVcsDir) + check vcsType == vcsTypeNone + check ($specialDirPath)[^1] == rootDir + + test "getVcsRevision": + check not isValidSha1Hash($getVcsRevision(testNoVcsDir)) + + test "getPackageFileList": + check getPackageFileList(testNoVcsDir) == @[testFile, testSubDirFile] + + tearDownSuite(testNoVcsDir) diff --git a/tests/.gitignore b/tests/.gitignore index a142ead6..19beeacc 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -4,6 +4,7 @@ tester testscommon tissues tlocaldeps +tlockfile tmisctests tmoduletests tmultipkgs diff --git a/tests/tester.nim b/tests/tester.nim index b80d5bf7..1f4436e4 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -2,20 +2,21 @@ # BSD License. Look at license.txt for more info. # import suites -import tnimblerefresh -import tnimscript -import tuninstall +import tcheckcommand +import tdevelopfeature +import tissues +import tlocaldeps +import tlockfile +import tmisctests +import tmoduletests +import tmultipkgs import tnimbledump +import tnimblerefresh import tnimbletasks -import ttwobinaryversions -import treversedeps -import tdevelopfeature +import tnimscript import tpathcommand -import ttestcommand -import tcheckcommand -import tmultipkgs -import tmoduletests +import treversedeps import truncommand -import tlocaldeps -import tmisctests -import tissues +import ttestcommand +import ttwobinaryversions +import tuninstall diff --git a/tests/testscommon.nim b/tests/testscommon.nim index 020945ba..ed4d9fd7 100644 --- a/tests/testscommon.nim +++ b/tests/testscommon.nim @@ -183,6 +183,10 @@ proc developFile*(includes: seq[string], dependencies: seq[string]): string = result = """{"version":"$#","includes":[$#],"dependencies":[$#]}""" % [developFileVersion, filesList(includes), filesList(dependencies)] +proc writeDevelopFile*(path: string, includes: seq[string], + dependencies: seq[string]) = + writeFile(path, developFile(includes, dependencies)) + # Set env var to propagate nimble binary path putEnv("NIMBLE_TEST_BINARY_PATH", nimblePath) diff --git a/tests/tlockfile.nim b/tests/tlockfile.nim new file mode 100644 index 00000000..34d56f0f --- /dev/null +++ b/tests/tlockfile.nim @@ -0,0 +1,488 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strformat, json, strutils + +import testscommon + +import nimblepkg/displaymessages +import nimblepkg/sha1hashes +import nimblepkg/paths + +from nimblepkg/common import cd, dump, cdNewDir +from nimblepkg/tools import tryDoCmdEx, doCmdEx +from nimblepkg/vcstools import getVcsRevision, getCurrentBranch +from nimblepkg/packageinfotypes import DownloadMethod +from nimblepkg/lockfile import lockFileName, LockFileJsonKeys +from nimblepkg/sha1hashes import initSha1Hash +from nimblepkg/developfile import ValidationError, ValidationErrorKind, + developFileName, getValidationErrorMessage + +suite "lock file": + type + PackagesListFileRecord = object + name: string + url: string + `method`: DownloadMethod + tags: seq[string] + description: string + license: string + + PackagesListFileContent = seq[PackagesListFileRecord] + + PkgIdent {.pure.} = enum + main = "main" + dep1 = "dep1" + dep2 = "dep2" + + template definePackageConstants(pkgName: PkgIdent) = + ## By given dependency number defines all relevant constants for it. + + const + `pkgName"PkgName"` {.used, inject.} = $pkgName + `pkgName"PkgNimbleFileName"` {.used, inject.} = + `pkgName"PkgName"` & ".nimble" + `pkgName"PkgRepoPath"` {.used, inject.} = tempDir / `pkgName"PkgName"` + `pkgName"PkgOriginRepoPath"`{.used, inject.} = + originsDirPath / `pkgName"PkgName"` + `pkgName"PkgRemoteName"` {.used, inject.} = + `pkgName"PkgName"` & "Remote" + `pkgName"PkgRemotePath"` {.used, inject.} = + additionalRemotesDirPath / `pkgName"PkgRemoteName"` + `pkgName"PkgOriginRemoteName"` {.used, inject.} = + `pkgName"PkgName"` & "OriginRemote" + `pkgName"PkgOriginRemotePath"` {.used, inject.} = + additionalRemotesDirPath / `pkgName"PkgOriginRemoteName"` + + `pkgName"PkgListFileRecord"` {.used, inject.} = PackagesListFileRecord( + name: `pkgName"PkgName"`, + url: `pkgName"PkgOriginRepoPath"`, + `method`: DownloadMethod.git, + tags: @["test"], + description: "This is a test package.", + license: "MIT") + + const + tempDir = getTempDir() / "tlockfile" + + originsDirName = "origins" + originsDirPath = tempDir / originsDirName + + additionalRemotesDirName = "remotes" + additionalRemotesDirPath = tempDir / additionalRemotesDirName + + pkgListFileName = "packages.json" + pkgListFilePath = tempDir / pkgListFileName + + nimbleFileTemplate = """ +version = "0.1.0" +author = "Ivan Bobev" +description = "A new awesome nimble package" +license = "MIT" +requires "nim >= 1.5.1" +""" + additionalFileContent = "proc foo() =\n echo \"foo\"\n" + alternativeAdditionalFileContent = "proc bar() =\n echo \"bar\"\n" + + definePackageConstants(PkgIdent.main) + definePackageConstants(PkgIdent.dep1) + definePackageConstants(PkgIdent.dep2) + + proc newNimbleFileContent(pkgName, fileTemplate: string, + deps: seq[string]): string = + result = fileTemplate % pkgName + if deps.len == 0: + return + result &= "requires " + for i, dep in deps: + result &= &"\"{dep}\"" + if i != deps.len - 1: + result &= "," + + proc addFiles(files: varargs[string]) = + var filesStr = "" + for file in files: + filesStr &= file & " " + tryDoCmdEx("git add " & filesStr) + + proc commit(msg: string) = + tryDoCmdEx("git commit -m " & msg.quoteShell) + + proc push(remote: string) = + tryDoCmdEx("git push " & remote) + + proc pull(remote: string) = + tryDoCmdEx("git pull " & remote) + + proc addRemote(remoteName, remoteUrl: string) = + tryDoCmdEx(&"git remote add {remoteName} {remoteUrl}") + + proc initRepo(isBare = false) = + let bare = if isBare: "--bare" else: "" + tryDoCmdEx("git init " & bare) + + proc clone(urlFrom, pathTo: string) = + tryDoCmdEx(&"git clone {urlFrom} {pathTo}") + + proc branch(branchName: string) = + tryDoCmdEx(&"git branch {branchName}") + + proc checkout(what: string) = + tryDoCmdEx(&"git checkout {what}") + + proc createBranchAndSwitchToIt(branchName: string) = + if branchName.len > 0: + branch(branchName) + checkout(branchName) + + proc initNewNimbleFile(dir: string, deps: seq[string] = @[]): string = + let pkgName = dir.splitPath.tail + let nimbleFileName = pkgName & ".nimble" + let nimbleFileContent = newNimbleFileContent( + pkgName, nimbleFileTemplate, deps) + writeFile(nimbleFileName, nimbleFileContent) + return nimbleFileName + + proc initNewNimblePackage(dir, clonePath: string, deps: seq[string] = @[]) = + cdNewDir dir: + initRepo() + let nimbleFileName = dir.initNewNimbleFile(deps) + addFiles(nimbleFileName) + commit("Initial commit") + + clone(dir, clonePath) + + proc addAdditionalFileToTheRepo(fileName, fileContent: string) = + writeFile(fileName, fileContent) + addFiles(fileName) + commit("Add additional file") + + proc testLockedVcsRevisions(deps: seq[tuple[name, path: string]]) = + check lockFileName.fileExists + + let json = lockFileName.readFile.parseJson + for (depName, depPath) in deps: + let expectedVcsRevision = depPath.getVcsRevision + let lockedVcsRevision = + json{$lfjkPackages}{depName}{$lfjkPkgVcsRevision}.str.initSha1Hash + check lockedVcsRevision == expectedVcsRevision + + proc testLockFile(deps: seq[tuple[name, path: string]], isNew: bool) = + ## Generates or updates a lock file and tests whether it contains + ## dependencies with given names at given repository paths and whether their + ## VCS revisions match the written in the lock file ones. + ## + ## `isNew` - indicates whether it is expected a new lock file to be + ## generated if its value is `true` or already existing lock file to be + ## updated otherwise. + + if isNew: + check not fileExists(lockFileName) + else: + check fileExists(lockFileName) + + let (output, exitCode) = execNimbleYes("lock") + check exitCode == QuitSuccess + + var lines = output.processOutput + if isNew: + check lines.inLinesOrdered(generatingTheLockFileMsg) + check lines.inLinesOrdered(lockFileIsGeneratedMsg) + else: + check lines.inLinesOrdered(updatingTheLockFileMsg) + check lines.inLinesOrdered(lockFileIsUpdatedMsg) + + testLockedVcsRevisions(deps) + + template filesAndDirsToRemove() = + removeFile pkgListFilePath + removeDir installDir + removeDir tempDir + + template cleanUp() = + filesAndDirsToRemove() + defer: filesAndDirsToRemove() + + proc writePackageListFile(path: string, content: PackagesListFileContent) = + let dir = path.splitPath.head + createDir dir + writeFile(path, (%content).pretty) + + template withPkgListFile(body: untyped) = + writePackageListFile( + pkgListFilePath, @[dep1PkgListFileRecord, dep2PkgListFileRecord]) + usePackageListFile pkgListFilePath: + body + + test "can generate lock file": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + cd mainPkgRepoPath: + testLockFile(@[(dep1PkgName, dep1PkgRepoPath)], isNew = true) + + test "can download locked dependencies": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName, dep2PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + initNewNimblePackage(dep2PkgOriginRepoPath, dep2PkgRepoPath) + cd mainPkgRepoPath: + testLockFile(@[(dep1PkgName, dep1PkgRepoPath), + (dep2PkgName, dep2PkgRepoPath)], + isNew = true) + removeDir installDir + let (output, exitCode) = execNimbleYes("install") + check exitCode == QuitSuccess + let lines = output.processOutput + check lines.inLines(&"Downloading {dep1PkgOriginRepoPath} using git") + check lines.inLines(&"Downloading {dep2PkgOriginRepoPath} using git") + + test "can update already existing lock file": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + initNewNimblePackage(dep2PkgOriginRepoPath, dep2PkgRepoPath) + + cd mainPkgRepoPath: + testLockFile(@[(dep1PkgName, dep1PkgRepoPath)], isNew = true) + # Add additional dependency to the nimble file. + let mainPkgNimbleFileContent = newNimbleFileContent(mainPkgName, + nimbleFileTemplate, @[dep1PkgName, dep2PkgName]) + writeFile(mainPkgNimbleFileName, mainPkgNimbleFileContent) + # Make first dependency to be in develop mode. + writeDevelopFile(developFileName, @[], @[dep1PkgRepoPath]) + + cd dep1PkgOriginRepoPath: + # Add additional file to the first dependency, commit and push. + addAdditionalFileToTheRepo("dep1.nim", additionalFileContent) + + cd dep1PkgRepoPath: + pull("origin") + + cd mainPkgRepoPath: + # On second lock the first package revision is updated and a second + # package is added as dependency. + testLockFile(@[(dep1PkgName, dep1PkgRepoPath), + (dep2PkgName, dep2PkgRepoPath)], + isNew = false) + + template outOfSyncDepsTest(branchName: string, body: untyped) = + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName, dep2PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + initNewNimblePackage(dep2PkgOriginRepoPath, dep2PkgRepoPath) + + cd dep1PkgOriginRepoPath: + createBranchAndSwitchToIt(branchName) + addAdditionalFileToTheRepo("dep1.nim", additionalFileContent) + + cd dep2PkgOriginRepoPath: + createBranchAndSwitchToIt(branchName) + addAdditionalFileToTheRepo("dep2.nim", additionalFileContent) + + cd mainPkgOriginRepoPath: + testLockFile(@[(dep1PkgName, dep1PkgOriginRepoPath), + (dep2PkgName, dep2PkgOriginRepoPath)], + isNew = true) + addFiles(lockFileName) + commit("Add the lock file to version control") + + cd mainPkgRepoPath: + pull("origin") + let (_ {.used.}, devCmdExitCode) = execNimble("develop", + &"-a:{dep1PkgRepoPath}", &"-a:{dep2PkgRepoPath}") + check devCmdExitCode == QuitSuccess + `body` + + test "can list out of sync develop dependencies": + outOfSyncDepsTest(""): + let (output, exitCode) = execNimbleYes("sync", "--list-only") + check exitCode == QuitSuccess + let lines = output.processOutput + check lines.inLines( + pkgWorkingCopyNeedsSyncingMsg(dep1PkgName, dep1PkgRepoPath)) + check lines.inLines( + pkgWorkingCopyNeedsSyncingMsg(dep2PkgName, dep2PkgRepoPath)) + + proc testDepsSync = + let (output, exitCode) = execNimbleYes("sync") + check exitCode == QuitSuccess + let lines = output.processOutput + check lines.inLines( + pkgWorkingCopyIsSyncedMsg(dep1PkgName, dep1PkgRepoPath)) + check lines.inLines( + pkgWorkingCopyIsSyncedMsg(dep2PkgName, dep2PkgRepoPath)) + + cd mainPkgRepoPath: + # After successful sync the revisions written in the lock file must + # match those in the lock file. + testLockedVcsRevisions(@[(dep1PkgName, dep1PkgRepoPath), + (dep2PkgName, dep2PkgRepoPath)]) + + test "can sync out of sync develop dependencies": + outOfSyncDepsTest(""): + testDepsSync() + + test "can switch to another branch when syncing": + const newBranchName = "new-branch" + outOfSyncDepsTest(newBranchName): + testDepsSync() + check dep1PkgRepoPath.getCurrentBranch == newBranchName + check dep2PkgRepoPath.getCurrentBranch == newBranchName + + test "cannot lock because the directory is not under version control": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + cd dep1PkgRepoPath: + # Remove working copy from version control. + removeDir(".git") + cd mainPkgRepoPath: + writeDevelopFile(developFileName, @[], @[dep1PkgRepoPath]) + let (output, exitCode) = execNimbleYes("lock") + check exitCode == QuitFailure + let + error = ValidationError(kind: vekDirIsNotUnderVersionControl, + path: dep1PkgRepoPath) + errorMessage = getValidationErrorMessage(dep1PkgName, error) + check output.processOutput.inLines(errorMessage) + + test "cannot lock because the working copy is not clean": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + cd dep1PkgRepoPath: + # Modify the Nimble file to make the working copy not clean. + discard initNewNimbleFile(dep1PkgRepoPath, @[dep2PkgName]) + cd mainPkgRepoPath: + writeDevelopFile(developFileName, @[], @[dep1PkgRepoPath]) + let (output, exitCode) = execNimbleYes("lock") + check exitCode == QuitFailure + let + error = ValidationError(kind: vekWorkingCopyIsNotClean, + path: dep1PkgRepoPath) + errorMessage = getValidationErrorMessage(dep1PkgName, error) + check output.processOutput.inLines(errorMessage) + + test "cannot lock because the working copy has not pushed VCS revision": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + cd dep1PkgRepoPath: + addAdditionalFileToTheRepo("dep1.nim", additionalFileContent) + cd mainPkgRepoPath: + writeDevelopFile(developFileName, @[], @[dep1PkgRepoPath]) + let (output, exitCode) = execNimbleYes("lock") + check exitCode == QuitFailure + let + error = ValidationError(kind: vekVcsRevisionIsNotPushed, + path: dep1PkgRepoPath) + errorMessage = getValidationErrorMessage(dep1PkgName, error) + check output.processOutput.inLines(errorMessage) + + test "cannot sync because the working copy needs lock": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + cd mainPkgRepoPath: + writeDevelopFile(developFileName, @[], @[dep1PkgRepoPath]) + testLockFile(@[(dep1PkgName, dep1PkgRepoPath)], isNew = true) + cd dep1PkgOriginRepoPath: + addAdditionalFileToTheRepo("dep1.nim", additionalFileContent) + cd dep1PkgRepoPath: + pull("origin") + cd mainPkgRepoPath: + let (output, exitCode) = execNimbleYes("sync") + check exitCode == QuitFailure + let + error = ValidationError(kind: vekWorkingCopyNeedsLock, + path: dep1PkgRepoPath) + errorMessage = getValidationErrorMessage(dep1PkgName, error) + check output.processOutput.inLines(errorMessage) + + proc addAdditionalFileAndPushToRemote( + repoPath, remoteName, remotePath, fileContent: string) = + cdNewDir remotePath: + initRepo(isBare = true) + cd repoPath: + # Add commit to the dependency. + addAdditionalFileToTheRepo("dep1.nim", fileContent) + addRemote(remoteName, remotePath) + # Push it to the newly added remote to be able to lock. + push(remoteName) + + test "cannot sync because the working copy needs merge": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + + cd mainPkgOriginRepoPath: + testLockFile(@[(dep1PkgName, dep1PkgOriginRepoPath)], isNew = true) + addFiles(lockFileName) + commit("Add the lock file to version control") + + cd mainPkgRepoPath: + # Pull the lock file. + pull("origin") + # Create develop file. On this command also a sync file will be + # generated. + let (_, exitCode) = execNimble("develop", &"-a:{dep1PkgRepoPath}") + check exitCode == QuitSuccess + + addAdditionalFileAndPushToRemote( + dep1PkgRepoPath, dep1PkgRemoteName, dep1PkgRemotePath, + additionalFileContent) + + addAdditionalFileAndPushToRemote( + dep1PkgOriginRepoPath, dep1PkgOriginRemoteName, dep1PkgOriginRemotePath, + alternativeAdditionalFileContent) + + cd mainPkgOriginRepoPath: + writeDevelopFile(developFileName, @[], @[dep1PkgOriginRepoPath]) + # Update the origin lock file. + testLockFile(@[(dep1PkgName, dep1PkgOriginRepoPath)], isNew = false) + addFiles(lockFileName) + commit("Modify the lock file") + + cd mainPkgRepoPath: + # Pull modified origin lock file. At this point the revisions in the + # lock file, sync file and develop mode dependency working copy should + # be different from one another. + pull("origin") + let (output, exitCode) = execNimbleYes("sync") + check exitCode == QuitFailure + let + error = ValidationError(kind: vekWorkingCopyNeedsMerge, + path: dep1PkgRepoPath) + errorMessage = getValidationErrorMessage(dep1PkgName, error) + check output.processOutput.inLines(errorMessage) + + test "check fails because the working copy needs sync": + outOfSyncDepsTest(""): + let (output, exitCode) = execNimble("check") + check exitCode == QuitFailure + let + error = ValidationError(kind: vekWorkingCopyNeedsSync, + path: dep1PkgRepoPath) + errorMessage = getValidationErrorMessage(dep1PkgName, error) + check output.processOutput.inLines(errorMessage) diff --git a/tests/tuninstall.nim b/tests/tuninstall.nim index 4b481399..f1ccd7bd 100644 --- a/tests/tuninstall.nim +++ b/tests/tuninstall.nim @@ -15,6 +15,9 @@ suite "uninstall": let args = ["install", pkgBin2Url] check execNimbleYes(args).exitCode == QuitSuccess + proc cannotSatisfyMsg(v1, v2: string): string = + &"Cannot satisfy the dependency on PackageA {v1} and PackageA {v2}" + test "can reject same version dependencies": cleanDir(installDir) let (outp, exitCode) = execNimbleYes("install", pkgBinUrl) @@ -22,8 +25,8 @@ suite "uninstall": # stderr output being generated and flushed without first flushing stdout let ls = outp.strip.processOutput() check exitCode != QuitSuccess - check "Cannot satisfy the dependency on PackageA 0.2.0 and PackageA 0.5.0" in - ls[ls.len-1] + check ls.inLines(cannotSatisfyMsg("0.2.0", "0.5.0")) or + ls.inLines(cannotSatisfyMsg("0.5.0", "0.2.0")) proc setupIssue27Packages() = # Install b From 4c65f0cd8ed424e0af2ad4764883152f42ba4e9f Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 11 Jan 2021 18:11:03 +0200 Subject: [PATCH 24/73] Implement "setup" command Setup command which writes a file named "nimble.paths" with file system paths to the package dependencies is implemented. The command also adds code to include the paths file in "config.nims" configuration file to make it available for the compiler. If "config.nims" does not exist it will be created. Related to nim-lang/nimble#127 --- src/nimble.nim | 68 ++++++++++++++++++++++-- src/nimblepkg/options.nim | 10 +++- tests/.gitignore | 1 + tests/setup/binary/binary.nim | 3 ++ tests/setup/binary/binary.nimble | 8 +++ tests/setup/dependency/dependency.nim | 1 + tests/setup/dependency/dependency.nimble | 7 +++ tests/setup/dependent/dependent.nim | 3 ++ tests/setup/dependent/dependent.nimble | 8 +++ tests/tester.nim | 1 + tests/tsetupcommand.nim | 53 ++++++++++++++++++ 11 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 tests/setup/binary/binary.nim create mode 100644 tests/setup/binary/binary.nimble create mode 100644 tests/setup/dependency/dependency.nim create mode 100644 tests/setup/dependency/dependency.nimble create mode 100644 tests/setup/dependent/dependent.nim create mode 100644 tests/setup/dependent/dependent.nimble create mode 100644 tests/tsetupcommand.nim diff --git a/src/nimble.nim b/src/nimble.nim index 59c0e7dd..7ebd3cc0 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -23,6 +23,10 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/displaymessages, nimblepkg/sha1hashes, nimblepkg/syncfile, nimblepkg/vcstools +const + nimblePathsFileName* = "nimble.paths" + nimbleConfigFileName* = "config.nims" + proc refresh(options: Options) = ## Downloads the package list from the specified URL. ## @@ -571,12 +575,16 @@ proc install(packages: seq[PkgTuple], options: Options, else: raise +proc getDependenciesPaths(pkgInfo: PackageInfo, options: Options): + HashSet[string] = + let deps = pkgInfo.processAllDependencies(options) + return deps.map(dep => dep.getRealDir()) + proc build(options: Options) = let dir = getCurrentDir() let pkgInfo = getPkgInfo(dir, options) nimScriptHint(pkgInfo) - let deps = pkgInfo.processAllDependencies(options) - let paths = deps.map(dep => dep.getRealDir()) + let paths = pkgInfo.getDependenciesPaths(options) var args = options.getCompilationFlags() buildFromDir(pkgInfo, paths, args, options) @@ -1111,6 +1119,15 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = proc updateSyncFile(dependentPkg: PackageInfo, options: Options) +proc updatePathsFile(pkgInfo: PackageInfo, options: Options) = + let paths = pkgInfo.getDependenciesPaths(options) + var pathsFileContent: string + for path in paths: + pathsFileContent &= &"--path:\"{path}\"\n" + var action = if fileExists(nimblePathsFileName): "updated" else: "generated" + writeFile(nimblePathsFileName, pathsFileContent) + displayInfo(&"\"{nimblePathsFileName}\" is {action}.") + proc develop(options: var Options) = let hasPackages = options.action.packages.len > 0 @@ -1153,8 +1170,10 @@ proc develop(options: var Options) = else: hasError = not executeDevActionsAllowedOutsidePkgDir(options) or hasError - if currentDirPkgInfo.isLoaded: + if hasDevActionsAllowedOnlyInPkgDir: updateSyncFile(currentDirPkgInfo, options) + if fileExists(nimblePathsFileName): + updatePathsFile(currentDirPkgInfo, options) if hasError: raise nimbleError( @@ -1480,6 +1499,8 @@ proc sync(options: Options) = # On `sync` we also want to update Nimble cache with the dependencies' # versions from the lock file. discard processLockedDependencies(pkgInfo, options) + if fileExists(nimblePathsFileName): + updatePathsFile(pkgInfo, options) var errors: ValidationErrors findValidationErrorsOfDevDepsWithLockFile(pkgInfo, options, errors) @@ -1498,6 +1519,45 @@ proc sync(options: Options) = if errors.len > 0: raise validationErrors(errors) +proc setup(options: Options) = + ## Creates `nimble.paths` file containing file system paths to the + ## dependencies. Includes it in `config.nims` file to make them available + ## for the compiler. + const + configFileVersion = "0.1.0" + configFileHeader = &"# begin Nimble config (version {configFileVersion})\n" + configFileContent = fmt""" +when fileExists("{nimblePathsFileName}"): + include "{nimblePathsFileName}" +# end Nimble config +""" + + let currentDir = getCurrentDir() + let pkgInfo = getPkgInfo(currentDir, options) + updatePathsFile(pkgInfo, options) + + proc constructNimbleConfig: string = + result &= "\n" + result &= configFileHeader + result &= configFileContent + + var writeFile = false + var fileContent: string + if fileExists(nimbleConfigFileName): + fileContent = readFile(nimbleConfigFileName) + if not fileContent.contains(configFileHeader): + fileContent &= constructNimbleConfig() + writeFile = true + else: + fileContent = constructNimbleConfig() + writeFile = true + + if writeFile: + writeFile(nimbleConfigFileName, fileContent) + displayInfo(&"\"{nimbleConfigFileName}\" is set up.") + else: + displayInfo(&"\"{nimbleConfigFileName}\" is already set up.") + proc run(options: Options) = # Verify parameters. var pkgInfo = getPkgInfo(getCurrentDir(), options) @@ -1581,6 +1641,8 @@ proc doAction(options: var Options) = lock(options) of actionSync: sync(options) + of actionSetup: + setup(options) of actionNil: assert false of actionCustom: diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 4505c026..8d0bb9d9 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -46,7 +46,7 @@ type actionNil, actionRefresh, actionInit, actionDump, actionPublish, actionInstall, actionSearch, actionList, actionBuild, actionPath, actionUninstall, actionCompile, actionDoc, actionCustom, actionTasks, - actionDevelop, actionCheck, actionLock, actionRun, actionSync + actionDevelop, actionCheck, actionLock, actionRun, actionSync, actionSetup DevelopActionType* = enum datNewFile, datAdd, datRemoveByPath, datRemoveByName, datInclude, datExclude @@ -56,7 +56,7 @@ type Action* = object case typ*: ActionType of actionNil, actionList, actionPublish, actionTasks, actionCheck, - actionLock: nil + actionLock, actionSetup: nil of actionSync: listOnly*: bool of actionRefresh: @@ -161,6 +161,10 @@ Commands: the content of the lock file. [-l, --list-only] Only lists the packages which are not synced without actually performing the sync operation. + setup Creates `nimble.paths` file containing file + system paths to the dependencies. Also + includes the paths file in the `config.nims` + file to make them available for the compiler. Nimble Options: -h, --help Print this help message. @@ -238,6 +242,8 @@ proc parseActionType*(action: string): ActionType = result = actionLock of "sync": result = actionSync + of "setup": + result = actionSetup else: result = actionCustom diff --git a/tests/.gitignore b/tests/.gitignore index 19beeacc..f195953c 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -14,6 +14,7 @@ tnimscript tpathcommand treversedeps truncommand +tsetupcommand ttestcommand ttwobinaryversions tuninstall diff --git a/tests/setup/binary/binary.nim b/tests/setup/binary/binary.nim new file mode 100644 index 00000000..d57b8528 --- /dev/null +++ b/tests/setup/binary/binary.nim @@ -0,0 +1,3 @@ +import packagea + +echo test(2, 2) diff --git a/tests/setup/binary/binary.nimble b/tests/setup/binary/binary.nimble new file mode 100644 index 00000000..681aa7f9 --- /dev/null +++ b/tests/setup/binary/binary.nimble @@ -0,0 +1,8 @@ +version = "1.0" +author = "Ivan Bobev" +description = "binary" +license = "MIT" + +bin = @["binary"] + +requires "packagea == 0.2.0", "packageb" diff --git a/tests/setup/dependency/dependency.nim b/tests/setup/dependency/dependency.nim new file mode 100644 index 00000000..a43ebcac --- /dev/null +++ b/tests/setup/dependency/dependency.nim @@ -0,0 +1 @@ +proc test*(): string = "15" \ No newline at end of file diff --git a/tests/setup/dependency/dependency.nimble b/tests/setup/dependency/dependency.nimble new file mode 100644 index 00000000..be31a4fd --- /dev/null +++ b/tests/setup/dependency/dependency.nimble @@ -0,0 +1,7 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "test dependency" +license = "MIT" + +# Dependencies +requires "nim >= 1.5.1" \ No newline at end of file diff --git a/tests/setup/dependent/dependent.nim b/tests/setup/dependent/dependent.nim new file mode 100644 index 00000000..88b253f6 --- /dev/null +++ b/tests/setup/dependent/dependent.nim @@ -0,0 +1,3 @@ +import dependency + +doAssert test() == "15" diff --git a/tests/setup/dependent/dependent.nimble b/tests/setup/dependent/dependent.nimble new file mode 100644 index 00000000..f0f1f740 --- /dev/null +++ b/tests/setup/dependent/dependent.nimble @@ -0,0 +1,8 @@ +version = "0.1.0" +author = "Ivan Bobev" +description = "dependent" +license = "MIT" + +bin = @["dependent"] + +requires "nim >= 1.5.1", "dependency" \ No newline at end of file diff --git a/tests/tester.nim b/tests/tester.nim index 1f4436e4..9497798f 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -17,6 +17,7 @@ import tnimscript import tpathcommand import treversedeps import truncommand +import tsetupcommand import ttestcommand import ttwobinaryversions import tuninstall diff --git a/tests/tsetupcommand.nim b/tests/tsetupcommand.nim new file mode 100644 index 00000000..7b46a926 --- /dev/null +++ b/tests/tsetupcommand.nim @@ -0,0 +1,53 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +{.used.} + +import unittest, os, strutils, osproc +import testscommon +from nimble import nimblePathsFileName, nimbleConfigFileName +from nimblepkg/common import cd +from nimblepkg/developfile import developFileName + +suite "setup command": + cleanDir installDir + test "nimble setup (without develop file)": + cd "setup/binary": + usePackageListFile "../../develop/packages.json": + cleanFiles nimblePathsFileName, nimbleConfigFileName, "binary" + let (_, exitCode) = execNimble("setup") + check exitCode == QuitSuccess + # Check that the paths and config files are generated. + check fileExists(nimblePathsFileName) + check fileExists(nimbleConfigFileName) + # Check that the paths file contains the right path/ + let pkgADir = getPackageDir(pkgsDir, "packagea-0.2.0") + let pkgBDir = getPackageDir(pkgsDir, "packageb-0.1.0") + let pathsFileContent = nimblePathsFileName.readFile + check pathsFileContent.contains(pkgADir) + check pathsFileContent.contains(pkgBDir) + # Check that Nim can use "nimble.paths" file to find dependencies and + # build the project. + let (_, nimExitCode) = execCmdEx("nim c -r binary") + check nimExitCode == QuitSuccess + + test "nimble setup (with develop file)": + cd "setup/dependent": + usePackageListFile "../../develop/packages.json": + cleanFiles nimblePathsFileName, nimbleConfigFileName, + developFileName, "dependent" + let (_, developExitCode) = execNimble("develop", "-a:../dependency") + check developExitCode == QuitSuccess + let (_, setupExitCode) = execNimble("setup") + check setupExitCode == QuitSuccess + # Check that the paths and config files are generated. + check fileExists(nimblePathsFileName) + check fileExists(nimbleConfigFileName) + # Check that develop mode dependency path is written in the + # "nimble.paths" file. + let developDepDir = (getCurrentDir() / "../dependency").normalizedPath + check nimblePathsFileName.readFile.contains(developDepDir) + # Check that Nim can use "nimble.paths" file to find dependencies and + # build the project. + let (_, nimExitCode) = execCmdEx("nim c -r dependent") + check nimExitCode == QuitSuccess From 0498d5b05bad8a2ef8c8c78d11bedc9c5b0a3df0 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 12 Jan 2021 16:13:36 +0200 Subject: [PATCH 25/73] Fix bug in tests `testRefresh` procedure should remove current temporary config file before trying to restore the old file in its place, because otherwise if the old file is missing the temporary file will stay and this will be problem for the following tests. Related to nim-lang/nimble#127 --- tests/testscommon.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/testscommon.nim b/tests/testscommon.nim index ed4d9fd7..2eff40f8 100644 --- a/tests/testscommon.nim +++ b/tests/testscommon.nim @@ -138,6 +138,7 @@ template testRefresh*(body: untyped) = body # Restore config + removeFile configFile if fileExists(configBakFile): safeMoveFile(configBakFile, configFile) From 743643be448f50c9be50eda9a157a5e27b7667f3 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 22 Feb 2021 13:11:49 +0200 Subject: [PATCH 26/73] Fix a few compiler warnings * Remove usage of deprecated type TainedString. * Disable a few ProveInit warnings. Related to nim-lang/nimble#127 --- src/nimblepkg/packageinfotypes.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 713af6cc..40ad6cbf 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -88,6 +88,7 @@ type {.warning[UnsafeDefault]: off.} {.warning[ProveInit]: off.} aliasThis PackageInfo.metaData +{.warning[ProveInit]: on.} aliasThis PackageInfo.basicInfo {.warning[ProveInit]: on.} {.warning[UnsafeDefault]: on.} From 9956b7cea4f435d1f6d8f42841e5cd86afedd512 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 22 Feb 2021 19:32:35 +0200 Subject: [PATCH 27/73] Use object type for version fields * The `Version` type is changed to be object with a single version string field instead of distinct string type in order to allow it to be created from other modules only by `newVersion` procedure. * Version fields of objects in `packageinfotypes.nim` now use object type `Version` for representing package versions instead of string. Related to nim-lang/nimble#127 --- src/nimble.nim | 46 ++++------- src/nimblepkg/checksums.nim | 4 +- src/nimblepkg/developfile.nim | 4 +- src/nimblepkg/displaymessages.nim | 2 +- src/nimblepkg/download.nim | 4 +- src/nimblepkg/lockfile.nim | 3 +- src/nimblepkg/packageinfo.nim | 21 +++-- src/nimblepkg/packageinfotypes.nim | 10 +-- src/nimblepkg/packagemetadatafile.nim | 6 +- src/nimblepkg/packageparser.nim | 17 ++-- src/nimblepkg/publish.nim | 5 +- src/nimblepkg/reversedeps.nim | 13 +-- src/nimblepkg/tools.nim | 26 +++--- src/nimblepkg/topologicalsort.nim | 2 + src/nimblepkg/version.nim | 109 +++++++++++++++----------- tests/tdevelopfeature.nim | 5 +- tests/tissues.nim | 4 +- tests/tuninstall.nim | 8 +- 18 files changed, 159 insertions(+), 130 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 7ebd3cc0..7f75086d 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -4,24 +4,22 @@ import system except TResult import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc, - strformat, sequtils + strformat import std/options as std_opt import strutils except toLower from unicode import toLower -from sequtils import toSeq import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, nimblepkg/download, nimblepkg/config, nimblepkg/common, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, - nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/tools, + nimblepkg/nimscriptexecutor, nimblepkg/init, nimblepkg/vcstools, nimblepkg/checksums, nimblepkg/topologicalsort, nimblepkg/lockfile, nimblepkg/nimscriptwrapper, nimblepkg/developfile, nimblepkg/paths, nimblepkg/nimbledatafile, nimblepkg/packagemetadatafile, - nimblepkg/displaymessages, nimblepkg/sha1hashes, nimblepkg/syncfile, - nimblepkg/vcstools + nimblepkg/displaymessages, nimblepkg/sha1hashes, nimblepkg/syncfile const nimblePathsFileName* = "nimble.paths" @@ -77,7 +75,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): once: pkgList = initPkgList(pkgInfo, options) display("Verifying", - "dependencies for $1@$2" % [pkgInfo.name, pkgInfo.specialVersion], + "dependencies for $1@$2" % [pkgInfo.name, $pkgInfo.specialVersion], priority = HighPriority) var reverseDependencies: seq[PackageBasicInfo] = @[] @@ -123,14 +121,14 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): # Check if two packages of the same name (but different version) are listed # in the path. - var pkgsInPath: StringTableRef = newStringTable(modeCaseSensitive) + var pkgsInPath: Table[string, Version] for pkgInfo in result: let currentVer = pkgInfo.getConcreteVersion(options) if pkgsInPath.hasKey(pkgInfo.name) and pkgsInPath[pkgInfo.name] != currentVer: raise nimbleError( "Cannot satisfy the dependency on $1 $2 and $1 $3" % - [pkgInfo.name, currentVer, pkgsInPath[pkgInfo.name]]) + [pkgInfo.name, $currentVer, $pkgsInPath[pkgInfo.name]]) pkgsInPath[pkgInfo.name] = currentVer # We add the reverse deps to the JSON file here because we don't want @@ -160,7 +158,7 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: HashSet[string], var binariesBuilt = 0 args = args - args.add "-d:NimblePkgVersion=" & pkgInfo.version + args.add "-d:NimblePkgVersion=" & $pkgInfo.version for path in paths: args.add("--path:" & path.quoteShell) if options.verbosity >= HighPriority: @@ -267,7 +265,7 @@ proc packageExists(pkgInfo: PackageInfo, options: Options): bool = proc promptOverwriteExistingPackage(pkgInfo: PackageInfo, options: Options): bool = let message = "$1@$2 already exists. Overwrite?" % - [pkgInfo.name, pkgInfo.specialVersion] + [pkgInfo.name, $pkgInfo.specialVersion] return options.prompt(message) proc removeOldPackage(pkgInfo: PackageInfo, options: Options) = @@ -325,7 +323,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Overwrite the version if the requested version is "#head" or similar. if requestedVer.kind == verSpecial: - pkgInfo.specialVersion = $requestedVer.spe + pkgInfo.specialVersion = requestedVer.spe # Dependencies need to be processed before the creation of the pkg dir. if first and pkgInfo.lockedDeps.len > 0: @@ -337,7 +335,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, result.pkg = pkgInfo return result - display("Installing", "$1@$2" % [pkginfo.name, pkginfo.specialVersion], + display("Installing", "$1@$2" % [pkginfo.name, $pkginfo.specialVersion], priority = HighPriority) let isPackageAlreadyInCache = pkgInfo.packageExists(options) @@ -507,7 +505,7 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options, let (url, metadata) = getUrlData(pv.name) return (checkUrlType(url), url, metadata) else: - var pkg: Package + var pkg = initPackage() if getPackage(pv.name, options, pkg): let (url, metadata) = getUrlData(pkg.url) return (pkg.downloadMethod, url, metadata) @@ -607,7 +605,7 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = if not execHook(options, options.action.typ, true): raise nimbleError("Pre-hook prevented further execution.") - var args = @["-d:NimblePkgVersion=" & pkgInfo.version] + var args = @["-d:NimblePkgVersion=" & $pkgInfo.version] for dep in deps: args.add("--path:" & dep.getRealDir().quoteShell) if options.verbosity >= HighPriority: @@ -685,7 +683,7 @@ proc list(options: Options) = echo(" ") proc listInstalled(options: Options) = - var h = initOrderedTable[string, seq[string]]() + var h: OrderedTable[string, seq[Version]] let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) for pkg in pkgs: let @@ -696,7 +694,7 @@ proc listInstalled(options: Options) = add(s, pVer) h[pName] = s - h.sort(proc (a, b: (string, seq[string])): int = cmpIgnoreCase(a[0], b[0])) + h.sort(proc (a, b: (string, seq[Version])): int = cmpIgnoreCase(a[0], b[0])) for k in keys(h): echo k & " [" & h[k].join(", ") & "]" @@ -723,14 +721,8 @@ proc listPaths(options: Options) = var installed: seq[VersionAndPath] = @[] # There may be several, list all available ones and sort by version. for pkg in pkgs: - let - pName = pkg.name - pVer = pkg.specialVersion - if name == pName: - var v: VersionAndPath - v.version = newVersion(pVer) - v.path = pkg.getRealDir() - installed.add(v) + if name == pkg.name: + installed.add((pkg.specialVersion, pkg.getRealDir)) if installed.len > 0: sort(installed, cmp[VersionAndPath], Descending) @@ -773,10 +765,6 @@ proc getPackageByPattern(pattern: string, options: Options): PackageInfo = ) result = getPkgInfoFromFile(skeletonInfo.myPath, options) -# import std/jsonutils -proc `%`(a: Version): JsonNode = %a.string - -# proc dump(options: Options, json: bool) = proc dump(options: Options) = cli.setSuppressMessages(true) let p = getPackageByPattern(options.action.projName, options) @@ -808,7 +796,7 @@ proc dump(options: Options) = else: s.add val.join(", ").escape fn "name", p.name - fn "version", p.version + fn "version", $p.version fn "author", p.author fn "desc", p.description fn "license", p.license diff --git a/src/nimblepkg/checksums.nim b/src/nimblepkg/checksums.nim index edeb1169..97975269 100644 --- a/src/nimblepkg/checksums.nim +++ b/src/nimblepkg/checksums.nim @@ -2,12 +2,12 @@ # BSD License. Look at license.txt for more info. import os, std/sha1, strformat -import common, sha1hashes, vcstools, paths +import common, version, sha1hashes, vcstools, paths type ChecksumError* = object of NimbleError -proc checksumError*(name, version: string, +proc checksumError*(name: string, version: Version, vcsRevision, checksum, expectedChecksum: Sha1Hash): ref ChecksumError = result = newNimbleError[ChecksumError](&""" diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index 34c07dc0..f91106e2 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -202,7 +202,7 @@ proc validateDependency(dependencyPkg, dependentPkg: PackageInfo) = for pkg in dependentPkg.requires: if cmpIgnoreStyle(dependencyPkg.name, pkg.name) == 0: isNameFound = true - if Version(dependencyPkg.version) in pkg.ver: + if dependencyPkg.version in pkg.ver: # `dependencyPkg` is a valid dependency of `dependentPkg`. return else: @@ -234,7 +234,7 @@ proc validateIncludedDependency(dependencyPkg, dependentPkg: PackageInfo, ## object. Otherwise returns `nil`. return - if Version(dependencyPkg.version) in requiredVersionRange: nil + if dependencyPkg.version in requiredVersionRange: nil else: nimbleError( dependencyNotInRangeErrorMsg( dependencyPkg.getNameAndVersion, dependentPkg.getNameAndVersion, diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 1942b0b9..1d9ed528 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -115,7 +115,7 @@ proc notInclInDevFileMsg*(path: string): string = proc failedToLoadFileMsg*(path: string): string = &"Failed to load \"{path}\"." -proc cannotUninstallPkgMsg*(pkgName, pkgVersion: string, +proc cannotUninstallPkgMsg*(pkgName: string, pkgVersion: Version, deps: seq[string]): string = assert deps.len > 0, "The sequence must have at least one package." result = &"Cannot uninstall {pkgName} ({pkgVersion}) because\n" diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 63ee5f39..7bcef2dc 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -156,6 +156,7 @@ proc cloneSpecificRevision(downloadMethod: DownloadMethod, of DownloadMethod.hg: doCmd(fmt"hg clone {url} -r {vcsRevision}") +{.warning[ProveInit]: off.} proc doDownload(url: string, downloadDir: string, verRange: VersionRange, downMethod: DownloadMethod, options: Options, vcsRevision: Sha1Hash): Version = @@ -223,6 +224,7 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, else: display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, priority = HighPriority) +{.warning[ProveInit]: on.} proc downloadPkg*(url: string, verRange: VersionRange, downMethod: DownloadMethod, @@ -276,7 +278,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, ## Makes sure that the downloaded package's version satisfies the requested ## version range. let pkginfo = getPkgInfo(result[0], options) - if pkginfo.version.newVersion notin verRange: + if pkginfo.version notin verRange: raise nimbleError( "Downloaded package's version does not satisfy requested version " & "range: wanted $1 got $2." % diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim index feb5536e..c6438194 100644 --- a/src/nimblepkg/lockfile.nim +++ b/src/nimblepkg/lockfile.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import tables, os, json -import sha1hashes, packageinfotypes +import version, sha1hashes, packageinfotypes type LockFileJsonKeys* = enum @@ -16,6 +16,7 @@ const proc initLockFileDep(): LockFileDep = result = LockFileDep( + version: notSetVersion, vcsRevision: notSetSha1Hash, checksums: Checksums(sha1: notSetSha1Hash)) diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 91dd78a3..167e1d38 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -12,9 +12,12 @@ import version, tools, common, options, cli, config, lockfile, packageinfotypes, proc initPackageInfo*(): PackageInfo = result = PackageInfo( - basicInfo: ("", "", notSetSha1Hash), + basicInfo: ("", notSetVersion, notSetSha1Hash), metaData: initPackageMetaData()) +proc initPackage*(): Package = + result = Package(version: notSetVersion) + proc isLoaded*(pkgInfo: PackageInfo): bool = return pkgInfo.myPath.len > 0 @@ -74,6 +77,7 @@ proc parseDownloadMethod*(meth: string): DownloadMethod = else: raise nimbleError("Invalid download method: " & meth) +{.warning[ProveInit]: off.} proc fromJson(obj: JSonNode): Package = ## Constructs a Package object from a JSON node. ## @@ -83,7 +87,7 @@ proc fromJson(obj: JSonNode): Package = result.alias = obj.requiredField("alias") else: result.alias = "" - result.version = obj.optionalField("version") + result.version = newVersion(obj.optionalField("version")) result.url = obj.requiredField("url") result.downloadMethod = obj.requiredField("method").parseDownloadMethod result.dvcsTag = obj.optionalField("dvcs-tag") @@ -93,6 +97,7 @@ proc fromJson(obj: JSonNode): Package = result.tags.add(t.str) result.description = obj.requiredField("description") result.web = obj.optionalField("web") +{.warning[ProveInit]: on.} proc needsRefresh*(options: Options): bool = ## Determines whether a ``nimble refresh`` is needed. @@ -228,11 +233,13 @@ proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool = resPkg = resolveAlias(resPkg, options) return true +{.warning[ProveInit]: off.} proc getPackage*(name: string, options: Options): Package = let success = getPackage(name, options, result) if not success: raise nimbleError( "Cannot find package with name '" & name & "'.") +{.warning[ProveInit]: on.} proc getPackageList*(options: Options): seq[Package] = ## Returns the list of packages found in the downloaded packages.json files. @@ -268,7 +275,7 @@ proc findNimbleFile*(dir: string; error: bool): string = proc setNameVersionChecksum*(pkgInfo: var PackageInfo, pkgDir: string) = let (name, version, checksum) = getNameVersionChecksum(pkgDir) pkgInfo.name = name - if pkgInfo.version.len == 0: + if pkgInfo.version == notSetVersion: # if there is no previously set version from the `.nimble` file pkgInfo.version = version pkgInfo.specialVersion = version @@ -299,14 +306,14 @@ proc withinRange*(pkgInfo: PackageInfo, verRange: VersionRange): bool = ## Determines whether the specified package's version is within the ## specified range. The check works with ordinary versions as well as ## special ones. - return withinRange(newVersion(pkgInfo.version), verRange) or - withinRange(newVersion(pkgInfo.specialVersion), verRange) + return withinRange(pkgInfo.version, verRange) or + withinRange(pkgInfo.specialVersion, verRange) proc resolveAlias*(dep: PkgTuple, options: Options): PkgTuple = ## Looks up the specified ``dep.name`` in the packages.json files to resolve ## a potential alias into the package's real name. result = dep - var pkg: Package + var pkg = initPackage() # TODO: This needs better caching. if getPackage(dep.name, options, pkg): # The resulting ``pkg`` will contain the resolved name or the original if @@ -331,7 +338,7 @@ proc findPkg*(pkglist: seq[PackageInfo], dep: PkgTuple, "Should not happen the list to contain " & "the same package in develop mode twice." if withinRange(pkg, dep.ver): - let isNewer = newVersion(r.version) < newVersion(pkg.version) + let isNewer = r.version < pkg.version # If `pkg.isLink` this is a develop mode package and develop mode packages # are always with higher priority than installed packages. if not result or isNewer or pkg.isLink: diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 40ad6cbf..aab3a99e 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -12,7 +12,7 @@ type sha1*: Sha1Hash LockFileDep* = object - version*: string + version*: Version vcsRevision*: Sha1Hash url*: string downloadMethod*: DownloadMethod @@ -31,15 +31,15 @@ type isLink*: bool PackageMetaDataV2* = object of PackageMetaDataBase - specialVersion*: string + specialVersion*: Version PackageMetaData* = object of PackageMetaDataBase isLink*: bool - specialVersion*: string + specialVersion*: Version PackageBasicInfo* = tuple name: string - version: string + version: Version checksum: Sha1Hash PackageInfo* = object @@ -78,7 +78,7 @@ type description*: string tags*: seq[string] # Even if empty, always a valid non nil seq. \ # From here on, optional fields set to the empty string if not available. - version*: string + version*: Version dvcsTag*: string web*: string # Info url for humans. alias*: string ## A name of another package, that this package aliases. diff --git a/src/nimblepkg/packagemetadatafile.nim b/src/nimblepkg/packagemetadatafile.nim index 068bc94d..65c8b998 100644 --- a/src/nimblepkg/packagemetadatafile.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import json, os, strformat -import common, packageinfotypes, cli, tools, sha1hashes +import common, version, packageinfotypes, cli, tools, sha1hashes type MetaDataError* = object of NimbleError @@ -16,7 +16,9 @@ const packageMetaDataFileVersion = "0.1.0" proc initPackageMetaData*(): PackageMetaData = - result = PackageMetaData(vcsRevision: notSetSha1Hash) + result = PackageMetaData( + specialVersion: notSetVersion, + vcsRevision: notSetSha1Hash) proc metaDataError(msg: string): ref MetaDataError = newNimbleError[MetaDataError](msg) diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 5c438dd8..99ed85c3 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -166,7 +166,7 @@ proc validatePackageInfo(pkgInfo: PackageInfo, options: Options) = raise validationError( "The .nimble file name must match name specified inside " & path, true) - if pkgInfo.version == "": + if pkgInfo.version == notSetVersion: raise validationError("Incorrect .nimble file: " & path & " does not contain a version field.", false) @@ -232,7 +232,7 @@ proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = of "package": case ev.key.normalize of "name": result.name = ev.value - of "version": result.version = ev.value + of "version": result.version = newVersion(ev.value) of "author": result.author = ev.value of "description": result.description = ev.value of "license": result.license = ev.value @@ -399,7 +399,7 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): # Validate the rest of the package info last. if not options.disableValidation: - validateVersion(result.version) + validateVersion($result.version) validatePackageInfo(result, options) proc getPkgInfoFromFile*(file: NimbleFile, options: Options, @@ -437,8 +437,11 @@ proc getInstalledPkgs*(libsDir: string, options: Options): seq[PackageInfo] = proc createErrorMsg(tmplt, path, msg: string): string = let (name, version, checksum) = getNameVersionChecksum(path) - let fullVersion = if checksum != notSetSha1Hash: version & "@c." & $checksum - else: version + let fullVersion = + if checksum != notSetSha1Hash: + $version & "@c." & $checksum + else: + $version return tmplt % [name, fullVersion, msg] display("Loading", "list of installed packages", priority = MediumPriority) @@ -497,14 +500,14 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = else: return pkg -proc getConcreteVersion*(pkgInfo: PackageInfo, options: Options): string = +proc getConcreteVersion*(pkgInfo: PackageInfo, options: Options): Version = ## Returns a non-special version from the specified ``pkgInfo``. If the ## ``pkgInfo`` is minimal it looks it up and retrieves the concrete version. result = pkgInfo.version if pkgInfo.isMinimal: let pkgInfo = pkgInfo.toFullInfo(options) result = pkgInfo.version - assert(not newVersion(result).isSpecial) + assert not result.isSpecial when isMainModule: import unittest diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index d5c3674d..b968ae4c 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -213,8 +213,9 @@ proc publish*(p: PackageInfo, o: Options) = elif parsed.username != "" or parsed.password != "": # check for any confidential information # TODO: Use raiseNimbleError(msg, hintMsg) here - raise newException(NimbleError, - "Cannot publish the repository URL because it contains username and/or password. Fix the remote URL. Hint: \"git remote -v\"") + raise nimbleError( + "Cannot publish the repository URL because it contains username " & + "and/or password. Fix the remote URL. Hint: \"git remote -v\"") elif dirExists(os.getCurrentDir() / ".hg"): downloadMethod = "hg" diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index f29474dd..93983b22 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -52,7 +52,7 @@ proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, let dependencies = nimbleData.addIfNotExist( $ndjkRevDep, dep.name.toLower, - dep.version, + $dep.version, $dep.checksum, newJArray()) @@ -97,7 +97,7 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = if rd[$ndjkRevDepChecksum].str.len == 0 and pkg.checksum == notSetSha1Hash: if rd[$ndjkRevDepName].str != pkg.name.toLower or - rd[$ndjkRevDepVersion].str != pkg.specialVersion: + rd[$ndjkRevDepVersion].str != $pkg.specialVersion: newVal.add rd revDepsForVersion[checksum] = newVal @@ -124,7 +124,7 @@ proc getRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency): return let reverseDependencies = nimbleData[$ndjkRevDep]{ - pkg.pkgInfo.name.toLower}{pkg.pkgInfo.version}{$pkg.pkgInfo.checksum} + pkg.pkgInfo.name.toLower}{$pkg.pkgInfo.version}{$pkg.pkgInfo.checksum} if reverseDependencies.isNil: return @@ -138,7 +138,7 @@ proc getRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency): # This is an installed package. let pkgBasicInfo = (name: revDep[$ndjkRevDepName].str, - version: revDep[$ndjkRevDepVersion].str, + version: newVersion(revDep[$ndjkRevDepVersion].str), checksum: revDep[$ndjkRevDepChecksum].str.initSha1Hash) result.incl ReverseDependency(kind: rdkInstalled, pkgInfo: pkgBasicInfo) @@ -177,7 +177,8 @@ when isMainModule: proc initMetaData(isLink: bool): PackageMetaData = result = PackageMetaData( vcsRevision: notSetSha1Hash, - isLink: isLink) + isLink: isLink, + specialVersion: notSetVersion) proc parseRequires(requires: RequiresSeq): seq[PkgTuple] = requires.mapIt((it.name, it.versionRange.parseVersionRange)) @@ -191,7 +192,7 @@ when isMainModule: proc initPackageInfo(name, version, checksum: string, requires: RequiresSeq = @[]): PackageInfo = result = PackageInfo( - basicInfo: (name, version, checksum.initSha1Hash), + basicInfo: (name, version.newVersion, checksum.initSha1Hash), requires: requires.parseRequires, metaData: initMetaData(false)) diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index a30f658b..f0dd13f5 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -199,7 +199,7 @@ proc getNameVersionChecksum*(pkgpath: string): PackageBasicInfo = except InvalidSha1HashError: notSetSha1Hash - return (name, version, sha1Checksum) + return (name, newVersion(version), sha1Checksum) proc removePackageDir*(files: seq[string], dir: string, reportSuccess = false) = for file in files: @@ -229,62 +229,62 @@ when isMainModule: test "directory names without sha1 hashes": check getNameVersionChecksum( "/home/user/.nimble/libs/packagea-0.1") == - ("packagea", "0.1", notSetSha1Hash) + ("packagea", newVersion("0.1"), notSetSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-a-0.1") == - ("package-a", "0.1", notSetSha1Hash) + ("package-a", newVersion("0.1"), notSetSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-a-0.1/package.nimble") == - ("package-a", "0.1", notSetSha1Hash) + ("package-a", newVersion("0.1"), notSetSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-#head") == - ("package", "#head", notSetSha1Hash) + ("package", newVersion("#head"), notSetSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-#branch-with-dashes") == - ("package", "#branch-with-dashes", notSetSha1Hash) + ("package", newVersion("#branch-with-dashes"), notSetSha1Hash) # readPackageInfo (and possibly more) depends on this not raising. check getNameVersionChecksum( "/home/user/.nimble/libs/package") == - ("package", "", notSetSha1Hash) + ("package", newVersion(""), notSetSha1Hash) test "directory names with sha1 hashes": check getNameVersionChecksum( "/home/user/.nimble/libs/packagea-0.1-" & "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82") == - ("packagea", "0.1", + ("packagea", newVersion("0.1"), "9e6df089c5ee3d912006b2d1c016eb8fa7dcde82".initSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-a-0.1-" & "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6") == - ("package-a", "0.1", + ("package-a", newVersion("0.1"), "2f11b50a3d1933f9f8972bd09bc3325c38bc11d6".initSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-a-0.1-" & "43e3b1138312656310e93ffcfdd866b2dcce3b35/package.nimble") == - ("package-a", "0.1", + ("package-a", newVersion("0.1"), "43e3b1138312656310e93ffcfdd866b2dcce3b35".initSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-#head-" & "efba335dccf2631d7ac2740109142b92beb3b465") == - ("package", "#head", + ("package", newVersion("#head"), "efba335dccf2631d7ac2740109142b92beb3b465".initSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-#branch-with-dashes-" & "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c") == - ("package", "#branch-with-dashes", + ("package", newVersion("#branch-with-dashes"), "8f995e59d6fc1012b3c1509fcb0ef0a75cb3610c".initSha1Hash) check getNameVersionChecksum( "/home/user/.nimble/libs/package-" & "b12e18db49fc60df117e5d8a289c4c2050a272dd") == - ("package", "", + ("package", newVersion(""), "b12e18db49fc60df117e5d8a289c4c2050a272dd".initSha1Hash) diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index 8aa4297a..e2dfcadd 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -98,10 +98,12 @@ proc topologicalSort*(graph: LockFileDeps): when isMainModule: import unittest + from version import notSetVersion from sha1hashes import notSetSha1Hash proc initLockFileDep(deps: seq[string] = @[]): LockFileDep = result = LockFileDep( + version: notSetVersion, vcsRevision: notSetSha1Hash, dependencies: deps, checksums: Checksums(sha1: notSetSha1Hash)) diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index ad2efdfe..3ecaf932 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -2,10 +2,12 @@ # BSD License. Look at license.txt for more info. ## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5`` +import json import common, strutils, tables, hashes, parseutils type - Version* = distinct string + Version* {.requiresInit.} = object + version: string VersionRangeEnum* = enum verLater, # > V @@ -36,16 +38,29 @@ type ParseVersionError* = object of NimbleError +const + notSetVersion* = Version(version: "-1") + proc parseVersionError*(msg: string): ref ParseVersionError = result = newNimbleError[ParseVersionError](msg) -proc `$`*(ver: Version): string {.borrow.} -proc hash*(ver: Version): Hash {.borrow.} +template `$`*(ver: Version): string = ver.version +template hash*(ver: Version): Hash = ver.version.hash +template `%`*(ver: Version): JsonNode = %ver.version proc newVersion*(ver: string): Version = - doAssert(ver.len == 0 or ver[0] in {'#', '\0'} + Digits, - "Wrong version: " & ver) - return Version(ver) + if ver.len != 0 and ver[0] notin {'#', '\0'} + Digits: + raise parseVersionError("Wrong version: " & ver) + return Version(version: ver) + +proc initFromJson*(dst: var Version, jsonNode: JsonNode, jsonPath: var string) = + case jsonNode.kind + of JNull: dst = notSetVersion + of JObject: dst = newVersion(jsonNode["version"].str) + of JString: dst = newVersion(jsonNode.str) + else: + assert false, + "The `jsonNode` must have one of {JNull, JObject, JString} kinds." proc isSpecial*(ver: Version): bool = return ($ver).len > 0 and ($ver)[0] == '#' @@ -62,8 +77,8 @@ proc `<`*(ver: Version, ver2: Version): bool = return ($ver).normalize != "#head" # Handling for normal versions such as "0.1.0" or "1.0". - var sVer = string(ver).split('.') - var sVer2 = string(ver2).split('.') + var sVer = ver.version.split('.') + var sVer2 = ver2.version.split('.') for i in 0..max(sVer.len, sVer2.len)-1: var sVerI = 0 if i < sVer.len: @@ -81,9 +96,8 @@ proc `<`*(ver: Version, ver2: Version): bool = proc `==`*(ver: Version, ver2: Version): bool = if ver.isSpecial or ver2.isSpecial: return ($ver).toLowerAscii() == ($ver2).toLowerAscii() - - var sVer = string(ver).split('.') - var sVer2 = string(ver2).split('.') + var sVer = ver.version.split('.') + var sVer2 = ver2.version.split('.') for i in 0..max(sVer.len, sVer2.len)-1: var sVerI = 0 if i < sVer.len: @@ -137,9 +151,9 @@ proc withinRange*(ver: Version, ran: VersionRange): bool = proc contains*(ran: VersionRange, ver: Version): bool = return withinRange(ver, ran) -proc getNextIncompatibleVersion(version: string, semver: bool): string = +proc getNextIncompatibleVersion(version: Version, semver: bool): Version = ## try to get next higher version to exclude according to semver semantic - var numbers = version.split('.') + var numbers = version.version.split('.') let originalNumberLen = numbers.len while numbers.len < 3: numbers.add("0") @@ -166,32 +180,37 @@ proc getNextIncompatibleVersion(version: string, semver: bool): string = while zeroPosition < numbers.len: numbers[zeroPosition] = "0" inc(zeroPosition) - result = numbers.join(".") + result = newVersion(numbers.join(".")) -proc makeRange*(version: string, op: string): VersionRange = - if version == "": +proc makeRange*(version: Version, op: string): VersionRange = + if version == notSetVersion: raise parseVersionError("A version needs to accompany the operator.") + case op of ">": - result = VersionRange(kind: verLater) + result = VersionRange(kind: verLater, ver: version) of "<": - result = VersionRange(kind: verEarlier) + result = VersionRange(kind: verEarlier, ver: version) of ">=": - result = VersionRange(kind: verEqLater) + result = VersionRange(kind: verEqLater, ver: version) of "<=": - result = VersionRange(kind: verEqEarlier) + result = VersionRange(kind: verEqEarlier, ver: version) of "", "==": - result = VersionRange(kind: verEq) + result = VersionRange(kind: verEq, ver: version) of "^=", "~=": - result = VersionRange(kind: if op == "^=": verCaret else: verTilde) - result.verILeft = makeRange(version, ">=") - var excludedVersion = getNextIncompatibleVersion(version, - semver = (op == "^=")) - result.verIRight = makeRange(excludedVersion, "<") - return + let + excludedVersion = getNextIncompatibleVersion( + version, semver = (op == "^=")) + left = makeRange(version, ">=") + right = makeRange(excludedVersion, "<") + + result = + if op == "^=": + VersionRange(kind: verCaret, verILeft: left, verIRight: right) + else: + VersionRange(kind: verTilde, verILeft: left, verIRight: right) else: raise parseVersionError("Invalid operator: " & op) - result.ver = Version(version) proc parseVersionRange*(s: string): VersionRange = # >= 1.5 & <= 1.8 @@ -200,8 +219,7 @@ proc parseVersionRange*(s: string): VersionRange = return if s[0] == '#': - result = VersionRange(kind: verSpecial) - result.spe = s.Version + result = VersionRange(kind: verSpecial, spe: newVersion(s)) return var i = 0 @@ -213,7 +231,7 @@ proc parseVersionRange*(s: string): VersionRange = op.add(s[i]) of '&': result = VersionRange(kind: verIntersect) - result.verILeft = makeRange(version, op) + result.verILeft = makeRange(newVersion(version), op) # Parse everything after & # Recursion <3 @@ -238,17 +256,18 @@ proc parseVersionRange*(s: string): VersionRange = raise parseVersionError( "Unexpected char in version range '" & s & "': " & s[i]) inc(i) - result = makeRange(version, op) + result = makeRange(newVersion(version), op) + +proc parseVersionRange*(version: Version): VersionRange = + result = version.version.parseVersionRange proc toVersionRange*(ver: Version): VersionRange = ## Converts a version to either a verEq or verSpecial VersionRange. - new(result) - if ver.isSpecial: - result = VersionRange(kind: verSpecial) - result.spe = ver - else: - result = VersionRange(kind: verEq) - result.ver = ver + result = + if ver.isSpecial: + VersionRange(kind: verSpecial, spe: ver) + else: + VersionRange(kind: verEq, ver: ver) proc parseRequires*(req: string): PkgTuple = try: @@ -290,7 +309,7 @@ proc `$`*(verRange: VersionRange): string = of verAny: return "any version" - result.add(string(verRange.ver)) + result.add($verRange.ver) proc getSimpleString*(verRange: VersionRange): string = ## Gets a string with no special symbols and spaces. Used for dir name @@ -309,13 +328,11 @@ proc getSimpleString*(verRange: VersionRange): string = proc newVRAny*(): VersionRange = result = VersionRange(kind: verAny) -proc newVREarlier*(ver: string): VersionRange = - result = VersionRange(kind: verEarlier) - result.ver = newVersion(ver) +proc newVREarlier*(ver: Version): VersionRange = + result = VersionRange(kind: verEarlier, ver: ver) -proc newVREq*(ver: string): VersionRange = - result = VersionRange(kind: verEq) - result.ver = newVersion(ver) +proc newVREq*(ver: Version): VersionRange = + result = VersionRange(kind: verEq, ver: ver) proc findLatest*(verRange: VersionRange, versions: OrderedTable[Version, string]): tuple[ver: Version, tag: string] = diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index d78d33aa..5afd6e0b 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -8,7 +8,7 @@ import testscommon, nimblepkg/displaymessages, nimblepkg/paths from nimblepkg/common import cd from nimblepkg/developfile import developFileName, pkgFoundMoreThanOnceMsg -from nimblepkg/version import parseVersionRange +from nimblepkg/version import newVersion, parseVersionRange from nimblepkg/nimbledatafile import nimbleDataFileName, NimbleDataJsonKeys suite "develop feature": @@ -79,7 +79,8 @@ suite "develop feature": check exitCode == QuitFailure var lines = output.processOutput check lines.inLinesOrdered( - cannotUninstallPkgMsg(pkgAName, "0.2.0", @[installDir / pkgBName])) + cannotUninstallPkgMsg(pkgAName, newVersion("0.2.0"), + @[installDir / pkgBName])) test "can reject binary packages": cd "develop/binary": diff --git a/tests/tissues.nim b/tests/tissues.nim index 790dab20..fbc8a685 100644 --- a/tests/tissues.nim +++ b/tests/tissues.nim @@ -6,6 +6,7 @@ import unittest, os, osproc, strutils, sequtils, strformat import testscommon from nimblepkg/common import cd, nimbleVersion, nimblePackagesDirName +from nimblepkg/version import newVersion from nimblepkg/displaymessages import cannotUninstallPkgMsg suite "issues": @@ -372,7 +373,8 @@ suite "issues": pkgBInstallDir = getPackageDir(pkgsDir, "b-0.1.0").splitPath.tail check exitCode != QuitSuccess - check lines.inLines(cannotUninstallPkgMsg("c", "0.1.0", @[pkgBInstallDir])) + check lines.inLines( + cannotUninstallPkgMsg("c", newVersion("0.1.0"), @[pkgBInstallDir])) check execNimbleYes(["remove", "a"]).exitCode == QuitSuccess check execNimbleYes(["remove", "b"]).exitCode == QuitSuccess diff --git a/tests/tuninstall.nim b/tests/tuninstall.nim index f1ccd7bd..8da4ae47 100644 --- a/tests/tuninstall.nim +++ b/tests/tuninstall.nim @@ -8,6 +8,7 @@ import testscommon from nimblepkg/displaymessages import cannotUninstallPkgMsg from nimblepkg/common import cd +from nimblepkg/version import newVersion suite "uninstall": test "can install packagebin2": @@ -57,7 +58,8 @@ suite "uninstall": check exitCode != QuitSuccess var ls = outp.strip.processOutput() let pkg27ADir = getPackageDir(pkgsDir, "issue27a-0.1.0", false) - let expectedMsg = cannotUninstallPkgMsg("issue27b", "0.1.0", @[pkg27ADir]) + let expectedMsg = cannotUninstallPkgMsg( + "issue27b", newVersion("0.1.0"), @[pkg27ADir]) check ls.inLinesOrdered(expectedMsg) check execNimbleYes("uninstall", "issue27").exitCode == QuitSuccess @@ -73,9 +75,9 @@ suite "uninstall": pkgBin2Dir = getPackageDir(pkgsDir, "packagebin2-0.1.0", false) pkgBDir = getPackageDir(pkgsDir, "packageb-0.1.0", false) expectedMsgForPkgA0dot6 = cannotUninstallPkgMsg( - "PackageA", "0.6.0", @[pkgBin2Dir]) + "PackageA", newVersion("0.6.0"), @[pkgBin2Dir]) expectedMsgForPkgA0dot2 = cannotUninstallPkgMsg( - "PackageA", "0.2.0", @[pkgBDir]) + "PackageA", newVersion("0.2.0"), @[pkgBDir]) check ls.inLines(expectedMsgForPkgA0dot6) check ls.inLines(expectedMsgForPkgA0dot2) From 31085d774a5ec90345d6bcc3439f4fe5955593a4 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 24 Feb 2021 18:04:24 +0200 Subject: [PATCH 28/73] Change path command to return all installed paths Nimble's `path` command is changed to return all installed paths for a package, but not only the latest version, because multiple latest versions with different checksums can be installed. The command is no longer capable of giving paths to develop packages because already there is no record with a link file for them in the cache. Related to nim-lang/nimble#127 --- src/nimble.nim | 12 +++++++----- tests/tpathcommand.nim | 12 ------------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 7f75086d..37ac3e89 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -703,12 +703,13 @@ type VersionAndPath = tuple[version: Version, path: string] proc listPaths(options: Options) = ## Loops over the specified 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. + ## If there are several packages installed, all of them 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. + cli.setSuppressMessages(true) assert options.action.typ == actionPath @@ -727,7 +728,8 @@ proc listPaths(options: Options) = if installed.len > 0: sort(installed, cmp[VersionAndPath], Descending) # The output for this command is used by tools so we do not use display(). - echo installed[0].path + for pkg in installed: + echo pkg.path else: display("Warning:", "Package '$1' is not installed" % name, Warning, MediumPriority) diff --git a/tests/tpathcommand.nim b/tests/tpathcommand.nim index e98cbf8b..88609118 100644 --- a/tests/tpathcommand.nim +++ b/tests/tpathcommand.nim @@ -15,15 +15,3 @@ suite "path command": let (output, _) = execNimble("path", "srcdirtest") let packageDir = getPackageDir(pkgsDir, "srcdirtest-1.0") check output.strip() == packageDir - - # test "nimble path points to develop": - # cd "develop/srcdirtest": - # var (output, exitCode) = execNimble("develop") - # checkpoint output - # check exitCode == QuitSuccess - - # (output, exitCode) = execNimble("path", "srcdirtest") - - # checkpoint output - # check exitCode == QuitSuccess - # check output.strip() == getCurrentDir() / "src" From 552cc441b4524e98efdb65877d3aab45988be53e Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 16 Apr 2021 19:49:18 +0300 Subject: [PATCH 29/73] Add files to .gitignore on `nimble setup` On `nimble setup` command the files which should not be committed are added to the `.gitignore` file by first creating it in the case it doesn't already exist. For now, those are: - "nimble.develop" - "nimble.paths" Additional changes: - Fixed the preceding empty line on adding content or generating of the "config.nims" file on the execution of "nimble setup" command. - Fixed a warning about usage of deprecated procedure. Related to nim-lang/nimble#127 --- src/nimble.nim | 69 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 37ac3e89..8c2059e7 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -24,6 +24,8 @@ import nimblepkg/packageinfotypes, nimblepkg/packageinfo, nimblepkg/version, const nimblePathsFileName* = "nimble.paths" nimbleConfigFileName* = "config.nims" + gitIgnoreFileName = ".gitignore" + hgIgnoreFileName = ".hgignore" proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -1509,7 +1511,14 @@ proc sync(options: Options) = if errors.len > 0: raise validationErrors(errors) -proc setup(options: Options) = +proc append(existingContent: var string; newContent: string) = + ## Appends `newContent` to the `existingContent` on a new line by inserting it + ## if the new line doesn't already exist. + if existingContent.len > 0 and existingContent[^1] != '\n': + existingContent &= "\n" + existingContent &= newContent + +proc setupNimbleConfig(options: Options) = ## Creates `nimble.paths` file containing file system paths to the ## dependencies. Includes it in `config.nims` file to make them available ## for the compiler. @@ -1522,24 +1531,27 @@ when fileExists("{nimblePathsFileName}"): # end Nimble config """ - let currentDir = getCurrentDir() - let pkgInfo = getPkgInfo(currentDir, options) + let + currentDir = getCurrentDir() + pkgInfo = getPkgInfo(currentDir, options) + updatePathsFile(pkgInfo, options) - proc constructNimbleConfig: string = - result &= "\n" - result &= configFileHeader - result &= configFileContent + var + writeFile = false + fileContent: string + + proc appendNimbleConfigFileHeaderAndContent = + fileContent.append(configFileHeader) + fileContent.append(configFileContent) - var writeFile = false - var fileContent: string if fileExists(nimbleConfigFileName): fileContent = readFile(nimbleConfigFileName) if not fileContent.contains(configFileHeader): - fileContent &= constructNimbleConfig() + appendNimbleConfigFileHeaderAndContent() writeFile = true else: - fileContent = constructNimbleConfig() + appendNimbleConfigFileHeaderAndContent() writeFile = true if writeFile: @@ -1548,6 +1560,41 @@ when fileExists("{nimblePathsFileName}"): else: displayInfo(&"\"{nimbleConfigFileName}\" is already set up.") +proc setupVcsIgnoreFile = + ## Adds the names of some files which should not be committed to the VCS + ## ignore file. + let + currentDir = getCurrentDir() + vcsIgnoreFileName = case currentDir.getVcsType + of vcsTypeGit: gitIgnoreFileName + of vcsTypeHg: hgIgnoreFileName + of vcsTypeNone: raise nimbleError( + &"The directory {currentDir} is not under version control.") + + var + writeFile = false + fileContent: string + + if fileExists(vcsIgnoreFileName): + fileContent = readFile(vcsIgnoreFileName) + if not fileContent.contains(developFileName): + fileContent.append(developFileName) + writeFile = true + if not fileContent.contains(nimblePathsFileName): + fileContent.append(nimblePathsFileName) + writeFile = true + else: + fileContent.append(developFileName) + fileContent.append(nimblePathsFileName) + writeFile = true + + if writeFile: + writeFile(vcsIgnoreFileName, fileContent & "\n") + +proc setup(options: Options) = + setupNimbleConfig(options) + setupVcsIgnoreFile() + proc run(options: Options) = # Verify parameters. var pkgInfo = getPkgInfo(getCurrentDir(), options) From dbdd585e61657a23fb29ab65a19dde4a1d54beaa Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sat, 17 Apr 2021 23:15:48 +0300 Subject: [PATCH 30/73] Remove repository backward compatibility code The code added to support backward compatibility with the old version of the Nimble cache packages repository is removed because migration functionality to the new cache format will not be implemented. The reason for this is that checksums of the packages must be calculated on package download including all files, instead of only installed ones. Otherwise, some files which are not installed could be used in the build of a binary package executable and if so the checksum will not describe the package correctly since the executable is also installed. Additionally, this commit fixes some tests. Related to nim-lang/nimble#127 --- src/nimblepkg/common.nim | 45 --------------- src/nimblepkg/developfile.nim | 38 +------------ src/nimblepkg/nimbledatafile.nim | 19 ++----- src/nimblepkg/nimblelinkfile.nim | 17 ------ src/nimblepkg/packageinfotypes.nim | 12 +--- src/nimblepkg/packagemetadatafile.nim | 18 ++---- src/nimblepkg/reversedeps.nim | 9 +-- tests/revdep/nimbleData/new_nimble_data.json | 59 -------------------- tests/revdep/nimbleData/old_nimble_data.json | 44 --------------- tests/setup/binary/.gitignore | 2 + tests/setup/dependent/.gitignore | 2 + tests/tnimscript.nim | 3 +- tests/treversedeps.nim | 17 +----- 13 files changed, 23 insertions(+), 262 deletions(-) delete mode 100644 src/nimblepkg/nimblelinkfile.nim delete mode 100644 tests/revdep/nimbleData/new_nimble_data.json delete mode 100644 tests/revdep/nimbleData/old_nimble_data.json create mode 100644 tests/setup/binary/.gitignore create mode 100644 tests/setup/dependent/.gitignore diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 7fd2028d..412eba0a 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -44,27 +44,6 @@ proc nimbleQuit*(exitCode = QuitSuccess): ref NimbleQuit = result = newException(NimbleQuit, "") result.exitCode = exitCode -proc hasField(NewType: type[object], fieldName: static string, - FieldType: type): bool {.compiletime.} = - for name, value in fieldPairs(NewType.default): - if name == fieldName and $value.typeOf == $FieldType: - return true - return false - -macro accessField(obj: typed, name: static string): untyped = - newDotExpr(obj, ident(name)) - -proc to*(obj: object, NewType: type[object]): NewType = - ## Creates an object of `NewType` type, with all fields with both same name - ## and type like a field of `obj`, set to the values of the corresponding - ## fields of `obj`. - - # `ResultType` is a bug workaround: "Cannot evaluate at compile time: NewType" - type ResultType = NewType - for name, value in fieldPairs(obj): - when ResultType.hasField(name, value.typeOf): - accessField(result, name) = value - template newClone*[T: not ref](obj: T): ref T = ## Creates a garbage collected heap copy of not a reference object. {.warning[ProveInit]: off.} @@ -98,27 +77,3 @@ template cdNewDir*(dir: string, body: untyped) = createNewDir dir cd dir: body - -template debugTrace*(): untyped = - block: - let (filename, line, _ {.used.}) = instantiationInfo() - echo "filename = $#; line: $#" % [filename, $line] - -when isMainModule: - import unittest - - test "to": - type - Foo = object - i: int - f: float - - Bar = object - i: string - f: float - s: string - - let foo = Foo(i: 42, f: 3.1415) - var bar = to(foo, Bar) - bar.s = "hello" - check bar == Bar(i: "", f: 3.1415, s: "hello") diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index f91106e2..5c19d2d9 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -298,7 +298,7 @@ proc pkgFoundMoreThanOnceMsg*( pkgName: string, collisions: HashSet[NameCollisionRecord]): string = result = &"A package with name \"{pkgName}\" is found more than once." for (pkgPath, inclFilePath) in collisions: - result &= &"\"{pkgPath}\" from file \"{inclFilePath}\"" + result &= &"\n\"{pkgPath}\" from file \"{inclFilePath}\"" proc getErrorsDetails(errors: ErrorsCollection): string = ## Constructs a message with details about the collected errors. @@ -604,42 +604,6 @@ proc addDevelopPackage(data: var DevelopFileData, path: Path, return addDevelopPackage(data, pkgInfo) -# proc addDevelopPackageEx*(data: var DevelopFileData, path: Path, -# options: Options) = -# ## Adds a package at path `path` to a free develop file intended for inclusion -# ## in other packages develop files. -# ## -# ## Raises if: -# ## - the path in `path` does not point to a valid Nimble package. -# ## - a package with the same name but at different path is already present -# ## in the develop file or some of its includes. -# ## - the path is already present in the develop file. - -# assert not data.dependentPkg.isSome, -# "This procedure can only be used for free develop files intended " & -# "for inclusion in other packages develop files." - -# let (pkg, error) = validatePackage(path, PackageInfo.none, options) -# if error != nil: -# raise error - -# # Check whether the develop file already contains a package with a name -# # `pkg.name` at different path. -# if data.nameToPkg.hasKey(pkg.name) and not data.pathToPkg.hasKey(path): -# raise nimbleError( -# pkgAlreadyPresentAtDifferentPathMsg(pkg.name, $data.pathToPkg[pkg.name])) - -# # Add `pkg` to the develop file model. -# let success = not data.dependencies.containsOrIncl(path) - -# var collidingNames: CollidingNames -# addPackage(data, pkg, data.path, [data.path].toHashSet, collidingNames) -# assert collidingNames.len == 0, "Must not have the same package name at " & -# "path different than already existing one." - -# if not success: -# raise nimbleError(pkgAlreadyInDevModeMsg(pkg.getNameAndVersion, $path)) - proc removePackage(data: var DevelopFileData, pkg: ref PackageInfo, devFileName: Path) = ## Decreases the reference count for a package at path `path` and removes the diff --git a/src/nimblepkg/nimbledatafile.nim b/src/nimblepkg/nimbledatafile.nim index e50cdc91..68b9126e 100644 --- a/src/nimblepkg/nimbledatafile.nim +++ b/src/nimblepkg/nimbledatafile.nim @@ -34,19 +34,6 @@ proc saveNimbleData*(options: Options) = proc newNimbleDataNode*(): JsonNode = %{ $ndjkVersion: %nimbleDataFileVersion, $ndjkRevDep: newJObject() } -proc convertToTheNewFormat(nimbleData: JsonNode) = - nimbleData.add($ndjkVersion, %nimbleDataFileVersion) - for name, versions in nimbleData[$ndjkRevDep]: - for version, dependencies in versions: - for dependency in dependencies: - dependency.add($ndjkRevDepChecksum, %"") - versions[version] = %{ "": dependencies } - -proc loadNimbleData*(fileName: string): JsonNode = - result = parseFile(fileName) - if not result.hasKey($ndjkVersion): - convertToTheNewFormat(result) - proc removeDeadDevelopReverseDeps*(options: var Options) = template revDeps: var JsonNode = options.nimbleData[$ndjkRevDep] var hasDeleted = false @@ -67,7 +54,11 @@ proc loadNimbleData*(options: var Options) = fileName = nimbleDir / nimbleDataFileName if fileExists(fileName): - options.nimbleData = loadNimbleData(fileName) + options.nimbleData = parseFile(fileName) + if not options.nimbleData.hasKey($ndjkVersion): + raise nimbleError( + "You are working with an old version of Nimble cache repository.\n", + &"Please delete your \"{options.getNimbleDir()}\" directory.") removeDeadDevelopReverseDeps(options) displayInfo(&"Nimble data file \"{fileName}\" has been loaded.", LowPriority) diff --git a/src/nimblepkg/nimblelinkfile.nim b/src/nimblepkg/nimblelinkfile.nim deleted file mode 100644 index 00614def..00000000 --- a/src/nimblepkg/nimblelinkfile.nim +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) Dominik Picheta. All rights reserved. -# BSD License. Look at license.txt for more info. - -import strutils - -type - NimbleLink* = object - nimbleFilePath*: string - packageDir*: string - -const - nimbleLinkFileExt* = ".nimble-link" - -proc readNimbleLink*(nimbleLinkPath: string): NimbleLink = - let s = readFile(nimbleLinkPath).splitLines() - result.nimbleFilePath = s[0] - result.packageDir = s[1] diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index aab3a99e..478f278d 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -21,20 +21,11 @@ type LockFileDeps* = OrderedTable[string, LockFileDep] - PackageMetaDataBase* {.inheritable.} = object + PackageMetaData* = object url*: string vcsRevision*: Sha1Hash files*: seq[string] binaries*: seq[string] - - PackageMetaDataV1* = object of PackageMetaDataBase - isLink*: bool - - PackageMetaDataV2* = object of PackageMetaDataBase - specialVersion*: Version - - PackageMetaData* = object of PackageMetaDataBase - isLink*: bool specialVersion*: Version PackageBasicInfo* = tuple @@ -68,6 +59,7 @@ type basicInfo*: PackageBasicInfo lockedDeps*: LockFileDeps metaData*: PackageMetaData + isLink*: bool Package* = object ## Definition of package from packages.json. # Required fields in a package. diff --git a/src/nimblepkg/packagemetadatafile.nim b/src/nimblepkg/packagemetadatafile.nim index 65c8b998..12787ef6 100644 --- a/src/nimblepkg/packagemetadatafile.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -25,9 +25,7 @@ proc metaDataError(msg: string): ref MetaDataError = proc saveMetaData*(metaData: PackageMetaData, dirName: string) = ## Saves some important data to file in the package installation directory. - {.warning[ProveInit]: off.} - var metaDataWithChangedPaths = to(metaData, PackageMetaDataV2) - {.warning[ProveInit]: on.} + var metaDataWithChangedPaths = metaData for i, file in metaData.files: metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file) let json = %{ @@ -40,17 +38,9 @@ proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData = result = initPackageMetaData() let fileName = dirName / packageMetaDataFileName if fileExists(fileName): - let json = parseFile(fileName) - if not json.hasKey($pmdjkVersion): - {.warning[ProveInit]: off.} - result = to(json.to(PackageMetaDataV1), PackageMetaData) - {.warning[ProveInit]: on.} - let (_, specialVersion, _) = getNameVersionChecksum(dirName) - result.specialVersion = specialVersion - else: - {.warning[ProveInit]: off.} - result = to(json[$pmdjkMetaData].to(PackageMetaDataV2), PackageMetaData) - {.warning[ProveInit]: on.} + {.warning[ProveInit]: off.} + result = parseFile(fileName)[$pmdjkMetaData].to(PackageMetaData) + {.warning[ProveInit]: on.} elif raiseIfNotFound: raise metaDataError(&"No {packageMetaDataFileName} file found in {dirName}") else: diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 93983b22..d59e1f24 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -174,10 +174,9 @@ when isMainModule: type RequiresSeq = seq[tuple[name, versionRange: string]] - proc initMetaData(isLink: bool): PackageMetaData = + proc initMetaData: PackageMetaData = result = PackageMetaData( vcsRevision: notSetSha1Hash, - isLink: isLink, specialVersion: notSetVersion) proc parseRequires(requires: RequiresSeq): seq[PkgTuple] = @@ -187,14 +186,16 @@ when isMainModule: result = PackageInfo( myPath: path, requires: requires.parseRequires, - metaData: initMetaData(true)) + metaData: initMetaData(), + isLink: true) proc initPackageInfo(name, version, checksum: string, requires: RequiresSeq = @[]): PackageInfo = result = PackageInfo( basicInfo: (name, version.newVersion, checksum.initSha1Hash), requires: requires.parseRequires, - metaData: initMetaData(false)) + metaData: initMetaData(), + isLink: false) let nimforum1 = initPackageInfo( diff --git a/tests/revdep/nimbleData/new_nimble_data.json b/tests/revdep/nimbleData/new_nimble_data.json deleted file mode 100644 index 4a148b89..00000000 --- a/tests/revdep/nimbleData/new_nimble_data.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "version": "0.1.0", - "reverseDeps": { - "stew": { - "0.1.0": { - "": [ - { - "name": "faststreams", - "version": "0.1.0", - "checksum": "" - }, - { - "name": "serialization", - "version": "0.1.0", - "checksum": "" - }, - { - "name": "json_serialization", - "version": "0.1.0", - "checksum": "" - } - ] - } - }, - "faststreams": { - "0.1.0": { - "": [ - { - "name": "serialization", - "version": "0.1.0", - "checksum": "" - } - ] - } - }, - "serialization": { - "0.1.0": { - "": [ - { - "name": "json_serialization", - "version": "0.1.0", - "checksum": "" - } - ] - } - }, - "json_serialization": { - "0.1.0": { - "": [ - { - "name": "chronicles", - "version": "0.6.0", - "checksum": "" - } - ] - } - } - } -} diff --git a/tests/revdep/nimbleData/old_nimble_data.json b/tests/revdep/nimbleData/old_nimble_data.json deleted file mode 100644 index 519f9bc6..00000000 --- a/tests/revdep/nimbleData/old_nimble_data.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "reverseDeps": { - "stew": { - "0.1.0": [ - { - "name": "faststreams", - "version": "0.1.0" - }, - { - "name": "serialization", - "version": "0.1.0" - }, - { - "name": "json_serialization", - "version": "0.1.0" - } - ] - }, - "faststreams": { - "0.1.0": [ - { - "name": "serialization", - "version": "0.1.0" - } - ] - }, - "serialization": { - "0.1.0": [ - { - "name": "json_serialization", - "version": "0.1.0" - } - ] - }, - "json_serialization": { - "0.1.0": [ - { - "name": "chronicles", - "version": "0.6.0" - } - ] - } - } -} diff --git a/tests/setup/binary/.gitignore b/tests/setup/binary/.gitignore new file mode 100644 index 00000000..2ca3f1fe --- /dev/null +++ b/tests/setup/binary/.gitignore @@ -0,0 +1,2 @@ +nimble.develop +nimble.paths diff --git a/tests/setup/dependent/.gitignore b/tests/setup/dependent/.gitignore new file mode 100644 index 00000000..2ca3f1fe --- /dev/null +++ b/tests/setup/dependent/.gitignore @@ -0,0 +1,2 @@ +nimble.develop +nimble.paths diff --git a/tests/tnimscript.nim b/tests/tnimscript.nim index 26f79934..414c750b 100644 --- a/tests/tnimscript.nim +++ b/tests/tnimscript.nim @@ -105,8 +105,7 @@ suite "nimscript": cd "invalidPackage": let (output, exitCode) = execNimble("check") let lines = output.strip.processOutput() - check(lines.inLines( - "undeclared identifier: 'thisFieldDoesNotExist'")) + check(lines.inLines("undeclared identifier: 'thisFieldDoesNotExist'")) check exitCode == QuitFailure test "can accept short flags (#329)": diff --git a/tests/treversedeps.nim b/tests/treversedeps.nim index ece25f6b..7928d349 100644 --- a/tests/treversedeps.nim +++ b/tests/treversedeps.nim @@ -3,11 +3,10 @@ {.used.} -import unittest, os, strutils, json +import unittest, os, strutils import testscommon from nimblepkg/common import cd -from nimblepkg/nimbledatafile import loadNimbleData suite "reverse dependencies": test "basic test": @@ -68,17 +67,3 @@ suite "reverse dependencies": check execNimble("path", "nimboost").exitCode != QuitSuccess check execNimble("path", "nimfp").exitCode != QuitSuccess - - test "old format conversion": - const oldNimbleDataFileName = - "./revdep/nimbleData/old_nimble_data.json".normalizedPath - const newNimbleDataFileName = - "./revdep/nimbleData/new_nimble_data.json".normalizedPath - - doAssert fileExists(oldNimbleDataFileName) - doAssert fileExists(newNimbleDataFileName) - - let oldNimbleData = loadNimbleData(oldNimbleDataFileName) - let newNimbleData = loadNimbleData(newNimbleDataFileName) - - doAssert oldNimbleData == newNimbleData From 6e94e5b824c292f162cb6aee8faad6ec35e21721 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 18 Apr 2021 22:14:37 +0300 Subject: [PATCH 31/73] Fix a bug in the building of the dependency graph In the old code when constructing the dependency graph, which is stored in the lock file, the package URL and download method were get from the Nimble packages list. This is not correct because the package could be not included in it and even it is present in the packages list, if it is in development mode, its locked version could be from some fork. Now the package URL and download method are retrieved from the `PackageInfo` object. For not installed packages, package URL is being set there, by getting it from the repository directory using the push URL corresponding to the remote-tracking branch which the current branch tracks. If there is no such branch the push URL of the default remote of the VCS is being used. The download method is also set in the `PackageInfo` object. It is being determined either by querying the type of the VCS under which is the package directory in the case of not installed packages or by an added field in the package metadata in the case of installed ones. Related to nim-lang/nimble#127 --- src/nimble.nim | 2 +- src/nimblepkg/packageinfotypes.nim | 1 + src/nimblepkg/packageparser.nim | 20 +++++++++--- src/nimblepkg/topologicalsort.nim | 37 ++++++++++++++++------ src/nimblepkg/vcstools.nim | 50 ++++++++++++++++++++++++------ 5 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 8c2059e7..d2fe5e07 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -1349,7 +1349,7 @@ proc lock(options: Options) = let dependencies = pkgInfo.processFreeDependencies(options).map( pkg => pkg.toFullInfo(options)) - var dependencyGraph = buildDependencyGraph(dependencies, options) + var dependencyGraph = buildDependencyGraph(dependencies.toSeq, options) if currentDir.lockFileExists: # If we already have a lock file, merge its data with the newly generated diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 478f278d..573ba329 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -23,6 +23,7 @@ type PackageMetaData* = object url*: string + downloadMethod*: DownloadMethod vcsRevision*: Sha1Hash files*: seq[string] binaries*: seq[string] diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 99ed85c3..b43646bb 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -379,13 +379,25 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): let fileDir = nf.splitFile().dir if not fileDir.startsWith(options.getPkgsDir()): - # If the `.nimble` file is not in the installation directory but in the - # package repository we have to get its VCS revision and to calculate its - # checksum. - result.vcsRevision = getVcsRevision(fileDir.Path) + # If the `.nimble` file is not in the installation directory we have to get + # some of the package meta data from its directory. result.checksum = calculateDirSha1Checksum(fileDir) # By default specialVersion is the same as version. result.specialVersion = result.version + # If the `fileDir` is a VCS repository we can get some of the package meta + # data from it. + result.vcsRevision = getVcsRevision(fileDir) + + case getVcsType(fileDir) + of vcsTypeGit: result.downloadMethod = DownloadMethod.git + of vcsTypeHg: result.downloadMethod = DownloadMethod.hg + of vcsTypeNone: discard + + try: + result.url = getRemoteFetchUrl(fileDir, + getCorrespondingRemoteAndBranch(fileDir).remote) + except NimbleError: + discard else: # Otherwise we have to get its name, special version and checksum from the # package directory. diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index e2dfcadd..b1a65d0f 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -1,22 +1,39 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import sequtils, sugar, tables, strformat, algorithm, sets -import packageinfotypes, packageinfo, options, cli - -proc buildDependencyGraph*(packages: HashSet[PackageInfo], options: Options): +import sequtils, tables, strformat, algorithm, sets +import common, packageinfotypes, packageinfo, options, cli + +proc getDependencies(packages: seq[PackageInfo], package: PackageInfo, + options: Options): + seq[string] = + ## Returns the names of the packages which are dependencies of a given + ## package. It is needed because some of the names of the packages in the + ## `requires` clause of a package could be URLs. + for dep in package.requires: + if dep.name == "nim": + continue + var depPkgInfo = initPackageInfo() + var found = findPkg(packages, dep, depPkgInfo) + if not found: + let resolvedDep = dep.resolveAlias(options) + found = findPkg(packages, resolvedDep, depPkgInfo) + if not found: + raise nimbleError( + "Cannot build the dependency graph.\n" & + &"Missing package \"{dep.name}\".") + result.add depPkgInfo.name + +proc buildDependencyGraph*(packages: seq[PackageInfo], options: Options): LockFileDeps = ## Creates records which will be saved to the lock file. - for pkgInfo in packages: - let pkg = getPackage(pkgInfo.name, options) result[pkgInfo.name] = LockFileDep( version: pkgInfo.version, vcsRevision: pkgInfo.vcsRevision, - url: pkg.url, - downloadMethod: pkg.downloadMethod, - dependencies: pkgInfo.requires.map( - pkg => pkg.name).filter(name => name != "nim"), + url: pkgInfo.url, + downloadMethod: pkgInfo.downloadMethod, + dependencies: getDependencies(packages, pkgInfo, options), checksums: Checksums(sha1: pkgInfo.checksum)) proc topologicalSort*(graph: LockFileDeps): diff --git a/src/nimblepkg/vcstools.nim b/src/nimblepkg/vcstools.nim index cbc55f84..bf307087 100644 --- a/src/nimblepkg/vcstools.nim +++ b/src/nimblepkg/vcstools.nim @@ -9,8 +9,8 @@ import common, paths, tools, sha1hashes type VcsType* = enum - ## This type represents a marker for the type of VCS under which is some - ## file system directory. + ## Represents a marker for the type of VCS under which is some file system + ## directory. vcsTypeNone = "none" vcsTypeGit = "git" vcsTypeHg = "hg" @@ -18,6 +18,16 @@ type VcsTypeAndSpecialDirPath = tuple[vcsType: VcsType, path: Path] ## Represents a cache entry for the directory VCS type and VCS special ## directory path used by `getVcsTypeAndSpecialDirPath` procedure. + + BranchType* = enum + ## Determines the branch type which to be queried. + btLocal, btRemoteTracking, btBoth + + RemoteUrlType {.pure.} = enum + ## Represents the type of URL of some VCS remote repository. Fetch URLs are + ## for downloading data from the repository and push URLs are for uploading + ## data to it. + fetch, push const noVcsSpecialDir = "" @@ -248,21 +258,46 @@ proc getRemotesNames*(path: Path): seq[string] = if output.len > 0: result = output.splitLines -proc getRemotePushUrl*(path: Path, remoteName: string): string = - ## Retrieves a push URL for the remote with name `remoteName` set in +proc getRemoteUrl(path: Path, remoteName: string, + urlType: RemoteUrlType): string = + ## Retrieves a fetch or push URL for the remote with name `remoteName` set in ## repository at path `repositoryPath`. ## ## Raises a `NimbleError` if: ## - the external command fails. ## - the directory does not exist. ## - the directory is not under supported VCS type. + + let fetchOrPush = case urlType + of RemoteUrlType.fetch: "" + of RemoteUrlType.push: "--push" result = tryDoVcsCmd(path, - gitCmd = &"remote get-url --push {remoteName}", + gitCmd = &"remote get-url {fetchOrPush} {remoteName}", hgCmd = &"paths {remoteName}") return result.strip +proc getRemoteFetchUrl*(path: Path, remoteName: string): string = + ## Retrieves a fetch URL for the remote with name `remoteName` set in + ## repository at path `repositoryPath`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + getRemoteUrl(path, remoteName, RemoteUrlType.fetch) + +proc getRemotePushUrl*(path: Path, remoteName: string): string = + ## Retrieves a push URL for the remote with name `remoteName` set in + ## repository at path `repositoryPath`. + ## + ## Raises a `NimbleError` if: + ## - the external command fails. + ## - the directory does not exist. + ## - the directory is not under supported VCS type. + getRemoteUrl(path, remoteName, RemoteUrlType.push) + proc getRemotesPushUrls*(path: Path): seq[string] = ## Retrieves a sequence with the push URLs of the set remotes for the ## repository at path `path`. @@ -341,11 +376,6 @@ proc getCurrentBranch*(path: Path): string = return result.strip -type - BranchType* = enum - ## Determines the branch type which to be queried. - btLocal, btRemoteTracking, btBoth - proc getBranchesOnWhichVcsRevisionIsPresent*( path: Path, vcsRevision: Sha1Hash, branchType = btBoth): HashSet[string] = ## Returns a set of the names of all branches which contain revision From 005ca65096d5ab75c3e6d4f47b86fa4a364b718b Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 21 Apr 2021 22:50:43 +0300 Subject: [PATCH 32/73] Update the documentation The documentation is updated with all changed and new behaviors related to the implementation of the new develop mode and the lock files. Related to nim-lang/nimble#127 --- readme.markdown | 216 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 187 insertions(+), 29 deletions(-) diff --git a/readme.markdown b/readme.markdown index 0ce912b9..3a2fd9d0 100644 --- a/readme.markdown +++ b/readme.markdown @@ -21,6 +21,9 @@ The Nimble change log can be found [here](https://github.com/nim-lang/nimble/blo - [nimble refresh](#nimble-refresh) - [nimble install](#nimble-install) - [nimble develop](#nimble-develop) + - [nimble lock](#nimble-lock) + - [nimble sync](#nimble-sync) + - [nimble setup](#nimble-setup) - [nimble uninstall](#nimble-uninstall) - [nimble build](#nimble-build) - [nimble run](#nimble-run) @@ -68,9 +71,11 @@ installed and added to your environment ``PATH``. Same goes for repositories so you may be able to get away without installing Mercurial. **Warning:** Ensure that you have a fairly recent version of Git installed. -If the version is older than 1.9.0, then Nimble may have trouble using it. -See [this issue](https://github.com/nim-lang/nimble/issues/105) for more -information. +Cloning of a specific Git commit described in the lock file uses a method +(described [here](https://stackoverflow.com/a/3489576/853791)) requiring at +least Git version **2.5** from year **2015** and enabled on server side with +the configuration variable `uploadpack.allowReachableSHA1InWant`. Currently the +feature is supported by both **GitHub** and **BitBucket**. ## Installation @@ -151,7 +156,15 @@ Example: Hint: If 'incorrect' contains source files for building 'x', rename it to 'x'. Otherwise, prevent its installation by adding `skipDirs = @["incorrect"]` to the .nimble file. Failure: Validation failed +On `check` command the development mode dependencies are also validated against +the lock file. The following reasons for validation failure are possible: +* The package directory is not under version control. +* The package working copy directory is not in clean state. +* Current VCS revision is no pushed on any remote. +* The working copy needs sync. +* The working copy needs lock. +* The working copy needs merge or re-base. ### nimble install @@ -213,27 +226,184 @@ query parameter. For example: ### nimble develop -The ``develop`` command allows you to link an existing copy of a package into -your installation directory. This is so that when developing a package you -don't need to keep reinstalling it for every single change. - - $ cd ~/projects/jester - $ nimble develop +The develop command is used for putting packages in a development mode. When +executed with a list of packages it clones their repository and if it is +executed in a package directory it adds cloned packages to the special +`nimble.develop` file. This is a special file which is used for holding the +paths to development mode dependencies of the current directory package. It has +the following structure: + +```json +{ + "version": "0.1.0", + "includes": [], + "dependencies": [] +} +``` -Any packages depending on ``jester`` will now use the code in -``~/projects/jester``. +* `version` - JSON schema version +* `includes` - JSON array of paths to included files. +* `dependencies` - JSON array of paths to Nimble packages directories. -If you specify a package name to this command, Nimble will clone it into the -current working directory. +The format for included develop files is the same as the project's develop +file, but their validation works slightly different. - $ nimble develop jester +Validation rules: -The ``jester`` package will be cloned into ``./jester`` and it will be linked -to your installation directory. +* The included develop files must be valid. +* The packages listed in `dependencies` section must be dependencies required +by the package's `.nimble` file and to be in the required by its version range. +Transitive dependencies are not allowed but this may be changed in the future. +* The packages listed in the included develop files are required to be valid +**Nimble** packages, but they are not required to be valid dependencies of the +current project. In the last case, they are simply ignored. +* The develop files of the develop mode dependencies a package are being +followed and processed recursively. Finally, only one common set of develop +mode dependencies is created. +* In the final set of develop mode dependencies, it is not allowed to have more +than one packages with the same name but with different file system paths. Just as with the ``install`` command, a package URL may also be specified instead of a name. +If present the validity of the package's develop file is added to the +requirements for validity of the package which is determined by `nimble check` +command. + +The `develop` command has a list of options: + +* `-p, --path path` - Specifies the path whether the packages should be cloned. +* `-c, --create [path]` - Creates an empty develop file with the name +`nimble.develop` in the current directory or if a path is present to the given +directory with a given name. +* `-a, --add path` - Adds the package at the given path to the `nimble.develop` +file. +* `-r, --remove-path path` - Removes the package at the given path from the +`nimble.develop` file. +* `-n, --remove-name path` - Removed the package with the given name from the +`nimble.develop` file. +* `-i, --include file` - Includes a develop file into the current directory's +one. +* `-e, --exclude file` - Excludes a develop file from the current directory's +one. + +The options for manipulation of the develop files could be given only when +executing `develop` command from some package's directory and they work only on +the project's develop file named `nimble.develop` and not on free develop files +intended only for inclusion. + +Because the develop files are user-specific and they contain local file system +paths they **MUST NOT** be committed. + +**Current limitations:** + +* Currently transitive dependencies in the `dependencies` section of the +develop file are not allowed. In the future, they should be allowed because +this will allow using in develop mode some transitive package dependencies +without having in develop mode the full dependency tree path to them. It was a +design mistake that was not allowed at the beginning. The current workaround is +to add the transitive dependency as a dependency in the project's `.nimble` +file. + +### nimble lock + +The `nimble lock` command will generate or update a package lock file named +`nimble.lock`. This file is used for pinning the exact versions of the +dependencies of the package. The file is intended to be committed and used by +other developers to ensure that exactly the same version of the dependencies is +used by all developers. + +Currently the lock file have the structure as in the following example: + +```json +{ + "version": "0.1.0", + "packages": { + ... + "chronos": { + "version": "3.0.2", + "vcsRevision": "aab1e30a726bb47c5d3f4a75a826981836cde9e2", + "url": "https://github.com/status-im/nim-chronos", + "downloadMethod": "git", + "dependencies": [ + "stew", + "bearssl", + "httputils", + "unittest2" + ], + "checksums": { + "sha1": "a1cdaa77995f2d1381e8f9dc129594f2fa2ee07f" + } + }, + ... + } + } +} +``` + +* `version` - JSON schema version. +* `packages` - JSON object containing JSON objects for all dependencies, +* `chronos` - Nested JSON object keys are the names of the dependencies +packages. +* `version` - The version of the dependency. +* `vcsRevision` - The revision at which the dependency is locked. +* `url` - The URL of the repository of the package. +* `downloadMethod` - `git` or `hg` according to the type of the repository at +`url`. +* `dependencies` - The direct dependencies of the package. Used for writing the +reverse dependencies of the package in the `nimbledata.json` file. Those +packages' names also must be in the lock file. +* `checksums` - A JSON compound object containing different checksums used for +verifying that a downloaded package is exactly the same as the pinned in the +lock file package. Currently, only `sha1` checksums are supported +* `sha1` - The *sha1* checksum of the package files. + +If a lock file `nimble.lock` exists, then on performing all Nimble commands +which require searching for dependencies and downloading them in the case they +are missing (like `build`, `install`, `develop`), it is read and its content is +used to download the same version of the project dependencies by using the URL, +download method and VCS revision written in it. The checksum of the downloaded +package is compared against the one written in the lock file. In the case the +two checksums are not equal then it will be printed error message and the +operation will be aborted. Reverse dependencies are added for installed locked +dependencies just like for any other package being locally installed. + +### nimble sync + +The `nimble sync` command will synchronize develop mode dependencies with the +content of the lock file. If the specified in the lock file revision is not +found locally tries to fetch it from the configured remotes. If it is present +on multiple branches tries to stay on the current one and if cannot prefers +local branches rather than remote-tracking ones. If found on more than one +branch gives the user a choice whether to switch. + +Sync operation will also download non-develop mode dependencies versions +described in the lock file if they are not already present in the Nimble cache. + +If the `-l, --list-only` option is given then the command only lists +development mode dependencies which working copies are out of sync without +actually syncing them and without downloading missing non-develop mode +dependencies. + +**Important implementation details:** + +To be able to determine whether a working copy of development mode dependency +needs to be synced, locked again, or merged with or re-based on some other +branch a special sync file is kept in the VCS directory (.git or .hg) of the +current package. It keeps a record for every development mode dependency for +its current working copy revision during the last `lock`, `sync`, or `develop` +operation. The name of the file is `.nimble.sync`. + +### nimble setup + +The `nimble setup` command creates a `nimble.paths` file containing file system +paths to the dependencies. Also includes the paths file in the `config.nims` +file (by creating it if it does not already exist) to make them available for +the compiler. `nimble.paths` file is user-specific and MUST NOT be committed. + +The command also adds `nimble.develop` and `nimble.paths` files to the +`.gitignore` file. + ### nimble uninstall The ``uninstall`` command will remove an installed package. Attempting to remove @@ -314,19 +484,7 @@ package must be queried separately. The nimble ``path`` command will show the absolute path to the installed packages matching the specified parameters. Since there can be many versions of -the same package installed, the ``path`` command will always show the latest -version. Example: - - $ nimble path argument_parser - /home/user/.nimble/pkgs/argument_parser-0.1.2 - -Under Unix you can use backticks to quickly access the directory of a package, -which can be useful to read the bundled documentation. Example: - - $ pwd - /usr/local/bin - $ cd `nimble path argument_parser` - $ less README.md +the same package installed, the ``path`` command will list all of them. ### nimble init From 66742aed98091db81484276f4e12274795723411 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Thu, 22 Apr 2021 21:19:41 +0300 Subject: [PATCH 33/73] Update the change log Related to nim-lang/nimble#127 --- changelog.markdown | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog.markdown b/changelog.markdown index 3151bcfd..597d0b02 100644 --- a/changelog.markdown +++ b/changelog.markdown @@ -3,6 +3,13 @@ # Nimble changelog +## 0.14.0 + +This is a major release containing two big features: + +- A new dependencies development mode. +- Support for lock files. + ## 0.13.0 This is a bugfix release. It enhances the security in multiple aspects: From 56fd06777dc28f07da70db50102ab7b6e1dea9a2 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Thu, 22 Apr 2021 23:00:21 +0300 Subject: [PATCH 34/73] Fix some regressions while rebasing Related to nim-lang/nimble#127 --- src/nimblepkg/packageinfo.nim | 3 ++- tests/testscommon.nim | 3 ++- tests/tissues.nim | 2 +- tests/tnimblerefresh.nim | 10 +++++----- tests/tnimscript.nim | 3 ++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 167e1d38..c7d4851e 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -268,7 +268,8 @@ proc findNimbleFile*(dir: string; error: bool): string = elif hits == 0: if error: raise nimbleError( - "Specified directory ($1) does not contain a .nimble file." % dir) + "Could not find a file with a .nimble extension inside the specified " & + "directory: $1" % dir) else: displayWarning(&"No .nimble file found for {dir}") diff --git a/tests/testscommon.nim b/tests/testscommon.nim index 2eff40f8..4d0e4acb 100644 --- a/tests/testscommon.nim +++ b/tests/testscommon.nim @@ -20,6 +20,7 @@ const let rootDir = getCurrentDir().parentDir nimblePath* = rootDir / "src" / addFileExt("nimble", ExeExt) + nimbleCompilePath = rootDir / "src" / "nimble.nim" installDir* = rootDir / "tests" / "nimbleDir" buildTests* = rootDir / "buildTests" pkgsDir* = installDir / nimblePackagesDirName @@ -192,4 +193,4 @@ proc writeDevelopFile*(path: string, includes: seq[string], putEnv("NIMBLE_TEST_BINARY_PATH", nimblePath) # Always recompile. -doAssert execCmdEx("nim c " & nimblePath).exitCode == QuitSuccess +doAssert execCmdEx("nim c " & nimbleCompilePath).exitCode == QuitSuccess diff --git a/tests/tissues.nim b/tests/tissues.nim index fbc8a685..083d3e15 100644 --- a/tests/tissues.nim +++ b/tests/tissues.nim @@ -103,7 +103,7 @@ suite "issues": test "error if `bin` is a source file (#597)": cd "issue597": - var (output, exitCode) = execNimble("build") + let (output, exitCode) = execNimble("build") check exitCode != QuitSuccess check output.contains("entry should not be a source file: test.nim") diff --git a/tests/tnimblerefresh.nim b/tests/tnimblerefresh.nim index 187c8601..c03ef074 100644 --- a/tests/tnimblerefresh.nim +++ b/tests/tnimblerefresh.nim @@ -17,10 +17,10 @@ suite "nimble refresh": writeFile(configFile, """ [PackageList] name = "official" - url = "http://google.com" - url = "http://google.com/404" - url = "http://irclogs.nim-lang.org/packages.json" - url = "http://nim-lang.org/nimble/packages.json" + url = "https://google.com" + url = "https://google.com/404" + url = "https://irclogs.nim-lang.org/packages.json" + url = "https://nim-lang.org/nimble/packages.json" url = "https://github.com/nim-lang/packages/raw/master/packages.json" """.unindent) @@ -30,7 +30,7 @@ suite "nimble refresh": check exitCode == QuitSuccess check inLines(lines, "config file at") check inLines(lines, "official package list") - check inLines(lines, "http://google.com") + check inLines(lines, "https://google.com") check inLines(lines, "packages.json file is invalid") check inLines(lines, "404 not found") check inLines(lines, "Package list downloaded.") diff --git a/tests/tnimscript.nim b/tests/tnimscript.nim index 414c750b..884e076d 100644 --- a/tests/tnimscript.nim +++ b/tests/tnimscript.nim @@ -26,7 +26,7 @@ suite "nimscript": check line.endsWith("tests" / "nimscript") check lines[^1].startsWith("After PkgDir:") let packageDir = getPackageDir(pkgsDir, "nimscript-0.1.0") - check lines[^1].endsWith(packageDir) + check lines[^1].strip(trailing = true).endsWith(packageDir) test "before/after on build": cd "nimscript": @@ -98,6 +98,7 @@ suite "nimscript": let (output, exitCode) = execNimble("api") let lines = output.strip.processOutput() check exitCode == QuitSuccess + check inLines(lines, "thisDirCT: " & getCurrentDir()) check inLines(lines, "PKG_DIR: " & getCurrentDir()) check inLines(lines, "thisDir: " & getCurrentDir()) From 86da459d0bbbc80836ed7ebae9a23a2769927ad1 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 23 Apr 2021 21:54:11 +0300 Subject: [PATCH 35/73] Fix the tests on Windows Related to nim-lang/nimble#127 --- readme.markdown | 11 ++++++----- src/nimble.nim | 10 ++++++---- src/nimblepkg/pkgnameversionchecksum | Bin 337192 -> 0 bytes tests/tdevelopfeature.nim | 11 +++++++++-- tests/tlockfile.nim | 4 +++- tests/tsetupcommand.nim | 5 +++-- 6 files changed, 27 insertions(+), 14 deletions(-) delete mode 100755 src/nimblepkg/pkgnameversionchecksum diff --git a/readme.markdown b/readme.markdown index 3a2fd9d0..1a887e84 100644 --- a/readme.markdown +++ b/readme.markdown @@ -70,11 +70,12 @@ installed and added to your environment ``PATH``. Same goes for [Bitbucket](https://bitbucket.org). Nimble packages are typically hosted in Git repositories so you may be able to get away without installing Mercurial. -**Warning:** Ensure that you have a fairly recent version of Git installed. -Cloning of a specific Git commit described in the lock file uses a method -(described [here](https://stackoverflow.com/a/3489576/853791)) requiring at -least Git version **2.5** from year **2015** and enabled on server side with -the configuration variable `uploadpack.allowReachableSHA1InWant`. Currently the +**Warning:** Ensure that you have a fairly recent version of **Git** installed. +Current minimal supported version is **Git** `2.22` from `2019-06-07`. +Cloning of a specific **Git** commit described in the lock file uses a method +described [here](https://stackoverflow.com/a/3489576/853791) and requiring an +option enabled on server side with the configuration variable +`uploadpack.allowReachableSHA1InWant`. Currently the feature is supported by both **GitHub** and **BitBucket**. ## Installation diff --git a/src/nimble.nim b/src/nimble.nim index d2fe5e07..f8dd517b 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -239,7 +239,7 @@ proc reinstallSymlinksForOlderVersion(pkgDir: string, options: Options) = var newPkgInfo = initPackageInfo() if pkgList.findPkg((pkgName, newVRAny()), newPkgInfo): newPkgInfo = newPkgInfo.toFullInfo(options) - for bin in newPkgInfo.binaries: + for bin, _ in newPkgInfo.bin: let symlinkDest = newPkgInfo.getOutputDir(bin) let symlinkFilename = options.getBinDir() / bin.extractFilename discard setupBinSymlink(symlinkDest, symlinkFilename, options) @@ -1115,7 +1115,7 @@ proc updatePathsFile(pkgInfo: PackageInfo, options: Options) = let paths = pkgInfo.getDependenciesPaths(options) var pathsFileContent: string for path in paths: - pathsFileContent &= &"--path:\"{path}\"\n" + pathsFileContent &= &"--path:{path.escape}\n" var action = if fileExists(nimblePathsFileName): "updated" else: "generated" writeFile(nimblePathsFileName, pathsFileContent) displayInfo(&"\"{nimblePathsFileName}\" is {action}.") @@ -1568,8 +1568,10 @@ proc setupVcsIgnoreFile = vcsIgnoreFileName = case currentDir.getVcsType of vcsTypeGit: gitIgnoreFileName of vcsTypeHg: hgIgnoreFileName - of vcsTypeNone: raise nimbleError( - &"The directory {currentDir} is not under version control.") + of vcsTypeNone: "" + + if vcsIgnoreFileName.len == 0: + return var writeFile = false diff --git a/src/nimblepkg/pkgnameversionchecksum b/src/nimblepkg/pkgnameversionchecksum deleted file mode 100755 index e73513e1ba4e590ec5f2e4757c085867a8ead8ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337192 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%0AXC*!p3wbgIbjIJLbVe(kB}}Yb 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 diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index 5afd6e0b..1741a4b2 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -787,9 +787,16 @@ suite "develop feature": var lines = output.processOutput check lines.inLinesOrdered(failedToLoadFileMsg( getCurrentDir() / developFileName)) + + let + pkg3Path = (".." / "pkg3").Path + pkg32Path = (".." / "pkg3.2").Path + freeDevFile1Path = (".." / freeDevFile1Name).Path + freeDevFile2Path = (".." / freeDevFile2Name).Path + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", - [("../pkg3".Path, (&"../{freeDevFile1Name}").Path), - ("../pkg3.2".Path, (&"../{freeDevFile2Name}").Path)].toHashSet)) + [(pkg3Path, freeDevFile1Path), + (pkg32Path, freeDevFile2Path)].toHashSet)) test "create an empty develop file with default name in the current dir": cd dependentPkgPath: diff --git a/tests/tlockfile.nim b/tests/tlockfile.nim index 34d56f0f..7fbc211a 100644 --- a/tests/tlockfile.nim +++ b/tests/tlockfile.nim @@ -19,6 +19,7 @@ from nimblepkg/lockfile import lockFileName, LockFileJsonKeys from nimblepkg/sha1hashes import initSha1Hash from nimblepkg/developfile import ValidationError, ValidationErrorKind, developFileName, getValidationErrorMessage +from nimblepkg/vcstools import VcsType, getVcsDefaultBranchName suite "lock file": type @@ -111,7 +112,8 @@ requires "nim >= 1.5.1" tryDoCmdEx("git commit -m " & msg.quoteShell) proc push(remote: string) = - tryDoCmdEx("git push " & remote) + tryDoCmdEx( + &"git push --set-upstream {remote} {vcsTypeGit.getVcsDefaultBranchName}") proc pull(remote: string) = tryDoCmdEx("git pull " & remote) diff --git a/tests/tsetupcommand.nim b/tests/tsetupcommand.nim index 7b46a926..68faf466 100644 --- a/tests/tsetupcommand.nim +++ b/tests/tsetupcommand.nim @@ -45,8 +45,9 @@ suite "setup command": check fileExists(nimbleConfigFileName) # Check that develop mode dependency path is written in the # "nimble.paths" file. - let developDepDir = (getCurrentDir() / "../dependency").normalizedPath - check nimblePathsFileName.readFile.contains(developDepDir) + let developDepDir = + (getCurrentDir() / ".." / "dependency").normalizedPath + check nimblePathsFileName.readFile.contains(developDepDir.escape) # Check that Nim can use "nimble.paths" file to find dependencies and # build the project. let (_, nimExitCode) = execCmdEx("nim c -r dependent") From c471d31c95f8c80ff6025ad1abf046b1bcddb63c Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sat, 24 Apr 2021 23:27:59 +0300 Subject: [PATCH 36/73] Remove `isValidSha1Hash` procedure `isValidSha1Hash` procedure is removed from Nimble. The analogous procedure from `std/sha1` is used instead. Related to nim-lang/nimble#127 --- src/nimblepkg/sha1hashes.nim | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/nimblepkg/sha1hashes.nim b/src/nimblepkg/sha1hashes.nim index c776b3e3..5c12cdb6 100644 --- a/src/nimblepkg/sha1hashes.nim +++ b/src/nimblepkg/sha1hashes.nim @@ -26,18 +26,6 @@ proc invalidSha1Hash(value: string): ref InvalidSha1HashError = result = newNimbleError[InvalidSha1HashError]( &"The string '{value}' does not represent a valid sha1 hash value.") -proc isValidSha1Hash(value: string): bool = - ## Checks whether given string is a valid sha1 hash value. Only lower case - ## hexadecimal digits are accepted. - if value.len != 40: - # A valid sha1 hash should be 40 characters long string. - return false - for c in value: - if c notin {'0' .. '9', 'a'..'f'}: - # It also should contain only lower case hexadecimal digits. - return false - return true - proc initSha1Hash*(value: string): Sha1Hash = ## Creates a new `Sha1Hash` object from a string by making all latin letters ## lower case and validating the transformed value. In the case the supplied @@ -63,14 +51,6 @@ proc initFromJson*(dst: var Sha1Hash, jsonNode: JsonNode, when isMainModule: import unittest - test "validate sha1": - check not isValidSha1Hash("") - check not isValidSha1Hash("9") - check not isValidSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358g7") - check not isValidSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b") - check not isValidSha1Hash("99345CE680CD3E48ACDB9AB4212E4BD9BF9358B7") - check isValidSha1Hash("99345ce680cd3e48acdb9ab4212e4bd9bf9358b7") - test "init sha1": check initSha1Hash("") == notSetSha1Hash expect InvalidSha1HashError: discard initSha1Hash("9") From 40d968212a0113ed50204cddd0b8ef9daaf8ca45 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sat, 24 Apr 2021 23:30:03 +0300 Subject: [PATCH 37/73] Remove old Nim versions from Nimble CI Nim version containing changes from commit `8ede3bc0494aa27d98384fa5449c5c3026ac8f92` is required for successful build of latest Nimble. Related to nim-lang/nimble#127 --- .github/workflows/test.yml | 4 +--- tests/.gitignore | 20 -------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49b1e50a..d43d9f05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,9 +13,7 @@ jobs: - macos-latest - ubuntu-latest nimversion: - - 1.2.8 - - 1.4.2 - - nightly:https://github.com/nim-lang/nightlies/releases/tag/2021-02-06-devel-39230422d02bd41b02378211e8613f702e5b945c + - sourcetar:https://github.com/bobeff/Nim/tarball/8ede3bc0494aa27d98384fa5449c5c3026ac8f92 name: ${{ matrix.os }} - ${{ matrix.nimversion }} runs-on: ${{ matrix.os }} env: diff --git a/tests/.gitignore b/tests/.gitignore index f195953c..7696e2ad 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,23 +1,3 @@ -tcheckcommand -tdevelopfeature -tester -testscommon -tissues -tlocaldeps -tlockfile -tmisctests -tmoduletests -tmultipkgs -tnimbledump -tnimblerefresh -tnimscript -tpathcommand -treversedeps -truncommand -tsetupcommand -ttestcommand -ttwobinaryversions -tuninstall /nimble-test /buildDir /binaryPackage/v1/binaryPackage From 62d1db946b4deb7c31d72bed07f16c4c732b0717 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 14 May 2021 09:16:33 +0300 Subject: [PATCH 38/73] Implement downloading of package tarballs When working with GitHub repositories instead of cloning them now by default will be downloaded tarball using GitHub REST API. It is provided a fall-back option `-t, --no-tarballs` for disabling this behavior. Additionally, a bug in calculating package checksums is fixed. The list of package files now is being sorted before calculation of the checksum to ensure common behavior with different versions of the VCS tools and independence from the download method. Related to nim-lang/nimble#127 --- src/nimble.nim | 18 ++- src/nimblepkg/checksums.nim | 5 +- src/nimblepkg/download.nim | 218 +++++++++++++++++++++++++++++++----- src/nimblepkg/options.nim | 4 + 4 files changed, 208 insertions(+), 37 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index f8dd517b..4ca2e402 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -293,7 +293,8 @@ proc processAllDependencies(pkgInfo: PackageInfo, options: Options): pkgInfo.processFreeDependencies(options) proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, - url: string, first: bool, fromLockFile: bool): + url: string, first: bool, fromLockFile: bool, + vcsRevision = notSetSha1Hash): PackageDependenciesInfo = ## Returns where package has been installed to, together with paths ## to the packages this package depends on. @@ -317,6 +318,11 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Set the flag that the package is not in develop mode before saving it to the # reverse dependencies. pkgInfo.isLink = false + if vcsRevision != notSetSha1Hash: + ## In the case we downloaded the package as tarball we have to set the VCS + ## revision returned by download procedure because it cannot be queried from + ## the package directory. + pkgInfo.vcsRevision = vcsRevision let realDir = pkgInfo.getRealDir() let binDir = options.getBinDir() @@ -457,7 +463,7 @@ proc getLockedDep(pkgInfo: PackageInfo, name: string, dep: LockFileDep, let version = dep.version.parseVersionRange let subdir = metadata.getOrDefault("subdir") - let (downloadDir, _) = downloadPkg( + let (downloadDir, _, vcsRevision) = downloadPkg( url, version, dep.downloadMethod, subdir, options, downloadPath = "", dep.vcsRevision) @@ -466,8 +472,8 @@ proc getLockedDep(pkgInfo: PackageInfo, name: string, dep: LockFileDep, raise checksumError(name, dep.version, dep.vcsRevision, downloadedPackageChecksum, dep.checksums.sha1) - let (_, newlyInstalledPackageInfo) = installFromDir( - downloadDir, version, options, url, first = false, fromLockFile = true) + var (_, newlyInstalledPackageInfo) = installFromDir(downloadDir, version, + options, url, first = false, fromLockFile = true, vcsRevision) for depDepName in dep.dependencies: let depDep = pkgInfo.lockedDeps[depDepName] @@ -546,12 +552,12 @@ proc install(packages: seq[PkgTuple], options: Options, for pv in packages: let (meth, url, metadata) = getDownloadInfo(pv, options, doPrompt) let subdir = metadata.getOrDefault("subdir") - let (downloadDir, downloadVersion) = + let (downloadDir, downloadVersion, vcsRevision) = downloadPkg(url, pv.ver, meth, subdir, options, downloadPath = "", vcsRevision = notSetSha1Hash) try: result = installFromDir(downloadDir, pv.ver, options, url, - first, fromLockFile) + first, fromLockFile, vcsRevision) except BuildFailed as error: # The package failed to build. # Check if we tried building a tagged version of the package. diff --git a/src/nimblepkg/checksums.nim b/src/nimblepkg/checksums.nim index 97975269..6f8060d4 100644 --- a/src/nimblepkg/checksums.nim +++ b/src/nimblepkg/checksums.nim @@ -1,7 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, std/sha1, strformat +import os, std/sha1, strformat, algorithm import common, version, sha1hashes, vcstools, paths type @@ -42,7 +42,8 @@ proc calculateDirSha1Checksum*(dir: string): Sha1Hash = ## - the external command for getting the package file list fails. ## - the directory does not exist. - let packageFiles = getPackageFileList(dir.Path) + var packageFiles = getPackageFileList(dir.Path) + packageFiles.sort var checksum = newSha1State() for file in packageFiles: updateSha1Checksum(checksum, file) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 7bcef2dc..89981077 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -1,13 +1,14 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import parseutils, os, osproc, strutils, tables, pegs, uri, strformat +import parseutils, os, osproc, strutils, tables, pegs, uri, strformat, + httpclient, json from algorithm import SortOrder, sorted from sequtils import toSeq, filterIt, map import packageinfotypes, packageparser, version, tools, common, options, cli, - sha1hashes + sha1hashes, vcstools proc doCheckout(meth: DownloadMethod, downloadDir, branch: string) = case meth @@ -156,10 +157,138 @@ proc cloneSpecificRevision(downloadMethod: DownloadMethod, of DownloadMethod.hg: doCmd(fmt"hg clone {url} -r {vcsRevision}") +proc hasTar: bool = + ## Checks whether a `tar` external tool is available. + var hasTar {.global.} = false + once: hasTar = findExe("tar").len > 0 + return hasTar + +proc isGitHubRepo(url: string): bool = + ## Determines whether the `url` points to a GitHub repository. + url.contains("github.com") + +proc downloadTarball(url: string, options: Options): bool = + ## Determines whether to download the repository as a tarball. + hasTar() and + not options.forceFullClone and + not options.noTarballs and + url.isGitHubRepo + +proc removeTrailingGitString(url: string): string = + ## Removes ".git" from an URL. + ## + ## For example: + ## "https://github.com/nim-lang/nimble.git" -> "https://github.com/nim-lang/nimble" + if url.len > 4 and url.endsWith(".git"): url[0..^5] else: url + +proc getTarballDownloadLink(url, version: string): string = + ## Returns the package tarball download link for given repository URL and + ## version. + removeTrailingGitString(url) & "/tarball/" & version + +proc seemsLikeRevision(version: string): bool = + ## Checks whether the given `version` string seems like part of sha1 hash + ## value. + assert version.len > 0, "version must not be an empty string" + for c in version: + if c notin HexDigits: + return false + return true + +proc extractOwnerAndRepo(url: string): string = + ## Extracts owner and repository string from an URL to GitHub repository. + ## + ## For example: + ## "https://github.com/nim-lang/nimble.git" -> "nim-lang/nimble" + assert url.isGitHubRepo, "Only GitHub URLs are supported." + let url = removeTrailingGitString(url) + var slashPosition = url.rfind('/') + slashPosition = url.rfind('/', last = slashPosition - 1) + return url[slashPosition + 1 .. ^1] + +proc getGitHubApiUrl(url, commit: string): string = + ## By given URL to GitHub repository and part of a commit hash constructs + ## an URL for the GitHub REST API query for the full commit hash. + &"https://api.github.com/repos/{extractOwnerAndRepo(url)}/commits/{commit}" + +proc getUrlContent(url: string): string = + ## Makes a GET request to `url`. + var client {.global.}: HttpClient + once: client = newHttpClient() + return client.getContent(url) + +{.warning[ProveInit]: off.} +proc getFullRevisionFromGitHubApi(url, version: string): Sha1Hash = + ## By given a commit short hash and an URL to a GitHub repository retrieves + ## the full hash of the commit by using GitHub REST API. + try: + let gitHubApiUrl = getGitHubApiUrl(url, version) + display("Get", gitHubApiUrl); + let content = getUrlContent(gitHubApiUrl) + let json = parseJson(content) + if json.hasKey("sha"): + return json["sha"].str.initSha1Hash + else: + raise nimbleError(json["message"].str) + except CatchableError as error: + raise nimbleError(&"Cannot get revision for version \"{version}\" " & + &"of package at \"{url}\".", details = error) +{.warning[ProveInit]: on.} + +proc parseRevision(lsRemoteOutput: string): Sha1Hash = + ## Parses the output from `git ls-remote` call to extract the returned sha1 + ## hash value. Even when successful the first line of the command's output + ## can be a redirection warning. + let lines = lsRemoteOutput.splitLines + for line in lines: + if line.len >= 40: + try: + return initSha1Hash(line[0..39]) + except InvalidSha1HashError: + discard + return notSetSha1Hash + +proc getRevision(url, version: string): Sha1Hash = + ## Returns the commit hash corresponding to the given `version` of the package + ## in repository at `url`. + let output = tryDoCmdEx(&"git ls-remote {url} {version}") + result = parseRevision(output) + if result == notSetSha1Hash: + if version.seemsLikeRevision: + result = getFullRevisionFromGitHubApi(url, version) + else: + raise nimbleError(&"Cannot get revision for version \"{version}\" " & + &"of package at \"{url}\".") + +proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): + Sha1Hash = + ## Downloads package tarball from GitHub. Returns the commit hash of the + ## downloaded package in the case `queryRevision` is `true`. + + let downloadLink = getTarballDownloadLink(url, version) + display("Downloading", downloadLink) + let data = getUrlContent(downloadLink) + display("Completed", "downloading " & downloadLink) + + let filePath = downloadDir / "tarball.tar.gz" + display("Saving", filePath) + downloadDir.createDir + writeFile(filePath, data) + display("Completed", "saving " & filePath) + + display("Unpacking", filePath) + discard tryDoCmdEx( + &"tar -C {downloadDir} -xf {filePath} --strip-components 1") + display("Completed", "unpacking " & filePath) + + filePath.removeFile + return if queryRevision: getRevision(url, version) else: notSetSha1Hash + {.warning[ProveInit]: off.} proc doDownload(url: string, downloadDir: string, verRange: VersionRange, downMethod: DownloadMethod, options: Options, - vcsRevision: Sha1Hash): Version = + vcsRevision: Sha1Hash): + tuple[version: Version, vcsRevision: Sha1Hash] = ## Downloads the repository specified by ``url`` using the specified download ## method. ## @@ -175,45 +304,67 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, # https://github.com/nim-lang/nimble/issues/22 meth if $latest.ver != "": - result = latest.ver + result.version = latest.ver + + result.vcsRevision = notSetSha1Hash removeDir(downloadDir) if vcsRevision != notSetSha1Hash: - cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) + if downloadTarball(url, options): + discard doDownloadTarball(url, downloadDir, $vcsRevision, false) + else: + cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) + result.vcsRevision = vcsRevision elif verRange.kind == verSpecial: # We want a specific commit/branch/tag here. if verRange.spe == getHeadName(downMethod): # Grab HEAD. - doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) + if downloadTarball(url, options): + result.vcsRevision = doDownloadTarball(url, downloadDir, "HEAD", true) + else: + doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) else: - # Grab the full repo. - doClone(downMethod, url, downloadDir, onlyTip = false) - # Then perform a checkout operation to get the specified branch/commit. - # `spe` starts with '#', trim it. - doAssert(($verRange.spe)[0] == '#') - doCheckout(downMethod, downloadDir, substr($verRange.spe, 1)) - result = verRange.spe + assert ($verRange.spe)[0] == '#', + "The special version must start with '#'." + let specialVersion = substr($verRange.spe, 1) + if downloadTarball(url, options): + result.vcsRevision = doDownloadTarball( + url, downloadDir, specialVersion, true) + else: + # Grab the full repo. + doClone(downMethod, url, downloadDir, onlyTip = false) + # Then perform a checkout operation to get the specified branch/commit. + # `spe` starts with '#', trim it. + doCheckout(downMethod, downloadDir, specialVersion) + result.version = verRange.spe else: case downMethod of DownloadMethod.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. - result = getHeadName(downMethod) + result.version = getHeadName(downMethod) let versions = getTagsListRemote(url, downMethod).getVersionList() if versions.len > 0: getLatestByTag: - display("Cloning", "latest tagged version: " & latest.tag, - priority = MediumPriority) - doClone(downMethod, url, downloadDir, latest.tag, - onlyTip = not options.forceFullClone) + if downloadTarball(url, options): + result.vcsRevision = doDownloadTarball( + url, downloadDir, latest.tag, true) + else: + display("Cloning", "latest tagged version: " & latest.tag, + priority = MediumPriority) + doClone(downMethod, url, downloadDir, latest.tag, + onlyTip = not options.forceFullClone) else: - # If no commits have been tagged on the repo we just clone HEAD. display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, - priority = HighPriority) - doClone(downMethod, url, downloadDir) # Grab HEAD. + priority = HighPriority) + if downloadTarball(url, options): + result.vcsRevision = doDownloadTarball(url, downloadDir, "HEAD", true) + else: + # If no commits have been tagged on the repo we just clone HEAD. + doClone(downMethod, url, downloadDir) # Grab HEAD. of DownloadMethod.hg: doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) - result = getHeadName(downMethod) + result.version = getHeadName(downMethod) let versions = getTagsList(downloadDir, downMethod).getVersionList() if versions.len > 0: @@ -224,6 +375,11 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, else: display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, priority = HighPriority) + + if result.vcsRevision == notSetSha1Hash: + # In the case the package in not downloaded as tarball we must query its + # VCS revision from its download directory. + result.vcsRevision = getVcsRevision(downloadDir) {.warning[ProveInit]: on.} proc downloadPkg*(url: string, verRange: VersionRange, @@ -231,7 +387,8 @@ proc downloadPkg*(url: string, verRange: VersionRange, subdir: string, options: Options, downloadPath: string, - vcsRevision: Sha1Hash): (string, Version) = + vcsRevision: Sha1Hash): + tuple[dir: string, version: Version, vcsRevision: Sha1Hash] = ## Downloads the repository as specified by ``url`` and ``verRange`` using ## the download method specified. ## @@ -262,17 +419,20 @@ proc downloadPkg*(url: string, verRange: VersionRange, if modUrl.contains("github.com") and modUrl.endswith("/"): modUrl = modUrl[0 .. ^2] + let downloadMethod = if downloadTarball(modUrl, options): + "http" else: $downMethod + if subdir.len > 0: display("Downloading", "$1 using $2 (subdir is '$3')" % - [modUrl, $downMethod, subdir], + [modUrl, downloadMethod, subdir], priority = HighPriority) else: - display("Downloading", "$1 using $2" % [modUrl, $downMethod], + display("Downloading", "$1 using $2" % [modUrl, downloadMethod], priority = HighPriority) - result = ( - downloadDir / subdir, - doDownload(modUrl, downloadDir, verRange, downMethod, options, vcsRevision) - ) + + result.dir = downloadDir / subdir + (result.version, result.vcsRevision) = doDownload( + modUrl, downloadDir, verRange, downMethod, options, vcsRevision) if verRange.kind != verSpecial: ## Makes sure that the downloaded package's version satisfies the requested diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 8d0bb9d9..97d75a33 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -41,6 +41,7 @@ type localdeps*: bool # True if project local deps mode developLocaldeps*: bool # True if local deps + nimble develop pkg1 ... disableSslCertCheck*: bool + noTarballs*: bool # Disable downloading of packages as tarballs from GitHub. ActionType* = enum actionNil, actionRefresh, actionInit, actionDump, actionPublish, @@ -172,6 +173,8 @@ Nimble Options: -y, --accept Accept all interactive prompts. -n, --reject Reject all interactive prompts. -l, --localdeps Run in project local dependency mode + -t, --no-tarballs Disable downloading of packages as tarballs + when working with GitHub repositories. --ver Query remote server for package version information when searching or listing packages. --nimbleDir:dirname Set the Nimble directory. @@ -457,6 +460,7 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = of "nim": result.nim = val of "localdeps", "l": result.localdeps = true of "nosslcheck": result.disableSslCertCheck = true + of "no-tarballs", "t": result.noTarballs = true else: isGlobalFlag = false var wasFlagHandled = true From c4464e8fb9e91f6584824896b759e473ee8627ea Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 14 May 2021 11:31:32 +0300 Subject: [PATCH 39/73] Fix a bug in a package checksum calculation Package checksum calculation wasn't correct because a relative file path from the download directory was given to the `updateSha1Checksum` procedure and it being not able to find the files was exiting early instead of opening and reading them. Now the procedure is changed to get both relative and full path to the files. The first is used for accumulating the file name into the checksum and the second for the actual opening and reading the file. Related to nim-lang/nimble#127 --- src/nimblepkg/checksums.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nimblepkg/checksums.nim b/src/nimblepkg/checksums.nim index 6f8060d4..33e07fb9 100644 --- a/src/nimblepkg/checksums.nim +++ b/src/nimblepkg/checksums.nim @@ -17,15 +17,15 @@ Downloaded package checksum does not correspond to that in the lock file: Expected checksum: {expectedChecksum} """) -proc updateSha1Checksum(checksum: var Sha1State, fileName: string) = +proc updateSha1Checksum(checksum: var Sha1State, fileName, filePath: string) = checksum.update(fileName) - if not fileName.fileExists: + if not filePath.fileExists: # In some cases a file name returned by `git ls-files` or `hg manifest` # could be an empty directory name and if so trying to open it will result # in a crash. This happens for example in the case of a git sub module # directory from which no files are being installed. return - let file = fileName.open(fmRead) + let file = filePath.open(fmRead) defer: close(file) const bufferSize = 8192 var buffer = newString(bufferSize) @@ -46,5 +46,5 @@ proc calculateDirSha1Checksum*(dir: string): Sha1Hash = packageFiles.sort var checksum = newSha1State() for file in packageFiles: - updateSha1Checksum(checksum, file) + updateSha1Checksum(checksum, file, dir / file) result = initSha1Hash($SecureHash(checksum.finalize())) From e18e0916fa99521547c0fc3810153e2bf0400de1 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 14 May 2021 12:37:34 +0300 Subject: [PATCH 40/73] Fix bug in tarball downloading When latest tag is empty set "HEAD" to be downloaded. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 89981077..c4b7694d 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -347,8 +347,10 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, if versions.len > 0: getLatestByTag: if downloadTarball(url, options): + let versionToDownload = + if latest.tag.len > 0: latest.tag else: "HEAD" result.vcsRevision = doDownloadTarball( - url, downloadDir, latest.tag, true) + url, downloadDir, versionToDownload, true) else: display("Cloning", "latest tagged version: " & latest.tag, priority = MediumPriority) From d5a9fa6c9a442796098a1fb862b5956a721a3a66 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 14 May 2021 18:24:58 +0300 Subject: [PATCH 41/73] Implement tarball package downloads on Windows Tarball package downloads are implemented on Windows by using `tar` from Git for Windows installation. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 38 +++++++++++++++++++++++++++++++++++--- src/nimblepkg/tools.nim | 8 +++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index c4b7694d..f6286d43 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -157,10 +157,25 @@ proc cloneSpecificRevision(downloadMethod: DownloadMethod, of DownloadMethod.hg: doCmd(fmt"hg clone {url} -r {vcsRevision}") +proc getTarExePath: string = + ## Returns path to `tar` executable. + var tarExePath {.global.}: string + once: + tarExePath = + when defined(Windows): + findExe("git").splitPath.head / "../usr/bin/tar.exe" + else: + findExe("tar") + tarExePath = tarExePath.quoteShell + return tarExePath + proc hasTar: bool = ## Checks whether a `tar` external tool is available. var hasTar {.global.} = false - once: hasTar = findExe("tar").len > 0 + once: + # Try to execute `tar` to ensure that it is available. + let (_, exitCode) = execCmdEx(getTarExePath() & " --version") + hasTar = exitCode == QuitSuccess return hasTar proc isGitHubRepo(url: string): bool = @@ -260,6 +275,16 @@ proc getRevision(url, version: string): Sha1Hash = raise nimbleError(&"Cannot get revision for version \"{version}\" " & &"of package at \"{url}\".") +proc getTarCmdLine(downloadDir, filePath: string): string = + ## Returns an OS specific command line for extracting the downloaded tarball. + when defined(Windows): + let downloadDir = downloadDir.replace('\\', '/') + let filePath = filePath.replace('\\', '/') + &"{getTarExePath()} -C {downloadDir} -xf {filePath} " & + "--strip-components 1 --force-local" + else: + &"tar -C {downloadDir} -xf {filePath} --strip-components 1" + proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): Sha1Hash = ## Downloads package tarball from GitHub. Returns the commit hash of the @@ -277,8 +302,15 @@ proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): display("Completed", "saving " & filePath) display("Unpacking", filePath) - discard tryDoCmdEx( - &"tar -C {downloadDir} -xf {filePath} --strip-components 1") + let cmd = getTarCmdLine(downloadDir, filePath) + let (output, exitCode) = doCmdEx(cmd) + if exitCode != QuitSuccess and not output.contains("Cannot create symlink to"): + # If the command fails for reason different then unable establishing a + # sym-link raise an exception. This reason for failure is common on Windows + # and the `tar` tool does not provide suitable option for avoiding it on + # unpack time. If this error occurs the files were previously extracted + # successfully and it should not be treated as error. + raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) display("Completed", "unpacking " & filePath) filePath.removeFile diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index f0dd13f5..7e65175b 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -50,12 +50,14 @@ proc doCmdEx*(cmd: string): ProcessOutput = raise nimbleError("'" & bin & "' not in PATH.") return execCmdEx(cmd) +proc tryDoCmdExErrorMessage*(cmd, output: string, exitCode: int): string = + &"Execution of '{cmd}' failed with an exit code {exitCode}.\n" & + &"Details: {output}" + proc tryDoCmdEx*(cmd: string): string {.discardable.} = let (output, exitCode) = doCmdEx(cmd) if exitCode != QuitSuccess: - raise nimbleError( - &"Execution of '{cmd}' failed with an exit code {exitCode}.\n" & - &"Details: {output}") + raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) return output proc getNimBin*: string = From 453e1156e55c80b2ab92eb1f494db15133357a48 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 1 Jun 2021 15:20:36 +0300 Subject: [PATCH 42/73] Improve the documentation Fix some grammatical and spelling errors pointed out by @narimiran. Related to nim-lang/nimble#127 --- readme.markdown | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/readme.markdown b/readme.markdown index 1a887e84..03b707f0 100644 --- a/readme.markdown +++ b/readme.markdown @@ -162,7 +162,7 @@ the lock file. The following reasons for validation failure are possible: * The package directory is not under version control. * The package working copy directory is not in clean state. -* Current VCS revision is no pushed on any remote. +* Current VCS revision is not pushed on any remote. * The working copy needs sync. * The working copy needs lock. * The working copy needs merge or re-base. @@ -228,7 +228,7 @@ query parameter. For example: ### nimble develop The develop command is used for putting packages in a development mode. When -executed with a list of packages it clones their repository and if it is +executed with a list of packages it clones their repository. If it is executed in a package directory it adds cloned packages to the special `nimble.develop` file. This is a special file which is used for holding the paths to development mode dependencies of the current directory package. It has @@ -254,20 +254,20 @@ Validation rules: * The included develop files must be valid. * The packages listed in `dependencies` section must be dependencies required by the package's `.nimble` file and to be in the required by its version range. -Transitive dependencies are not allowed but this may be changed in the future. +Transitive dependencies are not allowed, but this may be changed in the future. * The packages listed in the included develop files are required to be valid **Nimble** packages, but they are not required to be valid dependencies of the -current project. In the last case, they are simply ignored. -* The develop files of the develop mode dependencies a package are being +current project. In the latter case, they are simply ignored. +* The develop files of the develop mode dependencies of a package are being followed and processed recursively. Finally, only one common set of develop mode dependencies is created. * In the final set of develop mode dependencies, it is not allowed to have more -than one packages with the same name but with different file system paths. +than one package with the same name but with different file system paths. Just as with the ``install`` command, a package URL may also be specified instead of a name. -If present the validity of the package's develop file is added to the +If present, the validity of the package's develop file is added to the requirements for validity of the package which is determined by `nimble check` command. @@ -275,7 +275,7 @@ The `develop` command has a list of options: * `-p, --path path` - Specifies the path whether the packages should be cloned. * `-c, --create [path]` - Creates an empty develop file with the name -`nimble.develop` in the current directory or if a path is present to the given +`nimble.develop` in the current directory or, if a path is present, to the given directory with a given name. * `-a, --add path` - Adds the package at the given path to the `nimble.develop` file. @@ -338,7 +338,6 @@ Currently the lock file have the structure as in the following example: }, ... } - } } ``` @@ -372,17 +371,17 @@ dependencies just like for any other package being locally installed. ### nimble sync The `nimble sync` command will synchronize develop mode dependencies with the -content of the lock file. If the specified in the lock file revision is not -found locally tries to fetch it from the configured remotes. If it is present -on multiple branches tries to stay on the current one and if cannot prefers +content of the lock file. If the revision specified in the lock file is not +found locally, it tries to fetch it from the configured remotes. If it is present +on multiple branches, it tries to stay on the current one, and if can't, it prefers local branches rather than remote-tracking ones. If found on more than one -branch gives the user a choice whether to switch. +branch, it gives the user a choice whether to switch. Sync operation will also download non-develop mode dependencies versions described in the lock file if they are not already present in the Nimble cache. If the `-l, --list-only` option is given then the command only lists -development mode dependencies which working copies are out of sync without +development mode dependencies whose working copies are out of sync, without actually syncing them and without downloading missing non-develop mode dependencies. @@ -398,7 +397,7 @@ operation. The name of the file is `.nimble.sync`. ### nimble setup The `nimble setup` command creates a `nimble.paths` file containing file system -paths to the dependencies. Also includes the paths file in the `config.nims` +paths to the dependencies. It also includes the paths file in the `config.nims` file (by creating it if it does not already exist) to make them available for the compiler. `nimble.paths` file is user-specific and MUST NOT be committed. From cb194df62a69705a2a488d15667e013f7e529114 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 1 Jun 2021 21:30:49 +0300 Subject: [PATCH 43/73] Implement parallel package downloads Parallel downloads of locked packages are implemented. The default maximum simultaneous downloads are 20. `--max-parallel-downloads` option is added for setting a user-specified limit. The value 0 is used for no limit. Related to nim-lang/nimble#127 --- src/nimble.nim | 170 +++-- src/nimblepkg/asynctools/asyncpipe.nim | 532 +++++++++++++++ src/nimblepkg/asynctools/asyncproc.nim | 908 +++++++++++++++++++++++++ src/nimblepkg/download.nim | 179 ++--- src/nimblepkg/options.nim | 9 + src/nimblepkg/sha1hashes.nim | 2 + src/nimblepkg/tools.nim | 17 +- tests/tlockfile.nim | 4 +- 8 files changed, 1681 insertions(+), 140 deletions(-) create mode 100644 src/nimblepkg/asynctools/asyncpipe.nim create mode 100644 src/nimblepkg/asynctools/asyncproc.nim diff --git a/src/nimble.nim b/src/nimble.nim index 4ca2e402..adc9691b 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -4,7 +4,7 @@ import system except TResult import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc, - strformat + strformat, asyncdispatch import std/options as std_opt @@ -444,49 +444,99 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, cd pkgInfo.myPath.splitFile.dir: discard execHook(options, actionInstall, false) -proc getLockedDep(pkgInfo: PackageInfo, name: string, dep: LockFileDep, - options: Options): PackageInfo = - ## Returns the package info for dependency package `dep` with name `name` from - ## the lock file of the package `pkgInfo` by searching for it in the local - ## Nimble cache and downloading it from its repository if the version with the - ## required checksum is not found locally. - - let packagesDir = options.getPkgsDir() - let depDirName = packagesDir / &"{name}-{dep.version}-{dep.checksums.sha1}" - - if not fileExists(depDirName / packageMetaDataFileName): - if depDirName.dirExists: +proc getDependencyDir(name: string, dep: LockFileDep, options: Options): + string = + ## Returns the installation directory for a dependency from the lock file. + options.getPkgsDir() / &"{name}-{dep.version}-{dep.checksums.sha1}" + +proc isInstalled(name: string, dep: LockFileDep, options: Options): bool = + ## Checks whether a dependency from the lock file is already installed. + fileExists(getDependencyDir(name, dep, options) / packageMetaDataFileName) + +proc getDependency(name: string, dep: LockFileDep, options: Options): + PackageInfo = + ## Returns a `PackageInfo` for an already installed dependency from the + ## lock file. + let depDirName = getDependencyDir(name, dep, options) + let nimbleFilePath = findNimbleFile(depDirName, false) + getInstalledPackageMin(depDirName, nimbleFilePath).toFullInfo(options) + +type + DownloadInfo = ref object + ## Information for a downloaded dependency needed for installation. + dependency: LockFileDep + url: string + version: VersionRange + downloadDir: string + vcsRevision: Sha1HashRef + + DownloadQueue = ref seq[tuple[name: string, dep: LockFileDep]] + ## A queue of dependencies from the lock file which to be downloaded. + + DownloadResults = ref seq[DownloadInfo] + ## A list of `DownloadInfo` objects used for installing the downloaded + ## dependencies. + +proc downloadDependency(name: string, dep: LockFileDep, options: Options): + Future[DownloadInfo] {.async.} = + ## Asynchronously downloads a dependency from the lock file. + + let depDirName = getDependencyDir(name, dep, options) + + if depDirName.dirExists: promptRemoveEntirePackageDir(depDirName, options) removeDir(depDirName) - let (url, metadata) = getUrlData(dep.url) - let version = dep.version.parseVersionRange - let subdir = metadata.getOrDefault("subdir") - - let (downloadDir, _, vcsRevision) = downloadPkg( - url, version, dep.downloadMethod, subdir, options, - downloadPath = "", dep.vcsRevision) - - let downloadedPackageChecksum = calculateDirSha1Checksum(downloadDir) - if downloadedPackageChecksum != dep.checksums.sha1: - raise checksumError(name, dep.version, dep.vcsRevision, - downloadedPackageChecksum, dep.checksums.sha1) - - var (_, newlyInstalledPackageInfo) = installFromDir(downloadDir, version, - options, url, first = false, fromLockFile = true, vcsRevision) - - for depDepName in dep.dependencies: - let depDep = pkgInfo.lockedDeps[depDepName] - let revDep = (name: depDepName, version: depDep.version, - checksum: depDep.checksums.sha1) - options.nimbleData.addRevDep(revDep, newlyInstalledPackageInfo) + let (url, metadata) = getUrlData(dep.url) + let version = dep.version.parseVersionRange + let subdir = metadata.getOrDefault("subdir") - return newlyInstalledPackageInfo - else: - let nimbleFilePath = findNimbleFile(depDirName, false) - let packageInfo = getInstalledPackageMin( - depDirName, nimbleFilePath).toFullInfo(options) - return packageInfo + let (downloadDir, _, vcsRevision) = await downloadPkg( + url, version, dep.downloadMethod, subdir, options, + downloadPath = "", dep.vcsRevision) + + let downloadedPackageChecksum = calculateDirSha1Checksum(downloadDir) + if downloadedPackageChecksum != dep.checksums.sha1: + raise checksumError(name, dep.version, dep.vcsRevision, + downloadedPackageChecksum, dep.checksums.sha1) + + result = DownloadInfo( + dependency: dep, + url: url, + version: version, + downloadDir: downloadDir, + vcsRevision: vcsRevision) + +proc installDependency(pkgInfo: PackageInfo, downloadInfo: DownloadInfo, + options: Options): PackageInfo = + ## Installs an already downloaded dependency of the package `pkgInfo`. + let (_, newlyInstalledPkgInfo) = installFromDir( + downloadInfo.downloadDir, + downloadInfo.version, + options, + downloadInfo.url, + first = false, + fromLockFile = true, + downloadInfo.vcsRevision[]) + + downloadInfo.downloadDir.removeDir + + for depDepName in downloadInfo.dependency.dependencies: + let depDep = pkgInfo.lockedDeps[depDepName] + let revDep = (name: depDepName, version: depDep.version, + checksum: depDep.checksums.sha1) + options.nimbleData.addRevDep(revDep, newlyInstalledPkgInfo) + + return newlyInstalledPkgInfo + +proc startDownloadWorker(queue: DownloadQueue, options: Options, + downloadResults: DownloadResults) {.async.} = + ## Starts a new download worker. + while queue[].len > 0: + let download = queue[].pop + let index = queue[].len + downloadResults[index] = await downloadDependency( + download.name, download.dep, options) proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): HashSet[PackageInfo] = @@ -497,14 +547,32 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): # installs it by downloading it from its repository. let developModeDeps = getDevelopDependencies(pkgInfo, options) + + var dependenciesToDownload: DownloadQueue + dependenciesToDownload.new + for name, dep in pkgInfo.lockedDeps: - let depPkg = - if developModeDeps.hasKey(name): - developModeDeps[name][] - else: - getLockedDep(pkgInfo, name, dep, options) + if developModeDeps.hasKey(name): + result.incl developModeDeps[name][] + elif isInstalled(name, dep, options): + result.incl getDependency(name, dep, options) + else: + dependenciesToDownload[].add (name, dep) + + var downloadResults: DownloadResults + downloadResults.new + downloadResults[].setLen(dependenciesToDownload[].len) + + var downloadWorkers: seq[Future[void]] + let workersCount = min( + options.maxParallelDownloads, dependenciesToDownload[].len) + for i in 0 ..< workersCount: + downloadWorkers.add startDownloadWorker( + dependenciesToDownload, options, downloadResults) + waitFor all(downloadWorkers) - result.incl depPkg + for downloadResult in downloadResults[]: + result.incl installDependency(pkgInfo, downloadResult, options) proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string, @@ -553,11 +621,11 @@ proc install(packages: seq[PkgTuple], options: Options, let (meth, url, metadata) = getDownloadInfo(pv, options, doPrompt) let subdir = metadata.getOrDefault("subdir") let (downloadDir, downloadVersion, vcsRevision) = - downloadPkg(url, pv.ver, meth, subdir, options, downloadPath = "", - vcsRevision = notSetSha1Hash) + waitFor downloadPkg(url, pv.ver, meth, subdir, options, + downloadPath = "", vcsRevision = notSetSha1Hash) try: result = installFromDir(downloadDir, pv.ver, options, url, - first, fromLockFile, vcsRevision) + first, fromLockFile, vcsRevision[]) except BuildFailed as error: # The package failed to build. # Check if we tried building a tagged version of the package. @@ -1105,8 +1173,8 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = var options = options options.forceFullClone = true - discard downloadPkg(url, ver, meth, subdir, options, downloadDir, - vcsRevision = notSetSha1Hash) + discard waitFor downloadPkg(url, ver, meth, subdir, options, downloadDir, + vcsRevision = notSetSha1Hash) let pkgDir = downloadDir / subdir var pkgInfo = getPkgInfo(pkgDir, options) diff --git a/src/nimblepkg/asynctools/asyncpipe.nim b/src/nimblepkg/asynctools/asyncpipe.nim new file mode 100644 index 00000000..5d759d80 --- /dev/null +++ b/src/nimblepkg/asynctools/asyncpipe.nim @@ -0,0 +1,532 @@ +# +# +# Asynchronous tools for Nim Language +# (c) Copyright 2016 Eugene Kabanov +# +# See the file "LICENSE", included in this +# distribution, for details about the copyright. +# + +## This module implements cross-platform asynchronous pipes communication. +## +## Module uses named pipes for Windows, and anonymous pipes for +## Linux/BSD/MacOS. +## +## .. code-block:: nim +## var inBuffer = newString(64) +## var outBuffer = "TEST STRING BUFFER" +## +## # Create new pipe +## var o = createPipe() +## +## # Write string to pipe +## waitFor write(o, cast[pointer](addr outBuffer[0]), outBuffer.len) +## +## # Read data from pipe +## var c = waitFor readInto(o, cast[pointer](addr inBuffer[0]), inBuffer.len) +## +## inBuffer.setLen(c) +## doAssert(inBuffer == outBuffer) +## +## # Close pipe +## close(o) + +import asyncdispatch, os + +when defined(nimdoc): + type + AsyncPipe* = ref object ## Object represents ``AsyncPipe``. + + proc createPipe*(register = true): AsyncPipe = + ## Create descriptor pair for interprocess communication. + ## + ## Returns ``AsyncPipe`` object, which represents OS specific pipe. + ## + ## If ``register`` is `false`, both pipes will not be registered with + ## current dispatcher. + + proc closeRead*(pipe: AsyncPipe, unregister = true) = + ## Closes read side of pipe ``pipe``. + ## + ## If ``unregister`` is `false`, pipe will not be unregistered from + ## current dispatcher. + + proc closeWrite*(pipe: AsyncPipe, unregister = true) = + ## Closes write side of pipe ``pipe``. + ## + ## If ``unregister`` is `false`, pipe will not be unregistered from + ## current dispatcher. + + proc getReadHandle*(pipe: AsyncPipe): int = + ## Returns OS specific handle for read side of pipe ``pipe``. + + proc getWriteHandle*(pipe: AsyncPipe): int = + ## Returns OS specific handle for write side of pipe ``pipe``. + + proc getHandles*(pipe: AsyncPipe): array[2, Handle] = + ## Returns OS specific handles of ``pipe``. + + proc getHandles*(pipe: AsyncPipe): array[2, cint] = + ## Returns OS specific handles of ``pipe``. + + proc close*(pipe: AsyncPipe, unregister = true) = + ## Closes both ends of pipe ``pipe``. + ## + ## If ``unregister`` is `false`, pipe will not be unregistered from + ## current dispatcher. + + proc write*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = + ## This procedure writes an untyped ``data`` of ``size`` size to the + ## pipe ``pipe``. + ## + ## The returned future will complete once ``all`` data has been sent or + ## part of the data has been sent. + + proc readInto*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = + ## This procedure reads up to ``size`` bytes from pipe ``pipe`` + ## into ``data``, which must at least be of that size. + ## + ## Returned future will complete once all the data requested is read or + ## part of the data has been read. + + proc asyncWrap*(readHandle: Handle|cint = 0, + writeHandle: Handle|cint = 0): AsyncPipe = + ## Wraps existing OS specific pipe handles to ``AsyncPipe`` and register + ## it with current dispatcher. + ## + ## ``readHandle`` - read side of pipe (optional value). + ## ``writeHandle`` - write side of pipe (optional value). + ## **Note**: At least one handle must be specified. + ## + ## Returns ``AsyncPipe`` object. + ## + ## Windows handles must be named pipes created with ``CreateNamedPipe`` and + ## ``FILE_FLAG_OVERLAPPED`` in flags. You can use ``ReopenFile()`` function + ## to convert existing handle to overlapped variant. + ## + ## Posix handle will be modified with ``O_NONBLOCK``. + + proc asyncUnwrap*(pipe: AsyncPipe) = + ## Unregisters ``pipe`` handle from current async dispatcher. + + proc `$`*(pipe: AsyncPipe) = + ## Returns string representation of ``AsyncPipe`` object. + +else: + + when defined(windows): + import winlean + else: + import posix + + type + AsyncPipe* = ref object of RootRef + when defined(windows): + readPipe: Handle + writePipe: Handle + else: + readPipe: cint + writePipe: cint + + when defined(windows): + + proc QueryPerformanceCounter(res: var int64) + {.importc: "QueryPerformanceCounter", stdcall, dynlib: "kernel32".} + proc connectNamedPipe(hNamedPipe: Handle, lpOverlapped: pointer): WINBOOL + {.importc: "ConnectNamedPipe", stdcall, dynlib: "kernel32".} + + const + pipeHeaderName = r"\\.\pipe\asyncpipe_" + + const + DEFAULT_PIPE_SIZE = 65536'i32 + FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000'i32 + PIPE_WAIT = 0x00000000'i32 + PIPE_TYPE_BYTE = 0x00000000'i32 + PIPE_READMODE_BYTE = 0x00000000'i32 + ERROR_PIPE_CONNECTED = 535 + ERROR_PIPE_BUSY = 231 + ERROR_BROKEN_PIPE = 109 + ERROR_PIPE_NOT_CONNECTED = 233 + + proc `$`*(pipe: AsyncPipe): string = + result = "AsyncPipe [read = " & $(cast[uint](pipe.readPipe)) & + ", write = " & $(cast[int](pipe.writePipe)) & "]" + + proc createPipe*(register = true): AsyncPipe = + + var number = 0'i64 + var pipeName: WideCString + var pipeIn: Handle + var pipeOut: Handle + var sa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint, + lpSecurityDescriptor: nil, bInheritHandle: 1) + while true: + QueryPerformanceCounter(number) + let p = pipeHeaderName & $number + pipeName = newWideCString(p) + var openMode = FILE_FLAG_FIRST_PIPE_INSTANCE or FILE_FLAG_OVERLAPPED or + PIPE_ACCESS_INBOUND + var pipeMode = PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT + pipeIn = createNamedPipe(pipeName, openMode, pipeMode, 1'i32, + DEFAULT_PIPE_SIZE, DEFAULT_PIPE_SIZE, + 1'i32, addr sa) + if pipeIn == INVALID_HANDLE_VALUE: + let err = osLastError() + if err.int32 != ERROR_PIPE_BUSY: + raiseOsError(err) + else: + break + + var openMode = (FILE_WRITE_DATA or SYNCHRONIZE) + pipeOut = createFileW(pipeName, openMode, 0, addr(sa), OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, 0) + if pipeOut == INVALID_HANDLE_VALUE: + let err = osLastError() + discard closeHandle(pipeIn) + raiseOsError(err) + + result = AsyncPipe(readPipe: pipeIn, writePipe: pipeOut) + + var ovl = OVERLAPPED() + let res = connectNamedPipe(pipeIn, cast[pointer](addr ovl)) + if res == 0: + let err = osLastError() + if err.int32 == ERROR_PIPE_CONNECTED: + discard + elif err.int32 == ERROR_IO_PENDING: + var bytesRead = 0.Dword + if getOverlappedResult(pipeIn, addr ovl, bytesRead, 1) == 0: + let oerr = osLastError() + discard closeHandle(pipeIn) + discard closeHandle(pipeOut) + raiseOsError(oerr) + else: + discard closeHandle(pipeIn) + discard closeHandle(pipeOut) + raiseOsError(err) + + if register: + register(AsyncFD(pipeIn)) + register(AsyncFD(pipeOut)) + + proc asyncWrap*(readHandle = Handle(0), + writeHandle = Handle(0)): AsyncPipe = + doAssert(readHandle != 0 or writeHandle != 0) + + result = AsyncPipe(readPipe: readHandle, writePipe: writeHandle) + if result.readPipe != 0: + register(AsyncFD(result.readPipe)) + if result.writePipe != 0: + register(AsyncFD(result.writePipe)) + + proc asyncUnwrap*(pipe: AsyncPipe) = + if pipe.readPipe != 0: + unregister(AsyncFD(pipe.readPipe)) + if pipe.writePipe != 0: + unregister(AsyncFD(pipe.writePipe)) + + proc getReadHandle*(pipe: AsyncPipe): Handle {.inline.} = + result = pipe.readPipe + + proc getWriteHandle*(pipe: AsyncPipe): Handle {.inline.} = + result = pipe.writePipe + + proc getHandles*(pipe: AsyncPipe): array[2, Handle] {.inline.} = + result = [pipe.readPipe, pipe.writePipe] + + proc closeRead*(pipe: AsyncPipe, unregister = true) = + if pipe.readPipe != 0: + if unregister: + unregister(AsyncFD(pipe.readPipe)) + if closeHandle(pipe.readPipe) == 0: + raiseOsError(osLastError()) + pipe.readPipe = 0 + + proc closeWrite*(pipe: AsyncPipe, unregister = true) = + if pipe.writePipe != 0: + if unregister: + unregister(AsyncFD(pipe.writePipe)) + if closeHandle(pipe.writePipe) == 0: + raiseOsError(osLastError()) + pipe.writePipe = 0 + + proc close*(pipe: AsyncPipe, unregister = true) = + closeRead(pipe, unregister) + closeWrite(pipe, unregister) + + proc write*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = + var retFuture = newFuture[int]("asyncpipe.write") + var ol = PCustomOverlapped() + + if pipe.writePipe == 0: + retFuture.fail(newException(ValueError, + "Write side of pipe closed or not available")) + else: + GC_ref(ol) + ol.data = CompletionData(fd: AsyncFD(pipe.writePipe), cb: + proc (fd: AsyncFD, bytesCount: DWord, errcode: OSErrorCode) = + if not retFuture.finished: + if errcode == OSErrorCode(-1): + retFuture.complete(bytesCount) + else: + retFuture.fail(newException(OSError, osErrorMsg(errcode))) + ) + let res = writeFile(pipe.writePipe, data, nbytes.int32, nil, + cast[POVERLAPPED](ol)).bool + if not res: + let errcode = osLastError() + if errcode.int32 != ERROR_IO_PENDING: + GC_unref(ol) + retFuture.fail(newException(OSError, osErrorMsg(errcode))) + return retFuture + + proc readInto*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = + var retFuture = newFuture[int]("asyncpipe.readInto") + var ol = PCustomOverlapped() + + if pipe.readPipe == 0: + retFuture.fail(newException(ValueError, + "Read side of pipe closed or not available")) + else: + GC_ref(ol) + ol.data = CompletionData(fd: AsyncFD(pipe.readPipe), cb: + proc (fd: AsyncFD, bytesCount: DWord, errcode: OSErrorCode) = + if not retFuture.finished: + if errcode == OSErrorCode(-1): + assert(bytesCount > 0 and bytesCount <= nbytes.int32) + retFuture.complete(bytesCount) + else: + if errcode.int32 in {ERROR_BROKEN_PIPE, + ERROR_PIPE_NOT_CONNECTED}: + retFuture.complete(bytesCount) + else: + retFuture.fail(newException(OSError, osErrorMsg(errcode))) + ) + let res = readFile(pipe.readPipe, data, nbytes.int32, nil, + cast[POVERLAPPED](ol)).bool + if not res: + let err = osLastError() + if err.int32 in {ERROR_BROKEN_PIPE, ERROR_PIPE_NOT_CONNECTED}: + GC_unref(ol) + retFuture.complete(0) + elif err.int32 != ERROR_IO_PENDING: + GC_unref(ol) + retFuture.fail(newException(OSError, osErrorMsg(err))) + return retFuture + else: + + proc setNonBlocking(fd: cint) {.inline.} = + var x = fcntl(fd, F_GETFL, 0) + if x == -1: + raiseOSError(osLastError()) + else: + var mode = x or O_NONBLOCK + if fcntl(fd, F_SETFL, mode) == -1: + raiseOSError(osLastError()) + + proc `$`*(pipe: AsyncPipe): string = + result = "AsyncPipe [read = " & $(cast[uint](pipe.readPipe)) & + ", write = " & $(cast[uint](pipe.writePipe)) & "]" + + proc createPipe*(size = 65536, register = true): AsyncPipe = + var fds: array[2, cint] + if posix.pipe(fds) == -1: + raiseOSError(osLastError()) + setNonBlocking(fds[0]) + setNonBlocking(fds[1]) + + result = AsyncPipe(readPipe: fds[0], writePipe: fds[1]) + + if register: + register(AsyncFD(fds[0])) + register(AsyncFD(fds[1])) + + proc asyncWrap*(readHandle = cint(0), writeHandle = cint(0)): AsyncPipe = + doAssert((readHandle != 0) or (writeHandle != 0)) + result = AsyncPipe(readPipe: readHandle, writePipe: writeHandle) + if result.readPipe != 0: + setNonBlocking(result.readPipe) + register(AsyncFD(result.readPipe)) + if result.writePipe != 0: + setNonBlocking(result.writePipe) + register(AsyncFD(result.writePipe)) + + proc asyncUnwrap*(pipe: AsyncPipe) = + if pipe.readPipe != 0: + unregister(AsyncFD(pipe.readPipe)) + if pipe.writePipe != 0: + unregister(AsyncFD(pipe.writePipe)) + + proc getReadHandle*(pipe: AsyncPipe): cint {.inline.} = + result = pipe.readPipe + + proc getWriteHandle*(pipe: AsyncPipe): cint {.inline.} = + result = pipe.writePipe + + proc getHandles*(pipe: AsyncPipe): array[2, cint] {.inline.} = + result = [pipe.readPipe, pipe.writePipe] + + proc closeRead*(pipe: AsyncPipe, unregister = true) = + if pipe.readPipe != 0: + if unregister: + unregister(AsyncFD(pipe.readPipe)) + if posix.close(cint(pipe.readPipe)) != 0: + raiseOSError(osLastError()) + pipe.readPipe = 0 + + proc closeWrite*(pipe: AsyncPipe, unregister = true) = + if pipe.writePipe != 0: + if unregister: + unregister(AsyncFD(pipe.writePipe)) + if posix.close(cint(pipe.writePipe)) != 0: + raiseOSError(osLastError()) + pipe.writePipe = 0 + + proc close*(pipe: AsyncPipe, unregister = true) = + closeRead(pipe, unregister) + closeWrite(pipe, unregister) + + proc write*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = + var retFuture = newFuture[int]("asyncpipe.write") + var bytesWrote = 0 + + proc cb(fd: AsyncFD): bool = + result = true + let reminder = nbytes - bytesWrote + let pdata = cast[pointer](cast[uint](data) + bytesWrote.uint) + let res = posix.write(pipe.writePipe, pdata, cint(reminder)) + if res < 0: + let err = osLastError() + if err.int32 != EAGAIN: + retFuture.fail(newException(OSError, osErrorMsg(err))) + else: + result = false # We still want this callback to be called. + elif res == 0: + retFuture.complete(bytesWrote) + else: + bytesWrote.inc(res) + if res != reminder: + result = false + else: + retFuture.complete(bytesWrote) + + if pipe.writePipe == 0: + retFuture.fail(newException(ValueError, + "Write side of pipe closed or not available")) + else: + if not cb(AsyncFD(pipe.writePipe)): + addWrite(AsyncFD(pipe.writePipe), cb) + return retFuture + + proc readInto*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = + var retFuture = newFuture[int]("asyncpipe.readInto") + proc cb(fd: AsyncFD): bool = + result = true + let res = posix.read(pipe.readPipe, data, cint(nbytes)) + if res < 0: + let err = osLastError() + if err.int32 != EAGAIN: + retFuture.fail(newException(OSError, osErrorMsg(err))) + else: + result = false # We still want this callback to be called. + elif res == 0: + retFuture.complete(0) + else: + retFuture.complete(res) + + if pipe.readPipe == 0: + retFuture.fail(newException(ValueError, + "Read side of pipe closed or not available")) + else: + if not cb(AsyncFD(pipe.readPipe)): + addRead(AsyncFD(pipe.readPipe), cb) + return retFuture + +when isMainModule: + + when not defined(windows): + const + SIG_DFL = cast[proc(x: cint) {.noconv,gcsafe.}](0) + SIG_IGN = cast[proc(x: cint) {.noconv,gcsafe.}](1) + else: + const + ERROR_NO_DATA = 232 + + var outBuffer = "TEST STRING BUFFER" + + block test1: + # simple read/write test + var inBuffer = newString(64) + var o = createPipe() + var sc = waitFor write(o, cast[pointer](addr outBuffer[0]), + outBuffer.len) + doAssert(sc == len(outBuffer)) + var rc = waitFor readInto(o, cast[pointer](addr inBuffer[0]), + inBuffer.len) + inBuffer.setLen(rc) + doAssert(inBuffer == outBuffer) + close(o) + + block test2: + # read from pipe closed write side + var inBuffer = newString(64) + var o = createPipe() + o.closeWrite() + var rc = waitFor readInto(o, cast[pointer](addr inBuffer[0]), + inBuffer.len) + doAssert(rc == 0) + + block test3: + # write to closed read side + var sc: int = -1 + var o = createPipe() + o.closeRead() + when not defined(windows): + posix.signal(SIGPIPE, SIG_IGN) + + try: + sc = waitFor write(o, cast[pointer](addr outBuffer[0]), + outBuffer.len) + except: + discard + doAssert(sc == -1) + + when not defined(windows): + doAssert(osLastError().int32 == EPIPE) + else: + doAssert(osLastError().int32 == ERROR_NO_DATA) + + when not defined(windows): + posix.signal(SIGPIPE, SIG_DFL) + + block test4: + # bulk test of sending/receiving data + const + testsCount = 5000 + + proc sender(o: AsyncPipe) {.async.} = + var data = 1'i32 + for i in 1..testsCount: + data = int32(i) + let res = await write(o, addr data, sizeof(int32)) + doAssert(res == sizeof(int32)) + closeWrite(o) + + proc receiver(o: AsyncPipe): Future[tuple[count: int, sum: int]] {.async.} = + var data = 0'i32 + result = (count: 0, sum: 0) + while true: + let res = await readInto(o, addr data, sizeof(int32)) + if res == 0: + break + doAssert(res == sizeof(int32)) + inc(result.sum, data) + inc(result.count) + + var o = createPipe() + asyncCheck sender(o) + let res = waitFor(receiver(o)) + doAssert(res.count == testsCount) + doAssert(res.sum == testsCount * (1 + testsCount) div 2) + diff --git a/src/nimblepkg/asynctools/asyncproc.nim b/src/nimblepkg/asynctools/asyncproc.nim new file mode 100644 index 00000000..1fde87db --- /dev/null +++ b/src/nimblepkg/asynctools/asyncproc.nim @@ -0,0 +1,908 @@ +# +# +# Asynchronous tools for Nim Language +# (c) Copyright 2016 Eugene Kabanov +# +# See the file "LICENSE", included in this +# distribution, for details about the copyright. +# + +## This module implements an advanced facility for executing OS processes +## and process communication in asynchronous way. +## +## Most code for this module is borrowed from original ``osproc.nim`` by +## Andreas Rumpf, with some extensions, improvements and fixes. +## +## API is near compatible with stdlib's ``osproc.nim``. + +import strutils, os, strtabs +import asyncdispatch, asyncpipe + +when defined(windows): + import winlean +else: + const STILL_ACTIVE = 259 + import posix + +type + ProcessOption* = enum ## options that can be passed `startProcess` + poEchoCmd, ## echo the command before execution + poUsePath, ## Asks system to search for executable using PATH + ## environment variable. + ## On Windows, this is the default. + poEvalCommand, ## Pass `command` directly to the shell, without + ## quoting. + ## Use it only if `command` comes from trusted source. + poStdErrToStdOut, ## merge stdout and stderr to the stdout stream + poParentStreams, ## use the parent's streams + poInteractive, ## optimize the buffer handling for responsiveness for + ## UI applications. Currently this only affects + ## Windows: Named pipes are used so that you can peek + ## at the process' output streams. + poDemon ## Windows: The program creates no Window. + + AsyncProcessObj = object of RootObj + inPipe: AsyncPipe + outPipe: AsyncPipe + errPipe: AsyncPipe + + when defined(windows): + fProcessHandle: Handle + fThreadHandle: Handle + procId: int32 + threadId: int32 + isWow64: bool + else: + procId: Pid + isExit: bool + exitCode: cint + options: set[ProcessOption] + + AsyncProcess* = ref AsyncProcessObj ## represents an operating system process + +proc quoteShellWindows*(s: string): string = + ## Quote s, so it can be safely passed to Windows API. + ## + ## Based on Python's subprocess.list2cmdline + ## + ## See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx + let needQuote = {' ', '\t'} in s or s.len == 0 + + result = "" + var backslashBuff = "" + if needQuote: + result.add("\"") + + for c in s: + if c == '\\': + backslashBuff.add(c) + elif c == '\"': + result.add(backslashBuff) + result.add(backslashBuff) + backslashBuff.setLen(0) + result.add("\\\"") + else: + if backslashBuff.len != 0: + result.add(backslashBuff) + backslashBuff.setLen(0) + result.add(c) + + if needQuote: + result.add("\"") + +proc quoteShellPosix*(s: string): string = + ## Quote ``s``, so it can be safely passed to POSIX shell. + ## + ## Based on Python's pipes.quote + const safeUnixChars = {'%', '+', '-', '.', '/', '_', ':', '=', '@', + '0'..'9', 'A'..'Z', 'a'..'z'} + if s.len == 0: + return "''" + + let safe = s.allCharsInSet(safeUnixChars) + + if safe: + return s + else: + return "'" & s.replace("'", "'\"'\"'") & "'" + +proc quoteShell*(s: string): string = + ## Quote ``s``, so it can be safely passed to shell. + when defined(Windows): + return quoteShellWindows(s) + elif defined(posix): + return quoteShellPosix(s) + else: + {.error:"quoteShell is not supported on your system".} + + +proc execProcess*(command: string, args: seq[string] = @[], + env: StringTableRef = nil, + options: set[ProcessOption] = {poStdErrToStdOut, poUsePath, + poEvalCommand} + ): Future[tuple[exitcode: int, output: string]] {.async.} + ## A convenience asynchronous procedure that executes ``command`` + ## with ``startProcess`` and returns its exit code and output as a tuple. + ## + ## **WARNING**: this function uses poEvalCommand by default for backward + ## compatibility. Make sure to pass options explicitly. + ## + ## .. code-block:: Nim + ## + ## let outp = await execProcess("nim c -r mytestfile.nim") + ## echo "process exited with code = " & $outp.exitcode + ## echo "process output = " & outp.output + +proc startProcess*(command: string, workingDir: string = "", + args: openArray[string] = [], + env: StringTableRef = nil, + options: set[ProcessOption] = {poStdErrToStdOut}, + pipeStdin: AsyncPipe = nil, + pipeStdout: AsyncPipe = nil, + pipeStderr: AsyncPipe = nil): AsyncProcess + ## Starts a process. + ## and returns its exit code and output as a tuple + ## ``command`` is the executable file path + ## + ## ``workingDir`` is the process's working directory. If ``workingDir == ""`` + ## the current directory is used. + ## + ## ``args`` are the command line arguments that are passed to the + ## process. On many operating systems, the first command line argument is the + ## name of the executable. ``args`` should not contain this argument! + ## + ## ``env`` is the environment that will be passed to the process. + ## If ``env == nil`` the environment is inherited of + ## the parent process. + ## + ## ``options`` are additional flags that may be passed + ## to `startProcess`. See the documentation of ``ProcessOption`` for the + ## meaning of these flags. + ## + ## ``pipeStdin``, ``pipeStdout``, ``pipeStderr`` is ``AsyncPipe`` handles + ## which will be used as ``STDIN``, ``STDOUT`` and ``STDERR`` of started + ## process respectively. This handles are optional, unspecified handles + ## will be created automatically. + ## + ## Note that you can't pass any ``args`` if you use the option + ## ``poEvalCommand``, which invokes the system shell to run the specified + ## ``command``. In this situation you have to concatenate manually the + ## contents of ``args`` to ``command`` carefully escaping/quoting any special + ## characters, since it will be passed *as is* to the system shell. + ## Each system/shell may feature different escaping rules, so try to avoid + ## this kind of shell invocation if possible as it leads to non portable + ## software. + ## + ## Return value: The newly created process object. Nil is never returned, + ## but ``EOS`` is raised in case of an error. + +proc suspend*(p: AsyncProcess) + ## Suspends the process ``p``. + ## + ## On Posix OSes the procedure sends ``SIGSTOP`` signal to the process. + ## + ## On Windows procedure suspends main thread execution of process via + ## ``SuspendThread()``. WOW64 processes is also supported. + +proc resume*(p: AsyncProcess) + ## Resumes the process ``p``. + ## + ## On Posix OSes the procedure sends ``SIGCONT`` signal to the process. + ## + ## On Windows procedure resumes execution of main thread via + ## ``ResumeThread()``. WOW64 processes is also supported. + +proc terminate*(p: AsyncProcess) + ## Stop the process ``p``. On Posix OSes the procedure sends ``SIGTERM`` + ## to the process. On Windows the Win32 API function ``TerminateProcess()`` + ## is called to stop the process. + +proc kill*(p: AsyncProcess) + ## Kill the process ``p``. On Posix OSes the procedure sends ``SIGKILL`` to + ## the process. On Windows ``kill()`` is simply an alias for ``terminate()``. + +proc running*(p: AsyncProcess): bool + ## Returns `true` if the process ``p`` is still running. Returns immediately. + +proc peekExitCode*(p: AsyncProcess): int + ## Returns `STILL_ACTIVE` if the process is still running. + ## Otherwise the process' exit code. + +proc processID*(p: AsyncProcess): int = + ## Returns process ``p`` id. + return p.procId + +proc inputHandle*(p: AsyncProcess): AsyncPipe {.inline.} = + ## Returns ``AsyncPipe`` handle to ``STDIN`` pipe of process ``p``. + result = p.inPipe + +proc outputHandle*(p: AsyncProcess): AsyncPipe {.inline.} = + ## Returns ``AsyncPipe`` handle to ``STDOUT`` pipe of process ``p``. + result = p.outPipe + +proc errorHandle*(p: AsyncProcess): AsyncPipe {.inline.} = + ## Returns ``AsyncPipe`` handle to ``STDERR`` pipe of process ``p``. + result = p.errPipe + +proc waitForExit*(p: AsyncProcess): Future[int] + ## Waits for the process to finish in asynchronous way and returns + ## exit code. + +when defined(windows): + + const + STILL_ACTIVE = 0x00000103'i32 + HANDLE_FLAG_INHERIT = 0x00000001'i32 + + proc isWow64Process(hProcess: Handle, wow64Process: var WinBool): WinBool + {.importc: "IsWow64Process", stdcall, dynlib: "kernel32".} + proc wow64SuspendThread(hThread: Handle): Dword + {.importc: "Wow64SuspendThread", stdcall, dynlib: "kernel32".} + proc setHandleInformation(hObject: Handle, dwMask: Dword, + dwFlags: Dword): WinBool + {.importc: "SetHandleInformation", stdcall, dynlib: "kernel32".} + + proc buildCommandLine(a: string, args: openArray[string]): cstring = + var res = quoteShell(a) + for i in 0..high(args): + res.add(' ') + res.add(quoteShell(args[i])) + result = cast[cstring](alloc0(res.len+1)) + copyMem(result, cstring(res), res.len) + + proc buildEnv(env: StringTableRef): tuple[str: cstring, len: int] = + var L = 0 + for key, val in pairs(env): inc(L, key.len + val.len + 2) + var str = cast[cstring](alloc0(L+2)) + L = 0 + for key, val in pairs(env): + var x = key & "=" & val + copyMem(addr(str[L]), cstring(x), x.len+1) # copy \0 + inc(L, x.len+1) + (str, L) + + proc close(p: AsyncProcess) = + if p.inPipe != nil: close(p.inPipe) + if p.outPipe != nil: close(p.outPipe) + if p.errPipe != nil: close(p.errPipe) + + proc startProcess(command: string, workingDir: string = "", + args: openArray[string] = [], + env: StringTableRef = nil, + options: set[ProcessOption] = {poStdErrToStdOut}, + pipeStdin: AsyncPipe = nil, + pipeStdout: AsyncPipe = nil, + pipeStderr: AsyncPipe = nil): AsyncProcess = + var + si: STARTUPINFO + procInfo: PROCESS_INFORMATION + + result = AsyncProcess(options: options, isExit: true) + si.cb = sizeof(STARTUPINFO).cint + + if not isNil(pipeStdin): + si.hStdInput = pipeStdin.getReadHandle() + + # Mark other side of pipe as non inheritable. + let oh = pipeStdin.getWriteHandle() + if oh != 0: + if setHandleInformation(oh, HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + else: + if poParentStreams in options: + si.hStdInput = getStdHandle(STD_INPUT_HANDLE) + else: + let pipe = createPipe() + if poInteractive in options: + result.inPipe = pipe + si.hStdInput = pipe.getReadHandle() + else: + result.inPipe = pipe + si.hStdInput = pipe.getReadHandle() + + if setHandleInformation(pipe.getWriteHandle(), + HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + + if not isNil(pipeStdout): + si.hStdOutput = pipeStdout.getWriteHandle() + + # Mark other side of pipe as non inheritable. + let oh = pipeStdout.getReadHandle() + if oh != 0: + if setHandleInformation(oh, HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + else: + if poParentStreams in options: + si.hStdOutput = getStdHandle(STD_OUTPUT_HANDLE) + else: + let pipe = createPipe() + if poInteractive in options: + result.outPipe = pipe + si.hStdOutput = pipe.getWriteHandle() + else: + result.outPipe = pipe + si.hStdOutput = pipe.getWriteHandle() + if setHandleInformation(pipe.getReadHandle(), + HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + + if not isNil(pipeStderr): + si.hStdError = pipeStderr.getWriteHandle() + + # Mark other side of pipe as non inheritable. + let oh = pipeStderr.getReadHandle() + if oh != 0: + if setHandleInformation(oh, HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + else: + if poParentStreams in options: + si.hStdError = getStdHandle(STD_ERROR_HANDLE) + else: + if poInteractive in options: + let pipe = createPipe() + result.errPipe = pipe + si.hStdError = pipe.getWriteHandle() + if setHandleInformation(pipe.getReadHandle(), + HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + else: + if poStdErrToStdOut in options: + result.errPipe = result.outPipe + si.hStdError = si.hStdOutput + else: + let pipe = createPipe() + result.errPipe = pipe + si.hStdError = pipe.getWriteHandle() + if setHandleInformation(pipe.getReadHandle(), + HANDLE_FLAG_INHERIT, 0) == 0: + raiseOSError(osLastError()) + + if si.hStdInput != 0 or si.hStdOutput != 0 or si.hStdError != 0: + si.dwFlags = STARTF_USESTDHANDLES + + # building command line + var cmdl: cstring + if poEvalCommand in options: + cmdl = buildCommandLine("cmd.exe", ["/c", command]) + assert args.len == 0 + else: + cmdl = buildCommandLine(command, args) + # building environment + var e = (str: nil.cstring, len: -1) + if env != nil: e = buildEnv(env) + # building working directory + var wd: cstring = nil + if len(workingDir) > 0: wd = workingDir + # processing echo command line + if poEchoCmd in options: echo($cmdl) + # building security attributes for process and main thread + var psa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint, + lpSecurityDescriptor: nil, bInheritHandle: 1) + var tsa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint, + lpSecurityDescriptor: nil, bInheritHandle: 1) + + var tmp = newWideCString(cmdl) + var ee = + if e.str.isNil: nil + else: newWideCString(e.str, e.len) + var wwd = newWideCString(wd) + var flags = NORMAL_PRIORITY_CLASS or CREATE_UNICODE_ENVIRONMENT + if poDemon in options: flags = flags or CREATE_NO_WINDOW + let res = winlean.createProcessW(nil, tmp, addr psa, addr tsa, 1, flags, + ee, wwd, si, procInfo) + if e.str != nil: dealloc(e.str) + if res == 0: + close(result) + raiseOsError(osLastError()) + else: + result.fProcessHandle = procInfo.hProcess + result.procId = procInfo.dwProcessId + result.fThreadHandle = procInfo.hThread + result.threadId = procInfo.dwThreadId + when sizeof(int) == 8: + # If sizeof(int) == 8, then our process is 64bit, and we need to check + # architecture of just spawned process. + var iswow64 = WinBool(0) + if isWow64Process(procInfo.hProcess, iswow64) == 0: + raiseOsError(osLastError()) + result.isWow64 = (iswow64 != 0) + else: + result.isWow64 = false + + result.isExit = false + + if poParentStreams notin options: + closeRead(result.inPipe) + closeWrite(result.outPipe) + closeWrite(result.errPipe) + + proc suspend(p: AsyncProcess) = + var res = 0'i32 + if p.isWow64: + res = wow64SuspendThread(p.fThreadHandle) + else: + res = suspendThread(p.fThreadHandle) + if res < 0: + raiseOsError(osLastError()) + + proc resume(p: AsyncProcess) = + let res = resumeThread(p.fThreadHandle) + if res < 0: + raiseOsError(osLastError()) + + proc running(p: AsyncProcess): bool = + var value = 0'i32 + let res = getExitCodeProcess(p.fProcessHandle, value) + if res == 0: + raiseOsError(osLastError()) + else: + if value == STILL_ACTIVE: + result = true + else: + p.isExit = true + p.exitCode = value + + proc terminate(p: AsyncProcess) = + if running(p): + discard terminateProcess(p.fProcessHandle, 0) + + proc kill(p: AsyncProcess) = + terminate(p) + + proc peekExitCode(p: AsyncProcess): int = + if p.isExit: + result = p.exitCode + else: + var value = 0'i32 + let res = getExitCodeProcess(p.fProcessHandle, value) + if res == 0: + raiseOsError(osLastError()) + else: + result = value + if value != STILL_ACTIVE: + p.isExit = true + p.exitCode = value + + when declared(addProcess): + proc waitForExit(p: AsyncProcess): Future[int] = + var retFuture = newFuture[int]("asyncproc.waitForExit") + + proc cb(fd: AsyncFD): bool = + var value = 0'i32 + let res = getExitCodeProcess(p.fProcessHandle, value) + if res == 0: + retFuture.fail(newException(OSError, osErrorMsg(osLastError()))) + else: + p.isExit = true + p.exitCode = value + retFuture.complete(p.exitCode) + + if p.isExit: + retFuture.complete(p.exitCode) + else: + addProcess(p.procId, cb) + return retFuture + +else: + const + readIdx = 0 + writeIdx = 1 + + template statusToExitCode(status): int32 = + (status and 0xFF00) shr 8 + + proc envToCStringArray(t: StringTableRef): cstringArray = + result = cast[cstringArray](alloc0((t.len + 1) * sizeof(cstring))) + var i = 0 + for key, val in pairs(t): + var x = key & "=" & val + result[i] = cast[cstring](alloc(x.len+1)) + copyMem(result[i], addr(x[0]), x.len+1) + inc(i) + + proc envToCStringArray(): cstringArray = + var counter = 0 + for key, val in envPairs(): inc counter + result = cast[cstringArray](alloc0((counter + 1) * sizeof(cstring))) + var i = 0 + for key, val in envPairs(): + var x = key & "=" & val + result[i] = cast[cstring](alloc(x.len+1)) + copyMem(result[i], addr(x[0]), x.len+1) + inc(i) + + type StartProcessData = object + sysCommand: cstring + sysArgs: cstringArray + sysEnv: cstringArray + workingDir: cstring + pStdin, pStdout, pStderr, pErrorPipe: array[0..1, cint] + options: set[ProcessOption] + + const useProcessAuxSpawn = declared(posix_spawn) and not defined(useFork) and + not defined(useClone) and not defined(linux) + when useProcessAuxSpawn: + proc startProcessAuxSpawn(data: StartProcessData): Pid {. + tags: [ExecIOEffect, ReadEnvEffect], gcsafe.} + else: + proc startProcessAuxFork(data: StartProcessData): Pid {. + tags: [ExecIOEffect, ReadEnvEffect], gcsafe.} + + {.push stacktrace: off, profiler: off.} + proc startProcessAfterFork(data: ptr StartProcessData) {. + tags: [ExecIOEffect, ReadEnvEffect], cdecl, gcsafe.} + {.pop.} + + proc startProcess(command: string, workingDir: string = "", + args: openArray[string] = [], + env: StringTableRef = nil, + options: set[ProcessOption] = {poStdErrToStdOut}, + pipeStdin: AsyncPipe = nil, + pipeStdout: AsyncPipe = nil, + pipeStderr: AsyncPipe = nil): AsyncProcess = + var sd = StartProcessData() + + result = AsyncProcess(options: options, isExit: true) + + if not isNil(pipeStdin): + sd.pStdin = pipeStdin.getHandles() + else: + if poParentStreams notin options: + let pipe = createPipe() + sd.pStdin = pipe.getHandles() + result.inPipe = pipe + + if not isNil(pipeStdout): + sd.pStdout = pipeStdout.getHandles() + else: + if poParentStreams notin options: + let pipe = createPipe() + sd.pStdout = pipe.getHandles() + result.outPipe = pipe + + if not isNil(pipeStderr): + sd.pStderr = pipeStderr.getHandles() + else: + if poParentStreams notin options: + if poStdErrToStdOut in options: + sd.pStderr = sd.pStdout + result.errPipe = result.outPipe + else: + let pipe = createPipe() + sd.pStderr = pipe.getHandles() + result.errPipe = pipe + + var sysCommand: string + var sysArgsRaw: seq[string] + + if poEvalCommand in options: + sysCommand = "/bin/sh" + sysArgsRaw = @[sysCommand, "-c", command] + assert args.len == 0, "`args` has to be empty when using poEvalCommand." + else: + sysCommand = command + sysArgsRaw = @[command] + for arg in args.items: + sysArgsRaw.add arg + + var pid: Pid + + var sysArgs = allocCStringArray(sysArgsRaw) + defer: deallocCStringArray(sysArgs) + + var sysEnv = if env == nil: + envToCStringArray() + else: + envToCStringArray(env) + defer: deallocCStringArray(sysEnv) + + sd.sysCommand = sysCommand + sd.sysArgs = sysArgs + sd.sysEnv = sysEnv + sd.options = options + sd.workingDir = workingDir + + when useProcessAuxSpawn: + let currentDir = getCurrentDir() + pid = startProcessAuxSpawn(sd) + if workingDir.len > 0: + setCurrentDir(currentDir) + else: + pid = startProcessAuxFork(sd) + + # Parent process. Copy process information. + if poEchoCmd in options: + echo(command, " ", join(args, " ")) + result.procId = pid + + result.isExit = false + + if poParentStreams notin options: + closeRead(result.inPipe) + closeWrite(result.outPipe) + closeWrite(result.errPipe) + + when useProcessAuxSpawn: + proc startProcessAuxSpawn(data: StartProcessData): Pid = + var attr: Tposix_spawnattr + var fops: Tposix_spawn_file_actions + + template chck(e: untyped) = + if e != 0'i32: raiseOSError(osLastError()) + + chck posix_spawn_file_actions_init(fops) + chck posix_spawnattr_init(attr) + + var mask: Sigset + chck sigemptyset(mask) + chck posix_spawnattr_setsigmask(attr, mask) + + var flags = POSIX_SPAWN_USEVFORK or POSIX_SPAWN_SETSIGMASK + if poDemon in data.options: + flags = flags or POSIX_SPAWN_SETPGROUP + chck posix_spawnattr_setpgroup(attr, 0'i32) + + chck posix_spawnattr_setflags(attr, flags) + + if not (poParentStreams in data.options): + chck posix_spawn_file_actions_addclose(fops, data.pStdin[writeIdx]) + chck posix_spawn_file_actions_adddup2(fops, data.pStdin[readIdx], + readIdx) + chck posix_spawn_file_actions_addclose(fops, data.pStdout[readIdx]) + chck posix_spawn_file_actions_adddup2(fops, data.pStdout[writeIdx], + writeIdx) + if (poStdErrToStdOut in data.options): + chck posix_spawn_file_actions_adddup2(fops, data.pStdout[writeIdx], 2) + else: + chck posix_spawn_file_actions_addclose(fops, data.pStderr[readIdx]) + chck posix_spawn_file_actions_adddup2(fops, data.pStderr[writeIdx], 2) + + var res: cint + if data.workingDir.len > 0: + setCurrentDir($data.workingDir) + var pid: Pid + + if (poUsePath in data.options): + res = posix_spawnp(pid, data.sysCommand, fops, attr, data.sysArgs, + data.sysEnv) + else: + res = posix_spawn(pid, data.sysCommand, fops, attr, data.sysArgs, + data.sysEnv) + + discard posix_spawn_file_actions_destroy(fops) + discard posix_spawnattr_destroy(attr) + chck res + return pid + else: + proc startProcessAuxFork(data: StartProcessData): Pid = + if pipe(data.pErrorPipe) != 0: + raiseOSError(osLastError()) + + defer: + discard close(data.pErrorPipe[readIdx]) + + var pid: Pid + var dataCopy = data + + when defined(useClone): + const stackSize = 65536 + let stackEnd = cast[clong](alloc(stackSize)) + let stack = cast[pointer](stackEnd + stackSize) + let fn: pointer = startProcessAfterFork + pid = clone(fn, stack, + cint(CLONE_VM or CLONE_VFORK or SIGCHLD), + pointer(addr dataCopy), nil, nil, nil) + discard close(data.pErrorPipe[writeIdx]) + dealloc(stack) + else: + pid = fork() + if pid == 0: + startProcessAfterFork(addr(dataCopy)) + exitnow(1) + + discard close(data.pErrorPipe[writeIdx]) + if pid < 0: raiseOSError(osLastError()) + + var error: cint + + var res = read(data.pErrorPipe[readIdx], addr error, sizeof(error)) + if res == sizeof(error): + raiseOSError(osLastError(), + "Could not find command: '$1'. OS error: $2" % + [$data.sysCommand, $strerror(error)]) + return pid + + {.push stacktrace: off, profiler: off.} + proc startProcessFail(data: ptr StartProcessData) = + var error: cint = errno + discard write(data.pErrorPipe[writeIdx], addr error, sizeof(error)) + exitnow(1) + + when not defined(uClibc) and (not defined(linux) or defined(android)): + var environ {.importc.}: cstringArray + + proc startProcessAfterFork(data: ptr StartProcessData) = + # Warning: no GC here! + # Or anything that touches global structures - all called nim procs + # must be marked with stackTrace:off. Inspect C code after making changes. + if (poDemon in data.options): + if posix.setpgid(Pid(0), Pid(0)) != 0: + startProcessFail(data) + + if not (poParentStreams in data.options): + if posix.close(data.pStdin[writeIdx]) != 0: + startProcessFail(data) + + if dup2(data.pStdin[readIdx], readIdx) < 0: + startProcessFail(data) + + if posix.close(data.pStdout[readIdx]) != 0: + startProcessFail(data) + + if dup2(data.pStdout[writeIdx], writeIdx) < 0: + startProcessFail(data) + + if (poStdErrToStdOut in data.options): + if dup2(data.pStdout[writeIdx], 2) < 0: + startProcessFail(data) + else: + if posix.close(data.pStderr[readIdx]) != 0: + startProcessFail(data) + + if dup2(data.pStderr[writeIdx], 2) < 0: + startProcessFail(data) + + if data.workingDir.len > 0: + if chdir(data.workingDir) < 0: + startProcessFail(data) + + if posix.close(data.pErrorPipe[readIdx]) != 0: + startProcessFail(data) + + discard fcntl(data.pErrorPipe[writeIdx], F_SETFD, FD_CLOEXEC) + + if (poUsePath in data.options): + when defined(uClibc): + # uClibc environment (OpenWrt included) doesn't have the full execvpe + discard execve(data.sysCommand, data.sysArgs, data.sysEnv) + elif defined(linux) and not defined(android): + discard execvpe(data.sysCommand, data.sysArgs, data.sysEnv) + else: + # MacOSX doesn't have execvpe, so we need workaround. + # On MacOSX we can arrive here only from fork, so this is safe: + environ = data.sysEnv + discard execvp(data.sysCommand, data.sysArgs) + else: + discard execve(data.sysCommand, data.sysArgs, data.sysEnv) + + startProcessFail(data) + {.pop} + + proc close(p: AsyncProcess) = + ## We need to `wait` for process, to avoid `zombie`, so if `running()` + ## returns `false`, then process exited and `wait()` was called. + doAssert(not p.running()) + if p.inPipe != nil: close(p.inPipe) + if p.outPipe != nil: close(p.outPipe) + if p.errPipe != nil: close(p.errPipe) + + proc running(p: AsyncProcess): bool = + result = true + if p.isExit: + result = false + else: + var status = cint(0) + let res = posix.waitpid(p.procId, status, WNOHANG) + if res == 0: + result = true + elif res < 0: + raiseOsError(osLastError()) + else: + if WIFEXITED(status) or WIFSIGNALED(status): + p.isExit = true + p.exitCode = statusToExitCode(status) + result = false + + proc peekExitCode(p: AsyncProcess): int = + if p.isExit: + result = p.exitCode + else: + var status = cint(0) + let res = posix.waitpid(p.procId, status, WNOHANG) + if res < 0: + raiseOsError(osLastError()) + elif res > 0: + p.isExit = true + p.exitCode = statusToExitCode(status) + result = p.exitCode + else: + result = STILL_ACTIVE + + proc suspend(p: AsyncProcess) = + if posix.kill(p.procId, SIGSTOP) != 0'i32: + raiseOsError(osLastError()) + + proc resume(p: AsyncProcess) = + if posix.kill(p.procId, SIGCONT) != 0'i32: + raiseOsError(osLastError()) + + proc terminate(p: AsyncProcess) = + if posix.kill(p.procId, SIGTERM) != 0'i32: + raiseOsError(osLastError()) + + proc kill(p: AsyncProcess) = + if posix.kill(p.procId, SIGKILL) != 0'i32: + raiseOsError(osLastError()) + + when declared(addProcess): + proc waitForExit*(p: AsyncProcess): Future[int] = + var retFuture = newFuture[int]("asyncproc.waitForExit") + + proc cb(fd: AsyncFD): bool = + var status = cint(0) + let res = posix.waitpid(p.procId, status, WNOHANG) + if res <= 0: + retFuture.fail(newException(OSError, osErrorMsg(osLastError()))) + else: + p.isExit = true + p.exitCode = statusToExitCode(status) + retFuture.complete(p.exitCode) + + if p.isExit: + retFuture.complete(p.exitCode) + else: + while true: + var status = cint(0) + let res = posix.waitpid(p.procId, status, WNOHANG) + if res < 0: + retFuture.fail(newException(OSError, osErrorMsg(osLastError()))) + break + elif res > 0: + p.isExit = true + p.exitCode = statusToExitCode(status) + retFuture.complete(p.exitCode) + break + else: + try: + addProcess(p.procId, cb) + break + except: + let err = osLastError() + if cint(err) == ESRCH: + continue + else: + retFuture.fail(newException(OSError, osErrorMsg(err))) + break + return retFuture + +proc execProcess(command: string, args: seq[string] = @[], + env: StringTableRef = nil, + options: set[ProcessOption] = {poStdErrToStdOut, poUsePath, + poEvalCommand} + ): Future[tuple[exitcode: int, output: string]] {.async.} = + result = (exitcode: int(STILL_ACTIVE), output: "") + let bufferSize = 1024 + var data = newString(bufferSize) + var p = startProcess(command, args = args, env = env, options = options) + + while true: + let res = await p.outputHandle.readInto(addr data[0], bufferSize) + if res > 0: + data.setLen(res) + result.output &= data + data.setLen(bufferSize) + else: + break + result.exitcode = await p.waitForExit() + close(p) + +when isMainModule: + import os + + when defined(windows): + var data = waitFor(execProcess("cd")) + else: + var data = waitFor(execProcess("pwd")) + echo "exitCode = " & $data.exitcode + echo "output = [" & $data.output & "]" diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index f6286d43..4806d5c0 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import parseutils, os, osproc, strutils, tables, pegs, uri, strformat, - httpclient, json + httpclient, json, asyncdispatch from algorithm import SortOrder, sorted from sequtils import toSeq, filterIt, map @@ -10,55 +10,51 @@ from sequtils import toSeq, filterIt, map import packageinfotypes, packageparser, version, tools, common, options, cli, sha1hashes, vcstools -proc doCheckout(meth: DownloadMethod, downloadDir, branch: string) = - case meth - of DownloadMethod.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) - doCmd("git submodule update --recursive --depth 1") - of DownloadMethod.hg: - cd downloadDir: - doCmd("hg checkout " & branch) +type + DownloadPkgResult = tuple + dir: string + version: Version + vcsRevision: Sha1HashRef -proc doPull(meth: DownloadMethod, downloadDir: string) {.used.} = +proc doCheckout(meth: DownloadMethod, downloadDir, branch: string): + Future[void] {.async.} = case meth of DownloadMethod.git: - doCheckout(meth, downloadDir, "") - cd downloadDir: - doCmd("git pull") - if fileExists(".gitmodules"): - doCmd("git submodule update --recursive --depth 1") + # 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. + discard await tryDoCmdExAsync( + &"git -C {downloadDir} checkout --force {branch}") + discard await tryDoCmdExAsync( + &"git -C {downloadDir} submodule update --recursive --depth 1") of DownloadMethod.hg: - doCheckout(meth, downloadDir, "default") - cd downloadDir: - doCmd("hg pull") + discard await tryDoCmdExAsync(&"hg --cwd {downloadDir} checkout {branch}") proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "", - onlyTip = true) = + onlyTip = true) {.async.} = case meth of DownloadMethod.git: let - depthArg = if onlyTip: "--depth 1 " else: "" - branchArg = if branch == "": "" else: "-b " & branch & " " - doCmd("git clone --recursive " & depthArg & branchArg & - url & " " & downloadDir) + depthArg = if onlyTip: "--depth 1" else: "" + branchArg = if branch == "": "" else: "-b " & branch + discard await tryDoCmdExAsync( + &"git clone --recursive {depthArg} {branchArg} {url} {downloadDir}") of DownloadMethod.hg: let - tipArg = if onlyTip: "-r tip " else: "" - branchArg = if branch == "": "" else: "-b " & branch & " " - doCmd("hg clone " & tipArg & branchArg & url & " " & downloadDir) + tipArg = if onlyTip: "-r tip" else: "" + branchArg = if branch == "": "" else: "-b " & branch + discard await tryDoCmdExAsync( + &"hg clone {tipArg} {branchArg} {url} {downloadDir}") -proc getTagsList(dir: string, meth: DownloadMethod): seq[string] = +proc getTagsList(dir: string, meth: DownloadMethod): + Future[seq[string]] {.async.} = var output: string cd dir: case meth of DownloadMethod.git: - output = execProcess("git tag") + output = await tryDoCmdExAsync("git tag") of DownloadMethod.hg: - output = execProcess("hg tags") + output = await tryDoCmdExAsync("hg tags") if output.len > 0: case meth of DownloadMethod.git: @@ -77,11 +73,13 @@ proc getTagsList(dir: string, meth: DownloadMethod): seq[string] = else: result = @[] -proc getTagsListRemote*(url: string, meth: DownloadMethod): seq[string] = +proc getTagsListRemote*(url: string, meth: DownloadMethod): + Future[seq[string]] {.async.} = result = @[] case meth of DownloadMethod.git: - var (output, exitCode) = doCmdEx("git ls-remote --tags " & url.quoteShell()) + var (output, exitCode) = await doCmdExAsync( + &"git ls-remote --tags {url.quoteShell}") if exitCode != QuitSuccess: raise nimbleError("Unable to query remote tags for " & url & ". Git returned: " & output) @@ -143,19 +141,24 @@ proc isURL*(name: string): bool = name.startsWith(peg" @'://' ") proc cloneSpecificRevision(downloadMethod: DownloadMethod, - url, downloadDir: string, vcsRevision: Sha1Hash) = + url, downloadDir: string, + vcsRevision: Sha1Hash) {.async.} = assert vcsRevision != notSetSha1Hash display("Cloning", "revision: " & $vcsRevision, priority = MediumPriority) case downloadMethod of DownloadMethod.git: + let downloadDir = downloadDir.quoteShell createDir(downloadDir) - cd downloadDir: - doCmd("git init") - doCmd(fmt"git remote add origin {url}") - doCmd(fmt"git fetch --depth 1 origin {vcsRevision}") - doCmd("git reset --hard FETCH_HEAD") + discard await tryDoCmdExAsync( + &"git -C {downloadDir} init") + discard await tryDoCmdExAsync( + &"git -C {downloadDir} remote add origin {url}") + discard await tryDoCmdExAsync( + &"git -C {downloadDir} fetch --depth 1 origin {vcsRevision}") + discard await tryDoCmdExAsync( + &"git -C {downloadDir} reset --hard FETCH_HEAD") of DownloadMethod.hg: - doCmd(fmt"hg clone {url} -r {vcsRevision}") + discard await tryDoCmdExAsync(&"hg clone {url} -r {vcsRevision}") proc getTarExePath: string = ## Returns path to `tar` executable. @@ -226,23 +229,23 @@ proc getGitHubApiUrl(url, commit: string): string = ## an URL for the GitHub REST API query for the full commit hash. &"https://api.github.com/repos/{extractOwnerAndRepo(url)}/commits/{commit}" -proc getUrlContent(url: string): string = +proc getUrlContent(url: string): Future[string] {.async.} = ## Makes a GET request to `url`. - var client {.global.}: HttpClient - once: client = newHttpClient() - return client.getContent(url) + let client = newAsyncHttpClient() + return await client.getContent(url) {.warning[ProveInit]: off.} -proc getFullRevisionFromGitHubApi(url, version: string): Sha1Hash = +proc getFullRevisionFromGitHubApi(url, version: string): + Future[Sha1HashRef] {.async.} = ## By given a commit short hash and an URL to a GitHub repository retrieves ## the full hash of the commit by using GitHub REST API. try: let gitHubApiUrl = getGitHubApiUrl(url, version) display("Get", gitHubApiUrl); - let content = getUrlContent(gitHubApiUrl) + let content = await getUrlContent(gitHubApiUrl) let json = parseJson(content) if json.hasKey("sha"): - return json["sha"].str.initSha1Hash + return json["sha"].str.initSha1Hash.newClone else: raise nimbleError(json["message"].str) except CatchableError as error: @@ -250,7 +253,7 @@ proc getFullRevisionFromGitHubApi(url, version: string): Sha1Hash = &"of package at \"{url}\".", details = error) {.warning[ProveInit]: on.} -proc parseRevision(lsRemoteOutput: string): Sha1Hash = +proc parseRevision(lsRemoteOutput: string): Sha1HashRef = ## Parses the output from `git ls-remote` call to extract the returned sha1 ## hash value. Even when successful the first line of the command's output ## can be a redirection warning. @@ -258,19 +261,19 @@ proc parseRevision(lsRemoteOutput: string): Sha1Hash = for line in lines: if line.len >= 40: try: - return initSha1Hash(line[0..39]) + return line[0..39].initSha1Hash.newClone except InvalidSha1HashError: discard - return notSetSha1Hash + return notSetSha1Hash.newClone -proc getRevision(url, version: string): Sha1Hash = +proc getRevision(url, version: string): Future[Sha1HashRef] {.async.} = ## Returns the commit hash corresponding to the given `version` of the package ## in repository at `url`. - let output = tryDoCmdEx(&"git ls-remote {url} {version}") + let output = await tryDoCmdExAsync(&"git ls-remote {url} {version}") result = parseRevision(output) - if result == notSetSha1Hash: + if result[] == notSetSha1Hash: if version.seemsLikeRevision: - result = getFullRevisionFromGitHubApi(url, version) + result = await getFullRevisionFromGitHubApi(url, version) else: raise nimbleError(&"Cannot get revision for version \"{version}\" " & &"of package at \"{url}\".") @@ -286,13 +289,13 @@ proc getTarCmdLine(downloadDir, filePath: string): string = &"tar -C {downloadDir} -xf {filePath} --strip-components 1" proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): - Sha1Hash = + Future[Sha1HashRef] {.async.} = ## Downloads package tarball from GitHub. Returns the commit hash of the ## downloaded package in the case `queryRevision` is `true`. let downloadLink = getTarballDownloadLink(url, version) display("Downloading", downloadLink) - let data = getUrlContent(downloadLink) + let data = await getUrlContent(downloadLink) display("Completed", "downloading " & downloadLink) let filePath = downloadDir / "tarball.tar.gz" @@ -303,7 +306,7 @@ proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): display("Unpacking", filePath) let cmd = getTarCmdLine(downloadDir, filePath) - let (output, exitCode) = doCmdEx(cmd) + let (output, exitCode) = await doCmdExAsync(cmd) if exitCode != QuitSuccess and not output.contains("Cannot create symlink to"): # If the command fails for reason different then unable establishing a # sym-link raise an exception. This reason for failure is common on Windows @@ -314,13 +317,14 @@ proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): display("Completed", "unpacking " & filePath) filePath.removeFile - return if queryRevision: getRevision(url, version) else: notSetSha1Hash + return if queryRevision: await getRevision(url, version) + else: notSetSha1Hash.newClone {.warning[ProveInit]: off.} proc doDownload(url: string, downloadDir: string, verRange: VersionRange, downMethod: DownloadMethod, options: Options, vcsRevision: Sha1Hash): - tuple[version: Version, vcsRevision: Sha1Hash] = + Future[tuple[version: Version, vcsRevision: Sha1HashRef]] {.async.} = ## Downloads the repository specified by ``url`` using the specified download ## method. ## @@ -338,36 +342,38 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, if $latest.ver != "": result.version = latest.ver - result.vcsRevision = notSetSha1Hash + result.vcsRevision = notSetSha1Hash.newClone removeDir(downloadDir) if vcsRevision != notSetSha1Hash: if downloadTarball(url, options): - discard doDownloadTarball(url, downloadDir, $vcsRevision, false) + discard await doDownloadTarball(url, downloadDir, $vcsRevision, false) else: - cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) - result.vcsRevision = vcsRevision + await cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) + result.vcsRevision = vcsRevision.newClone elif verRange.kind == verSpecial: # We want a specific commit/branch/tag here. if verRange.spe == getHeadName(downMethod): # Grab HEAD. if downloadTarball(url, options): - result.vcsRevision = doDownloadTarball(url, downloadDir, "HEAD", true) + result.vcsRevision = await doDownloadTarball( + url, downloadDir, "HEAD", true) else: - doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) + await doClone(downMethod, url, downloadDir, + onlyTip = not options.forceFullClone) else: assert ($verRange.spe)[0] == '#', "The special version must start with '#'." let specialVersion = substr($verRange.spe, 1) if downloadTarball(url, options): - result.vcsRevision = doDownloadTarball( + result.vcsRevision = await doDownloadTarball( url, downloadDir, specialVersion, true) else: # Grab the full repo. - doClone(downMethod, url, downloadDir, onlyTip = false) + await doClone(downMethod, url, downloadDir, onlyTip = false) # Then perform a checkout operation to get the specified branch/commit. # `spe` starts with '#', trim it. - doCheckout(downMethod, downloadDir, specialVersion) + await doCheckout(downMethod, downloadDir, specialVersion) result.version = verRange.spe else: case downMethod @@ -375,45 +381,48 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, # 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. result.version = getHeadName(downMethod) - let versions = getTagsListRemote(url, downMethod).getVersionList() + let versions = (await getTagsListRemote(url, downMethod)).getVersionList() if versions.len > 0: getLatestByTag: if downloadTarball(url, options): let versionToDownload = if latest.tag.len > 0: latest.tag else: "HEAD" - result.vcsRevision = doDownloadTarball( + result.vcsRevision = await doDownloadTarball( url, downloadDir, versionToDownload, true) else: display("Cloning", "latest tagged version: " & latest.tag, priority = MediumPriority) - doClone(downMethod, url, downloadDir, latest.tag, - onlyTip = not options.forceFullClone) + await doClone(downMethod, url, downloadDir, latest.tag, + onlyTip = not options.forceFullClone) else: display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, priority = HighPriority) if downloadTarball(url, options): - result.vcsRevision = doDownloadTarball(url, downloadDir, "HEAD", true) + result.vcsRevision = await doDownloadTarball( + url, downloadDir, "HEAD", true) else: # If no commits have been tagged on the repo we just clone HEAD. - doClone(downMethod, url, downloadDir) # Grab HEAD. + await doClone(downMethod, url, downloadDir) # Grab HEAD. of DownloadMethod.hg: - doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) + await doClone(downMethod, url, downloadDir, + onlyTip = not options.forceFullClone) result.version = getHeadName(downMethod) - let versions = getTagsList(downloadDir, downMethod).getVersionList() + let versions = + (await getTagsList(downloadDir, downMethod)).getVersionList() if versions.len > 0: getLatestByTag: display("Switching", "to latest tagged version: " & latest.tag, priority = MediumPriority) - doCheckout(downMethod, downloadDir, latest.tag) + await doCheckout(downMethod, downloadDir, latest.tag) else: display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, priority = HighPriority) - if result.vcsRevision == notSetSha1Hash: + if result.vcsRevision[] == notSetSha1Hash: # In the case the package in not downloaded as tarball we must query its # VCS revision from its download directory. - result.vcsRevision = getVcsRevision(downloadDir) + result.vcsRevision = downloadDir.getVcsRevision.newClone {.warning[ProveInit]: on.} proc downloadPkg*(url: string, verRange: VersionRange, @@ -421,8 +430,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, subdir: string, options: Options, downloadPath: string, - vcsRevision: Sha1Hash): - tuple[dir: string, version: Version, vcsRevision: Sha1Hash] = + vcsRevision: Sha1Hash): Future[DownloadPkgResult] {.async.} = ## Downloads the repository as specified by ``url`` and ``verRange`` using ## the download method specified. ## @@ -465,7 +473,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, priority = HighPriority) result.dir = downloadDir / subdir - (result.version, result.vcsRevision) = doDownload( + (result.version, result.vcsRevision) = await doDownload( modUrl, downloadDir, verRange, downMethod, options, vcsRevision) if verRange.kind != verSpecial: @@ -483,7 +491,8 @@ proc echoPackageVersions*(pkg: Package) = case downMethod of DownloadMethod.git: try: - let versions = getTagsListRemote(pkg.url, downMethod).getVersionList() + let versions = + (waitFor getTagsListRemote(pkg.url, downMethod)).getVersionList() if versions.len > 0: let sortedVersions = toSeq(values(versions)) echo(" versions: " & join(sortedVersions, ", ")) diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 97d75a33..8b308b5a 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -42,6 +42,8 @@ type developLocaldeps*: bool # True if local deps + nimble develop pkg1 ... disableSslCertCheck*: bool noTarballs*: bool # Disable downloading of packages as tarballs from GitHub. + maxParallelDownloads*: int # This is the maximum number of parallel + # downloads. 0 means no limit. ActionType* = enum actionNil, actionRefresh, actionInit, actionDump, actionPublish, @@ -175,6 +177,8 @@ Nimble Options: -l, --localdeps Run in project local dependency mode -t, --no-tarballs Disable downloading of packages as tarballs when working with GitHub repositories. + -m, --max-parallel-downloads The maximum number of parallel downloads. + The default value is 20. Use 0 for no limit. --ver Query remote server for package version information when searching or listing packages. --nimbleDir:dirname Set the Nimble directory. @@ -461,6 +465,10 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = of "localdeps", "l": result.localdeps = true of "nosslcheck": result.disableSslCertCheck = true of "no-tarballs", "t": result.noTarballs = true + of "max-parallel-downloads", "m": + result.maxParallelDownloads = parseInt(val) + if result.maxParallelDownloads == 0: + result.maxParallelDownloads = int.high else: isGlobalFlag = false var wasFlagHandled = true @@ -559,6 +567,7 @@ proc initOptions*(): Options = verbosity: HighPriority, noColor: not isatty(stdout), startDir: getCurrentDir(), + maxParallelDownloads: 20, ) proc handleUnknownFlags(options: var Options) = diff --git a/src/nimblepkg/sha1hashes.nim b/src/nimblepkg/sha1hashes.nim index 5c12cdb6..497052fb 100644 --- a/src/nimblepkg/sha1hashes.nim +++ b/src/nimblepkg/sha1hashes.nim @@ -13,6 +13,8 @@ type ## procedure which validates the input. hashValue: string + Sha1HashRef* = ref Sha1Hash + const notSetSha1Hash* = Sha1Hash(hashValue: "") diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 7e65175b..a1286f6e 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -3,10 +3,12 @@ # # Various miscellaneous utility functions reside here. import osproc, pegs, strutils, os, uri, sets, json, parseutils, strformat, - sequtils + sequtils, asyncdispatch + from net import SslCVerifyMode, newContext, SslContext import version, cli, common, packageinfotypes, options, sha1hashes +import asynctools/asyncproc except quoteShell from compiler/nimblecmd import getPathVersionChecksum proc extractBin(cmd: string): string = @@ -50,6 +52,13 @@ proc doCmdEx*(cmd: string): ProcessOutput = raise nimbleError("'" & bin & "' not in PATH.") return execCmdEx(cmd) +proc doCmdExAsync*(cmd: string): Future[ProcessOutput] {.async.} = + let bin = extractBin(cmd) + if findExe(bin) == "": + raise nimbleError("'" & bin & "' not in PATH.") + let res = await asyncproc.execProcess(cmd) + return (res.output, res.exitCode) + proc tryDoCmdExErrorMessage*(cmd, output: string, exitCode: int): string = &"Execution of '{cmd}' failed with an exit code {exitCode}.\n" & &"Details: {output}" @@ -60,6 +69,12 @@ proc tryDoCmdEx*(cmd: string): string {.discardable.} = raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) return output +proc tryDoCmdExAsync*(cmd: string): Future[string] {.async.} = + let (output, exitCode) = await doCmdExAsync(cmd) + if exitCode != QuitSuccess: + raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) + return output + proc getNimBin*: string = result = "nim" if findExe("nim") != "": result = findExe("nim") diff --git a/tests/tlockfile.nim b/tests/tlockfile.nim index 7fbc211a..8af990bb 100644 --- a/tests/tlockfile.nim +++ b/tests/tlockfile.nim @@ -10,16 +10,14 @@ import testscommon import nimblepkg/displaymessages import nimblepkg/sha1hashes import nimblepkg/paths +import nimblepkg/vcstools from nimblepkg/common import cd, dump, cdNewDir from nimblepkg/tools import tryDoCmdEx, doCmdEx -from nimblepkg/vcstools import getVcsRevision, getCurrentBranch from nimblepkg/packageinfotypes import DownloadMethod from nimblepkg/lockfile import lockFileName, LockFileJsonKeys -from nimblepkg/sha1hashes import initSha1Hash from nimblepkg/developfile import ValidationError, ValidationErrorKind, developFileName, getValidationErrorMessage -from nimblepkg/vcstools import VcsType, getVcsDefaultBranchName suite "lock file": type From 8cef35a5c5788b5edc9581068047a09cefa23590 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Thu, 3 Jun 2021 14:15:18 +0300 Subject: [PATCH 44/73] Fix a bug with checksums calculation The names of the empty sub-module directories returned by `git ls-files` must not be included in checksum calculation, because otherwise when the package is downloaded via HTTP as tarball the sub-module directory is missing and the checksum will be different if its name is included in the calculation. Related to nim-lang/nimble#127 --- src/nimblepkg/checksums.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nimblepkg/checksums.nim b/src/nimblepkg/checksums.nim index 33e07fb9..b2df83dd 100644 --- a/src/nimblepkg/checksums.nim +++ b/src/nimblepkg/checksums.nim @@ -18,13 +18,13 @@ Downloaded package checksum does not correspond to that in the lock file: """) proc updateSha1Checksum(checksum: var Sha1State, fileName, filePath: string) = - checksum.update(fileName) if not filePath.fileExists: # In some cases a file name returned by `git ls-files` or `hg manifest` # could be an empty directory name and if so trying to open it will result # in a crash. This happens for example in the case of a git sub module # directory from which no files are being installed. return + checksum.update(fileName) let file = filePath.open(fmRead) defer: close(file) const bufferSize = 8192 From 1bd3ca8f73f3617f932a407c73a140498e080743 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 4 Jun 2021 18:21:48 +0300 Subject: [PATCH 45/73] Make all file schema versions to be a number Make all file schema versions to be a single number instead of semantic versioning strings. Semantic versioning in not really needed for eventual schema migration functionality for which the versioning is introduced. Related to nim-lang/nimble#127 --- readme.markdown | 4 ++-- src/nimble.nim | 2 +- src/nimblepkg/developfile.nim | 2 +- src/nimblepkg/lockfile.nim | 2 +- src/nimblepkg/nimbledatafile.nim | 2 +- src/nimblepkg/packagemetadatafile.nim | 2 +- src/nimblepkg/reversedeps.nim | 4 ++-- src/nimblepkg/syncfile.nim | 2 +- tests/testscommon.nim | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/readme.markdown b/readme.markdown index 03b707f0..343b055b 100644 --- a/readme.markdown +++ b/readme.markdown @@ -236,7 +236,7 @@ the following structure: ```json { - "version": "0.1.0", + "version": 1, "includes": [], "dependencies": [] } @@ -318,7 +318,7 @@ Currently the lock file have the structure as in the following example: ```json { - "version": "0.1.0", + "version": 1, "packages": { ... "chronos": { diff --git a/src/nimble.nim b/src/nimble.nim index adc9691b..c6822184 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -1597,7 +1597,7 @@ proc setupNimbleConfig(options: Options) = ## dependencies. Includes it in `config.nims` file to make them available ## for the compiler. const - configFileVersion = "0.1.0" + configFileVersion = 1 configFileHeader = &"# begin Nimble config (version {configFileVersion})\n" configFileContent = fmt""" when fileExists("{nimblePathsFileName}"): diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index 5c19d2d9..33268c99 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -99,7 +99,7 @@ const ## The default name of a Nimble's develop file. This must always be the name ## of develop files which are not only for inclusion but associated with a ## specific package. - developFileVersion* = "0.1.0" + developFileVersion* = 1 ## The version of the develop file's JSON schema. proc initDevelopFileData: DevelopFileData = diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim index c6438194..a80b8033 100644 --- a/src/nimblepkg/lockfile.nim +++ b/src/nimblepkg/lockfile.nim @@ -12,7 +12,7 @@ type const lockFileName* = "nimble.lock" - lockFileVersion = "0.1.0" + lockFileVersion = 1 proc initLockFileDep(): LockFileDep = result = LockFileDep( diff --git a/src/nimblepkg/nimbledatafile.nim b/src/nimblepkg/nimbledatafile.nim index 68b9126e..aaf99ce1 100644 --- a/src/nimblepkg/nimbledatafile.nim +++ b/src/nimblepkg/nimbledatafile.nim @@ -15,7 +15,7 @@ type const nimbleDataFileName* = "nimbledata.json" - nimbleDataFileVersion ="0.1.0" + nimbleDataFileVersion = 1 var isNimbleDataFileLoaded = false diff --git a/src/nimblepkg/packagemetadatafile.nim b/src/nimblepkg/packagemetadatafile.nim index 12787ef6..48d88fa0 100644 --- a/src/nimblepkg/packagemetadatafile.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -13,7 +13,7 @@ type const packageMetaDataFileName* = "nimblemeta.json" - packageMetaDataFileVersion = "0.1.0" + packageMetaDataFileVersion = 1 proc initPackageMetaData*(): PackageMetaData = result = PackageMetaData( diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index d59e1f24..d0c2dcca 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -242,7 +242,7 @@ when isMainModule: test "addRevDep": let expectedResult = """{ - "version": "0.1.0", + "version": 1, "reverseDeps": { "jester": { "0.1.0": { @@ -309,7 +309,7 @@ when isMainModule: test "removeRevDep": let expectedResult = """{ - "version": "0.1.0", + "version": 1, "reverseDeps": { "jester": { "0.1.0": { diff --git a/src/nimblepkg/syncfile.nim b/src/nimblepkg/syncfile.nim index 7e0015d4..030b7dd2 100644 --- a/src/nimblepkg/syncfile.nim +++ b/src/nimblepkg/syncfile.nim @@ -29,7 +29,7 @@ type const syncFileExt = ".nimble.sync" - syncFileVersion = "0.1.0" + syncFileVersion = 1 proc getPkgDir(pkgInfo: PackageInfo): string = pkgInfo.myPath.splitFile.dir diff --git a/tests/testscommon.nim b/tests/testscommon.nim index 4d0e4acb..00558660 100644 --- a/tests/testscommon.nim +++ b/tests/testscommon.nim @@ -182,8 +182,8 @@ proc filesList(filesNames: seq[string]): string = result.add ',' proc developFile*(includes: seq[string], dependencies: seq[string]): string = - result = """{"version":"$#","includes":[$#],"dependencies":[$#]}""" % - [developFileVersion, filesList(includes), filesList(dependencies)] + result = """{"version":$#,"includes":[$#],"dependencies":[$#]}""" % + [$developFileVersion, filesList(includes), filesList(dependencies)] proc writeDevelopFile*(path: string, includes: seq[string], dependencies: seq[string]) = From dfeed753646d3494039c0d891742bfb28d547976 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 11 Jun 2021 21:15:28 +0300 Subject: [PATCH 46/73] Remove validation of develop file dependencies The validation of develop file dependencies against the Nimble file is removed in order to allow not direct (transitive) dependencies to be added to the develop file. Now, all valid packages are allowed to be added to the project's develop file and not only packages listed in its `requires` clause. Version range validation for the present in the Nimble file packages is also removed. On dependency traversal, the version of the package coming from the develop file is always preferred instead of an installed package even if its version does not match the version from the Nimble file. The version ranges of the develop mode dependencies are validated on `nimble lock` and `nimble check` commands. Related to nim-lang/nimble#127 --- src/nimble.nim | 35 +++++++- src/nimblepkg/developfile.nim | 135 ++---------------------------- src/nimblepkg/displaymessages.nim | 29 +++---- src/nimblepkg/packageinfo.nim | 15 ++-- tests/tdevelopfeature.nim | 82 +++++++----------- tests/tlockfile.nim | 29 +++++++ 6 files changed, 122 insertions(+), 203 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index c6822184..00de1af9 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -1308,10 +1308,36 @@ proc test(options: Options) = if not execHook(options, actionCustom, false): return +proc notInRequiredRangeMsg*(dependentPkg, dependencyPkg: PackageInfo, + versionRange: VersionRange): string = + notInRequiredRangeMsg(dependencyPkg.name, dependencyPkg.getNimbleFileDir, + $dependencyPkg.version, dependentPkg.name, dependentPkg.getNimbleFileDir, + $versionRange) + +proc validateDevelopDependenciesVersionRanges(dependentPkg: PackageInfo, + dependencies: seq[PackageInfo], options: Options) = + let allPackages = concat(@[dependentPkg], dependencies) + let developDependencies = processDevelopDependencies(dependentPkg, options) + var errors: seq[string] + for pkg in allPackages: + for dep in pkg.requires: + var depPkg = initPackageInfo() + if not findPkg(developDependencies, dep, depPkg): + # This dependency is not part of the develop mode dependencies. + continue + if not withinRange(depPkg, dep.ver): + errors.add notInRequiredRangeMsg(pkg, depPkg, dep.ver) + if errors.len > 0: + raise nimbleError(invalidDevelopDependenciesVersionsMsg(errors)) + proc check(options: Options) = try: - let pkgInfo = getPkgInfo(getCurrentDir(), options, true) - validateDevelopFile(pkgInfo, options) + let currentDir = getCurrentDir() + let pkgInfo = getPkgInfo(currentDir, options, true) + if currentDir.developFileExists: + validateDevelopFile(pkgInfo, options) + let dependencies = pkgInfo.processAllDependencies(options).toSeq + validateDevelopDependenciesVersionRanges(pkgInfo, dependencies, options) displaySuccess(&"The package \"{pkgInfo.name}\" is valid.") except CatchableError as error: displayError(error) @@ -1422,8 +1448,9 @@ proc lock(options: Options) = validateDevModeDepsWorkingCopiesBeforeLock(pkgInfo, options) let dependencies = pkgInfo.processFreeDependencies(options).map( - pkg => pkg.toFullInfo(options)) - var dependencyGraph = buildDependencyGraph(dependencies.toSeq, options) + pkg => pkg.toFullInfo(options)).toSeq + pkgInfo.validateDevelopDependenciesVersionRanges(dependencies, options) + var dependencyGraph = buildDependencyGraph(dependencies, options) if currentDir.lockFileExists: # If we already have a lock file, merge its data with the newly generated diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index 33268c99..2617cae3 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -171,124 +171,23 @@ proc developFileExists*(pkg: PackageInfo): bool = ## the directory of the package's `pkg` `.nimble` file or `false` otherwise. pkg.getNimbleFilePath.developFileExists -proc raiseDependencyNotInRangeError( - dependencyNameAndVersion, dependentNameAndVersion: string, - versionRange: VersionRange) = - ## Raises `DependencyNotInRange` exception. - raise nimbleError( - dependencyNotInRangeErrorMsg( - dependencyNameAndVersion, dependentNameAndVersion, versionRange), - dependencyNotInRangeErrorHint) - -proc raiseNotADependencyError( - dependencyNameAndVersion, dependentNameAndVersion: string) = - ## Raises `NotADependency` exception. - raise nimbleError( - notADependencyErrorMsg(dependencyNameAndVersion, dependentNameAndVersion), - notADependencyErrorHint) - -proc validateDependency(dependencyPkg, dependentPkg: PackageInfo) = - ## Checks whether `dependencyPkg` is a valid dependency of the `dependentPkg`. - ## If it is not, then raises a `NimbleError` or otherwise simply returns. - ## - ## Raises a `NimbleError` if: - ## - the `dependencyPkg` is not a dependency of the `dependentPkg`. - ## - the `dependencyPkg` is a dependency og the `dependentPkg`, but its - ## version is out of the required by `dependentPkg` version range. - - var isNameFound = false - var versionRange = parseVersionRange("") # any version - - for pkg in dependentPkg.requires: - if cmpIgnoreStyle(dependencyPkg.name, pkg.name) == 0: - isNameFound = true - if dependencyPkg.version in pkg.ver: - # `dependencyPkg` is a valid dependency of `dependentPkg`. - return - else: - # The package with a name `dependencyPkg.name` is found among - # `dependentPkg` dependencies but its version is out of the required - # range. - versionRange = pkg.ver - break - - # If in `dependentPkg` requires clauses is not found a package with a name - # `dependencyPkg.name` or its version is not in the required range, then - # `dependencyPkg` is not a valid dependency of `dependentPkg`. - - let dependencyPkgNameAndVersion = dependencyPkg.getNameAndVersion() - let dependentPkgNameAndVersion = dependentPkg.getNameAndVersion() - - if isNameFound: - raiseDependencyNotInRangeError( - dependencyPkgNameAndVersion, dependentPkgNameAndVersion, versionRange) - else: - raiseNotADependencyError( - dependencyPkgNameAndVersion, dependentPkgNameAndVersion) - -proc validateIncludedDependency(dependencyPkg, dependentPkg: PackageInfo, - requiredVersionRange: VersionRange): - ref CatchableError = - ## Checks whether the `dependencyPkg` version is in required by the - ## `dependentPkg` version range and if not returns a reference to an error - ## object. Otherwise returns `nil`. - - return - if dependencyPkg.version in requiredVersionRange: nil - else: nimbleError( - dependencyNotInRangeErrorMsg( - dependencyPkg.getNameAndVersion, dependentPkg.getNameAndVersion, - requiredVersionRange), - dependencyNotInRangeErrorHint) - -proc validatePackage(pkgPath: Path, dependentPkg: PackageInfo, - options: Options): +proc validatePackage(pkgPath: Path, options: Options): tuple[pkgInfo: PackageInfo, error: ref CatchableError] = ## By given file system path `pkgPath`, determines whether it points to a ## valid Nimble package. ## - ## If a not empty `dependentPkg` argument is given checks whether the package - ## at `pkgPath` is a valid dependency of `dependentPkg`. - ## ## Returns a tuple containing: ## - `pkgInfo` - the package info of the package at `pkgPath` in case ## `pkgPath` directory contains a valid Nimble package. ## ## - `error` - a reference to the exception raised in case `pkgPath` is - ## not a valid package directory or the package in `pkgPath` - ## is not a valid dependency of the `dependentPkg`. + ## not a valid package directory. try: result.pkgInfo = getPkgInfo(string(pkgPath), options, true) - if dependentPkg.isLoaded: - validateDependency(result.pkgInfo, dependentPkg) except CatchableError as error: result.error = error -proc filterAndValidateIncludedPackages(dependentPkg: PackageInfo, - inclFileData: DevelopFileData, - invalidPackages: var InvalidPaths): - seq[ref PackageInfo] = - ## Iterates over `dependentPkg` dependencies and for each one found in the - ## `inclFileData` list of packages checks whether it is in the required - ## version range. If so stores it to the result sequence and otherwise stores - ## an error object in `invalidPackages` sequence for future error reporting. - - # For each dependency of the dependent package. - for pkg in dependentPkg.requires: - # Check whether it is in the loaded from the included develop file - # dependencies. - let inclPkg = inclFileData.nameToPkg.getOrDefault pkg.name - if inclPkg == nil: - # If not then continue. - continue - # Otherwise validate it against the dependent package. - let error = validateIncludedDependency(inclPkg[], dependentPkg, pkg.ver) - if error == nil: - result.add inclPkg - else: - invalidPackages[inclPkg[].getNimbleFilePath] = error - proc hasErrors(errors: ErrorsCollection): bool = ## Checks whether there are some errors in the `ErrorsCollection` - `errors`. errors.collidingNames.len > 0 or errors.invalidPackages.len > 0 or @@ -416,18 +315,9 @@ proc addPackages(lhs: var DevelopFileData, pkgs: seq[ref PackageInfo], proc mergeIncludedDevFileData(lhs: var DevelopFileData, rhs: DevelopFileData, errors: var ErrorsCollection) = ## Merges develop file data `rhs` coming from some included develop file into - ## `lhs`. If `lhs` represents develop file data of some package, but not a - ## free develop file, then first filter and validate `rhs` packages against - ## `lhs`'s list of dependencies. - - let pkgs = - if lhs.dependentPkg.isLoaded: - filterAndValidateIncludedPackages( - lhs.dependentPkg, rhs, errors.invalidPackages) - else: - rhs.nameToPkg.values - - lhs.addPackages(pkgs, rhs.path, rhs.pkgToDevFileNames, errors.collidingNames) + ## `lhs`. + lhs.addPackages(rhs.nameToPkg.values, rhs.path, rhs.pkgToDevFileNames, + errors.collidingNames) proc mergeFollowedDevFileData(lhs: var DevelopFileData, rhs: DevelopFileData, errors: var ErrorsCollection) = @@ -494,8 +384,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, for depPath in result.dependencies: let depPath = if depPath.isAbsolute: depPath.normalizedPath else: (path.splitFile.dir / depPath).normalizedPath - let (pkgInfo, error) = validatePackage( - depPath, result.dependentPkg, options) + let (pkgInfo, error) = validatePackage(depPath, options) if error == nil: result.addPackage(pkgInfo, path, [path].toHashSet, errors.collidingNames) else: @@ -547,7 +436,6 @@ proc addDevelopPackage(data: var DevelopFileData, pkg: PackageInfo): bool = ## Returns `false` in the case of error when: ## - a package with the same name but at different path is already present ## in the develop file or some of its includes. - ## - the package `pkg` is not a valid dependency of the dependent package. let pkgDir = pkg.getNimbleFilePath() @@ -558,14 +446,6 @@ proc addDevelopPackage(data: var DevelopFileData, pkg: PackageInfo): bool = displayError(pkgAlreadyPresentAtDifferentPathMsg(pkg.name, $otherPath)) return false - if data.dependentPkg.isLoaded: - # Check whether `pkg` is a valid dependency. - try: - validateDependency(pkg, data.dependentPkg) - except CatchableError as error: - displayError(error) - return false - # Add `pkg` to the develop file model. let success = not data.dependencies.containsOrIncl(pkgDir) @@ -594,9 +474,8 @@ proc addDevelopPackage(data: var DevelopFileData, path: Path, ## - the path in `path` does not point to a valid Nimble package. ## - a package with the same name but at different path is already present ## in the develop file or some of its includes. - ## - the package `pkg` is not a valid dependency of the dependent package. - let (pkgInfo, error) = validatePackage(path, initPackageInfo(), options) + let (pkgInfo, error) = validatePackage(path, options) if error != nil: displayError(invalidPkgMsg($path)) displayDetails(error) diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 1d9ed528..2ea4770a 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -50,20 +50,6 @@ proc pkgNotFoundMsg*(pkg: PkgTuple): string = &"Package {pkg} not found." proc pkgDepsAlreadySatisfiedMsg*(dep: PkgTuple): string = &"Dependency on {dep} already satisfied" -proc dependencyNotInRangeErrorMsg*( - dependencyNameAndVersion, dependentNameAndVersion: string, - versionRange: VersionRange): string = - ## Returns an error message for `DependencyNotInRange` exception. - &"The dependency package \"{dependencyNameAndVersion}\" version is out of " & - &"the required by the dependent package \"{dependentNameAndVersion}\" " & - &"version range \"{versionRange}\"." - -proc notADependencyErrorMsg*( - dependencyNameAndVersion, dependentNameAndVersion: string): string = - ## Returns an error message for `NotADependency` exception. - &"The package \"{dependencyNameAndVersion}\" is not a dependency of the " & - &"package \"{dependentNameAndVersion}\"." - proc invalidPkgMsg*(path: string): string = &"The package at \"{path}\" is invalid." @@ -133,3 +119,18 @@ proc pkgWorkingCopyNeedsSyncingMsg*(pkgName, pkgPath: string): string = proc pkgWorkingCopyIsSyncedMsg*(pkgName, pkgPath: string): string = &"Working copy of package \"{pkgName}\" at \"{pkgPath}\" is synced." + +proc notInRequiredRangeMsg*( + dependencyPkgName, dependencyPkgPath, dependencyPkgVersion, + dependentPkgName, dependentPkgPath, requiredVersionRange: string): string = + &"The version of the package \"{dependencyPkgName}\" at " & + &"\"{dependencyPkgPath}\" is \"{dependencyPkgVersion}\" and it does not " & + &"match the required by the package \"{dependentPkgName}\" at " & + &"\"{dependentPkgPath}\" version \"{requiredVersionRange}\"." + +proc invalidDevelopDependenciesVersionsMsg*(errors: seq[string]): string = + result = "Some of the develop mode dependencies are with versions which " & + "are not in the required by other package's Nimble file range." + for error in errors: + result &= "\n" + result &= error diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index c7d4851e..d954fa87 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -335,14 +335,15 @@ proc findPkg*(pkglist: seq[PackageInfo], dep: PkgTuple, for pkg in pkglist: if cmpIgnoreStyle(pkg.name, dep.name) != 0 and cmpIgnoreStyle(pkg.url, dep.name) != 0: continue - assert not (pkg.isLink and r.isLink), - "Should not happen the list to contain " & - "the same package in develop mode twice." - if withinRange(pkg, dep.ver): - let isNewer = r.version < pkg.version + if pkg.isLink: # If `pkg.isLink` this is a develop mode package and develop mode packages - # are always with higher priority than installed packages. - if not result or isNewer or pkg.isLink: + # are always with higher priority than installed packages. Version range + # is not being considered for them. + r = pkg + return true + elif withinRange(pkg, dep.ver): + let isNewer = r.version < pkg.version + if not result or isNewer: r = pkg result = true diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index 1741a4b2..f32dcb05 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -15,8 +15,6 @@ suite "develop feature": const pkgListFileName = "packages.json" dependentPkgName = "dependent" - dependentPkgVersion = "1.0" - dependentPkgNameAndVersion = &"{dependentPkgName}@{dependentPkgVersion}" dependentPkgPath = "develop/dependent".normalizedPath includeFileName = "included.develop" pkgAName = "packagea" @@ -171,7 +169,7 @@ suite "develop feature": check lines.inLinesOrdered( pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) - test "cannot add not a dependency downloaded package to the develop file": + test "can add not a dependency downloaded package to the develop file": cleanDir installDir cd "develop/dependency": usePackageListFile &"../{pkgListFileName}": @@ -181,8 +179,8 @@ suite "develop feature": "develop", &"-p:{installDir}", pkgAName, pkgBName) pkgAAbsPath = installDir / pkgAName pkgBAbsPath = installDir / pkgBName - developFileContent = developFile(@[], @[pkgAAbsPath]) - check exitCode == QuitFailure + developFileContent = developFile(@[], @[pkgAAbsPath, pkgBAbsPath]) + check exitCode == QuitSuccess check parseFile(developFileName) == parseJson(developFileContent) var lines = output.processOutput check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) @@ -190,7 +188,7 @@ suite "develop feature": check lines.inLinesOrdered( pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) check lines.inLinesOrdered( - notADependencyErrorMsg(&"{pkgBName}@0.2.0", depNameAndVersion)) + pkgAddedInDevModeMsg(&"{pkgBName}@0.2.0", pkgBAbsPath)) test "add package to develop file": cleanDir installDir @@ -229,14 +227,17 @@ suite "develop feature": check output.processOutput.inLines(invalidPkgMsg(invalidPkgDir)) check not developFileName.fileExists - test "cannot add not a dependency to develop file": + test "can add not a dependency to develop file": cd dependentPkgPath: cleanFile developFileName - let (output, exitCode) = execNimble("develop", "-a:../srcdirtest/") - check exitCode == QuitFailure - check output.processOutput.inLines( - notADependencyErrorMsg(&"{pkgSrcDirTestName}@1.0", "dependent@1.0")) - check not developFileName.fileExists + const srcDirTestPath = "../srcdirtest" + let (output, exitCode) = execNimble("develop", &"-a:{srcDirTestPath}") + check exitCode == QuitSuccess + let lines = output.processOutput + check lines.inLines( + pkgAddedInDevModeMsg("srcdirtest@1.0", srcDirTestPath)) + const developFileContent = developFile(@[], @[srcDirTestPath]) + check parseFile(developFileName) == parseJson(developFileContent) test "cannot add two packages with the same name to develop file": cd dependentPkgPath: @@ -407,23 +408,6 @@ suite "develop feature": (dep2Path.Path, includeFileName.Path)].toHashSet)) check parseFile(developFileName) == parseJson(developFileContent) - test "validate included dependencies version": - cd &"{dependentPkgPath}2": - cleanFiles developFileName, includeFileName - const includeFileContent = developFile(@[], @[dep2Path]) - writeFile(includeFileName, includeFileContent) - let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - check exitCode == QuitFailure - var lines = output.processOutput - let developFilePath = getCurrentDir() / developFileName - check lines.inLinesOrdered( - failedToInclInDevFileMsg(includeFileName, developFilePath)) - check lines.inLinesOrdered(invalidPkgMsg(dep2Path)) - check lines.inLinesOrdered(dependencyNotInRangeErrorMsg( - depNameAndVersion, dependentPkgNameAndVersion, - parseVersionRange(">= 0.2.0"))) - check not developFileName.fileExists - test "exclude develop file": cd dependentPkgPath: cleanFiles developFileName, includeFileName @@ -577,7 +561,7 @@ suite "develop feature": let (_, errorCode) = execNimble("run", "-n") check errorCode == QuitSuccess - test "filter not used included develop dependencies": + test "do not filter not used included develop dependencies": # +--------------------------+ +--------------------------+ # | pkg1 | +------------>| pkg2 | # +--------------------------+ | dependency +--------------------------+ @@ -598,10 +582,6 @@ suite "develop feature": # | version = "0.2.0" | # +---------------------+ - # Here the build must fail because "pkg3" coming from develop file included - # in "pkg2"'s develop file is not a dependency of "pkg2" itself and it must - # be filtered. In this way "pkg1"'s dependency to "pkg3" is not satisfied. - cd "develop": const pkg1DevFilePath = "pkg1" / developFileName @@ -619,11 +599,12 @@ suite "develop feature": cd "pkg1": cleanFile "pkg1".addFileExt(ExeExt) let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitFailure + check exitCode == QuitSuccess var lines = output.processOutput check lines.inLinesOrdered( pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) - check lines.inLinesOrdered(pkgNotFoundMsg(("pkg3", anyVersion))) + check lines.inLinesOrdered( + pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) test "do not filter used included develop dependencies": # +--------------------------+ +--------------------------+ @@ -678,7 +659,7 @@ suite "develop feature": check lines.inLinesOrdered( pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) - test "no version clash with filtered not used included develop dependencies": + test "version clash with not used included develop dependencies": # +--------------------------+ +--------------------------+ # | pkg1 | +------------>| pkg2 | # +--------------------------+ | dependency +--------------------------+ @@ -701,9 +682,9 @@ suite "develop feature": # | version = "0.1.0" | # +-------------------+ - # Here the build must pass because only the version of "pkg3" included via - # "develop1.json" must be taken into account, since "pkg2" does not depend - # on "pkg3" and the version coming from "develop2.json" must be filtered. + # Here the build must fail because both the version of "pkg3" included via + # "develop1.json" and the version of "pkg3" included via "develop2.json" are + # taken into account. cd "develop": const @@ -716,6 +697,10 @@ suite "develop feature": pkg2DevFileContent = developFile(@[&"../{freeDevFile2Name}"], @[]) freeDevFile1Content = developFile(@[], @["./pkg3"]) freeDevFile2Content = developFile(@[], @["./pkg3.2"]) + pkg3Path = (".." / "pkg3").Path + pkg32Path = (".." / "pkg3.2").Path + freeDevFile1Path = (".." / freeDevFile1Name).Path + freeDevFile2Path = (".." / freeDevFile2Name).Path cleanFiles pkg1DevFilePath, pkg2DevFilePath, freeDevFile1Name, freeDevFile2Name @@ -727,12 +712,11 @@ suite "develop feature": cd "pkg1": cleanFile "pkg1".addFileExt(ExeExt) let (output, exitCode) = execNimble("run", "-n") - check exitCode == QuitSuccess + check exitCode == QuitFailure var lines = output.processOutput - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg2", anyVersion))) - check lines.inLinesOrdered( - pkgDepsAlreadySatisfiedMsg(("pkg3", anyVersion))) + check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg("pkg3", + [(pkg3Path, freeDevFile1Path), + (pkg32Path, freeDevFile2Path)].toHashSet)) test "version clash with used included develop dependencies": # +--------------------------+ +--------------------------+ @@ -851,7 +835,7 @@ suite "develop feature": let developFilePath = getCurrentDir() / developFileName (output, errorCode) = execNimble("develop", &"-p:{installDir}", - pkgAName, # fail because not a direct dependency + pkgAName, # success "-c", # success &"-a:{depPath}", # success &"-a:{dep2Path}", # fail because of names collision @@ -887,9 +871,7 @@ suite "develop feature": check lines.inLinesOrdered(emptyDevFileCreatedMsg(dep2DevelopFilePath)) check parseFile(dep2DevelopFilePath) == parseJson(emptyDevelopFileContent) - check lines.inLinesOrdered(notADependencyErrorMsg( - &"{pkgAName}@0.6.0", dependentPkgNameAndVersion)) - const expectedDevelopFileContent = developFile( - @[includeFileName], @[dep2Path]) + let expectedDevelopFileContent = developFile( + @[includeFileName], @[dep2Path, &"{installDir}/{pkgAName}"]) check parseFile(developFileName) == parseJson(expectedDevelopFileContent) diff --git a/tests/tlockfile.nim b/tests/tlockfile.nim index 8af990bb..f01df8b4 100644 --- a/tests/tlockfile.nim +++ b/tests/tlockfile.nim @@ -225,6 +225,35 @@ requires "nim >= 1.5.1" cd mainPkgRepoPath: testLockFile(@[(dep1PkgName, dep1PkgRepoPath)], isNew = true) + test "cannot lock because develop dependency is out of range": + cleanUp() + withPkgListFile: + initNewNimblePackage(mainPkgOriginRepoPath, mainPkgRepoPath, + @[dep1PkgName, dep2PkgName]) + initNewNimblePackage(dep1PkgOriginRepoPath, dep1PkgRepoPath) + initNewNimblePackage(dep2PkgOriginRepoPath, dep2PkgRepoPath) + cd mainPkgRepoPath: + writeDevelopFile(developFileName, @[], + @[dep1PkgRepoPath, dep2PkgRepoPath]) + + # Make main package's Nimble file to require dependencies versions + # different than provided in the develop file. + let nimbleFileContent = mainPkgNimbleFileName.readFile + mainPkgNimbleFileName.writeFile(nimbleFileContent.replace( + &"\"{dep1PkgName}\",\"{dep2PkgName}\"", + &"\"{dep1PkgName} > 0.1.0\",\"{dep2PkgName} < 0.1.0\"")) + + let (output, exitCode) = execNimbleYes("lock") + check exitCode == QuitFailure + let errors = @[ + notInRequiredRangeMsg(dep1PkgName, dep1PkgRepoPath, "0.1.0", + mainPkgName, mainPkgRepoPath, "> 0.1.0"), + notInRequiredRangeMsg(dep2PkgName, dep2PkgRepoPath, "0.1.0", + mainPkgName, mainPkgRepoPath, "< 0.1.0") + ] + check output.processOutput.inLines( + invalidDevelopDependenciesVersionsMsg(errors)) + test "can download locked dependencies": cleanUp() withPkgListFile: From db668eaea9efe4a885fcf646b599a63ecc915f7c Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 29 Jun 2021 05:28:49 +0300 Subject: [PATCH 47/73] Remove some duplicated imports Remove some duplicated imports and add `[DuplicateModuleImport]` warning into the list of checked by the tests warnings. Related to nim-lang/nimble#127 --- tests/tmisctests.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tmisctests.nim b/tests/tmisctests.nim index bd11ec5b..cb0042a1 100644 --- a/tests/tmisctests.nim +++ b/tests/tmisctests.nim @@ -75,6 +75,7 @@ suite "misc tests": proc checkOutput(output: string): uint = const warningsToCheck = [ "[UnusedImport]", + "[DuplicateModuleImport]", "[Deprecated]", "[XDeclaredButNotUsed]", "[Spacing]", From 4b00f86f3a7e9581f1479856050b1a71dfd819d0 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Fri, 2 Jul 2021 15:47:58 +0300 Subject: [PATCH 48/73] Add `--with-dependencies` option Add `--with-dependencies` option for the develop command. When this option is given the dependencies of the packages for which the develop command is being executed are also being put in develop mode. Additionally this commit also: - Removes the restriction for putting in develop mode binary only packages. - The download directory names for the develop mode packages is determined always by the URL of the package and not by its name. This is in order to avoid discrepancies when the same package is being queried both by its name and by its URL from different places. Related to nim-lang/nimble#127 --- readme.markdown | 2 + src/nimble.nim | 215 +++++++++++++++++++++++++++---------- src/nimblepkg/download.nim | 4 +- src/nimblepkg/options.nim | 10 ++ tests/tdevelopfeature.nim | 19 +++- 5 files changed, 186 insertions(+), 64 deletions(-) diff --git a/readme.markdown b/readme.markdown index 343b055b..080762ed 100644 --- a/readme.markdown +++ b/readme.markdown @@ -287,6 +287,8 @@ file. one. * `-e, --exclude file` - Excludes a develop file from the current directory's one. +* `--with-dependencies` - Clones for develop also the dependencies of the +packages for which the develop command is executed. The options for manipulation of the develop files could be given only when executing `develop` command from some package's directory and they work only on diff --git a/src/nimble.nim b/src/nimble.nim index 00de1af9..a8c7c695 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -464,6 +464,7 @@ proc getDependency(name: string, dep: LockFileDep, options: Options): type DownloadInfo = ref object ## Information for a downloaded dependency needed for installation. + name: string dependency: LockFileDep url: string version: VersionRange @@ -477,23 +478,54 @@ type ## A list of `DownloadInfo` objects used for installing the downloaded ## dependencies. +proc developWithDependencies(options: Options): bool = + ## Determines whether the current executed action is a develop sub-command + ## with `--with-dependencies` flag. + options.action.typ == actionDevelop and options.action.withDependencies + +proc getDevelopDownloadDir(url, subdir: string, options: Options): string = + ## Returns the download dir for a develop mode dependency. + assert isURL(url), "The string \"{url}\" is not a URL." + + let downloadDirName = + if subdir.len == 0: + parseUri(url).path.splitFile.name + else: + subdir.splitFile.name + + result = + if options.action.path.isAbsolute: + options.action.path / downloadDirName + else: + getCurrentDir() / options.action.path / downloadDirName + +proc raiseCannotCloneInExistingDirException(downloadDir: string) = + let msg = "Cannot clone into '$1': directory exists." % downloadDir + const hint = "Remove the directory, or run this command somewhere else." + raise nimbleError(msg, hint) + proc downloadDependency(name: string, dep: LockFileDep, options: Options): Future[DownloadInfo] {.async.} = ## Asynchronously downloads a dependency from the lock file. - let depDirName = getDependencyDir(name, dep, options) - - if depDirName.dirExists: + if not options.developWithDependencies: + let depDirName = getDependencyDir(name, dep, options) + if depDirName.dirExists: promptRemoveEntirePackageDir(depDirName, options) removeDir(depDirName) let (url, metadata) = getUrlData(dep.url) let version = dep.version.parseVersionRange let subdir = metadata.getOrDefault("subdir") + let downloadPath = if options.developWithDependencies: + getDevelopDownloadDir(url, subdir, options) else: "" + + if dirExists(downloadPath): + raiseCannotCloneInExistingDirException(downloadPath) let (downloadDir, _, vcsRevision) = await downloadPkg( - url, version, dep.downloadMethod, subdir, options, - downloadPath = "", dep.vcsRevision) + url, version, dep.downloadMethod, subdir, options, downloadPath, + dep.vcsRevision) let downloadedPackageChecksum = calculateDirSha1Checksum(downloadDir) if downloadedPackageChecksum != dep.checksums.sha1: @@ -501,6 +533,7 @@ proc downloadDependency(name: string, dep: LockFileDep, options: Options): downloadedPackageChecksum, dep.checksums.sha1) result = DownloadInfo( + name: name, dependency: dep, url: url, version: version, @@ -538,6 +571,22 @@ proc startDownloadWorker(queue: DownloadQueue, options: Options, downloadResults[index] = await downloadDependency( download.name, download.dep, options) +proc lockedDepsDownload(dependenciesToDownload: DownloadQueue, + options: Options): DownloadResults = + ## By given queue with dependencies to download performs the downloads and + ## returns the result objects. + + result.new + result[].setLen(dependenciesToDownload[].len) + + var downloadWorkers: seq[Future[void]] + let workersCount = min( + options.maxParallelDownloads, dependenciesToDownload[].len) + for i in 0 ..< workersCount: + downloadWorkers.add startDownloadWorker( + dependenciesToDownload, options, result) + waitFor all(downloadWorkers) + proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): HashSet[PackageInfo] = # Returns a hash set with `PackageInfo` of all packages from the lock file of @@ -559,18 +608,7 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): else: dependenciesToDownload[].add (name, dep) - var downloadResults: DownloadResults - downloadResults.new - downloadResults[].setLen(dependenciesToDownload[].len) - - var downloadWorkers: seq[Future[void]] - let workersCount = min( - options.maxParallelDownloads, dependenciesToDownload[].len) - for i in 0 ..< workersCount: - downloadWorkers.add startDownloadWorker( - dependenciesToDownload, options, downloadResults) - waitFor all(downloadWorkers) - + let downloadResults = lockedDepsDownload(dependenciesToDownload, options) for downloadResult in downloadResults[]: result.incl installDependency(pkgInfo, downloadResult, options) @@ -1105,7 +1143,12 @@ proc listTasks(options: Options) = let nimbleFile = findNimbleFile(getCurrentDir(), true) nimscriptwrapper.listTasks(nimbleFile, options) -proc developFromDir(pkgInfo: PackageInfo, options: Options) = +proc developAllDependencies(pkgInfo: PackageInfo, options: var Options) + +proc developFromDir(pkgInfo: PackageInfo, options: var Options) = + assert options.action.typ == actionDevelop, + "This procedure should be called only when executing develop sub-command." + let dir = pkgInfo.getNimbleFileDir() if options.depsOnly: @@ -1116,9 +1159,6 @@ proc developFromDir(pkgInfo: PackageInfo, options: Options) = raise nimbleError("Pre-hook prevented further execution.") if pkgInfo.bin.len > 0: - if "nim" in pkgInfo.skipExt: - raise nimbleError("Cannot develop packages that are binaries only.") - displayWarning( "This package's binaries will not be compiled for development.") @@ -1129,10 +1169,16 @@ proc developFromDir(pkgInfo: PackageInfo, options: Options) = optsCopy.startDir = dir createDir(optsCopy.getPkgsDir()) cd dir: - discard processAllDependencies(pkgInfo, optsCopy) + if options.action.withDependencies: + developAllDependencies(pkgInfo, optsCopy) + else: + discard processAllDependencies(pkgInfo, optsCopy) else: - # Dependencies need to be processed before the creation of the pkg dir. - discard processAllDependencies(pkgInfo, options) + if options.action.withDependencies: + developAllDependencies(pkgInfo, options) + else: + # Dependencies need to be processed before the creation of the pkg dir. + discard processAllDependencies(pkgInfo, options) displaySuccess(pkgSetupInDevModeMsg(pkgInfo.name, dir)) @@ -1140,29 +1186,14 @@ proc developFromDir(pkgInfo: PackageInfo, options: Options) = cd dir: discard execHook(options, actionDevelop, false) -proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = +proc installDevelopPackage(pkgTup: PkgTuple, options: var Options): + PackageInfo = let (meth, url, metadata) = getDownloadInfo(pkgTup, options, true) let subdir = metadata.getOrDefault("subdir") - - let name = - if isURL(pkgTup.name): - if subdir.len == 0: - parseUri(pkgTup.name).path.splitFile.name - else: - subdir.splitFile.name - else: - pkgTup.name - - let downloadDir = - if options.action.path.isAbsolute: - options.action.path / name - else: - getCurrentDir() / options.action.path / name + let downloadDir = getDevelopDownloadDir(url, subdir, options) if dirExists(downloadDir): - let msg = "Cannot clone into '$1': directory exists." % downloadDir - let hint = "Remove the directory, or run this command somewhere else." - raise nimbleError(msg, hint) + raiseCannotCloneInExistingDirException(downloadDir) # Download the HEAD and make sure the full history is downloaded. let ver = @@ -1171,8 +1202,6 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = else: pkgTup.ver - var options = options - options.forceFullClone = true discard waitFor downloadPkg(url, ver, meth, subdir, options, downloadDir, vcsRevision = notSetSha1Hash) @@ -1180,8 +1209,70 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: Options): string = var pkgInfo = getPkgInfo(pkgDir, options) developFromDir(pkgInfo, options) + options.action.devActions.add( + (datAdd, pkgInfo.getNimbleFileDir.normalizedPath)) + return pkgInfo + +proc developLockedDependencies(pkgInfo: PackageInfo, + alreadyDownloaded: var HashSet[string], options: var Options) = + ## Downloads for develop the dependencies from the lock file. + + var dependenciesToDownload: DownloadQueue + dependenciesToDownload.new + + for name, dep in pkgInfo.lockedDeps: + if dep.url.removeTrailingGitString notin alreadyDownloaded: + dependenciesToDownload[].add (name, dep) + + let downloadResults = lockedDepsDownload(dependenciesToDownload, options) + for downloadResult in downloadResults[]: + alreadyDownloaded.incl downloadResult.url.removeTrailingGitString + options.action.devActions.add( + (datAdd, downloadResult.downloadDir.normalizedPath)) + +proc check(alreadyDownloaded: HashSet[string], dep: PkgTuple, + options: Options): bool = + let (_, url, _) = getDownloadInfo(dep, options, false) + alreadyDownloaded.contains url.removeTrailingGitString + +proc developFreeDependencies(pkgInfo: PackageInfo, + alreadyDownloaded: var HashSet[string], + options: var Options) = + # Downloads for develop the dependencies of `pkgInfo` (including transitive + # ones) by recursively following the requires clauses in the Nimble files. + assert not pkgInfo.isMinimal, + "developFreeDependencies needs pkgInfo.requires" + + for dep in pkgInfo.requires: + if dep.name == "nimrod" or dep.name == "nim": + continue + + let resolvedDep = dep.resolveAlias(options) + var found = alreadyDownloaded.check(dep, options) + + if not found and resolvedDep.name != dep.name: + found = alreadyDownloaded.check(dep, options) + if found: + displayWarning(&"Develop package {dep.name} should be renamed to " & + resolvedDep.name) + + if found: + continue - return pkgInfo.getNimbleFileDir + let pkgInfo = installDevelopPackage(dep, options) + alreadyDownloaded.incl pkgInfo.url.removeTrailingGitString + +proc developAllDependencies(pkgInfo: PackageInfo, options: var Options) = + ## Puts all dependencies of `pkgInfo` (including transitive ones) in develop + ## mode by cloning their repositories. + + var alreadyDownloadedDependencies {.global.}: HashSet[string] + alreadyDownloadedDependencies.incl pkgInfo.url.removeTrailingGitString + + if pkgInfo.lockedDeps.len > 0: + pkgInfo.developLockedDependencies(alreadyDownloadedDependencies, options) + else: + pkgInfo.developFreeDependencies(alreadyDownloadedDependencies, options) proc updateSyncFile(dependentPkg: PackageInfo, options: Options) @@ -1194,26 +1285,31 @@ proc updatePathsFile(pkgInfo: PackageInfo, options: Options) = writeFile(nimblePathsFileName, pathsFileContent) displayInfo(&"\"{nimblePathsFileName}\" is {action}.") +proc hasDevActionsAllowedOnlyInPkgDir(options: Options): bool = + assert options.action.typ == actionDevelop, + "This function make sense only for the `develop` command." + for (action, _) in options.action.devActions: + if action != datNewFile: + return true + proc develop(options: var Options) = let hasPackages = options.action.packages.len > 0 hasPath = options.action.path.len > 0 hasDevActions = options.action.devActions.len > 0 - if not hasPackages and hasPath: + if not hasPackages and hasPath and not options.action.withDependencies: raise nimbleError(pathGivenButNoPkgsToDownloadMsg) var currentDirPkgInfo = initPackageInfo() hasError = false - hasDevActionsAllowedOnlyInPkgDir = options.action.devActions.filterIt( - it[0] != datNewFile).len > 0 try: # Check whether the current directory is a package directory. currentDirPkgInfo = getPkgInfo(getCurrentDir(), options) except CatchableError as error: - if hasDevActionsAllowedOnlyInPkgDir: + if options.hasDevActionsAllowedOnlyInPkgDir: raise nimbleError(developOptionsOutOfPkgDirectoryMsg, details = error) if currentDirPkgInfo.isLoaded and (not hasPackages) and (not hasDevActions): @@ -1222,24 +1318,19 @@ proc develop(options: var Options) = # Install each package. for pkgTup in options.action.packages: try: - let pkgPath = installDevelopPackage(pkgTup, options) - if currentDirPkgInfo.isLoaded: - options.action.devActions.add (datAdd, pkgPath.normalizedPath) - hasDevActionsAllowedOnlyInPkgDir = true + discard installDevelopPackage(pkgTup, options) except CatchableError as error: hasError = true displayError(&"Cannot install package \"{pkgTup}\" for develop.") displayDetails(error) - if hasDevActionsAllowedOnlyInPkgDir: + if currentDirPkgInfo.isLoaded and options.hasDevActionsAllowedOnlyInPkgDir: hasError = not updateDevelopFile(currentDirPkgInfo, options) or hasError - else: - hasError = not executeDevActionsAllowedOutsidePkgDir(options) or hasError - - if hasDevActionsAllowedOnlyInPkgDir: updateSyncFile(currentDirPkgInfo, options) if fileExists(nimblePathsFileName): updatePathsFile(currentDirPkgInfo, options) + else: + hasError = not executeDevActionsAllowedOutsidePkgDir(options) or hasError if hasError: raise nimbleError( @@ -1321,6 +1412,12 @@ proc validateDevelopDependenciesVersionRanges(dependentPkg: PackageInfo, var errors: seq[string] for pkg in allPackages: for dep in pkg.requires: + if dep.ver.kind == verSpecial: + # Develop packages versions are not being validated against the special + # versions in the Nimble files requires clauses, because there is no + # special versions for develop mode packages. If special version is + # required then any version for the develop package is allowed. + continue var depPkg = initPackageInfo() if not findPkg(developDependencies, dep, depPkg): # This dependency is not part of the develop mode dependencies. diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 4806d5c0..0bd3532e 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -11,7 +11,7 @@ import packageinfotypes, packageparser, version, tools, common, options, cli, sha1hashes, vcstools type - DownloadPkgResult = tuple + DownloadPkgResult* = tuple dir: string version: Version vcsRevision: Sha1HashRef @@ -192,7 +192,7 @@ proc downloadTarball(url: string, options: Options): bool = not options.noTarballs and url.isGitHubRepo -proc removeTrailingGitString(url: string): string = +proc removeTrailingGitString*(url: string): string = ## Removes ".git" from an URL. ## ## For example: diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 8b308b5a..90123153 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -70,6 +70,9 @@ type passNimFlags*: seq[string] devActions*: seq[DevelopAction] path*: string + withDependencies*: bool + ## Whether to put in develop mode also the dependencies of the packages + ## listed in the develop command. of actionSearch: search*: seq[string] # Search string. of actionInit, actionDump: @@ -101,6 +104,8 @@ Commands: executed in a package directory creates a `nimble.develop` file with paths to the cloned packages. + [--with-dependencies] Puts in develop mode also the dependencies + of the packages in the list. [-p, --path path] Specifies the path whether the packages should be cloned. [-c, --create [path]] Creates an empty develop file with name @@ -307,6 +312,9 @@ proc setNimbleDir*(options: var Options) = nimbleDir = options.config.nimbleDir propagate = false + if options.action.typ == actionDevelop: + options.forceFullClone = true + if (options.localdeps and options.action.typ == actionDevelop and options.action.packages.len != 0): # Localdeps + nimble develop pkg1 ... @@ -545,6 +553,8 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = result.action.path = val.normalizedPath else: raise nimbleError(multiplePathOptionsGivenMsg) + of "with-dependencies": + result.action.withDependencies = true else: wasFlagHandled = false of actionSync: diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index f32dcb05..6c03dcfb 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -56,6 +56,18 @@ suite "develop feature": check lines.inLinesOrdered( pkgSetupInDevModeMsg(pkgBName, installDir / pkgBName)) + test "can develop with dependencies": + cdCleanDir installDir: + usePackageListFile &"../develop/{pkgListFileName}": + let (output, exitCode) = execNimble( + "develop", "--with-dependencies", pkgBName) + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered( + pkgSetupInDevModeMsg(pkgAName, installDir / pkgAName)) + check lines.inLinesOrdered( + pkgSetupInDevModeMsg(pkgBName, installDir / pkgBName)) + test "can develop list of packages": cdCleanDir installDir: usePackageListFile &"../develop/{pkgListFileName}": @@ -80,11 +92,12 @@ suite "develop feature": cannotUninstallPkgMsg(pkgAName, newVersion("0.2.0"), @[installDir / pkgBName])) - test "can reject binary packages": + test "can develop binary packages": cd "develop/binary": let (output, exitCode) = execNimble("develop") - check output.processOutput.inLines("cannot develop packages") - check exitCode == QuitFailure + check exitCode == QuitSuccess + check output.processOutput.inLines( + pkgSetupInDevModeMsg("binary", getCurrentDir())) test "can develop hybrid": cd &"develop/{pkgHybridName}": From b1d2113758d815b40c56870d59d3eb6dabb09f9c Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 4 Jul 2021 05:40:44 +0300 Subject: [PATCH 49/73] Use `join` instead `foldl` Use `join` procedure instead of `foldl` procedure for strings joining. Related to nim-lang/nimble#127 --- src/nimblepkg/displaymessages.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 2ea4770a..10c19453 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -5,7 +5,7 @@ ## error messages in order to facilitate testing by removing the requirement ## the message to be repeated both in Nimble and the testing code. -import strformat, sequtils +import strformat, strutils import version const @@ -105,13 +105,13 @@ proc cannotUninstallPkgMsg*(pkgName: string, pkgVersion: Version, deps: seq[string]): string = assert deps.len > 0, "The sequence must have at least one package." result = &"Cannot uninstall {pkgName} ({pkgVersion}) because\n" - result &= deps.foldl(a & "\n" & b) + result &= deps.join("\n") result &= "\ndepend" & (if deps.len == 1: "s" else: "") & " on it" proc promptRemovePkgsMsg*(pkgs: seq[string]): string = assert pkgs.len > 0, "The sequence must have at least one package." result = "The following packages will be removed:\n" - result &= pkgs.foldl(a & "\n" & b) + result &= pkgs.join("\n") result &= "\nDo you wish to continue?" proc pkgWorkingCopyNeedsSyncingMsg*(pkgName, pkgPath: string): string = From 5634fa8b3859cf4b429cc700c4cb92092736a33c Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Thu, 8 Jul 2021 00:38:14 +0300 Subject: [PATCH 50/73] Implement `--develop-file` option Now to the `develop` command can be given `--develop-file` option which can specify different than the default `nimble.develop` file to be manipulated. The option is useful for creating free not bound to a specific package develop files, intended for inclusion in other develop files. Related to nim-lang/nimble#127 --- readme.markdown | 8 +- src/nimble.nim | 40 +++--- src/nimblepkg/developfile.nim | 87 ++++--------- src/nimblepkg/displaymessages.nim | 69 +++++----- src/nimblepkg/options.nim | 56 ++++---- tests/tdevelopfeature.nim | 209 ++++++++++++++++-------------- 6 files changed, 237 insertions(+), 232 deletions(-) diff --git a/readme.markdown b/readme.markdown index 080762ed..417de605 100644 --- a/readme.markdown +++ b/readme.markdown @@ -289,11 +289,13 @@ one. one. * `--with-dependencies` - Clones for develop also the dependencies of the packages for which the develop command is executed. +* `--develop-file` - Changes the name of the develop file which to be +manipulated. It is useful for creating a free develop file which is not +associated with any project intended for inclusion in some other develop file. The options for manipulation of the develop files could be given only when -executing `develop` command from some package's directory and they work only on -the project's develop file named `nimble.develop` and not on free develop files -intended only for inclusion. +executing `develop` command from some package's directory unless +`--develop-file` option with a name of develop file is explicitly given. Because the develop files are user-specific and they contain local file system paths they **MUST NOT** be committed. diff --git a/src/nimble.nim b/src/nimble.nim index a8c7c695..731e9ccb 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -1285,21 +1285,13 @@ proc updatePathsFile(pkgInfo: PackageInfo, options: Options) = writeFile(nimblePathsFileName, pathsFileContent) displayInfo(&"\"{nimblePathsFileName}\" is {action}.") -proc hasDevActionsAllowedOnlyInPkgDir(options: Options): bool = - assert options.action.typ == actionDevelop, - "This function make sense only for the `develop` command." - for (action, _) in options.action.devActions: - if action != datNewFile: - return true - proc develop(options: var Options) = let hasPackages = options.action.packages.len > 0 hasPath = options.action.path.len > 0 hasDevActions = options.action.devActions.len > 0 - - if not hasPackages and hasPath and not options.action.withDependencies: - raise nimbleError(pathGivenButNoPkgsToDownloadMsg) + hasDevFile = options.action.developFile.len > 0 + withDependencies = options.action.withDependencies var currentDirPkgInfo = initPackageInfo() @@ -1309,8 +1301,15 @@ proc develop(options: var Options) = # Check whether the current directory is a package directory. currentDirPkgInfo = getPkgInfo(getCurrentDir(), options) except CatchableError as error: - if options.hasDevActionsAllowedOnlyInPkgDir: - raise nimbleError(developOptionsOutOfPkgDirectoryMsg, details = error) + if hasDevActions and not hasDevFile: + raise nimbleError(developOptionsWithoutDevelopFileMsg, details = error) + + if withDependencies and not hasPackages and not currentDirPkgInfo.isLoaded: + raise nimbleError(developWithDependenciesWithoutPackagesMsg) + + if hasPath and not hasPackages and + (not currentDirPkgInfo.isLoaded or not withDependencies): + raise nimbleError(pathGivenButNoPkgsToDownloadMsg) if currentDirPkgInfo.isLoaded and (not hasPackages) and (not hasDevActions): developFromDir(currentDirPkgInfo, options) @@ -1324,13 +1323,18 @@ proc develop(options: var Options) = displayError(&"Cannot install package \"{pkgTup}\" for develop.") displayDetails(error) - if currentDirPkgInfo.isLoaded and options.hasDevActionsAllowedOnlyInPkgDir: + if currentDirPkgInfo.isLoaded and not hasDevFile: + options.action.developFile = developFileName + + if options.action.developFile.len > 0: hasError = not updateDevelopFile(currentDirPkgInfo, options) or hasError - updateSyncFile(currentDirPkgInfo, options) - if fileExists(nimblePathsFileName): - updatePathsFile(currentDirPkgInfo, options) - else: - hasError = not executeDevActionsAllowedOutsidePkgDir(options) or hasError + if currentDirPkgInfo.isLoaded and + options.action.developFile == developFileName: + # If we are updated package's develop file we have to update also + # sync and paths files. + updateSyncFile(currentDirPkgInfo, options) + if fileExists(nimblePathsFileName): + updatePathsFile(currentDirPkgInfo, options) if hasError: raise nimbleError( diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index 2617cae3..f6330bf5 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -120,12 +120,6 @@ proc getPkgDevFilePath(pkg: PackageInfo): Path = ## Returns the path to the develop file associated with the package `pkg`. pkg.getNimbleFilePath / developFileName -proc getDependentPkgDevFilePath(data: DevelopFileData): Path = - ## Returns the path to the develop file of the dependent package associated - ## with `data`. - data.assertHasDependentPkg - data.dependentPkg.getPkgDevFilePath - proc isEmpty*(data: DevelopFileData): bool = ## Checks whether there is some content (paths to packages directories or ## includes to other develop files) in the develop file. @@ -152,14 +146,7 @@ proc save*(data: DevelopFileData, path: Path, writeEmpty, overwrite: bool) = raise nimbleError(fileAlreadyExistsMsg($path)) writeFile(path, json.pretty) - -template save(data: DevelopFileData, args: varargs[untyped]) = - ## Saves the `data` to a JSON file in in the directory of `data`'s - ## `dependentPkg` Nimble file. Delegates the functionality to the `save` - ## procedure taking path to develop file. - data.assertHasDependentPkg - let fileName = data.getDependentPkgDevFilePath - data.save(fileName, args) + displaySuccess(developFileSavedMsg($path), priority = DebugPriority) proc developFileExists*(dir: Path): bool = ## Returns `true` if there is a Nimble develop file with a default name in @@ -402,7 +389,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, continue result.mergeIncludedDevFileData(inclDevFileData, errors) - if result.dependentPkg.isLoaded: + if result.dependentPkg.isLoaded and path.splitPath.tail == developFileName: # If this is a package develop file, but not a free one, for each of the # package's develop mode dependencies load its develop file if it is not # already loaded and merge its data to the current develop file's data. @@ -443,7 +430,8 @@ proc addDevelopPackage(data: var DevelopFileData, pkg: PackageInfo): bool = # `pkg.name` at different path. if data.nameToPkg.hasKey(pkg.name) and not data.pathToPkg.hasKey(pkgDir): let otherPath = data.nameToPkg[pkg.name][].getNimbleFilePath() - displayError(pkgAlreadyPresentAtDifferentPathMsg(pkg.name, $otherPath)) + displayError(pkgAlreadyPresentAtDifferentPathMsg( + pkg.name, $otherPath, $data.path)) return false # Add `pkg` to the develop file model. @@ -455,9 +443,11 @@ proc addDevelopPackage(data: var DevelopFileData, pkg: PackageInfo): bool = "path different than already existing one." if success: - displaySuccess(pkgAddedInDevModeMsg(pkg.getNameAndVersion, $pkgDir)) + displaySuccess(pkgAddedInDevFileMsg( + pkg.getNameAndVersion, $pkgDir, $data.path)) else: - displayWarning(pkgAlreadyInDevModeMsg(pkg.getNameAndVersion, $pkgDir)) + displayWarning(pkgAlreadyInDevFileMsg( + pkg.getNameAndVersion, $pkgDir, $data.path)) return true @@ -536,9 +526,9 @@ proc removeDevelopPackageByPath(data: var DevelopFileData, path: Path): bool = if success: let nameAndVersion = data.pathToPkg[path][].getNameAndVersion() data.removePackage(path, data.path) - displaySuccess(pkgRemovedFromDevModeMsg(nameAndVersion, $path)) + displaySuccess(pkgRemovedFromDevFileMsg(nameAndVersion, $path, $data.path)) else: - displayWarning(pkgPathNotInDevFileMsg($path)) + displayWarning(pkgPathNotInDevFileMsg($path, $data.path)) return success @@ -557,9 +547,10 @@ proc removeDevelopPackageByName(data: var DevelopFileData, name: string): bool = if success: data.removePackage(path, data.path) - displaySuccess(pkgRemovedFromDevModeMsg(pkg[].getNameAndVersion, $path)) + displaySuccess(pkgRemovedFromDevFileMsg( + pkg[].getNameAndVersion, $path, $data.path)) else: - displayWarning(pkgNameNotInDevFileMsg(name)) + displayWarning(pkgNameNotInDevFileMsg(name, $data.path)) return success @@ -601,9 +592,9 @@ proc includeDevelopFile(data: var DevelopFileData, path: Path, data.removePackage(pkgPath, path) return false - displaySuccess(inclInDevFileMsg($path)) + displaySuccess(inclInDevFileMsg($path, $data.path)) else: - displayWarning(alreadyInclInDevFileMsg($path)) + displayWarning(alreadyInclInDevFileMsg($path, $data.path)) return true @@ -630,28 +621,12 @@ proc excludeDevelopFile(data: var DevelopFileData, path: Path): bool = for pkg in packages: data.removePackage(pkg, path) - displaySuccess(exclFromDevFileMsg($path)) + displaySuccess(exclFromDevFileMsg($path, $data.path)) else: - displayWarning(notInclInDevFileMsg($path)) + displayWarning(notInclInDevFileMsg($path, $data.path)) return success -proc createEmptyDevelopFile(path: Path, options: Options): bool = - ## Creates an empty develop file at given path `path` or with a default name - ## in the current directory if there is no path given. - - let filePath = if path.len == 0: Path(developFileName) else: path - - try: - var data = initDevelopFileData() - data.save(filePath, writeEmpty = true, overwrite = false) - except CatchableError as error: - displayError(error) - return false - - displaySuccess(emptyDevFileCreatedMsg($filePath)) - return true - proc assertDevelopActionIsSet(options: Options) = ## Asserts that the currently set action in the `options` object is `develop`. assert options.action.typ == actionDevelop, @@ -671,20 +646,22 @@ proc updateDevelopFile*(dependentPkg: PackageInfo, options: Options): bool = ## Raises if cannot load an existing develop file. options.assertDevelopActionIsSet - dependentPkg.assertIsLoaded + + let developFile = options.action.developFile var hasError = false hasSuccessfulRemoves = false - data = load(dependentPkg, options, true, true) + data = load(developFile, dependentPkg, options, true, true) defer: - data.save(writeEmpty = hasSuccessfulRemoves, overwrite = true) + let writeEmpty = hasSuccessfulRemoves or + developFile != developFileName or + not dependentPkg.isLoaded + data.save(developFile, writeEmpty = writeEmpty, overwrite = true) for (actionType, argument) in options.action.devActions: case actionType - of datNewFile: - hasError = not createEmptyDevelopFile(argument, options) or hasError of datAdd: hasError = not data.addDevelopPackage(argument, options) or hasError of datRemoveByPath: @@ -701,22 +678,6 @@ proc updateDevelopFile*(dependentPkg: PackageInfo, options: Options): bool = return not hasError -proc executeDevActionsAllowedOutsidePkgDir*(options: Options): bool = - ## Executes develop command sub-commands allowed outside a valid package - ## directory. Currently this is only `--create, -c` option for creating an - ## empty develop file. - - options.assertDevelopActionIsSet - - var hasError = false - for (actionType, argument) in options.action.devActions: - case actionType - of datNewFile: - hasError = not createEmptyDevelopFile(argument, options) or hasError - else: - discard - return not hasError - proc processDevelopDependencies*(dependentPkg: PackageInfo, options: Options): seq[PackageInfo] = ## Returns a sequence with the develop mode dependencies of the `dependentPkg` diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 10c19453..013b7a30 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -14,9 +14,12 @@ const pathGivenButNoPkgsToDownloadMsg* = "Path option is given but there are no given packages for download." - developOptionsOutOfPkgDirectoryMsg* = + developOptionsWithoutDevelopFileMsg* = "Options 'add', 'remove', 'include' and 'exclude' cannot be given " & - "when develop is being executed out of a valid package directory." + "when no develop file is specified." + + developWithDependenciesWithoutPackagesMsg* = + "Option 'with-dependencies' is given without packages for develop." dependencyNotInRangeErrorHint* = "Update the version of the dependency package in its Nimble file or " & @@ -28,6 +31,9 @@ const multiplePathOptionsGivenMsg* = "Multiple path options are given." + multipleDevelopFileOptionsGivenMsg* = + "Multiple develop file options are given." + updatingTheLockFileMsg* = "Updating the lock file..." generatingTheLockFileMsg* = "Generating the lock file..." lockFileIsUpdatedMsg* = "The lock file is updated." @@ -36,8 +42,8 @@ const proc fileAlreadyExistsMsg*(path: string): string = &"Cannot create file \"{path}\" because it already exists." -proc emptyDevFileCreatedMsg*(path: string): string = - &"An empty develop file \"{path}\" has been created." +proc developFileSavedMsg*(path: string): string = + &"The develop file \"{path}\" has been saved." proc pkgSetupInDevModeMsg*(pkgName, pkgPath: string): string = &"\"{pkgName}\" set up in develop mode successfully to \"{pkgPath}\"." @@ -59,44 +65,47 @@ proc invalidDevFileMsg*(path: string): string = proc notAValidDevFileJsonMsg*(devFilePath: string): string = &"The file \"{devFilePath}\" has not a valid develop file JSON schema." -proc pkgAlreadyPresentAtDifferentPathMsg*(pkgName, otherPath: string): string = +proc pkgAlreadyPresentAtDifferentPathMsg*( + pkgName, otherPath, fileName: string): string = &"A package with a name \"{pkgName}\" at different path \"{otherPath}\" " & - "is already present in the develop file." + "is already present in the develop file \"{fileName}\"." -proc pkgAddedInDevModeMsg*(pkg, path: string): string = - &"The package \"{pkg}\" at path \"{path}\" is added as a develop mode " & - "dependency." +proc pkgAddedInDevFileMsg*(pkg, path, fileName: string): string = + &"The package \"{pkg}\" at path \"{path}\" is added to the develop file " & + &"\"{fileName}\"." -proc pkgAlreadyInDevModeMsg*(pkg, path: string): string = - &"The package \"{pkg}\" at path \"{path}\" is already in develop mode." +proc pkgAlreadyInDevFileMsg*(pkg, path, fileName: string): string = + &"The package \"{pkg}\" at path \"{path}\" is already present in the " & + &"develop file \"{fileName}\"." -proc pkgRemovedFromDevModeMsg*(pkg, path: string): string = - &"The package \"{pkg}\" at path \"{path}\" is removed from the develop file." +proc pkgRemovedFromDevFileMsg*(pkg, path, fileName: string): string = + &"The package \"{pkg}\" at path \"{path}\" is removed from the develop " & + &"file \"{fileName}\"." -proc pkgPathNotInDevFileMsg*(path: string): string = - &"The path \"{path}\" is not in the develop file." +proc pkgPathNotInDevFileMsg*(path, fileName: string): string = + &"The path \"{path}\" is not in the develop file \"{fileName}\"." -proc pkgNameNotInDevFileMsg*(pkgName: string): string = - &"A package with name \"{pkgName}\" is not in the develop file." +proc pkgNameNotInDevFileMsg*(pkgName, fileName: string): string = + &"A package with name \"{pkgName}\" is not in the develop file " & + &"\"{fileName}\"." proc failedToInclInDevFileMsg*(inclFile, devFile: string): string = - &"Failed to include \"{inclFile}\" to \"{devFile}\"" + &"Failed to include \"{inclFile}\" to the develop file \"{devFile}\"" -proc inclInDevFileMsg*(path: string): string = - &"The develop file \"{path}\" is successfully included into the current " & - "project's develop file." +proc inclInDevFileMsg*(path, fileName: string): string = + &"The develop file \"{path}\" is successfully included into the develop " & + &"file \"{fileName}\"" -proc alreadyInclInDevFileMsg*(path: string): string = - &"The develop file \"{path}\" is already included in the current project's " & - "develop file." +proc alreadyInclInDevFileMsg*(path, fileName: string): string = + &"The develop file \"{path}\" is already included in the develop file " & + &"\"{fileName}\"." -proc exclFromDevFileMsg*(path: string): string = - &"The develop file \"{path}\" is successfully excluded from the current " & - "project's develop file." +proc exclFromDevFileMsg*(path, fileName: string): string = + &"The develop file \"{path}\" is successfully excluded from the develop " & + &"file \"{fileName}\"." -proc notInclInDevFileMsg*(path: string): string = - &"The develop file \"{path}\" is not included in the current project's " & - "develop file." +proc notInclInDevFileMsg*(path, fileName: string): string = + &"The file \"{path}\" is not included in the develop file \"{fileName}\"." proc failedToLoadFileMsg*(path: string): string = &"Failed to load \"{path}\"." diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 90123153..17fb1ad7 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -52,7 +52,7 @@ type actionDevelop, actionCheck, actionLock, actionRun, actionSync, actionSetup DevelopActionType* = enum - datNewFile, datAdd, datRemoveByPath, datRemoveByName, datInclude, datExclude + datAdd, datRemoveByPath, datRemoveByName, datInclude, datExclude DevelopAction* = tuple[actionType: DevelopActionType, argument: string] @@ -73,6 +73,7 @@ type withDependencies*: bool ## Whether to put in develop mode also the dependencies of the packages ## listed in the develop command. + developFile*: string of actionSearch: search*: seq[string] # Search string. of actionInit, actionDump: @@ -100,28 +101,34 @@ Commands: install [pkgname, ...] Installs a list of packages. [-d, --depsOnly] Install only dependencies. [-p, --passNim] Forward specified flag to compiler. - develop [pkgname, ...] Clones a list of packages for development. If - executed in a package directory creates a - `nimble.develop` file with paths to the cloned - packages. + develop [pkgname, ...] Clones a list of packages for development. + Adds them to a develop file if specified or + to `nimble.develop` if not specified and + executed in package's directory. [--with-dependencies] Puts in develop mode also the dependencies - of the packages in the list. + of the packages in the list or of the current + directory package if the list is + [--develop-file] Specifies the name of the develop file which + to be manipulated. If not present creates it. [-p, --path path] Specifies the path whether the packages should be cloned. - [-c, --create [path]] Creates an empty develop file with name - `nimble.develop` in the current directory or - if path is present to the given directory with - a given name. - [-a, --add path] Adds a package at given path to the - `nimble.develop` file. - [-r, --remove-path path] Removes a package at given path from the - `nimble.develop` file. + [-a, --add path] Adds a package at given path to a specified + develop file or to `nimble.develop` if not + specified and executed in package's directory. + [-r, --remove-path path] Removes a package at given path from a + specified develop file or from `nimble.develop` + if not specified and executed in package's + directory. [-n, --remove-name name] Removes a package with a given name from - the `nimble.develop` file. - [-i, --include file] Includes a develop file into the current - directory's one. - [-e, --exclude file] Excludes a develop file from the current - directory's one. + a specified develop file or from `nimble.develop` + if not specified and executed in package's + directory. + [-i, --include file] Includes a develop file into a specified + develop file or to `nimble.develop` if not + specified and executed in package's directory. + [-e, --exclude file] Excludes a develop file from a specified + develop file or from `nimble.develop` if not + specified and executed in package's directory. check Verifies the validity of a package in the current working directory. init [pkgname] Initializes a new Nimble project in the @@ -130,7 +137,7 @@ Commands: [--git, --hg] Creates a git/hg repo in the new nimble project. publish Publishes a package on nim-lang/packages. The current working directory needs to be the - toplevel directory of the Nimble package. + top level directory of the Nimble package. uninstall [pkgname, ...] Uninstalls a list of packages. [-i, --inclDeps] Uninstalls package and dependent package(s). build [opts, ...] [bin] Builds a package. Passes options to the Nim @@ -536,8 +543,6 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = result.action.custRunFlags.add(getFlagString(kind, flag, val)) of actionDevelop: case f - of "c", "create": - result.action.devActions.add (datNewFile, val.normalizedPath) of "a", "add": result.action.devActions.add (datAdd, val.normalizedPath) of "r", "remove-path": @@ -555,7 +560,12 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = raise nimbleError(multiplePathOptionsGivenMsg) of "with-dependencies": result.action.withDependencies = true - else: + of "develop-file": + if result.action.developFile.len == 0: + result.action.developFile = val.normalizedPath + else: + raise nimbleError(multipleDevelopFileOptionsGivenMsg) + else: wasFlagHandled = false of actionSync: case f diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index 6c03dcfb..274418e5 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -152,7 +152,7 @@ suite "develop feature": cleanFile developFileName let (output, exitCode) = execNimble("develop", "-a:./develop/dependency/") check exitCode == QuitFailure - check output.processOutput.inLines(developOptionsOutOfPkgDirectoryMsg) + check output.processOutput.inLines(developOptionsWithoutDevelopFileMsg) test "cannot load invalid develop file": cd dependentPkgPath: @@ -179,8 +179,8 @@ suite "develop feature": check parseFile(developFileName) == parseJson(developFileContent) var lines = output.processOutput check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + &"{pkgAName}@0.6.0", pkgAAbsPath, developFileName)) test "can add not a dependency downloaded package to the develop file": cleanDir installDir @@ -198,10 +198,10 @@ suite "develop feature": var lines = output.processOutput check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAAbsPath)) check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgBName, pkgBAbsPath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(&"{pkgAName}@0.6.0", pkgAAbsPath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(&"{pkgBName}@0.2.0", pkgBAbsPath)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + &"{pkgAName}@0.6.0", pkgAAbsPath, developFileName)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + &"{pkgBName}@0.2.0", pkgBAbsPath, developFileName)) test "add package to develop file": cleanDir installDir @@ -211,8 +211,8 @@ suite "develop feature": var (output, exitCode) = execNimble("develop", &"-a:{depPath}") check exitCode == QuitSuccess check developFileName.fileExists - check output.processOutput.inLines( - pkgAddedInDevModeMsg(depNameAndVersion, depPath)) + check output.processOutput.inLines(pkgAddedInDevFileMsg( + depNameAndVersion, depPath, developFileName)) const expectedDevelopFile = developFile(@[], @[depPath]) check parseFile(developFileName) == parseJson(expectedDevelopFile) (output, exitCode) = execNimble("run") @@ -226,8 +226,8 @@ suite "develop feature": writeFile(developFileName, developFileContent) let (output, exitCode) = execNimble("develop", &"-a:{depPath}") check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgAlreadyInDevModeMsg(depNameAndVersion, depPath)) + check output.processOutput.inLines(pkgAlreadyInDevFileMsg( + depNameAndVersion, depPath, developFileName)) check parseFile(developFileName) == parseJson(developFileContent) test "cannot add invalid package to develop file": @@ -247,8 +247,8 @@ suite "develop feature": let (output, exitCode) = execNimble("develop", &"-a:{srcDirTestPath}") check exitCode == QuitSuccess let lines = output.processOutput - check lines.inLines( - pkgAddedInDevModeMsg("srcdirtest@1.0", srcDirTestPath)) + check lines.inLines(pkgAddedInDevFileMsg( + "srcdirtest@1.0", srcDirTestPath, developFileName)) const developFileContent = developFile(@[], @[srcDirTestPath]) check parseFile(developFileName) == parseJson(developFileContent) @@ -259,8 +259,8 @@ suite "develop feature": writeFile(developFileName, developFileContent) let (output, exitCode) = execNimble("develop", &"-a:{dep2Path}") check exitCode == QuitFailure - check output.processOutput.inLines( - pkgAlreadyPresentAtDifferentPathMsg(depName, depPath.absolutePath)) + check output.processOutput.inLines(pkgAlreadyPresentAtDifferentPathMsg( + depName, depPath, developFileName)) check parseFile(developFileName) == parseJson(developFileContent) test "found two packages with the same name in the develop file": @@ -289,8 +289,8 @@ suite "develop feature": writeFile(developFileName, developFileContent) let (output, exitCode) = execNimble("develop", &"-r:{depPath}") check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check output.processOutput.inLines(pkgRemovedFromDevFileMsg( + depNameAndVersion, depPath, developFileName)) check parseFile(developFileName) == parseJson(emptyDevelopFileContent) test "warning on attempt to remove not existing package path": @@ -300,7 +300,8 @@ suite "develop feature": writeFile(developFileName, developFileContent) let (output, exitCode) = execNimble("develop", &"-r:{dep2Path}") check exitCode == QuitSuccess - check output.processOutput.inLines(pkgPathNotInDevFileMsg(dep2Path)) + check output.processOutput.inLines(pkgPathNotInDevFileMsg( + dep2Path, developFileName)) check parseFile(developFileName) == parseJson(developFileContent) test "remove package from develop file by name": @@ -310,8 +311,8 @@ suite "develop feature": writeFile(developFileName, developFileContent) let (output, exitCode) = execNimble("develop", &"-n:{depName}") check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath.absolutePath)) + check output.processOutput.inLines(pkgRemovedFromDevFileMsg( + depNameAndVersion, depPath, developFileName)) check parseFile(developFileName) == parseJson(emptyDevelopFileContent) test "warning on attempt to remove not existing package name": @@ -322,8 +323,8 @@ suite "develop feature": const notExistingPkgName = "dependency2" let (output, exitCode) = execNimble("develop", &"-n:{notExistingPkgName}") check exitCode == QuitSuccess - check output.processOutput.inLines( - pkgNameNotInDevFileMsg(notExistingPkgName)) + check output.processOutput.inLines(pkgNameNotInDevFileMsg( + notExistingPkgName, developFileName)) check parseFile(developFileName) == parseJson(developFileContent) test "include develop file": @@ -337,7 +338,8 @@ suite "develop feature": var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") check exitCode == QuitSuccess check developFileName.fileExists - check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) + check output.processOutput.inLines(inclInDevFileMsg( + includeFileName, developFileName)) const expectedDevelopFile = developFile(@[includeFileName], @[]) check parseFile(developFileName) == parseJson(expectedDevelopFile) (output, exitCode) = execNimble("run") @@ -354,8 +356,8 @@ suite "develop feature": let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") check exitCode == QuitSuccess - check output.processOutput.inLines( - alreadyInclInDevFileMsg(includeFileName)) + check output.processOutput.inLines(alreadyInclInDevFileMsg( + includeFileName, developFileName)) check parseFile(developFileName) == parseJson(developFileContent) test "cannot include invalid develop file": @@ -392,7 +394,8 @@ suite "develop feature": writeFile(includeFileName, fileContent) var (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") check exitCode == QuitSuccess - check output.processOutput.inLines(inclInDevFileMsg(includeFileName)) + check output.processOutput.inLines(inclInDevFileMsg( + includeFileName, developFileName)) const expectedFileContent = developFile( @[includeFileName], @[depPath]) check parseFile(developFileName) == parseJson(expectedFileContent) @@ -408,16 +411,14 @@ suite "develop feature": const includeFileContent = developFile(@[], @[dep2Path]) writeFile(includeFileName, includeFileContent) - let - (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") - developFilePath = getCurrentDir() / developFileName + let (output, exitCode) = execNimble("develop", &"-i:{includeFileName}") check exitCode == QuitFailure var lines = output.processOutput check lines.inLinesOrdered( - failedToInclInDevFileMsg(includeFileName, developFilePath)) + failedToInclInDevFileMsg(includeFileName, developFileName)) check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, - [(depPath.absolutePath.Path, developFilePath.Path), + [(depPath.Path, developFileName.Path), (dep2Path.Path, includeFileName.Path)].toHashSet)) check parseFile(developFileName) == parseJson(developFileContent) @@ -430,7 +431,8 @@ suite "develop feature": writeFile(includeFileName, includeFileContent) let (output, exitCode) = execNimble("develop", &"-e:{includeFileName}") check exitCode == QuitSuccess - check output.processOutput.inLines(exclFromDevFileMsg(includeFileName)) + check output.processOutput.inLines(exclFromDevFileMsg( + includeFileName, developFileName)) check parseFile(developFileName) == parseJson(emptyDevelopFileContent) test "warning on attempt to exclude not included develop file": @@ -442,8 +444,8 @@ suite "develop feature": writeFile(includeFileName, includeFileContent) let (output, exitCode) = execNimble("develop", &"-e:../{includeFileName}") check exitCode == QuitSuccess - check output.processOutput.inLines( - notInclInDevFileMsg((&"../{includeFileName}").normalizedPath)) + check output.processOutput.inLines(notInclInDevFileMsg( + (&"../{includeFileName}").normalizedPath, developFileName)) check parseFile(developFileName) == parseJson(developFileContent) test "relative paths in the develop file and absolute from the command line": @@ -463,9 +465,10 @@ suite "develop feature": check exitCode == QuitSuccess var lines = output.processOutput - check lines.inLinesOrdered(exclFromDevFileMsg(includeFileAbsolutePath)) - check lines.inLinesOrdered( - pkgRemovedFromDevModeMsg(depNameAndVersion, dependencyPkgAbsolutePath)) + check lines.inLinesOrdered(exclFromDevFileMsg( + includeFileAbsolutePath, developFileName)) + check lines.inLinesOrdered(pkgRemovedFromDevFileMsg( + depNameAndVersion, dependencyPkgAbsolutePath, developFileName)) check parseFile(developFileName) == parseJson(emptyDevelopFileContent) test "absolute paths in the develop file and relative from the command line": @@ -487,9 +490,10 @@ suite "develop feature": check exitCode == QuitSuccess var lines = output.processOutput - check lines.inLinesOrdered(exclFromDevFileMsg(includeFileName)) - check lines.inLinesOrdered( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) + check lines.inLinesOrdered(exclFromDevFileMsg( + includeFileName, developFileName)) + check lines.inLinesOrdered(pkgRemovedFromDevFileMsg( + depNameAndVersion, depPath, developFileName)) check parseFile(developFileName) == parseJson(emptyDevelopFileContent) test "uninstall package with develop reverse dependencies": @@ -795,44 +799,68 @@ suite "develop feature": [(pkg3Path, freeDevFile1Path), (pkg32Path, freeDevFile2Path)].toHashSet)) - test "create an empty develop file with default name in the current dir": - cd dependentPkgPath: - cleanFile developFileName - let (output, errorCode) = execNimble("develop", "-c") - check errorCode == QuitSuccess - check parseFile(developFileName) == parseJson(emptyDevelopFileContent) - check output.processOutput.inLines( - emptyDevFileCreatedMsg(developFileName)) - test "create an empty develop file in some dir": cleanDir installDir let filePath = installDir / "develop.json" cleanFile filePath createDir installDir - let (output, errorCode) = execNimble("develop", &"-c:{filePath}") + let (output, errorCode) = execNimble( + "--debug", "develop", &"--develop-file:{filePath}") check errorCode == QuitSuccess check parseFile(filePath) == parseJson(emptyDevelopFileContent) - check output.processOutput.inLines(emptyDevFileCreatedMsg(filePath)) + check output.processOutput.inLines(developFileSavedMsg(filePath)) - test "try to create an empty develop file with already existing name": - cd dependentPkgPath: - cleanFile developFileName - const developFileContent = developFile(@[], @[depPath]) - writeFile(developFileName, developFileContent) - let - filePath = getCurrentDir() / developFileName - (output, errorCode) = execNimble("develop", &"-c:{filePath}") - check errorCode == QuitFailure - check output.processOutput.inLines(fileAlreadyExistsMsg(filePath)) - check parseFile(developFileName) == parseJson(developFileContent) - - test "try to create an empty develop file in not existing dir": + test "try to create a develop file in not existing dir": let filePath = installDir / "some/not/existing/dir/develop.json" cleanFile filePath - let (output, errorCode) = execNimble("develop", &"-c:{filePath}") + let (output, errorCode) = execNimble( + "--debug", "develop", &"--develop-file:{filePath}") check errorCode == QuitFailure check output.processOutput.inLines(&"cannot open: {filePath}") + test "can manipulate a free develop file": + cleanDir installDir + cd dependentPkgPath: + usePackageListFile &"../{pkgListFileName}": + const + developFileName = "develop.json" + includeFileName = "include.json" + includeFileContent = developFile(@[], @[depPath]) + cleanFiles developFileName, includeFileName + writeFile(includeFileName, includeFileContent) + var (output, exitCode) = execNimble( + "develop", &"--develop-file:{developFileName}", + &"-a:{depPath}", &"-i:{includeFileName}") + check exitCode == QuitSuccess + var lines = output.processOutput + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + depNameAndVersion, depPath, developFileName)) + check lines.inLinesOrdered(inclInDevFileMsg( + includeFileName, developFileName)) + const expectedDevelopFile = developFile(@[includeFileName], @[depPath]) + check parseFile(developFileName) == parseJson(expectedDevelopFile) + + test "add develop --with-dependencies packages to free develop file": + cdCleanDir installDir: + const developFile = "develop.json" + usePackageListFile &"../develop/{pkgListFileName}": + let (output, exitCode) = execNimble("--debug", "develop", + "--with-dependencies", &"--develop-file:{developFile}", pkgBName) + check exitCode == QuitSuccess + let + pkgAPath = installDir / pkgAName + pkgBPath = installDir / pkgBName + var lines = output.processOutput + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAPath)) + check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgBName, pkgBPath)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + &"{pkgAName}@0.2.0", pkgAPath, developFile)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + &"{pkgBName}@0.2.0", pkgBPath, developFile)) + check lines.inLinesOrdered(developFileSavedMsg(developFile)) + let developFileContent = developFile(@[], @[pkgAPath, pkgBPath]) + check parseFile(developFile) == parseJson(developFileContent) + test "partial success when some operations in single command failed": cleanDir installDir cd dependentPkgPath: @@ -845,45 +873,36 @@ suite "develop feature": cleanFiles developFileName, includeFileName, dep2DevelopFilePath writeFile(includeFileName, includeFileContent) - let - developFilePath = getCurrentDir() / developFileName - (output, errorCode) = execNimble("develop", &"-p:{installDir}", - pkgAName, # success - "-c", # success - &"-a:{depPath}", # success - &"-a:{dep2Path}", # fail because of names collision - &"-i:{includeFileName}", # fail because of names collision - &"-n:{depName}", # success - &"-c:{developFilePath}", # fail because the file already exists - &"-a:{dep2Path}", # success - &"-i:{includeFileName}", # success - &"-i:{invalidInclFilePath}", # fail - &"-c:{dep2DevelopFilePath}") # success + let (output, errorCode) = execNimble("develop", &"-p:{installDir}", + pkgAName, # success + &"-a:{depPath}", # success + &"-a:{dep2Path}", # fail because of names collision + &"-i:{includeFileName}", # fail because of names collision + &"-n:{depName}", # success + &"-a:{dep2Path}", # success + &"-i:{includeFileName}", # success + &"-i:{invalidInclFilePath}") # fail check errorCode == QuitFailure var lines = output.processOutput check lines.inLinesOrdered(pkgSetupInDevModeMsg( pkgAName, installDir / pkgAName)) - check lines.inLinesOrdered(emptyDevFileCreatedMsg(developFileName)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(depNameAndVersion, depPath)) - check lines.inLinesOrdered( - pkgAlreadyPresentAtDifferentPathMsg(depName, depPath)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + depNameAndVersion, depPath, developFileName)) + check lines.inLinesOrdered(pkgAlreadyPresentAtDifferentPathMsg( + depName, depPath, developFileName)) check lines.inLinesOrdered( - failedToInclInDevFileMsg(includeFileName, developFilePath)) + failedToInclInDevFileMsg(includeFileName, developFileName)) check lines.inLinesOrdered(pkgFoundMoreThanOnceMsg(depName, - [(depPath.Path, developFilePath.Path), + [(depPath.Path, developFileName.Path), (dep2Path.Path, includeFileName.Path)].toHashSet)) - check lines.inLinesOrdered( - pkgRemovedFromDevModeMsg(depNameAndVersion, depPath)) - check lines.inLinesOrdered(fileAlreadyExistsMsg(developFilePath)) - check lines.inLinesOrdered( - pkgAddedInDevModeMsg(depNameAndVersion, dep2Path)) - check lines.inLinesOrdered(inclInDevFileMsg(includeFileName)) + check lines.inLinesOrdered(pkgRemovedFromDevFileMsg( + depNameAndVersion, depPath, developFileName)) + check lines.inLinesOrdered(pkgAddedInDevFileMsg( + depNameAndVersion, dep2Path, developFileName)) + check lines.inLinesOrdered(inclInDevFileMsg( + includeFileName, developFileName)) check lines.inLinesOrdered(failedToLoadFileMsg(invalidInclFilePath)) - check lines.inLinesOrdered(emptyDevFileCreatedMsg(dep2DevelopFilePath)) - check parseFile(dep2DevelopFilePath) == - parseJson(emptyDevelopFileContent) let expectedDevelopFileContent = developFile( @[includeFileName], @[dep2Path, &"{installDir}/{pkgAName}"]) check parseFile(developFileName) == From bd6e422f33bd6c1ef740d87f3a9cdf5616b10119 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 11 Jul 2021 21:00:35 +0300 Subject: [PATCH 51/73] Fix the build on Windows Fix the build on Windows by applying the latest changes from `asynctools` library. Related to nim-lang/nimble#127 --- src/nimblepkg/asynctools/asyncpipe.nim | 4 ++++ src/nimblepkg/asynctools/asyncproc.nim | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/nimblepkg/asynctools/asyncpipe.nim b/src/nimblepkg/asynctools/asyncpipe.nim index 5d759d80..d0389987 100644 --- a/src/nimblepkg/asynctools/asyncpipe.nim +++ b/src/nimblepkg/asynctools/asyncpipe.nim @@ -135,6 +135,10 @@ else: proc connectNamedPipe(hNamedPipe: Handle, lpOverlapped: pointer): WINBOOL {.importc: "ConnectNamedPipe", stdcall, dynlib: "kernel32".} + when not declared(PCustomOverlapped): + type + PCustomOverlapped = CustomRef + const pipeHeaderName = r"\\.\pipe\asyncpipe_" diff --git a/src/nimblepkg/asynctools/asyncproc.nim b/src/nimblepkg/asynctools/asyncproc.nim index 1fde87db..42cff1b7 100644 --- a/src/nimblepkg/asynctools/asyncproc.nim +++ b/src/nimblepkg/asynctools/asyncproc.nim @@ -24,6 +24,9 @@ else: const STILL_ACTIVE = 259 import posix +when defined(linux): + import linux + type ProcessOption* = enum ## options that can be passed `startProcess` poEchoCmd, ## echo the command before execution @@ -141,7 +144,7 @@ proc startProcess*(command: string, workingDir: string = "", pipeStdout: AsyncPipe = nil, pipeStderr: AsyncPipe = nil): AsyncProcess ## Starts a process. - ## and returns its exit code and output as a tuple + ## ## ``command`` is the executable file path ## ## ``workingDir`` is the process's working directory. If ``workingDir == ""`` @@ -384,7 +387,7 @@ when defined(windows): var tmp = newWideCString(cmdl) var ee = - if e.str.isNil: nil + if e.str.isNil: newWideCString(cstring(nil)) else: newWideCString(e.str, e.len) var wwd = newWideCString(wd) var flags = NORMAL_PRIORITY_CLASS or CREATE_UNICODE_ENVIRONMENT @@ -507,7 +510,7 @@ else: result = cast[cstringArray](alloc0((counter + 1) * sizeof(cstring))) var i = 0 for key, val in envPairs(): - var x = key & "=" & val + var x = key.string & "=" & val.string result[i] = cast[cstring](alloc(x.len+1)) copyMem(result[i], addr(x[0]), x.len+1) inc(i) From 596b9dc276ba5d42124005335c057662e9991bd0 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 11 Jul 2021 21:03:55 +0300 Subject: [PATCH 52/73] Fix a bug in the `asyncproc.execProcess` procedure There was a rece condition in the `asyncproc.execProcess` procedure. The future fow awaiting a process to finish should be created before the process actually finishes. Related to nim-lang/nimble#127 --- src/nimblepkg/asynctools/asyncproc.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nimblepkg/asynctools/asyncproc.nim b/src/nimblepkg/asynctools/asyncproc.nim index 42cff1b7..2cc26f11 100644 --- a/src/nimblepkg/asynctools/asyncproc.nim +++ b/src/nimblepkg/asynctools/asyncproc.nim @@ -889,6 +889,7 @@ proc execProcess(command: string, args: seq[string] = @[], var data = newString(bufferSize) var p = startProcess(command, args = args, env = env, options = options) + let future = p.waitForExit() while true: let res = await p.outputHandle.readInto(addr data[0], bufferSize) if res > 0: @@ -897,7 +898,7 @@ proc execProcess(command: string, args: seq[string] = @[], data.setLen(bufferSize) else: break - result.exitcode = await p.waitForExit() + result.exitcode = await future close(p) when isMainModule: From 8f68c5a1b029b092d0f5f24cac16296a655a56b0 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Tue, 13 Jul 2021 18:32:07 +0300 Subject: [PATCH 53/73] Fix command passing to `asyncproc.execProcess` `asyncproc.execProcess` procedure internally does its own quoting of the passed command and its arguments. As a result of this if the command and its argument are passed as one whole string two cases are possible. * The command to be passed to the procedure quoted in which case after the second quoting by `execProcess` of the entire line with its arguements the first quotes are being escaped for example: "C:\Program Files\Git\usr\bin\tar.exe" becomes "\"C:\Program Files\Git\usr\bin\tar.exe\" " which leads to the error: > The filename, directory name, or volume label syntax is incorrect. * The command to be passed to the procedure unquoted in which case after quoting of the command with its arguments for example: C:\Program Files\Git\usr\bin\tar.exe becomes "C:\Program Files\Git\usr\bin\tar.exe " which leads to the error: > '"C:\Program Files\Git\usr\bin\tar.exe "' is not recognized as an internal or external command, operable program or batch file. The only viable solution is the command and its arguments to be passed to `asyncproc.execProcess` separately which this commit does. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 73 +++++++++++++++++++------------------- src/nimblepkg/tools.nim | 14 +++++--- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 0bd3532e..a23f6d8e 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -2,10 +2,9 @@ # BSD License. Look at license.txt for more info. import parseutils, os, osproc, strutils, tables, pegs, uri, strformat, - httpclient, json, asyncdispatch + httpclient, json, asyncdispatch, sequtils from algorithm import SortOrder, sorted -from sequtils import toSeq, filterIt, map import packageinfotypes, packageparser, version, tools, common, options, cli, sha1hashes, vcstools @@ -23,28 +22,29 @@ proc doCheckout(meth: DownloadMethod, downloadDir, branch: string): # 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. - discard await tryDoCmdExAsync( - &"git -C {downloadDir} checkout --force {branch}") - discard await tryDoCmdExAsync( - &"git -C {downloadDir} submodule update --recursive --depth 1") + discard await tryDoCmdExAsync("git", + @["-C", downloadDir, "checkout", "--force", "branch"]) + discard await tryDoCmdExAsync("git", + @["-C", downloadDir, "submodule", "update", "--recursive", "--depth", "1"]) of DownloadMethod.hg: - discard await tryDoCmdExAsync(&"hg --cwd {downloadDir} checkout {branch}") + discard await tryDoCmdExAsync("hg", + @["--cwd", downloadDir, "checkout", "branch"]) proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "", onlyTip = true) {.async.} = case meth of DownloadMethod.git: let - depthArg = if onlyTip: "--depth 1" else: "" - branchArg = if branch == "": "" else: "-b " & branch - discard await tryDoCmdExAsync( - &"git clone --recursive {depthArg} {branchArg} {url} {downloadDir}") + depthArgs = if onlyTip: @["--depth", "1"] else: @[] + branchArgs = if branch == "": @[] else: @["-b", branch] + discard await tryDoCmdExAsync("git", concat(@["clone", "--recursive"], + depthArgs, branchArgs, @[url, downloadDir])) of DownloadMethod.hg: let - tipArg = if onlyTip: "-r tip" else: "" - branchArg = if branch == "": "" else: "-b " & branch - discard await tryDoCmdExAsync( - &"hg clone {tipArg} {branchArg} {url} {downloadDir}") + tipArgs = if onlyTip: @["-r", "tip"] else: @[] + branchArgs = if branch == "": @[] else: @["-b", branch] + discard await tryDoCmdExAsync("hg", + concat(@["clone"], tipArgs, branchArgs, @[url, downloadDir])) proc getTagsList(dir: string, meth: DownloadMethod): Future[seq[string]] {.async.} = @@ -52,9 +52,9 @@ proc getTagsList(dir: string, meth: DownloadMethod): cd dir: case meth of DownloadMethod.git: - output = await tryDoCmdExAsync("git tag") + output = await tryDoCmdExAsync("git", @["tag"]) of DownloadMethod.hg: - output = await tryDoCmdExAsync("hg tags") + output = await tryDoCmdExAsync("hg", @["tags"]) if output.len > 0: case meth of DownloadMethod.git: @@ -78,8 +78,8 @@ proc getTagsListRemote*(url: string, meth: DownloadMethod): result = @[] case meth of DownloadMethod.git: - var (output, exitCode) = await doCmdExAsync( - &"git ls-remote --tags {url.quoteShell}") + var (output, exitCode) = await doCmdExAsync("git", + @["ls-remote", "--tags", url]) if exitCode != QuitSuccess: raise nimbleError("Unable to query remote tags for " & url & ". Git returned: " & output) @@ -149,16 +149,15 @@ proc cloneSpecificRevision(downloadMethod: DownloadMethod, of DownloadMethod.git: let downloadDir = downloadDir.quoteShell createDir(downloadDir) - discard await tryDoCmdExAsync( - &"git -C {downloadDir} init") - discard await tryDoCmdExAsync( - &"git -C {downloadDir} remote add origin {url}") - discard await tryDoCmdExAsync( - &"git -C {downloadDir} fetch --depth 1 origin {vcsRevision}") - discard await tryDoCmdExAsync( - &"git -C {downloadDir} reset --hard FETCH_HEAD") + discard await tryDoCmdExAsync("git", @["-C", downloadDir, "init"]) + discard await tryDoCmdExAsync("git", + @["-C", downloadDir, "remote", "add", "origin", url]) + discard await tryDoCmdExAsync("git", + @["-C", downloadDir, "fetch", "--depth", "1", "origin", $vcsRevision]) + discard await tryDoCmdExAsync("git", + @["-C", downloadDir, "reset", "--hard", "FETCH_HEAD"]) of DownloadMethod.hg: - discard await tryDoCmdExAsync(&"hg clone {url} -r {vcsRevision}") + discard await tryDoCmdExAsync("hg", @["clone", url, "-r", $vcsRevision]) proc getTarExePath: string = ## Returns path to `tar` executable. @@ -269,7 +268,7 @@ proc parseRevision(lsRemoteOutput: string): Sha1HashRef = proc getRevision(url, version: string): Future[Sha1HashRef] {.async.} = ## Returns the commit hash corresponding to the given `version` of the package ## in repository at `url`. - let output = await tryDoCmdExAsync(&"git ls-remote {url} {version}") + let output = await tryDoCmdExAsync("git", @["ls-remote", url, $version]) result = parseRevision(output) if result[] == notSetSha1Hash: if version.seemsLikeRevision: @@ -278,15 +277,17 @@ proc getRevision(url, version: string): Future[Sha1HashRef] {.async.} = raise nimbleError(&"Cannot get revision for version \"{version}\" " & &"of package at \"{url}\".") -proc getTarCmdLine(downloadDir, filePath: string): string = - ## Returns an OS specific command line for extracting the downloaded tarball. +proc getTarCmdLine(downloadDir, filePath: string): + tuple[cmd: string, args: seq[string]] = + ## Returns an OS specific command and arguments for extracting the downloaded + ## tarball. when defined(Windows): let downloadDir = downloadDir.replace('\\', '/') let filePath = filePath.replace('\\', '/') - &"{getTarExePath()} -C {downloadDir} -xf {filePath} " & - "--strip-components 1 --force-local" + (getTarExePath(), @["-C", downloadDir, "-xf", filePath, + "--strip-components", "1", "--force-local"]) else: - &"tar -C {downloadDir} -xf {filePath} --strip-components 1" + ("tar", @["-C", downloadDir, "-xf", filePath, "--strip-components", "1"]) proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): Future[Sha1HashRef] {.async.} = @@ -305,8 +306,8 @@ proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): display("Completed", "saving " & filePath) display("Unpacking", filePath) - let cmd = getTarCmdLine(downloadDir, filePath) - let (output, exitCode) = await doCmdExAsync(cmd) + let (cmd, args) = getTarCmdLine(downloadDir, filePath) + let (output, exitCode) = await doCmdExAsync(cmd, args) if exitCode != QuitSuccess and not output.contains("Cannot create symlink to"): # If the command fails for reason different then unable establishing a # sym-link raise an exception. This reason for failure is common on Windows diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index a1286f6e..ebd9bd66 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -52,11 +52,16 @@ proc doCmdEx*(cmd: string): ProcessOutput = raise nimbleError("'" & bin & "' not in PATH.") return execCmdEx(cmd) -proc doCmdExAsync*(cmd: string): Future[ProcessOutput] {.async.} = +proc removeQuotes(cmd: string): string = + cmd.filterIt(it != '"').join + +proc doCmdExAsync*(cmd: string, args: seq[string] = @[]): + Future[ProcessOutput] {.async.} = let bin = extractBin(cmd) if findExe(bin) == "": raise nimbleError("'" & bin & "' not in PATH.") - let res = await asyncproc.execProcess(cmd) + let res = await asyncproc.execProcess(cmd.removeQuotes, args, + options = {asyncproc.poStdErrToStdOut, asyncproc.poUsePath}) return (res.output, res.exitCode) proc tryDoCmdExErrorMessage*(cmd, output: string, exitCode: int): string = @@ -69,8 +74,9 @@ proc tryDoCmdEx*(cmd: string): string {.discardable.} = raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) return output -proc tryDoCmdExAsync*(cmd: string): Future[string] {.async.} = - let (output, exitCode) = await doCmdExAsync(cmd) +proc tryDoCmdExAsync*(cmd: string, args: seq[string] = @[]): + Future[string] {.async.} = + let (output, exitCode) = await doCmdExAsync(cmd, args) if exitCode != QuitSuccess: raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) return output From eba091d519d856df1ae4d857cbc4d9fb5a2cb795 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 18 Jul 2021 22:38:23 +0300 Subject: [PATCH 54/73] Fix a bug in the `asyncpipe.createPipe` procedure In the `asyncpipe.createPipe` procedure the `OVERLAPPED` object passed to the `connectNamedPipe` and `getOverlappedResult` procedures, was created on the stack instead of on the heap. It must be created on the heap as a `PCustomOverlapped` object, the same way as the other places in the library. Related to nim-lang/nimble#127 --- src/nimblepkg/asynctools/asyncpipe.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nimblepkg/asynctools/asyncpipe.nim b/src/nimblepkg/asynctools/asyncpipe.nim index d0389987..ac2e8b9b 100644 --- a/src/nimblepkg/asynctools/asyncpipe.nim +++ b/src/nimblepkg/asynctools/asyncpipe.nim @@ -192,15 +192,15 @@ else: result = AsyncPipe(readPipe: pipeIn, writePipe: pipeOut) - var ovl = OVERLAPPED() - let res = connectNamedPipe(pipeIn, cast[pointer](addr ovl)) + var ovl = PCustomOverlapped() + let res = connectNamedPipe(pipeIn, cast[pointer](ovl)) if res == 0: let err = osLastError() if err.int32 == ERROR_PIPE_CONNECTED: discard elif err.int32 == ERROR_IO_PENDING: var bytesRead = 0.Dword - if getOverlappedResult(pipeIn, addr ovl, bytesRead, 1) == 0: + if getOverlappedResult(pipeIn, cast[POVERLAPPED](ovl), bytesRead, 1) == 0: let oerr = osLastError() discard closeHandle(pipeIn) discard closeHandle(pipeOut) From 5cee1e10e9ee976db97e9e1c8aed0b69c8c7c888 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 18 Jul 2021 22:46:30 +0300 Subject: [PATCH 55/73] Fix a bug in the develop feature tests There was a bug that some filesystem paths were always created with POSIX style directory separators. Now they are created in a platform-specific way. Related to nim-lang/nimble#127 --- tests/tdevelopfeature.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index 274418e5..e7af50fd 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -243,7 +243,7 @@ suite "develop feature": test "can add not a dependency to develop file": cd dependentPkgPath: cleanFile developFileName - const srcDirTestPath = "../srcdirtest" + const srcDirTestPath = "../srcdirtest".normalizedPath let (output, exitCode) = execNimble("develop", &"-a:{srcDirTestPath}") check exitCode == QuitSuccess let lines = output.processOutput @@ -904,6 +904,6 @@ suite "develop feature": includeFileName, developFileName)) check lines.inLinesOrdered(failedToLoadFileMsg(invalidInclFilePath)) let expectedDevelopFileContent = developFile( - @[includeFileName], @[dep2Path, &"{installDir}/{pkgAName}"]) + @[includeFileName], @[dep2Path, installDir / pkgAName]) check parseFile(developFileName) == parseJson(expectedDevelopFileContent) From 22404bda69246c1ef87e5c7b69e11f12b7f68c75 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 18 Jul 2021 22:57:06 +0300 Subject: [PATCH 56/73] Update the CI to use the latest Nim nightly build The Nimble CI is updated to use the currently latest Nim nightly build from 18th of July 2021. Related to nim-lang/nimble#127 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d43d9f05..8c1fc5ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - macos-latest - ubuntu-latest nimversion: - - sourcetar:https://github.com/bobeff/Nim/tarball/8ede3bc0494aa27d98384fa5449c5c3026ac8f92 + - nightly:https://github.com/nim-lang/nightlies/releases/tag/2021-07-18-devel-923a1c6ea7d9f45b6389680717692690218228fb name: ${{ matrix.os }} - ${{ matrix.nimversion }} runs-on: ${{ matrix.os }} env: From 794ee440911c2691ac1cf4c7fdb9cd4bedc81327 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 18 Jul 2021 23:53:46 +0300 Subject: [PATCH 57/73] Add the `vcstools` module to `tmoduletests.nim` Related to nim-lang/nimble#127 --- tests/tmoduletests.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tmoduletests.nim b/tests/tmoduletests.nim index fd2369b3..202c198d 100644 --- a/tests/tmoduletests.nim +++ b/tests/tmoduletests.nim @@ -25,4 +25,5 @@ suite "Module tests": moduleTest "sha1hashes" moduleTest "tools" moduleTest "topologicalsort" + moduleTest "vcstools" moduleTest "version" From 14020d5430d3e7e1fe659d26d98d1d49924cba9d Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 19 Jul 2021 01:07:36 +0300 Subject: [PATCH 58/73] Fix tests - Add a Mercurial install step to the CI on Mac OS because it is not installed by default but it is required by the `vcstools.nim` module tests. - Add workaround for the `Resource temporarily unavailable (code: 11)` error on Linux systems. The error is most probably caused by a bug in the `asynctools` or the Nim standard library but currently, the workaround is an easier solution than a proper fix. - Add debug priority printing of the command to be executed by the `doCmdEx` and` doCmdExAsync` procedures. - Fix `vcstools.getBranchesOnWhichVcsRevisionIsPresent` procedure which had a wrong output on Windows. - Configure user name and email for the local testing repositories to avoid VCS operations failure when tests are executed on the CI. - Fix expected character for root directory in `getVcsTypeAndSpecialDirPath` test in` vcstools.nim` module. - Use a proper module name in the `dependency.nim` test file when the tests are executed on Mac OS. - Fix the expected main repository path on Mac OS to start with "/private" in "cannot lock because develop dependency is out of range" test in `tlockfile.nim`. Most probably the list is incomplete because I'm missing something. :) Related to nim-lang/nimble#127 --- .github/workflows/test.yml | 3 ++ src/nimblepkg/asynctools/asyncproc.nim | 27 +++++++++++----- src/nimblepkg/tools.nim | 2 ++ src/nimblepkg/vcstools.nim | 41 ++++++++++++++++--------- tests/develop/dependency/dependency.nim | 2 +- tests/tester.nim | 5 ++- tests/testscommon.nim | 2 ++ tests/tlockfile.nim | 17 ++++++++-- 8 files changed, 73 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c1fc5ef..01e3d7f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,9 @@ jobs: with: version: ${{ matrix.nimversion }} - run: nim --version + - name: Install Mercurial on macOS + if: matrix.os == 'macos-latest' + run: brew install mercurial - name: Run nim c -r tester run: | cd tests diff --git a/src/nimblepkg/asynctools/asyncproc.nim b/src/nimblepkg/asynctools/asyncproc.nim index 2cc26f11..489d68ff 100644 --- a/src/nimblepkg/asynctools/asyncproc.nim +++ b/src/nimblepkg/asynctools/asyncproc.nim @@ -24,9 +24,6 @@ else: const STILL_ACTIVE = 259 import posix -when defined(linux): - import linux - type ProcessOption* = enum ## options that can be passed `startProcess` poEchoCmd, ## echo the command before execution @@ -510,7 +507,7 @@ else: result = cast[cstringArray](alloc0((counter + 1) * sizeof(cstring))) var i = 0 for key, val in envPairs(): - var x = key.string & "=" & val.string + var x = key & "=" & val result[i] = cast[cstring](alloc(x.len+1)) copyMem(result[i], addr(x[0]), x.len+1) inc(i) @@ -889,7 +886,17 @@ proc execProcess(command: string, args: seq[string] = @[], var data = newString(bufferSize) var p = startProcess(command, args = args, env = env, options = options) - let future = p.waitForExit() + # Here the code path for Linux systems is a workaround for a bug eighter in + # the `asynctools` library or the Nim standard library which causes `Resource + # temporarily unavailable (code: 11)` error when executing multiple + # asynchronous operations. + # + # TODO: Add a proper fix that does not use a different code ordering on the + # different systems. + + when not defined(linux): + let future = p.waitForExit + while true: let res = await p.outputHandle.readInto(addr data[0], bufferSize) if res > 0: @@ -898,12 +905,16 @@ proc execProcess(command: string, args: seq[string] = @[], data.setLen(bufferSize) else: break - result.exitcode = await future + + result.exitcode = + when not defined(linux): + await future + else: + await p.waitForExit + close(p) when isMainModule: - import os - when defined(windows): var data = waitFor(execProcess("cd")) else: diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index ebd9bd66..a88dbf24 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -47,6 +47,7 @@ proc doCmd*(cmd: string) = [$exitCode, cmd, output]) proc doCmdEx*(cmd: string): ProcessOutput = + displayDebug("Executing", cmd) let bin = extractBin(cmd) if findExe(bin) == "": raise nimbleError("'" & bin & "' not in PATH.") @@ -57,6 +58,7 @@ proc removeQuotes(cmd: string): string = proc doCmdExAsync*(cmd: string, args: seq[string] = @[]): Future[ProcessOutput] {.async.} = + displayDebug("Executing", join(concat(@[cmd], args), " ")) let bin = extractBin(cmd) if findExe(bin) == "": raise nimbleError("'" & bin & "' not in PATH.") diff --git a/src/nimblepkg/vcstools.nim b/src/nimblepkg/vcstools.nim index bf307087..177ed32a 100644 --- a/src/nimblepkg/vcstools.nim +++ b/src/nimblepkg/vcstools.nim @@ -410,14 +410,15 @@ proc getBranchesOnWhichVcsRevisionIsPresent*( let vcsType = path.getVcsType for line in output.strip.splitLines: - var line = line.strip(chars = Whitespace + {'*'}) + var line = line.strip(chars = Whitespace + {'*', '\''}) if vcsType == vcsTypeGit and branchType == btBoth: # For "git branch -a" remote branches are starting with "remotes" which # have to be removed for uniformity with "git branch -r". const prefix = "remotes/" if line.startsWith(prefix): line = line[prefix.len .. ^1] - result.incl line + if line.len > 0: + result.incl line proc isVcsRevisionPresentOnSomeBranch*(path: Path, vcsRevision: Sha1Hash): bool = @@ -624,7 +625,7 @@ proc fastForwardMerge*(path: Path, remoteBranch, localBranch: string) = tryDoCmdEx(&"{git(path)} checkout {currentBranch}") when isMainModule: - import unittest, std/sha1, sequtils, os + import unittest, std/sha1, sequtils type NameToVcsRevision = OrderedTable[string, Sha1Hash] @@ -635,11 +636,11 @@ when isMainModule: testGitDir = tempDir / "testGitDir" testHgDir = tempDir / "testHgDir" testNoVcsDir = tempDir / "testNoVcsDir" - testSubDir = "./testSubDir" + testSubDir = "testSubDir" testFile = "test.txt" testFile2 = "test2.txt" testFileContent = "This is a test file.\n" - testSubDirFile = testSubDir / testFile + testSubDirFile = &"{testSubDir}/{testFile}" testRemotes: seq[tuple[name, url: string]] = @[ ("origin", "testRemote1Dir"), ("other", "testRemote2Dir"), @@ -654,12 +655,20 @@ when isMainModule: var nameToVcsRevision: NameToVcsRevision - proc getMercurialPathsDesc(): string = - result = "[paths]\n" + proc getMercurialRcFileContent(): string = + result = """ +[ui] +username = John Doe +[paths] +""" for remote in testRemotes: - result &= &"{remote.name} = {remote.url.absolutePath}\n" + result &= &"{remote.name} = {testHgDir / remote.url}\n" - proc initRepo(vcsType: VcsType) = tryDoCmdEx(&"{vcsType} init") + proc initRepo(vcsType: VcsType, url = ".") = + tryDoCmdEx(&"{vcsType} init {url}") + if vcsType == vcsTypeGit: + tryDoCmdEx(&"git -C {url} config user.name \"John Doe\"") + tryDoCmdEx(&"git -C {url} config user.email \"john.doe@example.com\"") proc collectFiles(files: varargs[string]): string = for file in files: result &= file & " " @@ -688,13 +697,13 @@ when isMainModule: for remote in testRemotes: tryDoCmdEx(&"git remote add {remote.name} {remote.url}") of vcsTypeHg: - writeFile(".hg/hgrc", getMercurialPathsDesc()) + writeFile(".hg/hgrc", getMercurialRcFileContent()) of vcsTypeNone: assert false, "VCS type must not be 'vcsTypeNone'." proc setupRemoteRepo(vcsType: VcsType, remoteUrl: string) = createDir remoteUrl - tryDoCmdEx(&"{vcsType} init {remoteUrl}") + initRepo(vcsType, remoteUrl) if vcsType == vcsTypeGit: cd remoteUrl: tryDoCmdEx("git config receive.denyCurrentBranch ignore") @@ -726,6 +735,9 @@ when isMainModule: tryDoCmdEx(&"{vcsType} branch {branchName}") proc pushToRemote(vcsType: VcsType, remoteName: string) = + if vcsType == vcsTypeGit and remoteName == gitDefaultRemote: + let branchName = getCurrentBranch(".") + tryDoCmdEx(&"git push --set-upstream {remoteName} {branchName}") tryDoCmdEx(&"{vcsType} push {remoteName}") proc pushToTestRemotes(vcsType: VcsType) = @@ -1033,13 +1045,13 @@ when isMainModule: suite "Mercurial": suiteTestCode(vcsTypeHg, testHgDir): - (testHgDir / remote.url).absolutePath + testHgDir / remote.url suite "no version control": setupNoVcsSuite() test "getVcsTypeAndSpecialDirPath": - const rootDir = when defined(windows): '\\' else: '/' + const rootDir = when defined(windows): ':' else: '/' let (vcsType, specialDirPath) = getVcsTypeAndSpecialDirPath(testNoVcsDir) check vcsType == vcsTypeNone check ($specialDirPath)[^1] == rootDir @@ -1048,6 +1060,7 @@ when isMainModule: check not isValidSha1Hash($getVcsRevision(testNoVcsDir)) test "getPackageFileList": - check getPackageFileList(testNoVcsDir) == @[testFile, testSubDirFile] + check getPackageFileList(testNoVcsDir) == + @[testFile, testSubDirFile.normalizedPath] tearDownSuite(testNoVcsDir) diff --git a/tests/develop/dependency/dependency.nim b/tests/develop/dependency/dependency.nim index 7e716e07..ed6c889a 100644 --- a/tests/develop/dependency/dependency.nim +++ b/tests/develop/dependency/dependency.nim @@ -1,7 +1,7 @@ import packagea proc test*(): string = - when defined(windows): + when defined(windows) or defined(macosx): $PackageA.test(6, 9) elif defined(unix): $packagea.test(6, 9) diff --git a/tests/tester.nim b/tests/tester.nim index 9497798f..6f0e3928 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1,7 +1,10 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -# import suites +import testscommon + +# suits imports + import tcheckcommand import tdevelopfeature import tissues diff --git a/tests/testscommon.nim b/tests/testscommon.nim index 00558660..ee031791 100644 --- a/tests/testscommon.nim +++ b/tests/testscommon.nim @@ -1,6 +1,8 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. +{.used.} + import sequtils, strutils, strformat, os, osproc, sugar, unittest, macros, std/sha1 diff --git a/tests/tlockfile.nim b/tests/tlockfile.nim index f01df8b4..f995f6e9 100644 --- a/tests/tlockfile.nim +++ b/tests/tlockfile.nim @@ -119,12 +119,18 @@ requires "nim >= 1.5.1" proc addRemote(remoteName, remoteUrl: string) = tryDoCmdEx(&"git remote add {remoteName} {remoteUrl}") + proc configUserAndEmail = + tryDoCmdEx("git config user.name \"John Doe\"") + tryDoCmdEx("git config user.email \"john.doe@example.com\"") + proc initRepo(isBare = false) = let bare = if isBare: "--bare" else: "" tryDoCmdEx("git init " & bare) + configUserAndEmail() proc clone(urlFrom, pathTo: string) = tryDoCmdEx(&"git clone {urlFrom} {pathTo}") + cd pathTo: configUserAndEmail() proc branch(branchName: string) = tryDoCmdEx(&"git branch {branchName}") @@ -183,7 +189,7 @@ requires "nim >= 1.5.1" else: check fileExists(lockFileName) - let (output, exitCode) = execNimbleYes("lock") + let (output, exitCode) = execNimbleYes("lock", "--debug") check exitCode == QuitSuccess var lines = output.processOutput @@ -245,6 +251,13 @@ requires "nim >= 1.5.1" let (output, exitCode) = execNimbleYes("lock") check exitCode == QuitFailure + let mainPkgRepoPath = + when defined(macosx): + # This is a workaround for the added `/private` prefix to the main + # repository Nimble file path when executing the test on macOS. + "/private" / mainPkgRepoPath + else: + mainPkgRepoPath let errors = @[ notInRequiredRangeMsg(dep1PkgName, dep1PkgRepoPath, "0.1.0", mainPkgName, mainPkgRepoPath, "> 0.1.0"), @@ -266,7 +279,7 @@ requires "nim >= 1.5.1" (dep2PkgName, dep2PkgRepoPath)], isNew = true) removeDir installDir - let (output, exitCode) = execNimbleYes("install") + let (output, exitCode) = execNimbleYes("install", "--debug") check exitCode == QuitSuccess let lines = output.processOutput check lines.inLines(&"Downloading {dep1PkgOriginRepoPath} using git") From 0864c1e4d5b7de2062552ce7ff2a4e8b297d881c Mon Sep 17 00:00:00 2001 From: narimiran Date: Thu, 22 Jul 2021 13:13:00 +0200 Subject: [PATCH 59/73] remove counttables.nim --- .gitignore | 1 - src/nimblepkg/counttables.nim | 94 ----------------------------------- src/nimblepkg/developfile.nim | 22 ++++++-- tests/tmoduletests.nim | 1 - 4 files changed, 19 insertions(+), 99 deletions(-) delete mode 100644 src/nimblepkg/counttables.nim diff --git a/.gitignore b/.gitignore index 44834188..a03816c5 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ src/nimblepkg/checksums src/nimblepkg/cli src/nimblepkg/common src/nimblepkg/config -src/nimblepkg/counttables src/nimblepkg/developfile src/nimblepkg/download src/nimblepkg/init diff --git a/src/nimblepkg/counttables.nim b/src/nimblepkg/counttables.nim deleted file mode 100644 index a47e31b2..00000000 --- a/src/nimblepkg/counttables.nim +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) Dominik Picheta. All rights reserved. -# BSD License. Look at license.txt for more info. - -import tables, strformat - -type - CountType = uint - CountTable*[K] = distinct Table[K, CountType] - ## Maps a key to some unsigned integer count. - -template withValue[K](t: var CountTable[K], k: K; - value, body1, body2: untyped) = - withValue(Table[K, CountType](t), k, value, body1, body2) - -proc `[]=`[K](t: var CountTable[K], k: K, v: CountType) {.inline.} = - Table[K, CountType](t)[k] = v - -proc del[K](t: var CountTable[K], k: K) {.inline.} = - del(Table[K, CountType](t), k) - -proc getOrDefault[K](t: CountTable[K], k: K): CountType {.inline.} = - getOrDefault(Table[K, CountType](t), k) - -proc inc*[K](t: var CountTable[K], k: K) = - ## Increments the count of key `k` in table `t`. If the key is missing the - ## procedure adds it with a count 1. - t.withValue(k, value) do: - const maxCount = CountType.high - assert value[] < maxCount, - "Cannot increment because the result will exceed the maximum " & - &"possible count value of {maxCount}." - value[].inc() - do: - t[k] = 1 - -proc dec*[K](t: var CountTable[K], k: K): bool {.discardable.} = - ## Decrements the count of key `k` in table `t`. If the count drops to zero - ## the procedure removes the key from the table. - ## - ## Returns `true` in the case the count for the key `k` drops to zero and the - ## key is removed from the table or `false` otherwise. - ## - ## If the key `k` is missing raises a `KeyError` exception. - t.withValue(k, value) do: - value[].dec() - if value[] == 0: - t.del(k) - result = true - do: - raise newException(KeyError, &"The key \"{k}\" is not found.") - -proc count*[K](t: CountTable[K], k: K): CountType = t.getOrDefault(k) - ## Returns the count of the key `k` from the table `t`. If the key is missing - ## returns zero. - -proc `[]`*[K](t: CountTable[K], k: K): CountType = Table[K, CountType](t)[k] - ## Returns the count of the key `k` from the table `t`. If the key is missing - ## raises a `KeyError` exception. - -proc hasKey*[K](t: CountTable[K], k: K): bool = t.count(k) != 0 - ## Checks whether the key `k` is present in the table `t`. - -when isMainModule: - import unittest - - let testKey = 'a' - var t: CountTable[testKey.typeOf] - - proc checkKeyCount[K](t: CountTable[K], k: K, c: CountType) = - check t.count(k) == c - if c != 0: - check t.hasKey(k) - check t[k] == c - else: - check not t.hasKey(k) - expect KeyError, (discard t[k]) - - test "key count": - checkKeyCount(t, testKey, 0) - - t.inc(testKey) - checkKeyCount(t, testKey, 1) - - t.inc(testKey) - checkKeyCount(t, testKey, 2) - - check not t.dec(testKey) - checkKeyCount(t, testKey, 1) - - check t.dec(testKey) - checkKeyCount(t, testKey, 0) - - expect KeyError, t.dec(testKey) - checkKeyCount(t, testKey, 0) diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index f6330bf5..e9839b3c 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -10,7 +10,7 @@ import sets, json, sequtils, os, strformat, tables, hashes, strutils, math, import typetraits except distinctBase import common, cli, packageinfotypes, packageinfo, packageparser, options, - version, counttables, aliasthis, paths, displaymessages, sha1hashes, + version, aliasthis, paths, displaymessages, sha1hashes, tools, vcstools, syncfile, lockfile type @@ -48,7 +48,7 @@ type ## For each package contains the set of names of the develop files where ## the path to its directory is mentioned. Used for colliding names error ## reporting when packages with same name but different paths are present. - pkgRefCount: counttables.CountTable[ref PackageInfo] + pkgRefCount: CountTable[ref PackageInfo] ## For each package contains the number of times it is included from ## different develop files. When the reference count drops to zero the ## package will be removed from all internal meta data structures. @@ -393,7 +393,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, # If this is a package develop file, but not a free one, for each of the # package's develop mode dependencies load its develop file if it is not # already loaded and merge its data to the current develop file's data. - for path, pkg in result.pathToPkg.dup: + for path, pkg in result.pathToPkg: if visitedPkgs.contains(path): continue var followedPkgDevFileData = initDevelopFileData() @@ -473,6 +473,22 @@ proc addDevelopPackage(data: var DevelopFileData, path: Path, return addDevelopPackage(data, pkgInfo) +proc dec[K](t: var CountTable[K], k: K): bool {.discardable.} = + ## Decrements the count of key `k` in table `t`. If the count drops to zero + ## the procedure removes the key from the table. + ## + ## Returns `true` in the case the count for the key `k` drops to zero and the + ## key is removed from the table or `false` otherwise. + ## + ## If the key `k` is missing raises a `KeyError` exception. + if k in t: + t[k] = t[k] - 1 + if t[k] == 0: + t.del(k) + result = true + else: + raise newException(KeyError, &"The key \"{k}\" is not found.") + proc removePackage(data: var DevelopFileData, pkg: ref PackageInfo, devFileName: Path) = ## Decreases the reference count for a package at path `path` and removes the diff --git a/tests/tmoduletests.nim b/tests/tmoduletests.nim index 202c198d..9269ace0 100644 --- a/tests/tmoduletests.nim +++ b/tests/tmoduletests.nim @@ -15,7 +15,6 @@ suite "Module tests": moduleTest "aliasthis" moduleTest "common" - moduleTest "counttables" moduleTest "download" moduleTest "jsonhelpers" moduleTest "packageinfo" From 2807e3c61072f1e52ede3297164b7ab79b658c80 Mon Sep 17 00:00:00 2001 From: narimiran Date: Thu, 22 Jul 2021 16:23:43 +0200 Subject: [PATCH 60/73] remove unnecessary `dup` --- src/nimble.nim | 4 ++-- src/nimblepkg/common.nim | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 731e9ccb..1288d249 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -1480,7 +1480,7 @@ proc validateDevModeDepsWorkingCopiesBeforeLock( } # Remove not errors from the errors set. - for name, error in common.dup(errors): + for name, error in errors: if error.kind in notAnErrorSet: errors.del name @@ -1699,7 +1699,7 @@ proc sync(options: Options) = var errors: ValidationErrors findValidationErrorsOfDevDepsWithLockFile(pkgInfo, options, errors) - for name, error in common.dup(errors): + for name, error in errors: if error.kind == vekWorkingCopyNeedsSync: if not options.action.listOnly: syncWorkingCopy(name, error.path, pkgInfo, options) diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 412eba0a..856dcead 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -52,8 +52,6 @@ template newClone*[T: not ref](obj: T): ref T = result[] = obj result -proc dup*[T](obj: T): T = obj - proc `$`*(p: ptr | ref): string = cast[int](p).toHex ## Converts the pointer `p` to its hex string representation. From 50ef9af39a63908745639391bb89047590a21f1f Mon Sep 17 00:00:00 2001 From: narimiran Date: Thu, 22 Jul 2021 18:41:05 +0200 Subject: [PATCH 61/73] remove aliasthis.nim --- .gitignore | 1 - src/nimble.nim | 74 ++++++------- src/nimblepkg/aliasthis.nim | 164 ----------------------------- src/nimblepkg/developfile.nim | 66 ++++++------ src/nimblepkg/download.nim | 4 +- src/nimblepkg/packageinfo.nim | 30 +++--- src/nimblepkg/packageinfotypes.nim | 10 +- src/nimblepkg/packageparser.nim | 56 +++++----- src/nimblepkg/publish.nim | 10 +- src/nimblepkg/reversedeps.nim | 18 ++-- src/nimblepkg/syncfile.nim | 2 +- src/nimblepkg/topologicalsort.nim | 14 +-- tests/tmoduletests.nim | 1 - 13 files changed, 135 insertions(+), 315 deletions(-) delete mode 100644 src/nimblepkg/aliasthis.nim diff --git a/.gitignore b/.gitignore index a03816c5..ec6622fd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ nimcache/ # executables from test and build /nimble -src/nimblepkg/aliasthis src/nimblepkg/checksums src/nimblepkg/cli src/nimblepkg/common diff --git a/src/nimble.nim b/src/nimble.nim index 1288d249..eef01243 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -77,7 +77,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): once: pkgList = initPkgList(pkgInfo, options) display("Verifying", - "dependencies for $1@$2" % [pkgInfo.name, $pkgInfo.specialVersion], + "dependencies for $1@$2" % [pkgInfo.basicInfo.name, $pkgInfo.metaData.specialVersion], priority = HighPriority) var reverseDependencies: seq[PackageBasicInfo] = @[] @@ -119,19 +119,19 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): # Process the dependencies of this dependency. result.incl processFreeDependencies(pkg.toFullInfo(options), options) if not pkg.isLink: - reverseDependencies.add((pkg.name, pkg.specialVersion, pkg.checksum)) + reverseDependencies.add((pkg.basicInfo.name, pkg.metaData.specialVersion, pkg.basicInfo.checksum)) # Check if two packages of the same name (but different version) are listed # in the path. var pkgsInPath: Table[string, Version] for pkgInfo in result: let currentVer = pkgInfo.getConcreteVersion(options) - if pkgsInPath.hasKey(pkgInfo.name) and - pkgsInPath[pkgInfo.name] != currentVer: + if pkgsInPath.hasKey(pkgInfo.basicInfo.name) and + pkgsInPath[pkgInfo.basicInfo.name] != currentVer: raise nimbleError( "Cannot satisfy the dependency on $1 $2 and $1 $3" % - [pkgInfo.name, $currentVer, $pkgsInPath[pkgInfo.name]]) - pkgsInPath[pkgInfo.name] = currentVer + [pkgInfo.basicInfo.name, $currentVer, $pkgsInPath[pkgInfo.basicInfo.name]]) + pkgsInPath[pkgInfo.basicInfo.name] = currentVer # We add the reverse deps to the JSON file here because we don't want # them added if the above errorenous condition occurs @@ -160,7 +160,7 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: HashSet[string], var binariesBuilt = 0 args = args - args.add "-d:NimblePkgVersion=" & $pkgInfo.version + args.add "-d:NimblePkgVersion=" & $pkgInfo.basicInfo.version for path in paths: args.add("--path:" & path.quoteShell) if options.verbosity >= HighPriority: @@ -185,7 +185,7 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: HashSet[string], let outputOpt = "-o:" & pkgInfo.getOutputDir(bin).quoteShell display("Building", "$1/$2 using $3 backend" % - [pkginfo.name, bin, pkgInfo.backend], priority = HighPriority) + [pkginfo.basicInfo.name, bin, pkgInfo.backend], priority = HighPriority) let outputDir = pkgInfo.getOutputDir("") if not dirExists(outputDir): @@ -202,7 +202,7 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: HashSet[string], binariesBuilt.inc() except CatchableError as error: raise buildFailed( - &"Build failed for the package: {pkgInfo.name}", details = error) + &"Build failed for the package: {pkgInfo.basicInfo.name}", details = error) if binariesBuilt == 0: raise nimbleError( @@ -225,10 +225,10 @@ proc promptRemoveEntirePackageDir(pkgDir: string, options: Options) = raise nimbleQuit() proc removePackageDir(pkgInfo: PackageInfo, pkgDestDir: string) = - removePackageDir(pkgInfo.files & packageMetaDataFileName, pkgDestDir) + removePackageDir(pkgInfo.metaData.files & packageMetaDataFileName, pkgDestDir) proc removeBinariesSymlinks(pkgInfo: PackageInfo, binDir: string) = - for bin in pkgInfo.binaries: + for bin in pkgInfo.metaData.binaries: when defined(windows): removeFile(binDir / bin.changeFileExt("cmd")) removeFile(binDir / bin) @@ -267,7 +267,7 @@ proc packageExists(pkgInfo: PackageInfo, options: Options): bool = proc promptOverwriteExistingPackage(pkgInfo: PackageInfo, options: Options): bool = let message = "$1@$2 already exists. Overwrite?" % - [pkgInfo.name, $pkgInfo.specialVersion] + [pkgInfo.basicInfo.name, $pkgInfo.metaData.specialVersion] return options.prompt(message) proc removeOldPackage(pkgInfo: PackageInfo, options: Options) = @@ -322,7 +322,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, ## In the case we downloaded the package as tarball we have to set the VCS ## revision returned by download procedure because it cannot be queried from ## the package directory. - pkgInfo.vcsRevision = vcsRevision + pkgInfo.metaData.vcsRevision = vcsRevision let realDir = pkgInfo.getRealDir() let binDir = options.getBinDir() @@ -331,7 +331,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Overwrite the version if the requested version is "#head" or similar. if requestedVer.kind == verSpecial: - pkgInfo.specialVersion = requestedVer.spe + pkgInfo.metaData.specialVersion = requestedVer.spe # Dependencies need to be processed before the creation of the pkg dir. if first and pkgInfo.lockedDeps.len > 0: @@ -343,7 +343,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, result.pkg = pkgInfo return result - display("Installing", "$1@$2" % [pkginfo.name, $pkginfo.specialVersion], + display("Installing", "$1@$2" % [pkginfo.basicInfo.name, $pkginfo.metaData.specialVersion], priority = HighPriority) let isPackageAlreadyInCache = pkgInfo.packageExists(options) @@ -368,7 +368,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, let pkgDestDir = pkgInfo.getPkgDest(options) # Fill package Meta data - pkgInfo.url = url + pkgInfo.metaData.url = url pkgInfo.isLink = false # Don't copy artifacts if project local deps mode and "installing" the top @@ -424,8 +424,8 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Update package path to point to installed directory rather than the temp # directory. pkgInfo.myPath = dest - pkgInfo.files = filesInstalled.toSeq - pkgInfo.binaries = binariesInstalled.toSeq + pkgInfo.metaData.files = filesInstalled.toSeq + pkgInfo.metaData.binaries = binariesInstalled.toSeq saveMetaData(pkgInfo.metaData, pkgDestDir) else: @@ -433,7 +433,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, pkgInfo.isInstalled = true - displaySuccess(pkgInstalledMsg(pkgInfo.name)) + displaySuccess(pkgInstalledMsg(pkgInfo.basicInfo.name)) result.deps.incl pkgInfo result.pkg = pkgInfo @@ -719,7 +719,7 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = if not execHook(options, options.action.typ, true): raise nimbleError("Pre-hook prevented further execution.") - var args = @["-d:NimblePkgVersion=" & $pkgInfo.version] + var args = @["-d:NimblePkgVersion=" & $pkgInfo.basicInfo.version] for dep in deps: args.add("--path:" & dep.getRealDir().quoteShell) if options.verbosity >= HighPriority: @@ -740,10 +740,10 @@ proc execBackend(pkgInfo: PackageInfo, options: Options) = if options.action.typ == actionCompile: display("Compiling", "$1 (from package $2) using $3 backend" % - [bin, pkgInfo.name, backend], priority = HighPriority) + [bin, pkgInfo.basicInfo.name, backend], priority = HighPriority) else: display("Generating", ("documentation for $1 (from package $2) using $3 " & - "backend") % [bin, pkgInfo.name, backend], priority = HighPriority) + "backend") % [bin, pkgInfo.basicInfo.name, backend], priority = HighPriority) doCmd(getNimBin(options).quoteShell & " $# --noNimblePath $# $#" % [backend, join(args, " "), bin.quoteShell]) @@ -801,8 +801,8 @@ proc listInstalled(options: Options) = let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) for pkg in pkgs: let - pName = pkg.name - pVer = pkg.specialVersion + pName = pkg.basicInfo.name + pVer = pkg.metaData.specialVersion if not h.hasKey(pName): h[pName] = @[] var s = h[pName] add(s, pVer) @@ -836,8 +836,8 @@ proc listPaths(options: Options) = var installed: seq[VersionAndPath] = @[] # There may be several, list all available ones and sort by version. for pkg in pkgs: - if name == pkg.name: - installed.add((pkg.specialVersion, pkg.getRealDir)) + if name == pkg.basicInfo.name: + installed.add((pkg.metaData.specialVersion, pkg.getRealDir)) if installed.len > 0: sort(installed, cmp[VersionAndPath], Descending) @@ -911,8 +911,8 @@ proc dump(options: Options) = s.add val.escape else: s.add val.join(", ").escape - fn "name", p.name - fn "version", $p.version + fn "name", p.basicInfo.name + fn "version", $p.basicInfo.version fn "author", p.author fn "desc", p.description fn "license", p.license @@ -1127,7 +1127,7 @@ proc uninstall(options: var Options) = if len(revDeps - pkgsToDelete) > 0: let pkgs = revDeps.collectNames(true) displayWarning( - cannotUninstallPkgMsg(pkgTup.name, pkg.specialVersion, pkgs)) + cannotUninstallPkgMsg(pkgTup.name, pkg.metaData.specialVersion, pkgs)) else: pkgsToDelete.incl pkg.toRevDep @@ -1180,7 +1180,7 @@ proc developFromDir(pkgInfo: PackageInfo, options: var Options) = # Dependencies need to be processed before the creation of the pkg dir. discard processAllDependencies(pkgInfo, options) - displaySuccess(pkgSetupInDevModeMsg(pkgInfo.name, dir)) + displaySuccess(pkgSetupInDevModeMsg(pkgInfo.basicInfo.name, dir)) # Execute the post-develop hook. cd dir: @@ -1260,14 +1260,14 @@ proc developFreeDependencies(pkgInfo: PackageInfo, continue let pkgInfo = installDevelopPackage(dep, options) - alreadyDownloaded.incl pkgInfo.url.removeTrailingGitString + alreadyDownloaded.incl pkgInfo.metaData.url.removeTrailingGitString proc developAllDependencies(pkgInfo: PackageInfo, options: var Options) = ## Puts all dependencies of `pkgInfo` (including transitive ones) in develop ## mode by cloning their repositories. var alreadyDownloadedDependencies {.global.}: HashSet[string] - alreadyDownloadedDependencies.incl pkgInfo.url.removeTrailingGitString + alreadyDownloadedDependencies.incl pkgInfo.metaData.url.removeTrailingGitString if pkgInfo.lockedDeps.len > 0: pkgInfo.developLockedDependencies(alreadyDownloadedDependencies, options) @@ -1405,8 +1405,8 @@ proc test(options: Options) = proc notInRequiredRangeMsg*(dependentPkg, dependencyPkg: PackageInfo, versionRange: VersionRange): string = - notInRequiredRangeMsg(dependencyPkg.name, dependencyPkg.getNimbleFileDir, - $dependencyPkg.version, dependentPkg.name, dependentPkg.getNimbleFileDir, + notInRequiredRangeMsg(dependencyPkg.basicInfo.name, dependencyPkg.getNimbleFileDir, + $dependencyPkg.basicInfo.version, dependentPkg.basicInfo.name, dependentPkg.getNimbleFileDir, $versionRange) proc validateDevelopDependenciesVersionRanges(dependentPkg: PackageInfo, @@ -1439,7 +1439,7 @@ proc check(options: Options) = validateDevelopFile(pkgInfo, options) let dependencies = pkgInfo.processAllDependencies(options).toSeq validateDevelopDependenciesVersionRanges(pkgInfo, dependencies, options) - displaySuccess(&"The package \"{pkgInfo.name}\" is valid.") + displaySuccess(&"The package \"{pkgInfo.basicInfo.name}\" is valid.") except CatchableError as error: displayError(error) display("Failure:", validationFailedMsg, Error, HighPriority) @@ -1457,7 +1457,7 @@ proc updateSyncFile(dependentPkg: PackageInfo, options: Options) = # Add all current develop packages' VCS revisions to the sync file. for dep in developDeps: - syncFile.setDepVcsRevision(dep.name, dep.vcsRevision) + syncFile.setDepVcsRevision(dep.basicInfo.name, dep.metaData.vcsRevision) syncFile.save @@ -1809,7 +1809,7 @@ proc run(options: Options) = if binary notin pkgInfo.bin: raise nimbleError( - "Binary '$#' is not defined in '$#' package." % [binary, pkgInfo.name] + "Binary '$#' is not defined in '$#' package." % [binary, pkgInfo.basicInfo.name] ) # Build the binary. diff --git a/src/nimblepkg/aliasthis.nim b/src/nimblepkg/aliasthis.nim deleted file mode 100644 index caed40bb..00000000 --- a/src/nimblepkg/aliasthis.nim +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (C) Dominik Picheta. All rights reserved. -# BSD License. Look at license.txt for more info. - -import macros - -template delegateField*(ObjectType: type[object], - objectField, accessor: untyped) = - ## Defines two additional templates for getter and setter of nested object - ## field directly via the embedding object. The name of the nested object - ## field must match the name of the accessor. - ## - ## Sample usage: - ## - ## .. code-block:: nim - ## - ## type - ## Object1 = object - ## field1: int - ## - ## Object2 = object - ## field2: Object1 - ## - ## delegateField(Object2, field2, field1) - ## - ## var obj: Object2 - ## obj.field1 = 42 - ## echo obj.field1 # prints 42 - ## echo obj.field2.field1 # also prints 42 - - type AccessorType = ObjectType.default.objectField.accessor.typeOf - - template accessor*(obj: ObjectType): AccessorType = - obj.objectField.accessor - - template `accessor "="`*(obj: var ObjectType, value: AccessorType) = - obj.objectField.accessor = value - -func fields(Object: type[object | tuple]): seq[string] = - ## Collects the names of the fields of an object. - let obj = Object.default - for name, _ in obj.fieldPairs: - result.add name - -macro aliasThisImpl(dotExpression: typed, fields: static seq[string]): untyped = - ## Accepts a dot expressions of an object and an object's field of object type - ## and the names of the fields of the nested object. Iterates them and for - ## each one generates getter and setter template accessors directly via the - ## embedding object. - - dotExpression.expectKind nnkDotExpr - result = newStmtList() - let ObjectType = dotExpression[0] - let objectField = dotExpression[1] - for accessor in fields: - result.add newCall( - "delegateField", ObjectType, objectField, accessor.newIdentNode) - -template aliasThis*(dotExpression: untyped) = - ## Makes fields of an object nested in another object accessible via the - ## embedding one. Currently only Nim's non variant `object` types and only a - ## single level of nesting are supported. - ## - ## Sample usage: - ## - ## .. code-block:: nim - ## - ## type - ## Object1 = object - ## field1: int - ## - ## Object2 = object - ## field2: Object1 - ## - ## aliasThis Object2.field2 - ## - ## var obj: Object2 - ## obj.field1 = 42 - ## echo obj.field1 # prints 42 - ## echo obj.field2.field1 # also prints 42 - - {.warning[UnsafeDefault]: off.} - aliasThisImpl(dotExpression, dotExpression.typeOf.fields) - {.warning[UnsafeDefault]: on.} - -when isMainModule: - import unittest - - type - Object1 = object - field11: float - field12: seq[int] - field13: int - - Tuple = tuple[tField1: string, tField2: int] - - Object2 = object - field11: float # intentionally the name is the same as in Object1 - field22: Object1 - field23: Tuple - - aliasThis(Object2.field22) - aliasThis(Object2.field23) - - const objPrototype = Object2( - field11: 3.14, - field22: Object1( - field11: 2.718, - field12: @[1, 1, 2, 3, 5, 8], - field13: 42), - field23: ("tuple", 1)) - - suite "aliasThis for objects": - setup: - var obj = objPrototype - - test "access to the original value in both ways": - check obj.field13 == 42 - check obj.field22.field13 == 42 - check obj.field12 == @[1, 1, 2, 3, 5, 8] - check obj.field22.field12 == @[1, 1, 2, 3, 5, 8] - - test "setter via an alias": - obj.field13 = -obj.field13 - check obj.field13 == -42 - check obj.field22.field13 == -42 - - test "setter without an alias": - obj.field22.field13 = 0 - check obj.field13 == 0 - check obj.field22.field13 == 0 - - test "procedure call via an alias": - obj.field12.add 13 - check obj.field12 == @[1, 1, 2, 3, 5, 8, 13] - check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13] - - test "procedure call without an alias": - obj.field22.field12.add 13 - check obj.field12 == @[1, 1, 2, 3, 5, 8, 13] - check obj.field22.field12 == @[1, 1, 2, 3, 5, 8, 13] - - test "the priority is on the not aliased field": - check obj.field11 == 3.14 - - test "the aliased, but shadowed field is still accessible": - check obj.field22.field11 == 2.718 - - test "setting via matching field name does not override the shadowed field": - obj.field11 = 0 - check obj.field11 == 0 - check obj.field22.field11 == 2.718 - - suite "aliasThis for tuples": - setup: - var obj = objPrototype - - test "access to tuple fields via an alias": - check obj.tField1 == "tuple" - check obj.tField2 == 1 - - test "modification of tuple fields via an alias": - obj.tField1 &= " test" - obj.tField2.inc - check obj.field23 == ("tuple test", 2) diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index e9839b3c..a168d274 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -10,7 +10,7 @@ import sets, json, sequtils, os, strformat, tables, hashes, strutils, math, import typetraits except distinctBase import common, cli, packageinfotypes, packageinfo, packageparser, options, - version, aliasthis, paths, displaymessages, sha1hashes, + version, paths, displaymessages, sha1hashes, tools, vcstools, syncfile, lockfile type @@ -63,7 +63,7 @@ type ## of the same file when its data is queried in the code. DevelopFileJsonKeys = enum - ## Develop file JSON objects names. + ## Develop file JSON objects names. dfjkVersion = "version" dfjkIncludes = "includes" dfjkDependencies = "dependencies" @@ -80,7 +80,7 @@ type ## Describes an invalid path to a Nimble package or included develop file. ## Contains the path as a key and the exact error occurred when we had tried ## to read the package or the develop file at it. - + ErrorsCollection = object ## Describes the different errors which are possible to occur on loading of ## a develop file. @@ -88,12 +88,6 @@ type invalidPackages: InvalidPaths invalidIncludeFiles: InvalidPaths -{.warning[UnsafeDefault]: off.} -{.warning[ProveInit]: off.} -aliasThis DevelopFileData.jsonData -{.warning[ProveInit]: on.} -{.warning[UnsafeDefault]: on.} - const developFileName* = "nimble.develop" ## The default name of a Nimble's develop file. This must always be the name @@ -121,9 +115,9 @@ proc getPkgDevFilePath(pkg: PackageInfo): Path = pkg.getNimbleFilePath / developFileName proc isEmpty*(data: DevelopFileData): bool = - ## Checks whether there is some content (paths to packages directories or + ## Checks whether there is some content (paths to packages directories or ## includes to other develop files) in the develop file. - data.includes.len == 0 and data.dependencies.len == 0 + data.jsonData.includes.len == 0 and data.jsonData.dependencies.len == 0 proc save*(data: DevelopFileData, path: Path, writeEmpty, overwrite: bool) = ## Saves the `data` to a JSON file with path `path`. If the `data` is empty @@ -138,8 +132,8 @@ proc save*(data: DevelopFileData, path: Path, writeEmpty, overwrite: bool) = let json = %{ $dfjkVersion: %developFileVersion, - $dfjkIncludes: %data.includes.toSeq, - $dfjkDependencies: %data.dependencies.toSeq, + $dfjkIncludes: %data.jsonData.includes.toSeq, + $dfjkDependencies: %data.jsonData.dependencies.toSeq, } if path.fileExists and not overwrite: @@ -248,7 +242,7 @@ proc addPackage(data: var DevelopFileData, pkgInfo: PackageInfo, ## but with different paths are registered for error ## reporting. - var pkg = data.nameToPkg.getOrDefault(pkgInfo.name) + var pkg = data.nameToPkg.getOrDefault(pkgInfo.basicInfo.name) if pkg == nil: # If a package with `pkgInfo.name` is missing add it to the # `DevelopFileData` internal data structures add it. @@ -256,7 +250,7 @@ proc addPackage(data: var DevelopFileData, pkgInfo: PackageInfo, pkg = pkgInfo.newClone {.warning[ProveInit]: on.} data.pkgRefCount.inc(pkg) - data.nameToPkg[pkg[].name] = pkg + data.nameToPkg[pkg[].basicInfo.name] = pkg data.pathToPkg[pkg[].getNimbleFilePath()] = pkg data.devFileNameToPkgs.add(comingFrom, pkg) data.pkgToDevFileNames.add(pkg, actualComingFrom) @@ -278,9 +272,9 @@ proc addPackage(data: var DevelopFileData, pkgInfo: PackageInfo, # register the name collision which to be reported as error. assertHasKey(data.pkgToDevFileNames, pkg) for devFileName in data.pkgToDevFileNames[pkg]: - collidingNames.add(pkg[].name, (alreadyIncludedPkgPath, devFileName)) + collidingNames.add(pkg[].basicInfo.name, (alreadyIncludedPkgPath, devFileName)) for devFileName in actualComingFrom: - collidingNames.add(pkg[].name, (newPkgPath, devFileName)) + collidingNames.add(pkg[].basicInfo.name, (newPkgPath, devFileName)) proc values[K, V](t: Table[K, V]): seq[V] = ## Returns a sequence containing table's `t` values. @@ -346,7 +340,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, var cache {.global.}: DevelopFileDataCache if cache.hasKey(path): return cache[path] - + result = initDevelopFileData() result.path = path result.dependentPkg = dependentPkg @@ -368,7 +362,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, except ValueError as error: raise nimbleError(notAValidDevFileJsonMsg($path), details = error) - for depPath in result.dependencies: + for depPath in result.jsonData.dependencies: let depPath = if depPath.isAbsolute: depPath.normalizedPath else: (path.splitFile.dir / depPath).normalizedPath let (pkgInfo, error) = validatePackage(depPath, options) @@ -377,7 +371,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, else: errors.invalidPackages[depPath] = error - for inclPath in result.includes: + for inclPath in result.jsonData.includes: let inclPath = inclPath.normalizedPath if visitedFiles.contains(inclPath): continue @@ -428,14 +422,14 @@ proc addDevelopPackage(data: var DevelopFileData, pkg: PackageInfo): bool = # Check whether the develop file already contains a package with a name # `pkg.name` at different path. - if data.nameToPkg.hasKey(pkg.name) and not data.pathToPkg.hasKey(pkgDir): - let otherPath = data.nameToPkg[pkg.name][].getNimbleFilePath() + if data.nameToPkg.hasKey(pkg.basicInfo.name) and not data.pathToPkg.hasKey(pkgDir): + let otherPath = data.nameToPkg[pkg.basicInfo.name][].getNimbleFilePath() displayError(pkgAlreadyPresentAtDifferentPathMsg( - pkg.name, $otherPath, $data.path)) + pkg.basicInfo.name, $otherPath, $data.path)) return false # Add `pkg` to the develop file model. - let success = not data.dependencies.containsOrIncl(pkgDir) + let success = not data.jsonData.dependencies.containsOrIncl(pkgDir) var collidingNames: CollidingNames addPackage(data, pkg, data.path, [data.path].toHashSet, collidingNames) @@ -509,7 +503,7 @@ proc removePackage(data: var DevelopFileData, pkg: ref PackageInfo, # But if the reference count is zero the package should be removed from all # other meta data structures to free memory for it and its indexes. - data.nameToPkg.del(pkg[].name) + data.nameToPkg.del(pkg[].basicInfo.name) data.pathToPkg.del(pkg[].getNimbleFilePath()) # The package `pkg` could already be missing from `pkgToDevFileNames` if it @@ -537,7 +531,7 @@ proc removeDevelopPackageByPath(data: var DevelopFileData, path: Path): bool = ## Returns `true` if path `path` is successfully removed from the develop file ## or `false` if there is no such path added in it. - let success = not data.dependencies.missingOrExcl(path) + let success = not data.jsonData.dependencies.missingOrExcl(path) if success: let nameAndVersion = data.pathToPkg[path][].getNameAndVersion() @@ -559,7 +553,7 @@ proc removeDevelopPackageByName(data: var DevelopFileData, name: string): bool = let pkg = data.nameToPkg.getOrDefault(name) path = if pkg != nil: pkg[].getNimbleFilePath() else: "" - success = not data.dependencies.missingOrExcl(path) + success = not data.jsonData.dependencies.missingOrExcl(path) if success: data.removePackage(path, data.path) @@ -594,16 +588,16 @@ proc includeDevelopFile(data: var DevelopFileData, path: Path, displayDetails(error) return false - let success = not data.includes.containsOrIncl(path) + let success = not data.jsonData.includes.containsOrIncl(path) if success: var errors: ErrorsCollection data.mergeIncludedDevFileData(inclFileData, errors) if errors.hasErrors: displayError(failedToInclInDevFileMsg($path, $data.path)) - displayDetails(errors.getErrorsDetails) + displayDetails(errors.getErrorsDetails) # Revert the inclusion in the case of merge errors. - data.includes.excl(path) + data.jsonData.includes.excl(path) for pkgPath, _ in inclFileData.pathToPkg: data.removePackage(pkgPath, path) return false @@ -622,7 +616,7 @@ proc excludeDevelopFile(data: var DevelopFileData, path: Path): bool = ## from the current project's develop file or `false` if there is no such ## file included in the current one. - let success = not data.includes.missingOrExcl(path) + let success = not data.jsonData.includes.missingOrExcl(path) if success: assertHasKey(data.devFileNameToPkgs, path) @@ -813,7 +807,7 @@ proc vcsRevisionIsNotPushed(depPkg: PackageInfo): bool = ## Checks whether current VCS revision of the working copy directory of a ## develop mode dependency package is pushed on some remote. not depPkg.getNimbleFileDir.isVcsRevisionPresentOnSomeRemote( - depPkg.vcsRevision) + depPkg.metaData.vcsRevision) proc workingCopyNeeds*(dependencyPkg, dependentPkg: PackageInfo, options: Options): NeedsOperation = @@ -824,15 +818,15 @@ proc workingCopyNeeds*(dependencyPkg, dependentPkg: PackageInfo, let lockFileVcsRev = dependentPkg.lockedDeps.getOrDefault( - dependencyPkg.name, notSetLockFileDep).vcsRevision + dependencyPkg.basicInfo.name, notSetLockFileDep).vcsRevision syncFile = getSyncFile(dependentPkg) - syncFileVcsRev = syncFile.getDepVcsRevision(dependencyPkg.name) + syncFileVcsRev = syncFile.getDepVcsRevision(dependencyPkg.basicInfo.name) workingCopyVcsRev = getVcsRevision(dependencyPkg.getNimbleFileDir) if lockFileVcsRev == syncFileVcsRev and syncFileVcsRev == workingCopyVcsRev: # When all revisions are matching nothing have to be done. return needsNone - + if lockFileVcsRev == syncFileVcsRev and syncFileVcsRev != workingCopyVcsRev: # When lock file and sync file revisions are matching, but working copy # revision is different, then most probably there are local changes and @@ -867,7 +861,7 @@ proc workingCopyNeeds*(dependencyPkg, dependentPkg: PackageInfo, return needsNone template addError(error: ValidationErrorKind) = - errors[depPkg.name] = ValidationError( + errors[depPkg.basicInfo.name] = ValidationError( path: depPkg.getNimbleFileDir, kind: error) proc findValidationErrorsOfDevDepsWithLockFile*( diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index a23f6d8e..1e0eb148 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -481,11 +481,11 @@ proc downloadPkg*(url: string, verRange: VersionRange, ## Makes sure that the downloaded package's version satisfies the requested ## version range. let pkginfo = getPkgInfo(result[0], options) - if pkginfo.version notin verRange: + if pkginfo.basicInfo.version notin verRange: raise nimbleError( "Downloaded package's version does not satisfy requested version " & "range: wanted $1 got $2." % - [$verRange, $pkginfo.version]) + [$verRange, $pkginfo.basicInfo.version]) proc echoPackageVersions*(pkg: Package) = let downMethod = pkg.downloadMethod diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index d954fa87..20faba3c 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -29,13 +29,13 @@ proc areLockedDepsLoaded*(pkgInfo: PackageInfo): bool = proc hasMetaData*(pkgInfo: PackageInfo): bool = # if the package info has loaded meta data its files list have to be not empty - pkgInfo.files.len > 0 + pkgInfo.metaData.files.len > 0 proc initPackageInfo*(filePath: string): PackageInfo = result = initPackageInfo() let (fileDir, fileName, _) = filePath.splitFile result.myPath = filePath - result.name = fileName + result.basicInfo.name = fileName result.backend = "c" result.lockedDeps = getLockedDependencies(fileDir) @@ -275,12 +275,12 @@ proc findNimbleFile*(dir: string; error: bool): string = proc setNameVersionChecksum*(pkgInfo: var PackageInfo, pkgDir: string) = let (name, version, checksum) = getNameVersionChecksum(pkgDir) - pkgInfo.name = name - if pkgInfo.version == notSetVersion: + pkgInfo.basicInfo.name = name + if pkgInfo.basicInfo.version == notSetVersion: # if there is no previously set version from the `.nimble` file - pkgInfo.version = version - pkgInfo.specialVersion = version - pkgInfo.checksum = checksum + pkgInfo.basicInfo.version = version + pkgInfo.metaData.specialVersion = version + pkgInfo.basicInfo.checksum = checksum proc getInstalledPackageMin*(pkgDir, nimbleFilePath: string): PackageInfo = result = initPackageInfo(nimbleFilePath) @@ -307,8 +307,8 @@ proc withinRange*(pkgInfo: PackageInfo, verRange: VersionRange): bool = ## Determines whether the specified package's version is within the ## specified range. The check works with ordinary versions as well as ## special ones. - return withinRange(pkgInfo.version, verRange) or - withinRange(pkgInfo.specialVersion, verRange) + return withinRange(pkgInfo.basicInfo.version, verRange) or + withinRange(pkgInfo.metaData.specialVersion, verRange) proc resolveAlias*(dep: PkgTuple, options: Options): PkgTuple = ## Looks up the specified ``dep.name`` in the packages.json files to resolve @@ -333,8 +333,8 @@ proc findPkg*(pkglist: seq[PackageInfo], dep: PkgTuple, ## ## **Note**: dep.name here could be a URL, hence the need for pkglist.meta. for pkg in pkglist: - if cmpIgnoreStyle(pkg.name, dep.name) != 0 and - cmpIgnoreStyle(pkg.url, dep.name) != 0: continue + if cmpIgnoreStyle(pkg.basicInfo.name, dep.name) != 0 and + cmpIgnoreStyle(pkg.metaData.url, dep.name) != 0: continue if pkg.isLink: # If `pkg.isLink` this is a develop mode package and develop mode packages # are always with higher priority than installed packages. Version range @@ -342,7 +342,7 @@ proc findPkg*(pkglist: seq[PackageInfo], dep: PkgTuple, r = pkg return true elif withinRange(pkg, dep.ver): - let isNewer = r.version < pkg.version + let isNewer = r.basicInfo.version < pkg.basicInfo.version if not result or isNewer: r = pkg result = true @@ -353,8 +353,8 @@ proc findAllPkgs*(pkglist: seq[PackageInfo], dep: PkgTuple): seq[PackageInfo] = ## packages if multiple are found. result = @[] for pkg in pkglist: - if cmpIgnoreStyle(pkg.name, dep.name) != 0 and - cmpIgnoreStyle(pkg.url, dep.name) != 0: continue + if cmpIgnoreStyle(pkg.basicInfo.name, dep.name) != 0 and + cmpIgnoreStyle(pkg.metaData.url, dep.name) != 0: continue if withinRange(pkg, dep.ver): result.add pkg @@ -513,7 +513,7 @@ proc hash*(x: PackageInfo): Hash = result = !$h proc getNameAndVersion*(pkgInfo: PackageInfo): string = - &"{pkgInfo.name}@{pkgInfo.version}" + &"{pkgInfo.basicInfo.name}@{pkgInfo.basicInfo.version}" when isMainModule: import unittest diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index 573ba329..e3e501e1 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import sets, tables -import version, aliasthis, sha1hashes +import version, sha1hashes type DownloadMethod* {.pure.} = enum @@ -77,11 +77,3 @@ type alias*: string ## A name of another package, that this package aliases. PackageDependenciesInfo* = tuple[deps: HashSet[PackageInfo], pkg: PackageInfo] - -{.warning[UnsafeDefault]: off.} -{.warning[ProveInit]: off.} -aliasThis PackageInfo.metaData -{.warning[ProveInit]: on.} -aliasThis PackageInfo.basicInfo -{.warning[ProveInit]: on.} -{.warning[UnsafeDefault]: on.} diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index b43646bb..d950ddf3 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -100,10 +100,10 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = (x) => x.changeFileExt("").toLowerAscii() ) correctDir = - if pkgInfo.name.toLowerAscii() in normalizedBinNames: - pkgInfo.name & "pkg" + if pkgInfo.basicInfo.name.toLowerAscii() in normalizedBinNames: + pkgInfo.basicInfo.name & "pkg" else: - pkgInfo.name + pkgInfo.basicInfo.name proc onFile(path: string) = # Remove the root to leave only the package subdirectories. @@ -117,7 +117,7 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = return if dir.len == 0: - if file != pkgInfo.name: + if file != pkgInfo.basicInfo.name: # A source file was found in the top level of srcDir that doesn't share # a name with the package. let @@ -126,7 +126,7 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = "should contain at most one module, " & "named '$2', but a file named '$3' was found. This " & "will be an error in the future.") % - [pkgInfo.name, pkgInfo.name & ext, file & ext] + [pkgInfo.basicInfo.name, pkgInfo.basicInfo.name & ext, file & ext] hint = ("If this is the primary source file in the package, " & "rename it to '$1'. If it's a source file required by " & "the main module, or if it is one of several " & @@ -135,7 +135,7 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = "to build the the package '$1', prevent its installation " & "by adding `skipFiles = @[\"$3\"]` to the .nimble file. See " & "https://github.com/nim-lang/nimble#libraries for more info.") % - [pkgInfo.name & ext, correctDir & DirSep, file & ext, pkgInfo.name] + [pkgInfo.basicInfo.name & ext, correctDir & DirSep, file & ext, pkgInfo.basicInfo.name] raise validationError(msg, true, hint, true) else: assert(not pkgInfo.isMinimal) @@ -147,26 +147,26 @@ proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) = "for source files, named '$3', but file '$1' " & "is in a directory named '$4' instead. " & "This will be an error in the future.") % - [file & ext, pkgInfo.name, correctDir, dir] + [file & ext, pkgInfo.basicInfo.name, correctDir, dir] hint = ("If '$1' contains source files for building '$2', rename it " & "to '$3'. Otherwise, prevent its installation " & "by adding `skipDirs = @[\"$1\"]` to the .nimble file.") % - [dir, pkgInfo.name, correctDir] + [dir, pkgInfo.basicInfo.name, correctDir] raise validationError(msg, true, hint, true) iterInstallFiles(realDir, pkgInfo, options, onFile) proc validatePackageInfo(pkgInfo: PackageInfo, options: Options) = let path = pkgInfo.myPath - if pkgInfo.name == "": + if pkgInfo.basicInfo.name == "": raise validationError("Incorrect .nimble file: " & path & " does not contain a name field.", false) - if pkgInfo.name.normalize != path.splitFile.name.normalize: + if pkgInfo.basicInfo.name.normalize != path.splitFile.name.normalize: raise validationError( "The .nimble file name must match name specified inside " & path, true) - if pkgInfo.version == notSetVersion: + if pkgInfo.basicInfo.version == notSetVersion: raise validationError("Incorrect .nimble file: " & path & " does not contain a version field.", false) @@ -231,8 +231,8 @@ proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = case currentSection.normalize of "package": case ev.key.normalize - of "name": result.name = ev.value - of "version": result.version = newVersion(ev.value) + of "name": result.basicInfo.name = ev.value + of "version": result.basicInfo.version = newVersion(ev.value) of "author": result.author = ev.value of "description": result.description = ev.value of "license": result.license = ev.value @@ -316,10 +316,10 @@ proc inferInstallRules(pkgInfo: var PackageInfo, options: Options) = # them and prevent the installation of anything else. The user can always # override this with `installFiles`. if pkgInfo.srcDir == "": - if dirExists(pkgInfo.getRealDir() / pkgInfo.name): - pkgInfo.installDirs.add(pkgInfo.name) - if fileExists(pkgInfo.getRealDir() / pkgInfo.name.addFileExt("nim")): - pkgInfo.installFiles.add(pkgInfo.name.addFileExt("nim")) + if dirExists(pkgInfo.getRealDir() / pkgInfo.basicInfo.name): + pkgInfo.installDirs.add(pkgInfo.basicInfo.name) + if fileExists(pkgInfo.getRealDir() / pkgInfo.basicInfo.name.addFileExt("nim")): + pkgInfo.installFiles.add(pkgInfo.basicInfo.name.addFileExt("nim")) proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): PackageInfo = @@ -381,20 +381,20 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): if not fileDir.startsWith(options.getPkgsDir()): # If the `.nimble` file is not in the installation directory we have to get # some of the package meta data from its directory. - result.checksum = calculateDirSha1Checksum(fileDir) + result.basicInfo.checksum = calculateDirSha1Checksum(fileDir) # By default specialVersion is the same as version. - result.specialVersion = result.version + result.metaData.specialVersion = result.basicInfo.version # If the `fileDir` is a VCS repository we can get some of the package meta # data from it. - result.vcsRevision = getVcsRevision(fileDir) + result.metaData.vcsRevision = getVcsRevision(fileDir) case getVcsType(fileDir) - of vcsTypeGit: result.downloadMethod = DownloadMethod.git - of vcsTypeHg: result.downloadMethod = DownloadMethod.hg + of vcsTypeGit: result.metaData.downloadMethod = DownloadMethod.git + of vcsTypeHg: result.metaData.downloadMethod = DownloadMethod.hg of vcsTypeNone: discard try: - result.url = getRemoteFetchUrl(fileDir, + result.metaData.url = getRemoteFetchUrl(fileDir, getCorrespondingRemoteAndBranch(fileDir).remote) except NimbleError: discard @@ -411,7 +411,7 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): # Validate the rest of the package info last. if not options.disableValidation: - validateVersion($result.version) + validateVersion($result.basicInfo.version) validatePackageInfo(result, options) proc getPkgInfoFromFile*(file: NimbleFile, options: Options, @@ -497,13 +497,13 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = # The `isLink` data from the meta data file is with priority because of the # old format develop packages. result.isLink = pkg.isLink - result.specialVersion = pkg.specialVersion + result.metaData.specialVersion = pkg.metaData.specialVersion assert not (pkg.isInstalled and pkg.isLink), "A package must not be simultaneously installed and linked." if result.isInstalled: - assert result.vcsRevision == notSetSha1Hash, + assert result.metaData.vcsRevision == notSetSha1Hash, "Should not have a VCS revision read from package directory for " & "installed packages." @@ -515,10 +515,10 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = proc getConcreteVersion*(pkgInfo: PackageInfo, options: Options): Version = ## Returns a non-special version from the specified ``pkgInfo``. If the ## ``pkgInfo`` is minimal it looks it up and retrieves the concrete version. - result = pkgInfo.version + result = pkgInfo.basicInfo.version if pkgInfo.isMinimal: let pkgInfo = pkgInfo.toFullInfo(options) - result = pkgInfo.version + result = pkgInfo.basicInfo.version assert not result.isSpecial when isMainModule: diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index b968ae4c..84439ef8 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -146,7 +146,7 @@ proc editJson(p: PackageInfo; url, tags, downloadMethod: string) = var contents = parseFile("packages.json") doAssert contents.kind == JArray contents.add(%*{ - "name": p.name, + "name": p.basicInfo.name, "url": url, "method": downloadMethod, "tags": tags.split(), @@ -225,7 +225,7 @@ proc publish*(p: PackageInfo, o: Options) = "No .git nor .hg directory found. Stopping.") if url.len == 0: - url = promptCustom("Github URL of " & p.name & "?", "") + url = promptCustom("Github URL of " & p.basicInfo.name & "?", "") if url.len == 0: userAborted() let tags = promptCustom( @@ -235,10 +235,10 @@ proc publish*(p: PackageInfo, o: Options) = cd pkgsDir: editJson(p, url, tags, downloadMethod) - let branchName = "add-" & p.name & getTime().utc.format("HHmm") + let branchName = "add-" & p.basicInfo.name & getTime().utc.format("HHmm") doCmd("git checkout -B " & branchName) - doCmd("git commit packages.json -m \"Added package " & p.name & "\"") + doCmd("git commit packages.json -m \"Added package " & p.basicInfo.name & "\"") display("Pushing", "to remote of fork.", priority = HighPriority) doCmd("git push https://" & auth.token & "@github.com/" & auth.user & "/packages " & branchName) - let prUrl = createPullRequest(auth, p.name, branchName) + let prUrl = createPullRequest(auth, p.basicInfo.name, branchName) display("Success:", "Pull request successful, check at " & prUrl , Success, HighPriority) diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index d0c2dcca..73980aa0 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -59,9 +59,9 @@ proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, var dependency: JsonNode if not pkg.isLink: dependency = %{ - $ndjkRevDepName: %pkg.name.toLower, - $ndjkRevDepVersion: %pkg.version, - $ndjkRevDepChecksum: %pkg.checksum} + $ndjkRevDepName: %pkg.basicInfo.name.toLower, + $ndjkRevDepVersion: %pkg.basicInfo.version, + $ndjkRevDepChecksum: %pkg.basicInfo.checksum} else: dependency = %{ $ndjkRevDepPath: %pkg.getNimbleFileDir().absolutePath } @@ -86,7 +86,7 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = if rd[$ndjkRevDepPath].str != pkg.getNimbleFileDir: # It is compared by its directory path. newVal.add rd - elif rd[$ndjkRevDepChecksum].str != $pkg.checksum: + elif rd[$ndjkRevDepChecksum].str != $pkg.basicInfo.checksum: # For the reverse dependencies added since the introduction of the # new format comparison of the checksums is specific enough. newVal.add rd @@ -95,9 +95,9 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = # from the old format packages and they must be compared by the # `name` and `specialVersion` fields. if rd[$ndjkRevDepChecksum].str.len == 0 and - pkg.checksum == notSetSha1Hash: - if rd[$ndjkRevDepName].str != pkg.name.toLower or - rd[$ndjkRevDepVersion].str != $pkg.specialVersion: + pkg.basicInfo.checksum == notSetSha1Hash: + if rd[$ndjkRevDepName].str != pkg.basicInfo.name.toLower or + rd[$ndjkRevDepVersion].str != $pkg.metaData.specialVersion: newVal.add rd revDepsForVersion[checksum] = newVal @@ -154,7 +154,7 @@ proc toRevDep*(pkg: PackageInfo): ReverseDependency = if not pkg.isLink: result = ReverseDependency( kind: rdkInstalled, - pkgInfo: (pkg.name, pkg.version, pkg.checksum)) + pkgInfo: (pkg.basicInfo.name, pkg.basicInfo.version, pkg.basicInfo.checksum)) else: result = ReverseDependency( kind: rdkDevelop, @@ -367,4 +367,4 @@ when isMainModule: nimbleData.getAllRevDeps(authRevDep, revDeps) check revDeps == [authRevDep, nimforum1RevDep, nimforum2RevDep, nimforumDevelopRevDep, captchaRevDep].toHashSet - \ No newline at end of file + diff --git a/src/nimblepkg/syncfile.nim b/src/nimblepkg/syncfile.nim index 030b7dd2..f28c94de 100644 --- a/src/nimblepkg/syncfile.nim +++ b/src/nimblepkg/syncfile.nim @@ -50,7 +50,7 @@ proc getSyncFilePath(pkgInfo: PackageInfo): Path = "supported type of version control.", hint = "Put package's working directory under version control.") - return vcsSpecialDirPath / (pkgInfo.name & syncFileExt).Path + return vcsSpecialDirPath / (pkgInfo.basicInfo.name & syncFileExt).Path proc load(syncFile: ref SyncFile, path: Path) = ## Loads a sync file. diff --git a/src/nimblepkg/topologicalsort.nim b/src/nimblepkg/topologicalsort.nim index b1a65d0f..302e49ed 100644 --- a/src/nimblepkg/topologicalsort.nim +++ b/src/nimblepkg/topologicalsort.nim @@ -22,19 +22,19 @@ proc getDependencies(packages: seq[PackageInfo], package: PackageInfo, raise nimbleError( "Cannot build the dependency graph.\n" & &"Missing package \"{dep.name}\".") - result.add depPkgInfo.name + result.add depPkgInfo.basicInfo.name proc buildDependencyGraph*(packages: seq[PackageInfo], options: Options): LockFileDeps = ## Creates records which will be saved to the lock file. for pkgInfo in packages: - result[pkgInfo.name] = LockFileDep( - version: pkgInfo.version, - vcsRevision: pkgInfo.vcsRevision, - url: pkgInfo.url, - downloadMethod: pkgInfo.downloadMethod, + result[pkgInfo.basicInfo.name] = LockFileDep( + version: pkgInfo.basicInfo.version, + vcsRevision: pkgInfo.metaData.vcsRevision, + url: pkgInfo.metaData.url, + downloadMethod: pkgInfo.metaData.downloadMethod, dependencies: getDependencies(packages, pkgInfo, options), - checksums: Checksums(sha1: pkgInfo.checksum)) + checksums: Checksums(sha1: pkgInfo.basicInfo.checksum)) proc topologicalSort*(graph: LockFileDeps): tuple[order: seq[string], cycles: seq[seq[string]]] = diff --git a/tests/tmoduletests.nim b/tests/tmoduletests.nim index 9269ace0..87cb22c0 100644 --- a/tests/tmoduletests.nim +++ b/tests/tmoduletests.nim @@ -13,7 +13,6 @@ suite "Module tests": check execCmdEx("nim c -r src/nimblepkg/" & moduleName). exitCode == QuitSuccess - moduleTest "aliasthis" moduleTest "common" moduleTest "download" moduleTest "jsonhelpers" From 5d432bb2437957e8f16a283fdb449ed246773b00 Mon Sep 17 00:00:00 2001 From: narimiran Date: Wed, 28 Jul 2021 14:27:03 +0200 Subject: [PATCH 62/73] Revert "remove unnecessary `dup`" This reverts commit 2807e3c61072f1e52ede3297164b7ab79b658c80. --- src/nimble.nim | 4 ++-- src/nimblepkg/common.nim | 2 ++ src/nimblepkg/developfile.nim | 4 +--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index eef01243..8b04f1b3 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -1480,7 +1480,7 @@ proc validateDevModeDepsWorkingCopiesBeforeLock( } # Remove not errors from the errors set. - for name, error in errors: + for name, error in common.dup(errors): if error.kind in notAnErrorSet: errors.del name @@ -1699,7 +1699,7 @@ proc sync(options: Options) = var errors: ValidationErrors findValidationErrorsOfDevDepsWithLockFile(pkgInfo, options, errors) - for name, error in errors: + for name, error in common.dup(errors): if error.kind == vekWorkingCopyNeedsSync: if not options.action.listOnly: syncWorkingCopy(name, error.path, pkgInfo, options) diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 856dcead..412eba0a 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -52,6 +52,8 @@ template newClone*[T: not ref](obj: T): ref T = result[] = obj result +proc dup*[T](obj: T): T = obj + proc `$`*(p: ptr | ref): string = cast[int](p).toHex ## Converts the pointer `p` to its hex string representation. diff --git a/src/nimblepkg/developfile.nim b/src/nimblepkg/developfile.nim index a168d274..45a8d284 100644 --- a/src/nimblepkg/developfile.nim +++ b/src/nimblepkg/developfile.nim @@ -246,9 +246,7 @@ proc addPackage(data: var DevelopFileData, pkgInfo: PackageInfo, if pkg == nil: # If a package with `pkgInfo.name` is missing add it to the # `DevelopFileData` internal data structures add it. - {.warning[ProveInit]: off.} pkg = pkgInfo.newClone - {.warning[ProveInit]: on.} data.pkgRefCount.inc(pkg) data.nameToPkg[pkg[].basicInfo.name] = pkg data.pathToPkg[pkg[].getNimbleFilePath()] = pkg @@ -387,7 +385,7 @@ proc load(path: Path, dependentPkg: PackageInfo, options: Options, # If this is a package develop file, but not a free one, for each of the # package's develop mode dependencies load its develop file if it is not # already loaded and merge its data to the current develop file's data. - for path, pkg in result.pathToPkg: + for path, pkg in result.pathToPkg.dup: if visitedPkgs.contains(path): continue var followedPkgDevFileData = initDevelopFileData() From 29ba18e452b5d31fe89327cabd7cc114abb988f7 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Sun, 1 Aug 2021 02:11:26 +0300 Subject: [PATCH 63/73] Make special version a set of versions Special versions are made a set of versions. Those are aliases with which a single package can be referred. For example, a package can be simultaneously versions: * 0.1.0 - the normal version from the Nimble file. * #head - the latest commit in the main branch * #master - the main branch name * 3c91b869 - part of the sha1 hash of the latest commit in the main branch When the same package is downloaded a second time (determined by the checksum) instead of proposing to replace it just print a warning that the package is already installed and merge the special version of the new package with a special version of the already installed one. Additionally this commit: - Removes some legacy code for supporting the old package format in the reverse dependencies. - The names of the packages in the reverse dependencies are written without converting to lower case. - The tests are fixed according to the new behavior. Related to nim-lang/nimble#127 --- src/nimble.nim | 100 +++++++++++++++----------- src/nimblepkg/displaymessages.nim | 12 +++- src/nimblepkg/packageinfo.nim | 13 ++-- src/nimblepkg/packageinfotypes.nim | 5 +- src/nimblepkg/packagemetadatafile.nim | 34 +++++++-- src/nimblepkg/packageparser.nim | 4 +- src/nimblepkg/reversedeps.nim | 43 +++++------ src/nimblepkg/version.nim | 10 ++- tests/tdevelopfeature.nim | 28 ++++---- tests/tmultipkgs.nim | 37 +++++++--- tests/tnimscript.nim | 2 + tests/treversedeps.nim | 2 + 12 files changed, 183 insertions(+), 107 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index 8b04f1b3..bc5ce811 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -76,8 +76,8 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): var pkgList {.global.}: seq[PackageInfo] = @[] once: pkgList = initPkgList(pkgInfo, options) - display("Verifying", - "dependencies for $1@$2" % [pkgInfo.basicInfo.name, $pkgInfo.metaData.specialVersion], + display("Verifying", "dependencies for $1@$2" % + [pkgInfo.basicInfo.name, $pkgInfo.basicInfo.version], priority = HighPriority) var reverseDependencies: seq[PackageBasicInfo] = @[] @@ -106,7 +106,15 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): let (packages, installedPkg) = install(toInstall, options, doPrompt = false, first = false, fromLockFile = false) - result.incl packages + for pkg in packages: + if result.contains pkg: + # If the result already contains the newly tried to install package + # we had to merge its special versions set into the set of the old + # one. + result[pkg].metaData.specialVersions.incl( + pkg.metaData.specialVersions) + else: + result.incl pkg pkg = installedPkg # For addRevDep fillMetaData(pkg, pkg.getRealDir(), false) @@ -119,7 +127,7 @@ proc processFreeDependencies(pkgInfo: PackageInfo, options: Options): # Process the dependencies of this dependency. result.incl processFreeDependencies(pkg.toFullInfo(options), options) if not pkg.isLink: - reverseDependencies.add((pkg.basicInfo.name, pkg.metaData.specialVersion, pkg.basicInfo.checksum)) + reverseDependencies.add(pkg.basicInfo) # Check if two packages of the same name (but different version) are listed # in the path. @@ -260,27 +268,24 @@ proc removePackage(pkgInfo: PackageInfo, options: Options) = reinstallSymlinksForOlderVersion(pkgDestDir, options) options.nimbleData.removeRevDep(pkgInfo) -proc packageExists(pkgInfo: PackageInfo, options: Options): bool = - let pkgDestDir = pkgInfo.getPkgDest(options) - return fileExists(pkgDestDir / packageMetaDataFileName) - -proc promptOverwriteExistingPackage(pkgInfo: PackageInfo, - options: Options): bool = - let message = "$1@$2 already exists. Overwrite?" % - [pkgInfo.basicInfo.name, $pkgInfo.metaData.specialVersion] - return options.prompt(message) - -proc removeOldPackage(pkgInfo: PackageInfo, options: Options) = +proc packageExists(pkgInfo: PackageInfo, options: Options): + Option[PackageInfo] = + ## Checks whether a package `pkgInfo` already exists in the Nimble cache. If a + ## package already exists returns the `PackageInfo` of the package in the + ## cache otherwise returns `none`. Raises a `NimbleError` in the case the + ## package exists in the cache but it is not valid. let pkgDestDir = pkgInfo.getPkgDest(options) - let oldPkgInfo = getPkgInfo(pkgDestDir, options) - removePackage(oldPkgInfo, options) - -proc promptRemovePackageIfExists(pkgInfo: PackageInfo, options: Options): bool = - if packageExists(pkgInfo, options): - if not promptOverwriteExistingPackage(pkgInfo, options): - return false - removeOldPackage(pkgInfo, options) - return true + if not fileExists(pkgDestDir / packageMetaDataFileName): + return none[PackageInfo]() + else: + var oldPkgInfo = initPackageInfo() + try: + oldPkgInfo = pkgDestDir.getPkgInfo(options) + except CatchableError as error: + raise nimbleError(&"The package inside \"{pkgDestDir}\" is invalid.", + details = error) + fillMetaData(oldPkgInfo, pkgDestDir, true) + return some(oldPkgInfo) proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): HashSet[PackageInfo] @@ -329,9 +334,10 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, var depsOptions = options depsOptions.depsOnly = false - # Overwrite the version if the requested version is "#head" or similar. if requestedVer.kind == verSpecial: - pkgInfo.metaData.specialVersion = requestedVer.spe + # Add a version alias to special versions set if requested version is a + # special one. + pkgInfo.metaData.specialVersions.incl requestedVer.spe # Dependencies need to be processed before the creation of the pkg dir. if first and pkgInfo.lockedDeps.len > 0: @@ -343,10 +349,24 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, result.pkg = pkgInfo return result - display("Installing", "$1@$2" % [pkginfo.basicInfo.name, $pkginfo.metaData.specialVersion], - priority = HighPriority) + display("Installing", "$1@$2" % + [pkginfo.basicInfo.name, $pkginfo.basicInfo.version], + priority = HighPriority) - let isPackageAlreadyInCache = pkgInfo.packageExists(options) + let oldPkg = pkgInfo.packageExists(options) + if oldPkg.isSome: + # In the case we already have the same package in the cache then only merge + # the new package special versions to the old one. + displayWarning(pkgAlreadyExistsInTheCacheMsg(pkgInfo)) + var oldPkg = oldPkg.get + oldPkg.metaData.specialVersions.incl pkgInfo.metaData.specialVersions + saveMetaData(oldPkg.metaData, oldPkg.getNimbleFileDir, changeRoots = false) + if result.deps.contains oldPkg: + result.deps[oldPkg].metaData.specialVersions.incl( + oldPkg.metaData.specialVersions) + result.deps.incl oldPkg + result.pkg = oldPkg + return # Build before removing an existing package (if one exists). This way # if the build fails then the old package will still be installed. @@ -361,8 +381,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, try: buildFromDir(pkgInfo, paths, "-d:release" & flags, options) except CatchableError: - if not isPackageAlreadyInCache: - removeRevDep(options.nimbleData, pkgInfo) + removeRevDep(options.nimbleData, pkgInfo) raise let pkgDestDir = pkgInfo.getPkgDest(options) @@ -374,9 +393,6 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # Don't copy artifacts if project local deps mode and "installing" the top # level package. if not (options.localdeps and options.isInstallingTopLevel(dir)): - if not promptRemovePackageIfExists(pkgInfo, options): - return - createDir(pkgDestDir) # Copy this package's files based on the preferences specified in PkgInfo. var filesInstalled: HashSet[string] @@ -797,18 +813,22 @@ proc list(options: Options) = echo(" ") proc listInstalled(options: Options) = - var h: OrderedTable[string, seq[Version]] + type + VersionChecksumTuple = tuple[version: Version, checksum: Sha1Hash] + var h: OrderedTable[string, seq[VersionChecksumTuple]] let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) for pkg in pkgs: let pName = pkg.basicInfo.name - pVer = pkg.metaData.specialVersion + pVersion = pkg.basicInfo.version + pChecksum = pkg.basicInfo.checksum if not h.hasKey(pName): h[pName] = @[] var s = h[pName] - add(s, pVer) + add(s, (pVersion, pChecksum)) h[pName] = s - h.sort(proc (a, b: (string, seq[Version])): int = cmpIgnoreCase(a[0], b[0])) + h.sort(proc (a, b: (string, seq[VersionChecksumTuple])): int = + cmpIgnoreCase(a[0], b[0])) for k in keys(h): echo k & " [" & h[k].join(", ") & "]" @@ -837,7 +857,7 @@ proc listPaths(options: Options) = # There may be several, list all available ones and sort by version. for pkg in pkgs: if name == pkg.basicInfo.name: - installed.add((pkg.metaData.specialVersion, pkg.getRealDir)) + installed.add((pkg.basicInfo.version, pkg.getRealDir)) if installed.len > 0: sort(installed, cmp[VersionAndPath], Descending) @@ -1127,7 +1147,7 @@ proc uninstall(options: var Options) = if len(revDeps - pkgsToDelete) > 0: let pkgs = revDeps.collectNames(true) displayWarning( - cannotUninstallPkgMsg(pkgTup.name, pkg.metaData.specialVersion, pkgs)) + cannotUninstallPkgMsg(pkgTup.name, pkg.basicInfo.version, pkgs)) else: pkgsToDelete.incl pkg.toRevDep diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 013b7a30..1068425b 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -6,7 +6,7 @@ ## the message to be repeated both in Nimble and the testing code. import strformat, strutils -import version +import version, packageinfotypes, sha1hashes const validationFailedMsg* = "Validation failed." @@ -143,3 +143,13 @@ proc invalidDevelopDependenciesVersionsMsg*(errors: seq[string]): string = for error in errors: result &= "\n" result &= error + +proc pkgAlreadyExistsInTheCacheMsg*(name, version, checksum: string): string = + &"A package \"{name}@{version}\" with checksum \"{checksum}\" already " & + "exists the the cache." + +proc pkgAlreadyExistsInTheCacheMsg*(pkgInfo: PackageInfo): string = + pkgAlreadyExistsInTheCacheMsg( + pkgInfo.basicInfo.name, + $pkgInfo.basicInfo.version, + $pkgInfo.basicInfo.checksum) diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 20faba3c..ca30441d 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -279,7 +279,7 @@ proc setNameVersionChecksum*(pkgInfo: var PackageInfo, pkgDir: string) = if pkgInfo.basicInfo.version == notSetVersion: # if there is no previously set version from the `.nimble` file pkgInfo.basicInfo.version = version - pkgInfo.metaData.specialVersion = version + pkgInfo.metaData.specialVersions.incl version pkgInfo.basicInfo.checksum = checksum proc getInstalledPackageMin*(pkgDir, nimbleFilePath: string): PackageInfo = @@ -304,11 +304,10 @@ proc getInstalledPkgsMin*(libsDir: string, options: Options): seq[PackageInfo] = result.add pkg proc withinRange*(pkgInfo: PackageInfo, verRange: VersionRange): bool = - ## Determines whether the specified package's version is within the - ## specified range. The check works with ordinary versions as well as - ## special ones. - return withinRange(pkgInfo.basicInfo.version, verRange) or - withinRange(pkgInfo.metaData.specialVersion, verRange) + ## Determines whether the specified package's version is within the specified + ## range. As the ordinary version is always added to the special versions set + ## checking only the special versions is enough. + return withinRange(pkgInfo.metaData.specialVersions, verRange) proc resolveAlias*(dep: PkgTuple, options: Options): PkgTuple = ## Looks up the specified ``dep.name`` in the packages.json files to resolve @@ -496,7 +495,7 @@ proc iterInstallFiles*(realDir: string, pkgInfo: PackageInfo, action(file) proc getCacheDir*(pkgInfo: PackageBasicInfo): string = - &"{pkgInfo.name}-{pkgInfo.version}-{pkgInfo.checksum}" + &"{pkgInfo.name}-{pkgInfo.version}-{$pkgInfo.checksum}" proc getPkgDest*(pkgInfo: PackageBasicInfo, options: Options): string = options.getPkgsDir() / pkgInfo.getCacheDir() diff --git a/src/nimblepkg/packageinfotypes.nim b/src/nimblepkg/packageinfotypes.nim index e3e501e1..482ab1c3 100644 --- a/src/nimblepkg/packageinfotypes.nim +++ b/src/nimblepkg/packageinfotypes.nim @@ -27,7 +27,10 @@ type vcsRevision*: Sha1Hash files*: seq[string] binaries*: seq[string] - specialVersion*: Version + specialVersions*: HashSet[Version] + # Special versions are aliases with which a single package can be + # referred. For example a package can be versions `0.1.0`, `#head` and + # `#master` at the same time. PackageBasicInfo* = tuple name: string diff --git a/src/nimblepkg/packagemetadatafile.nim b/src/nimblepkg/packagemetadatafile.nim index 48d88fa0..6d28ab8e 100644 --- a/src/nimblepkg/packagemetadatafile.nim +++ b/src/nimblepkg/packagemetadatafile.nim @@ -1,7 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import json, os, strformat +import json, os, strformat, sets, sequtils import common, version, packageinfotypes, cli, tools, sha1hashes type @@ -17,20 +17,40 @@ const proc initPackageMetaData*(): PackageMetaData = result = PackageMetaData( - specialVersion: notSetVersion, vcsRevision: notSetSha1Hash) proc metaDataError(msg: string): ref MetaDataError = newNimbleError[MetaDataError](msg) -proc saveMetaData*(metaData: PackageMetaData, dirName: string) = +proc `%`(specialVersions: HashSet[Version]): JsonNode = + %specialVersions.toSeq + +proc initFromJson(specialVersions: var HashSet[Version], jsonNode: JsonNode, + jsonPath: var string) = + case jsonNode.kind + of JArray: + let originalJsonPathLen = jsonPath.len + for i in 0 ..< jsonNode.len: + jsonPath.add '[' + jsonPath.addInt i + jsonPath.add ']' + var version = newVersion("") + initFromJson(version, jsonNode[i], jsonPath) + specialVersions.incl version + jsonPath.setLen originalJsonPathLen + else: + assert false, "The `jsonNode` must be of kind JArray." + +proc saveMetaData*(metaData: PackageMetaData, dirName: string, + changeRoots = true) = ## Saves some important data to file in the package installation directory. var metaDataWithChangedPaths = metaData - for i, file in metaData.files: - metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file) + if changeRoots: + for i, file in metaData.files: + metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file) let json = %{ $pmdjkVersion: %packageMetaDataFileVersion, - $pmdjkMetaData: %metaDataWithChangedPaths } + $pmdjkMetaData: %metaDataWithChangedPaths} writeFile(dirName / packageMetaDataFileName, json.pretty) proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData = @@ -39,7 +59,9 @@ proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData = let fileName = dirName / packageMetaDataFileName if fileExists(fileName): {.warning[ProveInit]: off.} + {.warning[UnsafeSetLen]: off.} result = parseFile(fileName)[$pmdjkMetaData].to(PackageMetaData) + {.warning[UnsafeSetLen]: on.} {.warning[ProveInit]: on.} elif raiseIfNotFound: raise metaDataError(&"No {packageMetaDataFileName} file found in {dirName}") diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index d950ddf3..3e8f8faa 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -383,7 +383,7 @@ proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): # some of the package meta data from its directory. result.basicInfo.checksum = calculateDirSha1Checksum(fileDir) # By default specialVersion is the same as version. - result.metaData.specialVersion = result.basicInfo.version + result.metaData.specialVersions.incl result.basicInfo.version # If the `fileDir` is a VCS repository we can get some of the package meta # data from it. result.metaData.vcsRevision = getVcsRevision(fileDir) @@ -497,7 +497,7 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = # The `isLink` data from the meta data file is with priority because of the # old format develop packages. result.isLink = pkg.isLink - result.metaData.specialVersion = pkg.metaData.specialVersion + result.metaData.specialVersions.incl pkg.metaData.specialVersions assert not (pkg.isInstalled and pkg.isLink), "A package must not be simultaneously installed and linked." diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 73980aa0..f3d06e93 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -1,7 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import json, sets, os, hashes, unicode +import json, sets, os, hashes import options, version, download, jsonhelpers, nimbledatafile, sha1hashes, packageinfotypes, packageinfo, packageparser @@ -51,7 +51,7 @@ proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, let dependencies = nimbleData.addIfNotExist( $ndjkRevDep, - dep.name.toLower, + dep.name, $dep.version, $dep.checksum, newJArray()) @@ -59,7 +59,7 @@ proc addRevDep*(nimbleData: JsonNode, dep: PackageBasicInfo, var dependency: JsonNode if not pkg.isLink: dependency = %{ - $ndjkRevDepName: %pkg.basicInfo.name.toLower, + $ndjkRevDepName: %pkg.basicInfo.name, $ndjkRevDepVersion: %pkg.basicInfo.version, $ndjkRevDepChecksum: %pkg.basicInfo.checksum} else: @@ -87,18 +87,8 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = # It is compared by its directory path. newVal.add rd elif rd[$ndjkRevDepChecksum].str != $pkg.basicInfo.checksum: - # For the reverse dependencies added since the introduction of the - # new format comparison of the checksums is specific enough. + # Installed dependencies are compared by checksum. newVal.add rd - else: - # But if the both checksums are not present, those are converted - # from the old format packages and they must be compared by the - # `name` and `specialVersion` fields. - if rd[$ndjkRevDepChecksum].str.len == 0 and - pkg.basicInfo.checksum == notSetSha1Hash: - if rd[$ndjkRevDepName].str != pkg.basicInfo.name.toLower or - rd[$ndjkRevDepVersion].str != $pkg.metaData.specialVersion: - newVal.add rd revDepsForVersion[checksum] = newVal let reverseDependencies = nimbleData[$ndjkRevDep] @@ -109,7 +99,7 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = for key, val in reverseDependencies: remove(pkg, depTup, val) else: - let thisDep = nimbleData{$ndjkRevDep, depTup.name.toLower} + let thisDep = nimbleData{$ndjkRevDep, depTup.name} if thisDep.isNil: continue remove(pkg, depTup, thisDep) @@ -124,7 +114,7 @@ proc getRevDeps*(nimbleData: JsonNode, pkg: ReverseDependency): return let reverseDependencies = nimbleData[$ndjkRevDep]{ - pkg.pkgInfo.name.toLower}{$pkg.pkgInfo.version}{$pkg.pkgInfo.checksum} + pkg.pkgInfo.name}{$pkg.pkgInfo.version}{$pkg.pkgInfo.checksum} if reverseDependencies.isNil: return @@ -176,8 +166,7 @@ when isMainModule: proc initMetaData: PackageMetaData = result = PackageMetaData( - vcsRevision: notSetSha1Hash, - specialVersion: notSetVersion) + vcsRevision: notSetSha1Hash) proc parseRequires(requires: RequiresSeq): seq[PkgTuple] = requires.mapIt((it.name, it.versionRange.parseVersionRange)) @@ -200,7 +189,7 @@ when isMainModule: let nimforum1 = initPackageInfo( "nimforum", "0.1.0", "46a96c3f2b0ecb3d3f7bd71e12200ed401e9b9f2", - @[("jester", "0.1.0"), ("captcha", "1.0.0"), ("auth", "#head")]) + @[("jester", "0.1.0"), ("captcha", "1.0.0"), ("auth", "2.0.0")]) nimforum1RevDep = nimforum1.toRevDep nimforum2 = initPackageInfo( @@ -218,21 +207,22 @@ when isMainModule: jester = initPackageInfo( "jester", "0.1.0", "1b629f98b23614df292f176a1681fa439dcc05e2") - jesterWithoutSha1 = initPackageInfo("jester", "0.1.0", "") + jester2 = initPackageInfo( + "jester", "0.1.0", "deff1d836528db4fd128932ebd48e568e52b7bb4") captcha = initPackageInfo( "captcha", "1.0.0", "ce128561b06dd106a83638ad415a2a52548f388e") captchaRevDep = captcha.toRevDep auth = initPackageInfo( - "auth", "#head", "c81545df8a559e3da7d38d125e0eaf2b4478cd01") + "auth", "2.0.0", "c81545df8a559e3da7d38d125e0eaf2b4478cd01") authRevDep = auth.toRevDep suite "reverse dependencies": setup: var nimbleData = newNimbleDataNode() nimbleData.addRevDep(jester.basicInfo, nimforum1) - nimbleData.addRevDep(jesterWithoutSha1.basicInfo, play) + nimbleData.addRevDep(jester2.basicInfo, play) nimbleData.addRevDep(captcha.basicInfo, nimforum1) nimbleData.addRevDep(captcha.basicInfo, nimforum2) nimbleData.addRevDep(captcha.basicInfo, nimforumDevelop) @@ -253,7 +243,7 @@ when isMainModule: "checksum": "46a96c3f2b0ecb3d3f7bd71e12200ed401e9b9f2" } ], - "": [ + "deff1d836528db4fd128932ebd48e568e52b7bb4": [ { "name": "play", "version": "2.0.1", @@ -282,7 +272,7 @@ when isMainModule: } }, "auth": { - "#head": { + "2.0.0": { "c81545df8a559e3da7d38d125e0eaf2b4478cd01": [ { "name": "nimforum", @@ -313,7 +303,7 @@ when isMainModule: "reverseDeps": { "jester": { "0.1.0": { - "": [ + "deff1d836528db4fd128932ebd48e568e52b7bb4": [ { "name": "play", "version": "2.0.1", @@ -334,7 +324,7 @@ when isMainModule: } }, "auth": { - "#head": { + "2.0.0": { "c81545df8a559e3da7d38d125e0eaf2b4478cd01": [ { "name": "nimforum", @@ -367,4 +357,3 @@ when isMainModule: nimbleData.getAllRevDeps(authRevDep, revDeps) check revDeps == [authRevDep, nimforum1RevDep, nimforum2RevDep, nimforumDevelopRevDep, captchaRevDep].toHashSet - diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index 3ecaf932..5515274e 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. ## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5`` -import json +import json, sets import common, strutils, tables, hashes, parseutils type @@ -148,6 +148,14 @@ proc withinRange*(ver: Version, ran: VersionRange): bool = of verAny: return true +proc withinRange*(versions: HashSet[Version], range: VersionRange): bool = + ## Checks whether any of the versions from the set `versions` are in the range + ## `range`. + + for version in versions: + if withinRange(version, range): + return true + proc contains*(ran: VersionRange, ver: Version): bool = return withinRange(ver, ran) diff --git a/tests/tdevelopfeature.nim b/tests/tdevelopfeature.nim index e7af50fd..b0a2a0e5 100644 --- a/tests/tdevelopfeature.nim +++ b/tests/tdevelopfeature.nim @@ -17,8 +17,8 @@ suite "develop feature": dependentPkgName = "dependent" dependentPkgPath = "develop/dependent".normalizedPath includeFileName = "included.develop" - pkgAName = "packagea" - pkgBName = "packageb" + pkgAName = "PackageA" + pkgBName = "PackageB" pkgSrcDirTestName = "srcdirtest" pkgHybridName = "hybrid" depPath = "../dependency".normalizedPath @@ -173,7 +173,7 @@ suite "develop feature": let (output, exitCode) = execNimble( "develop", &"-p:{installDir}", pkgAName) - pkgAAbsPath = installDir / pkgAName + pkgAAbsPath = installDir / pkgAName.toLower developFileContent = developFile(@[], @[pkgAAbsPath]) check exitCode == QuitSuccess check parseFile(developFileName) == parseJson(developFileContent) @@ -190,8 +190,8 @@ suite "develop feature": let (output, exitCode) = execNimble( "develop", &"-p:{installDir}", pkgAName, pkgBName) - pkgAAbsPath = installDir / pkgAName - pkgBAbsPath = installDir / pkgBName + pkgAAbsPath = installDir / pkgAName.toLower + pkgBAbsPath = installDir / pkgBName.toLower developFileContent = developFile(@[], @[pkgAAbsPath, pkgBAbsPath]) check exitCode == QuitSuccess check parseFile(developFileName) == parseJson(developFileContent) @@ -518,13 +518,17 @@ suite "develop feature": check not devRevDepPath.isNil check devRevDepPath.str == depAbsPath - block checkSuccessfulUninstallAndRemovalFromNimbleData: + block checkSuccessfulUninstallButNotRemoveFromNimbleData: let - (_, exitCode) = execNimble("uninstall", "-i", pkgAName, "-y") + (_, exitCode) = execNimbleYes("uninstall", "-i", pkgAName) nimbleData = parseFile(installDir / nimbleDataFileName) check exitCode == QuitSuccess - check not nimbleData[$ndjkRevDep].hasKey(pkgAName) + # The package should remain in the Nimble data because in the case it + # is installed again it should continue to block its uninstalling + # without the "-i" option until all reverse dependencies (leaf nodes + # of the JSON object) are uninstalled. + check nimbleData[$ndjkRevDep].hasKey(pkgAName) test "follow develop dependency's develop file": cd "develop": @@ -848,8 +852,8 @@ suite "develop feature": "--with-dependencies", &"--develop-file:{developFile}", pkgBName) check exitCode == QuitSuccess let - pkgAPath = installDir / pkgAName - pkgBPath = installDir / pkgBName + pkgAPath = installDir / pkgAName.toLower + pkgBPath = installDir / pkgBName.toLower var lines = output.processOutput check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgAName, pkgAPath)) check lines.inLinesOrdered(pkgSetupInDevModeMsg(pkgBName, pkgBPath)) @@ -886,7 +890,7 @@ suite "develop feature": check errorCode == QuitFailure var lines = output.processOutput check lines.inLinesOrdered(pkgSetupInDevModeMsg( - pkgAName, installDir / pkgAName)) + pkgAName, installDir / pkgAName.toLower)) check lines.inLinesOrdered(pkgAddedInDevFileMsg( depNameAndVersion, depPath, developFileName)) check lines.inLinesOrdered(pkgAlreadyPresentAtDifferentPathMsg( @@ -904,6 +908,6 @@ suite "develop feature": includeFileName, developFileName)) check lines.inLinesOrdered(failedToLoadFileMsg(invalidInclFilePath)) let expectedDevelopFileContent = developFile( - @[includeFileName], @[dep2Path, installDir / pkgAName]) + @[includeFileName], @[dep2Path, installDir / pkgAName.toLower]) check parseFile(developFileName) == parseJson(expectedDevelopFileContent) diff --git a/tests/tmultipkgs.nim b/tests/tmultipkgs.nim index abcee9a1..976bd07e 100644 --- a/tests/tmultipkgs.nim +++ b/tests/tmultipkgs.nim @@ -3,22 +3,39 @@ {.used.} -import unittest, strutils +import unittest, os import testscommon +from nimblepkg/common import nimblePackagesDirName +from nimblepkg/version import `$` +from nimblepkg/sha1hashes import `$` +from nimblepkg/displaymessages import pkgAlreadyExistsInTheCacheMsg +from nimblepkg/tools import getNameVersionChecksum + +template installAlpha = + cleanDir installDir + var args {.inject.} = @["install", pkgMultiAlphaUrl] + let (output, exitCode) = execNimbleYes(args) + check exitCode == QuitSuccess + check output.processOutput.inLines("alpha installed successfully") + suite "multi": test "can install package from git subdir": - var - args = @["install", pkgMultiAlphaUrl] - (output, exitCode) = execNimbleYes(args) - check exitCode == QuitSuccess + installAlpha() - # Issue 785 - args.add @[pkgMultiBetaUrl, "-n"] - (output, exitCode) = execNimble(args) + test "do not replace a package if already installed": + installAlpha() + args.add pkgMultiBetaUrl + let (output, exitCode) = execNimbleYes(args) check exitCode == QuitSuccess - check output.contains("forced no") - check output.contains("beta installed successfully") + var lines = output.processOutput + for _, dir in walkDir(installDir / nimblePackagesDirName): + let (name, version, checksum) = getNameVersionChecksum(dir) + if name != "alpha": continue + check lines.inLinesOrdered( + pkgAlreadyExistsInTheCacheMsg(name, $version, $checksum)) + break + check lines.inLinesOrdered("beta installed successfully") test "can develop package from git subdir": cleanDir "beta" diff --git a/tests/tnimscript.nim b/tests/tnimscript.nim index 884e076d..789997b1 100644 --- a/tests/tnimscript.nim +++ b/tests/tnimscript.nim @@ -9,12 +9,14 @@ from nimblepkg/common import cd suite "nimscript": test "can install nimscript package": + cleanDir installDir cd "nimscript": let nim = findExe("nim").relativePath(base = getCurrentDir()) check execNimbleYes(["install", "--nim:" & nim]).exitCode == QuitSuccess test "before/after install pkg dirs are correct": + cleanDir installDir cd "nimscript": let (output, exitCode) = execNimbleYes(["install", "--nim:nim"]) check exitCode == QuitSuccess diff --git a/tests/treversedeps.nim b/tests/treversedeps.nim index 7928d349..6e29ccf4 100644 --- a/tests/treversedeps.nim +++ b/tests/treversedeps.nim @@ -47,6 +47,8 @@ suite "reverse dependencies": cd "revdep/pkgWithDep": verify execNimbleYes("install") + verify execNimbleYes("remove", "pkgA") + cd "revdep/pkgNoDep": verify execNimbleYes("install") From ab4cfcb4a9914a7050e9b2f0cae6ed615d9e9502 Mon Sep 17 00:00:00 2001 From: monyarm Date: Wed, 4 Aug 2021 11:31:47 +0300 Subject: [PATCH 64/73] Proofreading and editing. --- readme.markdown | 65 ++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/readme.markdown b/readme.markdown index 417de605..0cbed9c3 100644 --- a/readme.markdown +++ b/readme.markdown @@ -65,7 +65,7 @@ The Nimble change log can be found [here](https://github.com/nim-lang/nimble/blo Nimble has some runtime dependencies on external tools, these tools are used to download Nimble packages. For instance, if a package is hosted on [GitHub](https://github.com), you need to have [git](https://www.git-scm.com) -installed and added to your environment ``PATH``. Same goes for +installed and added to your environment ``PATH``. The same goes for [Mercurial](http://mercurial.selenic.com) repositories on [Bitbucket](https://bitbucket.org). Nimble packages are typically hosted in Git repositories so you may be able to get away without installing Mercurial. @@ -81,7 +81,7 @@ feature is supported by both **GitHub** and **BitBucket**. ## Installation Nimble is now bundled with [Nim](https://nim-lang.org) -(since Nim version 0.15.0). +(as of Nim version 0.15.0). This means that you should have Nimble installed already, as long as you have the latest version of Nim installed as well. Because of this **you likely do not need to install Nimble manually**. @@ -181,10 +181,10 @@ Example: nake installed successfully Nimble always fetches and installs the latest version of a package. Note that -latest version is defined as the latest tagged version in the Git (or Mercurial) +the latest version is defined as the latest tagged version in the Git (or Mercurial) repository, if the package has no tagged versions then the latest commit in the remote repository will be installed. If you already have that version installed, -Nimble will ask you whether you wish it to overwrite your local copy. +Nimble will ask you whether you wish to overwrite your local copy. You can force Nimble to download the latest commit from the package's repo, for example: @@ -200,7 +200,7 @@ version range, for example: $ nimble install nimgame@0.5 $ nimble install nimgame@"> 0.5" -The latter command will install a version which is greater than ``0.5``. +The latter command will install a version that is greater than ``0.5``. If you don't specify a parameter and there is a ``package.nimble`` file in your current working directory then Nimble will install the package residing in @@ -215,7 +215,7 @@ files. #### Package URLs -A valid URL to a Git or Merurial repository can also be specified, Nimble will +A valid URL to a Git or Mercurial repository can also be specified, Nimble will automatically detect the type of the repository that the url points to and install it. @@ -411,7 +411,7 @@ The command also adds `nimble.develop` and `nimble.paths` files to the ### nimble uninstall The ``uninstall`` command will remove an installed package. Attempting to remove -a package which other packages depend on will result in an error. You can use the +a package that other packages depend on will result in an error. You can use the ``--inclDeps`` or ``-i`` flag to remove all dependent packages along with the package. Similar to the ``install`` command you can specify a version range, for example: @@ -518,7 +518,7 @@ an ini-compatible format. Useful for tools wishing to read metadata about Nimble packages who do not want to use the NimScript evaluator. The format can be specified with `--json` or `--ini` (and defaults to `--ini`). -Use `nimble dump pkg` to dump information about provided `pkg` instad. +Use `nimble dump pkg` to dump information about provided `pkg` instead. ## Configuration @@ -554,7 +554,7 @@ You can currently configure the following in this file: defaults to the "Official" package list, you can override it by specifying a ``[PackageList]`` section named "official". Multiple URLs can be specified under each section, Nimble will try each in succession if - downloading from the first fails. Alternately, ``path`` can specify a + downloading from the first fails. Alternatively, ``path`` can specify a local file path to copy a package list .json file from. * ``cloneUsingHttps`` - Whether to replace any ``git://`` inside URLs with ``https://``. @@ -640,13 +640,13 @@ requires "choosenim ~= 0" # choosenim >= 0.0.0 & < 1.0.0 requires "choosenim ^= 0" # choosenim >= 0.0.0 & < 1.0.0 ``` -Nimble currently supports installation of packages from a local directory, a +Nimble currently supports the installation of packages from a local directory, a Git repository and a mercurial repository. The .nimble file must be present in the root of the directory or repository being installed. The .nimble file is very flexible because it is interpreted using NimScript. -Because of Nim's flexibility the definitions remain declarative. With the added -ability of using the Nim language to enrich your package specification. +Because of Nim's flexibility, the definitions remain declarative. With the added +ability to use the Nim language to enrich your package specification. For example, you can define dependencies for specific platforms using Nim's ``when`` statement. @@ -676,7 +676,7 @@ which makes this feature very powerful. You can also check what tasks are supported by the package in the current directory by using the ``tasks`` command. -Nimble provides an API which adds even more functionality. For example, +Nimble provides an API that adds even more functionality. For example, you can specify pre and post hooks for any Nimble command (including commands that you define yourself). To do this you can add something like the following: @@ -707,7 +707,7 @@ flags are those specified before the task name and are forwarded to the Nim compiler that runs the `.nimble` task. This enables setting `--define:xxx` values that can be checked with `when defined(xxx)` in the task, and other compiler flags that are applicable in Nimscript mode. Run flags are those after -the task name and are available as command line arguments to the task. They can +the task name and are available as command-line arguments to the task. They can be accessed per usual from `commandLineParams: seq[string]`. In order to forward compiler flags to `exec("nim ...")` calls executed within a @@ -785,7 +785,7 @@ be solved in a few different ways: * Use a simple path modification to resolve the package properly. The latter is highly recommended. Reinstalling the package to test an actively -changing code base is a massive pain. +changing codebase is a massive pain. To modify the path for your tests only, simply add a ``nim.cfg`` file into your ``tests`` directory with the following contents: @@ -794,7 +794,7 @@ your ``tests`` directory with the following contents: --path:"../src/" ``` -Nimble offers a pre-defined ``test`` task which compiles and runs all files +Nimble offers a pre-defined ``test`` task that compiles and runs all files in the ``tests`` directory beginning with 't' in their filename. Nim flags provided to `nimble test` will be forwarded to the compiler when building the tests. @@ -825,14 +825,14 @@ determined by the nature of your package, that is, whether your package exposes only one module or multiple modules. If your package exposes only a single module, then that module should be -present in the source directory of your Git repository, and should be named +present in the source directory of your Git repository and should be named whatever your package's name is. A good example of this is the [jester](https://github.com/dom96/jester) package which exposes the ``jester`` -module. In this case the jester package is imported with ``import jester``. +module. In this case, the jester package is imported with ``import jester``. If your package exposes multiple modules then the modules should be in a ``PackageName`` directory. This will allow for a certain measure of isolation -from other packages which expose modules with the same names. In this case +from other packages which expose modules with the same names. In this case, the package's modules will be imported with ``import PackageName/module``. Here's a simple example multi-module library package called `kool`: @@ -850,7 +850,7 @@ them in a ``PackageName/private`` directory. Your modules may then import these private modules with ``import PackageName/private/module``. This directory structure may be enforced in the future. -All files and folders in the directory of where the .nimble file resides will be +All files and folders in the directory where the .nimble file resides will be copied as-is, you can however skip some directories or files by setting the ``skipDirs``, ``skipFiles`` or ``skipExt`` options in your .nimble file. Directories and files can also be specified on a *whitelist* basis, if you @@ -869,7 +869,7 @@ bin = @["main"] In this case when ``nimble install`` is invoked, Nimble will build the ``main.nim`` file, copy it into ``$nimbleDir/pkgs/pkgname-ver/`` and subsequently create a -symlink to the binary in ``$nimbleDir/bin/``. On Windows a stub .cmd file is +symlink to the binary in ``$nimbleDir/bin/``. On Windows, a stub .cmd file is created instead. The binary can be named differently than the source file with the ``namedBin`` @@ -932,8 +932,7 @@ latest commit of Jester. related to it are more likely to be introduced than for any other Nimble features. -Starting with Nimble v0.8.0, you can now specify external dependencies. These -are dependencies which are not managed by Nimble and can only be installed via +Starting with Nimble v0.8.0, you can now specify external dependencies. These dependencies are not managed by Nimble and can only be installed via your system's package manager or downloaded manually via the internet. As an example, to specify a dependency on openssl you may put this in your @@ -969,11 +968,11 @@ installing your package (on macOS): Versions of cloned packages via Git or Mercurial are determined through the repository's *tags*. -When installing a package which needs to be downloaded, after the download is +When installing a package that needs to be downloaded, after the download is complete and if the package is distributed through a VCS, Nimble will check the cloned repository's tags list. If no tags exist, Nimble will simply install the HEAD (or tip in Mercurial) of the repository. If tags exist, Nimble will attempt -to look for tags which resemble versions (e.g. v0.1) and will then find the +to look for tags that resemble versions (e.g. v0.1) and will then find the latest version out of the available tags, once it does so it will install the package after checking out the latest version. @@ -1001,7 +1000,7 @@ the new version. ##### Git Version Tagging -Use dot separated numbers to represent the release version in the git +Use dot-separated numbers to represent the release version in the git tag label. Nimble will parse these git tag labels to know which versions of a package are published. @@ -1019,12 +1018,12 @@ a specific name to a URL pointing to your package. This mapping is stored in an official packages repository located [here](https://github.com/nim-lang/packages). -This repository contains a ``packages.json`` file which lists all the published +This repository contains a ``packages.json`` file that lists all the published packages. It contains a set of package names with associated metadata. You can read more about this metadata in the [readme for the packages repository](https://github.com/nim-lang/packages#readme). -To publish your package you need to fork that repository, and add an entry +To publish your package you need to fork that repository and add an entry into the ``packages.json`` file for your package. Then create a pull request with your changes. **You only need to do this once**. @@ -1042,7 +1041,7 @@ Nimble includes a ``publish`` command which does this for you automatically. **before** tagging the current version using ``git tag`` or ``hg tag``. * ``author`` - The name of the author of this package. * ``description`` - A string describing the package. -* ``license`` - The name of the license in which this package is licensed under. +* ``license`` - The name of the license under which this package is licensed. #### Optional @@ -1112,7 +1111,7 @@ ignored. This allows for project local dependencies and isolation from other projects. The `-l | --localdeps` flag can be used to setup a project in local dependency mode. -Nimble also allows overriding ``$nimbleDir`` on the command line with the +Nimble also allows overriding ``$nimbleDir`` on the command-line with the ``--nimbleDir`` flag or the ``NIMBLE_DIR`` environment variable if required. If the default ``$HOME/.nimble`` is overridden by one of the above methods, @@ -1128,7 +1127,7 @@ in Nimble's package directory when compiling your software. This means that it cannot resolve dependencies, and it can only use the latest version of a package when compiling. -When Nimble builds your package it actually executes the Nim compiler. +When Nimble builds your package it executes the Nim compiler. It resolves the dependencies and feeds the path of each package to the compiler so that it knows precisely which version to use. @@ -1191,9 +1190,9 @@ Make sure that you are running at least version 0.16.0 of Nim (or the latest nig * ``Error: cannot open '/home/user/.nimble/lib/system.nim'.`` Nimble cannot find the Nim standard library. This is considered a bug so -please report it. As a workaround you can set the ``NIM_LIB_PREFIX`` environment +please report it. As a workaround, you can set the ``NIM_LIB_PREFIX`` environment variable to the directory where ``lib/system.nim`` (and other standard library -files) are found. Alternatively you can also configure this in Nimble's +files) are found. Alternatively, you can also configure this in Nimble's config file. ## Repository information From f3398b00ce9306b0eff8557c8fbd4eae604e01a3 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 4 Aug 2021 14:51:29 +0300 Subject: [PATCH 65/73] Enhance `develop --with-dependencies` Make `develop --with-dependencies` to skip already existing directories when cloning the repositories. This is useful when a second `develop --with-dependencies` is executed for another package and some of the dependencies are the same as in the first run. In that case, we don't want the entire command to fail because some package directories already exist. Related to nim-lang/nimble#127 --- src/nimble.nim | 25 +++++++++++++++++++++++-- src/nimblepkg/displaymessages.nim | 4 ++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/nimble.nim b/src/nimble.nim index bc5ce811..bff53c2c 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -537,7 +537,19 @@ proc downloadDependency(name: string, dep: LockFileDep, options: Options): getDevelopDownloadDir(url, subdir, options) else: "" if dirExists(downloadPath): - raiseCannotCloneInExistingDirException(downloadPath) + if options.developWithDependencies: + displayWarning(skipDownloadingInAlreadyExistingDirectoryMsg( + downloadPath, name)) + result = DownloadInfo( + name: name, + dependency: dep, + url: url, + version: version, + downloadDir: downloadPath, + vcsRevision: dep.vcsRevision.newClone) + return + else: + raiseCannotCloneInExistingDirException(downloadPath) let (downloadDir, _, vcsRevision) = await downloadPkg( url, version, dep.downloadMethod, subdir, options, downloadPath, @@ -1213,7 +1225,16 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: var Options): let downloadDir = getDevelopDownloadDir(url, subdir, options) if dirExists(downloadDir): - raiseCannotCloneInExistingDirException(downloadDir) + if options.developWithDependencies: + displayWarning(skipDownloadingInAlreadyExistingDirectoryMsg( + downloadDir, pkgTup.name)) + let pkgInfo = getPkgInfo(downloadDir, options) + developFromDir(pkgInfo, options) + options.action.devActions.add( + (datAdd, pkgInfo.getNimbleFileDir.normalizedPath)) + return pkgInfo + else: + raiseCannotCloneInExistingDirException(downloadDir) # Download the HEAD and make sure the full history is downloaded. let ver = diff --git a/src/nimblepkg/displaymessages.nim b/src/nimblepkg/displaymessages.nim index 1068425b..046b5321 100644 --- a/src/nimblepkg/displaymessages.nim +++ b/src/nimblepkg/displaymessages.nim @@ -153,3 +153,7 @@ proc pkgAlreadyExistsInTheCacheMsg*(pkgInfo: PackageInfo): string = pkgInfo.basicInfo.name, $pkgInfo.basicInfo.version, $pkgInfo.basicInfo.checksum) + +proc skipDownloadingInAlreadyExistingDirectoryMsg*(dir, name: string): string = + &"The download directory \"{dir}\" already exists.\n" & + &"Skipping the download of \"{name}\"." From fb29e397b9d1ce94d230bc51b0e561f14ff6b057 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 4 Aug 2021 18:51:57 +0300 Subject: [PATCH 66/73] Optimize a slow copy when caculating the checksum Related to nim-lang/nimble#127 --- src/nimblepkg/checksums.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nimblepkg/checksums.nim b/src/nimblepkg/checksums.nim index b2df83dd..db740faf 100644 --- a/src/nimblepkg/checksums.nim +++ b/src/nimblepkg/checksums.nim @@ -32,7 +32,7 @@ proc updateSha1Checksum(checksum: var Sha1State, fileName, filePath: string) = while true: var bytesRead = readChars(file, buffer) if bytesRead == 0: break - checksum.update(buffer[0.. Date: Wed, 4 Aug 2021 18:56:46 +0300 Subject: [PATCH 67/73] Update the change log Related to nim-lang/nimble#127 --- changelog.markdown | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.markdown b/changelog.markdown index 597d0b02..ff781fc0 100644 --- a/changelog.markdown +++ b/changelog.markdown @@ -9,6 +9,9 @@ This is a major release containing two big features: - A new dependencies development mode. - Support for lock files. +- Download tarballs when downloading packages from GitHub. +- Parallel downloads of the locked dependencies. +- Setup command. ## 0.13.0 From e4fcbd0f8f32d91ac0876be4061ea1306dd4ad73 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 4 Aug 2021 19:05:51 +0300 Subject: [PATCH 68/73] Add a small code improvement In the `toRevDep` procedure directly use the `basicInfo` tuple without constructing a new tuple from its fields. Related to nim-lang/nimble#127 --- src/nimblepkg/reversedeps.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index f3d06e93..d4b803b0 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -144,7 +144,7 @@ proc toRevDep*(pkg: PackageInfo): ReverseDependency = if not pkg.isLink: result = ReverseDependency( kind: rdkInstalled, - pkgInfo: (pkg.basicInfo.name, pkg.basicInfo.version, pkg.basicInfo.checksum)) + pkgInfo: pkg.basicInfo) else: result = ReverseDependency( kind: rdkDevelop, From 95ce15b62fc52cba163218bd39f8b95e96c78749 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 9 Aug 2021 17:34:14 +0300 Subject: [PATCH 69/73] Fix checking whether `tar` is available When trying to execute `tar` to determine whether it is available catch an `OSError` raised when the path to the `tar` is invalid. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 1e0eb148..8508b103 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -175,9 +175,12 @@ proc hasTar: bool = ## Checks whether a `tar` external tool is available. var hasTar {.global.} = false once: - # Try to execute `tar` to ensure that it is available. - let (_, exitCode) = execCmdEx(getTarExePath() & " --version") - hasTar = exitCode == QuitSuccess + try: + # Try to execute `tar` to ensure that it is available. + let (_, exitCode) = execCmdEx(getTarExePath() & " --version") + hasTar = exitCode == QuitSuccess + except OSError: + discard return hasTar proc isGitHubRepo(url: string): bool = From ad17ab1f97f27e1d6ee5161f39d04ef88285d602 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 11 Aug 2021 15:02:21 +0300 Subject: [PATCH 70/73] Remove the parallel package downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parallel package downloads are removed, because the `asynctools` library for asynchronous processes, that is used is not production-ready. It has several hard-to-find bugs causing а crashing of Nimble. First, they must be fixed before using it. Related to nim-lang/nimble#127 --- changelog.markdown | 5 +- src/nimble.nim | 81 +-- src/nimblepkg/asynctools/asyncpipe.nim | 536 -------------- src/nimblepkg/asynctools/asyncproc.nim | 923 ------------------------- src/nimblepkg/download.nim | 168 ++--- src/nimblepkg/lockfile.nim | 2 +- src/nimblepkg/options.nim | 9 - src/nimblepkg/sha1hashes.nim | 2 - src/nimblepkg/tools.nim | 23 +- 9 files changed, 99 insertions(+), 1650 deletions(-) delete mode 100644 src/nimblepkg/asynctools/asyncpipe.nim delete mode 100644 src/nimblepkg/asynctools/asyncproc.nim diff --git a/changelog.markdown b/changelog.markdown index ff781fc0..afac2b54 100644 --- a/changelog.markdown +++ b/changelog.markdown @@ -5,13 +5,12 @@ ## 0.14.0 -This is a major release containing two big features: +This is a major release containing four new features: - A new dependencies development mode. - Support for lock files. - Download tarballs when downloading packages from GitHub. -- Parallel downloads of the locked dependencies. -- Setup command. +- A setup command. ## 0.13.0 diff --git a/src/nimble.nim b/src/nimble.nim index bff53c2c..93393f0f 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -4,7 +4,7 @@ import system except TResult import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc, - strformat, asyncdispatch + strformat import std/options as std_opt @@ -485,14 +485,7 @@ type url: string version: VersionRange downloadDir: string - vcsRevision: Sha1HashRef - - DownloadQueue = ref seq[tuple[name: string, dep: LockFileDep]] - ## A queue of dependencies from the lock file which to be downloaded. - - DownloadResults = ref seq[DownloadInfo] - ## A list of `DownloadInfo` objects used for installing the downloaded - ## dependencies. + vcsRevision: Sha1Hash proc developWithDependencies(options: Options): bool = ## Determines whether the current executed action is a develop sub-command @@ -521,8 +514,8 @@ proc raiseCannotCloneInExistingDirException(downloadDir: string) = raise nimbleError(msg, hint) proc downloadDependency(name: string, dep: LockFileDep, options: Options): - Future[DownloadInfo] {.async.} = - ## Asynchronously downloads a dependency from the lock file. + DownloadInfo = + ## Downloads a dependency from the lock file. if not options.developWithDependencies: let depDirName = getDependencyDir(name, dep, options) @@ -546,12 +539,12 @@ proc downloadDependency(name: string, dep: LockFileDep, options: Options): url: url, version: version, downloadDir: downloadPath, - vcsRevision: dep.vcsRevision.newClone) + vcsRevision: dep.vcsRevision) return else: raiseCannotCloneInExistingDirException(downloadPath) - let (downloadDir, _, vcsRevision) = await downloadPkg( + let (downloadDir, _, vcsRevision) = downloadPkg( url, version, dep.downloadMethod, subdir, options, downloadPath, dep.vcsRevision) @@ -578,7 +571,7 @@ proc installDependency(pkgInfo: PackageInfo, downloadInfo: DownloadInfo, downloadInfo.url, first = false, fromLockFile = true, - downloadInfo.vcsRevision[]) + downloadInfo.vcsRevision) downloadInfo.downloadDir.removeDir @@ -590,31 +583,6 @@ proc installDependency(pkgInfo: PackageInfo, downloadInfo: DownloadInfo, return newlyInstalledPkgInfo -proc startDownloadWorker(queue: DownloadQueue, options: Options, - downloadResults: DownloadResults) {.async.} = - ## Starts a new download worker. - while queue[].len > 0: - let download = queue[].pop - let index = queue[].len - downloadResults[index] = await downloadDependency( - download.name, download.dep, options) - -proc lockedDepsDownload(dependenciesToDownload: DownloadQueue, - options: Options): DownloadResults = - ## By given queue with dependencies to download performs the downloads and - ## returns the result objects. - - result.new - result[].setLen(dependenciesToDownload[].len) - - var downloadWorkers: seq[Future[void]] - let workersCount = min( - options.maxParallelDownloads, dependenciesToDownload[].len) - for i in 0 ..< workersCount: - downloadWorkers.add startDownloadWorker( - dependenciesToDownload, options, result) - waitFor all(downloadWorkers) - proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): HashSet[PackageInfo] = # Returns a hash set with `PackageInfo` of all packages from the lock file of @@ -625,20 +593,14 @@ proc processLockedDependencies(pkgInfo: PackageInfo, options: Options): let developModeDeps = getDevelopDependencies(pkgInfo, options) - var dependenciesToDownload: DownloadQueue - dependenciesToDownload.new - for name, dep in pkgInfo.lockedDeps: if developModeDeps.hasKey(name): result.incl developModeDeps[name][] elif isInstalled(name, dep, options): result.incl getDependency(name, dep, options) else: - dependenciesToDownload[].add (name, dep) - - let downloadResults = lockedDepsDownload(dependenciesToDownload, options) - for downloadResult in downloadResults[]: - result.incl installDependency(pkgInfo, downloadResult, options) + let downloadResult = downloadDependency(name, dep, options) + result.incl installDependency(pkgInfo, downloadResult, options) proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string, @@ -687,11 +649,11 @@ proc install(packages: seq[PkgTuple], options: Options, let (meth, url, metadata) = getDownloadInfo(pv, options, doPrompt) let subdir = metadata.getOrDefault("subdir") let (downloadDir, downloadVersion, vcsRevision) = - waitFor downloadPkg(url, pv.ver, meth, subdir, options, - downloadPath = "", vcsRevision = notSetSha1Hash) + downloadPkg(url, pv.ver, meth, subdir, options, + downloadPath = "", vcsRevision = notSetSha1Hash) try: result = installFromDir(downloadDir, pv.ver, options, url, - first, fromLockFile, vcsRevision[]) + first, fromLockFile, vcsRevision) except BuildFailed as error: # The package failed to build. # Check if we tried building a tagged version of the package. @@ -1243,8 +1205,8 @@ proc installDevelopPackage(pkgTup: PkgTuple, options: var Options): else: pkgTup.ver - discard waitFor downloadPkg(url, ver, meth, subdir, options, downloadDir, - vcsRevision = notSetSha1Hash) + discard downloadPkg(url, ver, meth, subdir, options, downloadDir, + vcsRevision = notSetSha1Hash) let pkgDir = downloadDir / subdir var pkgInfo = getPkgInfo(pkgDir, options) @@ -1258,18 +1220,12 @@ proc developLockedDependencies(pkgInfo: PackageInfo, alreadyDownloaded: var HashSet[string], options: var Options) = ## Downloads for develop the dependencies from the lock file. - var dependenciesToDownload: DownloadQueue - dependenciesToDownload.new - for name, dep in pkgInfo.lockedDeps: if dep.url.removeTrailingGitString notin alreadyDownloaded: - dependenciesToDownload[].add (name, dep) - - let downloadResults = lockedDepsDownload(dependenciesToDownload, options) - for downloadResult in downloadResults[]: - alreadyDownloaded.incl downloadResult.url.removeTrailingGitString - options.action.devActions.add( - (datAdd, downloadResult.downloadDir.normalizedPath)) + let downloadResult = downloadDependency(name, dep, options) + alreadyDownloaded.incl downloadResult.url.removeTrailingGitString + options.action.devActions.add( + (datAdd, downloadResult.downloadDir.normalizedPath)) proc check(alreadyDownloaded: HashSet[string], dep: PkgTuple, options: Options): bool = @@ -1965,6 +1921,7 @@ when isMainModule: except CatchableError as error: exitCode = QuitFailure displayTip() + echo error.getStackTrace() displayError(error) finally: try: diff --git a/src/nimblepkg/asynctools/asyncpipe.nim b/src/nimblepkg/asynctools/asyncpipe.nim deleted file mode 100644 index ac2e8b9b..00000000 --- a/src/nimblepkg/asynctools/asyncpipe.nim +++ /dev/null @@ -1,536 +0,0 @@ -# -# -# Asynchronous tools for Nim Language -# (c) Copyright 2016 Eugene Kabanov -# -# See the file "LICENSE", included in this -# distribution, for details about the copyright. -# - -## This module implements cross-platform asynchronous pipes communication. -## -## Module uses named pipes for Windows, and anonymous pipes for -## Linux/BSD/MacOS. -## -## .. code-block:: nim -## var inBuffer = newString(64) -## var outBuffer = "TEST STRING BUFFER" -## -## # Create new pipe -## var o = createPipe() -## -## # Write string to pipe -## waitFor write(o, cast[pointer](addr outBuffer[0]), outBuffer.len) -## -## # Read data from pipe -## var c = waitFor readInto(o, cast[pointer](addr inBuffer[0]), inBuffer.len) -## -## inBuffer.setLen(c) -## doAssert(inBuffer == outBuffer) -## -## # Close pipe -## close(o) - -import asyncdispatch, os - -when defined(nimdoc): - type - AsyncPipe* = ref object ## Object represents ``AsyncPipe``. - - proc createPipe*(register = true): AsyncPipe = - ## Create descriptor pair for interprocess communication. - ## - ## Returns ``AsyncPipe`` object, which represents OS specific pipe. - ## - ## If ``register`` is `false`, both pipes will not be registered with - ## current dispatcher. - - proc closeRead*(pipe: AsyncPipe, unregister = true) = - ## Closes read side of pipe ``pipe``. - ## - ## If ``unregister`` is `false`, pipe will not be unregistered from - ## current dispatcher. - - proc closeWrite*(pipe: AsyncPipe, unregister = true) = - ## Closes write side of pipe ``pipe``. - ## - ## If ``unregister`` is `false`, pipe will not be unregistered from - ## current dispatcher. - - proc getReadHandle*(pipe: AsyncPipe): int = - ## Returns OS specific handle for read side of pipe ``pipe``. - - proc getWriteHandle*(pipe: AsyncPipe): int = - ## Returns OS specific handle for write side of pipe ``pipe``. - - proc getHandles*(pipe: AsyncPipe): array[2, Handle] = - ## Returns OS specific handles of ``pipe``. - - proc getHandles*(pipe: AsyncPipe): array[2, cint] = - ## Returns OS specific handles of ``pipe``. - - proc close*(pipe: AsyncPipe, unregister = true) = - ## Closes both ends of pipe ``pipe``. - ## - ## If ``unregister`` is `false`, pipe will not be unregistered from - ## current dispatcher. - - proc write*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = - ## This procedure writes an untyped ``data`` of ``size`` size to the - ## pipe ``pipe``. - ## - ## The returned future will complete once ``all`` data has been sent or - ## part of the data has been sent. - - proc readInto*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = - ## This procedure reads up to ``size`` bytes from pipe ``pipe`` - ## into ``data``, which must at least be of that size. - ## - ## Returned future will complete once all the data requested is read or - ## part of the data has been read. - - proc asyncWrap*(readHandle: Handle|cint = 0, - writeHandle: Handle|cint = 0): AsyncPipe = - ## Wraps existing OS specific pipe handles to ``AsyncPipe`` and register - ## it with current dispatcher. - ## - ## ``readHandle`` - read side of pipe (optional value). - ## ``writeHandle`` - write side of pipe (optional value). - ## **Note**: At least one handle must be specified. - ## - ## Returns ``AsyncPipe`` object. - ## - ## Windows handles must be named pipes created with ``CreateNamedPipe`` and - ## ``FILE_FLAG_OVERLAPPED`` in flags. You can use ``ReopenFile()`` function - ## to convert existing handle to overlapped variant. - ## - ## Posix handle will be modified with ``O_NONBLOCK``. - - proc asyncUnwrap*(pipe: AsyncPipe) = - ## Unregisters ``pipe`` handle from current async dispatcher. - - proc `$`*(pipe: AsyncPipe) = - ## Returns string representation of ``AsyncPipe`` object. - -else: - - when defined(windows): - import winlean - else: - import posix - - type - AsyncPipe* = ref object of RootRef - when defined(windows): - readPipe: Handle - writePipe: Handle - else: - readPipe: cint - writePipe: cint - - when defined(windows): - - proc QueryPerformanceCounter(res: var int64) - {.importc: "QueryPerformanceCounter", stdcall, dynlib: "kernel32".} - proc connectNamedPipe(hNamedPipe: Handle, lpOverlapped: pointer): WINBOOL - {.importc: "ConnectNamedPipe", stdcall, dynlib: "kernel32".} - - when not declared(PCustomOverlapped): - type - PCustomOverlapped = CustomRef - - const - pipeHeaderName = r"\\.\pipe\asyncpipe_" - - const - DEFAULT_PIPE_SIZE = 65536'i32 - FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000'i32 - PIPE_WAIT = 0x00000000'i32 - PIPE_TYPE_BYTE = 0x00000000'i32 - PIPE_READMODE_BYTE = 0x00000000'i32 - ERROR_PIPE_CONNECTED = 535 - ERROR_PIPE_BUSY = 231 - ERROR_BROKEN_PIPE = 109 - ERROR_PIPE_NOT_CONNECTED = 233 - - proc `$`*(pipe: AsyncPipe): string = - result = "AsyncPipe [read = " & $(cast[uint](pipe.readPipe)) & - ", write = " & $(cast[int](pipe.writePipe)) & "]" - - proc createPipe*(register = true): AsyncPipe = - - var number = 0'i64 - var pipeName: WideCString - var pipeIn: Handle - var pipeOut: Handle - var sa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint, - lpSecurityDescriptor: nil, bInheritHandle: 1) - while true: - QueryPerformanceCounter(number) - let p = pipeHeaderName & $number - pipeName = newWideCString(p) - var openMode = FILE_FLAG_FIRST_PIPE_INSTANCE or FILE_FLAG_OVERLAPPED or - PIPE_ACCESS_INBOUND - var pipeMode = PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT - pipeIn = createNamedPipe(pipeName, openMode, pipeMode, 1'i32, - DEFAULT_PIPE_SIZE, DEFAULT_PIPE_SIZE, - 1'i32, addr sa) - if pipeIn == INVALID_HANDLE_VALUE: - let err = osLastError() - if err.int32 != ERROR_PIPE_BUSY: - raiseOsError(err) - else: - break - - var openMode = (FILE_WRITE_DATA or SYNCHRONIZE) - pipeOut = createFileW(pipeName, openMode, 0, addr(sa), OPEN_EXISTING, - FILE_FLAG_OVERLAPPED, 0) - if pipeOut == INVALID_HANDLE_VALUE: - let err = osLastError() - discard closeHandle(pipeIn) - raiseOsError(err) - - result = AsyncPipe(readPipe: pipeIn, writePipe: pipeOut) - - var ovl = PCustomOverlapped() - let res = connectNamedPipe(pipeIn, cast[pointer](ovl)) - if res == 0: - let err = osLastError() - if err.int32 == ERROR_PIPE_CONNECTED: - discard - elif err.int32 == ERROR_IO_PENDING: - var bytesRead = 0.Dword - if getOverlappedResult(pipeIn, cast[POVERLAPPED](ovl), bytesRead, 1) == 0: - let oerr = osLastError() - discard closeHandle(pipeIn) - discard closeHandle(pipeOut) - raiseOsError(oerr) - else: - discard closeHandle(pipeIn) - discard closeHandle(pipeOut) - raiseOsError(err) - - if register: - register(AsyncFD(pipeIn)) - register(AsyncFD(pipeOut)) - - proc asyncWrap*(readHandle = Handle(0), - writeHandle = Handle(0)): AsyncPipe = - doAssert(readHandle != 0 or writeHandle != 0) - - result = AsyncPipe(readPipe: readHandle, writePipe: writeHandle) - if result.readPipe != 0: - register(AsyncFD(result.readPipe)) - if result.writePipe != 0: - register(AsyncFD(result.writePipe)) - - proc asyncUnwrap*(pipe: AsyncPipe) = - if pipe.readPipe != 0: - unregister(AsyncFD(pipe.readPipe)) - if pipe.writePipe != 0: - unregister(AsyncFD(pipe.writePipe)) - - proc getReadHandle*(pipe: AsyncPipe): Handle {.inline.} = - result = pipe.readPipe - - proc getWriteHandle*(pipe: AsyncPipe): Handle {.inline.} = - result = pipe.writePipe - - proc getHandles*(pipe: AsyncPipe): array[2, Handle] {.inline.} = - result = [pipe.readPipe, pipe.writePipe] - - proc closeRead*(pipe: AsyncPipe, unregister = true) = - if pipe.readPipe != 0: - if unregister: - unregister(AsyncFD(pipe.readPipe)) - if closeHandle(pipe.readPipe) == 0: - raiseOsError(osLastError()) - pipe.readPipe = 0 - - proc closeWrite*(pipe: AsyncPipe, unregister = true) = - if pipe.writePipe != 0: - if unregister: - unregister(AsyncFD(pipe.writePipe)) - if closeHandle(pipe.writePipe) == 0: - raiseOsError(osLastError()) - pipe.writePipe = 0 - - proc close*(pipe: AsyncPipe, unregister = true) = - closeRead(pipe, unregister) - closeWrite(pipe, unregister) - - proc write*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = - var retFuture = newFuture[int]("asyncpipe.write") - var ol = PCustomOverlapped() - - if pipe.writePipe == 0: - retFuture.fail(newException(ValueError, - "Write side of pipe closed or not available")) - else: - GC_ref(ol) - ol.data = CompletionData(fd: AsyncFD(pipe.writePipe), cb: - proc (fd: AsyncFD, bytesCount: DWord, errcode: OSErrorCode) = - if not retFuture.finished: - if errcode == OSErrorCode(-1): - retFuture.complete(bytesCount) - else: - retFuture.fail(newException(OSError, osErrorMsg(errcode))) - ) - let res = writeFile(pipe.writePipe, data, nbytes.int32, nil, - cast[POVERLAPPED](ol)).bool - if not res: - let errcode = osLastError() - if errcode.int32 != ERROR_IO_PENDING: - GC_unref(ol) - retFuture.fail(newException(OSError, osErrorMsg(errcode))) - return retFuture - - proc readInto*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = - var retFuture = newFuture[int]("asyncpipe.readInto") - var ol = PCustomOverlapped() - - if pipe.readPipe == 0: - retFuture.fail(newException(ValueError, - "Read side of pipe closed or not available")) - else: - GC_ref(ol) - ol.data = CompletionData(fd: AsyncFD(pipe.readPipe), cb: - proc (fd: AsyncFD, bytesCount: DWord, errcode: OSErrorCode) = - if not retFuture.finished: - if errcode == OSErrorCode(-1): - assert(bytesCount > 0 and bytesCount <= nbytes.int32) - retFuture.complete(bytesCount) - else: - if errcode.int32 in {ERROR_BROKEN_PIPE, - ERROR_PIPE_NOT_CONNECTED}: - retFuture.complete(bytesCount) - else: - retFuture.fail(newException(OSError, osErrorMsg(errcode))) - ) - let res = readFile(pipe.readPipe, data, nbytes.int32, nil, - cast[POVERLAPPED](ol)).bool - if not res: - let err = osLastError() - if err.int32 in {ERROR_BROKEN_PIPE, ERROR_PIPE_NOT_CONNECTED}: - GC_unref(ol) - retFuture.complete(0) - elif err.int32 != ERROR_IO_PENDING: - GC_unref(ol) - retFuture.fail(newException(OSError, osErrorMsg(err))) - return retFuture - else: - - proc setNonBlocking(fd: cint) {.inline.} = - var x = fcntl(fd, F_GETFL, 0) - if x == -1: - raiseOSError(osLastError()) - else: - var mode = x or O_NONBLOCK - if fcntl(fd, F_SETFL, mode) == -1: - raiseOSError(osLastError()) - - proc `$`*(pipe: AsyncPipe): string = - result = "AsyncPipe [read = " & $(cast[uint](pipe.readPipe)) & - ", write = " & $(cast[uint](pipe.writePipe)) & "]" - - proc createPipe*(size = 65536, register = true): AsyncPipe = - var fds: array[2, cint] - if posix.pipe(fds) == -1: - raiseOSError(osLastError()) - setNonBlocking(fds[0]) - setNonBlocking(fds[1]) - - result = AsyncPipe(readPipe: fds[0], writePipe: fds[1]) - - if register: - register(AsyncFD(fds[0])) - register(AsyncFD(fds[1])) - - proc asyncWrap*(readHandle = cint(0), writeHandle = cint(0)): AsyncPipe = - doAssert((readHandle != 0) or (writeHandle != 0)) - result = AsyncPipe(readPipe: readHandle, writePipe: writeHandle) - if result.readPipe != 0: - setNonBlocking(result.readPipe) - register(AsyncFD(result.readPipe)) - if result.writePipe != 0: - setNonBlocking(result.writePipe) - register(AsyncFD(result.writePipe)) - - proc asyncUnwrap*(pipe: AsyncPipe) = - if pipe.readPipe != 0: - unregister(AsyncFD(pipe.readPipe)) - if pipe.writePipe != 0: - unregister(AsyncFD(pipe.writePipe)) - - proc getReadHandle*(pipe: AsyncPipe): cint {.inline.} = - result = pipe.readPipe - - proc getWriteHandle*(pipe: AsyncPipe): cint {.inline.} = - result = pipe.writePipe - - proc getHandles*(pipe: AsyncPipe): array[2, cint] {.inline.} = - result = [pipe.readPipe, pipe.writePipe] - - proc closeRead*(pipe: AsyncPipe, unregister = true) = - if pipe.readPipe != 0: - if unregister: - unregister(AsyncFD(pipe.readPipe)) - if posix.close(cint(pipe.readPipe)) != 0: - raiseOSError(osLastError()) - pipe.readPipe = 0 - - proc closeWrite*(pipe: AsyncPipe, unregister = true) = - if pipe.writePipe != 0: - if unregister: - unregister(AsyncFD(pipe.writePipe)) - if posix.close(cint(pipe.writePipe)) != 0: - raiseOSError(osLastError()) - pipe.writePipe = 0 - - proc close*(pipe: AsyncPipe, unregister = true) = - closeRead(pipe, unregister) - closeWrite(pipe, unregister) - - proc write*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = - var retFuture = newFuture[int]("asyncpipe.write") - var bytesWrote = 0 - - proc cb(fd: AsyncFD): bool = - result = true - let reminder = nbytes - bytesWrote - let pdata = cast[pointer](cast[uint](data) + bytesWrote.uint) - let res = posix.write(pipe.writePipe, pdata, cint(reminder)) - if res < 0: - let err = osLastError() - if err.int32 != EAGAIN: - retFuture.fail(newException(OSError, osErrorMsg(err))) - else: - result = false # We still want this callback to be called. - elif res == 0: - retFuture.complete(bytesWrote) - else: - bytesWrote.inc(res) - if res != reminder: - result = false - else: - retFuture.complete(bytesWrote) - - if pipe.writePipe == 0: - retFuture.fail(newException(ValueError, - "Write side of pipe closed or not available")) - else: - if not cb(AsyncFD(pipe.writePipe)): - addWrite(AsyncFD(pipe.writePipe), cb) - return retFuture - - proc readInto*(pipe: AsyncPipe, data: pointer, nbytes: int): Future[int] = - var retFuture = newFuture[int]("asyncpipe.readInto") - proc cb(fd: AsyncFD): bool = - result = true - let res = posix.read(pipe.readPipe, data, cint(nbytes)) - if res < 0: - let err = osLastError() - if err.int32 != EAGAIN: - retFuture.fail(newException(OSError, osErrorMsg(err))) - else: - result = false # We still want this callback to be called. - elif res == 0: - retFuture.complete(0) - else: - retFuture.complete(res) - - if pipe.readPipe == 0: - retFuture.fail(newException(ValueError, - "Read side of pipe closed or not available")) - else: - if not cb(AsyncFD(pipe.readPipe)): - addRead(AsyncFD(pipe.readPipe), cb) - return retFuture - -when isMainModule: - - when not defined(windows): - const - SIG_DFL = cast[proc(x: cint) {.noconv,gcsafe.}](0) - SIG_IGN = cast[proc(x: cint) {.noconv,gcsafe.}](1) - else: - const - ERROR_NO_DATA = 232 - - var outBuffer = "TEST STRING BUFFER" - - block test1: - # simple read/write test - var inBuffer = newString(64) - var o = createPipe() - var sc = waitFor write(o, cast[pointer](addr outBuffer[0]), - outBuffer.len) - doAssert(sc == len(outBuffer)) - var rc = waitFor readInto(o, cast[pointer](addr inBuffer[0]), - inBuffer.len) - inBuffer.setLen(rc) - doAssert(inBuffer == outBuffer) - close(o) - - block test2: - # read from pipe closed write side - var inBuffer = newString(64) - var o = createPipe() - o.closeWrite() - var rc = waitFor readInto(o, cast[pointer](addr inBuffer[0]), - inBuffer.len) - doAssert(rc == 0) - - block test3: - # write to closed read side - var sc: int = -1 - var o = createPipe() - o.closeRead() - when not defined(windows): - posix.signal(SIGPIPE, SIG_IGN) - - try: - sc = waitFor write(o, cast[pointer](addr outBuffer[0]), - outBuffer.len) - except: - discard - doAssert(sc == -1) - - when not defined(windows): - doAssert(osLastError().int32 == EPIPE) - else: - doAssert(osLastError().int32 == ERROR_NO_DATA) - - when not defined(windows): - posix.signal(SIGPIPE, SIG_DFL) - - block test4: - # bulk test of sending/receiving data - const - testsCount = 5000 - - proc sender(o: AsyncPipe) {.async.} = - var data = 1'i32 - for i in 1..testsCount: - data = int32(i) - let res = await write(o, addr data, sizeof(int32)) - doAssert(res == sizeof(int32)) - closeWrite(o) - - proc receiver(o: AsyncPipe): Future[tuple[count: int, sum: int]] {.async.} = - var data = 0'i32 - result = (count: 0, sum: 0) - while true: - let res = await readInto(o, addr data, sizeof(int32)) - if res == 0: - break - doAssert(res == sizeof(int32)) - inc(result.sum, data) - inc(result.count) - - var o = createPipe() - asyncCheck sender(o) - let res = waitFor(receiver(o)) - doAssert(res.count == testsCount) - doAssert(res.sum == testsCount * (1 + testsCount) div 2) - diff --git a/src/nimblepkg/asynctools/asyncproc.nim b/src/nimblepkg/asynctools/asyncproc.nim deleted file mode 100644 index 489d68ff..00000000 --- a/src/nimblepkg/asynctools/asyncproc.nim +++ /dev/null @@ -1,923 +0,0 @@ -# -# -# Asynchronous tools for Nim Language -# (c) Copyright 2016 Eugene Kabanov -# -# See the file "LICENSE", included in this -# distribution, for details about the copyright. -# - -## This module implements an advanced facility for executing OS processes -## and process communication in asynchronous way. -## -## Most code for this module is borrowed from original ``osproc.nim`` by -## Andreas Rumpf, with some extensions, improvements and fixes. -## -## API is near compatible with stdlib's ``osproc.nim``. - -import strutils, os, strtabs -import asyncdispatch, asyncpipe - -when defined(windows): - import winlean -else: - const STILL_ACTIVE = 259 - import posix - -type - ProcessOption* = enum ## options that can be passed `startProcess` - poEchoCmd, ## echo the command before execution - poUsePath, ## Asks system to search for executable using PATH - ## environment variable. - ## On Windows, this is the default. - poEvalCommand, ## Pass `command` directly to the shell, without - ## quoting. - ## Use it only if `command` comes from trusted source. - poStdErrToStdOut, ## merge stdout and stderr to the stdout stream - poParentStreams, ## use the parent's streams - poInteractive, ## optimize the buffer handling for responsiveness for - ## UI applications. Currently this only affects - ## Windows: Named pipes are used so that you can peek - ## at the process' output streams. - poDemon ## Windows: The program creates no Window. - - AsyncProcessObj = object of RootObj - inPipe: AsyncPipe - outPipe: AsyncPipe - errPipe: AsyncPipe - - when defined(windows): - fProcessHandle: Handle - fThreadHandle: Handle - procId: int32 - threadId: int32 - isWow64: bool - else: - procId: Pid - isExit: bool - exitCode: cint - options: set[ProcessOption] - - AsyncProcess* = ref AsyncProcessObj ## represents an operating system process - -proc quoteShellWindows*(s: string): string = - ## Quote s, so it can be safely passed to Windows API. - ## - ## Based on Python's subprocess.list2cmdline - ## - ## See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx - let needQuote = {' ', '\t'} in s or s.len == 0 - - result = "" - var backslashBuff = "" - if needQuote: - result.add("\"") - - for c in s: - if c == '\\': - backslashBuff.add(c) - elif c == '\"': - result.add(backslashBuff) - result.add(backslashBuff) - backslashBuff.setLen(0) - result.add("\\\"") - else: - if backslashBuff.len != 0: - result.add(backslashBuff) - backslashBuff.setLen(0) - result.add(c) - - if needQuote: - result.add("\"") - -proc quoteShellPosix*(s: string): string = - ## Quote ``s``, so it can be safely passed to POSIX shell. - ## - ## Based on Python's pipes.quote - const safeUnixChars = {'%', '+', '-', '.', '/', '_', ':', '=', '@', - '0'..'9', 'A'..'Z', 'a'..'z'} - if s.len == 0: - return "''" - - let safe = s.allCharsInSet(safeUnixChars) - - if safe: - return s - else: - return "'" & s.replace("'", "'\"'\"'") & "'" - -proc quoteShell*(s: string): string = - ## Quote ``s``, so it can be safely passed to shell. - when defined(Windows): - return quoteShellWindows(s) - elif defined(posix): - return quoteShellPosix(s) - else: - {.error:"quoteShell is not supported on your system".} - - -proc execProcess*(command: string, args: seq[string] = @[], - env: StringTableRef = nil, - options: set[ProcessOption] = {poStdErrToStdOut, poUsePath, - poEvalCommand} - ): Future[tuple[exitcode: int, output: string]] {.async.} - ## A convenience asynchronous procedure that executes ``command`` - ## with ``startProcess`` and returns its exit code and output as a tuple. - ## - ## **WARNING**: this function uses poEvalCommand by default for backward - ## compatibility. Make sure to pass options explicitly. - ## - ## .. code-block:: Nim - ## - ## let outp = await execProcess("nim c -r mytestfile.nim") - ## echo "process exited with code = " & $outp.exitcode - ## echo "process output = " & outp.output - -proc startProcess*(command: string, workingDir: string = "", - args: openArray[string] = [], - env: StringTableRef = nil, - options: set[ProcessOption] = {poStdErrToStdOut}, - pipeStdin: AsyncPipe = nil, - pipeStdout: AsyncPipe = nil, - pipeStderr: AsyncPipe = nil): AsyncProcess - ## Starts a process. - ## - ## ``command`` is the executable file path - ## - ## ``workingDir`` is the process's working directory. If ``workingDir == ""`` - ## the current directory is used. - ## - ## ``args`` are the command line arguments that are passed to the - ## process. On many operating systems, the first command line argument is the - ## name of the executable. ``args`` should not contain this argument! - ## - ## ``env`` is the environment that will be passed to the process. - ## If ``env == nil`` the environment is inherited of - ## the parent process. - ## - ## ``options`` are additional flags that may be passed - ## to `startProcess`. See the documentation of ``ProcessOption`` for the - ## meaning of these flags. - ## - ## ``pipeStdin``, ``pipeStdout``, ``pipeStderr`` is ``AsyncPipe`` handles - ## which will be used as ``STDIN``, ``STDOUT`` and ``STDERR`` of started - ## process respectively. This handles are optional, unspecified handles - ## will be created automatically. - ## - ## Note that you can't pass any ``args`` if you use the option - ## ``poEvalCommand``, which invokes the system shell to run the specified - ## ``command``. In this situation you have to concatenate manually the - ## contents of ``args`` to ``command`` carefully escaping/quoting any special - ## characters, since it will be passed *as is* to the system shell. - ## Each system/shell may feature different escaping rules, so try to avoid - ## this kind of shell invocation if possible as it leads to non portable - ## software. - ## - ## Return value: The newly created process object. Nil is never returned, - ## but ``EOS`` is raised in case of an error. - -proc suspend*(p: AsyncProcess) - ## Suspends the process ``p``. - ## - ## On Posix OSes the procedure sends ``SIGSTOP`` signal to the process. - ## - ## On Windows procedure suspends main thread execution of process via - ## ``SuspendThread()``. WOW64 processes is also supported. - -proc resume*(p: AsyncProcess) - ## Resumes the process ``p``. - ## - ## On Posix OSes the procedure sends ``SIGCONT`` signal to the process. - ## - ## On Windows procedure resumes execution of main thread via - ## ``ResumeThread()``. WOW64 processes is also supported. - -proc terminate*(p: AsyncProcess) - ## Stop the process ``p``. On Posix OSes the procedure sends ``SIGTERM`` - ## to the process. On Windows the Win32 API function ``TerminateProcess()`` - ## is called to stop the process. - -proc kill*(p: AsyncProcess) - ## Kill the process ``p``. On Posix OSes the procedure sends ``SIGKILL`` to - ## the process. On Windows ``kill()`` is simply an alias for ``terminate()``. - -proc running*(p: AsyncProcess): bool - ## Returns `true` if the process ``p`` is still running. Returns immediately. - -proc peekExitCode*(p: AsyncProcess): int - ## Returns `STILL_ACTIVE` if the process is still running. - ## Otherwise the process' exit code. - -proc processID*(p: AsyncProcess): int = - ## Returns process ``p`` id. - return p.procId - -proc inputHandle*(p: AsyncProcess): AsyncPipe {.inline.} = - ## Returns ``AsyncPipe`` handle to ``STDIN`` pipe of process ``p``. - result = p.inPipe - -proc outputHandle*(p: AsyncProcess): AsyncPipe {.inline.} = - ## Returns ``AsyncPipe`` handle to ``STDOUT`` pipe of process ``p``. - result = p.outPipe - -proc errorHandle*(p: AsyncProcess): AsyncPipe {.inline.} = - ## Returns ``AsyncPipe`` handle to ``STDERR`` pipe of process ``p``. - result = p.errPipe - -proc waitForExit*(p: AsyncProcess): Future[int] - ## Waits for the process to finish in asynchronous way and returns - ## exit code. - -when defined(windows): - - const - STILL_ACTIVE = 0x00000103'i32 - HANDLE_FLAG_INHERIT = 0x00000001'i32 - - proc isWow64Process(hProcess: Handle, wow64Process: var WinBool): WinBool - {.importc: "IsWow64Process", stdcall, dynlib: "kernel32".} - proc wow64SuspendThread(hThread: Handle): Dword - {.importc: "Wow64SuspendThread", stdcall, dynlib: "kernel32".} - proc setHandleInformation(hObject: Handle, dwMask: Dword, - dwFlags: Dword): WinBool - {.importc: "SetHandleInformation", stdcall, dynlib: "kernel32".} - - proc buildCommandLine(a: string, args: openArray[string]): cstring = - var res = quoteShell(a) - for i in 0..high(args): - res.add(' ') - res.add(quoteShell(args[i])) - result = cast[cstring](alloc0(res.len+1)) - copyMem(result, cstring(res), res.len) - - proc buildEnv(env: StringTableRef): tuple[str: cstring, len: int] = - var L = 0 - for key, val in pairs(env): inc(L, key.len + val.len + 2) - var str = cast[cstring](alloc0(L+2)) - L = 0 - for key, val in pairs(env): - var x = key & "=" & val - copyMem(addr(str[L]), cstring(x), x.len+1) # copy \0 - inc(L, x.len+1) - (str, L) - - proc close(p: AsyncProcess) = - if p.inPipe != nil: close(p.inPipe) - if p.outPipe != nil: close(p.outPipe) - if p.errPipe != nil: close(p.errPipe) - - proc startProcess(command: string, workingDir: string = "", - args: openArray[string] = [], - env: StringTableRef = nil, - options: set[ProcessOption] = {poStdErrToStdOut}, - pipeStdin: AsyncPipe = nil, - pipeStdout: AsyncPipe = nil, - pipeStderr: AsyncPipe = nil): AsyncProcess = - var - si: STARTUPINFO - procInfo: PROCESS_INFORMATION - - result = AsyncProcess(options: options, isExit: true) - si.cb = sizeof(STARTUPINFO).cint - - if not isNil(pipeStdin): - si.hStdInput = pipeStdin.getReadHandle() - - # Mark other side of pipe as non inheritable. - let oh = pipeStdin.getWriteHandle() - if oh != 0: - if setHandleInformation(oh, HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - else: - if poParentStreams in options: - si.hStdInput = getStdHandle(STD_INPUT_HANDLE) - else: - let pipe = createPipe() - if poInteractive in options: - result.inPipe = pipe - si.hStdInput = pipe.getReadHandle() - else: - result.inPipe = pipe - si.hStdInput = pipe.getReadHandle() - - if setHandleInformation(pipe.getWriteHandle(), - HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - - if not isNil(pipeStdout): - si.hStdOutput = pipeStdout.getWriteHandle() - - # Mark other side of pipe as non inheritable. - let oh = pipeStdout.getReadHandle() - if oh != 0: - if setHandleInformation(oh, HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - else: - if poParentStreams in options: - si.hStdOutput = getStdHandle(STD_OUTPUT_HANDLE) - else: - let pipe = createPipe() - if poInteractive in options: - result.outPipe = pipe - si.hStdOutput = pipe.getWriteHandle() - else: - result.outPipe = pipe - si.hStdOutput = pipe.getWriteHandle() - if setHandleInformation(pipe.getReadHandle(), - HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - - if not isNil(pipeStderr): - si.hStdError = pipeStderr.getWriteHandle() - - # Mark other side of pipe as non inheritable. - let oh = pipeStderr.getReadHandle() - if oh != 0: - if setHandleInformation(oh, HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - else: - if poParentStreams in options: - si.hStdError = getStdHandle(STD_ERROR_HANDLE) - else: - if poInteractive in options: - let pipe = createPipe() - result.errPipe = pipe - si.hStdError = pipe.getWriteHandle() - if setHandleInformation(pipe.getReadHandle(), - HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - else: - if poStdErrToStdOut in options: - result.errPipe = result.outPipe - si.hStdError = si.hStdOutput - else: - let pipe = createPipe() - result.errPipe = pipe - si.hStdError = pipe.getWriteHandle() - if setHandleInformation(pipe.getReadHandle(), - HANDLE_FLAG_INHERIT, 0) == 0: - raiseOSError(osLastError()) - - if si.hStdInput != 0 or si.hStdOutput != 0 or si.hStdError != 0: - si.dwFlags = STARTF_USESTDHANDLES - - # building command line - var cmdl: cstring - if poEvalCommand in options: - cmdl = buildCommandLine("cmd.exe", ["/c", command]) - assert args.len == 0 - else: - cmdl = buildCommandLine(command, args) - # building environment - var e = (str: nil.cstring, len: -1) - if env != nil: e = buildEnv(env) - # building working directory - var wd: cstring = nil - if len(workingDir) > 0: wd = workingDir - # processing echo command line - if poEchoCmd in options: echo($cmdl) - # building security attributes for process and main thread - var psa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint, - lpSecurityDescriptor: nil, bInheritHandle: 1) - var tsa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint, - lpSecurityDescriptor: nil, bInheritHandle: 1) - - var tmp = newWideCString(cmdl) - var ee = - if e.str.isNil: newWideCString(cstring(nil)) - else: newWideCString(e.str, e.len) - var wwd = newWideCString(wd) - var flags = NORMAL_PRIORITY_CLASS or CREATE_UNICODE_ENVIRONMENT - if poDemon in options: flags = flags or CREATE_NO_WINDOW - let res = winlean.createProcessW(nil, tmp, addr psa, addr tsa, 1, flags, - ee, wwd, si, procInfo) - if e.str != nil: dealloc(e.str) - if res == 0: - close(result) - raiseOsError(osLastError()) - else: - result.fProcessHandle = procInfo.hProcess - result.procId = procInfo.dwProcessId - result.fThreadHandle = procInfo.hThread - result.threadId = procInfo.dwThreadId - when sizeof(int) == 8: - # If sizeof(int) == 8, then our process is 64bit, and we need to check - # architecture of just spawned process. - var iswow64 = WinBool(0) - if isWow64Process(procInfo.hProcess, iswow64) == 0: - raiseOsError(osLastError()) - result.isWow64 = (iswow64 != 0) - else: - result.isWow64 = false - - result.isExit = false - - if poParentStreams notin options: - closeRead(result.inPipe) - closeWrite(result.outPipe) - closeWrite(result.errPipe) - - proc suspend(p: AsyncProcess) = - var res = 0'i32 - if p.isWow64: - res = wow64SuspendThread(p.fThreadHandle) - else: - res = suspendThread(p.fThreadHandle) - if res < 0: - raiseOsError(osLastError()) - - proc resume(p: AsyncProcess) = - let res = resumeThread(p.fThreadHandle) - if res < 0: - raiseOsError(osLastError()) - - proc running(p: AsyncProcess): bool = - var value = 0'i32 - let res = getExitCodeProcess(p.fProcessHandle, value) - if res == 0: - raiseOsError(osLastError()) - else: - if value == STILL_ACTIVE: - result = true - else: - p.isExit = true - p.exitCode = value - - proc terminate(p: AsyncProcess) = - if running(p): - discard terminateProcess(p.fProcessHandle, 0) - - proc kill(p: AsyncProcess) = - terminate(p) - - proc peekExitCode(p: AsyncProcess): int = - if p.isExit: - result = p.exitCode - else: - var value = 0'i32 - let res = getExitCodeProcess(p.fProcessHandle, value) - if res == 0: - raiseOsError(osLastError()) - else: - result = value - if value != STILL_ACTIVE: - p.isExit = true - p.exitCode = value - - when declared(addProcess): - proc waitForExit(p: AsyncProcess): Future[int] = - var retFuture = newFuture[int]("asyncproc.waitForExit") - - proc cb(fd: AsyncFD): bool = - var value = 0'i32 - let res = getExitCodeProcess(p.fProcessHandle, value) - if res == 0: - retFuture.fail(newException(OSError, osErrorMsg(osLastError()))) - else: - p.isExit = true - p.exitCode = value - retFuture.complete(p.exitCode) - - if p.isExit: - retFuture.complete(p.exitCode) - else: - addProcess(p.procId, cb) - return retFuture - -else: - const - readIdx = 0 - writeIdx = 1 - - template statusToExitCode(status): int32 = - (status and 0xFF00) shr 8 - - proc envToCStringArray(t: StringTableRef): cstringArray = - result = cast[cstringArray](alloc0((t.len + 1) * sizeof(cstring))) - var i = 0 - for key, val in pairs(t): - var x = key & "=" & val - result[i] = cast[cstring](alloc(x.len+1)) - copyMem(result[i], addr(x[0]), x.len+1) - inc(i) - - proc envToCStringArray(): cstringArray = - var counter = 0 - for key, val in envPairs(): inc counter - result = cast[cstringArray](alloc0((counter + 1) * sizeof(cstring))) - var i = 0 - for key, val in envPairs(): - var x = key & "=" & val - result[i] = cast[cstring](alloc(x.len+1)) - copyMem(result[i], addr(x[0]), x.len+1) - inc(i) - - type StartProcessData = object - sysCommand: cstring - sysArgs: cstringArray - sysEnv: cstringArray - workingDir: cstring - pStdin, pStdout, pStderr, pErrorPipe: array[0..1, cint] - options: set[ProcessOption] - - const useProcessAuxSpawn = declared(posix_spawn) and not defined(useFork) and - not defined(useClone) and not defined(linux) - when useProcessAuxSpawn: - proc startProcessAuxSpawn(data: StartProcessData): Pid {. - tags: [ExecIOEffect, ReadEnvEffect], gcsafe.} - else: - proc startProcessAuxFork(data: StartProcessData): Pid {. - tags: [ExecIOEffect, ReadEnvEffect], gcsafe.} - - {.push stacktrace: off, profiler: off.} - proc startProcessAfterFork(data: ptr StartProcessData) {. - tags: [ExecIOEffect, ReadEnvEffect], cdecl, gcsafe.} - {.pop.} - - proc startProcess(command: string, workingDir: string = "", - args: openArray[string] = [], - env: StringTableRef = nil, - options: set[ProcessOption] = {poStdErrToStdOut}, - pipeStdin: AsyncPipe = nil, - pipeStdout: AsyncPipe = nil, - pipeStderr: AsyncPipe = nil): AsyncProcess = - var sd = StartProcessData() - - result = AsyncProcess(options: options, isExit: true) - - if not isNil(pipeStdin): - sd.pStdin = pipeStdin.getHandles() - else: - if poParentStreams notin options: - let pipe = createPipe() - sd.pStdin = pipe.getHandles() - result.inPipe = pipe - - if not isNil(pipeStdout): - sd.pStdout = pipeStdout.getHandles() - else: - if poParentStreams notin options: - let pipe = createPipe() - sd.pStdout = pipe.getHandles() - result.outPipe = pipe - - if not isNil(pipeStderr): - sd.pStderr = pipeStderr.getHandles() - else: - if poParentStreams notin options: - if poStdErrToStdOut in options: - sd.pStderr = sd.pStdout - result.errPipe = result.outPipe - else: - let pipe = createPipe() - sd.pStderr = pipe.getHandles() - result.errPipe = pipe - - var sysCommand: string - var sysArgsRaw: seq[string] - - if poEvalCommand in options: - sysCommand = "/bin/sh" - sysArgsRaw = @[sysCommand, "-c", command] - assert args.len == 0, "`args` has to be empty when using poEvalCommand." - else: - sysCommand = command - sysArgsRaw = @[command] - for arg in args.items: - sysArgsRaw.add arg - - var pid: Pid - - var sysArgs = allocCStringArray(sysArgsRaw) - defer: deallocCStringArray(sysArgs) - - var sysEnv = if env == nil: - envToCStringArray() - else: - envToCStringArray(env) - defer: deallocCStringArray(sysEnv) - - sd.sysCommand = sysCommand - sd.sysArgs = sysArgs - sd.sysEnv = sysEnv - sd.options = options - sd.workingDir = workingDir - - when useProcessAuxSpawn: - let currentDir = getCurrentDir() - pid = startProcessAuxSpawn(sd) - if workingDir.len > 0: - setCurrentDir(currentDir) - else: - pid = startProcessAuxFork(sd) - - # Parent process. Copy process information. - if poEchoCmd in options: - echo(command, " ", join(args, " ")) - result.procId = pid - - result.isExit = false - - if poParentStreams notin options: - closeRead(result.inPipe) - closeWrite(result.outPipe) - closeWrite(result.errPipe) - - when useProcessAuxSpawn: - proc startProcessAuxSpawn(data: StartProcessData): Pid = - var attr: Tposix_spawnattr - var fops: Tposix_spawn_file_actions - - template chck(e: untyped) = - if e != 0'i32: raiseOSError(osLastError()) - - chck posix_spawn_file_actions_init(fops) - chck posix_spawnattr_init(attr) - - var mask: Sigset - chck sigemptyset(mask) - chck posix_spawnattr_setsigmask(attr, mask) - - var flags = POSIX_SPAWN_USEVFORK or POSIX_SPAWN_SETSIGMASK - if poDemon in data.options: - flags = flags or POSIX_SPAWN_SETPGROUP - chck posix_spawnattr_setpgroup(attr, 0'i32) - - chck posix_spawnattr_setflags(attr, flags) - - if not (poParentStreams in data.options): - chck posix_spawn_file_actions_addclose(fops, data.pStdin[writeIdx]) - chck posix_spawn_file_actions_adddup2(fops, data.pStdin[readIdx], - readIdx) - chck posix_spawn_file_actions_addclose(fops, data.pStdout[readIdx]) - chck posix_spawn_file_actions_adddup2(fops, data.pStdout[writeIdx], - writeIdx) - if (poStdErrToStdOut in data.options): - chck posix_spawn_file_actions_adddup2(fops, data.pStdout[writeIdx], 2) - else: - chck posix_spawn_file_actions_addclose(fops, data.pStderr[readIdx]) - chck posix_spawn_file_actions_adddup2(fops, data.pStderr[writeIdx], 2) - - var res: cint - if data.workingDir.len > 0: - setCurrentDir($data.workingDir) - var pid: Pid - - if (poUsePath in data.options): - res = posix_spawnp(pid, data.sysCommand, fops, attr, data.sysArgs, - data.sysEnv) - else: - res = posix_spawn(pid, data.sysCommand, fops, attr, data.sysArgs, - data.sysEnv) - - discard posix_spawn_file_actions_destroy(fops) - discard posix_spawnattr_destroy(attr) - chck res - return pid - else: - proc startProcessAuxFork(data: StartProcessData): Pid = - if pipe(data.pErrorPipe) != 0: - raiseOSError(osLastError()) - - defer: - discard close(data.pErrorPipe[readIdx]) - - var pid: Pid - var dataCopy = data - - when defined(useClone): - const stackSize = 65536 - let stackEnd = cast[clong](alloc(stackSize)) - let stack = cast[pointer](stackEnd + stackSize) - let fn: pointer = startProcessAfterFork - pid = clone(fn, stack, - cint(CLONE_VM or CLONE_VFORK or SIGCHLD), - pointer(addr dataCopy), nil, nil, nil) - discard close(data.pErrorPipe[writeIdx]) - dealloc(stack) - else: - pid = fork() - if pid == 0: - startProcessAfterFork(addr(dataCopy)) - exitnow(1) - - discard close(data.pErrorPipe[writeIdx]) - if pid < 0: raiseOSError(osLastError()) - - var error: cint - - var res = read(data.pErrorPipe[readIdx], addr error, sizeof(error)) - if res == sizeof(error): - raiseOSError(osLastError(), - "Could not find command: '$1'. OS error: $2" % - [$data.sysCommand, $strerror(error)]) - return pid - - {.push stacktrace: off, profiler: off.} - proc startProcessFail(data: ptr StartProcessData) = - var error: cint = errno - discard write(data.pErrorPipe[writeIdx], addr error, sizeof(error)) - exitnow(1) - - when not defined(uClibc) and (not defined(linux) or defined(android)): - var environ {.importc.}: cstringArray - - proc startProcessAfterFork(data: ptr StartProcessData) = - # Warning: no GC here! - # Or anything that touches global structures - all called nim procs - # must be marked with stackTrace:off. Inspect C code after making changes. - if (poDemon in data.options): - if posix.setpgid(Pid(0), Pid(0)) != 0: - startProcessFail(data) - - if not (poParentStreams in data.options): - if posix.close(data.pStdin[writeIdx]) != 0: - startProcessFail(data) - - if dup2(data.pStdin[readIdx], readIdx) < 0: - startProcessFail(data) - - if posix.close(data.pStdout[readIdx]) != 0: - startProcessFail(data) - - if dup2(data.pStdout[writeIdx], writeIdx) < 0: - startProcessFail(data) - - if (poStdErrToStdOut in data.options): - if dup2(data.pStdout[writeIdx], 2) < 0: - startProcessFail(data) - else: - if posix.close(data.pStderr[readIdx]) != 0: - startProcessFail(data) - - if dup2(data.pStderr[writeIdx], 2) < 0: - startProcessFail(data) - - if data.workingDir.len > 0: - if chdir(data.workingDir) < 0: - startProcessFail(data) - - if posix.close(data.pErrorPipe[readIdx]) != 0: - startProcessFail(data) - - discard fcntl(data.pErrorPipe[writeIdx], F_SETFD, FD_CLOEXEC) - - if (poUsePath in data.options): - when defined(uClibc): - # uClibc environment (OpenWrt included) doesn't have the full execvpe - discard execve(data.sysCommand, data.sysArgs, data.sysEnv) - elif defined(linux) and not defined(android): - discard execvpe(data.sysCommand, data.sysArgs, data.sysEnv) - else: - # MacOSX doesn't have execvpe, so we need workaround. - # On MacOSX we can arrive here only from fork, so this is safe: - environ = data.sysEnv - discard execvp(data.sysCommand, data.sysArgs) - else: - discard execve(data.sysCommand, data.sysArgs, data.sysEnv) - - startProcessFail(data) - {.pop} - - proc close(p: AsyncProcess) = - ## We need to `wait` for process, to avoid `zombie`, so if `running()` - ## returns `false`, then process exited and `wait()` was called. - doAssert(not p.running()) - if p.inPipe != nil: close(p.inPipe) - if p.outPipe != nil: close(p.outPipe) - if p.errPipe != nil: close(p.errPipe) - - proc running(p: AsyncProcess): bool = - result = true - if p.isExit: - result = false - else: - var status = cint(0) - let res = posix.waitpid(p.procId, status, WNOHANG) - if res == 0: - result = true - elif res < 0: - raiseOsError(osLastError()) - else: - if WIFEXITED(status) or WIFSIGNALED(status): - p.isExit = true - p.exitCode = statusToExitCode(status) - result = false - - proc peekExitCode(p: AsyncProcess): int = - if p.isExit: - result = p.exitCode - else: - var status = cint(0) - let res = posix.waitpid(p.procId, status, WNOHANG) - if res < 0: - raiseOsError(osLastError()) - elif res > 0: - p.isExit = true - p.exitCode = statusToExitCode(status) - result = p.exitCode - else: - result = STILL_ACTIVE - - proc suspend(p: AsyncProcess) = - if posix.kill(p.procId, SIGSTOP) != 0'i32: - raiseOsError(osLastError()) - - proc resume(p: AsyncProcess) = - if posix.kill(p.procId, SIGCONT) != 0'i32: - raiseOsError(osLastError()) - - proc terminate(p: AsyncProcess) = - if posix.kill(p.procId, SIGTERM) != 0'i32: - raiseOsError(osLastError()) - - proc kill(p: AsyncProcess) = - if posix.kill(p.procId, SIGKILL) != 0'i32: - raiseOsError(osLastError()) - - when declared(addProcess): - proc waitForExit*(p: AsyncProcess): Future[int] = - var retFuture = newFuture[int]("asyncproc.waitForExit") - - proc cb(fd: AsyncFD): bool = - var status = cint(0) - let res = posix.waitpid(p.procId, status, WNOHANG) - if res <= 0: - retFuture.fail(newException(OSError, osErrorMsg(osLastError()))) - else: - p.isExit = true - p.exitCode = statusToExitCode(status) - retFuture.complete(p.exitCode) - - if p.isExit: - retFuture.complete(p.exitCode) - else: - while true: - var status = cint(0) - let res = posix.waitpid(p.procId, status, WNOHANG) - if res < 0: - retFuture.fail(newException(OSError, osErrorMsg(osLastError()))) - break - elif res > 0: - p.isExit = true - p.exitCode = statusToExitCode(status) - retFuture.complete(p.exitCode) - break - else: - try: - addProcess(p.procId, cb) - break - except: - let err = osLastError() - if cint(err) == ESRCH: - continue - else: - retFuture.fail(newException(OSError, osErrorMsg(err))) - break - return retFuture - -proc execProcess(command: string, args: seq[string] = @[], - env: StringTableRef = nil, - options: set[ProcessOption] = {poStdErrToStdOut, poUsePath, - poEvalCommand} - ): Future[tuple[exitcode: int, output: string]] {.async.} = - result = (exitcode: int(STILL_ACTIVE), output: "") - let bufferSize = 1024 - var data = newString(bufferSize) - var p = startProcess(command, args = args, env = env, options = options) - - # Here the code path for Linux systems is a workaround for a bug eighter in - # the `asynctools` library or the Nim standard library which causes `Resource - # temporarily unavailable (code: 11)` error when executing multiple - # asynchronous operations. - # - # TODO: Add a proper fix that does not use a different code ordering on the - # different systems. - - when not defined(linux): - let future = p.waitForExit - - while true: - let res = await p.outputHandle.readInto(addr data[0], bufferSize) - if res > 0: - data.setLen(res) - result.output &= data - data.setLen(bufferSize) - else: - break - - result.exitcode = - when not defined(linux): - await future - else: - await p.waitForExit - - close(p) - -when isMainModule: - when defined(windows): - var data = waitFor(execProcess("cd")) - else: - var data = waitFor(execProcess("pwd")) - echo "exitCode = " & $data.exitcode - echo "output = [" & $data.output & "]" diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 8508b103..f44f24e6 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import parseutils, os, osproc, strutils, tables, pegs, uri, strformat, - httpclient, json, asyncdispatch, sequtils + httpclient, json, sequtils from algorithm import SortOrder, sorted @@ -13,48 +13,43 @@ type DownloadPkgResult* = tuple dir: string version: Version - vcsRevision: Sha1HashRef + vcsRevision: Sha1Hash -proc doCheckout(meth: DownloadMethod, downloadDir, branch: string): - Future[void] {.async.} = +proc doCheckout(meth: DownloadMethod, downloadDir, branch: string) = case meth of DownloadMethod.git: # 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. - discard await tryDoCmdExAsync("git", - @["-C", downloadDir, "checkout", "--force", "branch"]) - discard await tryDoCmdExAsync("git", - @["-C", downloadDir, "submodule", "update", "--recursive", "--depth", "1"]) + discard tryDoCmdEx(&"git -C {downloadDir} checkout --force branch") + discard tryDoCmdEx( + &"git -C {downloadDir} submodule update --recursive --depth 1") of DownloadMethod.hg: - discard await tryDoCmdExAsync("hg", - @["--cwd", downloadDir, "checkout", "branch"]) + discard tryDoCmdEx(&"hg --cwd {downloadDir} checkout branch") proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "", - onlyTip = true) {.async.} = + onlyTip = true) = case meth of DownloadMethod.git: let - depthArgs = if onlyTip: @["--depth", "1"] else: @[] - branchArgs = if branch == "": @[] else: @["-b", branch] - discard await tryDoCmdExAsync("git", concat(@["clone", "--recursive"], - depthArgs, branchArgs, @[url, downloadDir])) + depthArg = if onlyTip: "--depth 1" else: "" + branchArg = if branch == "": "" else: &"-b {branch}" + discard tryDoCmdEx( + &"git clone --recursive {depthArg} {branchArg} {url} {downloadDir}") of DownloadMethod.hg: let - tipArgs = if onlyTip: @["-r", "tip"] else: @[] - branchArgs = if branch == "": @[] else: @["-b", branch] - discard await tryDoCmdExAsync("hg", - concat(@["clone"], tipArgs, branchArgs, @[url, downloadDir])) + tipArg = if onlyTip: "-r tip " else: "" + branchArg = if branch == "": "" else: &"-b {branch}" + discard tryDoCmdEx(&"hg clone {tipArg} {branchArg} {url} {downloadDir}") -proc getTagsList(dir: string, meth: DownloadMethod): - Future[seq[string]] {.async.} = +proc getTagsList(dir: string, meth: DownloadMethod): seq[string] = var output: string cd dir: case meth of DownloadMethod.git: - output = await tryDoCmdExAsync("git", @["tag"]) + output = tryDoCmdEx("git tag") of DownloadMethod.hg: - output = await tryDoCmdExAsync("hg", @["tags"]) + output = tryDoCmdEx("hg tags") if output.len > 0: case meth of DownloadMethod.git: @@ -73,13 +68,11 @@ proc getTagsList(dir: string, meth: DownloadMethod): else: result = @[] -proc getTagsListRemote*(url: string, meth: DownloadMethod): - Future[seq[string]] {.async.} = +proc getTagsListRemote*(url: string, meth: DownloadMethod): seq[string] = result = @[] case meth of DownloadMethod.git: - var (output, exitCode) = await doCmdExAsync("git", - @["ls-remote", "--tags", url]) + var (output, exitCode) = doCmdEx(&"git ls-remote --tags {url}") if exitCode != QuitSuccess: raise nimbleError("Unable to query remote tags for " & url & ". Git returned: " & output) @@ -142,22 +135,20 @@ proc isURL*(name: string): bool = proc cloneSpecificRevision(downloadMethod: DownloadMethod, url, downloadDir: string, - vcsRevision: Sha1Hash) {.async.} = + vcsRevision: Sha1Hash) = assert vcsRevision != notSetSha1Hash display("Cloning", "revision: " & $vcsRevision, priority = MediumPriority) case downloadMethod of DownloadMethod.git: let downloadDir = downloadDir.quoteShell createDir(downloadDir) - discard await tryDoCmdExAsync("git", @["-C", downloadDir, "init"]) - discard await tryDoCmdExAsync("git", - @["-C", downloadDir, "remote", "add", "origin", url]) - discard await tryDoCmdExAsync("git", - @["-C", downloadDir, "fetch", "--depth", "1", "origin", $vcsRevision]) - discard await tryDoCmdExAsync("git", - @["-C", downloadDir, "reset", "--hard", "FETCH_HEAD"]) + discard tryDoCmdEx(&"git -C {downloadDir} init") + discard tryDoCmdEx(&"git -C {downloadDir} remote add origin {url}") + discard tryDoCmdEx( + &"git -C {downloadDir} fetch --depth 1 origin {vcsRevision}") + discard tryDoCmdEx(&"git -C {downloadDir} reset --hard FETCH_HEAD") of DownloadMethod.hg: - discard await tryDoCmdExAsync("hg", @["clone", url, "-r", $vcsRevision]) + discard tryDoCmdEx(&"hg clone {url} -r {vcsRevision}") proc getTarExePath: string = ## Returns path to `tar` executable. @@ -231,23 +222,22 @@ proc getGitHubApiUrl(url, commit: string): string = ## an URL for the GitHub REST API query for the full commit hash. &"https://api.github.com/repos/{extractOwnerAndRepo(url)}/commits/{commit}" -proc getUrlContent(url: string): Future[string] {.async.} = +proc getUrlContent(url: string): string = ## Makes a GET request to `url`. - let client = newAsyncHttpClient() - return await client.getContent(url) + let client = newHttpClient() + return client.getContent(url) {.warning[ProveInit]: off.} -proc getFullRevisionFromGitHubApi(url, version: string): - Future[Sha1HashRef] {.async.} = +proc getFullRevisionFromGitHubApi(url, version: string): Sha1Hash = ## By given a commit short hash and an URL to a GitHub repository retrieves ## the full hash of the commit by using GitHub REST API. try: let gitHubApiUrl = getGitHubApiUrl(url, version) display("Get", gitHubApiUrl); - let content = await getUrlContent(gitHubApiUrl) + let content = getUrlContent(gitHubApiUrl) let json = parseJson(content) if json.hasKey("sha"): - return json["sha"].str.initSha1Hash.newClone + return json["sha"].str.initSha1Hash else: raise nimbleError(json["message"].str) except CatchableError as error: @@ -255,7 +245,7 @@ proc getFullRevisionFromGitHubApi(url, version: string): &"of package at \"{url}\".", details = error) {.warning[ProveInit]: on.} -proc parseRevision(lsRemoteOutput: string): Sha1HashRef = +proc parseRevision(lsRemoteOutput: string): Sha1Hash = ## Parses the output from `git ls-remote` call to extract the returned sha1 ## hash value. Even when successful the first line of the command's output ## can be a redirection warning. @@ -263,43 +253,42 @@ proc parseRevision(lsRemoteOutput: string): Sha1HashRef = for line in lines: if line.len >= 40: try: - return line[0..39].initSha1Hash.newClone + return line[0..39].initSha1Hash except InvalidSha1HashError: discard - return notSetSha1Hash.newClone + return notSetSha1Hash -proc getRevision(url, version: string): Future[Sha1HashRef] {.async.} = +proc getRevision(url, version: string): Sha1Hash = ## Returns the commit hash corresponding to the given `version` of the package ## in repository at `url`. - let output = await tryDoCmdExAsync("git", @["ls-remote", url, $version]) + let output = tryDoCmdEx(&"git ls-remote {url} {version}") result = parseRevision(output) - if result[] == notSetSha1Hash: + if result == notSetSha1Hash: if version.seemsLikeRevision: - result = await getFullRevisionFromGitHubApi(url, version) + result = getFullRevisionFromGitHubApi(url, version) else: raise nimbleError(&"Cannot get revision for version \"{version}\" " & &"of package at \"{url}\".") -proc getTarCmdLine(downloadDir, filePath: string): - tuple[cmd: string, args: seq[string]] = +proc getTarCmdLine(downloadDir, filePath: string): string = ## Returns an OS specific command and arguments for extracting the downloaded ## tarball. when defined(Windows): let downloadDir = downloadDir.replace('\\', '/') let filePath = filePath.replace('\\', '/') - (getTarExePath(), @["-C", downloadDir, "-xf", filePath, - "--strip-components", "1", "--force-local"]) + &"{getTarExePath()} -C {downloadDir} -xf {filePath} --strip-components 1 " & + "--force-local" else: - ("tar", @["-C", downloadDir, "-xf", filePath, "--strip-components", "1"]) + &"tar -C {downloadDir} -xf {filePath} --strip-components 1" proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): - Future[Sha1HashRef] {.async.} = + Sha1Hash = ## Downloads package tarball from GitHub. Returns the commit hash of the ## downloaded package in the case `queryRevision` is `true`. let downloadLink = getTarballDownloadLink(url, version) display("Downloading", downloadLink) - let data = await getUrlContent(downloadLink) + let data = getUrlContent(downloadLink) display("Completed", "downloading " & downloadLink) let filePath = downloadDir / "tarball.tar.gz" @@ -309,8 +298,8 @@ proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): display("Completed", "saving " & filePath) display("Unpacking", filePath) - let (cmd, args) = getTarCmdLine(downloadDir, filePath) - let (output, exitCode) = await doCmdExAsync(cmd, args) + let cmd = getTarCmdLine(downloadDir, filePath) + let (output, exitCode) = doCmdEx(cmd) if exitCode != QuitSuccess and not output.contains("Cannot create symlink to"): # If the command fails for reason different then unable establishing a # sym-link raise an exception. This reason for failure is common on Windows @@ -321,14 +310,13 @@ proc doDownloadTarball(url, downloadDir, version: string, queryRevision: bool): display("Completed", "unpacking " & filePath) filePath.removeFile - return if queryRevision: await getRevision(url, version) - else: notSetSha1Hash.newClone + return if queryRevision: getRevision(url, version) else: notSetSha1Hash {.warning[ProveInit]: off.} -proc doDownload(url: string, downloadDir: string, verRange: VersionRange, +proc doDownload(url, downloadDir: string, verRange: VersionRange, downMethod: DownloadMethod, options: Options, vcsRevision: Sha1Hash): - Future[tuple[version: Version, vcsRevision: Sha1HashRef]] {.async.} = + tuple[version: Version, vcsRevision: Sha1Hash] = ## Downloads the repository specified by ``url`` using the specified download ## method. ## @@ -346,38 +334,37 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, if $latest.ver != "": result.version = latest.ver - result.vcsRevision = notSetSha1Hash.newClone + result.vcsRevision = notSetSha1Hash removeDir(downloadDir) if vcsRevision != notSetSha1Hash: if downloadTarball(url, options): - discard await doDownloadTarball(url, downloadDir, $vcsRevision, false) + discard doDownloadTarball(url, downloadDir, $vcsRevision, false) else: - await cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) - result.vcsRevision = vcsRevision.newClone + cloneSpecificRevision(downMethod, url, downloadDir, vcsRevision) + result.vcsRevision = vcsRevision elif verRange.kind == verSpecial: # We want a specific commit/branch/tag here. if verRange.spe == getHeadName(downMethod): # Grab HEAD. if downloadTarball(url, options): - result.vcsRevision = await doDownloadTarball( - url, downloadDir, "HEAD", true) + result.vcsRevision = doDownloadTarball(url, downloadDir, "HEAD", true) else: - await doClone(downMethod, url, downloadDir, - onlyTip = not options.forceFullClone) + doClone(downMethod, url, downloadDir, + onlyTip = not options.forceFullClone) else: assert ($verRange.spe)[0] == '#', "The special version must start with '#'." let specialVersion = substr($verRange.spe, 1) if downloadTarball(url, options): - result.vcsRevision = await doDownloadTarball( + result.vcsRevision = doDownloadTarball( url, downloadDir, specialVersion, true) else: # Grab the full repo. - await doClone(downMethod, url, downloadDir, onlyTip = false) + doClone(downMethod, url, downloadDir, onlyTip = false) # Then perform a checkout operation to get the specified branch/commit. # `spe` starts with '#', trim it. - await doCheckout(downMethod, downloadDir, specialVersion) + doCheckout(downMethod, downloadDir, specialVersion) result.version = verRange.spe else: case downMethod @@ -385,48 +372,46 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, # 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. result.version = getHeadName(downMethod) - let versions = (await getTagsListRemote(url, downMethod)).getVersionList() + let versions = getTagsListRemote(url, downMethod).getVersionList() if versions.len > 0: getLatestByTag: if downloadTarball(url, options): let versionToDownload = if latest.tag.len > 0: latest.tag else: "HEAD" - result.vcsRevision = await doDownloadTarball( + result.vcsRevision = doDownloadTarball( url, downloadDir, versionToDownload, true) else: display("Cloning", "latest tagged version: " & latest.tag, priority = MediumPriority) - await doClone(downMethod, url, downloadDir, latest.tag, - onlyTip = not options.forceFullClone) + doClone(downMethod, url, downloadDir, latest.tag, + onlyTip = not options.forceFullClone) else: display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, priority = HighPriority) if downloadTarball(url, options): - result.vcsRevision = await doDownloadTarball( - url, downloadDir, "HEAD", true) + result.vcsRevision = doDownloadTarball(url, downloadDir, "HEAD", true) else: # If no commits have been tagged on the repo we just clone HEAD. - await doClone(downMethod, url, downloadDir) # Grab HEAD. + doClone(downMethod, url, downloadDir) # Grab HEAD. of DownloadMethod.hg: - await doClone(downMethod, url, downloadDir, - onlyTip = not options.forceFullClone) + doClone(downMethod, url, downloadDir, + onlyTip = not options.forceFullClone) result.version = getHeadName(downMethod) - let versions = - (await getTagsList(downloadDir, downMethod)).getVersionList() + let versions = getTagsList(downloadDir, downMethod).getVersionList() if versions.len > 0: getLatestByTag: display("Switching", "to latest tagged version: " & latest.tag, priority = MediumPriority) - await doCheckout(downMethod, downloadDir, latest.tag) + doCheckout(downMethod, downloadDir, latest.tag) else: display("Warning:", "The package has no tagged releases, downloading HEAD instead.", Warning, priority = HighPriority) - if result.vcsRevision[] == notSetSha1Hash: + if result.vcsRevision == notSetSha1Hash: # In the case the package in not downloaded as tarball we must query its # VCS revision from its download directory. - result.vcsRevision = downloadDir.getVcsRevision.newClone + result.vcsRevision = downloadDir.getVcsRevision {.warning[ProveInit]: on.} proc downloadPkg*(url: string, verRange: VersionRange, @@ -434,7 +419,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, subdir: string, options: Options, downloadPath: string, - vcsRevision: Sha1Hash): Future[DownloadPkgResult] {.async.} = + vcsRevision: Sha1Hash): DownloadPkgResult = ## Downloads the repository as specified by ``url`` and ``verRange`` using ## the download method specified. ## @@ -477,7 +462,7 @@ proc downloadPkg*(url: string, verRange: VersionRange, priority = HighPriority) result.dir = downloadDir / subdir - (result.version, result.vcsRevision) = await doDownload( + (result.version, result.vcsRevision) = doDownload( modUrl, downloadDir, verRange, downMethod, options, vcsRevision) if verRange.kind != verSpecial: @@ -495,8 +480,7 @@ proc echoPackageVersions*(pkg: Package) = case downMethod of DownloadMethod.git: try: - let versions = - (waitFor getTagsListRemote(pkg.url, downMethod)).getVersionList() + let versions = getTagsListRemote(pkg.url, downMethod).getVersionList() if versions.len > 0: let sortedVersions = toSeq(values(versions)) echo(" versions: " & join(sortedVersions, ", ")) diff --git a/src/nimblepkg/lockfile.nim b/src/nimblepkg/lockfile.nim index a80b8033..7e871c90 100644 --- a/src/nimblepkg/lockfile.nim +++ b/src/nimblepkg/lockfile.nim @@ -14,7 +14,7 @@ const lockFileName* = "nimble.lock" lockFileVersion = 1 -proc initLockFileDep(): LockFileDep = +proc initLockFileDep*: LockFileDep = result = LockFileDep( version: notSetVersion, vcsRevision: notSetSha1Hash, diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 17fb1ad7..89388709 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -42,8 +42,6 @@ type developLocaldeps*: bool # True if local deps + nimble develop pkg1 ... disableSslCertCheck*: bool noTarballs*: bool # Disable downloading of packages as tarballs from GitHub. - maxParallelDownloads*: int # This is the maximum number of parallel - # downloads. 0 means no limit. ActionType* = enum actionNil, actionRefresh, actionInit, actionDump, actionPublish, @@ -189,8 +187,6 @@ Nimble Options: -l, --localdeps Run in project local dependency mode -t, --no-tarballs Disable downloading of packages as tarballs when working with GitHub repositories. - -m, --max-parallel-downloads The maximum number of parallel downloads. - The default value is 20. Use 0 for no limit. --ver Query remote server for package version information when searching or listing packages. --nimbleDir:dirname Set the Nimble directory. @@ -480,10 +476,6 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = of "localdeps", "l": result.localdeps = true of "nosslcheck": result.disableSslCertCheck = true of "no-tarballs", "t": result.noTarballs = true - of "max-parallel-downloads", "m": - result.maxParallelDownloads = parseInt(val) - if result.maxParallelDownloads == 0: - result.maxParallelDownloads = int.high else: isGlobalFlag = false var wasFlagHandled = true @@ -587,7 +579,6 @@ proc initOptions*(): Options = verbosity: HighPriority, noColor: not isatty(stdout), startDir: getCurrentDir(), - maxParallelDownloads: 20, ) proc handleUnknownFlags(options: var Options) = diff --git a/src/nimblepkg/sha1hashes.nim b/src/nimblepkg/sha1hashes.nim index 497052fb..5c12cdb6 100644 --- a/src/nimblepkg/sha1hashes.nim +++ b/src/nimblepkg/sha1hashes.nim @@ -13,8 +13,6 @@ type ## procedure which validates the input. hashValue: string - Sha1HashRef* = ref Sha1Hash - const notSetSha1Hash* = Sha1Hash(hashValue: "") diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index a88dbf24..29b7027b 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -3,12 +3,11 @@ # # Various miscellaneous utility functions reside here. import osproc, pegs, strutils, os, uri, sets, json, parseutils, strformat, - sequtils, asyncdispatch + sequtils from net import SslCVerifyMode, newContext, SslContext import version, cli, common, packageinfotypes, options, sha1hashes -import asynctools/asyncproc except quoteShell from compiler/nimblecmd import getPathVersionChecksum proc extractBin(cmd: string): string = @@ -53,19 +52,6 @@ proc doCmdEx*(cmd: string): ProcessOutput = raise nimbleError("'" & bin & "' not in PATH.") return execCmdEx(cmd) -proc removeQuotes(cmd: string): string = - cmd.filterIt(it != '"').join - -proc doCmdExAsync*(cmd: string, args: seq[string] = @[]): - Future[ProcessOutput] {.async.} = - displayDebug("Executing", join(concat(@[cmd], args), " ")) - let bin = extractBin(cmd) - if findExe(bin) == "": - raise nimbleError("'" & bin & "' not in PATH.") - let res = await asyncproc.execProcess(cmd.removeQuotes, args, - options = {asyncproc.poStdErrToStdOut, asyncproc.poUsePath}) - return (res.output, res.exitCode) - proc tryDoCmdExErrorMessage*(cmd, output: string, exitCode: int): string = &"Execution of '{cmd}' failed with an exit code {exitCode}.\n" & &"Details: {output}" @@ -76,13 +62,6 @@ proc tryDoCmdEx*(cmd: string): string {.discardable.} = raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) return output -proc tryDoCmdExAsync*(cmd: string, args: seq[string] = @[]): - Future[string] {.async.} = - let (output, exitCode) = await doCmdExAsync(cmd, args) - if exitCode != QuitSuccess: - raise nimbleError(tryDoCmdExErrorMessage(cmd, output, exitCode)) - return output - proc getNimBin*: string = result = "nim" if findExe("nim") != "": result = findExe("nim") From 6dfd5b87de59611590659b840e981f7c8a42009c Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 23 Aug 2021 18:57:27 +0300 Subject: [PATCH 71/73] Disable tarballs downloads by default Downloading of Nimble packages as tarballs when working with GitHub is now disabled by default. The option `--no-tarballs` for disabling it is removed and a new option `--tarballs` for explicitly enabling the feature is added instead. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 6 +++--- src/nimblepkg/options.nim | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index f44f24e6..871dd904 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -180,10 +180,10 @@ proc isGitHubRepo(url: string): bool = proc downloadTarball(url: string, options: Options): bool = ## Determines whether to download the repository as a tarball. - hasTar() and + options.enableTarballs and not options.forceFullClone and - not options.noTarballs and - url.isGitHubRepo + url.isGitHubRepo and + hasTar() proc removeTrailingGitString*(url: string): string = ## Removes ".git" from an URL. diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 89388709..8969a558 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -41,7 +41,7 @@ type localdeps*: bool # True if project local deps mode developLocaldeps*: bool # True if local deps + nimble develop pkg1 ... disableSslCertCheck*: bool - noTarballs*: bool # Disable downloading of packages as tarballs from GitHub. + enableTarballs*: bool # Enable downloading of packages as tarballs from GitHub. ActionType* = enum actionNil, actionRefresh, actionInit, actionDump, actionPublish, @@ -185,7 +185,7 @@ Nimble Options: -y, --accept Accept all interactive prompts. -n, --reject Reject all interactive prompts. -l, --localdeps Run in project local dependency mode - -t, --no-tarballs Disable downloading of packages as tarballs + -t, --tarballs Enable downloading of packages as tarballs when working with GitHub repositories. --ver Query remote server for package version information when searching or listing packages. @@ -475,7 +475,7 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = of "nim": result.nim = val of "localdeps", "l": result.localdeps = true of "nosslcheck": result.disableSslCertCheck = true - of "no-tarballs", "t": result.noTarballs = true + of "tarballs", "t": result.enableTarballs = true else: isGlobalFlag = false var wasFlagHandled = true From 226dfdd626abc3993474cb222a3bebd81e68f2f8 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Mon, 23 Aug 2021 19:12:38 +0300 Subject: [PATCH 72/73] Fix a regression in the `doCheckout` procedure Pass the requested branch to the `git` or `hg` command instead of the "branch" word. Related to nim-lang/nimble#127 --- src/nimblepkg/download.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index 871dd904..c550186c 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -21,11 +21,11 @@ proc doCheckout(meth: DownloadMethod, downloadDir, branch: string) = # 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. - discard tryDoCmdEx(&"git -C {downloadDir} checkout --force branch") + discard tryDoCmdEx(&"git -C {downloadDir} checkout --force {branch}") discard tryDoCmdEx( &"git -C {downloadDir} submodule update --recursive --depth 1") of DownloadMethod.hg: - discard tryDoCmdEx(&"hg --cwd {downloadDir} checkout branch") + discard tryDoCmdEx(&"hg --cwd {downloadDir} checkout {branch}") proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "", onlyTip = true) = From c8c252ec3b1833abfc731335e766ba7965909051 Mon Sep 17 00:00:00 2001 From: Ivan Bobev Date: Wed, 25 Aug 2021 22:19:01 +0300 Subject: [PATCH 73/73] Fix a crash when a package file cannot be open Sometimes when downloading a package some symbolic links files cannot be open on Windows when Nimble is run as administrator. For this reason, add exception handling to the `open` procedure to avoid the crash and display a warning message that the file content will not be count in the calculation of the package's checksum. Related to nim-lang/nimble#127 --- src/nimblepkg/checksums.nim | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/nimblepkg/checksums.nim b/src/nimblepkg/checksums.nim index db740faf..39d7534a 100644 --- a/src/nimblepkg/checksums.nim +++ b/src/nimblepkg/checksums.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. import os, std/sha1, strformat, algorithm -import common, version, sha1hashes, vcstools, paths +import common, version, sha1hashes, vcstools, paths, cli type ChecksumError* = object of NimbleError @@ -25,7 +25,15 @@ proc updateSha1Checksum(checksum: var Sha1State, fileName, filePath: string) = # directory from which no files are being installed. return checksum.update(fileName) - let file = filePath.open(fmRead) + var file: File + try: + file = filePath.open(fmRead) + except IOError: + ## If the file cannot be open for reading do not count its content in the + ## checksum. + displayWarning(&"The file \"{filePath}\" cannot be open for reading.\n" & + "Skipping it in the calculation of the checksum.") + return defer: close(file) const bufferSize = 8192 var buffer = newString(bufferSize)