nimble/src/nimble.nim
2019-10-03 18:38:51 -05:00

1225 lines
43 KiB
Nim

# 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..<pkgInfo.foreignDeps.len:
display("Hint:", " " & pkgInfo.foreignDeps[i], Warning, HighPriority)
of actionUninstall:
uninstall(options)
of actionSearch:
search(options)
of actionList:
if options.queryInstalled: listInstalled(options)
else: list(options)
of actionPath:
listPaths(options)
of actionBuild:
build(options)
of actionRun:
run(options)
of actionCompile, actionDoc:
execBackend(options)
of actionInit:
init(options)
of actionPublish:
var pkgInfo = getPkgInfo(getCurrentDir(), options)
publish(pkgInfo, options)
of actionDump:
dump(options)
of actionTasks:
listTasks(options)
of actionDevelop:
develop(options)
of actionCheck:
check(options)
of actionNil:
assert false
of actionCustom:
if not execHook(options, actionCustom, true):
display("Warning", "Pre-hook prevented further execution.", Warning,
HighPriority)
return
let isPreDefined = options.action.command.normalize == "test"
var execResult: ExecutionResult[bool]
if execCustom(options, execResult, failFast=not isPreDefined):
if execResult.hasTaskRequestedCommand():
var options = execResult.getOptionsForCommand(options)
doAction(options)
else:
# If there is no task defined for the `test` task, we run the pre-defined
# fallback logic.
if isPreDefined:
test(options)
# Run the post hook for `test` in case it exists.
discard execHook(options, actionCustom, false)
when isMainModule:
var error = ""
var hint = ""
var opt: Options
try:
opt = parseCmdLine()
opt.doAction()
except NimbleError:
let currentExc = (ref NimbleError)(getCurrentException())
(error, hint) = getOutputInfo(currentExc)
except NimbleQuit:
discard
finally:
try:
let folder = getNimbleTempDir()
if opt.shouldRemoveTmp(folder):
removeDir(folder)
except OSError:
let msg = "Couldn't remove Nimble's temp dir"
display("Warning:", msg, Warning, MediumPriority)
if error.len > 0:
displayTip()
display("Error:", error, Error, HighPriority)
if hint.len > 0:
display("Hint:", hint, Warning, HighPriority)
quit(1)