nim_lk/src/nimblepkg/developfile.nim

954 lines
39 KiB
Nim

# 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)