Initial commit
This commit is contained in:
commit
092252a727
|
@ -0,0 +1 @@
|
|||
NIM_FLAGS += --path:$(TUP_CWD)/../nim/v1.6.8
|
|
@ -0,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"
|
|
@ -0,0 +1,2 @@
|
|||
include_rules
|
||||
: nim_lk.nim |> !nim_bin |>
|
|
@ -0,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()
|
|
@ -0,0 +1,3 @@
|
|||
define:nixbuild
|
||||
define:ssl
|
||||
threads:off
|
|
@ -0,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()))
|
|
@ -0,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)
|
|
@ -0,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)
|
|
@ -0,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)
|
|
@ -0,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)
|
|
@ -0,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)
|
|
@ -0,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."
|
|
@ -0,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!")
|
|
@ -0,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)
|
|
@ -0,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
|
||||
}""")
|
|
@ -0,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
|
|
@ -0,0 +1,2 @@
|
|||
--path:"$nim/"
|
||||
--path:"$lib/packages/docutils"
|
|
@ -0,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
|
|
@ -0,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()
|
|
@ -0,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
|
|
@ -0,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)
|
|
@ -0,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
|
|
@ -0,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)
|
||||
|
||||
proc getPkgDest*(pkgInfo: PackageInfo, options: Options): string =
|
||||
let versionStr = '-' & pkgInfo.specialVersion
|
||||
let pkgDestDir = options.getPkgsDir() / (pkgInfo.name & versionStr)
|
||||
return pkgDestDir
|
||||
|
||||
proc `==`*(pkg1: PackageInfo, pkg2: PackageInfo): bool =
|
||||
if pkg1.name == pkg2.name and pkg1.myPath == pkg2.myPath:
|
||||
return true
|
||||
|
||||
proc hash*(x: PackageInfo): Hash =
|
||||
var h: Hash = 0
|
||||
h = h !& hash(x.myPath)
|
||||
result = !$h
|
||||
|
||||
when isMainModule:
|
||||
doAssert getNameVersion("/home/user/.nimble/libs/packagea-0.1") ==
|
||||
("packagea", "0.1")
|
||||
doAssert getNameVersion("/home/user/.nimble/libs/package-a-0.1") ==
|
||||
("package-a", "0.1")
|
||||
doAssert getNameVersion("/home/user/.nimble/libs/package-a-0.1/package.nimble") ==
|
||||
("package-a", "0.1")
|
||||
doAssert getNameVersion("/home/user/.nimble/libs/package-#head") ==
|
||||
("package", "#head")
|
||||
doAssert getNameVersion("/home/user/.nimble/libs/package-#branch-with-dashes") ==
|
||||
("package", "#branch-with-dashes")
|
||||
# readPackageInfo (and possibly more) depends on this not raising.
|
||||
doAssert getNameVersion("/home/user/.nimble/libs/package") == ("package", "")
|
||||
|
||||
doAssert toValidPackageName("foo__bar") == "foo_bar"
|
||||
doAssert toValidPackageName("jhbasdh!£$@%#^_&*_()qwe") == "jhbasdh_qwe"
|
||||
|
||||
echo("All tests passed!")
|
|
@ -0,0 +1,112 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
import os, strutils, sets, json
|
||||
|
||||
# Local imports
|
||||
import cli, options, tools
|
||||
|
||||
when defined(windows):
|
||||
import version
|
||||
|
||||
when not declared(initHashSet) or not declared(toHashSet):
|
||||
import common
|
||||
|
||||
when defined(windows):
|
||||
# This is just for Win XP support.
|
||||
# TODO: Drop XP support?
|
||||
from winlean import WINBOOL, DWORD
|
||||
type
|
||||
OSVERSIONINFO* {.final, pure.} = object
|
||||
dwOSVersionInfoSize*: DWORD
|
||||
dwMajorVersion*: DWORD
|
||||
dwMinorVersion*: DWORD
|
||||
dwBuildNumber*: DWORD
|
||||
dwPlatformId*: DWORD
|
||||
szCSDVersion*: array[0..127, char]
|
||||
|
||||
proc GetVersionExA*(VersionInformation: var OSVERSIONINFO): WINBOOL{.stdcall,
|
||||
dynlib: "kernel32", importc: "GetVersionExA".}
|
||||
|
||||
proc setupBinSymlink*(symlinkDest, symlinkFilename: string,
|
||||
options: Options): seq[string] =
|
||||
result = @[]
|
||||
let currentPerms = getFilePermissions(symlinkDest)
|
||||
setFilePermissions(symlinkDest, currentPerms + {fpUserExec})
|
||||
when defined(unix):
|
||||
display("Creating", "symlink: $1 -> $2" %
|
||||
[symlinkDest, symlinkFilename], priority = MediumPriority)
|
||||
if existsFile(symlinkFilename):
|
||||
let msg = "Symlink already exists in $1. Replacing." % symlinkFilename
|
||||
display("Warning:", msg, Warning, HighPriority)
|
||||
removeFile(symlinkFilename)
|
||||
|
||||
createSymlink(symlinkDest, symlinkFilename)
|
||||
result.add symlinkFilename.extractFilename
|
||||
elif defined(windows):
|
||||
# There is a bug on XP, described here:
|
||||
# http://stackoverflow.com/questions/2182568/batch-script-is-not-executed-if-chcp-was-called
|
||||
# But this workaround brakes code page on newer systems, so we need to detect OS version
|
||||
var osver = OSVERSIONINFO()
|
||||
osver.dwOSVersionInfoSize = cast[DWORD](sizeof(OSVERSIONINFO))
|
||||
if GetVersionExA(osver) == WINBOOL(0):
|
||||
raise newException(NimbleError,
|
||||
"Can't detect OS version: GetVersionExA call failed")
|
||||
let fixChcp = osver.dwMajorVersion <= 5
|
||||
|
||||
# Create cmd.exe/powershell stub.
|
||||
let dest = symlinkFilename.changeFileExt("cmd")
|
||||
display("Creating", "stub: $1 -> $2" % [symlinkDest, dest],
|
||||
priority = MediumPriority)
|
||||
var contents = "@"
|
||||
if options.config.chcp:
|
||||
if fixChcp:
|
||||
contents.add "chcp 65001 > nul && "
|
||||
else: contents.add "chcp 65001 > nul\n@"
|
||||
contents.add "\"" & symlinkDest & "\" %*\n"
|
||||
writeFile(dest, contents)
|
||||
result.add dest.extractFilename
|
||||
# For bash on Windows (Cygwin/Git bash).
|
||||
let bashDest = dest.changeFileExt("")
|
||||
display("Creating", "Cygwin stub: $1 -> $2" %
|
||||
[symlinkDest, bashDest], priority = MediumPriority)
|
||||
writeFile(bashDest, "\"" & symlinkDest & "\" \"$@\"\n")
|
||||
result.add bashDest.extractFilename
|
||||
else:
|
||||
{.error: "Sorry, your platform is not supported.".}
|
||||
|
||||
proc saveNimbleMeta*(pkgDestDir, url, vcsRevision: string,
|
||||
filesInstalled, bins: HashSet[string],
|
||||
isLink: bool = false) =
|
||||
## Saves the specified data into a ``nimblemeta.json`` file inside
|
||||
## ``pkgDestDir``.
|
||||
##
|
||||
## filesInstalled - A list of absolute paths to files which have been
|
||||
## installed.
|
||||
## bins - A list of binary filenames which have been installed for this
|
||||
## package.
|
||||
##
|
||||
## isLink - Determines whether the installed package is a .nimble-link.
|
||||
var nimblemeta = %{"url": %url}
|
||||
if vcsRevision.len > 0:
|
||||
nimblemeta["vcsRevision"] = %vcsRevision
|
||||
let files = newJArray()
|
||||
nimblemeta["files"] = files
|
||||
for file in filesInstalled:
|
||||
files.add(%changeRoot(pkgDestDir, "", file))
|
||||
let binaries = newJArray()
|
||||
nimblemeta["binaries"] = binaries
|
||||
for bin in bins:
|
||||
binaries.add(%bin)
|
||||
nimblemeta["isLink"] = %isLink
|
||||
writeFile(pkgDestDir / "nimblemeta.json", $nimblemeta)
|
||||
|
||||
proc saveNimbleMeta*(pkgDestDir, pkgDir, vcsRevision, nimbleLinkPath: string) =
|
||||
## Overload of saveNimbleMeta for linked (.nimble-link) packages.
|
||||
##
|
||||
## pkgDestDir - The directory where the package has been installed.
|
||||
## For example: ~/.nimble/pkgs/jester-#head/
|
||||
##
|
||||
## pkgDir - The directory where the original package files are.
|
||||
## For example: ~/projects/jester/
|
||||
saveNimbleMeta(pkgDestDir, "file://" & pkgDir, vcsRevision,
|
||||
toHashSet[string]([nimbleLinkPath]), initHashSet[string](), true)
|
|
@ -0,0 +1,73 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
import json, os, strformat, sets, sequtils
|
||||
import common, version, packageinfotypes, cli, tools, sha1hashes
|
||||
|
||||
type
|
||||
MetaDataError* = object of NimbleError
|
||||
|
||||
PackageMetaDataJsonKeys = enum
|
||||
pmdjkVersion = "version"
|
||||
pmdjkMetaData = "metaData"
|
||||
|
||||
const
|
||||
packageMetaDataFileName* = "nimblemeta.json"
|
||||
packageMetaDataFileVersion = 1
|
||||
|
||||
proc initPackageMetaData*(): PackageMetaData =
|
||||
result = PackageMetaData(
|
||||
vcsRevision: notSetSha1Hash)
|
||||
|
||||
proc metaDataError(msg: string): ref MetaDataError =
|
||||
newNimbleError[MetaDataError](msg)
|
||||
|
||||
proc `%`(specialVersions: HashSet[Version]): JsonNode =
|
||||
%specialVersions.toSeq
|
||||
|
||||
proc initFromJson(specialVersions: var HashSet[Version], jsonNode: JsonNode,
|
||||
jsonPath: var string) =
|
||||
case jsonNode.kind
|
||||
of JArray:
|
||||
let originalJsonPathLen = jsonPath.len
|
||||
for i in 0 ..< jsonNode.len:
|
||||
jsonPath.add '['
|
||||
jsonPath.addInt i
|
||||
jsonPath.add ']'
|
||||
var version = newVersion("")
|
||||
initFromJson(version, jsonNode[i], jsonPath)
|
||||
specialVersions.incl version
|
||||
jsonPath.setLen originalJsonPathLen
|
||||
else:
|
||||
assert false, "The `jsonNode` must be of kind JArray."
|
||||
|
||||
proc saveMetaData*(metaData: PackageMetaData, dirName: string,
|
||||
changeRoots = true) =
|
||||
## Saves some important data to file in the package installation directory.
|
||||
var metaDataWithChangedPaths = metaData
|
||||
if changeRoots:
|
||||
for i, file in metaData.files:
|
||||
metaDataWithChangedPaths.files[i] = changeRoot(dirName, "", file)
|
||||
let json = %{
|
||||
$pmdjkVersion: %packageMetaDataFileVersion,
|
||||
$pmdjkMetaData: %metaDataWithChangedPaths}
|
||||
writeFile(dirName / packageMetaDataFileName, json.pretty)
|
||||
|
||||
proc loadMetaData*(dirName: string, raiseIfNotFound: bool): PackageMetaData =
|
||||
## Returns package meta data read from file in package installation directory
|
||||
result = initPackageMetaData()
|
||||
let fileName = dirName / packageMetaDataFileName
|
||||
if fileExists(fileName):
|
||||
{.warning[ProveInit]: off.}
|
||||
{.warning[UnsafeSetLen]: off.}
|
||||
result = parseFile(fileName)[$pmdjkMetaData].to(PackageMetaData)
|
||||
{.warning[UnsafeSetLen]: on.}
|
||||
{.warning[ProveInit]: on.}
|
||||
elif raiseIfNotFound:
|
||||
raise metaDataError(&"No {packageMetaDataFileName} file found in {dirName}")
|
||||
else:
|
||||
displayWarning(&"No {packageMetaDataFileName} file found in {dirName}")
|
||||
|
||||
proc fillMetaData*(packageInfo: var PackageInfo, dirName: string,
|
||||
raiseIfNotFound: bool) =
|
||||
packageInfo.metaData = loadMetaData(dirName, raiseIfNotFound)
|
|
@ -0,0 +1,511 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
import parsecfg, sets, streams, strutils, os, tables, sugar
|
||||
from sequtils import apply, map
|
||||
|
||||
import version, tools, common, nimscriptwrapper, options, packageinfo, cli
|
||||
|
||||
## Contains procedures for parsing .nimble files. Moved here from ``packageinfo``
|
||||
## because it depends on ``nimscriptwrapper`` (``nimscriptwrapper`` also
|
||||
## depends on other procedures in ``packageinfo``.
|
||||
|
||||
type
|
||||
NimbleFile* = string
|
||||
|
||||
ValidationError* = object of NimbleError
|
||||
warnInstalled*: bool # Determines whether to show a warning for installed pkgs
|
||||
warnAll*: bool
|
||||
|
||||
const reservedNames = [
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
]
|
||||
|
||||
proc newValidationError(msg: string, warnInstalled: bool,
|
||||
hint: string, warnAll: bool): ref ValidationError =
|
||||
result = newException(ValidationError, msg)
|
||||
result.warnInstalled = warnInstalled
|
||||
result.warnAll = warnAll
|
||||
result.hint = hint
|
||||
|
||||
proc raiseNewValidationError(msg: string, warnInstalled: bool,
|
||||
hint: string = "", warnAll = false) =
|
||||
stderr.writeLine "nimble: ", msg
|
||||
|
||||
proc validatePackageName*(name: string) =
|
||||
## Raises an error if specified package name contains invalid characters.
|
||||
##
|
||||
## A valid package name is one which is a valid nim module name. So only
|
||||
## underscores, letters and numbers allowed.
|
||||
if name.len == 0: return
|
||||
|
||||
if name[0] in {'0'..'9'}:
|
||||
raiseNewValidationError(name &
|
||||
"\"$1\" is an invalid package name: cannot begin with $2" %
|
||||
[name, $name[0]], true)
|
||||
|
||||
var prevWasUnderscore = false
|
||||
for c in name:
|
||||
case c
|
||||
of '_':
|
||||
if prevWasUnderscore:
|
||||
raiseNewValidationError(
|
||||
"$1 is an invalid package name: cannot contain \"__\"" % name, true)
|
||||
prevWasUnderscore = true
|
||||
of AllChars - IdentChars:
|
||||
raiseNewValidationError(
|
||||
"$1 is an invalid package name: cannot contain '$2'" % [name, $c],
|
||||
true)
|
||||
else:
|
||||
prevWasUnderscore = false
|
||||
|
||||
if name.endsWith("pkg"):
|
||||
raiseNewValidationError("\"$1\" is an invalid package name: cannot end" &
|
||||
" with \"pkg\"" % name, false)
|
||||
if name.toUpperAscii() in reservedNames:
|
||||
raiseNewValidationError(
|
||||
"\"$1\" is an invalid package name: reserved name" % name, false)
|
||||
|
||||
proc validateVersion*(ver: string) =
|
||||
for c in ver:
|
||||
if c notin ({'.'} + Digits):
|
||||
raiseNewValidationError(
|
||||
"Version may only consist of numbers and the '.' character " &
|
||||
"but found '" & c & "'.", false)
|
||||
|
||||
proc validatePackageStructure(pkgInfo: PackageInfo, options: Options) =
|
||||
## This ensures that a package's source code does not leak into
|
||||
## another package's namespace.
|
||||
## https://github.com/nim-lang/nimble/issues/144
|
||||
let
|
||||
realDir = pkgInfo.getRealDir()
|
||||
normalizedBinNames = pkgInfo.bin.map(
|
||||
(x) => x.changeFileExt("").toLowerAscii()
|
||||
)
|
||||
correctDir =
|
||||
if pkgInfo.name.toLowerAscii() in normalizedBinNames:
|
||||
pkgInfo.name & "pkg"
|
||||
else:
|
||||
pkgInfo.name
|
||||
|
||||
proc onFile(path: string) =
|
||||
# Remove the root to leave only the package subdirectories.
|
||||
# ~/package-0.1/package/utils.nim -> package/utils.nim.
|
||||
var trailPath = changeRoot(realDir, "", path)
|
||||
if trailPath.startsWith(DirSep): trailPath = trailPath[1 .. ^1]
|
||||
let (dir, file, ext) = trailPath.splitFile
|
||||
# We're only interested in nim files, because only they can pollute our
|
||||
# namespace.
|
||||
if ext != (ExtSep & "nim"):
|
||||
return
|
||||
|
||||
if dir.len == 0:
|
||||
if file != pkgInfo.name:
|
||||
# A source file was found in the top level of srcDir that doesn't share
|
||||
# a name with the package.
|
||||
let
|
||||
msg = ("Package '$1' has an incorrect structure. " &
|
||||
"The top level of the package source directory " &
|
||||
"should contain at most one module, " &
|
||||
"named '$2', but a file named '$3' was found. This " &
|
||||
"will be an error in the future.") %
|
||||
[pkgInfo.name, pkgInfo.name & ext, file & ext]
|
||||
hint = ("If this is the primary source file in the package, " &
|
||||
"rename it to '$1'. If it's a source file required by " &
|
||||
"the main module, or if it is one of several " &
|
||||
"modules exposed by '$4', then move it into a '$2' subdirectory. " &
|
||||
"If it's a test file or otherwise not required " &
|
||||
"to build the the package '$1', prevent its installation " &
|
||||
"by adding `skipFiles = @[\"$3\"]` to the .nimble file. See " &
|
||||
"https://github.com/nim-lang/nimble#libraries for more info.") %
|
||||
[pkgInfo.name & ext, correctDir & DirSep, file & ext, pkgInfo.name]
|
||||
raiseNewValidationError(msg, true, hint, true)
|
||||
else:
|
||||
assert(not pkgInfo.isMinimal)
|
||||
# On Windows `pkgInfo.bin` has a .exe extension, so we need to normalize.
|
||||
if not (dir.startsWith(correctDir & DirSep) or dir == correctDir):
|
||||
let
|
||||
msg = ("Package '$2' has an incorrect structure. " &
|
||||
"It should contain a single directory hierarchy " &
|
||||
"for source files, named '$3', but file '$1' " &
|
||||
"is in a directory named '$4' instead. " &
|
||||
"This will be an error in the future.") %
|
||||
[file & ext, pkgInfo.name, correctDir, dir]
|
||||
hint = ("If '$1' contains source files for building '$2', rename it " &
|
||||
"to '$3'. Otherwise, prevent its installation " &
|
||||
"by adding `skipDirs = @[\"$1\"]` to the .nimble file.") %
|
||||
[dir, pkgInfo.name, correctDir]
|
||||
raiseNewValidationError(msg, true, hint, true)
|
||||
|
||||
iterInstallFiles(realDir, pkgInfo, options, onFile)
|
||||
|
||||
proc validatePackageInfo(pkgInfo: PackageInfo, options: Options) =
|
||||
let path = pkgInfo.myPath
|
||||
if pkgInfo.name == "":
|
||||
raiseNewValidationError("Incorrect .nimble file: " & path &
|
||||
" does not contain a name field.", false)
|
||||
|
||||
if pkgInfo.name.normalize != path.splitFile.name.normalize:
|
||||
raiseNewValidationError(
|
||||
"The .nimble file name must match name specified inside " & path, true)
|
||||
|
||||
if pkgInfo.version == "":
|
||||
raiseNewValidationError("Incorrect .nimble file: " & path &
|
||||
" does not contain a version field.", false)
|
||||
|
||||
if not pkgInfo.isMinimal:
|
||||
if pkgInfo.author == "":
|
||||
raiseNewValidationError("Incorrect .nimble file: " & path &
|
||||
" does not contain an author field.", false)
|
||||
if pkgInfo.description == "":
|
||||
raiseNewValidationError("Incorrect .nimble file: " & path &
|
||||
" does not contain a description field.", false)
|
||||
if pkgInfo.license == "":
|
||||
raiseNewValidationError("Incorrect .nimble file: " & path &
|
||||
" does not contain a license field.", false)
|
||||
if pkgInfo.backend notin ["c", "cc", "objc", "cpp", "js"]:
|
||||
raiseNewValidationError("'" & pkgInfo.backend &
|
||||
"' is an invalid backend.", false)
|
||||
|
||||
validatePackageStructure(pkginfo, options)
|
||||
|
||||
|
||||
proc nimScriptHint*(pkgInfo: PackageInfo) =
|
||||
if not pkgInfo.isNimScript:
|
||||
display("Warning:", "The .nimble file for this project could make use of " &
|
||||
"additional features, if converted into the new NimScript format." &
|
||||
"\nFor more details see:" &
|
||||
"https://github.com/nim-lang/nimble#creating-packages",
|
||||
Warning, HighPriority)
|
||||
|
||||
proc multiSplit(s: string): seq[string] =
|
||||
## Returns ``s`` split by newline and comma characters.
|
||||
##
|
||||
## Before returning, all individual entries are stripped of whitespace and
|
||||
## also empty entries are purged from the list. If after all the cleanups are
|
||||
## done no entries are found in the list, the proc returns a sequence with
|
||||
## the original string as the only entry.
|
||||
result = split(s, {char(0x0A), char(0x0D), ','})
|
||||
apply(result, proc(x: var string) = x = x.strip())
|
||||
for i in countdown(result.len()-1, 0):
|
||||
if len(result[i]) < 1:
|
||||
result.del(i)
|
||||
# Huh, nothing to return? Return given input.
|
||||
if len(result) < 1:
|
||||
if s.strip().len != 0:
|
||||
return @[s]
|
||||
else:
|
||||
return @[]
|
||||
|
||||
proc readPackageInfoFromNimble(path: string; result: var PackageInfo) =
|
||||
var fs = newFileStream(path, fmRead)
|
||||
if fs != nil:
|
||||
var p: CfgParser
|
||||
open(p, fs, path)
|
||||
defer: close(p)
|
||||
var currentSection = ""
|
||||
while true:
|
||||
var ev = next(p)
|
||||
case ev.kind
|
||||
of cfgEof:
|
||||
break
|
||||
of cfgSectionStart:
|
||||
currentSection = ev.section
|
||||
of cfgKeyValuePair:
|
||||
case currentSection.normalize
|
||||
of "package":
|
||||
case ev.key.normalize
|
||||
of "name": result.name = ev.value
|
||||
of "version": result.version = ev.value
|
||||
of "author": result.author = ev.value
|
||||
of "description": result.description = ev.value
|
||||
of "license": result.license = ev.value
|
||||
of "srcdir": result.srcDir = ev.value
|
||||
of "bindir": result.binDir = ev.value
|
||||
of "skipdirs":
|
||||
result.skipDirs.add(ev.value.multiSplit)
|
||||
of "skipfiles":
|
||||
result.skipFiles.add(ev.value.multiSplit)
|
||||
of "skipext":
|
||||
result.skipExt.add(ev.value.multiSplit)
|
||||
of "installdirs":
|
||||
result.installDirs.add(ev.value.multiSplit)
|
||||
of "installfiles":
|
||||
result.installFiles.add(ev.value.multiSplit)
|
||||
of "installext":
|
||||
result.installExt.add(ev.value.multiSplit)
|
||||
of "bin":
|
||||
for i in ev.value.multiSplit:
|
||||
if i.splitFile().ext == ".nim":
|
||||
raise newException(NimbleError, "`bin` entry should not be a source file: " & i)
|
||||
result.bin.add(i.addFileExt(ExeExt))
|
||||
of "backend":
|
||||
result.backend = ev.value.toLowerAscii()
|
||||
case result.backend.normalize
|
||||
of "javascript": result.backend = "js"
|
||||
else: discard
|
||||
of "beforehooks":
|
||||
for i in ev.value.multiSplit:
|
||||
result.preHooks.incl(i.normalize)
|
||||
of "afterhooks":
|
||||
for i in ev.value.multiSplit:
|
||||
result.postHooks.incl(i.normalize)
|
||||
else:
|
||||
raise newException(NimbleError, "Invalid field: " & ev.key)
|
||||
of "deps", "dependencies":
|
||||
case ev.key.normalize
|
||||
of "requires":
|
||||
for v in ev.value.multiSplit:
|
||||
result.requires.add(parseRequires(v.strip))
|
||||
of "foreigndeps":
|
||||
for v in ev.value.multiSplit:
|
||||
result.foreignDeps.add(v.strip)
|
||||
else:
|
||||
raise newException(NimbleError, "Invalid field: " & ev.key)
|
||||
else: raise newException(NimbleError,
|
||||
"Invalid section: " & currentSection)
|
||||
of cfgOption: raise newException(NimbleError,
|
||||
"Invalid package info, should not contain --" & ev.value)
|
||||
of cfgError:
|
||||
raise newException(NimbleError, "Error parsing .nimble file: " & ev.msg)
|
||||
else:
|
||||
raise newException(ValueError, "Cannot open package info: " & path)
|
||||
|
||||
proc readPackageInfoFromNims(scriptName: string, options: Options,
|
||||
result: var PackageInfo) =
|
||||
let
|
||||
iniFile = getIniFile(scriptName, options)
|
||||
|
||||
if iniFile.fileExists():
|
||||
readPackageInfoFromNimble(iniFile, result)
|
||||
|
||||
proc inferInstallRules(pkgInfo: var PackageInfo, options: Options) =
|
||||
# Binary packages shouldn't install .nim files by default.
|
||||
# (As long as the package info doesn't explicitly specify what should be
|
||||
# installed.)
|
||||
let installInstructions =
|
||||
pkgInfo.installDirs.len + pkgInfo.installExt.len + pkgInfo.installFiles.len
|
||||
if installInstructions == 0 and pkgInfo.bin.len > 0:
|
||||
pkgInfo.skipExt.add("nim")
|
||||
|
||||
# When a package doesn't specify a `srcDir` it's fair to assume that
|
||||
# the .nim files are in the root of the package. So we can explicitly select
|
||||
# them and prevent the installation of anything else. The user can always
|
||||
# override this with `installFiles`.
|
||||
if pkgInfo.srcDir == "":
|
||||
if dirExists(pkgInfo.getRealDir() / pkgInfo.name):
|
||||
pkgInfo.installDirs.add(pkgInfo.name)
|
||||
if fileExists(pkgInfo.getRealDir() / pkgInfo.name.addFileExt("nim")):
|
||||
pkgInfo.installFiles.add(pkgInfo.name.addFileExt("nim"))
|
||||
|
||||
proc readPackageInfo*(nf: NimbleFile, options: Options,
|
||||
onlyMinimalInfo=false): PackageInfo =
|
||||
## Reads package info from the specified Nimble file.
|
||||
##
|
||||
## Attempts to read it using the "old" Nimble ini format first, if that
|
||||
## fails attempts to evaluate it as a nimscript file.
|
||||
##
|
||||
## If both fail then returns an error.
|
||||
##
|
||||
## When ``onlyMinimalInfo`` is true, only the `name` and `version` fields are
|
||||
## populated. The ``isNimScript`` field can also be relied on.
|
||||
##
|
||||
## This version uses a cache stored in ``options``, so calling it multiple
|
||||
## times on the same ``nf`` shouldn't require re-evaluation of the Nimble
|
||||
## file.
|
||||
|
||||
assert fileExists(nf)
|
||||
|
||||
# Check the cache.
|
||||
if options.pkgInfoCache.hasKey(nf):
|
||||
return options.pkgInfoCache[nf]
|
||||
|
||||
result = initPackageInfo(nf)
|
||||
let minimalInfo = getNameVersion(nf)
|
||||
|
||||
validatePackageName(nf.splitFile.name)
|
||||
|
||||
var success = false
|
||||
var iniError: ref NimbleError
|
||||
# Attempt ini-format first.
|
||||
try:
|
||||
readPackageInfoFromNimble(nf, result)
|
||||
success = true
|
||||
result.isNimScript = false
|
||||
except NimbleError:
|
||||
iniError = (ref NimbleError)(getCurrentException())
|
||||
|
||||
if not success:
|
||||
if onlyMinimalInfo:
|
||||
result.name = minimalInfo.name
|
||||
result.version = minimalInfo.version
|
||||
result.isNimScript = true
|
||||
result.isMinimal = true
|
||||
|
||||
# It's possible this proc will receive a .nimble-link file eventually,
|
||||
# I added this assert to hopefully make this error clear for everyone.
|
||||
let msg = "No version detected. Received nimble-link?"
|
||||
assert result.version.len > 0, msg
|
||||
else:
|
||||
try:
|
||||
readPackageInfoFromNims(nf, options, result)
|
||||
result.isNimScript = true
|
||||
except NimbleError as exc:
|
||||
if exc.hint.len > 0:
|
||||
raise
|
||||
let msg = "Could not read package info file in " & nf & ";\n" &
|
||||
" Reading as ini file failed with: \n" &
|
||||
" " & iniError.msg & ".\n" &
|
||||
" Evaluating as NimScript file failed with: \n" &
|
||||
" " & exc.msg & "."
|
||||
raise newException(NimbleError, msg)
|
||||
|
||||
# By default specialVersion is the same as version.
|
||||
result.specialVersion = result.version
|
||||
|
||||
# Only attempt to read a special version if `nf` is inside the $nimbleDir.
|
||||
if nf.startsWith(options.getNimbleDir()):
|
||||
# The package directory name may include a "special" version
|
||||
# (example #head). If so, it is given higher priority and therefore
|
||||
# overwrites the .nimble file's version.
|
||||
let version = parseVersionRange(minimalInfo.version)
|
||||
if version.kind == verSpecial:
|
||||
result.specialVersion = minimalInfo.version
|
||||
|
||||
# Apply rules to infer which files should/shouldn't be installed. See #469.
|
||||
inferInstallRules(result, options)
|
||||
|
||||
if not result.isMinimal:
|
||||
options.pkgInfoCache[nf] = result
|
||||
|
||||
# Validate the rest of the package info last.
|
||||
if not options.disableValidation:
|
||||
validateVersion(result.version)
|
||||
validatePackageInfo(result, options)
|
||||
|
||||
proc validate*(file: NimbleFile, options: Options,
|
||||
error: var ValidationError, pkgInfo: var PackageInfo): bool =
|
||||
try:
|
||||
pkgInfo = readPackageInfo(file, options)
|
||||
except ValidationError as exc:
|
||||
error = exc[]
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
proc getPkgInfoFromFile*(file: NimbleFile, options: Options): PackageInfo =
|
||||
## Reads the specified .nimble file and returns its data as a PackageInfo
|
||||
## object. Any validation errors are handled and displayed as warnings.
|
||||
try:
|
||||
result = readPackageInfo(file, options)
|
||||
except ValidationError:
|
||||
let exc = (ref ValidationError)(getCurrentException())
|
||||
if exc.warnAll:
|
||||
display("Warning:", exc.msg, Warning, HighPriority)
|
||||
display("Hint:", exc.hint, Warning, HighPriority)
|
||||
else:
|
||||
raise
|
||||
|
||||
proc getPkgInfo*(dir: string, options: Options): PackageInfo =
|
||||
## Find the .nimble file in ``dir`` and parses it, returning a PackageInfo.
|
||||
let nimbleFile = findNimbleFile(dir, true)
|
||||
return getPkgInfoFromFile(nimbleFile, options)
|
||||
|
||||
proc getInstalledPkgs*(libsDir: string, options: Options):
|
||||
seq[tuple[pkginfo: PackageInfo, meta: MetaData]] =
|
||||
## Gets a list of installed packages.
|
||||
##
|
||||
## ``libsDir`` is in most cases: ~/.nimble/pkgs/
|
||||
const
|
||||
readErrorMsg = "Installed package '$1@$2' is outdated or corrupt."
|
||||
validationErrorMsg = readErrorMsg & "\nPackage did not pass validation: $3"
|
||||
hintMsg = "The corrupted package will need to be removed manually. To fix" &
|
||||
" this error message, remove $1."
|
||||
|
||||
proc createErrorMsg(tmplt, path, msg: string): string =
|
||||
let (name, version) = getNameVersion(path)
|
||||
return tmplt % [name, version, msg]
|
||||
|
||||
display("Loading", "list of installed packages", priority = MediumPriority)
|
||||
|
||||
result = @[]
|
||||
for kind, path in walkDir(libsDir):
|
||||
if kind == pcDir:
|
||||
let nimbleFile = findNimbleFile(path, false)
|
||||
if nimbleFile != "":
|
||||
let meta = readMetaData(path)
|
||||
var pkg: PackageInfo
|
||||
try:
|
||||
pkg = readPackageInfo(nimbleFile, options, onlyMinimalInfo=false)
|
||||
except ValidationError:
|
||||
let exc = (ref ValidationError)(getCurrentException())
|
||||
exc.msg = createErrorMsg(validationErrorMsg, path, exc.msg)
|
||||
exc.hint = hintMsg % path
|
||||
if exc.warnInstalled or exc.warnAll:
|
||||
display("Warning:", exc.msg, Warning, HighPriority)
|
||||
# Don't show hints here because they are only useful for package
|
||||
# owners.
|
||||
else:
|
||||
raise exc
|
||||
except:
|
||||
let tmplt = readErrorMsg & "\nMore info: $3"
|
||||
let msg = createErrorMsg(tmplt, path, getCurrentException().msg)
|
||||
var exc = newException(NimbleError, msg)
|
||||
exc.hint = hintMsg % path
|
||||
raise exc
|
||||
|
||||
pkg.isInstalled = true
|
||||
pkg.isLinked =
|
||||
cmpPaths(nimbleFile.splitFile().dir, path) != 0
|
||||
result.add((pkg, meta))
|
||||
|
||||
proc isNimScript*(nf: string, options: Options): bool =
|
||||
result = readPackageInfo(nf, options).isNimScript
|
||||
|
||||
proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo =
|
||||
if pkg.isMinimal:
|
||||
result = getPkgInfoFromFile(pkg.mypath, options)
|
||||
result.isInstalled = pkg.isInstalled
|
||||
result.isLinked = pkg.isLinked
|
||||
else:
|
||||
return pkg
|
||||
|
||||
proc getConcreteVersion*(pkgInfo: PackageInfo, options: Options): string =
|
||||
## Returns a non-special version from the specified ``pkgInfo``. If the
|
||||
## ``pkgInfo`` is minimal it looks it up and retrieves the concrete version.
|
||||
result = pkgInfo.version
|
||||
if pkgInfo.isMinimal:
|
||||
let pkgInfo = pkgInfo.toFullInfo(options)
|
||||
result = pkgInfo.version
|
||||
assert(not newVersion(result).isSpecial)
|
||||
|
||||
when isMainModule:
|
||||
validatePackageName("foo_bar")
|
||||
validatePackageName("f_oo_b_a_r")
|
||||
try:
|
||||
validatePackageName("foo__bar")
|
||||
assert false
|
||||
except NimbleError:
|
||||
assert true
|
||||
|
||||
echo("Everything passed!")
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
## This module implements operations with file system paths in a way independent
|
||||
## of weather the path is absolute or relative to the current directory.
|
||||
|
||||
import os, json, hashes
|
||||
|
||||
type Path* = distinct string
|
||||
|
||||
converter toPath*(path: string): Path = Path(path)
|
||||
|
||||
proc `%`*(path: Path): JsonNode {.borrow.}
|
||||
proc `$`*(path: Path): string {.borrow.}
|
||||
|
||||
proc isAbsolute*(path: Path): bool {.borrow.}
|
||||
proc splitFile*(path: Path): tuple[dir, name, ext: Path] {.borrow.}
|
||||
proc splitPath*(path: Path): tuple[head, tail: Path] {.borrow.}
|
||||
proc normalizedPath*(path: Path): Path {.borrow.}
|
||||
proc dirExists*(dirname: Path): bool {.borrow.}
|
||||
proc fileExists*(filename: Path): bool {.borrow.}
|
||||
proc parseFile*(filename: Path): JsonNode {.borrow.}
|
||||
proc `/`*(head, tail: Path): Path {.borrow.}
|
||||
proc writeFile*(filename: Path, content: string) {.borrow.}
|
||||
proc len*(path: Path): int {.borrow.}
|
||||
proc isRootDir*(path: Path): bool {.borrow.}
|
||||
proc parentDir*(path: Path): Path {.borrow.}
|
||||
proc quoteShell*(s: Path): Path {.borrow.}
|
||||
|
||||
proc hash*(path: Path): Hash = hash(absolutePath(string(path)))
|
||||
|
||||
proc `==`*(lhs, rhs: Path): bool =
|
||||
absolutePath(string(lhs)) == absolutePath(string(rhs))
|
||||
|
||||
when isMainModule:
|
||||
import unittest
|
||||
|
||||
const testDir: Path = "some/relative/path/"
|
||||
let absolutePathToTestDir: Path = getCurrentDir() / testDir
|
||||
|
||||
test "hashing":
|
||||
check hash(testDir) == hash(absolutePathToTestDir)
|
||||
|
||||
test "equals":
|
||||
check testDir == absolutePathToTestDir
|
|
@ -0,0 +1,229 @@
|
|||
# Copyright (C) Andreas Rumpf. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
## Implements 'nimble publish' to create a pull request against
|
||||
## nim-lang/packages automatically.
|
||||
|
||||
import system except TResult
|
||||
import httpclient, strutils, json, os, browsers, times, uri
|
||||
import version, tools, common, cli, config, options
|
||||
|
||||
type
|
||||
Auth = object
|
||||
user: string
|
||||
token: string ## Github access token
|
||||
http: HttpClient ## http client for doing API requests
|
||||
|
||||
const
|
||||
ApiKeyFile = "github_api_token"
|
||||
ApiTokenEnvironmentVariable = "NIMBLE_GITHUB_API_TOKEN"
|
||||
ReposUrl = "https://api.github.com/repos/"
|
||||
|
||||
proc userAborted() =
|
||||
raise newException(NimbleError, "User aborted the process.")
|
||||
|
||||
proc createHeaders(a: Auth) =
|
||||
a.http.headers = newHttpHeaders({
|
||||
"Authorization": "token $1" % a.token,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "*/*"
|
||||
})
|
||||
|
||||
proc requestNewToken(cfg: Config): string =
|
||||
display("Info:", "Please create a new personal access token on Github in" &
|
||||
" order to allow Nimble to fork the packages repository.",
|
||||
priority = HighPriority)
|
||||
display("Hint:", "Make sure to give the access token access to public repos" &
|
||||
" (public_repo scope)!", Warning, HighPriority)
|
||||
sleep(5000)
|
||||
display("Info:", "Your default browser should open with the following URL: " &
|
||||
"https://github.com/settings/tokens/new", priority = HighPriority)
|
||||
sleep(3000)
|
||||
openDefaultBrowser("https://github.com/settings/tokens/new")
|
||||
let token = promptCustom("Personal access token?", "").strip()
|
||||
# inform the user that their token will be written to disk
|
||||
let tokenWritePath = cfg.nimbleDir / ApiKeyFile
|
||||
display("Info:", "Writing access token to file:" & tokenWritePath,
|
||||
priority = HighPriority)
|
||||
writeFile(tokenWritePath, token)
|
||||
sleep(3000)
|
||||
return token
|
||||
|
||||
proc getGithubAuth(o: Options): Auth =
|
||||
let cfg = o.config
|
||||
result.http = newHttpClient(proxy = getProxy(o))
|
||||
# always prefer the environment variable to asking for a new one
|
||||
if existsEnv(ApiTokenEnvironmentVariable):
|
||||
result.token = getEnv(ApiTokenEnvironmentVariable)
|
||||
display("Info:", "Using the '" & ApiTokenEnvironmentVariable &
|
||||
"' environment variable for the GitHub API Token.",
|
||||
priority = HighPriority)
|
||||
else:
|
||||
# try to read from disk, if it cannot be found write a new one
|
||||
try:
|
||||
let apiTokenFilePath = cfg.nimbleDir / ApiKeyFile
|
||||
result.token = readFile(apiTokenFilePath).strip()
|
||||
display("Info:", "Using GitHub API Token in file: " & apiTokenFilePath,
|
||||
priority = HighPriority)
|
||||
except IOError:
|
||||
result.token = requestNewToken(cfg)
|
||||
createHeaders(result)
|
||||
let resp = result.http.getContent("https://api.github.com/user").parseJson()
|
||||
|
||||
result.user = resp["login"].str
|
||||
display("Success:", "Verified as " & result.user, Success, HighPriority)
|
||||
|
||||
proc isCorrectFork(j: JsonNode): bool =
|
||||
# Check whether this is a fork of the nimble packages repo.
|
||||
result = false
|
||||
if j{"fork"}.getBool():
|
||||
result = j{"parent"}{"full_name"}.getStr() == "nim-lang/packages"
|
||||
|
||||
proc forkExists(a: Auth): bool =
|
||||
try:
|
||||
let x = a.http.getContent(ReposUrl & a.user & "/packages")
|
||||
let j = parseJson(x)
|
||||
result = isCorrectFork(j)
|
||||
except JsonParsingError, IOError:
|
||||
result = false
|
||||
|
||||
proc createFork(a: Auth) =
|
||||
try:
|
||||
discard a.http.postContent(ReposUrl & "nim-lang/packages/forks")
|
||||
except HttpRequestError:
|
||||
raise newException(NimbleError, "Unable to create fork. Access token" &
|
||||
" might not have enough permissions.")
|
||||
|
||||
proc createPullRequest(a: Auth, packageName, branch: string): string =
|
||||
display("Info", "Creating PR", priority = HighPriority)
|
||||
var body = a.http.postContent(ReposUrl & "nim-lang/packages/pulls",
|
||||
body="""{"title": "Add package $1", "head": "$2:$3",
|
||||
"base": "master"}""" % [packageName, a.user, branch])
|
||||
var pr = parseJson(body)
|
||||
return pr{"html_url"}.getStr()
|
||||
|
||||
proc `%`(s: openArray[string]): JsonNode =
|
||||
result = newJArray()
|
||||
for x in s: result.add(%x)
|
||||
|
||||
proc cleanupWhitespace(s: string): string =
|
||||
## Removes trailing whitespace and normalizes line endings to LF.
|
||||
result = newStringOfCap(s.len)
|
||||
var i = 0
|
||||
while i < s.len:
|
||||
if s[i] == ' ':
|
||||
var j = i+1
|
||||
while s[j] == ' ': inc j
|
||||
if s[j] == '\c':
|
||||
inc j
|
||||
if s[j] == '\L': inc j
|
||||
result.add '\L'
|
||||
i = j
|
||||
elif s[j] == '\L':
|
||||
result.add '\L'
|
||||
i = j+1
|
||||
else:
|
||||
result.add ' '
|
||||
inc i
|
||||
elif s[i] == '\c':
|
||||
inc i
|
||||
if s[i] == '\L': inc i
|
||||
result.add '\L'
|
||||
elif s[i] == '\L':
|
||||
result.add '\L'
|
||||
inc i
|
||||
else:
|
||||
result.add s[i]
|
||||
inc i
|
||||
if result[^1] != '\L':
|
||||
result.add '\L'
|
||||
|
||||
proc editJson(p: PackageInfo; url, tags, downloadMethod: string) =
|
||||
var contents = parseFile("packages.json")
|
||||
doAssert contents.kind == JArray
|
||||
contents.add(%*{
|
||||
"name": p.name,
|
||||
"url": url,
|
||||
"method": downloadMethod,
|
||||
"tags": tags.split(),
|
||||
"description": p.description,
|
||||
"license": p.license,
|
||||
"web": url
|
||||
})
|
||||
writeFile("packages.json", contents.pretty.cleanupWhitespace)
|
||||
|
||||
proc publish*(p: PackageInfo, o: Options) =
|
||||
## Publishes the package p.
|
||||
let auth = getGithubAuth(o)
|
||||
var pkgsDir = getNimbleUserTempDir() / "nimble-packages-fork"
|
||||
if not forkExists(auth):
|
||||
createFork(auth)
|
||||
display("Info:", "Waiting 10s to let Github create a fork",
|
||||
priority = HighPriority)
|
||||
os.sleep(10_000)
|
||||
|
||||
display("Info:", "Finished waiting", priority = LowPriority)
|
||||
if dirExists(pkgsDir):
|
||||
display("Removing", "old packages fork git directory.",
|
||||
priority = LowPriority)
|
||||
removeDir(pkgsDir)
|
||||
createDir(pkgsDir)
|
||||
cd pkgsDir:
|
||||
# Avoid git clone to prevent token from being stored in repo
|
||||
# https://github.com/blog/1270-easier-builds-and-deployments-using-git-over-https-and-oauth
|
||||
display("Copying", "packages fork into: " & pkgsDir, priority = HighPriority)
|
||||
doCmd("git init")
|
||||
doCmd("git pull https://github.com/" & auth.user & "/packages")
|
||||
# Make sure to update the fork
|
||||
display("Updating", "the fork", priority = HighPriority)
|
||||
doCmd("git pull https://github.com/nim-lang/packages.git master")
|
||||
doCmd("git push https://" & auth.token & "@github.com/" & auth.user & "/packages master")
|
||||
|
||||
if not dirExists(pkgsDir):
|
||||
raise newException(NimbleError,
|
||||
"Cannot find nimble-packages-fork git repository. Cloning failed.")
|
||||
|
||||
if not fileExists(pkgsDir / "packages.json"):
|
||||
raise newException(NimbleError,
|
||||
"No packages file found in cloned fork.")
|
||||
|
||||
# We need to do this **before** the cd:
|
||||
# Determine what type of repo this is.
|
||||
var url = ""
|
||||
var downloadMethod = ""
|
||||
if dirExists(os.getCurrentDir() / ".git"):
|
||||
let (output, exitCode) = doCmdEx("git ls-remote --get-url")
|
||||
if exitCode == 0:
|
||||
url = output.string.strip
|
||||
if url.endsWith(".git"): url.setLen(url.len - 4)
|
||||
downloadMethod = "git"
|
||||
let parsed = parseUri(url)
|
||||
if parsed.scheme == "":
|
||||
# Assuming that we got an ssh write/read URL.
|
||||
let sshUrl = parseUri("ssh://" & url)
|
||||
url = "https://" & sshUrl.hostname & "/" & sshUrl.port & sshUrl.path
|
||||
elif dirExists(os.getCurrentDir() / ".hg"):
|
||||
downloadMethod = "hg"
|
||||
# TODO: Retrieve URL from hg.
|
||||
else:
|
||||
raise newException(NimbleError,
|
||||
"No .git nor .hg directory found. Stopping.")
|
||||
|
||||
if url.len == 0:
|
||||
url = promptCustom("Github URL of " & p.name & "?", "")
|
||||
if url.len == 0: userAborted()
|
||||
|
||||
let tags = promptCustom(
|
||||
"Whitespace separated list of tags? (For example: web library wrapper)",
|
||||
""
|
||||
)
|
||||
|
||||
cd pkgsDir:
|
||||
editJson(p, url, tags, downloadMethod)
|
||||
let branchName = "add-" & p.name & getTime().utc.format("HHmm")
|
||||
doCmd("git checkout -B " & branchName)
|
||||
doCmd("git commit packages.json -m \"Added package " & p.name & "\"")
|
||||
display("Pushing", "to remote of fork.", priority = HighPriority)
|
||||
doCmd("git push https://" & auth.token & "@github.com/" & auth.user & "/packages " & branchName)
|
||||
let prUrl = createPullRequest(auth, p.name, branchName)
|
||||
display("Success:", "Pull request successful, check at " & prUrl , Success, HighPriority)
|
|
@ -0,0 +1,135 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
import os, json, sets
|
||||
|
||||
import options, common, version, download, packageinfo
|
||||
|
||||
proc saveNimbleData*(options: Options) =
|
||||
# TODO: This file should probably be locked.
|
||||
writeFile(options.getNimbleDir() / "nimbledata.json",
|
||||
pretty(options.nimbleData))
|
||||
|
||||
proc addRevDep*(nimbleData: JsonNode, dep: tuple[name, version: string],
|
||||
pkg: PackageInfo) =
|
||||
# Add a record which specifies that `pkg` has a dependency on `dep`, i.e.
|
||||
# the reverse dependency of `dep` is `pkg`.
|
||||
if not nimbleData["reverseDeps"].hasKey(dep.name):
|
||||
nimbleData["reverseDeps"][dep.name] = newJObject()
|
||||
if not nimbleData["reverseDeps"][dep.name].hasKey(dep.version):
|
||||
nimbleData["reverseDeps"][dep.name][dep.version] = newJArray()
|
||||
let revDep = %{ "name": %pkg.name, "version": %pkg.specialVersion}
|
||||
let thisDep = nimbleData["reverseDeps"][dep.name][dep.version]
|
||||
if revDep notin thisDep:
|
||||
thisDep.add revDep
|
||||
|
||||
proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) =
|
||||
## Removes ``pkg`` from the reverse dependencies of every package.
|
||||
assert(not pkg.isMinimal)
|
||||
proc remove(pkg: PackageInfo, depTup: PkgTuple, thisDep: JsonNode) =
|
||||
for ver, val in thisDep:
|
||||
if ver.newVersion in depTup.ver:
|
||||
var newVal = newJArray()
|
||||
for revDep in val:
|
||||
if not (revDep["name"].str == pkg.name and
|
||||
revDep["version"].str == pkg.specialVersion):
|
||||
newVal.add revDep
|
||||
thisDep[ver] = newVal
|
||||
|
||||
for depTup in pkg.requires:
|
||||
if depTup.name.isURL():
|
||||
# We sadly must go through everything in this case...
|
||||
for key, val in nimbleData["reverseDeps"]:
|
||||
remove(pkg, depTup, val)
|
||||
else:
|
||||
let thisDep = nimbleData{"reverseDeps", depTup.name}
|
||||
if thisDep.isNil: continue
|
||||
remove(pkg, depTup, thisDep)
|
||||
|
||||
# Clean up empty objects/arrays
|
||||
var newData = newJObject()
|
||||
for key, val in nimbleData["reverseDeps"]:
|
||||
if val.len != 0:
|
||||
var newVal = newJObject()
|
||||
for ver, elem in val:
|
||||
if elem.len != 0:
|
||||
newVal[ver] = elem
|
||||
if newVal.len != 0:
|
||||
newData[key] = newVal
|
||||
nimbleData["reverseDeps"] = newData
|
||||
|
||||
proc getRevDepTups*(options: Options, pkg: PackageInfo): seq[PkgTuple] =
|
||||
## Returns a list of *currently installed* reverse dependencies for `pkg`.
|
||||
result = @[]
|
||||
let thisPkgsDep =
|
||||
options.nimbleData["reverseDeps"]{pkg.name}{pkg.specialVersion}
|
||||
if not thisPkgsDep.isNil:
|
||||
let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options)
|
||||
for pkg in thisPkgsDep:
|
||||
let pkgTup = (
|
||||
name: pkg["name"].getStr(),
|
||||
ver: parseVersionRange(pkg["version"].getStr())
|
||||
)
|
||||
var pkgInfo: PackageInfo
|
||||
if not findPkg(pkgList, pkgTup, pkgInfo):
|
||||
continue
|
||||
|
||||
result.add(pkgTup)
|
||||
|
||||
proc getRevDeps*(options: Options, pkg: PackageInfo): HashSet[PackageInfo] =
|
||||
result.init()
|
||||
let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options)
|
||||
for rdepTup in getRevDepTups(options, pkg):
|
||||
for rdepInfo in findAllPkgs(installedPkgs, rdepTup):
|
||||
result.incl rdepInfo
|
||||
|
||||
proc getAllRevDeps*(options: Options, pkg: PackageInfo, result: var HashSet[PackageInfo]) =
|
||||
if pkg in result:
|
||||
return
|
||||
|
||||
let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options)
|
||||
for rdepTup in getRevDepTups(options, pkg):
|
||||
for rdepInfo in findAllPkgs(installedPkgs, rdepTup):
|
||||
if rdepInfo in result:
|
||||
continue
|
||||
|
||||
getAllRevDeps(options, rdepInfo, result)
|
||||
result.incl pkg
|
||||
|
||||
when isMainModule:
|
||||
var nimbleData = %{"reverseDeps": newJObject()}
|
||||
|
||||
let nimforum1 = PackageInfo(
|
||||
isMinimal: false,
|
||||
name: "nimforum",
|
||||
specialVersion: "0.1.0",
|
||||
requires: @[("jester", parseVersionRange("0.1.0")),
|
||||
("captcha", parseVersionRange("1.0.0")),
|
||||
("auth", parseVersionRange("#head"))]
|
||||
)
|
||||
let nimforum2 = PackageInfo(isMinimal: false, name: "nimforum", specialVersion: "0.2.0")
|
||||
let play = PackageInfo(isMinimal: false, name: "play", specialVersion: "#head")
|
||||
|
||||
nimbleData.addRevDep(("jester", "0.1.0"), nimforum1)
|
||||
nimbleData.addRevDep(("jester", "0.1.0"), play)
|
||||
nimbleData.addRevDep(("captcha", "1.0.0"), nimforum1)
|
||||
nimbleData.addRevDep(("auth", "#head"), nimforum1)
|
||||
nimbleData.addRevDep(("captcha", "1.0.0"), nimforum2)
|
||||
nimbleData.addRevDep(("auth", "#head"), nimforum2)
|
||||
|
||||
doAssert nimbleData["reverseDeps"]["jester"]["0.1.0"].len == 2
|
||||
doAssert nimbleData["reverseDeps"]["captcha"]["1.0.0"].len == 2
|
||||
doAssert nimbleData["reverseDeps"]["auth"]["#head"].len == 2
|
||||
|
||||
block:
|
||||
nimbleData.removeRevDep(nimforum1)
|
||||
let jester = nimbleData["reverseDeps"]["jester"]["0.1.0"][0]
|
||||
doAssert jester["name"].getStr() == play.name
|
||||
doAssert jester["version"].getStr() == play.specialVersion
|
||||
|
||||
let captcha = nimbleData["reverseDeps"]["captcha"]["1.0.0"][0]
|
||||
doAssert captcha["name"].getStr() == nimforum2.name
|
||||
doAssert captcha["version"].getStr() == nimforum2.specialVersion
|
||||
|
||||
echo("Everything works!")
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
## This module implement operations on a special `sync` file which is being kept
|
||||
## in the hidden special VCS directory. It is used to keep the revisions of the
|
||||
## package's develop mode dependencies at the time when the last `lock` or
|
||||
## `sync` operation had been performed. The file is used to determine whether a
|
||||
## new `lock` or `sync` command or a VCS `merge` or `rebase` command is needed
|
||||
## when there is a conflict between the data written in it and the data from the
|
||||
## lock file and from the working copy.
|
||||
|
||||
import tables, json, os
|
||||
import common, sha1hashes, paths, vcstools, packageinfotypes
|
||||
|
||||
type
|
||||
SyncFileData = Table[string, Sha1Hash]
|
||||
# Maps develop mode dependency name to the VCS revision it has in the time
|
||||
# of the last `lock` or `sync` operation or when it is added as a develop
|
||||
# mode dependency if there is no such operations after that moment.
|
||||
|
||||
SyncFile = object
|
||||
path: Path
|
||||
data: SyncFileData
|
||||
|
||||
SyncFileJsonKeys = enum
|
||||
## Represents the keys for the `sync` file Json objects.
|
||||
lsfjkVersion = "version"
|
||||
lsfjkData = "data"
|
||||
|
||||
const
|
||||
syncFileExt = ".nimble.sync"
|
||||
syncFileVersion = 1
|
||||
|
||||
proc getPkgDir(pkgInfo: PackageInfo): string =
|
||||
pkgInfo.myPath.splitFile.dir
|
||||
|
||||
proc getSyncFilePath(pkgInfo: PackageInfo): Path =
|
||||
## Returns a path to the sync file for package `pkgInfo`.
|
||||
|
||||
let (vcsType, vcsSpecialDirPath) =
|
||||
# Do not use `pkgInfo.getNimbleFileDir` in order to avoid circular
|
||||
# dependencies.
|
||||
getVcsTypeAndSpecialDirPath(pkgInfo.getPkgDir)
|
||||
|
||||
if vcsType == vcsTypeNone:
|
||||
# The directory is not under version control, and we have not a place where
|
||||
# to hide the sync file.
|
||||
raise nimbleError(
|
||||
msg = "Sync file require current working directory to be under some " &
|
||||
"supported type of version control.",
|
||||
hint = "Put package's working directory under version control.")
|
||||
|
||||
return vcsSpecialDirPath / (pkgInfo.basicInfo.name & syncFileExt).Path
|
||||
|
||||
proc load(syncFile: ref SyncFile, path: Path) =
|
||||
## Loads a sync file.
|
||||
|
||||
syncFile.path = path
|
||||
if not path.fileExists:
|
||||
return
|
||||
|
||||
{.warning[UnsafeDefault]: off.}
|
||||
{.warning[ProveInit]: off.}
|
||||
syncFile.data = parseFile(path)[$lsfjkData].to(SyncFileData)
|
||||
{.warning[ProveInit]: on.}
|
||||
{.warning[UnsafeDefault]: on.}
|
||||
|
||||
proc getSyncFile*(pkgInfo: PackageInfo): ref SyncFile =
|
||||
# Returns a reference to the sync file data of the current working directory
|
||||
# package `pkgInfo`.
|
||||
|
||||
assert pkgInfo.getPkgDir == getCurrentDir():
|
||||
"The package `pkgInfo` must be the current working directory package."
|
||||
|
||||
var syncFile {.global.}: ref SyncFile
|
||||
once:
|
||||
syncFile.new
|
||||
let path = getSyncFilePath(pkgInfo)
|
||||
syncFile.load(path)
|
||||
return syncFile
|
||||
|
||||
proc save*(syncFile: ref SyncFile) =
|
||||
## Saves a sync file.
|
||||
|
||||
let jsonNode = %{
|
||||
$lsfjkVersion: %syncFileVersion,
|
||||
$lsfjkData: %syncFile.data,
|
||||
}
|
||||
|
||||
writeFile(syncFile.path, jsonNode.pretty)
|
||||
|
||||
proc getDepVcsRevision*(syncFile: ref SyncFile, depName: string): Sha1Hash =
|
||||
## Returns the revision written in the sync file for develop mode dependency
|
||||
## `depName`.
|
||||
syncFile.data.getOrDefault(depName, notSetSha1Hash)
|
||||
|
||||
proc setDepVcsRevision*(syncFile: ref SyncFile, depName: string,
|
||||
vcsRevision: Sha1Hash) =
|
||||
## Sets the revision in the sync file for the develop mode dependency
|
||||
## `depName` to be equal to `vcsRevision`.
|
||||
|
||||
syncFile.data[depName] = vcsRevision
|
||||
|
||||
proc clear*(syncFile: ref SyncFile) =
|
||||
## Clears all the data from the sync file.
|
||||
|
||||
{.warning[UnsafeDefault]: off.}
|
||||
syncFile.data.clear
|
||||
{.warning[UnsafeDefault]: on.}
|
|
@ -0,0 +1,181 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
#
|
||||
# Various miscellaneous utility functions reside here.
|
||||
import osproc, pegs, strutils, os, uri, sets, json, parseutils
|
||||
import version, cli
|
||||
|
||||
proc extractBin(cmd: string): string =
|
||||
if cmd[0] == '"':
|
||||
return cmd.captureBetween('"')
|
||||
else:
|
||||
return cmd.split(' ')[0]
|
||||
|
||||
proc doCmd*(cmd: string, showOutput = false) =
|
||||
let bin = extractBin(cmd)
|
||||
if findExe(bin) == "":
|
||||
raise newException(NimbleError, "'" & bin & "' not in PATH.")
|
||||
|
||||
# To keep output in sequence
|
||||
stdout.flushFile()
|
||||
stderr.flushFile()
|
||||
|
||||
displayDebug("Executing", cmd)
|
||||
if showOutput:
|
||||
let exitCode = execCmd(cmd)
|
||||
displayDebug("Finished", "with exit code " & $exitCode)
|
||||
if exitCode != QuitSuccess:
|
||||
raise newException(NimbleError,
|
||||
"Execution failed with exit code $1\nCommand: $2" %
|
||||
[$exitCode, cmd])
|
||||
else:
|
||||
let (output, exitCode) = execCmdEx(cmd)
|
||||
displayDebug("Finished", "with exit code " & $exitCode)
|
||||
displayDebug("Output", output)
|
||||
|
||||
if exitCode != QuitSuccess:
|
||||
raise newException(NimbleError,
|
||||
"Execution failed with exit code $1\nCommand: $2\nOutput: $3" %
|
||||
[$exitCode, cmd, output])
|
||||
|
||||
proc doCmdEx*(cmd: string): tuple[output: TaintedString, exitCode: int] =
|
||||
let bin = extractBin(cmd)
|
||||
if findExe(bin) == "":
|
||||
raise newException(NimbleError, "'" & bin & "' not in PATH.")
|
||||
return execCmdEx(cmd)
|
||||
|
||||
template cd*(dir: string, body: untyped) =
|
||||
## Sets the current dir to ``dir``, executes ``body`` and restores the
|
||||
## previous working dir.
|
||||
let lastDir = getCurrentDir()
|
||||
setCurrentDir(dir)
|
||||
body
|
||||
setCurrentDir(lastDir)
|
||||
|
||||
proc getNimBin*: string =
|
||||
result = "nim"
|
||||
if findExe("nim") != "": result = findExe("nim")
|
||||
elif findExe("nimrod") != "": result = findExe("nimrod")
|
||||
|
||||
proc getNimrodVersion*: Version =
|
||||
let nimBin = getNimBin()
|
||||
let vOutput = doCmdEx('"' & nimBin & "\" -v").output
|
||||
var matches: array[0..MaxSubpatterns, string]
|
||||
if vOutput.find(peg"'Version'\s{(\d+\.)+\d}", matches) == -1:
|
||||
raise newException(NimbleError, "Couldn't find Nim version.")
|
||||
newVersion(matches[0])
|
||||
|
||||
proc samePaths*(p1, p2: string): bool =
|
||||
## Normalizes path (by adding a trailing slash) and compares.
|
||||
var cp1 = if not p1.endsWith("/"): p1 & "/" else: p1
|
||||
var cp2 = if not p2.endsWith("/"): p2 & "/" else: p2
|
||||
cp1 = cp1.replace('/', DirSep).replace('\\', DirSep)
|
||||
cp2 = cp2.replace('/', DirSep).replace('\\', DirSep)
|
||||
|
||||
return cmpPaths(cp1, cp2) == 0
|
||||
|
||||
proc changeRoot*(origRoot, newRoot, path: string): string =
|
||||
## origRoot: /home/dom/
|
||||
## newRoot: /home/test/
|
||||
## path: /home/dom/bar/blah/2/foo.txt
|
||||
## Return value -> /home/test/bar/blah/2/foo.txt
|
||||
|
||||
## The additional check of `path.samePaths(origRoot)` is necessary to prevent
|
||||
## a regression, where by ending the `srcDir` defintion in a nimble file in a
|
||||
## trailing separator would cause the `path.startsWith(origRoot)` evaluation to
|
||||
## fail because of the value of `origRoot` would be longer than `path` due to
|
||||
## the trailing separator. This would cause this method to throw during package
|
||||
## installation.
|
||||
if path.startsWith(origRoot) or path.samePaths(origRoot):
|
||||
return newRoot / path.substr(origRoot.len, path.len-1)
|
||||
else:
|
||||
raise newException(ValueError,
|
||||
"Cannot change root of path: Path does not begin with original root.")
|
||||
|
||||
proc copyFileD*(fro, to: string): string =
|
||||
## Returns the destination (``to``).
|
||||
display("Copying", "file $# to $#" % [fro, to], priority = LowPriority)
|
||||
copyFileWithPermissions(fro, to)
|
||||
result = to
|
||||
|
||||
proc copyDirD*(fro, to: string): seq[string] =
|
||||
## Returns the filenames of the files in the directory that were copied.
|
||||
result = @[]
|
||||
display("Copying", "directory $# to $#" % [fro, to], priority = LowPriority)
|
||||
for path in walkDirRec(fro):
|
||||
createDir(changeRoot(fro, to, path.splitFile.dir))
|
||||
result.add copyFileD(path, changeRoot(fro, to, path))
|
||||
|
||||
proc createDirD*(dir: string) =
|
||||
display("Creating", "directory $#" % dir, priority = LowPriority)
|
||||
createDir(dir)
|
||||
|
||||
proc getDownloadDirName*(uri: string, verRange: VersionRange): string =
|
||||
## Creates a directory name based on the specified ``uri`` (url)
|
||||
result = ""
|
||||
let puri = parseUri(uri)
|
||||
for i in puri.hostname:
|
||||
case i
|
||||
of strutils.Letters, strutils.Digits:
|
||||
result.add i
|
||||
else: discard
|
||||
result.add "_"
|
||||
for i in puri.path:
|
||||
case i
|
||||
of strutils.Letters, strutils.Digits:
|
||||
result.add i
|
||||
else: discard
|
||||
|
||||
let verSimple = getSimpleString(verRange)
|
||||
if verSimple != "":
|
||||
result.add "_"
|
||||
result.add verSimple
|
||||
|
||||
proc incl*(s: var HashSet[string], v: seq[string] | HashSet[string]) =
|
||||
for i in v:
|
||||
s.incl i
|
||||
|
||||
when not declared(json.contains):
|
||||
proc contains*(j: JsonNode, elem: JsonNode): bool =
|
||||
for i in j:
|
||||
if i == elem:
|
||||
return true
|
||||
|
||||
proc contains*(j: JsonNode, elem: tuple[key: string, val: JsonNode]): bool =
|
||||
for key, val in pairs(j):
|
||||
if key == elem.key and val == elem.val:
|
||||
return true
|
||||
|
||||
when not defined(windows):
|
||||
from posix import getpid
|
||||
|
||||
proc getProcessId*(): string =
|
||||
when defined(windows):
|
||||
proc GetCurrentProcessId(): int32 {.stdcall, dynlib: "kernel32",
|
||||
importc: "GetCurrentProcessId".}
|
||||
result = $GetCurrentProcessId()
|
||||
else:
|
||||
result = $getpid()
|
||||
|
||||
proc getNimbleTempDir*(): string =
|
||||
## Returns a path to a temporary directory.
|
||||
##
|
||||
## The returned path will be the same for the duration of the process but
|
||||
## different for different runs of it. You have to make sure to create it
|
||||
## first. In release builds the directory will be removed when nimble finishes
|
||||
## its work.
|
||||
result = getTempDir() / "nimble_" & getProcessId()
|
||||
|
||||
proc getNimbleUserTempDir*(): string =
|
||||
## Returns a path to a temporary directory.
|
||||
##
|
||||
## The returned path will be the same for the duration of the process but
|
||||
## different for different runs of it. You have to make sure to create it
|
||||
## first. In release builds the directory will be removed when nimble finishes
|
||||
## its work.
|
||||
var tmpdir: string
|
||||
if existsEnv("TMPDIR") and existsEnv("USER"):
|
||||
tmpdir = joinPath(getEnv("TMPDIR"), getEnv("USER"))
|
||||
else:
|
||||
tmpdir = getTempDir()
|
||||
return tmpdir
|
|
@ -0,0 +1,169 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
import sequtils, tables, strformat, algorithm, sets
|
||||
import common, packageinfotypes, packageinfo, options, cli
|
||||
|
||||
proc getDependencies(packages: seq[PackageInfo], package: PackageInfo,
|
||||
options: Options):
|
||||
seq[string] =
|
||||
## Returns the names of the packages which are dependencies of a given
|
||||
## package. It is needed because some of the names of the packages in the
|
||||
## `requires` clause of a package could be URLs.
|
||||
for dep in package.requires:
|
||||
if dep.name.isNim:
|
||||
continue
|
||||
var depPkgInfo = initPackageInfo()
|
||||
var found = findPkg(packages, dep, depPkgInfo)
|
||||
if not found:
|
||||
let resolvedDep = dep.resolveAlias(options)
|
||||
found = findPkg(packages, resolvedDep, depPkgInfo)
|
||||
if not found:
|
||||
raise nimbleError(
|
||||
"Cannot build the dependency graph.\n" &
|
||||
&"Missing package \"{dep.name}\".")
|
||||
result.add depPkgInfo.basicInfo.name
|
||||
|
||||
proc buildDependencyGraph*(packages: seq[PackageInfo], options: Options):
|
||||
LockFileDeps =
|
||||
## Creates records which will be saved to the lock file.
|
||||
for pkgInfo in packages:
|
||||
result[pkgInfo.basicInfo.name] = LockFileDep(
|
||||
version: pkgInfo.basicInfo.version,
|
||||
vcsRevision: pkgInfo.metaData.vcsRevision,
|
||||
url: pkgInfo.metaData.url,
|
||||
downloadMethod: pkgInfo.metaData.downloadMethod,
|
||||
dependencies: getDependencies(packages, pkgInfo, options),
|
||||
checksums: Checksums(sha1: pkgInfo.basicInfo.checksum))
|
||||
|
||||
proc topologicalSort*(graph: LockFileDeps):
|
||||
tuple[order: seq[string], cycles: seq[seq[string]]] =
|
||||
## Topologically sorts dependency graph which will be saved to the lock file.
|
||||
##
|
||||
## Returns tuple containing sequence with the package names in the
|
||||
## topologically sorted order and another sequence with detected cyclic
|
||||
## dependencies if any (should not be such). Only cycles which don't have
|
||||
## edges part of another cycle are being detected (in the order of the
|
||||
## visiting).
|
||||
|
||||
type
|
||||
NodeMark = enum
|
||||
nmNotMarked
|
||||
nmTemporary
|
||||
nmPermanent
|
||||
|
||||
NodeInfo = tuple[mark: NodeMark, cameFrom: string]
|
||||
NodesInfo = OrderedTable[string, NodeInfo]
|
||||
|
||||
var
|
||||
order = newSeqOfCap[string](graph.len)
|
||||
cycles: seq[seq[string]]
|
||||
nodesInfo: NodesInfo
|
||||
|
||||
proc getCycle(finalNode: string): seq[string] =
|
||||
var
|
||||
path = newSeqOfCap[string](graph.len)
|
||||
previousNode = nodesInfo[finalNode].cameFrom
|
||||
|
||||
path.add finalNode
|
||||
while previousNode != finalNode:
|
||||
path.add previousNode
|
||||
previousNode = nodesInfo[previousNode].cameFrom
|
||||
|
||||
path.add previousNode
|
||||
path.reverse()
|
||||
|
||||
return path
|
||||
|
||||
proc printNotADagWarning() =
|
||||
let message = cycles.foldl(
|
||||
a & "\nCycle detected: " & b.foldl(&"{a} -> {b}"),
|
||||
"The dependency graph is not a DAG.")
|
||||
display("Warning", message, Warning, HighPriority)
|
||||
|
||||
for node, _ in graph:
|
||||
nodesInfo[node] = (mark: nmNotMarked, cameFrom: "")
|
||||
|
||||
proc visit(node: string) =
|
||||
template nodeInfo: untyped = nodesInfo[node]
|
||||
|
||||
if nodeInfo.mark == nmPermanent:
|
||||
return
|
||||
|
||||
if nodeInfo.mark == nmTemporary:
|
||||
cycles.add getCycle(node)
|
||||
return
|
||||
|
||||
nodeInfo.mark = nmTemporary
|
||||
|
||||
let neighbors = graph[node].dependencies
|
||||
for node2 in neighbors:
|
||||
nodesInfo[node2].cameFrom = node
|
||||
visit(node2)
|
||||
|
||||
nodeInfo.mark = nmPermanent
|
||||
order.add node
|
||||
|
||||
for node, nodeInfo in nodesInfo:
|
||||
if nodeInfo.mark != nmPermanent:
|
||||
visit(node)
|
||||
|
||||
if cycles.len > 0:
|
||||
printNotADagWarning()
|
||||
|
||||
return (order, cycles)
|
||||
|
||||
when isMainModule:
|
||||
import unittest
|
||||
from version import notSetVersion
|
||||
from sha1hashes import notSetSha1Hash
|
||||
|
||||
proc initLockFileDep(deps: seq[string] = @[]): LockFileDep =
|
||||
result = LockFileDep(
|
||||
version: notSetVersion,
|
||||
vcsRevision: notSetSha1Hash,
|
||||
dependencies: deps,
|
||||
checksums: Checksums(sha1: notSetSha1Hash))
|
||||
|
||||
suite "topological sort":
|
||||
|
||||
test "graph without cycles":
|
||||
let
|
||||
graph = {
|
||||
"json_serialization": initLockFileDep(
|
||||
@["serialization", "stew"]),
|
||||
"faststreams": initLockFileDep(@["stew"]),
|
||||
"testutils": initLockFileDep(),
|
||||
"stew": initLockFileDep(),
|
||||
"serialization": initLockFileDep(@["faststreams", "stew"]),
|
||||
"chronicles": initLockFileDep(
|
||||
@["json_serialization", "testutils"])
|
||||
}.toOrderedTable
|
||||
|
||||
expectedTopologicallySortedOrder = @[
|
||||
"stew", "faststreams", "serialization", "json_serialization",
|
||||
"testutils", "chronicles"]
|
||||
expectedCycles: seq[seq[string]] = @[]
|
||||
|
||||
(actualTopologicallySortedOrder, actualCycles) = topologicalSort(graph)
|
||||
|
||||
check actualTopologicallySortedOrder == expectedTopologicallySortedOrder
|
||||
check actualCycles == expectedCycles
|
||||
|
||||
test "graph with cycles":
|
||||
let
|
||||
graph = {
|
||||
"A": initLockFileDep(@["B", "E"]),
|
||||
"B": initLockFileDep(@["A", "C"]),
|
||||
"C": initLockFileDep(@["D"]),
|
||||
"D": initLockFileDep(@["B"]),
|
||||
"E": initLockFileDep(@["D", "E"])
|
||||
}.toOrderedTable
|
||||
|
||||
expectedTopologicallySortedOrder = @["D", "C", "B", "E", "A"]
|
||||
expectedCycles = @[@["A", "B", "A"], @["B", "C", "D", "B"], @["E", "E"]]
|
||||
|
||||
(actualTopologicallySortedOrder, actualCycles) = topologicalSort(graph)
|
||||
|
||||
check actualTopologicallySortedOrder == expectedTopologicallySortedOrder
|
||||
check actualCycles == expectedCycles
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,360 @@
|
|||
# Copyright (C) Dominik Picheta. All rights reserved.
|
||||
# BSD License. Look at license.txt for more info.
|
||||
|
||||
## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5``
|
||||
import strutils, tables, hashes, parseutils
|
||||
type
|
||||
Version* = distinct string
|
||||
|
||||
VersionRangeEnum* = enum
|
||||
verLater, # > V
|
||||
verEarlier, # < V
|
||||
verEqLater, # >= V -- Equal or later
|
||||
verEqEarlier, # <= V -- Equal or earlier
|
||||
verIntersect, # > V & < V
|
||||
verEq, # V
|
||||
verAny, # *
|
||||
verSpecial # #head
|
||||
|
||||
VersionRange* = ref VersionRangeObj
|
||||
VersionRangeObj = object
|
||||
case kind*: VersionRangeEnum
|
||||
of verLater, verEarlier, verEqLater, verEqEarlier, verEq:
|
||||
ver*: Version
|
||||
of verSpecial:
|
||||
spe*: Version
|
||||
of verIntersect:
|
||||
verILeft, verIRight: VersionRange
|
||||
of verAny:
|
||||
nil
|
||||
|
||||
## Tuple containing package name and version range.
|
||||
PkgTuple* = tuple[name: string, ver: VersionRange]
|
||||
|
||||
ParseVersionError* = object of ValueError
|
||||
NimbleError* = object of CatchableError
|
||||
hint*: string
|
||||
|
||||
proc `$`*(ver: Version): string {.borrow.}
|
||||
|
||||
proc hash*(ver: Version): Hash {.borrow.}
|
||||
|
||||
proc newVersion*(ver: string): Version =
|
||||
doAssert(ver.len == 0 or ver[0] in {'#', '\0'} + Digits,
|
||||
"Wrong version: " & ver)
|
||||
return Version(ver)
|
||||
|
||||
proc isSpecial*(ver: Version): bool =
|
||||
return ($ver).len > 0 and ($ver)[0] == '#'
|
||||
|
||||
proc `<`*(ver: Version, ver2: Version): bool =
|
||||
# Handling for special versions such as "#head" or "#branch".
|
||||
if ver.isSpecial or ver2.isSpecial:
|
||||
# TODO: This may need to be reverted. See #311.
|
||||
if ver2.isSpecial and ($ver2).normalize == "#head":
|
||||
return ($ver).normalize != "#head"
|
||||
|
||||
if not ver2.isSpecial:
|
||||
# `#aa111 < 1.1`
|
||||
return ($ver).normalize != "#head"
|
||||
|
||||
# Handling for normal versions such as "0.1.0" or "1.0".
|
||||
var sVer = string(ver).split('.')
|
||||
var sVer2 = string(ver2).split('.')
|
||||
for i in 0..max(sVer.len, sVer2.len)-1:
|
||||
var sVerI = 0
|
||||
if i < sVer.len:
|
||||
discard parseInt(sVer[i], sVerI)
|
||||
var sVerI2 = 0
|
||||
if i < sVer2.len:
|
||||
discard parseInt(sVer2[i], sVerI2)
|
||||
if sVerI < sVerI2:
|
||||
return true
|
||||
elif sVerI == sVerI2:
|
||||
discard
|
||||
else:
|
||||
return false
|
||||
|
||||
proc `==`*(ver: Version, ver2: Version): bool =
|
||||
if ver.isSpecial or ver2.isSpecial:
|
||||
return ($ver).toLowerAscii() == ($ver2).toLowerAscii()
|
||||
|
||||
var sVer = string(ver).split('.')
|
||||
var sVer2 = string(ver2).split('.')
|
||||
for i in 0..max(sVer.len, sVer2.len)-1:
|
||||
var sVerI = 0
|
||||
if i < sVer.len:
|
||||
discard parseInt(sVer[i], sVerI)
|
||||
var sVerI2 = 0
|
||||
if i < sVer2.len:
|
||||
discard parseInt(sVer2[i], sVerI2)
|
||||
if sVerI == sVerI2:
|
||||
result = true
|
||||
else:
|
||||
return false
|
||||
|
||||
proc cmp*(a, b: Version): int =
|
||||
if a < b: -1
|
||||
elif a > b: 1
|
||||
else: 0
|
||||
|
||||
proc `<=`*(ver: Version, ver2: Version): bool =
|
||||
return (ver == ver2) or (ver < ver2)
|
||||
|
||||
proc `==`*(range1: VersionRange, range2: VersionRange): bool =
|
||||
if range1.kind != range2.kind : return false
|
||||
result = case range1.kind
|
||||
of verLater, verEarlier, verEqLater, verEqEarlier, verEq:
|
||||
range1.ver == range2.ver
|
||||
of verSpecial:
|
||||
range1.spe == range2.spe
|
||||
of verIntersect:
|
||||
range1.verILeft == range2.verILeft and range1.verIRight == range2.verIRight
|
||||
of verAny: true
|
||||
|
||||
proc withinRange*(ver: Version, ran: VersionRange): bool =
|
||||
case ran.kind
|
||||
of verLater:
|
||||
return ver > ran.ver
|
||||
of verEarlier:
|
||||
return ver < ran.ver
|
||||
of verEqLater:
|
||||
return ver >= ran.ver
|
||||
of verEqEarlier:
|
||||
return ver <= ran.ver
|
||||
of verEq:
|
||||
return ver == ran.ver
|
||||
of verSpecial:
|
||||
return ver == ran.spe
|
||||
of verIntersect:
|
||||
return withinRange(ver, ran.verILeft) and withinRange(ver, ran.verIRight)
|
||||
of verAny:
|
||||
return true
|
||||
|
||||
proc contains*(ran: VersionRange, ver: Version): bool =
|
||||
return withinRange(ver, ran)
|
||||
|
||||
proc makeRange*(version: string, op: string): VersionRange =
|
||||
if version == "":
|
||||
raise newException(ParseVersionError,
|
||||
"A version needs to accompany the operator.")
|
||||
case op
|
||||
of ">":
|
||||
result = VersionRange(kind: verLater)
|
||||
of "<":
|
||||
result = VersionRange(kind: verEarlier)
|
||||
of ">=":
|
||||
result = VersionRange(kind: verEqLater)
|
||||
of "<=":
|
||||
result = VersionRange(kind: verEqEarlier)
|
||||
of "":
|
||||
result = VersionRange(kind: verEq)
|
||||
else:
|
||||
raise newException(ParseVersionError, "Invalid operator: " & op)
|
||||
result.ver = Version(version)
|
||||
|
||||
proc parseVersionRange*(s: string): VersionRange =
|
||||
# >= 1.5 & <= 1.8
|
||||
if s.len == 0:
|
||||
result = VersionRange(kind: verAny)
|
||||
return
|
||||
|
||||
if s[0] == '#':
|
||||
result = VersionRange(kind: verSpecial)
|
||||
result.spe = s.Version
|
||||
return
|
||||
|
||||
var i = 0
|
||||
var op = ""
|
||||
var version = ""
|
||||
while i < s.len:
|
||||
case s[i]
|
||||
of '>', '<', '=':
|
||||
op.add(s[i])
|
||||
of '&':
|
||||
result = VersionRange(kind: verIntersect)
|
||||
result.verILeft = makeRange(version, op)
|
||||
|
||||
# Parse everything after &
|
||||
# Recursion <3
|
||||
result.verIRight = parseVersionRange(substr(s, i + 1))
|
||||
|
||||
# Disallow more than one verIntersect. It's pointless and could lead to
|
||||
# major unpredictable mistakes.
|
||||
if result.verIRight.kind == verIntersect:
|
||||
raise newException(ParseVersionError,
|
||||
"Having more than one `&` in a version range is pointless")
|
||||
|
||||
return
|
||||
|
||||
of '0'..'9', '.':
|
||||
version.add(s[i])
|
||||
|
||||
of ' ':
|
||||
# Make sure '0.9 8.03' is not allowed.
|
||||
if version != "" and i < s.len - 1:
|
||||
if s[i+1] in {'0'..'9', '.'}:
|
||||
raise newException(ParseVersionError,
|
||||
"Whitespace is not allowed in a version literal.")
|
||||
|
||||
else:
|
||||
raise newException(ParseVersionError,
|
||||
"Unexpected char in version range '" & s & "': " & s[i])
|
||||
inc(i)
|
||||
result = makeRange(version, op)
|
||||
|
||||
proc toVersionRange*(ver: Version): VersionRange =
|
||||
## Converts a version to either a verEq or verSpecial VersionRange.
|
||||
new(result)
|
||||
if ver.isSpecial:
|
||||
result = VersionRange(kind: verSpecial)
|
||||
result.spe = ver
|
||||
else:
|
||||
result = VersionRange(kind: verEq)
|
||||
result.ver = ver
|
||||
|
||||
proc parseRequires*(req: string): PkgTuple =
|
||||
try:
|
||||
if ' ' in req:
|
||||
var i = skipUntil(req, Whitespace)
|
||||
result.name = req[0 .. i].strip
|
||||
result.ver = parseVersionRange(req[i .. req.len-1])
|
||||
elif '#' in req:
|
||||
var i = skipUntil(req, {'#'})
|
||||
result.name = req[0 .. i-1]
|
||||
result.ver = parseVersionRange(req[i .. req.len-1])
|
||||
else:
|
||||
result.name = req.strip
|
||||
result.ver = VersionRange(kind: verAny)
|
||||
except ParseVersionError:
|
||||
raise newException(NimbleError,
|
||||
"Unable to parse dependency version range: " & getCurrentExceptionMsg())
|
||||
|
||||
proc `$`*(verRange: VersionRange): string =
|
||||
case verRange.kind
|
||||
of verLater:
|
||||
result = "> "
|
||||
of verEarlier:
|
||||
result = "< "
|
||||
of verEqLater:
|
||||
result = ">= "
|
||||
of verEqEarlier:
|
||||
result = "<= "
|
||||
of verEq:
|
||||
result = ""
|
||||
of verSpecial:
|
||||
return $verRange.spe
|
||||
of verIntersect:
|
||||
return $verRange.verILeft & " & " & $verRange.verIRight
|
||||
of verAny:
|
||||
return "any version"
|
||||
|
||||
result.add(string(verRange.ver))
|
||||
|
||||
proc getSimpleString*(verRange: VersionRange): string =
|
||||
## Gets a string with no special symbols and spaces. Used for dir name
|
||||
## creation in tools.nim
|
||||
case verRange.kind
|
||||
of verSpecial:
|
||||
result = $verRange.spe
|
||||
of verLater, verEarlier, verEqLater, verEqEarlier, verEq:
|
||||
result = $verRange.ver
|
||||
of verIntersect:
|
||||
result = getSimpleString(verRange.verILeft) & "_" &
|
||||
getSimpleString(verRange.verIRight)
|
||||
of verAny:
|
||||
result = ""
|
||||
|
||||
proc newVRAny*(): VersionRange =
|
||||
result = VersionRange(kind: verAny)
|
||||
|
||||
proc newVREarlier*(ver: string): VersionRange =
|
||||
result = VersionRange(kind: verEarlier)
|
||||
result.ver = newVersion(ver)
|
||||
|
||||
proc newVREq*(ver: string): VersionRange =
|
||||
result = VersionRange(kind: verEq)
|
||||
result.ver = newVersion(ver)
|
||||
|
||||
proc findLatest*(verRange: VersionRange,
|
||||
versions: OrderedTable[Version, string]): tuple[ver: Version, tag: string] =
|
||||
result = (newVersion(""), "")
|
||||
for ver, tag in versions:
|
||||
if not withinRange(ver, verRange): continue
|
||||
if ver > result.ver:
|
||||
result = (ver, tag)
|
||||
|
||||
proc `$`*(dep: PkgTuple): string =
|
||||
return dep.name & "@" & $dep.ver
|
||||
|
||||
when isMainModule:
|
||||
doAssert(newVersion("1.0") < newVersion("1.4"))
|
||||
doAssert(newVersion("1.0.1") > newVersion("1.0"))
|
||||
doAssert(newVersion("1.0.6") <= newVersion("1.0.6"))
|
||||
doAssert(not withinRange(newVersion("0.1.0"), parseVersionRange("> 0.1")))
|
||||
doAssert(not (newVersion("0.1.0") < newVersion("0.1")))
|
||||
doAssert(not (newVersion("0.1.0") > newVersion("0.1")))
|
||||
doAssert(newVersion("0.1.0") < newVersion("0.1.0.0.1"))
|
||||
doAssert(newVersion("0.1.0") <= newVersion("0.1"))
|
||||
|
||||
var inter1 = parseVersionRange(">= 1.0 & <= 1.5")
|
||||
doAssert inter1.kind == verIntersect
|
||||
var inter2 = parseVersionRange("1.0")
|
||||
doAssert(inter2.kind == verEq)
|
||||
|
||||
doAssert(not withinRange(newVersion("1.5.1"), inter1))
|
||||
doAssert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1))
|
||||
|
||||
doAssert(newVersion("1") == newVersion("1"))
|
||||
doAssert(newVersion("1.0.2.4.6.1.2.123") == newVersion("1.0.2.4.6.1.2.123"))
|
||||
doAssert(newVersion("1.0.2") != newVersion("1.0.2.4.6.1.2.123"))
|
||||
doAssert(newVersion("1.0.3") != newVersion("1.0.2"))
|
||||
|
||||
doAssert(not (newVersion("") < newVersion("0.0.0")))
|
||||
doAssert(newVersion("") < newVersion("1.0.0"))
|
||||
doAssert(newVersion("") < newVersion("0.1.0"))
|
||||
|
||||
var versions = toOrderedTable[Version, string]({
|
||||
newVersion("0.1.1"): "v0.1.1",
|
||||
newVersion("0.2.3"): "v0.2.3",
|
||||
newVersion("0.5"): "v0.5"
|
||||
})
|
||||
doAssert findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) ==
|
||||
(newVersion("0.2.3"), "v0.2.3")
|
||||
|
||||
# TODO: Allow these in later versions?
|
||||
#doAssert newVersion("0.1-rc1") < newVersion("0.2")
|
||||
#doAssert newVersion("0.1-rc1") < newVersion("0.1")
|
||||
|
||||
# Special tests
|
||||
doAssert newVersion("#ab26sgdt362") != newVersion("#qwersaggdt362")
|
||||
doAssert newVersion("#ab26saggdt362") == newVersion("#ab26saggdt362")
|
||||
doAssert newVersion("#head") == newVersion("#HEAD")
|
||||
doAssert newVersion("#head") == newVersion("#head")
|
||||
|
||||
var sp = parseVersionRange("#ab26sgdt362")
|
||||
doAssert newVersion("#ab26sgdt362") in sp
|
||||
doAssert newVersion("#ab26saggdt362") notin sp
|
||||
|
||||
doAssert newVersion("#head") in parseVersionRange("#head")
|
||||
|
||||
# We assume that #head > 0.1.0, in practice this shouldn't be a problem.
|
||||
doAssert(newVersion("#head") > newVersion("0.1.0"))
|
||||
doAssert(not(newVersion("#head") > newVersion("#head")))
|
||||
doAssert(withinRange(newVersion("#head"), parseVersionRange(">= 0.5.0")))
|
||||
doAssert newVersion("#a111") < newVersion("#head")
|
||||
# We assume that all other special versions are not higher than a normal
|
||||
# version.
|
||||
doAssert newVersion("#a111") < newVersion("1.1")
|
||||
|
||||
# An empty version range should give verAny
|
||||
doAssert parseVersionRange("").kind == verAny
|
||||
|
||||
# toVersionRange tests
|
||||
doAssert toVersionRange(newVersion("#head")).kind == verSpecial
|
||||
doAssert toVersionRange(newVersion("0.2.0")).kind == verEq
|
||||
|
||||
# Something raised on IRC
|
||||
doAssert newVersion("1") == newVersion("1.0")
|
||||
|
||||
echo("Everything works!")
|
|
@ -0,0 +1,120 @@
|
|||
import std/sequtils, std/strutils, std/tables
|
||||
|
||||
type
|
||||
ValueKind* = enum
|
||||
tInt,
|
||||
tBool,
|
||||
tString,
|
||||
tPath,
|
||||
tNull,
|
||||
tAttrs,
|
||||
tList,
|
||||
tThunk,
|
||||
tApp,
|
||||
tLambda,
|
||||
tBlackhole,
|
||||
tPrimOp,
|
||||
tPrimOpApp,
|
||||
tExternal,
|
||||
tFloat
|
||||
Value* = ref object
|
||||
case kind: ValueKind
|
||||
of tInt:
|
||||
num*: BiggestInt
|
||||
of tBool:
|
||||
boolean*: bool
|
||||
of tString:
|
||||
str*: string
|
||||
of tPath:
|
||||
path*: string
|
||||
of tNull:
|
||||
discard
|
||||
of tAttrs:
|
||||
attrs*: Table[string, Value]
|
||||
of tList:
|
||||
list*: seq[Value]
|
||||
of tThunk: discard
|
||||
of tApp: discard
|
||||
of tLambda: discard
|
||||
of tBlackhole: discard
|
||||
of tPrimOp: discard
|
||||
of tPrimOpApp: discard
|
||||
of tExternal: discard
|
||||
of tFloat:
|
||||
fnum*: float
|
||||
|
||||
func `$`*(v: Value): string =
|
||||
case v.kind:
|
||||
of tInt:
|
||||
result = $v.num
|
||||
of tBool:
|
||||
result = if v.boolean: "True" else: "False"
|
||||
of tString:
|
||||
result = "\"$1\"" % (v.str.replace("\"", "\\\""))
|
||||
of tPath:
|
||||
result = v.path
|
||||
of tNull:
|
||||
result = "null"
|
||||
of tAttrs:
|
||||
result = "{"
|
||||
for key, val in v.attrs.pairs:
|
||||
let key = if key.validIdentifier: key else: key.escape
|
||||
result.add("$1=$2;" % [key, $val])
|
||||
result.add "}"
|
||||
of tList:
|
||||
result = "[ "
|
||||
for e in v.list:
|
||||
result.add $e
|
||||
result.add " "
|
||||
result.add "]"
|
||||
of tFloat:
|
||||
result = $v.fnum
|
||||
else:
|
||||
result = $v.kind
|
||||
|
||||
func toNix*(x: Value): Value = x
|
||||
|
||||
func toNix*(x: SomeInteger): Value =
|
||||
Value(kind: tInt, num: x)
|
||||
|
||||
func toNix*(x: bool): Value =
|
||||
Value(kind: tBool, boolean: x)
|
||||
|
||||
func toNix*(x: string): Value =
|
||||
Value(kind: tString, str: x)
|
||||
|
||||
func toPath*(x: string): Value =
|
||||
Value(kind: tPath, path: x)
|
||||
|
||||
template toNix*(pairs: openArray[(string, Value)]): Value =
|
||||
Value(kind: tAttrs, attrs: toTable pairs)
|
||||
|
||||
func toNix*(x: seq[Value]): Value =
|
||||
Value(kind: tList, list: x)
|
||||
|
||||
template toNix*(x: seq[untyped]): Value =
|
||||
map(x, toNix).toNix
|
||||
|
||||
func toNix*(x: openarray[Value]): Value =
|
||||
Value(kind: tList, list: x[x.low .. x.high])
|
||||
|
||||
func toNix*(x: float): Value =
|
||||
Value(kind: tFloat, fnum: x)
|
||||
|
||||
template `[]=`*(result: Value; key: string; val: untyped) =
|
||||
result.attrs[key] = val.toNix
|
||||
|
||||
proc `[]`*(attrs: Value; key: string): Value =
|
||||
attrs.attrs[key]
|
||||
|
||||
proc newNull*(): Value =
|
||||
Value(kind: tNull)
|
||||
|
||||
proc newAttrs*(): Value =
|
||||
Value(kind: tAttrs, attrs: initTable[string, Value]())
|
||||
|
||||
func newList*(): Value =
|
||||
Value(kind: tList)
|
||||
|
||||
proc add*(x, y: Value) =
|
||||
x.list.add y
|
Loading…
Reference in New Issue