A => .envrc +2 -0
@@ 1,2 @@
+source_env ..
+use nix
A => README.md +1 -0
@@ 1,1 @@
+Nim lockfile generator
A => Tuprules.tup +1 -0
@@ 1,1 @@
+NIM_FLAGS += --path:$(TUP_CWD)/../nim/v1.6.8
A => nim_lk.nimble +8 -0
@@ 1,8 @@
+author = "Emery Hemingway"
+bin = @["nim_lk"]
+description = "Tool for generating Nim lockfiles"
+license = "BSD-3-Clause"
+srcDir = "src"
+version = "20231001"
+
+requires "nim >= 2.0.0"
A => shell.nix +1 -0
@@ 1,1 @@
+let pkgs = import <nixpkgs> { }; in pkgs.nitter
A => src/Tupfile +2 -0
@@ 1,2 @@
+include_rules
+: nim_lk.nim |> !nim_bin |>
A => src/nim_lk.nim +195 -0
@@ 1,195 @@
+import private/nix
+
+import nimblepkg/common,
+ nimblepkg/download,
+ nimblepkg/packageinfo,
+ nimblepkg/options,
+ nimblepkg/version,
+ nimblepkg/packageparser,
+ nimblepkg/cli
+
+import std/[algorithm, deques, httpclient, json, monotimes, os, osproc, parseutils, random, sequtils, streams, strutils, tables, times, uri]
+
+const githubPackagesUrl =
+ "https://raw.githubusercontent.com/nim-lang/packages/master/packages.json"
+
+proc registryCachePath: string =
+ result = getEnv("XDG_CACHE_HOME")
+ if result == "":
+ let home = getEnv("HOME")
+ if home == "":
+ result = getEnv("TMPDIR", "/tmp")
+ else:
+ result = home / ".cache"
+ result.add "/packages.json"
+
+func isGitUrl(uri: Uri): bool =
+ uri.path.endsWith(".git") or
+ uri.scheme == "git" or
+ uri.scheme.startsWith("git+")
+
+proc gitLsRemote(url: string): seq[tuple[tag: string, rev: string]] =
+ var lines = execProcess(
+ "git",
+ args = ["ls-remote", "--tags", url],
+ options = {poUsePath},
+ )
+ result.setLen(lines.countLines.pred)
+ var off = 0
+ for i in 0..result.high:
+ off.inc parseUntil(lines, result[i].rev, {'\x09'}, off)
+ off.inc skipWhiteSpace(lines, off)
+ off.inc skipUntil(lines, '/', off).succ
+ off.inc skipUntil(lines, '/', off).succ
+ off.inc parseUntil(lines, result[i].tag, {'\x0a'}, off).succ
+
+proc matchRev(url: string; wanted: VersionRange): tuple[tag: string, rev: string] =
+ let pairs = gitLsRemote(url)
+ var resultVersion: Version
+ for (tag, rev) in pairs:
+ var tagVer = Version(tag)
+ if tagVer.withinRange(wanted) and resultVersion < tagVer:
+ resultVersion = tagVer
+ result = (tag, rev)
+ if result.rev == "" and pairs.len > 0:
+ result = pairs[pairs.high]
+
+proc collectMetadata(data: JsonNode) =
+ let storePath = data["path"].getStr
+ var packageNames = newJArray()
+ for (kind, path) in walkDir(storePath):
+ if kind in {pcFile, pcLinkToFile} and path.endsWith(".nimble"):
+ var (dir, name, ext) = splitFile(path)
+ packageNames.add %name
+ if packageNames.len == 0:
+ quit("no .nimble files found in " & storePath)
+ data["packages"] = packageNames
+
+proc prefechtGit(uri: Uri; version: VersionRange): JsonNode =
+ var
+ uri = uri
+ subdir = ""
+ uri.scheme.removePrefix("git+")
+ if uri.query != "":
+ if uri.query.startsWith("subdir="):
+ subdir = uri.query[7 .. ^1]
+ uri.query = ""
+ let url = $uri
+ let (tag, rev) = matchRev(url, version)
+ var args = @["--quiet", "--fetch-submodules", "--url", url]
+ if rev != "":
+ args.add "--rev"
+ args.add rev
+ let dump = execProcess(
+ "nix-prefetch-git",
+ args = args,
+ options = {poUsePath})
+ try: result = parseJson dump
+ except JsonParsingError:
+ stderr.writeLine "failed to parse output of nix-prefetch-git ", join(args, " ")
+ quit(dump)
+ if subdir != "":
+ result["subdir"] = %* subdir
+ result["method"] = %"git"
+ if tag != "":
+ result["ref"] = %tag
+ collectMetadata(result)
+
+proc containsPackageUri(lockAttrs: JsonNode; pkgUri: string): bool =
+ for e in lockAttrs.items:
+ if e["url"].getStr == pkgUri:
+ return true
+
+proc containsPackage(lockAttrs: JsonNode; pkgName: string): bool =
+ for e in lockAttrs.items:
+ for other in e["packages"].items:
+ if pkgName == other.getStr:
+ return true
+
+proc collectRequires(pending: var Deque[PkgTuple]; options: Options; pkgPath: string) =
+ var
+ nimbleFilePath = findNimbleFile(pkgPath, true)
+ pkg = readPackageInfo(nimbleFilePath, options)
+ for pair in pkg.requires:
+ if pair.name != "nim" and pair.name != "compiler":
+ pending.addLast(pair)
+
+var globalRegistry: JsonNode
+
+proc getPackgeUri(name: string): tuple[uri: string, meth: string] =
+ if globalRegistry.isNil:
+ let registryPath = registryCachePath()
+ if fileExists(registryPath):
+ globalRegistry = parseFile(registryPath)
+ else:
+ let client = newHttpClient()
+ var raw = client.getContent(githubPackagesUrl)
+ close(client)
+ writeFile(registryPath, raw)
+ globalRegistry = parseJson(raw)
+ var
+ name = name
+ i = 0
+ while i < globalRegistry.len:
+ var e = globalRegistry[i]
+ if e["name"].getStr == name:
+ if e.hasKey "alias":
+ var alias = e["alias"].getStr
+ doAssert alias != name
+ name = alias
+ i = 0
+ else:
+ try:
+ return (e["url"].getStr, e["method"].getStr,)
+ except CatchableError:
+ quit("Failed to parse shit JSON " & $e)
+ inc i
+
+proc generateLockfile(options: Options): JsonNode =
+ result = newJObject()
+ var
+ deps = newJArray()
+ pending: Deque[PkgTuple]
+ collectRequires(pending, options, getCurrentDir())
+ while pending.len > 0:
+ let batchLen = pending.len
+ for i in 1..batchLen:
+ var pkgData: JsonNode
+ let pkg = pending.popFirst()
+ if pkg.name == "nim" or pkg.name == "compiler":
+ continue
+ var uri = parseUri(pkg.name)
+ if uri.scheme == "" and not deps.containsPackage(pkg.name):
+ pending.addLast(pkg)
+ elif not deps.containsPackageUri(pkg.name):
+ if uri.isGitUrl:
+ pkgData = prefechtGit(uri, pkg.ver)
+ else:
+ quit("unhandled URI " & $uri)
+ collectRequires(pending, options, pkgData["path"].getStr)
+ deps.add pkgData
+
+ if batchLen == pending.len:
+ var
+ pkg = pending.popFirst()
+ info = getPackgeUri(pkg.name)
+ case info.meth
+ of "git":
+ stderr.writeLine "prefetch ", info.uri
+ var pkgData = prefechtGit(parseUri info.uri, pkg.ver)
+ collectRequires(pending, options, pkgData["path"].getStr)
+ deps.add pkgData
+ else:
+ quit("unhandled fetch method " & $info.meth & " for " & info.uri)
+ sort(deps.elems)
+ result["depends"] = deps
+
+proc main =
+ var options = parseCmdLine()
+ # parse nimble options, not recommended
+ if options.action.typ != actionCustom:
+ options.action = Action(typ: actionCustom)
+ var lockInfo = generateLockfile(options)
+ stdout.writeLine lockInfo
+
+main()
A => src/nim_lk.nim.cfg +3 -0
@@ 1,3 @@
+define:nixbuild
+define:ssl
+threads:off
A => src/nimblepkg/checksums.nim +72 -0
@@ 1,72 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+import os, std/sha1, strformat, algorithm
+import common, version, sha1hashes, vcstools, paths, cli
+
+type
+ ChecksumError* = object of NimbleError
+
+proc checksumError*(name: string, version: Version,
+ 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}
+ Expected checksum: {expectedChecksum}
+""")
+
+proc updateSha1Checksum(checksum: var Sha1State, fileName, filePath: string) =
+ 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)
+ if symlinkExists(filePath):
+ # Check whether a file is a symbolic link and if so update the checksum with
+ # the path to the file that the link points to.
+ var linkPath: string
+ try:
+ linkPath = expandSymlink(filePath)
+ except OSError:
+ displayWarning(&"Cannot expand symbolic link \"{filePath}\".\n" &
+ "Skipping it in the calculation of the checksum.")
+ return
+ checksum.update(linkPath)
+ else:
+ # Otherwise this is an ordinary file and we are adding its content to the
+ # checksum.
+ 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)
+ while true:
+ var bytesRead = readChars(file, buffer)
+ if bytesRead == 0: break
+ checksum.update(buffer.toOpenArray(0, bytesRead - 1))
+
+proc calculateDirSha1Checksum*(dir: string): Sha1Hash =
+ ## Recursively calculates the sha1 checksum of the contents of the directory
+ ## `dir` and its subdirectories.
+ ##
+ ## Raises a `NimbleError` if:
+ ## - the external command for getting the package file list fails.
+ ## - the directory does not exist.
+
+ var packageFiles = getPackageFileList(dir.Path)
+ packageFiles.sort
+ var checksum = newSha1State()
+ for file in packageFiles:
+ updateSha1Checksum(checksum, file, dir / file)
+ result = initSha1Hash($SecureHash(checksum.finalize()))
A => src/nimblepkg/cli.nim +293 -0
@@ 1,293 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+#
+# Rough rules/philosophy for the messages that Nimble displays are the following:
+# - Green is only shown when the requested operation is successful.
+# - Blue can be used to emphasise certain keywords, for example actions such
+# as "Downloading" or "Reading".
+# - Red is used when the requested operation fails with an error.
+# - Yellow is used for warnings.
+#
+# - Dim for LowPriority.
+# - Bright for HighPriority.
+# - Normal for MediumPriority.
+
+import terminal, sets, strutils
+import version
+
+when not declared(initHashSet):
+ import common
+
+type
+ CLI* = ref object
+ level: Priority
+ warnings: HashSet[(string, string)]
+ suppressionCount: int ## Amount of messages which were not shown.
+ showColor: bool ## Whether messages should be colored.
+ suppressMessages: bool ## Whether Warning, Message and Success messages
+ ## should be suppressed, useful for
+ ## commands like `dump` whose output should be
+ ## machine readable.
+
+ Priority* = enum
+ DebugPriority, LowPriority, MediumPriority, HighPriority
+
+ DisplayType* = enum
+ Error, Warning, Message, Success
+
+ ForcePrompt* = enum
+ dontForcePrompt, forcePromptYes, forcePromptNo
+
+const
+ longestCategory = len("Downloading")
+ foregrounds: array[Error .. Success, ForegroundColor] =
+ [fgRed, fgYellow, 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
+
+proc isSuppressed(displayType: DisplayType): bool =
+ # Don't print any Warning, Message or Success messages when suppression of
+ # warnings is enabled. That is, unless the user asked for --verbose output.
+ if globalCLI.suppressMessages and displayType >= Warning and
+ globalCLI.level == HighPriority:
+ return true
+
+proc displayCategory(category: string, displayType: DisplayType,
+ priority: Priority) =
+ if isSuppressed(displayType):
+ return
+
+ # Calculate how much the `category` must be offset to align along a center
+ # line.
+ let offset = calculateCategoryOffset(category)
+
+ # Display the category.
+ let text = "$1$2 " % [spaces(offset), category]
+ if globalCLI.showColor:
+ if priority != DebugPriority:
+ setForegroundColor(stdout, foregrounds[displayType])
+ writeStyled(text, styles[priority])
+ resetAttributes()
+ else:
+ stdout.write(text)
+
+proc displayLine(category, line: string, displayType: DisplayType,
+ priority: Priority) =
+ if isSuppressed(displayType):
+ return
+
+ displayCategory(category, displayType, priority)
+
+ # Display the message.
+ echo(line)
+
+proc display*(category, msg: string, displayType = Message,
+ priority = MediumPriority) =
+ # Multiple warnings containing the same messages should not be shown.
+ let warningPair = (category, msg)
+ if displayType == Warning:
+ if warningPair in globalCLI.warnings:
+ return
+ else:
+ globalCLI.warnings.incl(warningPair)
+
+ # Suppress this message if its priority isn't high enough.
+ # TODO: Per-priority suppression counts?
+ if priority < globalCLI.level:
+ if priority != DebugPriority:
+ globalCLI.suppressionCount.inc
+ return
+
+ # Display each line in the message.
+ var i = 0
+ for line in msg.splitLines():
+ if len(line) == 0: continue
+ displayLine(if i == 0: category else: "...", line, displayType, priority)
+ i.inc
+
+proc displayDebug*(category, msg: string) =
+ ## Convenience for displaying debug messages.
+ display(category, msg, priority = DebugPriority)
+
+proc displayDebug*(msg: string) =
+ ## Convenience for displaying debug messages with a default category.
+ displayDebug("Debug:", msg)
+
+proc displayTip*() =
+ ## Called just before Nimble exits. Shows some tips for the user, for example
+ ## the amount of messages that were suppressed and how to show them.
+ if globalCLI.suppressionCount > 0:
+ let msg = "$1 messages have been suppressed, use --verbose to show them." %
+ $globalCLI.suppressionCount
+ display("Tip:", msg, Warning, HighPriority)
+
+proc prompt*(forcePrompts: ForcePrompt, question: string): bool =
+ case forcePrompts
+ of forcePromptYes:
+ display("Prompt:", question & " -> [forced yes]", Warning, HighPriority)
+ return true
+ of forcePromptNo:
+ display("Prompt:", question & " -> [forced no]", Warning, HighPriority)
+ return false
+ of dontForcePrompt:
+ displayLine("Prompt:", question & " [y/N]", Warning, HighPriority)
+ displayCategory("Answer:", Warning, HighPriority)
+ let yn = stdin.readLine()
+ case yn.normalize
+ of "y", "yes":
+ return true
+ of "n", "no":
+ return false
+ else:
+ return false
+
+proc promptCustom*(forcePrompts: ForcePrompt, question, default: string): string =
+ case forcePrompts:
+ of forcePromptYes:
+ display("Prompt:", question & " -> [forced " & default & "]", Warning,
+ HighPriority)
+ return default
+ else:
+ if default == "":
+ display("Prompt:", question, Warning, HighPriority)
+ displayCategory("Answer:", Warning, HighPriority)
+ let user = stdin.readLine()
+ if user.len == 0: return promptCustom(forcePrompts, question, default)
+ else: return user
+ else:
+ display("Prompt:", question & " [" & default & "]", Warning, HighPriority)
+ displayCategory("Answer:", Warning, HighPriority)
+ let user = stdin.readLine()
+ if user == "": return default
+ else: return user
+
+proc promptCustom*(question, default: string): string =
+ return promptCustom(dontForcePrompt, question, default)
+
+proc promptListInteractive(question: string, args: openarray[string]): string =
+ display("Prompt:", question, Warning, HighPriority)
+ display("Select", "Cycle with 'Tab', 'Enter' when done", Message,
+ HighPriority)
+ displayCategory("Choices:", Warning, HighPriority)
+ var
+ current = 0
+ selected = false
+ # Incase the cursor is at the bottom of the terminal
+ for arg in args:
+ stdout.write "\n"
+ # Reset the cursor to the start of the selection prompt
+ cursorUp(stdout, args.len)
+ cursorForward(stdout, longestCategory)
+ hideCursor(stdout)
+
+ # The selection loop
+ while not selected:
+ setForegroundColor(fgDefault)
+ # Loop through the options
+ for i, arg in args:
+ # Check if the option is the current
+ if i == current:
+ writeStyled("> " & arg & " <", {styleBright})
+ else:
+ writeStyled(" " & arg & " ", {styleDim})
+ # Move the cursor back to the start
+ for s in 0..<(arg.len + 4):
+ cursorBackward(stdout)
+ # Move down for the next item
+ cursorDown(stdout)
+ # Move the cursor back up to the start of the selection prompt
+ for i in 0..<(args.len()):
+ cursorUp(stdout)
+ resetAttributes(stdout)
+
+ # Begin key input
+ while true:
+ case getch():
+ of '\t':
+ current = (current + 1) mod args.len
+ break
+ of '\r':
+ selected = true
+ break
+ of '\3':
+ showCursor(stdout)
+ raise newException(NimbleError, "Keyboard interrupt")
+ else: discard
+
+ # Erase all lines of the selection
+ for i in 0..<args.len:
+ eraseLine(stdout)
+ cursorDown(stdout)
+ # Move the cursor back up to the initial selection line
+ for i in 0..<args.len():
+ cursorUp(stdout)
+ showCursor(stdout)
+ display("Answer:", args[current], Warning,HighPriority)
+ return args[current]
+
+proc promptListFallback(question: string, args: openarray[string]): string =
+ display("Prompt:", question & " [" & join(args, "/") & "]", Warning,
+ HighPriority)
+ displayCategory("Answer:", Warning, HighPriority)
+ result = stdin.readLine()
+ for arg in args:
+ if arg.cmpIgnoreCase(result) == 0:
+ return arg
+
+proc promptList*(forcePrompts: ForcePrompt, question: string, args: openarray[string]): string =
+ case forcePrompts:
+ of forcePromptYes:
+ result = args[0]
+ display("Prompt:", question & " -> [forced " & result & "]", Warning,
+ HighPriority)
+ else:
+ if isatty(stdout):
+ return promptListInteractive(question, args)
+ else:
+ return promptListFallback(question, args)
+
+proc setVerbosity*(level: Priority) =
+ globalCLI.level = level
+
+proc setShowColor*(val: bool) =
+ globalCLI.showColor = val
+
+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)
A => src/nimblepkg/common.nim +78 -0
@@ 1,78 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+#
+# Various miscellaneous common types reside here, to avoid problems with
+# recursive imports
+
+when not defined(nimscript):
+ import sets
+
+ 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`
+ 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'.
+ ## If in doubt, use `getConcreteVersion` instead.
+ 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*: seq[string]
+ binDir*: string
+ srcDir*: string
+ backend*: string
+ foreignDeps*: seq[string]
+
+ ## Same as quit(QuitSuccess), but allows cleanup.
+ NimbleQuit* = ref object of CatchableError
+
+ proc raiseNimbleError*(msg: string, hint = "") =
+ var exc = newException(NimbleError, msg)
+ exc.hint = hint
+ raise exc
+
+ 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
+
+ return (error, hint)
+
+const
+ nimbleVersion* = "0.11.0"
+
+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)
A => src/nimblepkg/config.nim +124 -0
@@ 1,124 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+import parsecfg, streams, strutils, os, tables, uri
+
+import version, cli
+
+type
+ Config* = object
+ nimbleDir*: string
+ chcp*: bool # Whether to change the code page in .cmd files on Win.
+ packageLists*: Table[string, PackageList] ## Names -> packages.json files
+ cloneUsingHttps*: bool # Whether to replace git:// for https://
+ httpProxy*: Uri # Proxy for package list downloads.
+ nimLibPrefix*: string # Nim stdlib prefix.
+
+ PackageList* = object
+ name*: string
+ urls*: seq[string]
+ path*: string
+
+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: @[
+ "https://github.com/nim-lang/packages/raw/master/packages.json",
+ "http://irclogs.nim-lang.org/packages.json",
+ "http://nim-lang.org/nimble/packages.json"
+ ])
+ result.packageLists["official"] = defaultPkgList
+
+ result.nimLibPrefix = ""
+
+proc initPackageList(): PackageList =
+ result.name = ""
+ result.urls = @[]
+ result.path = ""
+
+proc addCurrentPkgList(config: var Config, currentPackageList: PackageList) =
+ if currentPackageList.name.len > 0:
+ config.packageLists[currentPackageList.name.normalize] = currentPackageList
+
+proc parseConfig*(): Config =
+ result = initConfig()
+ var confFile = getConfigDir() / "nimble" / "nimble.ini"
+
+ var f = newFileStream(confFile, fmRead)
+ if f == nil:
+ # Try the old deprecated babel.ini
+ # TODO: This can be removed.
+ confFile = getConfigDir() / "babel" / "babel.ini"
+ f = newFileStream(confFile, fmRead)
+ if f != nil:
+ display("Warning", "Using deprecated config file at " & confFile,
+ Warning, HighPriority)
+ if f != nil:
+ display("Reading", "config file at " & confFile, priority = LowPriority)
+ var p: CfgParser
+ open(p, f, confFile)
+ var currentSection = ""
+ var currentPackageList = initPackageList()
+ while true:
+ var e = next(p)
+ case e.kind
+ 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)
+ 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)
+ addCurrentPkgList(result, currentPackageList)
+ break
+ of cfgSectionStart:
+ addCurrentPkgList(result, currentPackageList)
+ currentSection = e.section
+ case currentSection.normalize
+ of "packagelist":
+ currentPackageList = initPackageList()
+ else:
+ raise newException(NimbleError, "Unable to parse config file:" &
+ " Unknown section: " & e.key)
+ of cfgKeyValuePair, cfgOption:
+ case e.key.normalize
+ of "nimbledir":
+ # Ensure we don't restore the deprecated nimble dir.
+ if e.value != getHomeDir() / ".babel":
+ result.nimbleDir = e.value
+ of "chcp":
+ result.chcp = parseBool(e.value)
+ of "cloneusinghttps":
+ result.cloneUsingHttps = parseBool(e.value)
+ of "httpproxy":
+ result.httpProxy = parseUri(e.value)
+ of "name":
+ case currentSection.normalize
+ of "packagelist":
+ currentPackageList.name = e.value
+ else: assert false
+ of "url":
+ case currentSection.normalize
+ of "packagelist":
+ currentPackageList.urls.add(e.value)
+ else: assert false
+ of "path":
+ case currentSection.normalize
+ of "packagelist":
+ if currentPackageList.path != "":
+ raise newException(NimbleError, "Attempted to specify more than one `path` for the same package list.")
+ else:
+ currentPackageList.path = e.value
+ else: assert false
+ of "nimlibprefix":
+ result.nimLibPrefix = e.value
+ else:
+ raise newException(NimbleError, "Unable to parse config file:" &
+ " Unknown key: " & e.key)
+ of cfgError:
+ raise newException(NimbleError, "Unable to parse config file: " & e.msg)
+ close(p)
A => src/nimblepkg/deps.nim +52 -0
@@ 1,52 @@
+import packageinfotypes, developfile, packageinfo, version, tables, strformat, strutils
+
+type
+ DependencyNode = ref object of RootObj
+ name*: string
+ version*: string
+ resolvedTo*: string
+ error*: string
+ dependencies*: seq[DependencyNode]
+
+proc depsRecursive*(pkgInfo: PackageInfo,
+ dependencies: seq[PackageInfo],
+ errors: ValidationErrors): seq[DependencyNode] =
+ result = @[]
+
+ for (name, ver) in pkgInfo.fullRequirements:
+ var depPkgInfo = initPackageInfo()
+ let
+ found = dependencies.findPkg((name, ver), depPkgInfo)
+ packageName = if found: depPkgInfo.basicInfo.name else: name
+
+ let node = DependencyNode(name: packageName)
+
+ result.add node
+ node.version = if ver.kind == verAny: "@any" else: $ver
+ node.resolvedTo = if found: $depPkgInfo.basicInfo.version else: ""
+ node.error = if errors.contains(packageName):
+ getValidationErrorMessage(packageName, errors.getOrDefault packageName)
+ else: ""
+
+ if found:
+ node.dependencies = depsRecursive(depPkgInfo, dependencies, errors)
+
+proc printDepsHumanReadable*(pkgInfo: PackageInfo,
+ dependencies: seq[PackageInfo],
+ level: int,
+ errors: ValidationErrors) =
+ for (name, ver) in pkgInfo.requires:
+ var depPkgInfo = initPackageInfo()
+ let
+ found = dependencies.findPkg((name, ver), depPkgInfo)
+ packageName = if found: depPkgInfo.basicInfo.name else: name
+
+ echo " ".repeat(level * 2),
+ packageName,
+ if ver.kind == verAny: "@any" else: " " & $ver,
+ if found: fmt "(resolved {depPkgInfo.basicInfo.version})" else: "",
+ if errors.contains(packageName):
+ " - error: " & getValidationErrorMessage(packageName, errors.getOrDefault packageName)
+ else:
+ ""
+ if found: printDepsHumanReadable(depPkgInfo, dependencies, level + 1, errors)
A => src/nimblepkg/developfile.nim +953 -0
@@ 1,953 @@
+# 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, strutils, math,
+ std/jsonutils
+
+import typetraits except distinctBase
+
+import common, cli, packageinfotypes, packageinfo, packageparser, options,
+ version, paths, displaymessages, sha1hashes,
+ tools, vcstools, syncfile, lockfile
+
+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* = 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: 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.
+
+ DevelopFileDataCache = Table[Path, DevelopFileData]
+ ## A cache for the loaded develop files data used to avoid multiple loads
+ ## of the same file when its data is queried in the code.
+
+ 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
+
+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* = 1
+ ## The version of the develop file's JSON schema.
+
+proc initDevelopFileData: DevelopFileData =
+ result = DevelopFileData(dependentPkg: initPackageInfo())
+
+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.dependentPkg.isLoaded,
+ "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 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.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
+ ## 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.jsonData.includes.toSeq,
+ $dfjkDependencies: %data.jsonData.dependencies.toSeq,
+ }
+
+ if path.fileExists and not overwrite:
+ raise nimbleError(fileAlreadyExistsMsg($path))
+
+ writeFile(path, json.pretty)
+ displaySuccess(developFileSavedMsg($path), priority = DebugPriority)
+
+proc developFileExists*(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 developFileExists*(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.developFileExists
+
+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.
+ ##
+ ## 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.
+
+ try:
+ result.pkgInfo = getPkgInfo(string(pkgPath), options, true)
+ except CatchableError as error:
+ result.error = 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 &= &"\n\"{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.basicInfo.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[].basicInfo.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[].basicInfo.name, (alreadyIncludedPkgPath, devFileName))
+ for devFileName in actualComingFrom:
+ 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.
+ 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`.
+ lhs.addPackages(rhs.nameToPkg.values, 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(path: Path, dependentPkg: PackageInfo, options: Options,
+ silentIfFileNotExists, raiseOnValidationErrors, loadGlobalDeps: bool):
+ DevelopFileData
+
+template load(dependentPkg: PackageInfo, args: varargs[untyped]):
+ DevelopFileData =
+ ## Loads data for the `dependentPkg`'s develop file by searching it in the
+ ## package's Nimble file directory. Delegates the functionality to the `load`
+ ## procedure taking path to develop file.
+ dependentPkg.assertIsLoaded
+ load(dependentPkg.getPkgDevFilePath, dependentPkg, args)
+
+proc loadGlobalDependencies(result: var DevelopFileData,
+ collidingNames: var CollidingNames,
+ options: Options) =
+ ## Loads data from the `links` subdirectory in the Nimble cache. The links
+ ## in the cache are treated as paths in a global develop file used when a
+ ## local one does not exist.
+
+ for (kind, path) in walkDir(options.getPkgsLinksDir):
+ if kind != pcDir:
+ continue
+ let (pkgName, _, _) = getNameVersionChecksum(path)
+ let linkFilePath = path / pkgName.getLinkFileName
+ if not linkFilePath.fileExists:
+ displayWarning(&"Not found link file in \"{path}\".")
+ continue
+ let lines = linkFilePath.readFile.split("\n")
+ if lines.len != 2:
+ displayWarning(&"Invalid link file \"{linkFilePath}\".")
+ continue
+ let pkgPath = lines[1]
+ let (pkgInfo, error) = validatePackage(pkgPath, options)
+ if error == nil:
+ let path = path.Path
+ result.addPackage(pkgInfo, path, [path].toHashSet, collidingNames)
+ else:
+ displayWarning(
+ &"Package \"{pkgName}\" at path \"{pkgPath}\" is invalid. Skipping it.")
+ displayDetails(error.msg)
+
+proc load(path: Path, dependentPkg: PackageInfo, options: Options,
+ silentIfFileNotExists, raiseOnValidationErrors, loadGlobalDeps: bool):
+ DevelopFileData =
+ ## 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.
+ ##
+ ## If `loadGlobalDeps` then load the packages pointed by the link files in the
+ ## `links` directory in the Nimble cache instead of the once pointed by the
+ ## local develop file.
+ ##
+ ## 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 cache {.global.}: DevelopFileDataCache
+ if cache.hasKey(path):
+ return cache[path]
+
+ result = initDevelopFileData()
+ result.path = path
+ result.dependentPkg = dependentPkg
+
+ var
+ errors {.global.}: ErrorsCollection
+ visitedFiles {.global.}: HashSet[Path]
+ visitedPkgs {.global.}: HashSet[Path]
+
+ visitedFiles.incl path
+ if dependentPkg.isLoaded:
+ visitedPkgs.incl dependentPkg.getNimbleFileDir
+
+ if loadGlobalDeps:
+ loadGlobalDependencies(result, errors.collidingNames, options)
+ else:
+ if silentIfFileNotExists and not path.fileExists:
+ return
+
+ try:
+ fromJson(result.jsonData, parseFile(path), Joptions(allowExtraKeys: true))
+ except ValueError as error:
+ raise nimbleError(notAValidDevFileJsonMsg($path), details = error)
+
+ 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)
+ if error == nil:
+ result.addPackage(pkgInfo, path, [path].toHashSet, errors.collidingNames)
+ else:
+ errors.invalidPackages[depPath] = error
+
+ for inclPath in result.jsonData.includes:
+ let inclPath = inclPath.normalizedPath
+ if visitedFiles.contains(inclPath):
+ continue
+ var inclDevFileData = initDevelopFileData()
+ try:
+ inclDevFileData = load(
+ inclPath, initPackageInfo(), options, false, false, false)
+ except CatchableError as error:
+ errors.invalidIncludeFiles[path] = error
+ continue
+ result.mergeIncludedDevFileData(inclDevFileData, errors)
+
+ 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.
+ for path, pkg in result.pathToPkg.dup:
+ if visitedPkgs.contains(path):
+ continue
+ var followedPkgDevFileData = initDevelopFileData()
+ try:
+ followedPkgDevFileData = load(pkg[], options, true, false, false)
+ except:
+ # The errors will be accumulated in `errors` global variable and
+ # reported by the `load` call which initiated the recursive process.
+ discard
+ result.mergeFollowedDevFileData(followedPkgDevFileData, errors)
+
+ if not errors.hasErrors:
+ cache[path] = result
+ return result
+ elif raiseOnValidationErrors:
+ raise nimbleError(failedToLoadFileMsg($path),
+ details = nimbleError(errors.getErrorsDetails))
+
+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.
+
+ 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.basicInfo.name) and not data.pathToPkg.hasKey(pkgDir):
+ let otherPath = data.nameToPkg[pkg.basicInfo.name][].getNimbleFilePath()
+ displayError(pkgAlreadyPresentAtDifferentPathMsg(
+ pkg.basicInfo.name, $otherPath, $data.path))
+ return false
+
+ # Add `pkg` to the develop file model.
+ let success = not data.jsonData.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(pkgAddedInDevFileMsg(
+ pkg.getNameAndVersion, $pkgDir, $data.path))
+ else:
+ displayWarning(pkgAlreadyInDevFileMsg(
+ pkg.getNameAndVersion, $pkgDir, $data.path))
+
+ return true
+
+proc addDevelopPackage(data: var DevelopFileData, path: Path,
+ options: Options): 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.
+
+ let (pkgInfo, error) = validatePackage(path, options)
+ if error != nil:
+ displayError(invalidPkgMsg($path))
+ displayDetails(error)
+ return false
+
+ 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
+ ## 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[].basicInfo.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.jsonData.dependencies.missingOrExcl(path)
+
+ if success:
+ let nameAndVersion = data.pathToPkg[path][].getNameAndVersion()
+ data.removePackage(path, data.path)
+ displaySuccess(pkgRemovedFromDevFileMsg(nameAndVersion, $path, $data.path))
+ else:
+ displayWarning(pkgPathNotInDevFileMsg($path, $data.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.jsonData.dependencies.missingOrExcl(path)
+
+ if success:
+ data.removePackage(path, data.path)
+ displaySuccess(pkgRemovedFromDevFileMsg(
+ pkg[].getNameAndVersion, $path, $data.path))
+ else:
+ displayWarning(pkgNameNotInDevFileMsg(name, $data.path))
+
+ return success
+
+proc includeDevelopFile(data: var DevelopFileData, path: Path,
+ options: Options): 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 = initDevelopFileData()
+ try:
+ inclFileData = load(path, initPackageInfo(), options, false, true, false)
+ except CatchableError as error:
+ displayError(failedToLoadFileMsg($path))
+ displayDetails(error)
+ return false
+
+ 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)
+ # Revert the inclusion in the case of merge errors.
+ data.jsonData.includes.excl(path)
+ for pkgPath, _ in inclFileData.pathToPkg:
+ data.removePackage(pkgPath, path)
+ return false
+
+ displaySuccess(inclInDevFileMsg($path, $data.path))
+ else:
+ displayWarning(alreadyInclInDevFileMsg($path, $data.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.jsonData.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, $data.path))
+ else:
+ displayWarning(notInclInDevFileMsg($path, $data.path))
+
+ return success
+
+proc assertDevelopActionIsSet(options: Options) =
+ ## Asserts that the currently set action in the `options` object is `develop`.
+ assert options.action.typ == actionDevelop,
+ "This procedure must be called only on develop command."
+
+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.
+
+ options.assertDevelopActionIsSet
+
+ let developFile = options.action.developFile
+
+ var
+ hasError = false
+ hasSuccessfulRemoves = false
+ data = load(developFile, dependentPkg, options, true, true, false)
+
+ defer:
+ 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 datAdd:
+ hasError = not data.addDevelopPackage(argument, options) or hasError
+ of datRemoveByPath:
+ hasSuccessfulRemoves = data.removeDevelopPackageByPath(argument) or
+ hasSuccessfulRemoves
+ of datRemoveByName:
+ hasSuccessfulRemoves = data.removeDevelopPackageByName(argument) or
+ hasSuccessfulRemoves
+ of datInclude:
+ hasError = not data.includeDevelopFile(argument, options) or hasError
+ of datExclude:
+ hasSuccessfulRemoves = data.excludeDevelopFile(argument) or
+ hasSuccessfulRemoves
+
+ return not hasError
+
+proc processDevelopDependencies*(dependentPkg: PackageInfo, options: Options):
+ seq[PackageInfo] =
+ ## Returns a sequence with the develop mode dependencies of the `dependentPkg`
+ ## and recursively all of their develop mode dependencies.
+
+ let loadGlobalDeps = not dependentPkg.getPkgDevFilePath.fileExists
+ let data = load(dependentPkg, options, true, true, loadGlobalDeps)
+ result = newSeqOfCap[PackageInfo](data.nameToPkg.len)
+ for _, pkg in data.nameToPkg:
+ result.add pkg[]
+
+proc getDevelopDependencies*(dependentPkg: PackageInfo, options: Options):
+ Table[string, ref PackageInfo] =
+ ## Returns a table with a mapping between names and `PackageInfo`s of develop
+ ## mode dependencies of package `dependentPkg` and recursively all of their
+ ## develop mode dependencies.
+
+ let loadGlobalDeps = not dependentPkg.getPkgDevFilePath.fileExists
+ let data = load(dependentPkg, options, true, true, loadGlobalDeps)
+ return data.nameToPkg
+
+type
+ ValidationErrorKind* = enum
+ ## Types of possible errors when validating the develop file against the
+ ## lock file with corresponding parts of their error messages.
+ vekDirIsNotUnderVersionControl = "is not under version control."
+ vekWorkingCopyIsNotClean = "has not clean working copy."
+ vekVcsRevisionIsNotPushed = "has not pushed VCS revisions."
+ vekWorkingCopyNeedsSync = "has not synced working copy."
+ vekWorkingCopyNeedsLock = "has not locked commits."
+ vekWorkingCopyNeedsMerge = "has local changes which are in " &
+ "conflict with the remote changes."
+
+ ValidationErrorFlags = set[ValidationErrorKind]
+ ## Set containing flags for the already met validation errors.
+
+ ValidationError* = object
+ ## Contains information for a validation error for some develop mode
+ ## package.
+ kind*: ValidationErrorKind
+ path*: Path
+
+ ValidationErrors* = Table[string, ValidationError]
+ ## Mapping between package names and their validation errors info.
+
+ NeedsOperation = enum
+ ## Helper enum for the return type of the procedure determining whether a
+ ## develop mode dependency working copy needs some operation to resolve the
+ ## conflict between it and the lock file.
+ needsNone, needsLock, needsSync, needsMerge
+
+proc assertHasValidationErrors(errors: ValidationErrors) =
+ assert errors.len > 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.metaData.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("").getOrDefault(
+ dependencyPkg.basicInfo.name, notSetLockFileDep).vcsRevision
+ syncFile = getSyncFile(dependentPkg)
+ 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
+ # `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.basicInfo.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.
+
+ let loadGlobalDeps = not dependentPkg.getPkgDevFilePath.fileExists
+ discard load(dependentPkg, options, true, true, loadGlobalDeps)
+ if dependentPkg.areLockedDepsLoaded:
+ validateDevelopFileAgainstLockFile(dependentPkg, options)
A => src/nimblepkg/displaymessages.nim +172 -0
@@ 1,172 @@
+# 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, strutils
+import version, packageinfotypes, sha1hashes
+
+const
+ validationFailedMsg* = "Validation failed."
+
+ pathGivenButNoPkgsToDownloadMsg* =
+ "Path option is given but there are no given packages for download."
+
+ developOptionsWithoutDevelopFileMsg* =
+ "Options 'add', 'remove', 'include' and 'exclude' cannot be given " &
+ "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 " &
+ "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."
+
+ multipleDevelopFileOptionsGivenMsg* =
+ "Multiple develop file options are given."
+
+ ignoringCompilationFlagsMsg* =
+ "Ignoring compilation flags for installed package."
+
+ 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."
+
+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}\"."
+
+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 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, fileName: string): string =
+ &"A package with a name \"{pkgName}\" at different path \"{otherPath}\" " &
+ "is already present in the develop file \"{fileName}\"."
+
+proc pkgAddedInDevFileMsg*(pkg, path, fileName: string): string =
+ &"The package \"{pkg}\" at path \"{path}\" is added to the develop file " &
+ &"\"{fileName}\"."
+
+proc pkgAlreadyInDevFileMsg*(pkg, path, fileName: string): string =
+ &"The package \"{pkg}\" at path \"{path}\" is already present in the " &
+ &"develop file \"{fileName}\"."
+
+proc pkgRemovedFromDevFileMsg*(pkg, path, fileName: string): string =
+ &"The package \"{pkg}\" at path \"{path}\" is removed from the develop " &
+ &"file \"{fileName}\"."
+
+proc pkgPathNotInDevFileMsg*(path, fileName: string): string =
+ &"The path \"{path}\" is not in the develop file \"{fileName}\"."
+
+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 the develop file \"{devFile}\""
+
+proc inclInDevFileMsg*(path, fileName: string): string =
+ &"The develop file \"{path}\" is successfully included into the develop " &
+ &"file \"{fileName}\""
+
+proc alreadyInclInDevFileMsg*(path, fileName: string): string =
+ &"The develop file \"{path}\" is already included in the develop file " &
+ &"\"{fileName}\"."
+
+proc exclFromDevFileMsg*(path, fileName: string): string =
+ &"The develop file \"{path}\" is successfully excluded from the develop " &
+ &"file \"{fileName}\"."
+
+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}\"."
+
+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.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.join("\n")
+ 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."
+
+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
+
+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)
+
+proc skipDownloadingInAlreadyExistingDirectoryMsg*(dir, name: string): string =
+ &"The download directory \"{dir}\" already exists.\n" &
+ &"Skipping the download of \"{name}\"."
+
+proc binaryNotDefinedInPkgMsg*(binaryName, pkgName: string): string =
+ &"Binary '{binaryName}' is not defined in '{pkgName}' package."
+
+proc notFoundPkgWithNameInPkgDepTree*(pkgName: string): string =
+ &"Not found package with name '{pkgName}' in the current package's " &
+ "dependency tree."
+
+proc pkgLinkFileSavedMsg*(path: string): string =
+ &"Package link file \"{path}\" is saved."
A => src/nimblepkg/download.nim +324 -0
@@ 1,324 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+import parseutils, os, osproc, strutils, tables, pegs, uri, json
+
+import packageinfo, packageparser, version, tools, common, options, cli
+from algorithm import SortOrder, sorted
+from sequtils import toSeq, filterIt, map
+
+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:
+ 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")
+ of DownloadMethod.hg:
+ cd downloadDir:
+ doCmd("hg checkout " & branch)
+
+proc doPull(meth: DownloadMethod, downloadDir: string) {.used.} =
+ case meth
+ of DownloadMethod.git:
+ doCheckout(meth, downloadDir, "")
+ cd downloadDir:
+ doCmd("git pull")
+ if existsFile(".gitmodules"):
+ doCmd("git submodule update")
+ of DownloadMethod.hg:
+ doCheckout(meth, downloadDir, "default")
+ cd downloadDir:
+ doCmd("hg pull")
+
+proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "",
+ onlyTip = true) =
+ 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)
+ of DownloadMethod.hg:
+ let
+ tipArg = if onlyTip: "-r tip " else: ""
+ branchArg = if branch == "": "" else: "-b " & branch & " "
+ doCmd("hg clone " & tipArg & branchArg & url & " " & downloadDir)
+
+proc getTagsList(dir: string, meth: DownloadMethod): seq[string] =
+ cd dir:
+ var output = execProcess("git tag")
+ case meth
+ of DownloadMethod.git:
+ output = execProcess("git tag")
+ of DownloadMethod.hg:
+ output = execProcess("hg tags")
+ if output.len > 0:
+ case meth
+ of DownloadMethod.git:
+ result = @[]
+ for i in output.splitLines():
+ if i == "": continue
+ result.add(i)
+ of DownloadMethod.hg:
+ result = @[]
+ for i in output.splitLines():
+ if i == "": continue
+ var tag = ""
+ discard parseUntil(i, tag, ' ')
+ if tag != "tip":
+ result.add(tag)
+ else:
+ result = @[]
+
+proc getTagsListRemote*(url: string, meth: DownloadMethod): seq[string] =
+ var
+ url = url
+ uri = parseUri url
+ if uri.query != "":
+ uri.query = ""
+ url = $uri
+ result = @[]
+ case meth
+ of DownloadMethod.git:
+ var (output, exitCode) = doCmdEx("git ls-remote --tags " & url)
+ if exitCode != QuitSuccess:
+ raise newException(OSError, "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
+ if refStart == -1: continue
+ let start = refStart+"refs/tags/".len
+ let tag = i[start .. i.len-1]
+ if not tag.endswith("^{}"): result.add(tag)
+
+ of DownloadMethod.hg:
+ # http://stackoverflow.com/questions/2039150/show-tags-for-remote-hg-repository
+ raise newException(ValueError, "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
+ ## in descending order with the most recent version first.
+ let taggedVers: seq[tuple[ver: Version, tag: string]] =
+ tags
+ .filterIt(it != "")
+ .map(proc(s: string): tuple[ver: Version, tag: string] =
+ # skip any chars before the version
+ let i = skipUntil(s, Digits)
+ # TODO: Better checking, tags can have any
+ # names. Add warnings and such.
+ result = (newVersion(s[i .. s.len-1]), s))
+ .sorted(proc(a, b: (Version, string)): int = cmp(a[0], b[0]),
+ 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 newException(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``.
+ case meth
+ of DownloadMethod.git: newVersion("#head")
+ of DownloadMethod.hg: newVersion("#tip")
+
+proc checkUrlType*(url: string): DownloadMethod =
+ ## Determines the download method based on the URL.
+ if doCmdEx("git ls-remote " & url).exitCode == QuitSuccess:
+ return DownloadMethod.git
+ elif doCmdEx("hg identify " & url).exitCode == QuitSuccess:
+ return DownloadMethod.hg
+ else:
+ raise newException(NimbleError, "Unable to identify url: " & url)
+
+proc getUrlData*(url: string): (string, Table[string, string]) =
+ var uri = parseUri(url)
+ # TODO: use uri.parseQuery once it lands... this code is quick and dirty.
+ var subdir = ""
+ if uri.query.startsWith("subdir="):
+ subdir = uri.query[7 .. ^1]
+
+ uri.query = ""
+ return ($uri, {"subdir": subdir}.toTable())
+
+proc isURL*(name: string): bool =
+ name.startsWith(peg" @'://' ")
+
+proc doDownload(url: string, downloadDir: string, verRange: VersionRange,
+ downMethod: DownloadMethod,
+ options: Options): Version =
+ ## Downloads the repository specified by ``url`` using the specified download
+ ## method.
+ ##
+ ## Returns the version of the repository which has been downloaded.
+ template getLatestByTag(meth: untyped) {.dirty.} =
+ # Find latest version that fits our ``verRange``.
+ var latest = findLatest(verRange, versions)
+ ## Note: HEAD is not used when verRange.kind is verAny. This is
+ ## intended behaviour, the latest tagged version will be used in this case.
+
+ # If no tagged versions satisfy our range latest.tag will be "".
+ # We still clone in that scenario because we want to try HEAD in that case.
+ # https://github.com/nim-lang/nimble/issues/22
+ meth
+ if $latest.ver != "":
+ result = latest.ver
+
+ removeDir(downloadDir)
+ if 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)
+ 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
+ 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)
+ 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)
+ 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)
+ let versions = getTagsList(downloadDir, downMethod).getVersionList()
+
+ if versions.len > 0:
+ getLatestByTag:
+ display("Switching", "to latest tagged version: " & latest.tag,
+ priority = MediumPriority)
+ doCheckout(downMethod, downloadDir, latest.tag)
+
+proc downloadPkg*(url: string, verRange: VersionRange,
+ downMethod: DownloadMethod,
+ subdir: string,
+ options: Options,
+ downloadPath = ""): (string, Version) =
+ ## Downloads the repository as specified by ``url`` and ``verRange`` using
+ ## the download method specified.
+ ##
+ ## If `downloadPath` isn't specified a location in /tmp/ will be used.
+ ##
+ ## Returns the directory where it was downloaded (subdir is appended) and
+ ## the concrete version which was downloaded.
+ let downloadDir =
+ if downloadPath == "":
+ (getNimbleTempDir() / getDownloadDirName(url, verRange))
+ else:
+ downloadPath
+
+ createDir(downloadDir)
+ var modUrl =
+ if url.startsWith("git://") and options.config.cloneUsingHttps:
+ "https://" & url[6 .. ^1]
+ else: url
+
+ # Fixes issue #204
+ # github + https + trailing url slash causes a
+ # checkout/ls-remote to fail with Repository not found
+ if modUrl.contains("github.com") and modUrl.endswith("/"):
+ modUrl = modUrl[0 .. ^2]
+
+ if subdir.len > 0:
+ display("Downloading", "$1 using $2 (subdir is '$3')" %
+ [modUrl, $downMethod, subdir],
+ priority = HighPriority)
+ else:
+ display("Downloading", "$1 using $2" % [modUrl, $downMethod],
+ priority = HighPriority)
+ result = (
+ downloadDir / subdir,
+ doDownload(modUrl, downloadDir, verRange, downMethod, options)
+ )
+
+ if verRange.kind != verSpecial:
+ ## 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:
+ raise newException(NimbleError,
+ "Downloaded package's version does not satisfy requested version " &
+ "range: wanted $1 got $2." %
+ [$verRange, $pkginfo.version])
+
+proc echoPackageVersions*(pkg: Package) =
+ let downMethod = pkg.downloadMethod.getDownloadMethod()
+ case downMethod
+ of DownloadMethod.git:
+ try:
+ let versions = getTagsListRemote(pkg.url, downMethod).getVersionList()
+ if versions.len > 0:
+ let sortedVersions = toSeq(values(versions))
+ echo(" versions: " & join(sortedVersions, ", "))
+ else:
+ echo(" versions: (No versions tagged in the remote repository)")
+ except OSError:
+ echo(getCurrentExceptionMsg())
+ of DownloadMethod.hg:
+ echo(" versions: (Remote tag retrieval not supported by " &
+ pkg.downloadMethod & ")")
+
+proc packageVersionsJson*(pkg: Package): JsonNode =
+ result = newJArray()
+ let downMethod = pkg.downloadMethod.getDownloadMethod()
+ try:
+ case downMethod
+ of DownloadMethod.git:
+ let versions = getTagsListRemote(pkg.url, downMethod).getVersionList()
+ for v in values(versions):
+ result.add %v
+ of DownloadMethod.hg:
+ discard
+ except:
+ result = %* { "exception": getCurrentExceptionMsg() }
+
+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)
+
+ echo("Everything works!")
A => src/nimblepkg/init.nim +184 -0
@@ 1,184 @@
+import os, strutils
+
+import ./cli, ./tools
+
+type
+ PkgInitInfo* = tuple
+ pkgName: string
+ pkgVersion: string
+ pkgAuthor: string
+ pkgDesc: string
+ pkgLicense: string
+ pkgBackend: string
+ pkgSrcDir: string
+ pkgNimDep: string
+ pkgType: string
+
+proc writeExampleIfNonExistent(file: string, content: string) =
+ if not existsFile(file):
+ writeFile(file, content)
+ else:
+ display("Info:", "File " & file & " already exists, did not write " &
+ "example code", priority = HighPriority)
+
+proc createPkgStructure*(info: PkgInitInfo, pkgRoot: string) =
+ # Create source directory
+ createDirD(pkgRoot / info.pkgSrcDir)
+
+ # Initialise the source code directories and create some example code.
+ var nimbleFileOptions = ""
+ case info.pkgType
+ of "binary":
+ let mainFile = pkgRoot / info.pkgSrcDir / info.pkgName.changeFileExt("nim")
+ writeExampleIfNonExistent(mainFile,
+"""
+# This is just an example to get you started. A typical binary package
+# uses this file as the main entry point of the application.
+
+when isMainModule:
+ echo("Hello, World!")
+"""
+ )
+ nimbleFileOptions.add("bin = @[\"$1\"]\n" % info.pkgName)
+ of "library":
+ let mainFile = pkgRoot / info.pkgSrcDir / info.pkgName.changeFileExt("nim")
+ writeExampleIfNonExistent(mainFile,
+"""
+# This is just an example to get you started. A typical library package
+# exports the main API in this file. Note that you cannot rename this file
+# but you can remove it if you wish.
+
+proc add*(x, y: int): int =
+ ## Adds two files together.
+ return x + y
+"""
+ )
+
+ createDirD(pkgRoot / info.pkgSrcDir / info.pkgName)
+ let submodule = pkgRoot / info.pkgSrcDir / info.pkgName /
+ "submodule".addFileExt("nim")
+ writeExampleIfNonExistent(submodule,
+"""
+# This is just an example to get you started. Users of your library will
+# import this file by writing ``import $1/submodule``. Feel free to rename or
+# remove this file altogether. You may create additional modules alongside
+# this file as required.
+
+type
+ Submodule* = object
+ name*: string
+
+proc initSubmodule*(): Submodule =
+ ## Initialises a new ``Submodule`` object.
+ Submodule(name: "Anonymous")
+""" % info.pkgName
+ )
+ of "hybrid":
+ let mainFile = pkgRoot / info.pkgSrcDir / info.pkgName.changeFileExt("nim")
+ writeExampleIfNonExistent(mainFile,
+"""
+# This is just an example to get you started. A typical hybrid package
+# uses this file as the main entry point of the application.
+
+import $1pkg/submodule
+
+when isMainModule:
+ echo(getWelcomeMessage())
+""" % info.pkgName
+ )
+
+ let pkgSubDir = pkgRoot / info.pkgSrcDir / info.pkgName & "pkg"
+ createDirD(pkgSubDir)
+ let submodule = pkgSubDir / "submodule".addFileExt("nim")
+ writeExampleIfNonExistent(submodule,
+"""
+# This is just an example to get you started. Users of your hybrid library will
+# import this file by writing ``import $1pkg/submodule``. Feel free to rename or
+# remove this file altogether. You may create additional modules alongside
+# this file as required.
+
+proc getWelcomeMessage*(): string = "Hello, World!"
+""" % info.pkgName
+ )
+ nimbleFileOptions.add("installExt = @[\"nim\"]\n")
+ nimbleFileOptions.add("bin = @[\"$1\"]\n" % info.pkgName)
+ else:
+ assert false, "Invalid package type specified."
+
+ let pkgTestDir = "tests"
+ # Create test directory
+ case info.pkgType
+ of "binary":
+ discard
+ of "hybrid", "library":
+ let pkgTestPath = pkgRoot / pkgTestDir
+ createDirD(pkgTestPath)
+
+ writeFile(pkgTestPath / "config".addFileExt("nims"),
+ "switch(\"path\", \"$$projectDir/../$#\")" % info.pkgSrcDir
+ )
+
+ if info.pkgType == "library":
+ writeExampleIfNonExistent(pkgTestPath / "test1".addFileExt("nim"),
+"""
+# This is just an example to get you started. You may wish to put all of your
+# tests into a single file, or separate them into multiple `test1`, `test2`
+# etc. files (better names are recommended, just make sure the name starts with
+# the letter 't').
+#
+# To run these tests, simply execute `nimble test`.
+
+import unittest
+
+import $1
+test "can add":
+ check add(5, 5) == 10
+""" % info.pkgName
+ )
+ else:
+ writeExampleIfNonExistent(pkgTestPath / "test1".addFileExt("nim"),
+"""
+# This is just an example to get you started. You may wish to put all of your
+# tests into a single file, or separate them into multiple `test1`, `test2`
+# etc. files (better names are recommended, just make sure the name starts with
+# the letter 't').
+#
+# To run these tests, simply execute `nimble test`.
+
+import unittest
+
+import $1pkg/submodule
+test "correct welcome":
+ check getWelcomeMessage() == "Hello, World!"
+""" % info.pkgName
+ )
+ else:
+ assert false, "Invalid package type specified."
+
+ # Write the nimble file
+ let nimbleFile = pkgRoot / info.pkgName.changeFileExt("nimble")
+ # Only write backend if it isn't "c"
+ var pkgBackend = ""
+ if (info.pkgBackend != "c"):
+ pkgBackend = "backend = " & info.pkgbackend.escape()
+ writeFile(nimbleFile, """# Package
+
+version = $#
+author = "$#"
+description = "$#"
+license = $#
+srcDir = $#
+$#
+$#
+
+# Dependencies
+
+requires "nim >= $#"
+""" % [
+ info.pkgVersion.escape(), info.pkgAuthor.replace("\"", "\\\""), info.pkgDesc.replace("\"", "\\\""),
+ info.pkgLicense.escape(), info.pkgSrcDir.escape(), nimbleFileOptions,
+ pkgBackend, info.pkgNimDep
+ ]
+ )
+
+ display("Info:", "Nimble file created successfully", priority=MediumPriority)
A => src/nimblepkg/jsonhelpers.nim +140 -0
@@ 1,140 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+import json
+
+proc newJObjectIfKeyNotExists(obj: JsonNode, key: string): JsonNode =
+ assert obj.kind == JObject
+ if not obj.hasKey(key):
+ let newObj = newJObject()
+ obj.add(key, newObj)
+ return newObj
+ else:
+ return obj[key]
+
+proc addIfNotExist*(obj: JsonNode, keys: varargs[string],
+ val: JsonNode): JsonNode =
+ # If the path in the `obj` json tree described by `keys` does not exist create
+ # it, add the node `val` to it and return the added node, otherwise return the
+ # value of the existing object at the end of the path.
+
+ assert obj.kind == JObject
+ var obj = obj
+ for i in 0 ..< keys.len() - 1:
+ obj = obj.newJObjectIfKeyNotExists(keys[i])
+ if not obj.hasKey(keys[^1]):
+ obj.add(keys[^1], val)
+ return val
+ else:
+ return obj[keys[^1]]
+
+proc cleanUpEmptyObjects*(obj: JsonNode): JsonNode =
+ if obj.kind == JObject:
+ result = newJObject()
+ for key, value in obj:
+ var newValue = cleanUpEmptyObjects(value)
+ if newValue.kind notin {JObject, JArray} or newValue.len != 0:
+ result.add(key, newValue)
+ elif obj.kind == JArray:
+ result = newJArray()
+ for value in obj:
+ var newValue = cleanUpEmptyObjects(value)
+ if newValue.kind notin {JObject, JArray} or newValue.len != 0:
+ result.add(newValue)
+ else:
+ result = obj
+
+when isMainModule:
+ import unittest
+
+ test "bewJObjectIfKeyNotExists":
+ proc testProc(testedJson, key, expectedResult: string) =
+ let testedJson = parseJson(testedJson)
+ let expectedResult = parseJson(expectedResult)
+ let actualResult = newJObjectIfKeyNotExists(testedJson, key)
+ check actualResult == expectedResult
+
+ 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] }")
+
+ 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)
+ let expectedEndObject = parseJson(expectedEndObject)
+ let addedOrOldNode =actualResult.addIfNotExist(keys, jsonToAdd)
+ check actualResult == expectedResult
+ check addedOrOldNode == expectedEndObject
+
+ 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
+
+ testProc("{}", "{}")
+ testProc("[]", "[]")
+ testProc("{ \"key\": \"value\" }", "{ \"key\": \"value\" }")
+ testProc("[ 3, 1415 ]", "[ 3, 1415 ]")
+
+ testProc("{ \"key\": [ \"value1\", \"value2\" ] }",
+ "{ \"key\": [ \"value1\", \"value2\" ] }")
+
+ testProc("{ \"key\": {} }", "{}")
+ testProc("[ [], [] ]", "[]")
+ testProc("[ { \"key1\": [ { \"key1.1\": [] } ] }, { \"key2\": [] } ]", "[]")
+
+ testProc(""" {
+ "key1": {
+ "key1.1": "value1.1",
+ "key1.2": "value1.2"
+ },
+ "key2": {},
+ "key3": [
+ {
+ "key3.1": "value3.1",
+ "key3.2": "value3.2"
+ },
+ {},
+ {
+ "key3.3": "value3.3"
+ },
+ {}
+ ],
+ "key4": {
+ "key4.1": {},
+ "key4.2": []
+ },
+ "key5": 5
+ }""", """ {
+ "key1": {
+ "key1.1": "value1.1",
+ "key1.2": "value1.2"
+ },
+ "key3": [
+ {
+ "key3.1": "value3.1",
+ "key3.2": "value3.2"
+ },
+ {
+ "key3.3": "value3.3"
+ },
+ ],
+ "key5": 5
+ }""")
A => src/nimblepkg/lockfile.nim +57 -0
@@ 1,57 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+import tables, os, json
+import version, sha1hashes, packageinfotypes
+
+type
+ LockFileJsonKeys* = enum
+ lfjkVersion = "version"
+ lfjkPackages = "packages"
+ lfjkPkgVcsRevision = "vcsRevision"
+ lfjkTasks = "tasks"
+
+const
+ lockFileVersion = 2
+
+proc initLockFileDep*: LockFileDep =
+ result = LockFileDep(
+ version: notSetVersion,
+ vcsRevision: notSetSha1Hash,
+ checksums: Checksums(sha1: notSetSha1Hash))
+
+const
+ notSetLockFileDep* = initLockFileDep()
+
+proc writeLockFile*(fileName: string, packages: AllLockFileDeps) =
+ ## Saves lock file on the disk in topologically sorted order of the
+ ## dependencies.
+
+ let mainJsonNode = %{
+ $lfjkVersion: %lockFileVersion,
+ $lfjkPackages: %packages[noTask]
+ }
+ # Store task graph seperate
+ mainJsonNode[$lfjkTasks] = newJObject()
+ for task, deps in packages:
+ if task != noTask:
+ mainJsonNode[$lfjkTasks][task] = %deps
+
+ var s = mainJsonNode.pretty
+ s.add '\n'
+ writeFile(fileName, s)
+
+proc readLockFile*(filePath: string): AllLockFileDeps =
+ {.warning[UnsafeDefault]: off.}
+ {.warning[ProveInit]: off.}
+ let data = parseFile(filePath)
+ result[noTask] = data[$lfjkPackages].to(LockFileDeps)
+ if $lfjkTasks in data:
+ for task, deps in data[$lfjkTasks]:
+ result[task] = deps.to(LockFileDeps)
+ {.warning[ProveInit]: on.}
+ {.warning[UnsafeDefault]: on.}
+
+proc getLockedDependencies*(lockFile: string): AllLockFileDeps =
+ if lockFile.fileExists:
+ result = lockFile.readLockFile
A => src/nimblepkg/nim.cfg +2 -0
@@ 1,2 @@
+--path:"$nim/"
+--path:"$lib/packages/docutils"<
\ No newline at end of file
A => src/nimblepkg/nimbledatafile.nim +66 -0
@@ 1,66 @@
+# 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* = "nimbledata2.json"
+ nimbleDataFileVersion = 1
+
+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 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 = parseFile(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
A => src/nimblepkg/nimscriptapi.nim +204 -0
@@ 1,204 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+## This module is implicitly imported in NimScript .nimble files.
+
+import system except getCommand, setCommand, switch, `--`
+import strformat, strutils, tables
+
+when not defined(nimscript):
+ import os
+
+var
+ packageName* = "" ## Set this to the package name. It
+ ## is usually not required to do that, nims' filename is
+ ## the default.
+ version*: string ## The package's version.
+ author*: string ## The package's author.
+ description*: string ## The package's description.
+ license*: string ## The package's license.
+ srcdir*: string ## The package's source directory.
+ binDir*: string ## The package's binary directory.
+ backend*: string ## The package's backend.
+
+ skipDirs*, skipFiles*, skipExt*, installDirs*, installFiles*,
+ installExt*, bin*: seq[string] = @[] ## Nimble metadata.
+ requiresData*: seq[string] = @[] ## The package's dependencies.
+
+ foreignDeps*: seq[string] = @[] ## The foreign dependencies. Only
+ ## exported for 'distros.nim'.
+
+ beforeHooks: seq[string] = @[]
+ afterHooks: seq[string] = @[]
+ commandLineParams: seq[string] = @[]
+ flags: TableRef[string, seq[string]]
+
+ command = "e"
+ project = ""
+ success = false
+ retVal = true
+ projectFile = ""
+ outFile = ""
+
+proc requires*(deps: varargs[string]) =
+ ## Call this to set the list of requirements of your Nimble
+ ## package.
+ for d in deps: requiresData.add(d)
+
+proc foreignDep*(deps: varargs[string]) =
+ ## Call this to set the list of external dependencies of your Nimble
+ ## package.
+ for d in deps: foreignDeps.add(d)
+
+proc getParams() =
+ # Called by nimscriptwrapper.nim:execNimscript()
+ # nim e --flags /full/path/to/file.nims /full/path/to/file.out action
+ for i in 2 .. paramCount():
+ let
+ param = paramStr(i)
+ if param[0] != '-':
+ if projectFile.len == 0:
+ projectFile = param
+ elif outFile.len == 0:
+ outFile = param
+ else:
+ commandLineParams.add param.normalize
+
+proc getCommand*(): string =
+ return command
+
+proc setCommand*(cmd: string, prj = "") =
+ command = cmd
+ if prj.len != 0:
+ project = prj
+
+proc switch*(key: string, value="") =
+ if flags.isNil:
+ flags = newTable[string, seq[string]]()
+
+ if flags.hasKey(key):
+ flags[key].add(value)
+ else:
+ flags[key] = @[value]
+
+template `--`*(key, val: untyped) =
+ switch(astToStr(key), strip astToStr(val))
+
+template `--`*(key: untyped) =
+ switch(astToStr(key), "")
+
+template printIfLen(varName) =
+ if varName.len != 0:
+ result &= astToStr(varName) & ": \"\"\"" & varName & "\"\"\"\n"
+
+template printSeqIfLen(varName) =
+ if varName.len != 0:
+ result &= astToStr(varName) & ": \"" & varName.join(", ") & "\"\n"
+
+proc printPkgInfo(): string =
+ if backend.len == 0:
+ backend = "c"
+
+ result = "[Package]\n"
+ if packageName.len != 0:
+ result &= "name: \"" & packageName & "\"\n"
+ printIfLen version
+ printIfLen author
+ printIfLen description
+ printIfLen license
+ printIfLen srcdir
+ printIfLen binDir
+ printIfLen backend
+
+ printSeqIfLen skipDirs
+ printSeqIfLen skipFiles
+ printSeqIfLen skipExt
+ printSeqIfLen installDirs
+ printSeqIfLen installFiles
+ printSeqIfLen installExt
+ printSeqIfLen bin
+ printSeqIfLen beforeHooks
+ printSeqIfLen afterHooks
+
+ if requiresData.len != 0 or foreignDeps.len != 0:
+ result &= "\n[Deps]\n"
+ if requiresData.len != 0:
+ result &= &"requires: \"{requiresData.join(\", \")}\"\n"
+ if foreignDeps.len != 0:
+ result &= &"foreignDeps: \"{foreignDeps.join(\", \")}\"\n"
+
+proc onExit*() =
+ if "printPkgInfo".normalize in commandLineParams:
+ if outFile.len != 0:
+ writeFile(outFile, printPkgInfo())
+ else:
+ var
+ output = ""
+ output &= "\"success\": " & $success & ", "
+ output &= "\"command\": \"" & command & "\", "
+ if project.len != 0:
+ output &= "\"project\": \"" & project & "\", "
+ if not flags.isNil and flags.len != 0:
+ output &= "\"flags\": {"
+ for key, val in flags.pairs:
+ output &= "\"" & key & "\": ["
+ for v in val:
+ let v = if v.len > 0 and v[0] == '"': strutils.unescape(v)
+ else: v
+ output &= v.escape & ", "
+ output = output[0 .. ^3] & "], "
+ output = output[0 .. ^3] & "}, "
+
+ output &= "\"retVal\": " & $retVal
+
+ if outFile.len != 0:
+ writeFile(outFile, "{" & output & "}")
+
+# TODO: New release of Nim will move this `task` template under a
+# `when not defined(nimble)`. This will allow us to override it in the future.
+template task*(name: untyped; description: string; body: untyped): untyped =
+ ## Defines a task. Hidden tasks are supported via an empty description.
+ ## Example:
+ ##
+ ## .. code-block:: nim
+ ## task build, "default build is via the C backend":
+ ## setCommand "c"
+ proc `name Task`*() = body
+
+ if commandLineParams.len == 0 or "help" in commandLineParams:
+ success = true
+ echo(astToStr(name), " ", description)
+ elif astToStr(name).normalize in commandLineParams:
+ success = true
+ `name Task`()
+
+template before*(action: untyped, body: untyped): untyped =
+ ## Defines a block of code which is evaluated before ``action`` is executed.
+ proc `action Before`*(): bool =
+ result = true
+ body
+
+ beforeHooks.add astToStr(action)
+
+ if (astToStr(action) & "Before").normalize in commandLineParams:
+ success = true
+ retVal = `action Before`()
+
+template after*(action: untyped, body: untyped): untyped =
+ ## Defines a block of code which is evaluated after ``action`` is executed.
+ proc `action After`*(): bool =
+ result = true
+ body
+
+ afterHooks.add astToStr(action)
+
+ if (astToStr(action) & "After").normalize in commandLineParams:
+ success = true
+ retVal = `action After`()
+
+proc getPkgDir*(): string =
+ ## Returns the package directory containing the .nimble file currently
+ ## being evaluated.
+ result = projectFile.rsplit(seps={'/', '\\', ':'}, maxsplit=1)[0]
+
+getParams()
A => src/nimblepkg/nimscriptexecutor.nim +76 -0
@@ 1,76 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+import os, strutils, sets
+
+import packageparser, common, packageinfo, options, nimscriptwrapper, cli,
+ version
+
+proc execHook*(options: Options, hookAction: ActionType, before: bool): bool =
+ ## Returns whether to continue.
+ result = true
+
+ # For certain commands hooks should not be evaluated.
+ if hookAction in noHookActions:
+ return
+
+ var nimbleFile = ""
+ try:
+ nimbleFile = findNimbleFile(getCurrentDir(), true)
+ except NimbleError: return true
+ # PackageInfos are cached so we can read them as many times as we want.
+ let pkgInfo = getPkgInfoFromFile(nimbleFile, options)
+ let actionName =
+ if hookAction == actionCustom: options.action.command
+ else: ($hookAction)[6 .. ^1]
+ let hookExists =
+ if before: actionName.normalize in pkgInfo.preHooks
+ else: actionName.normalize in pkgInfo.postHooks
+ if pkgInfo.isNimScript and hookExists:
+ let res = execHook(nimbleFile, actionName, before, options)
+ if res.success:
+ result = res.retVal
+
+proc execCustom*(options: Options,
+ execResult: var ExecutionResult[bool],
+ failFast = true): bool =
+ ## Executes the custom command using the nimscript backend.
+ ##
+ ## If failFast is true then exceptions will be raised when something is wrong.
+ ## Otherwise this function will just return false.
+
+ # Custom command. Attempt to call a NimScript task.
+ let nimbleFile = findNimbleFile(getCurrentDir(), true)
+ if not nimbleFile.isNimScript(options) and failFast:
+ writeHelp()
+
+ execResult = execTask(nimbleFile, options.action.command, options)
+ if not execResult.success:
+ if not failFast:
+ return
+ 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.")
+
+ if execResult.command.normalize == "nop":
+ display("Warning:", "Using `setCommand 'nop'` is not necessary.", Warning,
+ HighPriority)
+ return
+
+ if not execHook(options, actionCustom, false):
+ return
+
+ return true
+
+proc getOptionsForCommand*(execResult: ExecutionResult,
+ options: Options): Options =
+ ## Creates an Options object for the requested command.
+ var newOptions = options.briefClone()
+ parseCommand(execResult.command, newOptions)
+ for arg in execResult.arguments:
+ parseArgument(arg, newOptions)
+ for flag, vals in execResult.flags:
+ for val in vals:
+ parseFlag(flag, val, newOptions)
+ return newOptions
A => src/nimblepkg/nimscriptwrapper.nim +216 -0
@@ 1,216 @@
+# Copyright (C) Andreas Rumpf. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+## Implements the new configuration system for Nimble. Uses Nim as a
+## scripting language.
+
+import hashes, json, os, strutils, tables, times, osproc, strtabs
+
+import version, options, cli, tools
+
+type
+ Flags = TableRef[string, seq[string]]
+ ExecutionResult*[T] = object
+ success*: bool
+ command*: string
+ arguments*: seq[string]
+ flags*: Flags
+ retVal*: T
+ stdout*: string
+
+const
+ internalCmd = "e"
+ nimscriptApi = staticRead("nimscriptapi.nim")
+ printPkgInfo = "printPkgInfo"
+
+proc isCustomTask(actionName: string, options: Options): bool =
+ options.action.typ == actionCustom and actionName != printPkgInfo
+
+proc needsLiveOutput(actionName: string, options: Options, isHook: bool): bool =
+ let isCustomTask = isCustomTask(actionName, options)
+ return isCustomTask or isHook or actionName == ""
+
+proc writeExecutionOutput(data: string) =
+ # TODO: in the future we will likely want this to be live, users will
+ # undoubtedly be doing loops and other crazy things in their top-level
+ # Nimble files.
+ display("Info", data)
+
+proc execNimscript(
+ nimsFile, projectDir, actionName: string, options: Options, isHook: bool
+): tuple[output: string, exitCode: int, stdout: string] =
+ let
+ nimsFileCopied = getTempDir() / nimsFile.splitFile().name & "_" & getProcessId() & ".nims"
+ outFile = getNimbleTempDir() & ".out"
+
+ let
+ isScriptResultCopied =
+ nimsFileCopied.fileExists() and
+ nimsFileCopied.getLastModificationTime() >= nimsFile.getLastModificationTime()
+
+ if not isScriptResultCopied:
+ nimsFile.copyFile(nimsFileCopied)
+
+ defer:
+ # Only if copied in this invocation, allows recursive calls of nimble
+ if not isScriptResultCopied and options.shouldRemoveTmp(nimsFileCopied):
+ nimsFileCopied.removeFile()
+
+ var cmd = (
+ "nim e $# -p:$# $# $# $#" % [
+ "--hints:off --verbosity:0",
+ (getTempDir() / "nimblecache").quoteShell,
+ nimsFileCopied.quoteShell,
+ outFile.quoteShell,
+ actionName
+ ]
+ ).strip()
+
+ let isCustomTask = isCustomTask(actionName, options)
+ if isCustomTask:
+ for i in options.action.arguments:
+ cmd &= " " & i.quoteShell()
+ for key, val in options.action.flags.pairs():
+ cmd &= " $#$#" % [if key.len == 1: "-" else: "--", key]
+ if val.len != 0:
+ cmd &= ":" & val.quoteShell()
+
+ displayDebug("Executing " & cmd)
+
+ if needsLiveOutput(actionName, options, isHook):
+ result.exitCode = execCmd(cmd)
+ else:
+ # We want to capture any possible errors when parsing a .nimble
+ # file's metadata. See #710.
+ (result.stdout, result.exitCode) = execCmdEx(cmd)
+ if outFile.fileExists():
+ result.output = outFile.readFile()
+ if options.shouldRemoveTmp(outFile):
+ discard outFile.tryRemoveFile()
+
+proc getNimsFile(scriptName: string, options: Options): string =
+ let
+ cacheDir = getTempDir() / "nimblecache"
+ shash = $scriptName.parentDir().hash().abs()
+ prjCacheDir = cacheDir / scriptName.splitFile().name & "_" & shash
+ nimscriptApiFile = cacheDir / "nimscriptapi.nim"
+
+ result = prjCacheDir / scriptName.extractFilename().changeFileExt ".nims"
+
+ let
+ iniFile = result.changeFileExt(".ini")
+
+ isNimscriptApiCached =
+ nimscriptApiFile.fileExists() and nimscriptApiFile.getLastModificationTime() >
+ getAppFilename().getLastModificationTime()
+
+ isScriptResultCached =
+ isNimscriptApiCached and result.fileExists() and result.getLastModificationTime() >
+ scriptName.getLastModificationTime()
+
+ if not isNimscriptApiCached:
+ createDir(cacheDir)
+ writeFile(nimscriptApiFile, nimscriptApi)
+
+ if not isScriptResultCached:
+ createDir(result.parentDir())
+ writeFile(result, """
+import system except getCommand, setCommand, switch, `--`,
+ packageName, version, author, description, license, srcDir, binDir, backend,
+ skipDirs, skipFiles, skipExt, installDirs, installFiles, installExt, bin, foreignDeps,
+ requires, task, packageName
+""" &
+ "import nimscriptapi, strutils\n" & scriptName.readFile() & "\nonExit()\n")
+ discard tryRemoveFile(iniFile)
+
+proc getIniFile*(scriptName: string, options: Options): string =
+ let
+ nimsFile = getNimsFile(scriptName, options)
+
+ result = nimsFile.changeFileExt(".ini")
+
+ let
+ isIniResultCached =
+ result.fileExists() and result.getLastModificationTime() >
+ scriptName.getLastModificationTime()
+
+ if not isIniResultCached:
+ let (output, exitCode, stdout) = execNimscript(
+ nimsFile, scriptName.parentDir(), printPkgInfo, options, isHook=false
+ )
+
+ if exitCode == 0 and output.len != 0:
+ result.writeFile(output)
+ stdout.writeExecutionOutput()
+ else:
+ raise newException(NimbleError, stdout & "\nprintPkgInfo() failed")
+
+proc execScript(
+ scriptName, actionName: string, options: Options, isHook: bool
+): ExecutionResult[bool] =
+ let nimsFile = getNimsFile(scriptName, options)
+
+ let (output, exitCode, stdout) =
+ execNimscript(
+ nimsFile, scriptName.parentDir(), actionName, options, isHook
+ )
+
+ if exitCode != 0:
+ let errMsg =
+ if stdout.len != 0:
+ stdout
+ else:
+ "Exception raised during nimble script execution"
+ raise newException(NimbleError, errMsg)
+
+ let
+ j =
+ if output.len != 0:
+ parseJson(output)
+ else:
+ parseJson("{}")
+
+ result.flags = newTable[string, seq[string]]()
+ result.success = j{"success"}.getBool()
+ result.command = j{"command"}.getStr()
+ if "project" in j:
+ result.arguments.add j["project"].getStr()
+ if "flags" in j:
+ for flag, vals in j["flags"].pairs:
+ result.flags[flag] = @[]
+ for val in vals.items():
+ result.flags[flag].add val.getStr()
+ result.retVal = j{"retVal"}.getBool()
+
+ stdout.writeExecutionOutput()
+
+proc execTask*(scriptName, taskName: string,
+ options: Options): ExecutionResult[bool] =
+ ## Executes the specified task in the specified script.
+ ##
+ ## `scriptName` should be a filename pointing to the nimscript file.
+ display("Executing", "task $# in $#" % [taskName, scriptName],
+ priority = HighPriority)
+
+ result = execScript(scriptName, taskName, options, isHook=false)
+
+proc execHook*(scriptName, actionName: string, before: bool,
+ options: Options): ExecutionResult[bool] =
+ ## Executes the specified action's hook. Depending on ``before``, either
+ ## the "before" or the "after" hook.
+ ##
+ ## `scriptName` should be a filename pointing to the nimscript file.
+ let hookName =
+ if before: actionName.toLowerAscii & "Before"
+ else: actionName.toLowerAscii & "After"
+ display("Attempting", "to execute hook $# in $#" % [hookName, scriptName],
+ priority = MediumPriority)
+
+ result = execScript(scriptName, hookName, options, isHook=true)
+
+proc hasTaskRequestedCommand*(execResult: ExecutionResult): bool =
+ ## Determines whether the last executed task used ``setCommand``
+ return execResult.command != internalCmd
+
+proc listTasks*(scriptName: string, options: Options) =
+ discard execScript(scriptName, "", options, isHook=false)
A => src/nimblepkg/options.nim +534 -0
@@ 1,534 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+import json, strutils, os, parseopt, strtabs, uri, tables, terminal
+import sequtils, sugar
+import std/options as std_opt
+from httpclient import Proxy, newProxy
+
+import config, version, common, cli
+
+type
+ Options* = object
+ forcePrompts*: ForcePrompt
+ depsOnly*: bool
+ uninstallRevDeps*: bool
+ queryVersions*: bool
+ queryInstalled*: bool
+ jsonOutput*:bool
+ nimbleDir*: string
+ verbosity*: cli.Priority
+ action*: Action
+ config*: Config
+ nimbleData*: JsonNode ## Nimbledata.json
+ pkgInfoCache*: TableRef[string, PackageInfo]
+ showHelp*: bool
+ showVersion*: bool
+ noColor*: bool
+ disableValidation*: bool
+ continueTestsOnFailure*: bool
+ ## Whether packages' repos should always be downloaded with their history.
+ forceFullClone*: bool
+ # Temporary storage of flags that have not been captured by any specific Action.
+ unknownFlags*: seq[(CmdLineKind, string, string)]
+
+ ActionType* = enum
+ actionNil, actionRefresh, actionInit, actionDump, actionPublish,
+ actionInstall, actionSearch,
+ actionList, actionBuild, actionPath, actionUninstall, actionCompile,
+ actionDoc, actionCustom, actionTasks, actionDevelop, actionCheck,
+ actionRun
+
+ Action* = object
+ case typ*: ActionType
+ of actionNil, actionList, actionPublish, actionTasks, actionCheck: nil
+ of actionRefresh:
+ optionalURL*: string # Overrides default package list.
+ of actionInstall, actionPath, actionUninstall, actionDevelop:
+ packages*: seq[PkgTuple] # Optional only for actionInstall
+ # and actionDevelop.
+ passNimFlags*: seq[string]
+ of actionSearch:
+ search*: seq[string] # Search string.
+ of actionInit, actionDump:
+ projName*: string
+ of actionCompile, actionDoc, actionBuild:
+ file*: string
+ backend*: string
+ compileOptions: seq[string]
+ of actionRun:
+ runFile: string
+ compileFlags: seq[string]
+ runFlags*: seq[string]
+ of actionCustom:
+ command*: string
+ arguments*: seq[string]
+ flags*: StringTableRef
+
+const
+ help* = """
+Usage: nimble COMMAND [opts]
+
+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.
+ Symlinks the cloned packages or any package
+ in the current working directory.
+ check Verifies the validity of a package in the
+ current working directory.
+ init [pkgname] Initializes a new Nimble project in the
+ current directory or if a name is provided a
+ new directory of the same name.
+ publish Publishes a package on nim-lang/packages.
+ The current working directory needs to be the
+ toplevel directory of the Nimble package.
+ uninstall [pkgname, ...] Uninstalls a list of packages.
+ [-i, --inclDeps] Uninstall package and dependent package(s).
+ build [opts, ...] [bin] Builds a package.
+ run [opts, ...] bin Builds and runs a package.
+ A binary name needs
+ to be specified after any compilation options,
+ any flags after the binary name are passed to
+ the binary when it is run.
+ c, cc, js [opts, ...] f.nim Builds a file inside a package. Passes options
+ to the Nim compiler.
+ test Compiles and executes tests
+ [-c, --continue] Don't stop execution on a failed test.
+ doc, doc2 [opts, ...] f.nim Builds documentation for a file inside a
+ package. Passes options to the Nim compiler.
+ refresh [url] Refreshes the package list. A package list URL
+ can be optionally specified.
+ search pkg/tag Searches for a specified package. Search is
+ performed by tag and by name.
+ [--json] Format output as JSON.
+ [--ver] Query remote server for package version.
+ list Lists all packages.
+ [--json] Format output as JSON.
+ [--ver] Query remote server for package version.
+ [-i, --installed] Lists all installed packages.
+ tasks Lists the tasks specified in the Nimble
+ package's Nimble file.
+ path pkgname ... Shows absolute path to the installed packages
+ specified.
+ dump [pkgname] Outputs Nimble package information for
+ external tools. The argument can be a
+ .nimble file, a project directory or
+ the name of an installed package.
+
+
+Options:
+ -h, --help Print this help message.
+ -v, --version Print version information.
+ -y, --accept Accept all interactive prompts.
+ -n, --reject Reject all interactive prompts.
+ --json Produce JSON formatted output when
+ searching or listing packages
+ --ver Query remote server for package version
+ information when searching or listing packages
+ --nimbleDir:dirname Set the Nimble directory.
+ --verbose Show all non-debug output.
+ --debug Show all output including debug messages.
+ --noColor Don't colorise output.
+
+For more information read the Github readme:
+ https://github.com/nim-lang/nimble#readme
+"""
+
+const noHookActions* = {actionCheck}
+
+proc writeHelp*(quit=true) =
+ echo(help)
+ if quit:
+ raise NimbleQuit(msg: "")
+
+proc writeVersion*() =
+ echo("nimble v$# compiled at $# $#" %
+ [nimbleVersion, CompileDate, CompileTime])
+ const execResult = gorgeEx("git rev-parse HEAD")
+ when execResult[0].len > 0 and execResult[1] == QuitSuccess:
+ echo "git hash: ", execResult[0]
+ else:
+ {.warning: "Couldn't determine GIT hash: " & execResult[0].}
+ echo "git hash: couldn't determine git hash"
+ raise NimbleQuit(msg: "")
+
+proc parseActionType*(action: string): ActionType =
+ case action.normalize()
+ of "install":
+ result = actionInstall
+ of "path":
+ result = actionPath
+ of "build":
+ result = actionBuild
+ of "run":
+ result = actionRun
+ of "c", "compile", "js", "cpp", "cc":
+ result = actionCompile
+ of "doc", "doc2":
+ result = actionDoc
+ of "init":
+ result = actionInit
+ of "dump":
+ result = actionDump
+ of "update", "refresh":
+ result = actionRefresh
+ of "search":
+ result = actionSearch
+ of "list":
+ result = actionList
+ of "uninstall", "remove", "delete", "del", "rm":
+ result = actionUninstall
+ of "publish":
+ result = actionPublish
+ of "tasks":
+ result = actionTasks
+ of "develop":
+ result = actionDevelop
+ of "check":
+ result = actionCheck
+ else:
+ result = actionCustom
+
+proc initAction*(options: var Options, key: string) =
+ ## Intialises `options.actions` fields based on `options.actions.typ` and
+ ## `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 = ""
+ of actionDump:
+ options.action.projName = ""
+ options.forcePrompts = forcePromptYes
+ of actionRefresh:
+ options.action.optionalURL = ""
+ of actionSearch:
+ options.action.search = @[]
+ of actionCustom:
+ options.action.command = key
+ options.action.arguments = @[]
+ options.action.flags = newStringTable()
+ of actionPublish, actionList, actionTasks, actionCheck, actionRun,
+ actionNil: discard
+
+proc prompt*(options: Options, question: string): bool =
+ ## Asks an interactive question and returns the result.
+ ##
+ ## The proc will return immediately without asking the user if the global
+ ## forcePrompts has a value different than dontForcePrompt.
+ return prompt(options.forcePrompts, question)
+
+proc promptCustom*(options: Options, question, default: string): string =
+ ## Asks an interactive question and returns the result.
+ ##
+ ## The proc will return "default" without asking the user if the global
+ ## forcePrompts is forcePromptYes.
+ return promptCustom(options.forcePrompts, question, default)
+
+proc promptList*(options: Options, question: string, args: openarray[string]): string =
+ ## Asks an interactive question and returns the result.
+ ##
+ ## The proc will return one of the provided args. If not prompting the first
+ ## options is selected.
+ return promptList(options.forcePrompts, question, args)
+
+proc getNimbleDir*(options: Options): string =
+ result = options.config.nimbleDir
+ if options.nimbleDir.len != 0:
+ # --nimbleDir:<dir> takes priority...
+ result = options.nimbleDir
+ else:
+ # ...followed by the environment variable.
+ let env = getEnv("NIMBLE_DIR")
+ if env.len != 0:
+ display("Warning:", "Using the environment variable: NIMBLE_DIR='" &
+ env & "'", Warning)
+ result = env
+
+ return expandTilde(result)
+
+proc getPkgsDir*(options: Options): string =
+ options.getNimbleDir() / "pkgs"
+
+proc getBinDir*(options: Options): string =
+ options.getNimbleDir() / "bin"
+
+proc parseCommand*(key: string, result: var Options) =
+ result.action = Action(typ: parseActionType(key))
+ initAction(result, key)
+
+proc parseArgument*(key: string, result: var Options) =
+ case result.action.typ
+ of actionNil:
+ assert false
+ of actionInstall, actionPath, actionDevelop, actionUninstall:
+ # Parse pkg@verRange
+ if '@' in key:
+ 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 '@'.")
+ result.action.packages.add((pkgName, pkgVer.parseVersionRange()))
+ else:
+ result.action.packages.add((key, VersionRange(kind: verAny)))
+ of actionRefresh:
+ result.action.optionalURL = key
+ of actionSearch:
+ 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."
+ )
+ result.action.projName = key
+ of actionCompile, actionDoc:
+ result.action.file = key
+ of actionList, actionPublish:
+ result.showHelp = true
+ of actionBuild:
+ result.action.file = key
+ of actionRun:
+ if result.action.runFile.len == 0:
+ result.action.runFile = key
+ else:
+ result.action.runFlags.add(key)
+ of actionCustom:
+ result.action.arguments.add(key)
+ else:
+ discard
+
+proc getFlagString(kind: CmdLineKind, flag, val: string): string =
+ let prefix =
+ case kind
+ of cmdShortOption: "-"
+ of cmdLongOption: "--"
+ else: ""
+ if val == "":
+ return prefix & flag
+ else:
+ return prefix & flag & ":" & val
+
+proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) =
+
+ let f = flag.normalize()
+
+ # Global flags.
+ var isGlobalFlag = true
+ case f
+ of "help", "h": result.showHelp = true
+ of "version", "v": result.showVersion = true
+ of "accept", "y": result.forcePrompts = forcePromptYes
+ of "reject", "n": result.forcePrompts = forcePromptNo
+ of "nimbledir": result.nimbleDir = val
+ of "verbose": result.verbosity = LowPriority
+ of "debug": result.verbosity = DebugPriority
+ of "nocolor": result.noColor = true
+ of "disablevalidation": result.disableValidation = true
+ else: isGlobalFlag = false
+
+ var wasFlagHandled = true
+ # Action-specific flags.
+ case result.action.typ
+ of actionSearch, actionList:
+ case f
+ of "installed", "i":
+ result.queryInstalled = true
+ of "ver":
+ result.queryVersions = true
+ of "json":
+ result.jsonOutput = true
+ else:
+ wasFlagHandled = false
+ of actionInstall:
+ case f
+ of "depsonly", "d":
+ result.depsOnly = true
+ of "passnim", "p":
+ result.action.passNimFlags.add(val)
+ else:
+ wasFlagHandled = false
+ of actionUninstall:
+ case f
+ of "incldeps", "i":
+ result.uninstallRevDeps = true
+ else:
+ wasFlagHandled = false
+ of actionCompile, actionDoc, actionBuild:
+ if not isGlobalFlag:
+ result.action.compileOptions.add(getFlagString(kind, flag, val))
+ of actionRun:
+ result.action.runFlags.add(getFlagString(kind, flag, val))
+ of actionCustom:
+ if result.action.command.normalize == "test":
+ if f == "continue" or f == "c":
+ result.continueTestsOnFailure = true
+ result.action.flags[flag] = val
+ else:
+ wasFlagHandled = false
+
+ if not wasFlagHandled and not isGlobalFlag:
+ result.unknownFlags.add((kind, flag, val))
+
+proc initOptions(): Options =
+ Options(
+ action: Action(typ: actionNil),
+ pkgInfoCache: newTable[string, PackageInfo](),
+ verbosity: HighPriority,
+ noColor: not isatty(stdout)
+ )
+
+proc parseMisc(options: var Options) =
+ # Load nimbledata.json
+ let nimbledataFilename = options.getNimbleDir() / "nimbledata.json"
+
+ if fileExists(nimbledataFilename):
+ try:
+ options.nimbleData = parseFile(nimbledataFilename)
+ except:
+ raise newException(NimbleError, "Couldn't parse nimbledata.json file " &
+ "located at " & nimbledataFilename)
+ else:
+ options.nimbleData = %{"reverseDeps": newJObject()}
+
+proc handleUnknownFlags(options: var Options) =
+ if options.action.typ == actionRun:
+ # ActionRun uses flags that come before the command as compilation flags
+ # and flags that come after as run flags.
+ options.action.compileFlags =
+ map(options.unknownFlags, x => getFlagString(x[0], x[1], x[2]))
+ options.unknownFlags = @[]
+ else:
+ # For everything else, handle the flags that came before the command
+ # normally.
+ let unknownFlags = options.unknownFlags
+ options.unknownFlags = @[]
+ for flag in unknownFlags:
+ parseFlag(flag[1], flag[2], options, flag[0])
+
+ # 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])
+ )
+
+proc parseCmdLine*(): Options =
+ result = initOptions()
+
+ # Parse command line params first. A simple `--version` shouldn't require
+ # a config to be parsed.
+ for kind, key, val in getOpt():
+ case kind
+ of cmdArgument:
+ if result.action.typ == actionNil:
+ parseCommand(key, result)
+ else:
+ parseArgument(key, result)
+ of cmdLongOption, cmdShortOption:
+ parseFlag(key, val, result, kind)
+ of cmdEnd: assert(false) # cannot happen
+
+ handleUnknownFlags(result)
+
+ # Set verbosity level.
+ setVerbosity(result.verbosity)
+
+ # Set whether color should be shown.
+ setShowColor(not result.noColor)
+
+ # Parse config.
+ result.config = parseConfig()
+
+ # Parse other things, for example the nimbledata.json file.
+ parseMisc(result)
+
+ if result.action.typ == actionNil and not result.showVersion:
+ result.showHelp = true
+
+ if result.action.typ != actionNil and result.showVersion:
+ # We've got another command that should be handled. For example:
+ # nimble run foobar -v
+ result.showVersion = false
+
+proc getProxy*(options: Options): Proxy =
+ ## Returns ``nil`` if no proxy is specified.
+ var url = ""
+ if ($options.config.httpProxy).len > 0:
+ url = $options.config.httpProxy
+ else:
+ try:
+ if existsEnv("http_proxy"):
+ url = getEnv("http_proxy")
+ elif existsEnv("https_proxy"):
+ url = getEnv("https_proxy")
+ elif existsEnv("HTTP_PROXY"):
+ url = getEnv("HTTP_PROXY")
+ elif existsEnv("HTTPS_PROXY"):
+ url = getEnv("HTTPS_PROXY")
+ except ValueError:
+ display("Warning:", "Unable to parse proxy from environment: " &
+ getCurrentExceptionMsg(), Warning, HighPriority)
+
+ if url.len > 0:
+ var parsed = parseUri(url)
+ if parsed.scheme.len == 0 or parsed.hostname.len == 0:
+ parsed = parseUri("http://" & url)
+ let auth =
+ if parsed.username.len > 0: parsed.username & ":" & parsed.password
+ else: ""
+ return newProxy($parsed, auth)
+ 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
+ return newOptions
+
+proc shouldRemoveTmp*(options: Options, file: string): bool =
+ result = true
+ if options.verbosity <= DebugPriority:
+ let msg = "Not removing temporary path because of debug verbosity: " & file
+ display("Warning:", msg, Warning, MediumPriority)
+ return false
+
+proc getCompilationFlags*(options: var Options): var seq[string] =
+ case options.action.typ
+ of actionBuild, actionDoc, actionCompile:
+ return options.action.compileOptions
+ of actionRun:
+ return options.action.compileFlags
+ else:
+ assert false
+
+proc getCompilationFlags*(options: Options): seq[string] =
+ var opt = options
+ return opt.getCompilationFlags()
+
+proc getCompilationBinary*(options: Options): Option[string] =
+ case options.action.typ
+ of actionBuild, actionDoc, actionCompile:
+ let file = options.action.file.changeFileExt("")
+ if file.len > 0:
+ return some(file)
+ of actionRun:
+ let runFile = options.action.runFile.changeFileExt(ExeExt)
+ if runFile.len > 0:
+ return some(runFile)
+ else:
+ discard<
\ No newline at end of file
A => src/nimblepkg/packageinfo.nim +577 -0
@@ 1,577 @@
+# Copyright (C) Dominik Picheta. All rights reserved.
+# BSD License. Look at license.txt for more info.
+
+# Stdlib imports
+import system except TResult
+import hashes, json, strutils, os, sets, tables, httpclient
+
+# Local imports
+import version, tools, common, options, cli, config
+
+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
+
+proc initPackageInfo*(path: string): PackageInfo =
+ result.myPath = path
+ result.specialVersion = ""
+ result.preHooks.init()
+ result.postHooks.init()
+ # reasonable default:
+ result.name = path.splitFile.name
+ result.version = ""
+ result.author = ""
+ result.description = ""
+ result.license = ""
+ result.skipDirs = @[]
+ result.skipFiles = @[]
+ result.skipExt = @[]
+ result.installDirs = @[]
+ result.installFiles = @[]
+ result.installExt = @[]
+ result.requires = @[]
+ result.foreignDeps = @[]
+ result.bin = @[]
+ result.srcDir = ""
+ result.binDir = ""
+ result.backend = "c"
+
+proc toValidPackageName*(name: string): string =
+ result = ""
+ for c in name:
+ case c
+ of '_', '-':
+ if result[^1] != '_': result.add('_')
+ of AllChars - IdentChars - {'-'}: discard
+ else: result.add(c)
+
+proc getNameVersion*(pkgpath: string): tuple[name, version: string] =
+ ## 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.
+ ##
+ ## Returns the value of ``name`` if it is a valid string, or aborts execution
+ ## if the field exists but is not of string type. If ``name`` is not present,
+ ## returns ``default``.
+ if hasKey(obj, name):
+ if obj[name].kind == JString:
+ return obj[name].str
+ else:
+ raise newException(NimbleError, "Corrupted packages.json file. " & name &
+ " field is of unexpected type.")
+ else: return default
+
+proc requiredField(obj: JsonNode, name: string): string =
+ ## Queries ``obj`` for the required ``name`` string.
+ ##
+ ## Aborts execution if the field does not exist or is of invalid json type.
+ result = optionalField(obj, name)
+ if result.len == 0:
+ raise newException(NimbleError,
+ "Package in packages.json file does not contain a " & name & " field.")
+
+proc fromJson(obj: JSonNode): Package =
+ ## Constructs a Package object from a JSON node.
+ ##
+ ## Aborts execution if the JSON node doesn't contain the required fields.
+ result.name = obj.requiredField("name")
+ if obj.hasKey("alias"):
+ result.alias = obj.requiredField("alias")
+ else:
+ result.alias = ""
+ result.version = obj.optionalField("version")
+ result.url = obj.optionalField("url")
+ result.downloadMethod = obj.optionalField("method")
+ result.dvcsTag = obj.optionalField("dvcs-tag")
+ result.license = obj.requiredField("license")
+ result.tags = @[]
+ for t in obj["tags"]:
+ result.tags.add(t.str)
+ result.description = obj.requiredField("description")
+ result.web = obj.optionalField("web")
+
+proc readMetaData*(path: string): MetaData =
+ ## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json``
+ var bmeta = path / "nimblemeta.json"
+ if not existsFile(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]
+ 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.
+ ##
+ ## In the future this will check a stored time stamp to determine how long
+ ## ago the package list was refreshed.
+ result = true
+ for name, list in options.config.packageLists:
+ if fileExists(options.getNimbleDir() / "packages_" & name & ".json"):
+ result = false
+
+proc validatePackagesList(path: string): bool =
+ ## Determines whether package list at ``path`` is valid.
+ try:
+ let pkgList = parseFile(path)
+ if pkgList.kind == JArray:
+ if pkgList.len == 0:
+ display("Warning:", path & " contains no packages.", Warning,
+ HighPriority)
+ return true
+ except ValueError, JsonParsingError:
+ return false
+
+proc fetchList*(list: PackageList, options: Options) =
+ ## Downloads or copies the specified package list and saves it in $nimbleDir.
+ let verb = if list.urls.len > 0: "Downloading" else: "Copying"
+ display(verb, list.name & " package list", priority = HighPriority)
+
+ var
+ lastError = ""
+ copyFromPath = ""
+ if list.urls.len > 0:
+ for i in 0 ..< list.urls.len:
+ let url = list.urls[i]
+ display("Trying", url)
+ let tempPath = options.getNimbleDir() / "packages_temp.json"
+
+ # Grab the proxy
+ let proxy = getProxy(options)
+ if not proxy.isNil:
+ var maskedUrl = proxy.url
+ if maskedUrl.password.len > 0: maskedUrl.password = "***"
+ display("Connecting", "to proxy at " & $maskedUrl,
+ priority = LowPriority)
+
+ try:
+ let client = newHttpClient(proxy = proxy)
+ client.downloadFile(url, tempPath)
+ except:
+ let message = "Could not download: " & getCurrentExceptionMsg()
+ display("Warning:", message, Warning)
+ lastError = message
+ continue
+
+ if not validatePackagesList(tempPath):
+ lastError = "Downloaded packages.json file is invalid"
+ display("Warning:", lastError & ", discarding.", Warning)
+ continue
+
+ copyFromPath = tempPath
+ display("Success", "Package list downloaded.", Success, HighPriority)
+ lastError = ""
+ break
+
+ elif list.path != "":
+ if not validatePackagesList(list.path):
+ lastError = "Copied packages.json file is invalid"
+ display("Warning:", lastError & ", discarding.", Warning)
+ else:
+ copyFromPath = list.path
+ display("Success", "Package list copied.", Success, HighPriority)
+
+ if lastError.len != 0:
+ raise newException(NimbleError, "Refresh failed\n" & lastError)
+
+ if copyFromPath.len > 0:
+ copyFile(copyFromPath,
+ options.getNimbleDir() / "packages_$1.json" % list.name.toLowerAscii())
+
+proc readPackageList(name: string, options: Options): JsonNode =
+ # If packages.json is not present ask the user if they want to download it.
+ if needsRefresh(options):
+ if options.prompt("No local packages.json found, download it from " &
+ "internet?"):
+ for name, list in options.config.packageLists:
+ fetchList(list, options)
+ else:
+ # The user might not need a package list for now. So let's try
+ # going further.
+ return newJArray()
+ return parseFile(options.getNimbleDir() / "packages_" &
+ name.toLowerAscii() & ".json")
+
+proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool
+proc resolveAlias(pkg: Package, options: Options): Package =
+ result = pkg
+ # Resolve alias.
+ if pkg.alias.len > 0:
+ 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: " &
+ pkg.alias)
+
+proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool =
+ ## Searches any packages.json files defined in ``options.config.packageLists``
+ ## Saves the found package into ``resPkg``.
+ ##
+ ## Pass in ``pkg`` the name of the package you are searching for. As
+ ## convenience the proc returns a boolean specifying if the ``resPkg`` was
+ ## successfully filled with good data.
+ ##
+ ## Aliases are handled and resolved.
+ for name, list in options.config.packageLists:
+ display("Reading", "$1 package list" % name, priority = LowPriority)
+ let packages = readPackageList(name, options)
+ for p in packages:
+ if normalize(p["name"].str) == normalize(pkg):
+ resPkg = p.fromJson()
+ resPkg = resolveAlias(resPkg, options)
+ return true
+
+proc getPackageList*(options: Options): seq[Package] =
+ ## Returns the list of packages found in the downloaded packages.json files.
+ result = @[]
+ var namesAdded = initHashSet[string]()
+ for name, list in options.config.packageLists:
+ let packages = readPackageList(name, options)
+ for p in packages:
+ let pkg: Package = p.fromJson()
+ if pkg.name notin namesAdded:
+ result.add(pkg)
+ 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":
+ result = path
+ inc hits
+ else: discard
+ if hits >= 2:
+ raise newException(NimbleError,
+ "Only one .nimble file should be present in " & dir)
+ elif hits == 0:
+ if error:
+ raise newException(NimbleError,
+ "Specified directory ($1) does not contain a .nimble file." % 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)
+
+proc getInstalledPkgsMin*(libsDir: string, options: Options):
+ seq[tuple[pkginfo: PackageInfo, meta: MetaData]] =
+ ## 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``.
+ ##
+ ## ``libsDir`` is in most cases: ~/.nimble/pkgs/ (options.getPkgsDir)
+ result = @[]
+ for kind, path in walkDir(libsDir):
+ if kind == pcDir:
+ let nimbleFile = findNimbleFile(path, false)
+ if nimbleFile != "":
+ let meta = readMetaData(path)
+ let (name, version) = getNameVersion(path)
+ var pkg = initPackageInfo(nimbleFile)
+ pkg.name = name
+ pkg.version = version
+ pkg.specialVersion = version
+ 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)
+ result.add((pkg, meta))
+
+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)
+
+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
+ # TODO: This needs better caching.
+ if getPackage(dep.name, options, pkg):
+ # The resulting ``pkg`` will contain the resolved name or the original if
+ # no alias is present.
+ result.name = pkg.name
+
+proc findPkg*(pkglist: seq[tuple[pkgInfo: PackageInfo, meta: MetaData]],
+ 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
+ ## version number)
+ ##
+ ## **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 not result or isNewer:
+ r = pkg.pkginfo
+ result = true
+
+proc findAllPkgs*(pkglist: seq[tuple[pkgInfo: PackageInfo, meta: MetaData]],
+ 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
+
+proc getRealDir*(pkgInfo: PackageInfo): string =
+ ## Returns the directory containing the package source files.
+ if pkgInfo.srcDir != "" and (not pkgInfo.isInstalled or pkgInfo.isLinked):
+ result = pkgInfo.mypath.splitFile.dir / pkgInfo.srcDir
+ else:
+ result = pkgInfo.mypath.splitFile.dir
+
+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
+ else:
+ result = pkgInfo.mypath.splitFile.dir / bin
+
+proc echoPackage*(pkg: Package) =
+ echo(pkg.name & ":")
+ if pkg.alias.len > 0:
+ echo(" Alias for ", pkg.alias)
+ else:
+ echo(" url: " & pkg.url & " (" & pkg.downloadMethod & ")")
+ echo(" tags: " & pkg.tags.join(", "))
+ echo(" description: " & pkg.description)
+ echo(" license: " & pkg.license)
+ if pkg.web.len > 0:
+ echo(" website: " & pkg.web)
+
+func toJson*(pkg: Package, queryVersions = false): JsonNode =
+ result = %*{
+ "name": %pkg.name,
+ "url": %pkg.url,
+ "method": %pkg.downloadMethod,
+ "tags": %pkg.tags,
+ "description": %pkg.description,
+ "license": %pkg.license,
+ }
+ if pkg.web.len > 0:
+ result["web"] = %pkg.web
+
+proc getDownloadDirName*(pkg: Package, verRange: VersionRange): string =
+ result = pkg.name
+ let verSimple = getSimpleString(verRange)
+ if verSimple != "":
+ result.add "_"
+ result.add verSimple
+
+proc checkInstallFile(pkgInfo: PackageInfo,
+ origDir, file: string): bool =
+ ## Checks whether ``file`` should be installed.
+ ## ``True`` means file should be skipped.
+
+ for ignoreFile in pkgInfo.skipFiles:
+ if ignoreFile.endswith("nimble"):
+ raise newException(NimbleError, ignoreFile & " must be installed.")
+ if samePaths(file, origDir / ignoreFile):
+ result = true
+ break
+
+ for ignoreExt in pkgInfo.skipExt:
+ if file.splitFile.ext == ('.' & ignoreExt):
+ result = true
+ break
+
+ if file.splitFile().name[0] == '.': result = true
+
+proc checkInstallDir(pkgInfo: PackageInfo,
+ origDir, dir: string): bool =
+ ## Determines whether ``dir`` should be installed.
+ ## ``True`` means dir should be skipped.
+ for ignoreDir in pkgInfo.skipDirs:
+ if samePaths(dir, origDir / ignoreDir):
+ result = true
+ break
+
+ let thisDir = splitPath(dir).tail
+ assert thisDir != ""
+ if thisDir[0] == '.': result = true
+ if thisDir == "nimcache": result = true
+
+proc iterFilesWithExt(dir: string, pkgInfo: PackageInfo,
+ action: proc (f: string)) =
+ ## Runs `action` for each filename of the files that have a whitelisted
+ ## file extension.
+ for kind, path in walkDir(dir):
+ if kind == pcDir:
+ iterFilesWithExt(path, pkgInfo, action)
+ else:
+ if path.splitFile.ext.substr(1) in pkgInfo.installExt:
+ action(path)
+
+proc iterFilesInDir(dir: string, action: proc (f: string)) =
+ ## Runs `action` for each file in ``dir`` and any
+ ## subdirectories that are in it.
+ for kind, path in walkDir(dir):
+ if kind == pcDir:
+ iterFilesInDir(path, action)
+ else:
+ action(path)
+
+proc iterInstallFiles*(realDir: string, pkgInfo: PackageInfo,
+ options: Options, action: proc (f: string)) =
+ ## Runs `action` for each file within the ``realDir`` that should be
+ ## installed.
+ let whitelistMode =
+ pkgInfo.installDirs.len != 0 or
+ pkgInfo.installFiles.len != 0 or
+ pkgInfo.installExt.len != 0
+ if whitelistMode:
+ for file in pkgInfo.installFiles:
+ let src = realDir / file
+ if not src.existsFile():
+ if options.prompt("Missing file " & src & ". Continue?"):
+ continue
+ else:
+ raise NimbleQuit(msg: "")
+
+ action(src)
+
+ for dir in pkgInfo.installDirs:
+ # TODO: Allow skipping files inside dirs?
+ let src = realDir / dir
+ if not src.existsDir():
+ if options.prompt("Missing directory " & src & ". Continue?"):
+ continue
+ else:
+ raise NimbleQuit(msg: "")
+
+ iterFilesInDir(src, action)
+
+ iterFilesWithExt(realDir, pkgInfo, action)
+ else:
+ for kind, file in walkDir(realDir):
+ if kind == pcDir:
+ let skip = pkgInfo.checkInstallDir(realDir, file)
+ if skip: continue
+ # we also have to stop recursing if we reach an in-place nimbleDir
+ if file == options.getNimbleDir().expandFilename(): continue
+
+ iterInstallFiles(file, pkgInfo, options, action)
+ else:
+ let skip = pkgInfo.checkInstallFile(realDir, file)
+ if skip: continue
+
+ action(file)
+