Merge branch 'master' into native-pkg-support

This commit is contained in:
Araq 2016-12-23 16:01:37 +01:00
commit e813aa6448
14 changed files with 646 additions and 305 deletions

View file

@ -12,34 +12,36 @@ Interested in learning **how to create a package**? Skip directly to that sectio
- [Installation](#installation)
- [Nimble's folder structure and packages](#nimbles-folder-structure-and-packages)
- [Nimble usage](#nimble-usage)
- [nimble refresh](#nimble-refresh)
- [nimble install](#nimble-install)
- [nimble uninstall](#nimble-uninstall)
- [nimble build](#nimble-build)
- [nimble c](#nimble-c)
- [nimble list](#nimble-list)
- [nimble search](#nimble-search)
- [nimble path](#nimble-path)
- [nimble init](#nimble-init)
- [nimble publish](#nimble-publish)
- [nimble tasks](#nimble-tasks)
- [nimble dump](#nimble-dump)
- [nimble refresh](#nimble-refresh)
- [nimble install](#nimble-install)
- [nimble uninstall](#nimble-uninstall)
- [nimble build](#nimble-build)
- [nimble c](#nimble-c)
- [nimble list](#nimble-list)
- [nimble search](#nimble-search)
- [nimble path](#nimble-path)
- [nimble init](#nimble-init)
- [nimble publish](#nimble-publish)
- [nimble tasks](#nimble-tasks)
- [nimble dump](#nimble-dump)
- [Configuration](#configuration)
- [Creating Packages](#creating-packages)
- [The new NimScript format](#the-new-nimscript-format)
- [Libraries](#libraries)
- [Binary packages](#binary-packages)
- [Hybrids](#hybrids)
- [Dependencies](#dependencies)
- [Nim compiler](#nim-compiler)
- [Versions](#versions)
- [Submitting your package to the package list.](#submitting-your-package-to-the-package-list)
- [Project structure](#project-structure)
- [Tests](#tests)
- [Libraries](#libraries)
- [Binary packages](#binary-packages)
- [Hybrids](#hybrids)
- [Dependencies](#dependencies)
- [Nim compiler](#nim-compiler)
- [Versions](#versions)
- [Releasing a new version](#releasing-a-new-version)
- [Publishing packages](#publishing-packages)
- [.nimble reference](#nimble-reference)
- [[Package]](#package)
- [Required](#required)
- [Optional](#optional)
- [[Deps]/[Dependencies]](#depsdependencies)
- [Optional](#optional)
- [[Package]](#package)
- [Required](#required)
- [Optional](#optional)
- [[Deps]/[Dependencies]](#depsdependencies)
- [Optional](#optional)
- [Troubleshooting](#troubleshooting)
- [Contribution](#contribution)
- [About](#about)
@ -195,9 +197,10 @@ Similar to the ``install`` command you can specify a version range, for example:
### nimble build
The ``build`` command is mostly used by developers who want to test building
their ``.nimble`` package. This command will build the package in debug mode,
without installing anything. The ``install`` command will build the package
in release mode instead.
their ``.nimble`` package. This command will build the package with default
flags, i.e. a debug build which includes stack traces but no GDB debug
information.
The ``install`` command will build the package in release mode instead.
### nimble c
@ -373,6 +376,7 @@ You can also specify multiple dependencies like so:
requires "nim >= 0.10.0", "foobar >= 0.1.0"
requires "fizzbuzz >= 1.0"
```
Nimble currently supports installation of packages from a local directory, a
git repository and a mercurial repository. The .nimble file must be present in
@ -436,12 +440,54 @@ also return ``false`` from these blocks to stop further execution.
The ``nimscriptapi.nim`` module specifies this and includes other definitions
which are also useful. Take a look at it for more information.
### Project structure
There is nothing surprising about the recommended project structure. The advice
resembles that of many other package managers.
| Directory | Purpose |
| ------------- | -------------------------------------- |
| ``.`` | Root directory containing .nimble file.|
| ``./src/`` | Project source code |
| ``./tests/`` | Project test files |
| ``./docs/`` | Project documentation |
#### Tests
A common problem that arises with tests is the fact that they need to import
the associated package. But the package is in the parent directory. This can
be solved in a few different ways:
* Expect that the package has been installed locally into your
``~/.nimble`` directory.
* Use a simple path modification to resolve the package properly.
The latter is highly recommended. Reinstalling the package to test an actively
changing code base is a massive pain.
To modify the path for your tests only, simply add a ``nim.cfg`` file into
your ``tests`` directory with the following contents:
```
--path:"../src/"
```
To make testing even more convenient, you may wish to define a ``test`` task
in your ``.nimble`` file. Like so:
```nim
task "test", "Runs the test suite":
exec "nim c -r tests/tester"
```
You can compile and run a single tester application or multiple test files.
### Libraries
Library packages are likely the most popular form of Nimble packages. They are
meant to be used by other library packages or the ultimate binary packages.
meant to be used by other library or binary packages.
When nimble installs a library it will copy all the files in the package
When nimble installs a library it will copy all of its files
into ``$nimbleDir/pkgs/pkgname-ver``. It's up to the package creator to make sure
that the package directory layout is correct, this is so that users of the
package can correctly import the package.
@ -483,23 +529,23 @@ A package is automatically a binary package as soon as it sets at least one
``bin`` value, like so:
```ini
bin = "main" # NimScript config expects a seq instead, e.g. @["main"]
bin = @["main"]
```
In this case when ``nimble install`` is invoked, nimble will build the ``main.nim``
file, copy it into ``$nimbleDir/pkgs/pkgname-ver/`` and subsequently create a
symlink to the binary in ``$nimbleDir/bin/``. On Windows a stub .bat file is
symlink to the binary in ``$nimbleDir/bin/``. On Windows a stub .cmd file is
created instead.
Other files will be copied in the same way as they are for library packages.
Binary packages should not install .nim files so you should include
Binary packages should not install .nim files so include
``SkipExt = "nim"`` in your .nimble file, unless you intend for your package to
be a binary/library combo which is fine.
Dependencies are automatically installed before building. Before publishing your
package you should ensure that the dependencies you specified are correct.
You can do this by running ``nimble build`` or ``nimble install`` in the directory
Dependencies are automatically installed before building.
It's a good idea to test that the dependencies you specified are correct by
running by running ``nimble build`` or ``nimble install`` in the directory
of your package.
### Hybrids
@ -516,28 +562,28 @@ done for nimble.
### Dependencies
Dependencies are specified under the ``[Deps]`` section in a nimble file.
The ``requires`` key field is used to specify them. For example:
Dependencies are specified using the ``requires`` function. For example:
```ini
[Deps]
Requires: "nim >= 0.10.0, jester > 0.1 & <= 0.5"
```
# Dependencies
requires "nim >= 0.10.0", "jester > 0.1 & <= 0.5"
```
Dependency lists support version ranges. These versions may either be a concrete
version like ``0.1``, or they may contain any of the less-than (``<``),
greater-than (``>``), less-than-or-equal-to (``<=``) and greater-than-or-equal-to
(``>=``). Two version ranges may be combined using the ``&`` operator for example:
``> 0.2 & < 1.0`` which will install a package with the version greater than 0.2
(``>=``) oeprators.
Two version ranges may be combined using the ``&`` operator, for example
``> 0.2 & < 1.0``, which will install a package with the version greater than 0.2
and less than 1.0.
Specifying a concrete version as a dependency is not a good idea because your
package may end up depending on two different versions of the same package.
If this happens Nimble will refuse to install the package. Similarly you should
not specify an upper-bound as this can lead to a similar issue.
If this happens Nimble will refuse to install the package.
In addition to versions you may also specify git/hg tags, branches and commits.
These have to be concrete however. This is done with the ``#`` character,
Although these have to be specific; ranges of commits are not supported.
This is done with the ``#`` character,
for example: ``jester#head``. Which will make your package depend on the
latest commit of Jester.
@ -573,11 +619,43 @@ package after checking out the latest version.
You can force the installation of the HEAD of the repository by specifying
``#head`` after the package name in your dependency list.
## Submitting your package to the package list.
#### Releasing a new version
Nimble's packages list is stored on github and everyone is encouraged to add
their own packages to it! Take a look at
[nim-lang/packages](https://github.com/nim-lang/packages) to learn more.
Version releases are done by creating a tag in your Git or Mercurial
repository.
Whenever you want to release a new version, you should remember to first
increment the version in your ``.nimble`` file and commit your changes. Only
after that is done should you tag the release.
To summarise, the steps for release are:
* Increment the version in your ``.nimble`` file.
* Commit your changes.
* Tag your release, by for example running ``git tag v0.2.0``.
* Push your tags and commits.
Once the new tag is in the remote repository, Nimble will be able to detect
the new version.
##<a name="submitting-your-package-to-the-package-list"> Publishing packages
Publishing packages isn't a requirement. But doing so allows people to associate
a specific name to a URL pointing to your package. This mapping is stored
in an official packages repository located
[here](https://github.com/nim-lang/packages).
This repository contains a ``packages.json`` file which lists all the published
packages. It contains a set of package names with associated metadata. You
can read more about this metadata in the
[readme for the packages repository](https://github.com/nim-lang/packages#readme).
To publish your package you need to fork that repository, and add an entry
into the ``packages.json`` file for your package. Then create a pull request
with your changes. **You only need to do this
once**.
Nimble includes a ``publish`` command which does this for you automatically.
## .nimble reference

View file

@ -1,6 +1,8 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
import system except TResult
import httpclient, parseopt, os, osproc, pegs, tables, parseutils,
strtabs, json, algorithm, sets, uri
@ -10,7 +12,8 @@ from sequtils import toSeq
import nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools,
nimblepkg/download, nimblepkg/config, nimblepkg/common,
nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser
nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser,
nimblepkg/cli
import nimblepkg/nimscriptsupport
@ -32,24 +35,7 @@ else:
proc GetVersionExA*(VersionInformation: var OSVERSIONINFO): WINBOOL{.stdcall,
dynlib: "kernel32", importc: "GetVersionExA".}
proc writeVersion() =
echo("nimble v$# compiled at $# $#" %
[nimbleVersion, CompileDate, CompileTime])
quit(QuitSuccess)
proc promptCustom(question, default: string): string =
if default == "":
stdout.write(question, ": ")
let user = stdin.readLine()
if user.len == 0: return promptCustom(question, default)
else: return user
else:
stdout.write(question, " [", default, "]: ")
let user = stdin.readLine()
if user == "": return default
else: return user
proc update(options: Options) =
proc refresh(options: Options) =
## Downloads the package list from the specified URL.
##
## If the download is not successful, an exception is raised.
@ -60,10 +46,12 @@ proc update(options: Options) =
""
proc downloadList(list: PackageList, options: Options) =
echo("Downloading \"", list.name, "\" package list")
display("Downloading", list.name & " package list", priority = HighPriority)
var lastError = ""
for i in 0 .. <list.urls.len:
let url = list.urls[i]
echo("Trying ", url, "...")
display("Trying", url)
let tempPath = options.getNimbleDir() / "packages_temp.json"
# Grab the proxy
@ -71,25 +59,41 @@ proc update(options: Options) =
if not proxy.isNil:
var maskedUrl = proxy.url
if maskedUrl.password.len > 0: maskedUrl.password = "***"
echo("Using proxy ", maskedUrl)
display("Connecting", "to proxy at " & $maskedUrl,
priority = LowPriority)
try:
downloadFile(url, tempPath, proxy = getProxy(options))
except:
if i == <list.urls.len:
raise
echo("Could not download: ", getCurrentExceptionMsg())
let message = "Could not download: " & getCurrentExceptionMsg()
display("Warning:", message, Warning)
lastError = message
continue
if not validatePackagesList(tempPath):
echo("Downloaded packages.json file is invalid, discarding.")
lastError = "Downloaded packages.json file is invalid"
display("Warning:", lastError & ", discarding.", Warning)
continue
copyFile(tempPath,
options.getNimbleDir() / "packages_$1.json" % list.name.toLowerAscii())
echo("Done.")
display("Success", "Package list downloaded.", Success, HighPriority)
lastError = ""
break
if parameter.isUrl:
downloadList(PackageList(name: "commandline", urls: @[parameter]), options)
if lastError.len != 0:
raise newException(NimbleError, "Refresh failed\n" & lastError)
if parameter.len > 0:
if parameter.isUrl:
let cmdLine = PackageList(name: "commandline", urls: @[parameter])
downloadList(cmdLine, options)
else:
if parameter notin options.config.packageLists:
let msg = "Package list with the specified name not found."
raise newException(NimbleError, msg)
downloadList(options.config.packageLists[parameter], options)
else:
# Try each package list in config
for name, list in options.config.packageLists:
@ -258,24 +262,31 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[string] =
## Returns the list of paths to pass to the compiler during build phase.
result = @[]
assert(not pkginfo.isMinimal, "processDeps needs pkginfo.requires")
display("Verifying",
"dependencies for $1 v$2" % [pkginfo.name, pkginfo.version],
priority = HighPriority)
let pkglist = getInstalledPkgs(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):
quit("Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")")
let msg = "Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")"
raise newException(NimbleError, msg)
else:
echo("Looking for ", dep.name, " (", $dep.ver, ")...")
let depDesc = "$1 ($2)" % [dep.name, $dep.ver]
display("Checking", "for $1" % depDesc, priority = MediumPriority)
var pkg: PackageInfo
if not findPkg(pkglist, dep, pkg):
echo("None found, installing...")
display("Installing", depDesc, priority = HighPriority)
let (paths, installedPkg) = install(@[(dep.name, dep.ver)], options)
result.add(paths)
pkg = installedPkg # For addRevDep
else:
echo("Dependency already satisfied.")
display("Info:", "Dependency on $1 already satisfied" % depDesc,
priority = HighPriority)
result.add(pkg.mypath.splitFile.dir)
# Process the dependencies of this dependency.
result.add(processDeps(pkg, options))
@ -311,8 +322,8 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: seq[string], forRelease: bool) =
for path in paths: args.add("--path:\"" & path & "\" ")
for bin in pkgInfo.bin:
let outputOpt = "-o:\"" & pkgInfo.getOutputDir(bin) & "\""
echo("Building ", pkginfo.name, "/", bin, " using ", pkgInfo.backend,
" backend...")
display("Building", "$1/$2 using $3 backend" %
[pkginfo.name, bin, pkgInfo.backend], priority = HighPriority)
let outputDir = pkgInfo.getOutputDir("")
if not existsDir(outputDir):
@ -352,13 +363,14 @@ proc removePkgDir(dir: string, options: Options) =
if toSeq(walkDirRec(dir)).len == 0:
removeDir(dir)
else:
echo("WARNING: Cannot completely remove " & dir &
". Files not installed by nimble are present.")
display("Warning:", ("Cannot completely remove $1. Files not installed " &
"by nimble are present.") % dir, Warning, HighPriority)
except OSError, JsonParsingError:
echo("Error: Unable to read nimblemeta.json: ", getCurrentExceptionMsg())
display("Warning", "Unable to read nimblemeta.json: " &
getCurrentExceptionMsg(), Warning, HighPriority)
if not options.prompt("Would you like to COMPLETELY remove ALL files " &
"in " & dir & "?"):
quit(QuitSuccess)
raise NimbleQuit(msg: "")
removeDir(dir)
proc vcsRevisionInDir(dir: string): string =
@ -398,7 +410,8 @@ proc installFromDir(dir: string, latest: bool, options: Options,
result.pkg = pkgInfo
return result
echo("Installing ", pkginfo.name, "-", pkginfo.version)
display("Installing", "$1 v$2" % [pkginfo.name, pkginfo.version],
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.
@ -409,7 +422,7 @@ proc installFromDir(dir: string, latest: bool, options: Options,
if existsDir(pkgDestDir) and existsFile(pkgDestDir / "nimblemeta.json"):
if not options.prompt(pkgInfo.name & versionStr &
" already exists. Overwrite?"):
quit(QuitSuccess)
raise NimbleQuit(msg: "")
removePkgDir(pkgDestDir, options)
# Remove any symlinked binaries
for bin in pkgInfo.bin:
@ -446,12 +459,13 @@ proc installFromDir(dir: string, latest: bool, options: Options,
# TODO: Verify that we are removing an old bin of this package, not
# some other package's binary!
if existsFile(binDir / cleanBin): removeFile(binDir / cleanBin)
echo("Creating symlink: ", pkgDestDir / bin, " -> ", binDir / cleanBin)
display("Creating", "symlink: $1 -> $2" %
[pkgDestDir / bin, binDir / cleanBin], priority = MediumPriority)
createSymlink(pkgDestDir / bin, binDir / cleanBin)
elif defined(windows):
# There is a bug on XP, described here:
# http://stackoverflow.com/questions/2182568/batch-script-is-not-executed-if-chcp-was-called
# But this workaround brokes code page on newer systems, so we need to detect OS version
# But this workaround brakes code page on newer systems, so we need to detect OS version
var osver = OSVERSIONINFO()
osver.dwOSVersionInfoSize = cast[DWORD](sizeof(OSVERSIONINFO))
if GetVersionExA(osver) == WINBOOL(0):
@ -460,7 +474,8 @@ proc installFromDir(dir: string, latest: bool, options: Options,
let fixChcp = osver.dwMajorVersion <= 5
let dest = binDir / cleanBin.changeFileExt("cmd")
echo("Creating stub: ", pkgDestDir / bin, " -> ", dest)
display("Creating", "stub: $1 -> $2" % [pkgDestDir / bin, dest],
priority = MediumPriority)
var contents = "@"
if options.config.chcp:
if fixChcp:
@ -470,7 +485,8 @@ proc installFromDir(dir: string, latest: bool, options: Options,
writeFile(dest, contents)
# For bash on Windows (Cygwin/Git bash).
let bashDest = dest.changeFileExt("")
echo("Creating Cygwin stub: ", pkgDestDir / bin, " -> ", bashDest)
display("Creating", "Cygwin stub: $1 -> $2" %
[pkgDestDir / bin, bashDest], priority = MediumPriority)
writeFile(bashDest, "\"" & pkgDestDir / bin & "\" \"$@\"\n")
else:
{.error: "Sorry, your platform is not supported.".}
@ -491,7 +507,8 @@ proc installFromDir(dir: string, latest: bool, options: Options,
result.paths.add pkgDestDir
result.pkg = pkgInfo
echo(pkgInfo.name & " installed successfully.")
display("Success:", pkgInfo.name & " installed successfully.",
Success, HighPriority)
proc getNimbleTempDir(): string =
## Returns a path to a temporary directory.
@ -529,8 +546,8 @@ proc downloadPkg(url: string, verRange: VersionRange,
if modUrl.contains("github.com") and modUrl.endswith("/"):
modUrl = modUrl[0 .. ^2]
echo("Downloading ", modUrl, " into ", downloadDir, " using ",
downMethod, "...")
display("Downloading", "$1 using $2" % [modUrl, $downMethod],
priority = HighPriority)
result = (
downloadDir,
doDownload(modUrl, downloadDir, verRange, downMethod, options)
@ -545,15 +562,15 @@ proc getDownloadInfo*(pv: PkgTuple, options: Options,
if getPackage(pv.name, options, pkg):
return (pkg.downloadMethod.getDownloadMethod(), pkg.url)
else:
# If package is not found give the user a chance to update
# 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?"):
update(options)
refresh(options)
# Once we've updated, try again, but don't prompt if not found
# (as we've already updated and a failure means it really
# 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:
@ -570,9 +587,9 @@ proc install(packages: seq[PkgTuple],
if doPrompt and
options.prompt("No local packages.json found, download it from " &
"internet?"):
update(options)
refresh(options)
else:
quit("Please run nimble refresh.", QuitFailure)
raise newException(NimbleError, "Please run nimble refresh.")
# Install each package.
for pv in packages:
@ -608,6 +625,13 @@ proc build(options: Options) =
buildFromDir(pkgInfo, paths, false)
proc compile(options: Options) =
let bin = options.action.file
if bin == "":
raise newException(NimbleError, "You need to specify a file to compile.")
if not fileExists(bin):
raise newException(NimbleError, "Specified file does not exist.")
var pkgInfo = getPkgInfo(getCurrentDir(), options)
nimScriptHint(pkgInfo)
let paths = processDeps(pkginfo, options)
@ -617,20 +641,17 @@ proc compile(options: Options) =
for option in options.action.compileOptions:
args.add(option & " ")
let bin = options.action.file
let backend =
if options.action.backend.len > 0:
options.action.backend
else:
pkgInfo.backend
if bin == "":
raise newException(NimbleError, "You need to specify a file to compile.")
echo("Compiling ", bin, " (", pkgInfo.name, ") using ", backend,
" backend...")
display("Compiling", "$1 (from package $2) using $3 backend" %
[bin, pkgInfo.name, backend], priority = HighPriority)
doCmd("\"" & getNimBin() & "\" $# --noBabelPath $# \"$#\"" %
[backend, args, bin])
display("Success:", "Compilation finished", Success, HighPriority)
proc search(options: Options) =
## Searches for matches in ``options.action.search``.
@ -663,7 +684,7 @@ proc search(options: Options) =
onFound()
if not found:
echo("No package found.")
display("Error", "No package found.", Error, HighPriority)
proc list(options: Options) =
if needsRefresh(options):
@ -694,7 +715,7 @@ proc listInstalled(options: Options) =
type VersionAndPath = tuple[version: Version, path: string]
proc listPaths(options: Options) =
## Loops over installing packages displaying their installed paths.
## 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
@ -704,6 +725,10 @@ proc listPaths(options: Options) =
## On success the proc returns normally.
assert options.action.typ == actionPath
assert(not options.action.packages.isNil)
if options.action.packages.len == 0:
raise newException(NimbleError, "A package name needs to be specified")
var errors = 0
for name, version in options.action.packages.items:
var installed: seq[VersionAndPath] = @[]
@ -713,11 +738,9 @@ proc listPaths(options: Options) =
continue
let
nimScriptFile = path / name.addFileExt("nims")
babelFile = path / name.addFileExt("babel")
nimbleFile = path / name.addFileExt("nimble")
hasSpec = nimScriptFile.existsFile or
nimbleFile.existsFile or babelFile.existsFile
hasSpec = nimbleFile.existsFile
if hasSpec:
var pkgInfo = getPkgInfo(path, options)
var v: VersionAndPath
@ -725,13 +748,16 @@ proc listPaths(options: Options) =
v.path = options.getPkgsDir / (pkgInfo.name & '-' & pkgInfo.version)
installed.add(v)
else:
echo "Warning: No .nimble file found for ", path
display("Warning:", "No .nimble file found for " & path, Warning,
MediumPriority)
if installed.len > 0:
sort(installed, system.cmp[VersionAndPath], Descending)
# The output for this command is used by tools so we do not use display().
echo installed[0].path
else:
echo "Warning: Package '" & name & "' not installed"
display("Warning:", "Package '$1' is not installed" % name, Warning,
MediumPriority)
errors += 1
if errors > 0:
raise newException(NimbleError,
@ -768,9 +794,10 @@ proc dump(options: Options) =
proc init(options: Options) =
var nimbleFile: string = ""
echo("In order to initialise a new Nimble package, I will need to ask you\n" &
display("Info:",
"In order to initialise a new Nimble package, I will need to ask you\n" &
"some questions. Default values are shown in square brackets, press\n" &
"enter to use them.")
"enter to use them.", priority = HighPriority)
# Ask for package name.
if options.action.projName != "":
@ -778,7 +805,7 @@ proc init(options: Options) =
nimbleFile = pkgName.changeFileExt("nimble")
else:
var pkgName = os.getCurrentDir().splitPath.tail.toValidPackageName()
pkgName = promptCustom("Enter package name", pkgName)
pkgName = promptCustom("Package name?", pkgName)
nimbleFile = pkgName.changeFileExt("nimble")
validatePackageName(nimbleFile.changeFileExt(""))
@ -787,7 +814,7 @@ proc init(options: Options) =
raise newException(NimbleError, "Nimble file already exists.")
# Ask for package version.
let pkgVersion = promptCustom("Enter initial version of package", "0.1.0")
let pkgVersion = promptCustom("Initial version of package?", "0.1.0")
validateVersion(pkgVersion)
# Ask for package author
@ -800,25 +827,20 @@ proc init(options: Options) =
let (name, exitCode) = doCmdEx("hg config ui.username")
if exitCode == QuitSuccess and name.len > 0:
defaultAuthor = name.strip()
let pkgAuthor = promptCustom("Enter your name", defaultAuthor)
let pkgAuthor = promptCustom("Your name?", defaultAuthor)
# Ask for description
let pkgDesc = promptCustom("Enter package description", "")
let pkgDesc = promptCustom("Package description?", "")
# Ask for license
# TODO: Provide selection of licenses, or select random default license.
let pkgLicense = promptCustom("Enter package license", "MIT")
let pkgLicense = promptCustom("Package license?", "MIT")
# Ask for Nim dependency
let nimDepDef = getNimrodVersion()
let pkgNimDep = promptCustom("Enter lowest supported Nim version", $nimDepDef)
let pkgNimDep = promptCustom("Lowest supported Nim version?", $nimDepDef)
validateVersion(pkgNimDep)
# Now generate the .nimble file.
if existsFile(os.getCurrentDir() / nimbleFile):
raise newException(NimbleError,
"Looks like a Nimble file has already been created.")
var outFile: File
if open(f = outFile, filename = nimbleFile, mode = fmWrite):
outFile.writeLine """# Package
@ -838,6 +860,8 @@ requires "nim >= $#"
raise newException(NimbleError, "Unable to open file " & nimbleFile &
" for writing: " & osErrorMsg(osLastError()))
display("Success:", "Nimble file created successfully", Success, HighPriority)
proc uninstall(options: Options) =
if options.action.packages.len == 0:
raise newException(NimbleError,
@ -846,13 +870,14 @@ proc uninstall(options: Options) =
var pkgsToDelete: seq[PackageInfo] = @[]
# Do some verification.
for pkgTup in options.action.packages:
echo("Looking for ", pkgTup.name, " (", $pkgTup.ver, ")...")
display("Looking", "for $1 ($2)" % [pkgTup.name, $pkgTup.ver],
priority = HighPriority)
let installedPkgs = getInstalledPkgs(options.getPkgsDir(), options)
var pkgList = findAllPkgs(installedPkgs, pkgTup)
if pkgList.len == 0:
raise newException(NimbleError, "Package not found")
echo("Checking reverse dependencies...")
display("Checking", "reverse dependencies", priority = HighPriority)
var errors: seq[string] = @[]
for pkg in pkgList:
# Check whether any packages depend on the ones the user is trying to
@ -861,17 +886,17 @@ proc uninstall(options: Options) =
if not thisPkgsDep.isNil:
var reason = ""
if thisPkgsDep.len == 1:
reason = thisPkgsDep[0]["name"].str &
" (" & thisPkgsDep[0]["version"].str & ") depends on it"
reason = "$1 ($2) depends on it" % [thisPkgsDep[0]["name"].str,
thisPkgsDep[0]["version"].str]
else:
for i in 0 .. <thisPkgsDep.len:
reason.add thisPkgsDep[i]["name"].str &
" (" & thisPkgsDep[i]["version"].str & ")"
reason.add("$1 ($2)" % [thisPkgsDep[i]["name"].str,
thisPkgsDep[i]["version"].str])
if i != <thisPkgsDep.len:
reason.add ", "
reason.add " depend on it"
errors.add("Cannot uninstall " & pkgTup.name & " (" & pkg.version &
")" & " because " & reason)
errors.add("Cannot uninstall $1 ($2) because $3" % [pkgTup.name,
pkg.version, reason])
else:
pkgsToDelete.add pkg
@ -882,12 +907,13 @@ proc uninstall(options: Options) =
for i in 0 .. <pkgsToDelete.len:
if i != 0: pkgNames.add ", "
let pkg = pkgsToDelete[i]
pkgNames.add pkg.name & " (" & pkg.version & ")"
pkgNames.add("$1 ($2)" % [pkg.name, pkg.version])
# Let's confirm that the user wants these packages removed.
if not options.prompt("The following packages will be removed:\n " &
pkgNames & "\nDo you wish to continue?"):
quit(QuitSuccess)
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.
@ -897,7 +923,8 @@ proc uninstall(options: Options) =
let pkgFull = readPackageInfo(pkg.mypath, options, false)
removeRevDep(options, pkgFull)
removePkgDir(options.getPkgsDir / (pkg.name & '-' & pkg.version), options)
echo("Removed ", pkg.name, " (", $pkg.version, ")")
display("Removed", "$1 ($2)" % [pkg.name, $pkg.version], Success,
HighPriority)
proc listTasks(options: Options) =
let nimbleFile = findNimbleFile(getCurrentDir(), true)
@ -930,11 +957,12 @@ proc doAction(options: Options) =
createDir(options.getPkgsDir)
if not execHook(options, true):
echo("Pre-hook prevented further execution.")
display("Warning", "Pre-hook prevented further execution.", Warning,
HighPriority)
return
case options.action.typ
of actionRefresh:
update(options)
refresh(options)
of actionInstall:
let (_, pkgInfo) = install(options.action.packages, options)
if options.action.packages.len == 0:
@ -965,8 +993,6 @@ proc doAction(options: Options) =
dump(options)
of actionTasks:
listTasks(options)
of actionVersion:
writeVersion()
of actionNil:
assert false
of actionCustom:
@ -977,14 +1003,19 @@ proc doAction(options: Options) =
let execResult = execTask(nimbleFile, options.action.command, options)
if not execResult.success:
echo("FAILURE: Could not find task ", options.action.command, " in ",
nimbleFile)
writeHelp()
raiseNimbleError(msg = "Could not find task $1 in $2" %
[options.action.command, nimbleFile],
hint = "Run `nimble --help` and/or `nimble tasks` for" &
" a list of possible commands.")
if execResult.command.normalize == "nop":
echo("WARNING: Using `setCommand 'nop'` is not necessary.")
display("Warning:", "Using `setCommand 'nop'` is not necessary.", Warning,
HighPriority)
return
if not execHook(options, false):
return
if execResult.hasTaskRequestedCommand():
var newOptions = initOptions()
newOptions.config = options.config
@ -1000,12 +1031,27 @@ proc doAction(options: Options) =
discard execHook(options, false)
when isMainModule:
when defined(release):
try:
parseCmdLine().doAction()
except NimbleError:
quit("FAILURE: " & getCurrentExceptionMsg())
finally:
removeDir(getNimbleTempDir())
else:
var error = ""
var hint = ""
try:
parseCmdLine().doAction()
except NimbleError:
let err = (ref NimbleError)(getCurrentException())
error = err.msg
when not defined(release):
let stackTrace = getStackTrace(getCurrentException())
error = stackTrace & "\n\n" & error
if not err.isNil:
hint = err.hint
except NimbleQuit:
nil
finally:
removeDir(getNimbleTempDir())
if error.len > 0:
displayTip()
display("Error:", error, Error, HighPriority)
if hint.len > 0:
display("Hint:", hint, Warning, HighPriority)
quit(1)

166
src/nimblepkg/cli.nim Normal file
View file

@ -0,0 +1,166 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
#
# Rough rules/philosophy for the messages that Nimble displays are the following:
# - Green is only shown when the requested operation is successful.
# - Blue can be used to emphasise certain keywords, for example actions such
# as "Downloading" or "Reading".
# - Red is used when the requested operation fails with an error.
# - Yellow is used for warnings.
#
# - Dim for LowPriority.
# - Bright for HighPriority.
# - Normal for MediumPriority.
import logging, terminal, sets, strutils
type
CLI* = ref object
level: Priority
warnings: HashSet[(string, string)]
suppressionCount: int ## Amount of messages which were not shown.
Priority* = enum
DebugPriority, LowPriority, MediumPriority, HighPriority
DisplayType* = enum
Error, Warning, Message, Success
ForcePrompt* = enum
dontForcePrompt, forcePromptYes, forcePromptNo
const
longestCategory = len("Downloading")
foregrounds: array[Error .. Success, ForegroundColor] =
[fgRed, fgYellow, fgCyan, fgGreen]
styles: array[DebugPriority .. HighPriority, set[Style]] =
[{styleDim}, {styleDim}, {}, {styleBright}]
proc newCLI(): CLI =
result = CLI(
level: HighPriority,
warnings: initSet[(string, string)](),
suppressionCount: 0
)
var globalCLI = newCLI()
proc calculateCategoryOffset(category: string): int =
assert category.len <= longestCategory
return longestCategory - category.len
proc displayCategory(category: string, displayType: DisplayType,
priority: Priority) =
# Calculate how much the `category` must be offset to align along a center
# line.
let offset = calculateCategoryOffset(category)
# Display the category.
if priority != DebugPriority:
setForegroundColor(stdout, foregrounds[displayType])
writeStyled("$1$2 " % [repeatChar(offset), category], styles[priority])
resetAttributes()
proc displayLine(category, line: string, displayType: DisplayType,
priority: Priority) =
displayCategory(category, displayType, priority)
# Display the message.
echo(line)
proc display*(category, msg: string, displayType = Message,
priority = MediumPriority) =
# Multiple warnings containing the same messages should not be shown.
let warningPair = (category, msg)
if displayType == Warning:
if warningPair in globalCLI.warnings:
return
else:
globalCLI.warnings.incl(warningPair)
# Suppress this message if its priority isn't high enough.
# TODO: Per-priority suppression counts?
if priority < globalCLI.level:
if priority != DebugPriority:
globalCLI.suppressionCount.inc
return
# Display each line in the message.
var i = 0
for line in msg.splitLines():
if len(line) == 0: continue
displayLine(if i == 0: category else: "...", line, displayType, priority)
i.inc
proc displayDebug*(category, msg: string) =
## Convenience for displaying debug messages.
display(category, msg, priority = DebugPriority)
proc displayDebug*(msg: string) =
## Convenience for displaying debug messages with a default category.
displayDebug("Debug:", msg)
proc displayTip*() =
## Called just before Nimble exits. Shows some tips for the user, for example
## the amount of messages that were suppressed and how to show them.
if globalCLI.suppressionCount > 0:
let msg = "$1 messages have been suppressed, use --verbose to show them." %
$globalCLI.suppressionCount
display("Tip:", msg, Warning, HighPriority)
proc prompt*(forcePrompts: ForcePrompt, question: string): bool =
case forcePrompts
of forcePromptYes:
display("Prompt:", question & " -> [forced yes]", Warning, HighPriority)
return true
of forcePromptNo:
display("Prompt:", question & " -> [forced no]", Warning, HighPriority)
return false
of dontForcePrompt:
display("Prompt:", question & " [y/N]", Warning, HighPriority)
displayCategory("Answer:", Warning, HighPriority)
let yn = stdin.readLine()
case yn.normalize
of "y", "yes":
return true
of "n", "no":
return false
else:
return false
proc promptCustom*(question, default: string): string =
if default == "":
display("Prompt:", question, Warning, HighPriority)
displayCategory("Answer:", Warning, HighPriority)
let user = stdin.readLine()
if user.len == 0: return promptCustom(question, default)
else: return user
else:
display("Prompt:", question & " [" & default & "]", Warning, HighPriority)
displayCategory("Answer:", Warning, HighPriority)
let user = stdin.readLine()
if user == "": return default
else: return user
proc setVerbosity*(level: Priority) =
globalCLI.level = level
when isMainModule:
display("Reading", "config file at /Users/dom/.config/nimble/nimble.ini",
priority = LowPriority)
display("Reading", "official package list",
priority = LowPriority)
display("Downloading", "daemonize v0.0.2 using Git",
priority = HighPriority)
display("Warning", "dashes in package names will be deprecated", Warning,
priority = HighPriority)
display("Error", """Unable to read package info for /Users/dom/.nimble/pkgs/nimble-0.7.11
Reading as ini file failed with:
Invalid section: .
Evaluating as NimScript file failed with:
Users/dom/.nimble/pkgs/nimble-0.7.11/nimble.nimble(3, 23) Error: cannot open 'src/nimblepkg/common'.
""", Error, HighPriority)

View file

@ -1,3 +1,4 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
#
# Various miscellaneous common types reside here, to avoid problems with
@ -7,7 +8,7 @@ when not defined(nimscript):
import sets
import version
export version.NimbleError
export version.NimbleError # TODO: Surely there is a better way?
type
BuildFailed* = object of NimbleError
@ -37,5 +38,13 @@ when not defined(nimscript):
backend*: string
foreignDeps*: seq[string]
## Same as quit(QuitSuccess), but allows cleanup.
NimbleQuit* = ref object of Exception
proc raiseNimbleError*(msg: string, hint = "") =
var exc = newException(NimbleError, msg)
exc.hint = hint
raise exc
const
nimbleVersion* = "0.7.11"

View file

@ -2,13 +2,13 @@
# BSD License. Look at license.txt for more info.
import parsecfg, streams, strutils, os, tables, Uri
import tools, version, common
import tools, version, common, cli
type
Config* = object
nimbleDir*: string
chcp*: bool # Whether to change the code page in .cmd files on Win.
packageLists*: Table[string, PackageList] ## URLs to packages.json files
packageLists*: Table[string, PackageList] ## Names -> packages.json files
cloneUsingHttps*: bool # Whether to replace git:// for https://
httpProxy*: Uri # Proxy for package list downloads.
@ -50,13 +50,15 @@ proc parseConfig*(): Config =
var f = newFileStream(confFile, fmRead)
if f == nil:
# Try the old deprecated babel.ini
# TODO: This can be removed.
confFile = getConfigDir() / "babel" / "babel.ini"
f = newFileStream(confFile, fmRead)
if f != nil:
echo("[Warning] Using deprecated config file at ", confFile)
display("Warning", "Using deprecated config file at " & confFile,
Warning, HighPriority)
if f != nil:
echo("Reading from config file at ", confFile)
display("Reading", "config file at " & confFile, priority = LowPriority)
var p: CfgParser
open(p, f, confFile)
var currentSection = ""

View file

@ -3,7 +3,7 @@
import parseutils, os, osproc, strutils, tables, pegs
import packageinfo, packageparser, version, tools, common, options
import packageinfo, packageparser, version, tools, common, options, cli
type
DownloadMethod* {.pure.} = enum
@ -142,7 +142,6 @@ proc doDownload*(url: string, downloadDir: string, verRange: VersionRange,
##
## Returns the version of the repository which has been downloaded.
template getLatestByTag(meth: untyped) {.dirty.} =
echo("Found tags...")
# Find latest version that fits our ``verRange``.
var latest = findLatest(verRange, versions)
## Note: HEAD is not used when verRange.kind is verAny. This is
@ -150,7 +149,7 @@ proc doDownload*(url: string, downloadDir: string, verRange: VersionRange,
# If no tagged versions satisfy our range latest.tag will be "".
# We still clone in that scenario because we want to try HEAD in that case.
# https://github.com/nimrod-code/nimble/issues/22
# https://github.com/nim-lang/nimble/issues/22
meth
if $latest.ver != "":
result = parseVersionRange($latest.ver)
@ -188,7 +187,8 @@ proc doDownload*(url: string, downloadDir: string, verRange: VersionRange,
let versions = getTagsListRemote(url, downMethod).getVersionList()
if versions.len > 0:
getLatestByTag:
echo("Cloning latest tagged version: ", latest.tag)
display("Cloning", "latest tagged version: " & latest.tag,
priority = MediumPriority)
doClone(downMethod, url, downloadDir, latest.tag)
else:
# If no commits have been tagged on the repo we just clone HEAD.
@ -202,7 +202,8 @@ proc doDownload*(url: string, downloadDir: string, verRange: VersionRange,
if versions.len > 0:
getLatestByTag:
echo("Switching to latest tagged version: ", latest.tag)
display("Switching", "to latest tagged version: " & latest.tag,
priority = MediumPriority)
doCheckout(downMethod, downloadDir, latest.tag)
verifyClone()

View file

@ -15,7 +15,7 @@ from compiler/scriptconfig import setupVM
from compiler/astalgo import strTableGet
import compiler/options as compiler_options
import common, version, options, packageinfo
import common, version, options, packageinfo, cli
import os, strutils, strtabs, times, osproc, sets
when not declared(resetAllModulesHard):
@ -299,7 +299,7 @@ proc readPackageInfoFromNims*(scriptName: string, options: Options,
if msgs.gErrorCounter > 0:
raise newException(NimbleError, previousMsg)
elif previousMsg.len > 0:
echo(previousMsg)
display("Info", previousMsg, priority = HighPriority)
if output.normalize.startsWith("error"):
raise newException(NimbleError, output)
previousMsg = output
@ -387,7 +387,8 @@ proc execTask*(scriptName, taskName: string,
result.success = true
result.flags = newStringTable()
compiler_options.command = internalCmd
echo("Executing task ", taskName, " in ", scriptName)
display("Executing", "task $# in $#" % [taskName, scriptName],
priority = HighPriority)
let thisModule = execScript(scriptName, result.flags, options)
let prc = thisModule.tab.strTableGet(getIdent(taskName & "Task"))
@ -417,7 +418,8 @@ proc execHook*(scriptName, actionName: string, before: bool,
let hookName =
if before: actionName.toLowerAscii & "Before"
else: actionName.toLowerAscii & "After"
echo("Attempting to execute hook ", hookName, " in ", scriptName)
display("Attempting", "to execute hook $# in $#" % [hookName, scriptName],
priority = MediumPriority)
let thisModule = execScript(scriptName, result.flags, options)
# Explicitly execute the task procedure, instead of relying on hack.

View file

@ -4,7 +4,7 @@
import json, strutils, os, parseopt, strtabs, uri, tables
from httpclient import Proxy, newProxy
import config, version, tools, common
import config, version, tools, common, cli
type
Options* = object
@ -12,6 +12,8 @@ type
depsOnly*: bool
queryVersions*: bool
queryInstalled*: bool
nimbleDir*: string
verbosity*: cli.Priority
action*: Action
config*: Config
nimbleData*: JsonNode ## Nimbledata.json
@ -21,12 +23,11 @@ type
actionNil, actionRefresh, actionInit, actionDump, actionPublish,
actionInstall, actionSearch,
actionList, actionBuild, actionPath, actionUninstall, actionCompile,
actionCustom, actionTasks, actionVersion
actionCustom, actionTasks
Action* = object
case typ*: ActionType
of actionNil, actionList, actionBuild, actionPublish, actionTasks,
actionVersion: nil
of actionNil, actionList, actionBuild, actionPublish, actionTasks: nil
of actionRefresh:
optionalURL*: string # Overrides default package list.
of actionInstall, actionPath, actionUninstall:
@ -44,9 +45,6 @@ type
arguments*: seq[string]
flags*: StringTableRef
ForcePrompt* = enum
dontForcePrompt, forcePromptYes, forcePromptNo
const
help* = """
@ -54,6 +52,7 @@ Usage: nimble COMMAND [opts]
Commands:
install [pkgname, ...] Installs a list of packages.
[-d, --depsOnly] Install only dependencies.
init [pkgname] Initializes a new Nimble project.
publish Publishes a package on nim-lang/packages.
The current working directory needs to be the
@ -64,9 +63,11 @@ Commands:
to the Nim compiler.
refresh [url] Refreshes the package list. A package list URL
can be optionally specified.
search [--ver] pkg/tag Searches for a specified package. Search is
search pkg/tag Searches for a specified package. Search is
performed by tag and by name.
list [--ver] Lists all packages.
[--ver] Query remote server for package version.
list Lists all packages.
[--ver] Query remote server for package version.
[-i, --installed] Lists all installed packages.
tasks Lists the tasks specified in the Nimble
package's Nimble file.
@ -83,26 +84,29 @@ Options:
--ver Query remote server for package version
information when searching or listing packages
--nimbleDir dirname Set the Nimble directory.
-d --depsOnly Install only dependencies.
--verbose Show all non-debug output.
--debug Show all output including debug messages.
For more information read the Github readme:
https://github.com/nim-lang/nimble#readme
"""
proc writeHelp*() =
proc writeHelp*(quit=true) =
echo(help)
quit(QuitSuccess)
if quit:
raise NimbleQuit(msg: "")
proc writeVersion() =
echo("nimble v$# compiled at $# $#" %
[nimbleVersion, CompileDate, CompileTime])
raise NimbleQuit(msg: "")
proc parseActionType*(action: string): ActionType =
case action.normalize()
of "install", "path":
case action.normalize()
of "install":
result = actionInstall
of "path":
result = actionPath
else:
discard
of "install":
result = actionInstall
of "path":
result = actionPath
of "build":
result = actionBuild
of "c", "compile", "js", "cpp", "cc":
@ -153,30 +157,14 @@ proc initAction*(options: var Options, key: string) =
options.action.arguments = @[]
options.action.flags = newStringTable()
of actionBuild, actionPublish, actionList, actionTasks,
actionNil, actionVersion: discard
actionNil: discard
proc prompt*(options: Options, question: string): bool =
## Asks an interactive question and returns the result.
##
## The proc will return immediately without asking the user if the global
## forcePrompts has a value different than dontForcePrompt.
case options.forcePrompts
of forcePromptYes:
echo(question & " -> [forced yes]")
return true
of forcePromptNo:
echo(question & " -> [forced no]")
return false
of dontForcePrompt:
echo(question & " [y/N]")
let yn = stdin.readLine()
case yn.normalize
of "y", "yes":
return true
of "n", "no":
return false
else:
return false
return prompt(options.forcePrompts, question)
proc renameBabelToNimble(options: Options) {.deprecated.} =
let babelDir = getHomeDir() / ".babel"
@ -191,7 +179,13 @@ proc renameBabelToNimble(options: Options) {.deprecated.} =
removeFile(nimbleDir / "babeldata.json")
proc getNimbleDir*(options: Options): string =
expandTilde(options.config.nimbleDir)
result =
if options.nimbleDir.len == 0:
options.config.nimbleDir
else:
options.nimbleDir
return expandTilde(result)
proc getPkgsDir*(options: Options): string =
options.getNimbleDir() / "pkgs"
@ -235,33 +229,53 @@ proc parseArgument*(key: string, result: var Options) =
discard
proc parseFlag*(flag, val: string, result: var Options) =
case result.action.typ
of actionCompile:
if val == "":
result.action.compileOptions.add("--" & flag)
var wasFlagHandled = true
# Global flags.
case flag.normalize()
of "help", "h": writeHelp()
of "version", "v": writeVersion()
of "accept", "y": result.forcePrompts = forcePromptYes
of "reject", "n": result.forcePrompts = forcePromptNo
of "nimbledir": result.nimbleDir = val
of "verbose": result.verbosity = LowPriority
of "debug": result.verbosity = DebugPriority
# Action-specific flags.
of "installed", "i":
if result.action.typ in [actionSearch, actionList]:
result.queryInstalled = true
else:
result.action.compileOptions.add("--" & flag & ":" & val)
of actionCustom:
result.action.flags[flag] = val
wasFlagHandled = false
of "depsonly", "d":
if result.action.typ == actionInstall:
result.depsOnly = true
else:
wasFlagHandled = false
of "ver":
if result.action.typ in [actionSearch, actionList]:
result.queryVersions = true
else:
wasFlagHandled = false
else:
# TODO: These should not be global.
case flag.normalize()
of "help", "h": writeHelp()
of "version", "v":
assert result.action.typ == actionNil
result.action.typ = actionVersion
of "accept", "y": result.forcePrompts = forcePromptYes
of "reject", "n": result.forcePrompts = forcePromptNo
of "ver": result.queryVersions = true
of "nimbledir": result.config.nimbleDir = val # overrides option from file
of "installed", "i": result.queryInstalled = true
of "depsonly", "d": result.depsOnly = true
case result.action.typ
of actionCompile:
if val == "":
result.action.compileOptions.add("--" & flag)
else:
result.action.compileOptions.add("--" & flag & ":" & val)
of actionCustom:
result.action.flags[flag] = val
else:
raise newException(NimbleError, "Unknown option: --" & flag)
wasFlagHandled = false
if not wasFlagHandled:
raise newException(NimbleError, "Unknown option: --" & flag)
proc initOptions*(): Options =
result.action.typ = actionNil
result.pkgInfoCache = newTable[string, PackageInfo]()
result.nimbleDir = ""
result.verbosity = HighPriority
proc parseMisc(options: var Options) =
# Load nimbledata.json
@ -278,9 +292,9 @@ proc parseMisc(options: var Options) =
proc parseCmdLine*(): Options =
result = initOptions()
result.config = parseConfig()
# Parse command line params.
# Parse command line params first. A simple `--version` shouldn't require
# a config to be parsed.
for kind, key, val in getOpt():
case kind
of cmdArgument:
@ -292,6 +306,12 @@ proc parseCmdLine*(): Options =
parseFlag(key, val, result)
of cmdEnd: assert(false) # cannot happen
# Set verbosity level.
setVerbosity(result.verbosity)
# Parse config.
result.config = parseConfig()
# Parse other things, for example the nimbledata.json file.
parseMisc(result)
@ -310,8 +330,8 @@ proc getProxy*(options: Options): Proxy =
elif existsEnv("https_proxy"):
url = getEnv("https_proxy")
except ValueError:
echo("WARNING: Unable to parse proxy from environment: ",
getCurrentExceptionMsg())
display("Warning:", "Unable to parse proxy from environment: " &
getCurrentExceptionMsg(), Warning, HighPriority)
if url.len > 0:
var parsed = parseUri(url)

View file

@ -1,7 +1,7 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
import parsecfg, json, streams, strutils, parseutils, os, sets, tables
import version, tools, common, options
import version, tools, common, options, cli
type
Package* = object
@ -117,13 +117,10 @@ proc fromJson(obj: JSonNode): Package =
proc readMetaData*(path: string): MetaData =
## Reads the metadata present in ``~/.nimble/pkgs/pkg-0.1/nimblemeta.json``
var bmeta = path / "nimblemeta.json"
if not existsFile(bmeta):
bmeta = path / "babelmeta.json"
if existsFile(bmeta):
echo("WARNING: using deprecated babelmeta.json file in " & path)
if not existsFile(bmeta):
result.url = ""
echo("WARNING: No nimblemeta.json file found in " & path)
display("Warning:", "No nimblemeta.json file found in " & path,
Warning, HighPriority)
return
# TODO: Make this an error.
let cont = readFile(bmeta)
@ -139,7 +136,7 @@ proc getPackage*(pkg: string, options: Options,
## convenience the proc returns a boolean specifying if the ``resPkg`` was
## successfully filled with good data.
for name, list in options.config.packageLists:
echo("Searching in \"", name, "\" package list...")
display("Reading", "$1 package list" % name, priority = LowPriority)
let packages = parseFile(options.getNimbleDir() /
"packages_" & name.toLowerAscii() & ".json")
for p in packages:
@ -179,8 +176,8 @@ proc findNimbleFile*(dir: string; error: bool): string =
raise newException(NimbleError,
"Specified directory does not contain a .nimble file.")
else:
# TODO: Abstract logging.
echo("WARNING: No .nimble file found for ", dir)
display("Warning:", "No .nimble file found for " & dir, Warning,
HighPriority)
proc getInstalledPkgsMin*(libsDir: string, options: Options):
seq[tuple[pkginfo: PackageInfo, meta: MetaData]] =
@ -279,7 +276,8 @@ proc validatePackagesList*(path: string): bool =
let pkgList = parseFile(path)
if pkgList.kind == JArray:
if pkgList.len == 0:
echo("WARNING: ", path, " contains no packages.")
display("Warning:", path & " contains no packages.", Warning,
HighPriority)
return true
except ValueError, JsonParsingError:
return false

View file

@ -1,7 +1,7 @@
# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
import parsecfg, json, streams, strutils, parseutils, os, tables
import version, tools, common, nimscriptsupport, options, packageinfo
import version, tools, common, nimscriptsupport, options, packageinfo, cli
## Contains procedures for parsing .nimble files. Moved here from ``packageinfo``
## because it depends on ``nimscriptsupport`` (``nimscriptsupport`` also
@ -20,11 +20,7 @@ proc newValidationError(msg: string, warnInstalled: bool): ref ValidationError =
result.warnInstalled = warnInstalled
proc raiseNewValidationError(msg: string, warnInstalled: bool) =
if warnInstalled:
# TODO: We warn everywhere for now. Raise the error in the next version.
echo("WARNING: ", msg, ". Will be an error in next version!")
else:
raise newValidationError(msg, warnInstalled)
raise newValidationError(msg, warnInstalled)
proc validatePackageName*(name: string) =
## Raises an error if specified package name contains invalid characters.
@ -91,10 +87,11 @@ proc validatePackageInfo(pkgInfo: PackageInfo, path: string) =
proc nimScriptHint*(pkgInfo: PackageInfo) =
if not pkgInfo.isNimScript:
# TODO: Turn this into a warning.
# TODO: Add a URL explaining more.
echo("NOTE: The .nimble file for this project could make use of " &
"additional features, if converted into the new NimScript format.")
display("Warning:", "The .nimble file for this project could make use of " &
"additional features, if converted into the new NimScript format." &
"\nFor more details see:" &
"https://github.com/nim-lang/nimble#creating-packages",
Warning, HighPriority)
proc multiSplit(s: string): seq[string] =
## Returns ``s`` split by newline and comma characters.
@ -242,6 +239,18 @@ proc getInstalledPkgs*(libsDir: string, options: Options):
## Gets a list of installed packages.
##
## ``libsDir`` is in most cases: ~/.nimble/pkgs/
const
readErrorMsg = "Installed package $1 v$2 is outdated or corrupt."
validationErrorMsg = readErrorMsg & "\nPackage did not pass validation: $3"
hintMsg = "The corrupted package will need to be removed manually. To fix" &
" this error message, remove $1."
proc createErrorMsg(tmplt, path, msg: string): string =
let (name, version) = getNameVersion(path)
return tmplt % [name, version, msg]
display("Loading", "list of installed packages", priority = MediumPriority)
result = @[]
for kind, path in walkDir(libsDir):
if kind == pcDir:
@ -254,17 +263,17 @@ proc getInstalledPkgs*(libsDir: string, options: Options):
result.add((pkg, meta))
except ValidationError:
let exc = (ref ValidationError)(getCurrentException())
exc.msg = createErrorMsg(validationErrorMsg, path, exc.msg)
exc.hint = hintMsg % path
if exc.warnInstalled:
echo("WARNING: Unable to read package info for " & path & "\n" &
" Package did not pass validation: " & exc.msg)
display("Warning:", exc.msg, Warning, HighPriority)
else:
exc.msg = "Unable to read package info for " & path & "\n" &
" Package did not pass validation: " & exc.msg
raise exc
except:
let exc = getCurrentException()
exc.msg = "Unable to read package info for " & path & "\n" &
" Error: " & exc.msg
let tmplt = readErrorMsg & "\nMore info: $3"
let msg = createErrorMsg(tmplt, path, getCurrentException().msg)
var exc = newException(NimbleError, msg)
exc.hint = hintMsg % path
raise exc
proc isNimScript*(nf: string, options: Options): bool =

View file

@ -4,8 +4,9 @@
## Implements 'nimble publish' to create a pull request against
## nim-lang/packages automatically.
import system except TResult
import httpclient, base64, strutils, rdstdin, json, os, browsers, times, uri
import tools, common
import tools, common, cli
type
Auth = object
@ -22,19 +23,20 @@ proc createHeaders(a: Auth): string =
"Accept: */*\c\L")
proc getGithubAuth(): Auth =
echo("Please create a new personal access token on Github in order to " &
"allow Nimble to fork the packages repository.")
display("Info:", "Please create a new personal access token on Github in" &
" order to allow Nimble to fork the packages repository.",
priority = HighPriority)
sleep(5000)
echo("Your default browser should open with the following URL: " &
"https://github.com/settings/tokens/new")
display("Info:", "Your default browser should open with the following URL: " &
"https://github.com/settings/tokens/new", priority = HighPriority)
sleep(3000)
openDefaultBrowser("https://github.com/settings/tokens/new")
result.token = readLineFromStdin("Personal access token: ").strip()
result.token = promptCustom("Personal access token?", "").strip()
let resp = getContent("https://api.github.com/user",
extraHeaders=createHeaders(result)).parseJson()
result.user = resp["login"].str
echo("Successfully verified as ", result.user)
display("Success:", "Verified as " & result.user, Success, HighPriority)
proc isCorrectFork(j: JsonNode): bool =
# Check whether this is a fork of the nimble packages repo.
@ -56,7 +58,7 @@ proc createFork(a: Auth) =
extraHeaders=createHeaders(a))
proc createPullRequest(a: Auth, packageName, branch: string) =
echo("Creating PR")
display("Info", "Creating PR", priority = HighPriority)
discard postContent("https://api.github.com/repos/nim-lang/packages/pulls",
extraHeaders=createHeaders(a),
body="""{"title": "Add package $1", "head": "$2:$3",
@ -129,17 +131,19 @@ proc publish*(p: PackageInfo) =
var pkgsDir = getTempDir() / "nimble-packages-fork"
if not forkExists(auth):
createFork(auth)
echo "waiting 10s to let Github create a fork ..."
display("Info:", "Waiting 10s to let Github create a fork",
priority = HighPriority)
os.sleep(10_000)
echo "... done"
display("Info:", "Finished waiting", priority = LowPriority)
if dirExists(pkgsDir):
echo("Removing old packages fork git directory.")
display("Removing", "old packages fork git directory.",
priority = LowPriority)
removeDir(pkgsDir)
echo "Cloning packages into: ", pkgsDir
display("Cloning", "packages into: " & pkgsDir, priority = HighPriority)
doCmd("git clone git@github.com:" & auth.user & "/packages " & pkgsDir)
# Make sure to update the clone.
echo("Updating the fork...")
display("Updating", "the fork", priority = HighPriority)
cd pkgsDir:
doCmd("git pull https://github.com/nim-lang/packages.git master")
doCmd("git push origin master")
@ -175,20 +179,20 @@ proc publish*(p: PackageInfo) =
"No .git nor .hg directory found. Stopping.")
if url.len == 0:
url = readLineFromStdin("Github URL of " & p.name & ": ")
url = promptCustom("Github URL of " & p.name & "?", "")
if url.len == 0: userAborted()
let tags = readLineFromStdin("Please enter a whitespace separated list of tags: ")
let tags = promptCustom("Whitespace separated list of tags?", "")
cd pkgsDir:
editJson(p, url, tags, downloadMethod)
let branchName = "add-" & p.name & getTime().getGMTime().format("HHmm")
doCmd("git checkout -B " & branchName)
doCmd("git commit packages.json -m \"Added package " & p.name & "\"")
echo("Pushing to remote of fork.")
display("Pushing", "to remote of fork.", priority = HighPriority)
doCmd("git push " & getPackageOriginUrl(auth) & " " & branchName)
createPullRequest(auth, p.name, branchName)
echo "Pull request successful."
display("Success:", "Pull request successful.", Success, HighPriority)
when isMainModule:
import packageinfo

View file

@ -3,7 +3,7 @@
#
# Various miscellaneous utility functions reside here.
import osproc, pegs, strutils, os, uri, sets, json, parseutils
import version, common
import version, common, cli
proc extractBin(cmd: string): string =
if cmd[0] == '"':
@ -20,11 +20,16 @@ proc doCmd*(cmd: string) =
stdout.flushFile()
stderr.flushFile()
let exitCode = execCmd(cmd)
displayDebug("Executing", cmd)
let (output, exitCode) = execCmdEx(cmd)
displayDebug("Finished", "with exit code " & $exitCode)
# TODO: Improve to show output in real-time.
displayDebug("Output", output)
if exitCode != QuitSuccess:
raise newException(NimbleError,
"Execution failed with exit code " & $exitCode)
"Execution failed with exit code $1\nCommand: $2\nOutput: $3" %
[$exitCode, cmd, output])
proc doCmdEx*(cmd: string): tuple[output: TaintedString, exitCode: int] =
let bin = extractBin(cmd)
@ -50,7 +55,7 @@ proc getNimrodVersion*: Version =
let vOutput = doCmdEx('"' & nimBin & "\" -v").output
var matches: array[0..MaxSubpatterns, string]
if vOutput.find(peg"'Version'\s{(\d+\.)+\d}", matches) == -1:
quit("Couldn't find Nim version.", QuitFailure)
raise newException(NimbleError, "Couldn't find Nim version.")
newVersion(matches[0])
proc samePaths*(p1, p2: string): bool =
@ -75,14 +80,14 @@ proc changeRoot*(origRoot, newRoot, path: string): string =
proc copyFileD*(fro, to: string): string =
## Returns the destination (``to``).
echo(fro, " -> ", to)
display("Copying", "file $# to $#" % [fro, to], priority = LowPriority)
copyFileWithPermissions(fro, to)
result = to
proc copyDirD*(fro, to: string): seq[string] =
## Returns the filenames of the files in the directory that were copied.
result = @[]
echo("Copying directory: ", fro, " -> ", to)
display("Copying", "directory $# to $#" % [fro, to], priority = LowPriority)
for path in walkDirRec(fro):
createDir(changeRoot(fro, to, path.splitFile.dir))
result.add copyFileD(path, changeRoot(fro, to, path))

View file

@ -34,6 +34,7 @@ type
ParseVersionError* = object of ValueError
NimbleError* = object of Exception
hint*: string
proc newVersion*(ver: string): Version = return Version(ver)
proc newSpecial*(spe: string): Special = return Special(spe)

View file

@ -93,15 +93,15 @@ test "can refresh with custom urls":
url = "http://nim-lang.org/nimble/packages.json"
""".unindent)
let (output, exitCode) = execNimble(["refresh"])
let (output, exitCode) = execNimble(["refresh", "--verbose"])
let lines = output.strip.splitLines()
check exitCode == QuitSuccess
check inLines(lines, "reading from config file")
check inLines(lines, "downloading \"official\" package list")
check inLines(lines, "trying http://google.com")
check inLines(lines, "config file at")
check inLines(lines, "official package list")
check inLines(lines, "http://google.com")
check inLines(lines, "packages.json file is invalid")
check inLines(lines, "404 not found")
check inLines(lines, "done")
check inLines(lines, "Package list downloaded.")
# Restore config
if fileExists(configBakFile):
@ -113,24 +113,24 @@ test "can install nimscript package":
test "can execute nimscript tasks":
cd "nimscript":
let (output, exitCode) = execNimble("test")
let (output, exitCode) = execNimble("--verbose", "test")
let lines = output.strip.splitLines()
check exitCode == QuitSuccess
check lines[^1] == "10"
test "can use nimscript's setCommand":
cd "nimscript":
let (output, exitCode) = execNimble("cTest")
let (output, exitCode) = execNimble("--verbose", "cTest")
let lines = output.strip.splitLines()
check exitCode == QuitSuccess
check "Hint: operation successful".normalize in lines[^1].normalize
check "Compilation finished".normalize in lines[^1].normalize
test "can use nimscript's setCommand with flags":
cd "nimscript":
let (output, exitCode) = execNimble("cr")
let (output, exitCode) = execNimble("--debug", "cr")
let lines = output.strip.splitLines()
check exitCode == QuitSuccess
check "Hello World".normalize in lines[^1].normalize
check "Hello World".normalize in lines[^2].normalize
test "can list nimscript tasks":
cd "nimscript":
@ -166,8 +166,8 @@ test "can reject same version dependencies":
# stderr output being generated and flushed without first flushing stdout
let ls = outp.strip.splitLines()
check exitCode != QuitSuccess
check ls[ls.len-1] == "Error: unhandled exception: Cannot satisfy the " &
"dependency on PackageA 0.2.0 and PackageA 0.5.0 [NimbleError]"
check "Cannot satisfy the dependency on PackageA 0.2.0 and PackageA 0.5.0" in
ls[ls.len-1]
test "can update":
check execNimble("update").exitCode == QuitSuccess
@ -194,7 +194,7 @@ test "issue #126":
cd "issue126/b":
let (output1, exitCode1) = execNimble("install", "-y")
let lines1 = output1.strip.splitLines()
check exitCode1 == QuitSuccess
check exitCode1 != QuitSuccess
check inLines(lines1, "The .nimble file name must match name specified inside")
test "issue #108":
@ -202,7 +202,7 @@ test "issue #108":
let (output, exitCode) = execNimble("build")
let lines = output.strip.splitLines()
check exitCode != QuitSuccess
check "Nothing to build" in lines[^1]
check inLines(lines, "Nothing to build")
test "issue #206":
cd "issue206":
@ -222,8 +222,8 @@ test "can uninstall":
let ls = outp.strip.splitLines()
check exitCode != QuitSuccess
check ls[ls.len-1] == " Cannot uninstall issue27b (0.1.0) because " &
"issue27a (0.1.0) depends on it [NimbleError]"
check "Cannot uninstall issue27b (0.1.0) because issue27a (0.1.0) depends" &
" on it" in ls[ls.len-1]
check execNimble("uninstall", "-y", "issue27").exitCode == QuitSuccess
check execNimble("uninstall", "-y", "issue27a").exitCode == QuitSuccess
@ -234,8 +234,8 @@ test "can uninstall":
let (outp, exitCode) = execNimble("uninstall", "-y", "PackageA")
check exitCode != QuitSuccess
let ls = outp.processOutput()
check ls[ls.len-2].startsWith(" Cannot uninstall PackageA ")
check ls[ls.len-1].startsWith(" Cannot uninstall PackageA ")
check inLines(ls, "Cannot uninstall PackageA (0.2.0)")
check inLines(ls, "Cannot uninstall PackageA (0.6.0)")
check execNimble("uninstall", "-y", "PackageBin2").exitCode == QuitSuccess
# Case insensitive