# Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. import system except TResult import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils import std/options as std_opt import strutils except toLower from unicode import toLower from sequtils import toSeq import nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, nimblepkg/download, nimblepkg/config, nimblepkg/common, nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser, nimblepkg/cli, nimblepkg/packageinstaller, nimblepkg/reversedeps, nimblepkg/nimscriptexecutor, nimblepkg/init import nimblepkg/nimscriptwrapper proc refresh(options: Options) = ## Downloads the package list from the specified URL. ## ## If the download is not successful, an exception is raised. let parameter = if options.action.typ == actionRefresh: options.action.optionalURL else: "" if parameter.len > 0: if parameter.isUrl: let cmdLine = PackageList(name: "commandline", urls: @[parameter]) fetchList(cmdLine, options) else: if parameter notin options.config.packageLists: let msg = "Package list with the specified name not found." raise newException(NimbleError, msg) fetchList(options.config.packageLists[parameter], options) else: # Try each package list in config for name, list in options.config.packageLists: fetchList(list, options) 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 copyWithExt(origDir, currentDir, dest: string, pkgInfo: PackageInfo): seq[string] = ## Returns the filenames of the files that have been copied ## (their destination). result = @[] for kind, path in walkDir(currentDir): if kind == pcDir: result.add copyWithExt(origDir, path, dest, pkgInfo) else: for iExt in pkgInfo.installExt: if path.splitFile.ext == ('.' & iExt): createDir(changeRoot(origDir, dest, path).splitFile.dir) result.add copyFileD(path, changeRoot(origDir, dest, path)) proc copyFilesRec(origDir, currentDir, dest: string, options: Options, pkgInfo: PackageInfo): HashSet[string] = ## Copies all the required files, skips files specified in the .nimble file ## (PackageInfo). ## Returns a list of filepaths to files which have been installed. result = initHashSet[string]() 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 = origDir / file if not src.existsFile(): if options.prompt("Missing file " & src & ". Continue?"): continue else: raise NimbleQuit(msg: "") createDir(dest / file.splitFile.dir) result.incl copyFileD(src, dest / file) for dir in pkgInfo.installDirs: # TODO: Allow skipping files inside dirs? let src = origDir / dir if not src.existsDir(): if options.prompt("Missing directory " & src & ". Continue?"): continue else: raise NimbleQuit(msg: "") result.incl copyDirD(origDir / dir, dest / dir) result.incl copyWithExt(origDir, currentDir, dest, pkgInfo) else: for kind, file in walkDir(currentDir): if kind == pcDir: let skip = pkgInfo.checkInstallDir(origDir, file) if skip: continue # Create the dir. createDir(changeRoot(origDir, dest, file)) result.incl copyFilesRec(origDir, file, dest, options, pkgInfo) else: let skip = pkgInfo.checkInstallFile(origDir, file) if skip: continue result.incl copyFileD(file, changeRoot(origDir, dest, file)) result.incl copyFileD(pkgInfo.mypath, changeRoot(pkgInfo.mypath.splitFile.dir, dest, pkgInfo.mypath)) proc install(packages: seq[PkgTuple], options: Options, doPrompt = true): tuple[deps: seq[PackageInfo], pkg: PackageInfo] proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = ## Verifies and installs dependencies. ## ## Returns the list of PackageInfo (for paths) to pass to the compiler ## during build phase. result = @[] assert(not pkginfo.isMinimal, "processDeps needs pkginfo.requires") display("Verifying", "dependencies for $1@$2" % [pkginfo.name, pkginfo.specialVersion], priority = HighPriority) var pkgList {.global.}: seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = @[] once: pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) var reverseDeps: seq[tuple[name, version: string]] = @[] for dep in pkginfo.requires: if dep.name == "nimrod" or dep.name == "nim": let nimVer = getNimrodVersion() if not withinRange(nimVer, dep.ver): let msg = "Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")" raise newException(NimbleError, msg) else: let resolvedDep = dep.resolveAlias(options) display("Checking", "for $1" % $resolvedDep, priority = MediumPriority) var pkg: PackageInfo var found = findPkg(pkgList, resolvedDep, pkg) # Check if the original name exists. if not found and resolvedDep.name != dep.name: display("Checking", "for $1" % $dep, priority = MediumPriority) found = findPkg(pkgList, dep, pkg) if found: display("Warning:", "Installed package $1 should be renamed to $2" % [dep.name, resolvedDep.name], Warning, HighPriority) if not found: display("Installing", $resolvedDep, priority = HighPriority) let toInstall = @[(resolvedDep.name, resolvedDep.ver)] let (pkgs, installedPkg) = install(toInstall, options) result.add(pkgs) pkg = installedPkg # For addRevDep # This package has been installed so we add it to our pkgList. pkgList.add((pkg, readMetaData(pkg.getRealDir()))) else: display("Info:", "Dependency on $1 already satisfied" % $dep, priority = HighPriority) result.add(pkg) # Process the dependencies of this dependency. result.add(processDeps(pkg.toFullInfo(options), options)) reverseDeps.add((pkg.name, pkg.specialVersion)) # Check if two packages of the same name (but different version) are listed # in the path. var pkgsInPath: StringTableRef = newStringTable(modeCaseSensitive) for pkgInfo in result: let currentVer = pkgInfo.getConcreteVersion(options) if pkgsInPath.hasKey(pkgInfo.name) and pkgsInPath[pkgInfo.name] != currentVer: raise newException(NimbleError, "Cannot satisfy the dependency on $1 $2 and $1 $3" % [pkgInfo.name, currentVer, pkgsInPath[pkgInfo.name]]) pkgsInPath[pkgInfo.name] = currentVer # We add the reverse deps to the JSON file here because we don't want # them added if the above errorenous condition occurs # (unsatisfiable dependendencies). # N.B. NimbleData is saved in installFromDir. for i in reverseDeps: addRevDep(options.nimbleData, i, pkginfo) proc buildFromDir( pkgInfo: PackageInfo, paths, args: seq[string], options: Options ) = ## Builds a package as specified by ``pkgInfo``. let binToBuild = options.getCompilationBinary() # Handle pre-`build` hook. let realDir = pkgInfo.getRealDir() cd realDir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionBuild, true): raise newException(NimbleError, "Pre-hook prevented further execution.") if pkgInfo.bin.len == 0: raise newException(NimbleError, "Nothing to build. Did you specify a module to build using the" & " `bin` key in your .nimble file?") var args = args let nimblePkgVersion = "-d:NimblePkgVersion=" & pkgInfo.version for path in paths: args.add("--path:\"" & path & "\" ") var binariesBuilt = 0 for bin in pkgInfo.bin: # Check if this is the only binary that we want to build. if binToBuild.isSome() and binToBuild.get() != bin: let binToBuild = binToBuild.get() if bin.extractFilename().changeFileExt("") != binToBuild: continue let outputOpt = "-o:\"" & pkgInfo.getOutputDir(bin) & "\"" display("Building", "$1/$2 using $3 backend" % [pkginfo.name, bin, pkgInfo.backend], priority = HighPriority) let outputDir = pkgInfo.getOutputDir("") if not existsDir(outputDir): createDir(outputDir) try: doCmd("\"" & getNimBin() & "\" $# --noNimblePath $# $# $# \"$#\"" % [pkgInfo.backend, nimblePkgVersion, join(args, " "), outputOpt, realDir / bin.changeFileExt("nim")]) binariesBuilt.inc() except NimbleError: let currentExc = (ref NimbleError)(getCurrentException()) let exc = newException(BuildFailed, "Build failed for package: " & pkgInfo.name) let (error, hint) = getOutputInfo(currentExc) exc.msg.add("\nDetails:\n" & error) exc.hint = hint raise exc if binariesBuilt == 0: raiseNimbleError( "No binaries built, did you specify a valid binary name?" ) # Handle post-`build` hook. cd realDir: # Make sure `execHook` executes the correct .nimble file. discard execHook(options, actionBuild, false) proc removePkgDir(dir: string, options: Options) = ## Removes files belonging to the package in ``dir``. try: var nimblemeta = parseFile(dir / "nimblemeta.json") if not nimblemeta.hasKey("files"): raise newException(JsonParsingError, "Meta data does not contain required info.") for file in nimblemeta["files"]: removeFile(dir / file.str) removeFile(dir / "nimblemeta.json") # If there are no files left in the directory, remove the directory. if toSeq(walkDirRec(dir)).len == 0: removeDir(dir) else: display("Warning:", ("Cannot completely remove $1. Files not installed " & "by nimble are present.") % dir, Warning, HighPriority) if nimblemeta.hasKey("binaries"): # Remove binaries. for binary in nimblemeta["binaries"]: removeFile(options.getBinDir() / binary.str) # Search for an older version of the package we are removing. # So that we can reinstate its symlink. let (pkgName, _) = getNameVersion(dir) let pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) var pkgInfo: PackageInfo if pkgList.findPkg((pkgName, newVRAny()), pkgInfo): pkgInfo = pkgInfo.toFullInfo(options) for bin in pkgInfo.bin: let symlinkDest = pkgInfo.getRealDir() / bin let symlinkFilename = options.getBinDir() / bin.extractFilename discard setupBinSymlink(symlinkDest, symlinkFilename, options) else: display("Warning:", ("Cannot completely remove $1. Binary symlinks may " & "have been left over in $2.") % [dir, options.getBinDir()]) except OSError, JsonParsingError: display("Warning", "Unable to read nimblemeta.json: " & getCurrentExceptionMsg(), Warning, HighPriority) if not options.prompt("Would you like to COMPLETELY remove ALL files " & "in " & dir & "?"): raise NimbleQuit(msg: "") removeDir(dir) proc vcsRevisionInDir(dir: string): string = ## Returns current revision number of HEAD if dir is inside VCS, or nil in ## case of failure. var cmd = "" if dirExists(dir / ".git"): cmd = "git -C " & quoteShell(dir) & " rev-parse HEAD" elif dirExists(dir / ".hg"): cmd = "hg --cwd " & quoteShell(dir) & " id -i" if cmd.len > 0: try: let res = doCmdEx(cmd) if res.exitCode == 0: result = string(res.output).strip() except: discard proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, url: string): tuple[ deps: seq[PackageInfo], pkg: PackageInfo ] = ## Returns where package has been installed to, together with paths ## to the packages this package depends on. ## The return value of this function is used by ## ``processDeps`` to gather a list of paths to pass to the nim compiler. # Handle pre-`install` hook. if not options.depsOnly: cd dir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionInstall, true): raise newException(NimbleError, "Pre-hook prevented further execution.") var pkgInfo = getPkgInfo(dir, options) let realDir = pkgInfo.getRealDir() let binDir = options.getBinDir() var depsOptions = options depsOptions.depsOnly = false # Overwrite the version if the requested version is "#head" or similar. if requestedVer.kind == verSpecial: pkgInfo.specialVersion = $requestedVer.spe # Dependencies need to be processed before the creation of the pkg dir. result.deps = processDeps(pkgInfo, depsOptions) if options.depsOnly: result.pkg = pkgInfo return result display("Installing", "$1@$2" % [pkginfo.name, pkginfo.specialVersion], priority = HighPriority) # Build before removing an existing package (if one exists). This way # if the build fails then the old package will still be installed. if pkgInfo.bin.len > 0: let paths = result.deps.map(dep => dep.getRealDir()) let flags = if options.action.typ in {actionInstall, actionPath, actionUninstall, actionDevelop}: options.action.passNimFlags else: @[] buildFromDir(pkgInfo, paths, flags & "-d:release", options) let pkgDestDir = pkgInfo.getPkgDest(options) if existsDir(pkgDestDir) and existsFile(pkgDestDir / "nimblemeta.json"): let msg = "$1@$2 already exists. Overwrite?" % [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): raise NimbleQuit(msg: "") # Remove reverse deps. let pkgInfo = getPkgInfo(pkgDestDir, options) options.nimbleData.removeRevDep(pkgInfo) removePkgDir(pkgDestDir, options) # Remove any symlinked binaries for bin in pkgInfo.bin: # TODO: Check that this binary belongs to the package being installed. when defined(windows): removeFile(binDir / bin.changeFileExt("cmd")) removeFile(binDir / bin.changeFileExt("")) else: removeFile(binDir / bin) createDir(pkgDestDir) # Copy this package's files based on the preferences specified in PkgInfo. var filesInstalled = initHashSet[string]() iterInstallFiles(realDir, pkgInfo, options, proc (file: string) = createDir(changeRoot(realDir, pkgDestDir, file.splitFile.dir)) let dest = changeRoot(realDir, pkgDestDir, file) filesInstalled.incl copyFileD(file, dest) ) # Copy the .nimble file. let dest = changeRoot(pkgInfo.myPath.splitFile.dir, pkgDestDir, pkgInfo.myPath) filesInstalled.incl copyFileD(pkgInfo.myPath, dest) var binariesInstalled = initHashSet[string]() if pkgInfo.bin.len > 0: # Make sure ~/.nimble/bin directory is created. createDir(binDir) # Set file permissions to +x for all binaries built, # and symlink them on *nix OS' to $nimbleDir/bin/ for bin in pkgInfo.bin: if existsFile(pkgDestDir / bin): display("Warning:", ("Binary '$1' was already installed from source" & " directory. Will be overwritten.") % bin, Warning, MediumPriority) # Copy the binary file. filesInstalled.incl copyFileD(pkgInfo.getOutputDir(bin), pkgDestDir / bin) # Set up a symlink. let symlinkDest = pkgDestDir / bin let symlinkFilename = binDir / bin.extractFilename for filename in setupBinSymlink(symlinkDest, symlinkFilename, options): binariesInstalled.incl(filename) let vcsRevision = vcsRevisionInDir(realDir) # Save a nimblemeta.json file. saveNimbleMeta(pkgDestDir, url, vcsRevision, filesInstalled, binariesInstalled) # Save the nimble data (which might now contain reverse deps added in # processDeps). saveNimbleData(options) # Return the dependencies of this package (mainly for paths). result.deps.add pkgInfo result.pkg = pkgInfo result.pkg.isInstalled = true result.pkg.myPath = dest display("Success:", pkgInfo.name & " installed successfully.", Success, HighPriority) # Run post-install hook now that package is installed. The `execHook` proc # executes the hook defined in the CWD, so we set it to where the package # has been installed. cd dest.splitFile.dir: discard execHook(options, actionInstall, false) proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string, Table[string, string]) = if pv.name.isURL: let (url, metadata) = getUrlData(pv.name) return (checkUrlType(url), url, metadata) else: var pkg: Package if getPackage(pv.name, options, pkg): let (url, metadata) = getUrlData(pkg.url) return (pkg.downloadMethod.getDownloadMethod(), url, metadata) else: # If package is not found give the user a chance to refresh # package.json if doPrompt and options.prompt(pv.name & " not found in any local packages.json, " & "check internet for updated packages?"): refresh(options) # Once we've refreshed, try again, but don't prompt if not found # (as we've already refreshed and a failure means it really # isn't there) return getDownloadInfo(pv, options, false) else: raise newException(NimbleError, "Package not found.") proc install(packages: seq[PkgTuple], options: Options, doPrompt = true): tuple[deps: seq[PackageInfo], pkg: PackageInfo] = if packages == @[]: result = installFromDir(getCurrentDir(), newVRAny(), options, "") else: # Install each package. for pv in packages: let (meth, url, metadata) = getDownloadInfo(pv, options, doPrompt) let subdir = metadata.getOrDefault("subdir") let (downloadDir, downloadVersion) = downloadPkg(url, pv.ver, meth, subdir, options) try: result = installFromDir(downloadDir, pv.ver, options, url) except BuildFailed: # The package failed to build. # Check if we tried building a tagged version of the package. let headVer = getHeadName(meth) if pv.ver.kind != verSpecial and downloadVersion != headVer: # If we tried building a tagged version of the package then # ask the user whether they want to try building #head. let promptResult = doPrompt and options.prompt(("Build failed for '$1@$2', would you" & " like to try installing '$1@#head' (latest unstable)?") % [pv.name, $downloadVersion]) if promptResult: let toInstall = @[(pv.name, headVer.toVersionRange())] result = install(toInstall, options, doPrompt) else: raise newException(BuildFailed, "Aborting installation due to build failure") else: raise proc build(options: Options) = var pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) let deps = processDeps(pkginfo, options) let paths = deps.map(dep => dep.getRealDir()) var args = options.getCompilationFlags() buildFromDir(pkgInfo, paths, args, options) proc execBackend(options: Options) = let bin = options.getCompilationBinary().get() binDotNim = bin.addFileExt("nim") if bin == "": raise newException(NimbleError, "You need to specify a file.") if not (fileExists(bin) or fileExists(binDotNim)): raise newException(NimbleError, "Specified file, " & bin & " or " & binDotNim & ", does not exist.") var pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) let deps = processDeps(pkginfo, options) let nimblePkgVersion = "-d:NimblePkgVersion=" & pkgInfo.version var args = "" for dep in deps: args.add("--path:\"" & dep.getRealDir() & "\" ") for option in options.getCompilationFlags(): args.add("\"" & option & "\" ") let backend = if options.action.backend.len > 0: options.action.backend else: pkgInfo.backend if options.action.typ == actionCompile: display("Compiling", "$1 (from package $2) using $3 backend" % [bin, pkgInfo.name, backend], priority = HighPriority) else: display("Generating", ("documentation for $1 (from package $2) using $3 " & "backend") % [bin, pkgInfo.name, backend], priority = HighPriority) doCmd("\"" & getNimBin() & "\" $# --noNimblePath $# $# \"$#\"" % [backend, nimblePkgVersion, args, bin], showOutput = true) display("Success:", "Execution finished", Success, HighPriority) proc search(options: Options) = ## Searches for matches in ``options.action.search``. ## ## Searches are done in a case insensitive way making all strings lower case. assert options.action.typ == actionSearch if options.action.search == @[]: raise newException(NimbleError, "Please specify a search string.") if needsRefresh(options): raise newException(NimbleError, "Please run nimble refresh.") let pkgList = getPackageList(options) var found = false template onFound {.dirty.} = echoPackage(pkg) if pkg.alias.len == 0 and options.queryVersions: echoPackageVersions(pkg) echo(" ") found = true break forPkg for pkg in pkgList: block forPkg: for word in options.action.search: # Search by name. if word.toLower() in pkg.name.toLower(): onFound() # Search by tag. for tag in pkg.tags: if word.toLower() in tag.toLower(): onFound() if not found: display("Error", "No package found.", Error, HighPriority) proc list(options: Options) = if needsRefresh(options): raise newException(NimbleError, "Please run nimble refresh.") let pkgList = getPackageList(options) for pkg in pkgList: echoPackage(pkg) if pkg.alias.len == 0 and options.queryVersions: echoPackageVersions(pkg) echo(" ") proc listInstalled(options: Options) = var h = initOrderedTable[string, seq[string]]() let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) for x in pkgs.items(): let pName = x.pkginfo.name pVer = x.pkginfo.specialVersion if not h.hasKey(pName): h[pName] = @[] var s = h[pName] add(s, pVer) h[pName] = s h.sort(proc (a, b: (string, seq[string])): int = cmpIgnoreCase(a[0], b[0])) for k in keys(h): echo k & " [" & h[k].join(", ") & "]" type VersionAndPath = tuple[version: Version, path: string] proc listPaths(options: Options) = ## Loops over the specified packages displaying their installed paths. ## ## If there are several packages installed, only the last one (the version ## listed in the packages.json) will be displayed. If any package name is not ## found, the proc displays a missing message and continues through the list, ## but at the end quits with a non zero exit error. ## ## On success the proc returns normally. cli.setSuppressMessages(true) assert options.action.typ == actionPath if options.action.packages.len == 0: raise newException(NimbleError, "A package name needs to be specified") var errors = 0 let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) for name, version in options.action.packages.items: var installed: seq[VersionAndPath] = @[] # There may be several, list all available ones and sort by version. for x in pkgs.items(): let pName = x.pkginfo.name pVer = x.pkginfo.specialVersion if name == pName: var v: VersionAndPath v.version = newVersion(pVer) v.path = x.pkginfo.getRealDir() installed.add(v) if installed.len > 0: sort(installed, cmp[VersionAndPath], Descending) # The output for this command is used by tools so we do not use display(). echo installed[0].path else: display("Warning:", "Package '$1' is not installed" % name, Warning, MediumPriority) errors += 1 if errors > 0: raise newException(NimbleError, "At least one of the specified packages was not found") proc join(x: seq[PkgTuple]; y: string): string = if x.len == 0: return "" result = x[0][0] & " " & $x[0][1] for i in 1 ..< x.len: result.add y result.add x[i][0] & " " & $x[i][1] proc getPackageByPattern(pattern: string, options: Options): PackageInfo = ## Search for a package file using multiple strategies. if pattern == "": # Not specified - using current directory result = getPkgInfo(os.getCurrentDir(), options) elif pattern.splitFile.ext == ".nimble" and pattern.existsFile: # project file specified result = getPkgInfoFromFile(pattern, options) elif pattern.existsDir: # project directory specified result = getPkgInfo(pattern, options) else: # Last resort - attempt to read as package identifier let packages = getInstalledPkgsMin(options.getPkgsDir(), options) let identTuple = parseRequires(pattern) var skeletonInfo: PackageInfo if not findPkg(packages, identTuple, skeletonInfo): raise newException(NimbleError, "Specified package not found" ) result = getPkgInfoFromFile(skeletonInfo.myPath, options) proc dump(options: Options) = cli.setSuppressMessages(true) let p = getPackageByPattern(options.action.projName, options) echo "name: ", p.name.escape echo "version: ", p.version.escape echo "author: ", p.author.escape echo "desc: ", p.description.escape echo "license: ", p.license.escape echo "skipDirs: ", p.skipDirs.join(", ").escape echo "skipFiles: ", p.skipFiles.join(", ").escape echo "skipExt: ", p.skipExt.join(", ").escape echo "installDirs: ", p.installDirs.join(", ").escape echo "installFiles: ", p.installFiles.join(", ").escape echo "installExt: ", p.installExt.join(", ").escape echo "requires: ", p.requires.join(", ").escape echo "bin: ", p.bin.join(", ").escape echo "binDir: ", p.binDir.escape echo "srcDir: ", p.srcDir.escape echo "backend: ", p.backend.escape proc init(options: Options) = # Determine the package name. let pkgName = if options.action.projName != "": options.action.projName else: os.getCurrentDir().splitPath.tail.toValidPackageName() # Validate the package name. validatePackageName(pkgName) # Determine the package root. let pkgRoot = if pkgName == os.getCurrentDir().splitPath.tail: os.getCurrentDir() else: os.getCurrentDir() / pkgName let nimbleFile = (pkgRoot / pkgName).changeFileExt("nimble") if existsFile(nimbleFile): let errMsg = "Nimble file already exists: $#" % nimbleFile raise newException(NimbleError, errMsg) if options.forcePrompts != forcePromptYes: display( "Info:", "Package initialisation requires info which could not be inferred.\n" & "Default values are shown in square brackets, press\n" & "enter to use them.", priority = HighPriority ) display("Using", "$# for new package name" % [pkgName.escape()], priority = HighPriority) # Determine author by running an external command proc getAuthorWithCmd(cmd: string): string = let (name, exitCode) = doCmdEx(cmd) if exitCode == QuitSuccess and name.len > 0: result = name.strip() display("Using", "$# for new package author" % [result], priority = HighPriority) # Determine package author via git/hg or asking proc getAuthor(): string = if findExe("git") != "": result = getAuthorWithCmd("git config --global user.name") elif findExe("hg") != "": result = getAuthorWithCmd("hg config ui.username") if result.len == 0: result = promptCustom(options, "Your name?", "Anonymous") let pkgAuthor = getAuthor() # Declare the src/ directory let pkgSrcDir = "src" display("Using", "$# for new package source directory" % [pkgSrcDir.escape()], priority = HighPriority) # Determine the type of package let pkgType = promptList( options, """Package type? Library - provides functionality for other packages. Binary - produces an executable for the end-user. Hybrid - combination of library and binary For more information see https://goo.gl/cm2RX5""", ["library", "binary", "hybrid"] ) # Ask for package version. let pkgVersion = promptCustom(options, "Initial version of package?", "0.1.0") validateVersion(pkgVersion) # Ask for description let pkgDesc = promptCustom(options, "Package description?", "A new awesome nimble package") # Ask for license # License list is based on: # https://www.blackducksoftware.com/top-open-source-licenses var pkgLicense = options.promptList( """Package License? This should ideally be a valid SPDX identifier. See https://spdx.org/licenses/. """, [ "MIT", "GPL-2.0", "Apache-2.0", "ISC", "GPL-3.0", "BSD-3-Clause", "LGPL-2.1", "LGPL-3.0", "EPL-2.0", # This is what npm calls "UNLICENSED" (which is too similar to "Unlicense") "Proprietary", "Other" ]) if pkgLicense.toLower == "other": pkgLicense = promptCustom(options, """Package license? Please specify a valid SPDX identifier.""", "MIT" ) var pkgBackend = options.promptList( """Package Backend? c - Compile using C backend. cpp - Compile using C++ backend. objc - Compile using Objective-C backend. js - Compile using JavaScript backend.""", ["c", "cpp", "objc", "js"] ) # Ask for Nim dependency let nimDepDef = getNimrodVersion() let pkgNimDep = promptCustom(options, "Lowest supported Nim version?", $nimDepDef) validateVersion(pkgNimDep) createPkgStructure( ( pkgName, pkgVersion, pkgAuthor, pkgDesc, pkgLicense, pkgBackend, pkgSrcDir, pkgNimDep, pkgType ), pkgRoot ) display("Success:", "Package $# created successfully" % [pkgName], Success, HighPriority) proc uninstall(options: Options) = if options.action.packages.len == 0: raise newException(NimbleError, "Please specify the package(s) to uninstall.") var pkgsToDelete: HashSet[PackageInfo] pkgsToDelete.init() # Do some verification. for pkgTup in options.action.packages: display("Looking", "for $1 ($2)" % [pkgTup.name, $pkgTup.ver], priority = HighPriority) let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) var pkgList = findAllPkgs(installedPkgs, pkgTup) if pkgList.len == 0: raise newException(NimbleError, "Package not found") display("Checking", "reverse dependencies", priority = HighPriority) for pkg in pkgList: # Check whether any packages depend on the ones the user is trying to # uninstall. if options.uninstallRevDeps: getAllRevDeps(options, pkg, pkgsToDelete) else: let revDeps = getRevDeps(options, pkg) var reason = "" for revDep in revDeps: if reason.len != 0: reason.add ", " reason.add("$1 ($2)" % [revDep.name, revDep.version]) if reason.len != 0: reason &= " depend" & (if revDeps.len == 1: "s" else: "") & " on it" if len(revDeps - pkgsToDelete) > 0: display("Cannot", "uninstall $1 ($2) because $3" % [pkgTup.name, pkg.specialVersion, reason], Warning, HighPriority) else: pkgsToDelete.incl pkg if pkgsToDelete.len == 0: raise newException(NimbleError, "Failed uninstall - no packages to delete") var pkgNames = "" for pkg in pkgsToDelete.items: if pkgNames.len != 0: pkgNames.add ", " pkgNames.add("$1 ($2)" % [pkg.name, pkg.specialVersion]) # Let's confirm that the user wants these packages removed. let msg = ("The following packages will be removed:\n $1\n" & "Do you wish to continue?") % pkgNames if not options.prompt(msg): raise NimbleQuit(msg: "") for pkg in pkgsToDelete: # If we reach this point then the package can be safely removed. # removeRevDep needs the package dependency info, so we can't just pass # a minimal pkg info. removeRevDep(options.nimbleData, pkg.toFullInfo(options)) removePkgDir(options.getPkgsDir / (pkg.name & '-' & pkg.specialVersion), options) display("Removed", "$1 ($2)" % [pkg.name, $pkg.specialVersion], Success, HighPriority) saveNimbleData(options) proc listTasks(options: Options) = let nimbleFile = findNimbleFile(getCurrentDir(), true) nimscriptwrapper.listTasks(nimbleFile, options) proc developFromDir(dir: string, options: Options) = if options.depsOnly: raiseNimbleError("Cannot develop dependencies only.") cd dir: # Make sure `execHook` executes the correct .nimble file. if not execHook(options, actionDevelop, true): raise newException(NimbleError, "Pre-hook prevented further execution.") var pkgInfo = getPkgInfo(dir, options) if pkgInfo.bin.len > 0: if "nim" in pkgInfo.skipExt: raiseNimbleError("Cannot develop packages that are binaries only.") display("Warning:", "This package's binaries will not be compiled " & "nor symlinked for development.", Warning, HighPriority) # Overwrite the version to #head always. pkgInfo.specialVersion = "#head" # Dependencies need to be processed before the creation of the pkg dir. discard processDeps(pkgInfo, options) # This is similar to the code in `installFromDir`, except that we # *consciously* not worry about the package's binaries. let pkgDestDir = pkgInfo.getPkgDest(options) if existsDir(pkgDestDir) and existsFile(pkgDestDir / "nimblemeta.json"): let msg = "$1@$2 already exists. Overwrite?" % [pkgInfo.name, pkgInfo.specialVersion] if not options.prompt(msg): raise NimbleQuit(msg: "") removePkgDir(pkgDestDir, options) createDir(pkgDestDir) # The .nimble-link file contains the path to the real .nimble file, # and a secondary path to the source directory of the package. # The secondary path is necessary so that the package's .nimble file doesn't # need to be read. This will mean that users will need to re-run # `nimble develop` if they change their `srcDir` but I think it's a worthy # compromise. let nimbleLinkPath = pkgDestDir / pkgInfo.name.addFileExt("nimble-link") let nimbleLink = NimbleLink( nimbleFilePath: pkgInfo.myPath, packageDir: pkgInfo.getRealDir() ) writeNimbleLink(nimbleLinkPath, nimbleLink) # Save a nimblemeta.json file. saveNimbleMeta(pkgDestDir, dir, vcsRevisionInDir(dir), nimbleLinkPath) # Save the nimble data (which might now contain reverse deps added in # processDeps). saveNimbleData(options) display("Success:", (pkgInfo.name & " linked successfully to '$1'.") % dir, Success, HighPriority) # Execute the post-develop hook. cd dir: discard execHook(options, actionDevelop, false) proc develop(options: Options) = if options.action.packages == @[]: developFromDir(getCurrentDir(), options) else: # Install each package. for pv in options.action.packages: let name = if isURL(pv.name): parseUri(pv.name).path else: pv.name let downloadDir = getCurrentDir() / name if dirExists(downloadDir): let msg = "Cannot clone into '$1': directory exists." % downloadDir let hint = "Remove the directory, or run this command somewhere else." raiseNimbleError(msg, hint) let (meth, url, metadata) = getDownloadInfo(pv, options, true) let subdir = metadata.getOrDefault("subdir") # Download the HEAD and make sure the full history is downloaded. let ver = if pv.ver.kind == verAny: parseVersionRange("#head") else: pv.ver var options = options options.forceFullClone = true discard downloadPkg(url, ver, meth, subdir, options, downloadDir) developFromDir(downloadDir / subdir, options) proc test(options: Options) = ## Executes all tests starting with 't' in the ``tests`` directory. ## Subdirectories are not walked. var pkgInfo = getPkgInfo(getCurrentDir(), options) var files = toSeq(walkDir(getCurrentDir() / "tests")) tests, failures: int if files.len < 1: display("Warning:", "No tests found!", Warning, HighPriority) return files.sort((a, b) => cmp(a.path, b.path)) for file in files: let (_, name, ext) = file.path.splitFile() if ext == ".nim" and name[0] == 't' and file.kind in {pcFile, pcLinkToFile}: var optsCopy = options.briefClone() optsCopy.action = Action(typ: actionCompile) optsCopy.action.file = file.path optsCopy.action.backend = pkgInfo.backend optsCopy.getCompilationFlags() = @[] optsCopy.getCompilationFlags().add("-r") optsCopy.getCompilationFlags().add("--path:.") let binFileName = file.path.changeFileExt(ExeExt) existsBefore = existsFile(binFileName) if options.continueTestsOnFailure: inc tests try: execBackend(optsCopy) except NimbleError: inc failures else: execBackend(optsCopy) let existsAfter = existsFile(binFileName) canRemove = not existsBefore and existsAfter if canRemove: try: removeFile(binFileName) except OSError as exc: display("Warning:", "Failed to delete " & binFileName & ": " & exc.msg, Warning, MediumPriority) if failures == 0: display("Success:", "All tests passed", Success, HighPriority) else: let error = "Only " & $(tests - failures) & "/" & $tests & " tests passed" display("Error:", error, Error, HighPriority) proc check(options: Options) = ## Validates a package in the current working directory. let nimbleFile = findNimbleFile(getCurrentDir(), true) var error: ValidationError var pkgInfo: PackageInfo var validationResult = false try: validationResult = validate(nimbleFile, options, error, pkgInfo) except: raiseNimbleError("Could not validate package:\n" & getCurrentExceptionMsg()) if validationResult: display("Success:", pkgInfo.name & " is valid!", Success, HighPriority) else: display("Error:", error.msg, Error, HighPriority) display("Hint:", error.hint, Warning, HighPriority) display("Failure:", "Validation failed", Error, HighPriority) quit(QuitFailure) proc run(options: Options) = # Verify parameters. let binary = options.getCompilationBinary().get("") if binary.len == 0: raiseNimbleError("Please specify a binary to run") var pkgInfo = getPkgInfo(getCurrentDir(), options) if binary notin pkgInfo.bin: raiseNimbleError( "Binary '$#' is not defined in '$#' package." % [binary, pkgInfo.name] ) let binaryPath = pkgInfo.getOutputDir(binary) # Build the binary. build(options) # Now run it. let args = options.action.runFlags.join(" ") doCmd("$# $#" % [binaryPath, args], showOutput = true) proc doAction(options: var Options) = if options.showHelp: writeHelp() if options.showVersion: writeVersion() if not existsDir(options.getNimbleDir()): createDir(options.getNimbleDir()) if not existsDir(options.getPkgsDir): createDir(options.getPkgsDir) if options.action.typ in {actionTasks, actionRun, actionBuild, actionCompile}: # Implicitly disable package validation for these commands. options.disableValidation = true case options.action.typ of actionRefresh: refresh(options) of actionInstall: let (_, pkgInfo) = install(options.action.packages, options) if options.action.packages.len == 0: nimScriptHint(pkgInfo) if pkgInfo.foreignDeps.len > 0: display("Hint:", "This package requires some external dependencies.", Warning, HighPriority) display("Hint:", "To install them you may be able to run:", Warning, HighPriority) for i in 0.. 0: displayTip() display("Error:", error, Error, HighPriority) if hint.len > 0: display("Hint:", hint, Warning, HighPriority) quit(1)