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