diff --git a/.gitignore b/.gitignore index 5744c47..bac71d6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,15 @@ nimcache/ # Absolute paths /src/babel /src/nimble -/tests/tester # executables from test and build /nimble -/tests/nimscript/nimscript -/tests/issue27/issue27 +src/nimblepkg/cli +src/nimblepkg/packageinfo +src/nimblepkg/packageparser +src/nimblepkg/reversedeps +src/nimblepkg/version +src/nimblepkg/download # Windows executables *.exe @@ -19,4 +22,13 @@ nimcache/ # VCC compiler and linker artifacts *.ilk -*.pdb \ No newline at end of file +*.pdb + +# Editors and IDEs project files and folders +.vscode + +# VCS artifacts +*.orig + +# Test procedure artifacts +nimble_*.nims diff --git a/.travis.yml b/.travis.yml index f9acefc..f1ff69e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,24 @@ os: + - windows - linux -dist: trusty + - osx language: c -install: - - | - wget https://nim-lang.org/download/nim-0.17.2.tar.xz - tar -xf nim-0.17.2.tar.xz - cd nim-0.17.2 - sh build.sh - cd .. +env: + - BRANCH=0.19.6 + - BRANCH=0.20.2 + - BRANCH=1.0.6 + # This is the latest working Nim version against which Nimble is being tested + - BRANCH=#ab525cc48abdbbbed1f772e58e9fe21474f70f07 -before_script: - - set -e - - set -x - - export PATH=`pwd`/nim-0.17.2/bin:$PATH +cache: + directories: + - "$HOME/.choosenim" + +install: + - curl https://gist.github.com/genotrance/fb53504a4fba88bc5201d3783df5c522/raw/travis.sh -LsSf -o travis.sh + - source travis.sh script: - cd tests diff --git a/changelog.markdown b/changelog.markdown index 29c1bef..92f1d31 100644 --- a/changelog.markdown +++ b/changelog.markdown @@ -3,6 +3,122 @@ # Nimble changelog +## 0.11.0 - 22/09/2019 + +This is a major release containing nearly 60 commits. Most changes are +bug fixes, but this release also includes a couple new features: + +- Binaries can now be built and run using the new ``run`` command. +- The ``NimblePkgVersion`` is now defined so you can easily get the package + version in your source code + ([example](https://github.com/nim-lang/nimble/blob/4a2aaa07d/tests/nimbleVersionDefine/src/nimbleVersionDefine.nim)). + +Some other highlights: + +- Temporary files are now kept when the ``--debug`` flag is used. +- Fixed dependency resolution issues with "#head" packages (#432 and #672). +- The `install` command can now take Nim compiler flags via the new + ``--passNim`` flag. +- Command line arguments are now passed properly to tasks (#633). +- The ``test`` command now respects the specified backend (#631). +- The ``dump`` command will no longer prompt and now has an implicit ``-y``. +- Fixed bugs with the new nimscript executor (#665). +- Fixed multiple downloads and installs of the same package (#678). +- Nimble init no longer overwrites existing files (#581). +- Fixed incorrect submodule version being pulled when in a non-master branch (#675). + +---- + +Full changelog: https://github.com/nim-lang/nimble/compare/v0.10.2...v0.11.0 + +## 0.10.2 - 03/06/2019 + +This is a small release which avoids object variant changes that are now +treated as runtime errors (Nim #1286). It also adds support for `backend` +selection during `nimble init`. + +Multiple bug fixes are also included: +- Fixed an issue where failing tasks were not returning a non-zero return + value (#655). +- Error out if `bin` is a Nim source file (#597). +- Fixed an issue where nimble task would not run if file of same name exists. +- Fixed an issue that prevented multiple instances of nimble from running on + the same package. + +---- + +Full changelog: https://github.com/nim-lang/nimble/compare/v0.10.0...v0.10.2 + +## 0.10.0 - 27/05/2019 + +Nimble now uses the Nim compiler directly via `nim e` to execute nimble +scripts rather than embedding the Nim VM. This has multiple benefits: +- Evolve independently from Nim enabling new versions of Nimble to work + with multiple versions of Nim. +- Inherit all nimscript enhancements and bug fixes rather than having to + duplicate functionality. +- Fast build time and smaller binary. +- No dependency on the compiler package which could cause dependency issues + when nimble is used as a package. + +Several other features and fixes have been implemented to improve general +development and test workflows. +- `nimble test` now sports a `-continue` or `-c` flag that allows tests + to continue on failure, removes all created test binaries on completion + and warns if no tests found. +- The `--inclDeps` or `-i` flag enables `nimble uninstall` to remove all + dependent packages during uninstall. +- Added documentation on the usage of a custom `nimbleDir`. +- Package type interactive prompt is more readable. +- Save temporary files in a per-user temp dir to enable Nimble on multi-user + systems. +- CTRL-C is now handled correctly in interactive prompts. +- Fixed issue where empty package list led to error. +- Fixed issue where file:// was prepended incorrectly. +- Fixed miscellaneous issues in version parsing, Github auth and briefClone. +- Miscellaneous cleanup of deprecated procs. + +---- + +Full changelog: https://github.com/nim-lang/nimble/compare/v0.9.0...v0.10.0 + +## 0.9.0 - 19/09/2018 + +This is a major new release which contains at least one breaking change. +Unfortunately even though it was planned, support for lock files did not +make it into this release. The release does +however contain a large number of fixes spread across 57 commits. + +The breaking change in this release is to do with the handling of binary +package. **Any package that specifies a ``bin`` value in it's .nimble file** +**will no longer install any Nim source code files**, in other words it's not +going to be a hybrid package by default. This means that so called "hybrid +packages" now need to specify ``installExt = @["nim"]`` in their metadata, +otherwise they will become binary packages only. + +- **Breaking:** hybrid packages require ``installExt = @["nim"]`` + ([Commit](https://github.com/nim-lang/nimble/commit/09091792615eacd503e87ca70252c572a4bde2b5)) +- **The ``init`` command can now show a list of choices for information such as** + **the license.** +- **The ``init`` command now creates correct project structures for all package** + **types.** +- **Fatal errors are no longer created when the path inside a .nimble-link file** + **doesn't exist.** +- **The ``develop`` command now always clones HEAD and grabs the full repo history.** +- **The default ``test`` task no longer executes all tests (only those starting with 't').** +- Colour is no longer used when `isatty` is false. +- ``publish`` now shows the URL of the created PR. +- The ``getPkgDir`` procedure has been fixed in the Nimble file API. +- Improved handling of proxy environment variables. +- Codebase has been improved not to rely on `nil` in strings and seqs. +- The handling of pre- and post-hooks has been improved significantly. +- Fixed the ``path`` command for packages with a ``srcDir`` and optimised the + package look-up. + +---- + +Full changelog: https://github.com/nim-lang/nimble/compare/v0.8.10...v0.9.0 + ## 0.8.10 - 23/02/2018 The first release of 2018! Another fairly big release containing 40 commits. diff --git a/nimble.nimble b/nimble.nimble index 68b439b..6cd7e9e 100644 --- a/nimble.nimble +++ b/nimble.nimble @@ -1,26 +1,17 @@ -import ospaths -template thisModuleFile: string = instantiationInfo(fullPaths = true).filename - -when fileExists(thisModuleFile.parentDir / "src/nimblepkg/common.nim"): - # In the git repository the Nimble sources are in a ``src`` directory. - import src/nimblepkg/common -else: - # When the package is installed, the ``src`` directory disappears. - import nimblepkg/common - # Package -version = nimbleVersion +version = "0.11.0" author = "Dominik Picheta" description = "Nim package manager." license = "BSD" bin = @["nimble"] srcDir = "src" +installExt = @["nim"] # Dependencies -requires "nim >= 0.13.0", "compiler#head" +requires "nim >= 0.13.0" when defined(nimdistros): import distros diff --git a/readme.markdown b/readme.markdown index c414684..89f7a22 100644 --- a/readme.markdown +++ b/readme.markdown @@ -15,6 +15,7 @@ Interested in learning **how to create a package**? Skip directly to that sectio - [nimble install](#nimble-install) - [nimble uninstall](#nimble-uninstall) - [nimble build](#nimble-build) + - [nimble run](#nimble-run) - [nimble c](#nimble-c) - [nimble list](#nimble-list) - [nimble search](#nimble-search) @@ -74,10 +75,15 @@ not need to install Nimble manually**. But in case you still want to install Nimble manually, you can follow the following instructions. -There are two ways to install Nimble manually. The first is using the -``koch`` tool included in the Nim distribution and +There are two ways to install Nimble manually. Using ``koch`` and using Nimble +itself. + +### Using koch + +The ``koch`` tool is included in the Nim distribution and [repository](https://github.com/nim-lang/Nim/blob/devel/koch.nim). -Simply execute the following command to compile and install Nimble. +Simply navigate to the location of your Nim installation and execute the +following command to compile and install Nimble. ``` ./koch nimble @@ -86,23 +92,19 @@ Simply execute the following command to compile and install Nimble. This will clone the Nimble repository, compile Nimble and copy it into Nim's bin directory. -The second approach is to install Nimble as a Nimble package. You can do this -by compiling Nimble, then running ``nimble install`` in Nimble's directory. +### Using Nimble + +In most cases you will already have Nimble installed, you can install a newer +version of Nimble by simply running the following command: ``` -git clone https://github.com/nim-lang/nimble.git -cd nimble -nim c src/nimble -src/nimble install +nimble install nimble ``` -**Note for Windows users**: You will need to rename ``nimble.exe`` after -compilation to something else like ``nimble1.exe``, then run -``src\nimble1.exe install``. +This will download the latest release of Nimble and install it on your system. -This will install Nimble to the default Nimble packages location: -``~/.nimble/pkgs``. The binary will be installed to ``~/.nimble/bin``, so you -will need to add this directory to your PATH. +Note that you must have `~/.nimble/bin` in your PATH for this to work, if you're +using choosenim then you likely already have this set up correctly. ## Nimble usage @@ -170,12 +172,13 @@ example: This is of course Git-specific, for Mercurial, use ``tip`` instead of ``head``. A branch, tag, or commit hash may also be specified in the place of ``head``. -Instead of specifying a VCS branch, you may also specify a version range, for -example: +Instead of specifying a VCS branch, you may also specify a concrete version or a +version range, for example: + $ nimble install nimgame@0.5 $ nimble install nimgame@"> 0.5" -In this case a version which is greater than ``0.5`` will be installed. +The latter command will install a version which is greater than ``0.5``. If you don't specify a parameter and there is a ``package.nimble`` file in your current working directory then Nimble will install the package residing in @@ -221,8 +224,8 @@ instead of a name. ### nimble uninstall The ``uninstall`` command will remove an installed package. Attempting to remove -a package which other packages depend on is disallowed and will result in an -error. You must currently manually remove the reverse dependencies first. +a package which other packages depend on will result in an error. You can use the +``--inclDeps`` or ``-i`` flag to remove all dependent packages along with the package. Similar to the ``install`` command you can specify a version range, for example: @@ -236,6 +239,13 @@ 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 run + +The ``run`` command can be used to build and run any binary specified in your +package's ``bin`` list. You can pass any compilation flags you wish by specifying +them before the ``run`` command, and you can specify arguments for your binary +by specifying them after the ``run`` command. + ### nimble c The ``c`` (or ``compile``, ``js``, ``cc``, ``cpp``) command can be used by @@ -303,7 +313,7 @@ which can be useful to read the bundled documentation. Example: ### nimble init The nimble ``init`` command will start a simple wizard which will create -a quick ``.nimble`` file for your project. +a quick ``.nimble`` file for your project in the current directory. As of version 0.7.0, the ``.nimble`` file that this command creates will use the new NimScript format. @@ -313,7 +323,7 @@ Check out the [Creating Packages](#creating-packages) section for more info. Publishes your Nimble package to the official Nimble package repository. -**Note:** Requires a valid GitHub account. +**Note:** Requires a valid GitHub account with an SSH key attached to it. To upload your public key onto your GitHub account, follow [this link](https://github.com/settings/keys). ### nimble tasks @@ -391,7 +401,7 @@ a package. A .nimble file can be created easily using Nimble's ``init`` command. This command will ask you a bunch of questions about your package, then generate a -.nimble file for you. +.nimble file for you in the current directory. A bare minimum .nimble file follows: @@ -418,6 +428,7 @@ You can also specify multiple dependencies like so: requires "nim >= 0.10.0", "foobar >= 0.1.0" requires "fizzbuzz >= 1.0" +requires "https://github.com/user/pkg#5a54b5e" ``` Nimble currently supports installation of packages from a local directory, a @@ -494,7 +505,7 @@ For a package named "foobar", the recommended project structure is the following └── src └── foobar.nim # Imported via `import foobar` └── tests # Contains the tests - ├── nim.cfg + ├── config.nims ├── tfoo1.nim # First test └── tfoo2.nim # Second test @@ -647,7 +658,7 @@ combo. 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 +running ``nimble build`` or ``nimble install`` in the directory of your package. ### Hybrids @@ -727,13 +738,13 @@ installing your package (on macOS): ``` Hint: This package requires some external dependencies. Hint: To install them you may be able to run: - Hint: sudo brew install openssl + Hint: brew install openssl ``` ### Nim compiler The Nim compiler cannot read .nimble files. Its knowledge of Nimble is -limited to the ``nimblePaths`` feature which allows it to use packages installed +limited to the ``nimblePath`` feature which allows it to use packages installed in Nimble's package directory when compiling your software. This means that it cannot resolve dependencies, and it can only use the latest version of a package when compiling. @@ -746,6 +757,27 @@ This means that you can safely compile using the compiler when developing your software, but you should use Nimble to build the package before publishing it to ensure that the dependencies you specified are correct. +### Compile with `nim` after changing the nimble directory + +The Nim compiler has been preconfigured to look at the default nimble directory while compiling, +so no extra step is required to use nimble managed packages in your code. +However, if you are using a custom `nimbleDir`, you need to specify the +`--nimblePath:PATH` option. For example, +if your `nimble` directory is located at `/some/custom/path/nimble`, this should work: + +``` +nim c --nimblePath:/some/custom/path/nimble/pkgs main.nim +``` + +Some code editors rely on `nim check` to check for errors under the hood (e.g. VScode), +and the editor extension may not allow users to pass custom option to `nim check`, which +will cause `nim check` to scream `Error: cannot open file:`. In this case, +you will have to use [Nim compiler's configuration files](https://nim-lang.org/docs/nimc.html#compiler-usage-configuration-files). Simply add the line: +``` +nimblePath = "/some/custom/path/nimble/pkgs" +``` +to the `nim.cfg` located in any directory listed in the [documentation](https://nim-lang.org/docs/nimc.html#compiler-usage-configuration-files), this should resolve the problem. + ### Versions Versions of cloned packages via Git or Mercurial are determined through the @@ -781,6 +813,19 @@ To summarise, the steps for release are: Once the new tag is in the remote repository, Nimble will be able to detect the new version. +##### Git Version Tagging + +Use dot separated numbers to represent the release version in the git +tag label. Nimble will parse these git tag labels to know which +versions of a package are published. + +``` text +v0.2.0 # 0.2.0 +v1 # 1 +v1.2.3-zuzu # 1.2.3 +foo-1.2.3.4 # 1.2.3.4 +``` + ## Publishing packages Publishing packages isn't a requirement. But doing so allows people to associate @@ -881,6 +926,10 @@ flag to the file ```src/nimble.nim.cfg```. After that, you can run ```src/nimble install``` and overwrite the existing installation. +* ``Could not download: error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure`` + +If you are on macOS, you need to set and export the ```DYLD_LIBRARY_PATH``` environment variable to the directory where your OpenSSL libraries are. For example, if you use OpenSSL, you have to set ```export DYLD_LIBRARY_PATH=/usr/local/opt/openssl/lib``` in your ```$HOME/.bashrc``` file. + * ``Error: ambiguous identifier: 'version' --use nimscriptapi.version or system.version`` Make sure that you are running at least version 0.16.0 of Nim (or the latest nightly). @@ -901,7 +950,7 @@ The ``master`` branch is... * default * bleeding edge -* tested to compile with the latest Nim version +* tested to compile with a pinned (close to HEAD) commit of Nim The ``stable`` branch is... diff --git a/src/nimble.nim b/src/nimble.nim index f51f7fd..d7e011e 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -3,20 +3,21 @@ import system except TResult -import httpclient, parseopt, os, osproc, pegs, tables, parseutils, - strtabs, json, algorithm, sets, uri, future, sequtils +import os, tables, strtabs, json, algorithm, sets, uri, sugar, sequtils, osproc +import std/options as std_opt import strutils except toLower from unicode import toLower from sequtils import toSeq +from strformat import fmt 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/nimscriptexecutor, nimblepkg/init -import nimblepkg/nimscriptsupport +import nimblepkg/nimscriptwrapper proc refresh(options: Options) = ## Downloads the package list from the specified URL. @@ -95,7 +96,7 @@ proc copyFilesRec(origDir, currentDir, dest: 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 = initSet[string]() + result = initHashSet[string]() let whitelistMode = pkgInfo.installDirs.len != 0 or pkgInfo.installFiles.len != 0 or @@ -156,7 +157,8 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = "dependencies for $1@$2" % [pkginfo.name, pkginfo.specialVersion], priority = HighPriority) - var pkgList = getInstalledPkgsMin(options.getPkgsDir(), options) + 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": @@ -190,11 +192,6 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = else: display("Info:", "Dependency on $1 already satisfied" % $dep, priority = HighPriority) - if pkg.isLinked: - # TODO (#393): This can be optimised since the .nimble-link files have - # a secondary line that specifies the srcDir. - pkg = pkg.toFullInfo(options) - result.add(pkg) # Process the dependencies of this dependency. result.add(processDeps(pkg.toFullInfo(options), options)) @@ -204,12 +201,13 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = # 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] != pkgInfo.version: + pkgsInPath[pkgInfo.name] != currentVer: raise newException(NimbleError, "Cannot satisfy the dependency on $1 $2 and $1 $3" % - [pkgInfo.name, pkgInfo.version, pkgsInPath[pkgInfo.name]]) - pkgsInPath[pkgInfo.name] = pkgInfo.version + [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 @@ -218,16 +216,33 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[PackageInfo] = for i in reverseDeps: addRevDep(options.nimbleData, i, pkginfo) -proc buildFromDir(pkgInfo: PackageInfo, paths: seq[string], - args: var seq[string]) = +proc buildFromDir( + pkgInfo: PackageInfo, paths, args: seq[string], + options: Options +) = ## Builds a package as specified by ``pkgInfo``. + let binToBuild = options.getCompilationBinary(pkgInfo) + # 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?") - let realDir = pkgInfo.getRealDir() + 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) @@ -236,10 +251,15 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: seq[string], if not existsDir(outputDir): createDir(outputDir) + let input = realDir / bin.changeFileExt("nim") + # `quoteShell` would be more robust than `\"` (and avoid quoting when + # un-necessary) but would require changing `extractBin` + let cmd = "\"$#\" $# --noNimblePath $# $# $# \"$#\"" % + [getNimBin(), pkgInfo.backend, nimblePkgVersion, + join(args, " "), outputOpt, input] try: - doCmd("\"" & getNimBin() & "\" $# --noBabelPath $# $# \"$#\"" % - [pkgInfo.backend, join(args, " "), outputOpt, - realDir / bin.changeFileExt("nim")]) + doCmd(cmd, showCmd = true) + binariesBuilt.inc() except NimbleError: let currentExc = (ref NimbleError)(getCurrentException()) let exc = newException(BuildFailed, "Build failed for package: " & @@ -249,13 +269,14 @@ proc buildFromDir(pkgInfo: PackageInfo, paths: seq[string], exc.hint = hint raise exc -proc buildFromDir(pkgInfo: PackageInfo, paths: seq[string], forRelease: bool) = - var args: seq[string] - if forRelease: - args = @["-d:release"] - else: - args = @[] - buildFromDir(pkgInfo, paths, args) + 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``. @@ -330,6 +351,13 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, ## 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() @@ -354,7 +382,11 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, # 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()) - buildFromDir(pkgInfo, paths, true) + let flags = if options.action.typ in {actionInstall, actionPath, actionUninstall, actionDevelop}: + options.action.passNimFlags + else: + @[] + buildFromDir(pkgInfo, paths, "-d:release" & flags, options) let pkgDestDir = pkgInfo.getPkgDest(options) if existsDir(pkgDestDir) and existsFile(pkgDestDir / "nimblemeta.json"): @@ -379,7 +411,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, createDir(pkgDestDir) # Copy this package's files based on the preferences specified in PkgInfo. - var filesInstalled = initSet[string]() + var filesInstalled = initHashSet[string]() iterInstallFiles(realDir, pkgInfo, options, proc (file: string) = createDir(changeRoot(realDir, pkgDestDir, file.splitFile.dir)) @@ -392,7 +424,7 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, pkgInfo.myPath) filesInstalled.incl copyFileD(pkgInfo.myPath, dest) - var binariesInstalled = initSet[string]() + var binariesInstalled = initHashSet[string]() if pkgInfo.bin.len > 0: # Make sure ~/.nimble/bin directory is created. createDir(binDir) @@ -433,6 +465,12 @@ proc installFromDir(dir: string, requestedVer: VersionRange, options: Options, 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]) = @@ -472,17 +510,7 @@ proc install(packages: seq[PkgTuple], let (downloadDir, downloadVersion) = downloadPkg(url, pv.ver, meth, subdir, options) try: - # Run pre-install hook in download directory now that package is downloaded - cd downloadDir: - if not execHook(options, true): - raise newException(NimbleError, "Pre-hook prevented further execution.") - result = installFromDir(downloadDir, pv.ver, options, url) - - # Run post-install hook in installed directory now that package is installed - # Standard hooks run in current directory so it won't detect this new package - cd result.pkg.myPath.parentDir(): - discard execHook(options, false) except BuildFailed: # The package failed to build. # Check if we tried building a tagged version of the package. @@ -508,12 +536,12 @@ proc build(options: Options) = nimScriptHint(pkgInfo) let deps = processDeps(pkginfo, options) let paths = deps.map(dep => dep.getRealDir()) - var args = options.action.compileOptions - buildFromDir(pkgInfo, paths, args) + var args = options.getCompilationFlags() + buildFromDir(pkgInfo, paths, args, options) -proc execBackend(options: Options) = +proc execBackend(pkgInfo: PackageInfo, options: Options) = let - bin = options.action.file + bin = options.getCompilationBinary(pkgInfo).get() binDotNim = bin.addFileExt("nim") if bin == "": raise newException(NimbleError, "You need to specify a file.") @@ -526,9 +554,10 @@ proc execBackend(options: 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.action.compileOptions: + for option in options.getCompilationFlags(): args.add("\"" & option & "\" ") let backend = @@ -543,8 +572,8 @@ proc execBackend(options: Options) = else: display("Generating", ("documentation for $1 (from package $2) using $3 " & "backend") % [bin, pkgInfo.name, backend], priority = HighPriority) - doCmd("\"" & getNimBin() & "\" $# --noNimblePath $# \"$#\"" % - [backend, args, bin], showOutput = true) + doCmd("\"" & getNimBin() & "\" $# --noNimblePath $# $# \"$#\"" % + [backend, nimblePkgVersion, args, bin], showOutput = true) display("Success:", "Execution finished", Success, HighPriority) proc search(options: Options) = @@ -560,7 +589,7 @@ proc search(options: Options) = var found = false template onFound {.dirty.} = echoPackage(pkg) - if options.queryVersions: + if pkg.alias.len == 0 and options.queryVersions: echoPackageVersions(pkg) echo(" ") found = true @@ -586,7 +615,7 @@ proc list(options: Options) = let pkgList = getPackageList(options) for pkg in pkgList: echoPackage(pkg) - if options.queryVersions: + if pkg.alias.len == 0 and options.queryVersions: echoPackageVersions(pkg) echo(" ") @@ -619,29 +648,24 @@ proc listPaths(options: Options) = ## On success the proc returns normally. cli.setSuppressMessages(true) 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 + 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 kind, path in walkDir(options.getPkgsDir): - if kind != pcDir or not path.startsWith(options.getPkgsDir / name & "-"): - continue - - var nimbleFile = findNimbleFile(path, false) - if nimbleFile.existsFile: - var pkgInfo = getPkgInfo(path, options) + for x in pkgs.items(): + let + pName = x.pkginfo.name + pVer = x.pkginfo.specialVersion + if name == pName: var v: VersionAndPath - v.version = newVersion(pkgInfo.specialVersion) - v.path = pkgInfo.getRealDir() + v.version = newVersion(pVer) + v.path = x.pkginfo.getRealDir() installed.add(v) - else: - display("Warning:", "No .nimble file found for " & path, Warning, - MediumPriority) if installed.len > 0: sort(installed, cmp[VersionAndPath], Descending) @@ -705,44 +729,59 @@ proc dump(options: Options) = echo "backend: ", p.backend.escape proc init(options: Options) = - var nimbleFile: string = "" + # Check whether the vcs is installed. + let vcsBin = options.action.vcsOption + if vcsBin != "" and findExe(vcsBin, true) == "": + raise newException(NimbleError, "Please install git or mercurial first") - if options.forcePrompts != forcePromptYes: - 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.", priority = HighPriority) - - # Ask for package name. + # Determine the package name. let pkgName = if options.action.projName != "": options.action.projName else: os.getCurrentDir().splitPath.tail.toValidPackageName() - nimbleFile = pkgName.changeFileExt("nimble") - validatePackageName(nimbleFile.changeFileExt("")) + # Validate the package name. + validatePackageName(pkgName) - if existsFile(os.getCurrentDir() / nimbleFile): - raise newException(NimbleError, "Nimble file already exists.") + # 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) - # Ask for package author + # 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") != "": - let (name, exitCode) = doCmdEx("git config --global user.name") - if exitCode == QuitSuccess and name.len > 0: - result = name.strip() - display("Using", "$# for new package author" % [result.escape()], - priority = HighPriority) + result = getAuthorWithCmd("git config --global user.name") elif findExe("hg") != "": - let (name, exitCode) = doCmdEx("hg config ui.username") - if exitCode == QuitSuccess and name.len > 0: - result = name.strip() - display("Using", "$# for new package author" % [result.escape()], - priority = HighPriority) + result = getAuthorWithCmd("hg config ui.username") if result.len == 0: result = promptCustom(options, "Your name?", "Anonymous") let pkgAuthor = getAuthor() @@ -753,10 +792,16 @@ proc init(options: Options) = priority = HighPriority) # Determine the type of package - let pkgType = promptList(options, "Package type?", [ - "lib", - "bin", - ]) + 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") @@ -767,105 +812,72 @@ proc init(options: Options) = "A new awesome nimble package") # Ask for license - let pkgLicense = options.promptList("Package 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", - "BSD2", - "GPLv3", - "LGPLv3", - "Apache2", + "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) - let pkgTestDir = "tests" + createPkgStructure( + ( + pkgName, + pkgVersion, + pkgAuthor, + pkgDesc, + pkgLicense, + pkgBackend, + pkgSrcDir, + pkgNimDep, + pkgType + ), + pkgRoot + ) - # Create source directory - os.createDir(pkgSrcDir) + # Create a git or hg repo in the new nimble project. + if vcsBin != "": + let cmd = fmt"cd {pkgRoot} && {vcsBin} init" + let ret: tuple[output: string, exitCode: int] = execCmdEx(cmd) + if ret.exitCode != 0: quit ret.output - display("Success:", "Source directory created successfully", Success, - MediumPriority) - - # Create initial source file - cd pkgSrcDir: - let pkgFile = pkgName.changeFileExt("nim") - try: - if pkgType == "bin": - pkgFile.writeFile "# Hello Nim!\necho \"Hello, World!\"\n" - else: - pkgFile.writeFile """# $# -# Copyright $# -# $# -""" % [pkgName, pkgAuthor, pkgDesc] - display("Success:", "Created initial source file successfully", Success, - MediumPriority) - except: - raise newException(NimbleError, "Unable to open file " & pkgFile & - " for writing: " & osErrorMsg(osLastError())) - - # Create test directory - os.createDir(pkgTestDir) - - display("Success:", "Test directory created successfully", Success, - MediumPriority) - - cd pkgTestDir: - try: - "test1.nims".writeFile("""switch("path", "$$projectDir/../$#")""" % - [pkgSrcDir]) - display("Success:", "Test config file created successfully", Success, - MediumPriority) - except: - raise newException(NimbleError, "Unable to open file " & "test1.nims" & - " for writing: " & osErrorMsg(osLastError())) - try: - "test1.nim".writeFile("doAssert(1 + 1 == 2)\n") - display("Success:", "Test file created successfully", Success, - MediumPriority) - except: - raise newException(NimbleError, "Unable to open file " & "test1.nim" & - " for writing: " & osErrorMsg(osLastError())) - - # Write the nimble file - try: - if pkgType == "lib": - nimbleFile.writeFile """# Package - -version = $# -author = $# -description = $# -license = $# -srcDir = $# - -# Dependencies - -requires "nim >= $#" -""" % [pkgVersion.escape(), pkgAuthor.escape(), pkgDesc.escape(), - pkgLicense.escape(), pkgSrcDir.escape(), pkgNimDep] - else: - nimbleFile.writeFile """# Package - -version = $# -author = $# -description = $# -license = $# -srcDir = $# -bin = @[$#] - -# Dependencies - -requires "nim >= $#" -""" % [pkgVersion.escape(), pkgAuthor.escape(), pkgDesc.escape(), - pkgLicense.escape(), pkgSrcDir.escape(), pkgName.escape(), pkgNimDep] - except: - raise newException(NimbleError, "Unable to open file " & "test1.nim" & - " for writing: " & osErrorMsg(osLastError())) - - display("Success:", "Nimble file created successfully", Success, - MediumPriority) + var ignoreFile = if vcsBin == "git": ".gitignore" else: ".hgignore" + var fd = open(joinPath(pkgRoot, ignoreFile), fmWrite) + fd.write(pkgName & "\n") + fd.close() display("Success:", "Package $# created successfully" % [pkgName], Success, HighPriority) @@ -875,7 +887,8 @@ proc uninstall(options: Options) = raise newException(NimbleError, "Please specify the package(s) to uninstall.") - var pkgsToDelete: seq[PackageInfo] = @[] + var pkgsToDelete: HashSet[PackageInfo] + pkgsToDelete.init() # Do some verification. for pkgTup in options.action.packages: display("Looking", "for $1 ($2)" % [pkgTup.name, $pkgTup.ver], @@ -886,34 +899,33 @@ proc uninstall(options: Options) = raise newException(NimbleError, "Package not found") 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 # uninstall. - let revDeps = getRevDeps(options, pkg) - var reason = "" - if revDeps.len == 1: - reason = "$1 ($2) depends on it" % [revDeps[0].name, $revDeps[0].ver] + if options.uninstallRevDeps: + getAllRevDeps(options, pkg, pkgsToDelete) else: - for i in 0 ..< revDeps.len: - reason.add("$1 ($2)" % [revDeps[i].name, $revDeps[i].ver]) - if i != revDeps.len-1: - reason.add ", " - reason.add " depend on it" + 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 revDeps.len > 0: - errors.add("Cannot uninstall $1 ($2) because $3" % - [pkgTup.name, pkg.specialVersion, reason]) - else: - pkgsToDelete.add pkg + 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, "\n " & errors.join("\n ")) + if pkgsToDelete.len == 0: + raise newException(NimbleError, "Failed uninstall - no packages to delete") var pkgNames = "" - for i in 0 ..< pkgsToDelete.len: - if i != 0: pkgNames.add ", " - let pkg = pkgsToDelete[i] + 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. @@ -937,12 +949,16 @@ proc uninstall(options: Options) = proc listTasks(options: Options) = let nimbleFile = findNimbleFile(getCurrentDir(), true) - nimscriptsupport.listTasks(nimbleFile, options) + 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: @@ -982,8 +998,7 @@ proc developFromDir(dir: string, options: Options) = writeNimbleLink(nimbleLinkPath, nimbleLink) # Save a nimblemeta.json file. - saveNimbleMeta(pkgDestDir, "file://" & dir, vcsRevisionInDir(dir), - nimbleLinkPath) + saveNimbleMeta(pkgDestDir, dir, vcsRevisionInDir(dir), nimbleLinkPath) # Save the nimble data (which might now contain reverse deps added in # processDeps). @@ -992,6 +1007,10 @@ proc developFromDir(dir: string, options: 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) @@ -1011,29 +1030,74 @@ proc develop(options: Options) = let (meth, url, metadata) = getDownloadInfo(pv, options, true) let subdir = metadata.getOrDefault("subdir") - discard downloadPkg(url, pv.ver, meth, subdir, options, downloadDir) + + # 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. - var files = toSeq(walkDir(getCurrentDir() / "tests")) + ## 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: - if file.path.endsWith(".nim") and file.kind in {pcFile, pcLinkToFile}: + 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.typ = actionCompile + optsCopy.action = Action(typ: actionCompile) optsCopy.action.file = file.path - optsCopy.action.backend = "c" - optsCopy.action.compileOptions = @[] - optsCopy.action.compileOptions.add("-r") - optsCopy.action.compileOptions.add("--path:.") - execBackend(optsCopy) + optsCopy.action.backend = pkgInfo.backend + optsCopy.getCompilationFlags() = @[] + optsCopy.getCompilationFlags().add("-r") + optsCopy.getCompilationFlags().add("--path:.") + let + binFileName = file.path.changeFileExt(ExeExt) + existsBefore = existsFile(binFileName) - display("Success:", "All tests passed", Success, HighPriority) + if options.continueTestsOnFailure: + inc tests + try: + execBackend(pkgInfo, optsCopy) + except NimbleError: + inc failures + else: + execBackend(pkgInfo, 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 a in the current working directory. + ## Validates a package in the current working directory. let nimbleFile = findNimbleFile(getCurrentDir(), true) var error: ValidationError var pkgInfo: PackageInfo @@ -1051,7 +1115,29 @@ proc check(options: Options) = display("Failure:", "Validation failed", Error, HighPriority) quit(QuitFailure) -proc doAction(options: Options) = +proc run(options: Options) = + # Verify parameters. + var pkgInfo = getPkgInfo(getCurrentDir(), options) + + let binary = options.getCompilationBinary(pkgInfo).get("") + if binary.len == 0: + raiseNimbleError("Please specify a binary to run") + + if binary notin pkgInfo.bin: + raiseNimbleError( + "Binary '$#' is not defined in '$#' package." % [binary, pkgInfo.name] + ) + + # Build the binary. + build(options) + + let binaryPath = pkgInfo.getOutputDir(binary) + let cmd = quoteShellCommand(binaryPath & options.action.runFlags) + displayDebug("Executing", cmd) + cmd.execCmd.quit + + +proc doAction(options: var Options) = if options.showHelp: writeHelp() if options.showVersion: @@ -1062,10 +1148,10 @@ proc doAction(options: Options) = if not existsDir(options.getPkgsDir): createDir(options.getPkgsDir) - if not execHook(options, true): - display("Warning", "Pre-hook prevented further execution.", Warning, - HighPriority) - return + 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) @@ -1091,8 +1177,11 @@ proc doAction(options: Options) = listPaths(options) of actionBuild: build(options) + of actionRun: + run(options) of actionCompile, actionDoc: - execBackend(options) + var pkgInfo = getPkgInfo(getCurrentDir(), options) + execBackend(pkgInfo, options) of actionInit: init(options) of actionPublish: @@ -1109,36 +1198,46 @@ proc doAction(options: 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[void] + var execResult: ExecutionResult[bool] if execCustom(options, execResult, failFast=not isPreDefined): if execResult.hasTaskRequestedCommand(): - doAction(execResult.getOptionsForCommand(options)) + 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, false) - - if options.action.typ != actionCustom: - discard execHook(options, false) + discard execHook(options, actionCustom, false) when isMainModule: var error = "" var hint = "" + var opt: Options try: - parseCmdLine().doAction() + opt = parseCmdLine() + opt.doAction() except NimbleError: let currentExc = (ref NimbleError)(getCurrentException()) (error, hint) = getOutputInfo(currentExc) except NimbleQuit: discard finally: - removeDir(getNimbleTempDir()) + 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() diff --git a/src/nimble.nim.cfg b/src/nimble.nim.cfg index 223dfc3..e459055 100644 --- a/src/nimble.nim.cfg +++ b/src/nimble.nim.cfg @@ -3,3 +3,4 @@ --path:"$nim/" --path:"./vendor/nim" -d:ssl +-d:nimcore # Enable 'gorge' in Nim's VM. See https://github.com/nim-lang/Nim/issues/8096 diff --git a/src/nimblepkg/cli.nim b/src/nimblepkg/cli.nim index a232940..3afda35 100644 --- a/src/nimblepkg/cli.nim +++ b/src/nimblepkg/cli.nim @@ -12,7 +12,11 @@ # - Bright for HighPriority. # - Normal for MediumPriority. -import logging, terminal, sets, strutils +import terminal, sets, strutils +import version + +when not declared(initHashSet): + import common type CLI* = ref object @@ -41,10 +45,11 @@ const styles: array[DebugPriority .. HighPriority, set[Style]] = [{styleDim}, {styleDim}, {}, {styleBright}] + proc newCLI(): CLI = result = CLI( level: HighPriority, - warnings: initSet[(string, string)](), + warnings: initHashSet[(string, string)](), suppressionCount: 0, showColor: true, suppressMessages: false @@ -57,8 +62,18 @@ proc calculateCategoryOffset(category: string): int = assert category.len <= longestCategory return longestCategory - category.len +proc isSuppressed(displayType: DisplayType): bool = + # Don't print any Warning, Message or Success messages when suppression of + # warnings is enabled. That is, unless the user asked for --verbose output. + if globalCLI.suppressMessages and displayType >= Warning and + globalCLI.level == HighPriority: + return true + proc displayCategory(category: string, displayType: DisplayType, priority: Priority) = + if isSuppressed(displayType): + return + # Calculate how much the `category` must be offset to align along a center # line. let offset = calculateCategoryOffset(category) @@ -75,6 +90,9 @@ proc displayCategory(category: string, displayType: DisplayType, proc displayLine(category, line: string, displayType: DisplayType, priority: Priority) = + if isSuppressed(displayType): + return + displayCategory(category, displayType, priority) # Display the message. @@ -82,12 +100,6 @@ proc displayLine(category, line: string, displayType: DisplayType, proc display*(category, msg: string, displayType = Message, priority = MediumPriority) = - # Don't print any Warning, Message or Success messages when suppression of - # warnings is enabled. That is, unless the user asked for --verbose output. - if globalCLI.suppressMessages and displayType >= Warning and - globalCLI.level == HighPriority: - return - # Multiple warnings containing the same messages should not be shown. let warningPair = (category, msg) if displayType == Warning: @@ -135,7 +147,7 @@ proc prompt*(forcePrompts: ForcePrompt, question: string): bool = display("Prompt:", question & " -> [forced no]", Warning, HighPriority) return false of dontForcePrompt: - display("Prompt:", question & " [y/N]", Warning, HighPriority) + displayLine("Prompt:", question & " [y/N]", Warning, HighPriority) displayCategory("Answer:", Warning, HighPriority) let yn = stdin.readLine() case yn.normalize @@ -169,6 +181,76 @@ proc promptCustom*(forcePrompts: ForcePrompt, question, default: string): string proc promptCustom*(question, default: string): string = return promptCustom(dontForcePrompt, question, default) +proc promptListInteractive(question: string, args: openarray[string]): string = + display("Prompt:", question, Warning, HighPriority) + display("Select", "Cycle with 'Tab', 'Enter' when done", Message, + HighPriority) + displayCategory("Choices:", Warning, HighPriority) + var + current = 0 + selected = false + # Incase the cursor is at the bottom of the terminal + for arg in args: + stdout.write "\n" + # Reset the cursor to the start of the selection prompt + cursorUp(stdout, args.len) + cursorForward(stdout, longestCategory) + hideCursor(stdout) + + # The selection loop + while not selected: + setForegroundColor(fgDefault) + # Loop through the options + for i, arg in args: + # Check if the option is the current + if i == current: + writeStyled("> " & arg & " <", {styleBright}) + else: + writeStyled(" " & arg & " ", {styleDim}) + # Move the cursor back to the start + for s in 0..<(arg.len + 4): + cursorBackward(stdout) + # Move down for the next item + cursorDown(stdout) + # Move the cursor back up to the start of the selection prompt + for i in 0..<(args.len()): + cursorUp(stdout) + resetAttributes(stdout) + + # Begin key input + while true: + case getch(): + of '\t': + current = (current + 1) mod args.len + break + of '\r': + selected = true + break + of '\3': + showCursor(stdout) + raise newException(NimbleError, "Keyboard interrupt") + else: discard + + # Erase all lines of the selection + for i in 0.. [forced " & result & "]", Warning, HighPriority) else: - display("Prompt:", question & " [" & join(args, "/") & "]", Warning, HighPriority) - displayCategory("Answer:", Warning, HighPriority) - result = stdin.readLine() - for arg in args: - if arg.cmpIgnoreCase(result) == 0: - return arg - return promptList(forcePrompts, question, args) + if isatty(stdout): + return promptListInteractive(question, args) + else: + return promptListFallback(question, args) proc setVerbosity*(level: Priority) = globalCLI.level = level diff --git a/src/nimblepkg/common.nim b/src/nimblepkg/common.nim index 436775e..ab83175 100644 --- a/src/nimblepkg/common.nim +++ b/src/nimblepkg/common.nim @@ -8,7 +8,6 @@ when not defined(nimscript): import sets import version - export version.NimbleError # TODO: Surely there is a better way? type BuildFailed* = object of NimbleError @@ -23,7 +22,8 @@ when not defined(nimscript): preHooks*: HashSet[string] name*: string ## The version specified in the .nimble file.Assuming info is non-minimal, - ## it will always be a non-special version such as '0.1.4' + ## it will always be a non-special version such as '0.1.4'. + ## If in doubt, use `getConcreteVersion` instead. version*: string specialVersion*: string ## Either `myVersion` or a special version such as #head. author*: string @@ -63,4 +63,16 @@ when not defined(nimscript): return (error, hint) const - nimbleVersion* = "0.8.10" + nimbleVersion* = "0.11.0" + +when not declared(initHashSet): + import sets + + template initHashSet*[A](initialSize = 64): HashSet[A] = + initSet[A](initialSize) + +when not declared(toHashSet): + import sets + + template toHashSet*[A](keys: openArray[A]): HashSet[A] = + toSet(keys) diff --git a/src/nimblepkg/config.nim b/src/nimblepkg/config.nim index 78e3950..c4a48fc 100644 --- a/src/nimblepkg/config.nim +++ b/src/nimblepkg/config.nim @@ -1,8 +1,8 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import parsecfg, streams, strutils, os, tables, Uri +import parsecfg, streams, strutils, os, tables, uri -import tools, version, common, cli +import version, cli type Config* = object @@ -68,11 +68,12 @@ proc parseConfig*(): Config = var e = next(p) case e.kind of cfgEof: - if currentPackageList.urls.len == 0 and currentPackageList.path == "": - raise newException(NimbleError, "Package list '$1' requires either url or path" % currentPackageList.name) - if currentPackageList.urls.len > 0 and currentPackageList.path != "": - raise newException(NimbleError, "Attempted to specify `url` and `path` for the same package list '$1'" % currentPackageList.name) - addCurrentPkgList(result, currentPackageList) + if currentSection.len > 0: + if currentPackageList.urls.len == 0 and currentPackageList.path == "": + raise newException(NimbleError, "Package list '$1' requires either url or path" % currentPackageList.name) + if currentPackageList.urls.len > 0 and currentPackageList.path != "": + raise newException(NimbleError, "Attempted to specify `url` and `path` for the same package list '$1'" % currentPackageList.name) + addCurrentPkgList(result, currentPackageList) break of cfgSectionStart: addCurrentPkgList(result, currentPackageList) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index cad8d3d..3bbfa25 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -2,14 +2,15 @@ # BSD License. Look at license.txt for more info. import parseutils, os, osproc, strutils, tables, pegs, uri - import packageinfo, packageparser, version, tools, common, options, cli +from algorithm import SortOrder, sorted +from sequtils import toSeq, filterIt, map type DownloadMethod* {.pure.} = enum git = "git", hg = "hg" -proc getSpecificDir(meth: DownloadMethod): string = +proc getSpecificDir(meth: DownloadMethod): string {.used.} = case meth of DownloadMethod.git: ".git" @@ -24,11 +25,12 @@ proc doCheckout(meth: DownloadMethod, downloadDir, branch: string) = # clone has happened. Like in the case of git on Windows where it # messes up the damn line endings. doCmd("git checkout --force " & branch) + doCmd("git submodule update --recursive") of DownloadMethod.hg: cd downloadDir: doCmd("hg checkout " & branch) -proc doPull(meth: DownloadMethod, downloadDir: string) = +proc doPull(meth: DownloadMethod, downloadDir: string) {.used.} = case meth of DownloadMethod.git: doCheckout(meth, downloadDir, "") @@ -42,17 +44,17 @@ proc doPull(meth: DownloadMethod, downloadDir: string) = doCmd("hg pull") proc doClone(meth: DownloadMethod, url, downloadDir: string, branch = "", - tip = true) = + onlyTip = true) = case meth of DownloadMethod.git: let - depthArg = if tip: "--depth 1 " else: "" + depthArg = if onlyTip: "--depth 1 " else: "" branchArg = if branch == "": "" else: "-b " & branch & " " doCmd("git clone --recursive " & depthArg & branchArg & url & " " & downloadDir) of DownloadMethod.hg: let - tipArg = if tip: "-r tip " else: "" + tipArg = if onlyTip: "-r tip " else: "" branchArg = if branch == "": "" else: "-b " & branch & " " doCmd("hg clone " & tipArg & branchArg & url & " " & downloadDir) @@ -102,14 +104,21 @@ proc getTagsListRemote*(url: string, meth: DownloadMethod): seq[string] = # http://stackoverflow.com/questions/2039150/show-tags-for-remote-hg-repository raise newException(ValueError, "Hg doesn't support remote tag querying.") -proc getVersionList*(tags: seq[string]): Table[Version, string] = - # Returns: TTable of version -> git tag name - result = initTable[Version, string]() - for tag in tags: - if tag != "": - let i = skipUntil(tag, Digits) # skip any chars before the version - # TODO: Better checking, tags can have any names. Add warnings and such. - result[newVersion(tag[i .. tag.len-1])] = tag +proc getVersionList*(tags: seq[string]): OrderedTable[Version, string] = + ## Return an ordered table of Version -> git tag label. Ordering is + ## in descending order with the most recent version first. + let taggedVers: seq[tuple[ver: Version, tag: string]] = + tags + .filterIt(it != "") + .map(proc(s: string): tuple[ver: Version, tag: string] = + # skip any chars before the version + let i = skipUntil(s, Digits) + # TODO: Better checking, tags can have any + # names. Add warnings and such. + result = (newVersion(s[i .. s.len-1]), s)) + .sorted(proc(a, b: (Version, string)): int = cmp(a[0], b[0]), + SortOrder.Descending) + result = toOrderedTable[Version, string](taggedVers) proc getDownloadMethod*(meth: string): DownloadMethod = case meth @@ -166,18 +175,16 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, meth if $latest.ver != "": result = latest.ver - else: - # Result should already be set to #head here. - assert(not result.isNil) removeDir(downloadDir) if verRange.kind == verSpecial: # We want a specific commit/branch/tag here. if verRange.spe == getHeadName(downMethod): - doClone(downMethod, url, downloadDir) # Grab HEAD. + # Grab HEAD. + doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) else: # Grab the full repo. - doClone(downMethod, url, downloadDir, tip = false) + doClone(downMethod, url, downloadDir, onlyTip = false) # Then perform a checkout operation to get the specified branch/commit. # `spe` starts with '#', trim it. doAssert(($verRange.spe)[0] == '#') @@ -194,12 +201,13 @@ proc doDownload(url: string, downloadDir: string, verRange: VersionRange, getLatestByTag: display("Cloning", "latest tagged version: " & latest.tag, priority = MediumPriority) - doClone(downMethod, url, downloadDir, latest.tag) + doClone(downMethod, url, downloadDir, latest.tag, + onlyTip = not options.forceFullClone) else: # If no commits have been tagged on the repo we just clone HEAD. doClone(downMethod, url, downloadDir) # Grab HEAD. of DownloadMethod.hg: - doClone(downMethod, url, downloadDir) + doClone(downMethod, url, downloadDir, onlyTip = not options.forceFullClone) result = getHeadName(downMethod) let versions = getTagsList(downloadDir, downMethod).getVersionList() @@ -268,14 +276,8 @@ proc echoPackageVersions*(pkg: Package) = try: let versions = getTagsListRemote(pkg.url, downMethod).getVersionList() if versions.len > 0: - var vstr = "" - var i = 0 - for v in values(versions): - if i != 0: - vstr.add(", ") - vstr.add(v) - i.inc - echo(" versions: " & vstr) + let sortedVersions = toSeq(values(versions)) + echo(" versions: " & join(sortedVersions, ", ")) else: echo(" versions: (No versions tagged in the remote repository)") except OSError: @@ -283,3 +285,32 @@ proc echoPackageVersions*(pkg: Package) = of DownloadMethod.hg: echo(" versions: (Remote tag retrieval not supported by " & pkg.downloadMethod & ")") + +when isMainModule: + # Test version sorting + block: + let data = @["v9.0.0-taeyeon", "v9.0.1-jessica", "v9.2.0-sunny", + "v9.4.0-tiffany", "v9.4.2-hyoyeon"] + let expected = toOrderedTable[Version, string]({ + newVersion("9.4.2-hyoyeon"): "v9.4.2-hyoyeon", + newVersion("9.4.0-tiffany"): "v9.4.0-tiffany", + newVersion("9.2.0-sunny"): "v9.2.0-sunny", + newVersion("9.0.1-jessica"): "v9.0.1-jessica", + newVersion("9.0.0-taeyeon"): "v9.0.0-taeyeon" + }) + doAssert expected == getVersionList(data) + + + block: + let data2 = @["v0.1.0", "v0.1.1", "v0.2.0", + "0.4.0", "v0.4.2"] + let expected2 = toOrderedTable[Version, string]({ + newVersion("0.4.2"): "v0.4.2", + newVersion("0.4.0"): "0.4.0", + newVersion("0.2.0"): "v0.2.0", + newVersion("0.1.1"): "v0.1.1", + newVersion("0.1.0"): "v0.1.0", + }) + doAssert expected2 == getVersionList(data2) + + echo("Everything works!") diff --git a/src/nimblepkg/init.nim b/src/nimblepkg/init.nim new file mode 100644 index 0000000..8e7c33b --- /dev/null +++ b/src/nimblepkg/init.nim @@ -0,0 +1,184 @@ +import os, strutils + +import ./cli, ./tools + +type + PkgInitInfo* = tuple + pkgName: string + pkgVersion: string + pkgAuthor: string + pkgDesc: string + pkgLicense: string + pkgBackend: string + pkgSrcDir: string + pkgNimDep: string + pkgType: string + +proc writeExampleIfNonExistent(file: string, content: string) = + if not existsFile(file): + writeFile(file, content) + else: + display("Info:", "File " & file & " already exists, did not write " & + "example code", priority = HighPriority) + +proc createPkgStructure*(info: PkgInitInfo, pkgRoot: string) = + # Create source directory + createDirD(pkgRoot / info.pkgSrcDir) + + # Initialise the source code directories and create some example code. + var nimbleFileOptions = "" + case info.pkgType + of "binary": + let mainFile = pkgRoot / info.pkgSrcDir / info.pkgName.changeFileExt("nim") + writeExampleIfNonExistent(mainFile, +""" +# This is just an example to get you started. A typical binary package +# uses this file as the main entry point of the application. + +when isMainModule: + echo("Hello, World!") +""" + ) + nimbleFileOptions.add("bin = @[\"$1\"]\n" % info.pkgName) + of "library": + let mainFile = pkgRoot / info.pkgSrcDir / info.pkgName.changeFileExt("nim") + writeExampleIfNonExistent(mainFile, +""" +# This is just an example to get you started. A typical library package +# exports the main API in this file. Note that you cannot rename this file +# but you can remove it if you wish. + +proc add*(x, y: int): int = + ## Adds two files together. + return x + y +""" + ) + + createDirD(pkgRoot / info.pkgSrcDir / info.pkgName) + let submodule = pkgRoot / info.pkgSrcDir / info.pkgName / + "submodule".addFileExt("nim") + writeExampleIfNonExistent(submodule, +""" +# This is just an example to get you started. Users of your library will +# import this file by writing ``import $1/submodule``. Feel free to rename or +# remove this file altogether. You may create additional modules alongside +# this file as required. + +type + Submodule* = object + name*: string + +proc initSubmodule*(): Submodule = + ## Initialises a new ``Submodule`` object. + Submodule(name: "Anonymous") +""" % info.pkgName + ) + of "hybrid": + let mainFile = pkgRoot / info.pkgSrcDir / info.pkgName.changeFileExt("nim") + writeExampleIfNonExistent(mainFile, +""" +# This is just an example to get you started. A typical hybrid package +# uses this file as the main entry point of the application. + +import $1pkg/submodule + +when isMainModule: + echo(getWelcomeMessage()) +""" % info.pkgName + ) + + let pkgSubDir = pkgRoot / info.pkgSrcDir / info.pkgName & "pkg" + createDirD(pkgSubDir) + let submodule = pkgSubDir / "submodule".addFileExt("nim") + writeExampleIfNonExistent(submodule, +""" +# This is just an example to get you started. Users of your hybrid library will +# import this file by writing ``import $1pkg/submodule``. Feel free to rename or +# remove this file altogether. You may create additional modules alongside +# this file as required. + +proc getWelcomeMessage*(): string = "Hello, World!" +""" % info.pkgName + ) + nimbleFileOptions.add("installExt = @[\"nim\"]\n") + nimbleFileOptions.add("bin = @[\"$1\"]\n" % info.pkgName) + else: + assert false, "Invalid package type specified." + + let pkgTestDir = "tests" + # Create test directory + case info.pkgType + of "binary": + discard + of "hybrid", "library": + let pkgTestPath = pkgRoot / pkgTestDir + createDirD(pkgTestPath) + + writeFile(pkgTestPath / "config".addFileExt("nims"), + "switch(\"path\", \"$$projectDir/../$#\")" % info.pkgSrcDir + ) + + if info.pkgType == "library": + writeExampleIfNonExistent(pkgTestPath / "test1".addFileExt("nim"), +""" +# This is just an example to get you started. You may wish to put all of your +# tests into a single file, or separate them into multiple `test1`, `test2` +# etc. files (better names are recommended, just make sure the name starts with +# the letter 't'). +# +# To run these tests, simply execute `nimble test`. + +import unittest + +import $1 +test "can add": + check add(5, 5) == 10 +""" % info.pkgName + ) + else: + writeExampleIfNonExistent(pkgTestPath / "test1".addFileExt("nim"), +""" +# This is just an example to get you started. You may wish to put all of your +# tests into a single file, or separate them into multiple `test1`, `test2` +# etc. files (better names are recommended, just make sure the name starts with +# the letter 't'). +# +# To run these tests, simply execute `nimble test`. + +import unittest + +import $1pkg/submodule +test "correct welcome": + check getWelcomeMessage() == "Hello, World!" +""" % info.pkgName + ) + else: + assert false, "Invalid package type specified." + + # Write the nimble file + let nimbleFile = pkgRoot / info.pkgName.changeFileExt("nimble") + # Only write backend if it isn't "c" + var pkgBackend = "" + if (info.pkgBackend != "c"): + pkgBackend = "backend = " & info.pkgbackend.escape() + writeFile(nimbleFile, """# Package + +version = $# +author = "$#" +description = "$#" +license = $# +srcDir = $# +$# +$# + +# Dependencies + +requires "nim >= $#" +""" % [ + info.pkgVersion.escape(), info.pkgAuthor.replace("\"", "\\\""), info.pkgDesc.replace("\"", "\\\""), + info.pkgLicense.escape(), info.pkgSrcDir.escape(), nimbleFileOptions, + pkgBackend, info.pkgNimDep + ] + ) + + display("Info:", "Nimble file created successfully", priority=MediumPriority) diff --git a/src/nimblepkg/nimscriptapi.nim b/src/nimblepkg/nimscriptapi.nim index b711851..ec2f46c 100644 --- a/src/nimblepkg/nimscriptapi.nim +++ b/src/nimblepkg/nimscriptapi.nim @@ -3,6 +3,12 @@ ## This module is implicitly imported in NimScript .nimble files. +import system except getCommand, setCommand, switch, `--` +import strformat, strutils, tables + +when not defined(nimscript): + import os + var packageName* = "" ## Set this to the package name. It ## is usually not required to do that, nims' filename is @@ -11,7 +17,7 @@ var author*: string ## The package's author. description*: string ## The package's description. license*: string ## The package's license. - srcdir*: string ## The package's source directory. + srcDir*: string ## The package's source directory. binDir*: string ## The package's binary directory. backend*: string ## The package's backend. @@ -22,26 +28,169 @@ var foreignDeps*: seq[string] = @[] ## The foreign dependencies. Only ## exported for 'distros.nim'. + beforeHooks: seq[string] = @[] + afterHooks: seq[string] = @[] + commandLineParams: seq[string] = @[] + flags: TableRef[string, seq[string]] + + command = "e" + project = "" + success = false + retVal = true + projectFile = "" + outFile = "" + proc requires*(deps: varargs[string]) = ## Call this to set the list of requirements of your Nimble ## package. for d in deps: requiresData.add(d) +proc getParams() = + # Called by nimscriptwrapper.nim:execNimscript() + # nim e --flags /full/path/to/file.nims /full/path/to/file.out action + for i in 2 .. paramCount(): + let + param = paramStr(i) + if param[0] != '-': + if projectFile.len == 0: + projectFile = param + elif outFile.len == 0: + outFile = param + else: + commandLineParams.add param.normalize + +proc getCommand*(): string = + return command + +proc setCommand*(cmd: string, prj = "") = + command = cmd + if prj.len != 0: + project = prj + +proc switch*(key: string, value="") = + if flags.isNil: + flags = newTable[string, seq[string]]() + + if flags.hasKey(key): + flags[key].add(value) + else: + flags[key] = @[value] + +template `--`*(key, val: untyped) = + switch(astToStr(key), strip astToStr(val)) + +template `--`*(key: untyped) = + switch(astToStr(key), "") + +template printIfLen(varName) = + if varName.len != 0: + result &= astToStr(varName) & ": \"\"\"" & varName & "\"\"\"\n" + +template printSeqIfLen(varName) = + if varName.len != 0: + result &= astToStr(varName) & ": \"" & varName.join(", ") & "\"\n" + +proc printPkgInfo(): string = + if backend.len == 0: + backend = "c" + + result = "[Package]\n" + if packageName.len != 0: + result &= "name: \"" & packageName & "\"\n" + printIfLen version + printIfLen author + printIfLen description + printIfLen license + printIfLen srcDir + printIfLen binDir + printIfLen backend + + printSeqIfLen skipDirs + printSeqIfLen skipFiles + printSeqIfLen skipExt + printSeqIfLen installDirs + printSeqIfLen installFiles + printSeqIfLen installExt + printSeqIfLen bin + printSeqIfLen beforeHooks + printSeqIfLen afterHooks + + if requiresData.len != 0: + result &= "\n[Deps]\n" + result &= &"requires: \"{requiresData.join(\", \")}\"\n" + +proc onExit*() = + if "printPkgInfo".normalize in commandLineParams: + if outFile.len != 0: + writeFile(outFile, printPkgInfo()) + else: + var + output = "" + output &= "\"success\": " & $success & ", " + output &= "\"command\": \"" & command & "\", " + if project.len != 0: + output &= "\"project\": \"" & project & "\", " + if not flags.isNil and flags.len != 0: + output &= "\"flags\": {" + for key, val in flags.pairs: + output &= "\"" & key & "\": [" + for v in val: + let v = if v.len > 0 and v[0] == '"': strutils.unescape(v) + else: v + output &= v.escape & ", " + output = output[0 .. ^3] & "], " + output = output[0 .. ^3] & "}, " + + output &= "\"retVal\": " & $retVal + + if outFile.len != 0: + writeFile(outFile, "{" & output & "}") + +# TODO: New release of Nim will move this `task` template under a +# `when not defined(nimble)`. This will allow us to override it in the future. +template task*(name: untyped; description: string; body: untyped): untyped = + ## Defines a task. Hidden tasks are supported via an empty description. + ## Example: + ## + ## .. code-block:: nim + ## task build, "default build is via the C backend": + ## setCommand "c" + proc `name Task`*() = body + + if commandLineParams.len == 0 or "help" in commandLineParams: + success = true + echo(astToStr(name), " ", description) + elif astToStr(name).normalize in commandLineParams: + success = true + `name Task`() + template before*(action: untyped, body: untyped): untyped = ## Defines a block of code which is evaluated before ``action`` is executed. proc `action Before`*(): bool = result = true body + beforeHooks.add astToStr(action) + + if (astToStr(action) & "Before").normalize in commandLineParams: + success = true + retVal = `action Before`() + template after*(action: untyped, body: untyped): untyped = ## Defines a block of code which is evaluated after ``action`` is executed. proc `action After`*(): bool = result = true body -template builtin = discard + afterHooks.add astToStr(action) + + if (astToStr(action) & "After").normalize in commandLineParams: + success = true + retVal = `action After`() proc getPkgDir*(): string = ## Returns the package directory containing the .nimble file currently ## being evaluated. - builtin + result = projectFile.rsplit(seps={'/', '\\', ':'}, maxsplit=1)[0] + +getParams() diff --git a/src/nimblepkg/nimscriptexecutor.nim b/src/nimblepkg/nimscriptexecutor.nim index 70fc165..122c41c 100644 --- a/src/nimblepkg/nimscriptexecutor.nim +++ b/src/nimblepkg/nimscriptexecutor.nim @@ -1,16 +1,17 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, tables, strutils, sets +import os, strutils, sets -import packageparser, common, packageinfo, options, nimscriptsupport, cli +import packageparser, common, packageinfo, options, nimscriptwrapper, cli, + version -proc execHook*(options: Options, before: bool): bool = +proc execHook*(options: Options, hookAction: ActionType, before: bool): bool = ## Returns whether to continue. result = true # For certain commands hooks should not be evaluated. - if options.action.typ in noHookActions: + if hookAction in noHookActions: return var nimbleFile = "" @@ -20,8 +21,8 @@ proc execHook*(options: Options, before: bool): bool = # PackageInfos are cached so we can read them as many times as we want. let pkgInfo = getPkgInfoFromFile(nimbleFile, options) let actionName = - if options.action.typ == actionCustom: options.action.command - else: ($options.action.typ)[6 .. ^1] + if hookAction == actionCustom: options.action.command + else: ($hookAction)[6 .. ^1] let hookExists = if before: actionName.normalize in pkgInfo.preHooks else: actionName.normalize in pkgInfo.postHooks @@ -31,7 +32,7 @@ proc execHook*(options: Options, before: bool): bool = result = res.retVal proc execCustom*(options: Options, - execResult: var ExecutionResult[void], + execResult: var ExecutionResult[bool], failFast = true): bool = ## Executes the custom command using the nimscript backend. ## @@ -57,7 +58,7 @@ proc execCustom*(options: Options, HighPriority) return - if not execHook(options, false): + if not execHook(options, actionCustom, false): return return true diff --git a/src/nimblepkg/nimscriptsupport.nim b/src/nimblepkg/nimscriptsupport.nim deleted file mode 100644 index 0222980..0000000 --- a/src/nimblepkg/nimscriptsupport.nim +++ /dev/null @@ -1,531 +0,0 @@ -# Copyright (C) Andreas Rumpf. All rights reserved. -# BSD License. Look at license.txt for more info. - -## Implements the new configuration system for Nimble. Uses Nim as a -## scripting language. - -import - compiler/ast, compiler/modules, compiler/passes, compiler/passaux, - compiler/condsyms, compiler/sem, compiler/semdata, - compiler/llstream, compiler/vm, compiler/vmdef, compiler/commands, - compiler/msgs, compiler/magicsys, compiler/idents, - compiler/nimconf - -from compiler/scriptconfig import setupVM -from compiler/astalgo import strTableGet -import compiler/options as compiler_options - -import common, version, options, packageinfo, cli -import os, strutils, strtabs, tables, times, osproc, sets, pegs - -when not declared(resetAllModulesHard): - import compiler/modulegraphs - -type - Flags = TableRef[string, seq[string]] - ExecutionResult*[T] = object - success*: bool - command*: string - arguments*: seq[string] - flags*: Flags - retVal*: T - -const - internalCmd = "NimbleInternal" - nimscriptApi = staticRead("nimscriptapi.nim") - -proc raiseVariableError(ident, typ: string) {.noinline.} = - raise newException(NimbleError, - "NimScript's variable '" & ident & "' needs a value of type '" & typ & "'.") - -proc isStrLit(n: PNode): bool = n.kind in {nkStrLit..nkTripleStrLit} - -proc getGlobal(ident: PSym): string = - let n = vm.globalCtx.getGlobalValue(ident) - if n.isStrLit: - result = if n.strVal.isNil: "" else: n.strVal - else: - raiseVariableError(ident.name.s, "string") - -proc getGlobalAsSeq(ident: PSym): seq[string] = - let n = vm.globalCtx.getGlobalValue(ident) - result = @[] - if n.kind == nkBracket: - for x in n: - if x.isStrLit: - result.add x.strVal - else: - raiseVariableError(ident.name.s, "seq[string]") - else: - raiseVariableError(ident.name.s, "seq[string]") - -proc extractRequires(ident: PSym, result: var seq[PkgTuple]) = - let n = vm.globalCtx.getGlobalValue(ident) - if n.kind == nkBracket: - for x in n: - if x.kind == nkPar and x.len == 2 and x[0].isStrLit and x[1].isStrLit: - result.add(parseRequires(x[0].strVal & x[1].strVal)) - elif x.isStrLit: - result.add(parseRequires(x.strVal)) - else: - raiseVariableError("requiresData", "seq[(string, VersionReq)]") - else: - raiseVariableError("requiresData", "seq[(string, VersionReq)]") - -when declared(newIdentCache): - var identCache = newIdentCache() - -proc setupVM(module: PSym; scriptName: string, flags: Flags): PEvalContext = - ## This procedure is exported in the compiler sources, but its implementation - ## is too Nim-specific to be used by Nimble. - ## Specifically, the implementation of ``switch`` is problematic. Sooo - ## I simply copied it here and edited it :) - - when declared(newIdentCache): - result = newCtx(module, identCache) - else: - result = newCtx(module) - result.mode = emRepl - registerAdditionalOps(result) - - # captured vars: - var errorMsg: string - var vthisDir = scriptName.splitFile.dir - - proc listDirs(a: VmArgs, filter: set[PathComponent]) = - let dir = getString(a, 0) - var res: seq[string] = @[] - for kind, path in walkDir(dir): - if kind in filter: res.add path - setResult(a, res) - - template cbconf(name, body) {.dirty.} = - result.registerCallback "stdlib.system." & astToStr(name), - proc (a: VmArgs) = - body - - template cbos(name, body) {.dirty.} = - result.registerCallback "stdlib.system." & astToStr(name), - proc (a: VmArgs) = - try: - body - except OSError: - errorMsg = getCurrentExceptionMsg() - - # Idea: Treat link to file as a file, but ignore link to directory to prevent - # endless recursions out of the box. - cbos listFiles: - listDirs(a, {pcFile, pcLinkToFile}) - cbos listDirs: - listDirs(a, {pcDir}) - cbos removeDir: - os.removeDir getString(a, 0) - cbos removeFile: - os.removeFile getString(a, 0) - cbos createDir: - os.createDir getString(a, 0) - cbos getOsError: - setResult(a, errorMsg) - cbos setCurrentDir: - os.setCurrentDir getString(a, 0) - cbos getCurrentDir: - setResult(a, os.getCurrentDir()) - cbos moveFile: - os.moveFile(getString(a, 0), getString(a, 1)) - cbos copyFile: - os.copyFile(getString(a, 0), getString(a, 1)) - cbos getLastModificationTime: - setResult(a, toSeconds(getLastModificationTime(getString(a, 0)))) - - cbos rawExec: - setResult(a, osproc.execCmd getString(a, 0)) - - cbconf getEnv: - setResult(a, os.getEnv(a.getString 0)) - cbconf existsEnv: - setResult(a, os.existsEnv(a.getString 0)) - cbconf dirExists: - setResult(a, os.dirExists(a.getString 0)) - cbconf fileExists: - setResult(a, os.fileExists(a.getString 0)) - - cbconf thisDir: - setResult(a, vthisDir) - cbconf put: - compiler_options.setConfigVar(getString(a, 0), getString(a, 1)) - cbconf get: - setResult(a, compiler_options.getConfigVar(a.getString 0)) - cbconf exists: - setResult(a, compiler_options.existsConfigVar(a.getString 0)) - cbconf nimcacheDir: - setResult(a, compiler_options.getNimcacheDir()) - cbconf paramStr: - setResult(a, os.paramStr(int a.getInt 0)) - cbconf paramCount: - setResult(a, os.paramCount()) - cbconf cmpIgnoreStyle: - setResult(a, strutils.cmpIgnoreStyle(a.getString 0, a.getString 1)) - cbconf cmpIgnoreCase: - setResult(a, strutils.cmpIgnoreCase(a.getString 0, a.getString 1)) - cbconf setCommand: - compiler_options.command = a.getString 0 - let arg = a.getString 1 - if arg.len > 0: - gProjectName = arg - try: - gProjectFull = canonicalizePath(gProjectPath / gProjectName) - except OSError: - gProjectFull = gProjectName - cbconf getCommand: - setResult(a, compiler_options.command) - cbconf switch: - if not flags.isNil: - let - key = a.getString 0 - value = a.getString 1 - if flags.hasKey(key): - flags[key].add(value) - else: - flags[key] = @[value] - -proc isValidLibPath(lib: string): bool = - return fileExists(lib / "system.nim") - -proc getNimPrefixDir(options: Options): string = - let env = getEnv("NIM_LIB_PREFIX") - if env != "": - let msg = "Using env var NIM_LIB_PREFIX: " & env - display("Warning:", msg, Warning, HighPriority) - return env - - if options.config.nimLibPrefix != "": - result = options.config.nimLibPrefix - let msg = "Using Nim stdlib prefix from Nimble config file: " & result - display("Warning:", msg, Warning, HighPriority) - return - - result = splitPath(findExe("nim")).head.parentDir - # The above heuristic doesn't work for 'choosenim' proxies. Thankfully in - # that case the `nimble` binary is beside the `nim` binary so things should - # just work. - if not dirExists(result / "lib"): - # By specifying an empty string we instruct the Nim compiler to use - # getAppDir().head as the prefix dir. See compiler/options module for - # the code responsible for this. - result = "" - -proc getLibVersion(lib: string): Version = - ## This is quite a hacky procedure, but there is no other way to extract - ## this out of the ``system`` module. We could evaluate it, but that would - ## cause an error if the stdlib is out of date. The purpose of this - ## proc is to give a nice error message to the user instead of a confusing - ## Nim compile error. - let systemPath = lib / "system.nim" - if not fileExists(systemPath): - raiseNimbleError("system module not found in stdlib path: " & lib) - - let systemFile = readFile(systemPath) - let majorPeg = peg"'NimMajor' @ '=' \s* {\d*}" - let minorPeg = peg"'NimMinor' @ '=' \s* {\d*}" - let patchPeg = peg"'NimPatch' @ '=' \s* {\d*}" - - var majorMatches: array[1, string] - let major = find(systemFile, majorPeg, majorMatches) - var minorMatches: array[1, string] - let minor = find(systemFile, minorPeg, minorMatches) - var patchMatches: array[1, string] - let patch = find(systemFile, patchPeg, patchMatches) - - if major != -1 and minor != -1 and patch != -1: - return newVersion(majorMatches[0] & "." & minorMatches[0] & "." & patchMatches[0]) - else: - return system.NimVersion.newVersion() - -when declared(ModuleGraph): - var graph: ModuleGraph - -proc execScript(scriptName: string, flags: Flags, options: Options): PSym = - ## Executes the specified script. Returns the script's module symbol. - ## - ## No clean up is performed and must be done manually! - when declared(resetAllModulesHard): - # for compatibility with older Nim versions: - if "nimblepkg/nimscriptapi" notin compiler_options.implicitIncludes: - compiler_options.implicitIncludes.add("nimblepkg/nimscriptapi") - else: - if "nimblepkg/nimscriptapi" notin compiler_options.implicitImports: - compiler_options.implicitImports.add("nimblepkg/nimscriptapi") - - # Ensure the compiler can find its standard library #220. - compiler_options.gPrefixDir = getNimPrefixDir(options) - display("Setting", "Nim stdlib prefix to " & compiler_options.gPrefixDir, - priority=LowPriority) - - # Verify that lib path points to existing stdlib. - compiler_options.setDefaultLibpath() - display("Setting", "Nim stdlib path to " & compiler_options.libpath, - priority=LowPriority) - if not isValidLibPath(compiler_options.libpath): - let msg = "Nimble cannot find Nim's standard library.\nLast try in:\n - $1" % - compiler_options.libpath - let hint = "Nimble does its best to find Nim's standard library, " & - "sometimes this fails. You can set the environment variable " & - "NIM_LIB_PREFIX to where Nim's `lib` directory is located as " & - "a workaround. " & - "See https://github.com/nim-lang/nimble#troubleshooting for " & - "more info." - raiseNimbleError(msg, hint) - - # Verify that the stdlib that was found isn't older than the stdlib that Nimble - # was compiled with. - let libVersion = getLibVersion(compiler_options.libpath) - if NimVersion.newVersion() > libVersion: - let msg = ("Nimble cannot use an older stdlib than the one it was compiled " & - "with.\n Stdlib in '$#' has version: $#.\n Nimble needs at least: $#.") % - [compiler_options.libpath, $libVersion, NimVersion] - let hint = "You may be running a newer version of Nimble than you intended " & - "to. Run an older version of Nimble that is compatible with " & - "the stdlib that Nimble is attempting to use or set the environment variable " & - "NIM_LIB_PREFIX to where a different stdlib's `lib` directory is located as " & - "a workaround." & - "See https://github.com/nim-lang/nimble#troubleshooting for " & - "more info." - raiseNimbleError(msg, hint) - - let pkgName = scriptName.splitFile.name - - # Ensure that "nimblepkg/nimscriptapi" is in the PATH. - block: - let t = options.getNimbleDir / "nimblecache" - let tmpNimscriptApiPath = t / "nimblepkg" / "nimscriptapi.nim" - createDir(tmpNimscriptApiPath.splitFile.dir) - writeFile(tmpNimscriptApiPath, nimscriptApi) - searchPaths.add(t) - - initDefines() - loadConfigs(DefaultConfig) - passes.gIncludeFile = includeModule - passes.gImportModule = importModule - - defineSymbol("nimscript") - defineSymbol("nimconfig") - defineSymbol("nimble") - registerPass(semPass) - registerPass(evalPass) - - searchPaths.add(compiler_options.libpath) - - when declared(resetAllModulesHard): - result = makeModule(scriptName) - else: - graph = newModuleGraph() - result = graph.makeModule(scriptName) - - incl(result.flags, sfMainModule) - vm.globalCtx = setupVM(result, scriptName, flags) - - # Setup builtins defined in nimscriptapi.nim - template cbApi(name, body) {.dirty.} = - vm.globalCtx.registerCallback pkgName & "." & astToStr(name), - proc (a: VmArgs) = - body - - cbApi getPkgDir: - setResult(a, scriptName.splitFile.dir) - - when declared(newIdentCache): - graph.compileSystemModule(identCache) - graph.processModule(result, llStreamOpen(scriptName, fmRead), nil, identCache) - else: - compileSystemModule() - processModule(result, llStreamOpen(scriptName, fmRead), nil) - -proc cleanup() = - # ensure everything can be called again: - compiler_options.gProjectName = "" - compiler_options.command = "" - when declared(resetAllModulesHard): - resetAllModulesHard() - else: - resetSystemArtifacts() - clearPasses() - msgs.gErrorMax = 1 - msgs.writeLnHook = nil - vm.globalCtx = nil - initDefines() - -proc readPackageInfoFromNims*(scriptName: string, options: Options, - result: var PackageInfo) = - ## Executes the `scriptName` nimscript file. Reads the package information - ## that it populates. - - # Setup custom error handling. - msgs.gErrorMax = high(int) - var previousMsg = "" - msgs.writeLnHook = - proc (output: string) = - # The error counter is incremented after the writeLnHook is invoked. - if msgs.gErrorCounter > 0: - raise newException(NimbleError, previousMsg) - elif previousMsg.len > 0: - display("Info", previousMsg, priority = MediumPriority) - if output.normalize.startsWith("error"): - raise newException(NimbleError, output) - previousMsg = output - - compiler_options.command = internalCmd - - # Execute the nimscript file. - let thisModule = execScript(scriptName, nil, options) - - when declared(resetAllModulesHard): - let apiModule = thisModule - else: - var apiModule: PSym - for i in 0.. 0: - raise newException(NimbleError, previousMsg) - - # Extract all the necessary fields populated by the nimscript file. - proc getSym(apiModule: PSym, ident: string): PSym = - result = apiModule.tab.strTableGet(getIdent(ident)) - if result.isNil: - raise newException(NimbleError, "Ident not found: " & ident) - - template trivialField(field) = - result.field = getGlobal(getSym(apiModule, astToStr field)) - - template trivialFieldSeq(field) = - result.field.add getGlobalAsSeq(getSym(apiModule, astToStr field)) - - # keep reasonable default: - let name = getGlobal(apiModule.tab.strTableGet(getIdent"packageName")) - if name.len > 0: result.name = name - - trivialField version - trivialField author - trivialField description - trivialField license - trivialField srcdir - trivialField bindir - trivialFieldSeq skipDirs - trivialFieldSeq skipFiles - trivialFieldSeq skipExt - trivialFieldSeq installDirs - trivialFieldSeq installFiles - trivialFieldSeq installExt - trivialFieldSeq foreignDeps - - extractRequires(getSym(apiModule, "requiresData"), result.requires) - - let binSeq = getGlobalAsSeq(getSym(apiModule, "bin")) - for i in binSeq: - result.bin.add(i.addFileExt(ExeExt)) - - let backend = getGlobal(getSym(apiModule, "backend")) - if backend.len == 0: - result.backend = "c" - elif cmpIgnoreStyle(backend, "javascript") == 0: - result.backend = "js" - else: - result.backend = backend.toLowerAscii() - - # Grab all the global procs - for i in thisModule.tab.data: - if not i.isNil(): - let name = i.name.s.normalize() - if name.endsWith("before"): - result.preHooks.incl(name[0 .. ^7]) - if name.endsWith("after"): - result.postHooks.incl(name[0 .. ^6]) - - cleanup() - -proc execTask*(scriptName, taskName: string, - options: Options): ExecutionResult[void] = - ## Executes the specified task in the specified script. - ## - ## `scriptName` should be a filename pointing to the nimscript file. - result.success = true - result.flags = newTable[string, seq[string]]() - compiler_options.command = internalCmd - display("Executing", "task $# in $#" % [taskName, scriptName], - priority = HighPriority) - - let thisModule = execScript(scriptName, result.flags, options) - let prc = thisModule.tab.strTableGet(getIdent(taskName & "Task")) - if prc.isNil: - # Procedure not defined in the NimScript module. - result.success = false - return - discard vm.globalCtx.execProc(prc, []) - - # Read the command, arguments and flags set by the executed task. - result.command = compiler_options.command - result.arguments = @[] - for arg in compiler_options.gProjectName.split(): - result.arguments.add(arg) - - cleanup() - -proc execHook*(scriptName, actionName: string, before: bool, - options: Options): ExecutionResult[bool] = - ## Executes the specified action's hook. Depending on ``before``, either - ## the "before" or the "after" hook. - ## - ## `scriptName` should be a filename pointing to the nimscript file. - result.success = true - result.flags = newTable[string, seq[string]]() - compiler_options.command = internalCmd - let hookName = - if before: actionName.toLowerAscii & "Before" - else: actionName.toLowerAscii & "After" - 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. - let prc = thisModule.tab.strTableGet(getIdent(hookName)) - if prc.isNil: - # Procedure not defined in the NimScript module. - result.success = false - cleanup() - return - let returnVal = vm.globalCtx.execProc(prc, []) - case returnVal.kind - of nkCharLit..nkUInt64Lit: - result.retVal = returnVal.intVal == 1 - else: assert false - - # Read the command, arguments and flags set by the executed task. - result.command = compiler_options.command - result.arguments = @[] - for arg in compiler_options.gProjectName.split(): - result.arguments.add(arg) - - cleanup() - -proc getNimScriptCommand(): string = - compiler_options.command - -proc setNimScriptCommand(command: string) = - compiler_options.command = command - -proc hasTaskRequestedCommand*(execResult: ExecutionResult): bool = - ## Determines whether the last executed task used ``setCommand`` - return execResult.command != internalCmd - -proc listTasks*(scriptName: string, options: Options) = - setNimScriptCommand("help") - - discard execScript(scriptName, nil, options) - # TODO (#402): Make the 'task' template generate explicit data structure - # containing all the task names + descriptions. - cleanup() \ No newline at end of file diff --git a/src/nimblepkg/nimscriptwrapper.nim b/src/nimblepkg/nimscriptwrapper.nim new file mode 100644 index 0000000..4cfe6ec --- /dev/null +++ b/src/nimblepkg/nimscriptwrapper.nim @@ -0,0 +1,216 @@ +# Copyright (C) Andreas Rumpf. All rights reserved. +# BSD License. Look at license.txt for more info. + +## Implements the new configuration system for Nimble. Uses Nim as a +## scripting language. + +import hashes, json, os, strutils, tables, times, osproc, strtabs + +import version, options, cli, tools + +type + Flags = TableRef[string, seq[string]] + ExecutionResult*[T] = object + success*: bool + command*: string + arguments*: seq[string] + flags*: Flags + retVal*: T + stdout*: string + +const + internalCmd = "e" + nimscriptApi = staticRead("nimscriptapi.nim") + printPkgInfo = "printPkgInfo" + +proc isCustomTask(actionName: string, options: Options): bool = + options.action.typ == actionCustom and actionName != printPkgInfo + +proc needsLiveOutput(actionName: string, options: Options, isHook: bool): bool = + let isCustomTask = isCustomTask(actionName, options) + return isCustomTask or isHook or actionName == "" + +proc writeExecutionOutput(data: string) = + # TODO: in the future we will likely want this to be live, users will + # undoubtedly be doing loops and other crazy things in their top-level + # Nimble files. + display("Info", data) + +proc execNimscript( + nimsFile, projectDir, actionName: string, options: Options, isHook: bool +): tuple[output: string, exitCode: int, stdout: string] = + let + nimsFileCopied = projectDir / nimsFile.splitFile().name & "_" & getProcessId() & ".nims" + outFile = getNimbleTempDir() & ".out" + + let + isScriptResultCopied = + nimsFileCopied.fileExists() and + nimsFileCopied.getLastModificationTime() >= nimsFile.getLastModificationTime() + + if not isScriptResultCopied: + nimsFile.copyFile(nimsFileCopied) + + defer: + # Only if copied in this invocation, allows recursive calls of nimble + if not isScriptResultCopied and options.shouldRemoveTmp(nimsFileCopied): + nimsFileCopied.removeFile() + + var cmd = ( + "nim e $# -p:$# $# $# $#" % [ + "--hints:off --verbosity:0", + (getTempDir() / "nimblecache").quoteShell, + nimsFileCopied.quoteShell, + outFile.quoteShell, + actionName + ] + ).strip() + + let isCustomTask = isCustomTask(actionName, options) + if isCustomTask: + for i in options.action.arguments: + cmd &= " " & i.quoteShell() + for key, val in options.action.flags.pairs(): + cmd &= " $#$#" % [if key.len == 1: "-" else: "--", key] + if val.len != 0: + cmd &= ":" & val.quoteShell() + + displayDebug("Executing " & cmd) + + if needsLiveOutput(actionName, options, isHook): + result.exitCode = execCmd(cmd) + else: + # We want to capture any possible errors when parsing a .nimble + # file's metadata. See #710. + (result.stdout, result.exitCode) = execCmdEx(cmd) + if outFile.fileExists(): + result.output = outFile.readFile() + if options.shouldRemoveTmp(outFile): + discard outFile.tryRemoveFile() + +proc getNimsFile(scriptName: string, options: Options): string = + let + cacheDir = getTempDir() / "nimblecache" + shash = $scriptName.parentDir().hash().abs() + prjCacheDir = cacheDir / scriptName.splitFile().name & "_" & shash + nimscriptApiFile = cacheDir / "nimscriptapi.nim" + + result = prjCacheDir / scriptName.extractFilename().changeFileExt ".nims" + + let + iniFile = result.changeFileExt(".ini") + + isNimscriptApiCached = + nimscriptApiFile.fileExists() and nimscriptApiFile.getLastModificationTime() > + getAppFilename().getLastModificationTime() + + isScriptResultCached = + isNimscriptApiCached and result.fileExists() and result.getLastModificationTime() > + scriptName.getLastModificationTime() + + if not isNimscriptApiCached: + createDir(cacheDir) + writeFile(nimscriptApiFile, nimscriptApi) + + if not isScriptResultCached: + createDir(result.parentDir()) + writeFile(result, """ +import system except getCommand, setCommand, switch, `--`, + packageName, version, author, description, license, srcDir, binDir, backend, + skipDirs, skipFiles, skipExt, installDirs, installFiles, installExt, bin, foreignDeps, + requires, task, packageName +""" & + "import nimscriptapi, strutils\n" & scriptName.readFile() & "\nonExit()\n") + discard tryRemoveFile(iniFile) + +proc getIniFile*(scriptName: string, options: Options): string = + let + nimsFile = getNimsFile(scriptName, options) + + result = nimsFile.changeFileExt(".ini") + + let + isIniResultCached = + result.fileExists() and result.getLastModificationTime() > + scriptName.getLastModificationTime() + + if not isIniResultCached: + let (output, exitCode, stdout) = execNimscript( + nimsFile, scriptName.parentDir(), printPkgInfo, options, isHook=false + ) + + if exitCode == 0 and output.len != 0: + result.writeFile(output) + stdout.writeExecutionOutput() + else: + raise newException(NimbleError, stdout & "\nprintPkgInfo() failed") + +proc execScript( + scriptName, actionName: string, options: Options, isHook: bool +): ExecutionResult[bool] = + let nimsFile = getNimsFile(scriptName, options) + + let (output, exitCode, stdout) = + execNimscript( + nimsFile, scriptName.parentDir(), actionName, options, isHook + ) + + if exitCode != 0: + let errMsg = + if stdout.len != 0: + stdout + else: + "Exception raised during nimble script execution" + raise newException(NimbleError, errMsg) + + let + j = + if output.len != 0: + parseJson(output) + else: + parseJson("{}") + + result.flags = newTable[string, seq[string]]() + result.success = j{"success"}.getBool() + result.command = j{"command"}.getStr() + if "project" in j: + result.arguments.add j["project"].getStr() + if "flags" in j: + for flag, vals in j["flags"].pairs: + result.flags[flag] = @[] + for val in vals.items(): + result.flags[flag].add val.getStr() + result.retVal = j{"retVal"}.getBool() + + stdout.writeExecutionOutput() + +proc execTask*(scriptName, taskName: string, + options: Options): ExecutionResult[bool] = + ## Executes the specified task in the specified script. + ## + ## `scriptName` should be a filename pointing to the nimscript file. + display("Executing", "task $# in $#" % [taskName, scriptName], + priority = HighPriority) + + result = execScript(scriptName, taskName, options, isHook=false) + +proc execHook*(scriptName, actionName: string, before: bool, + options: Options): ExecutionResult[bool] = + ## Executes the specified action's hook. Depending on ``before``, either + ## the "before" or the "after" hook. + ## + ## `scriptName` should be a filename pointing to the nimscript file. + let hookName = + if before: actionName.toLowerAscii & "Before" + else: actionName.toLowerAscii & "After" + display("Attempting", "to execute hook $# in $#" % [hookName, scriptName], + priority = MediumPriority) + + result = execScript(scriptName, hookName, options, isHook=true) + +proc hasTaskRequestedCommand*(execResult: ExecutionResult): bool = + ## Determines whether the last executed task used ``setCommand`` + return execResult.command != internalCmd + +proc listTasks*(scriptName: string, options: Options) = + discard execScript(scriptName, "", options, isHook=false) diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 725b5ba..14d1c31 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -1,15 +1,18 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import json, strutils, os, parseopt, strtabs, uri, tables +import json, strutils, os, parseopt, strtabs, uri, tables, terminal +import sequtils, sugar +import std/options as std_opt from httpclient import Proxy, newProxy -import config, version, tools, common, cli +import config, version, common, cli type Options* = object forcePrompts*: ForcePrompt depsOnly*: bool + uninstallRevDeps*: bool queryVersions*: bool queryInstalled*: bool nimbleDir*: string @@ -22,12 +25,18 @@ type showVersion*: bool noColor*: bool disableValidation*: bool + continueTestsOnFailure*: bool + ## Whether packages' repos should always be downloaded with their history. + forceFullClone*: bool + # Temporary storage of flags that have not been captured by any specific Action. + unknownFlags*: seq[(CmdLineKind, string, string)] ActionType* = enum actionNil, actionRefresh, actionInit, actionDump, actionPublish, actionInstall, actionSearch, actionList, actionBuild, actionPath, actionUninstall, actionCompile, - actionDoc, actionCustom, actionTasks, actionDevelop, actionCheck + actionDoc, actionCustom, actionTasks, actionDevelop, actionCheck, + actionRun Action* = object case typ*: ActionType @@ -37,14 +46,20 @@ type of actionInstall, actionPath, actionUninstall, actionDevelop: packages*: seq[PkgTuple] # Optional only for actionInstall # and actionDevelop. + passNimFlags*: seq[string] of actionSearch: search*: seq[string] # Search string. of actionInit, actionDump: projName*: string + vcsOption*: string of actionCompile, actionDoc, actionBuild: file*: string backend*: string - compileOptions*: seq[string] + compileOptions: seq[string] + of actionRun: + runFile: Option[string] + compileFlags: seq[string] + runFlags*: seq[string] of actionCustom: command*: string arguments*: seq[string] @@ -57,20 +72,32 @@ Usage: nimble COMMAND [opts] Commands: install [pkgname, ...] Installs a list of packages. [-d, --depsOnly] Install only dependencies. + [-p, --passNim] Forward specified flag to compiler. develop [pkgname, ...] Clones a list of packages for development. Symlinks the cloned packages or any package in the current working directory. check Verifies the validity of a package in the current working directory. - init [pkgname] Initializes a new Nimble project. + init [pkgname] Initializes a new Nimble project in the + current directory or if a name is provided a + new directory of the same name. + --git + --hg Create a git or hg repo in the new nimble project. publish Publishes a package on nim-lang/packages. The current working directory needs to be the toplevel directory of the Nimble package. uninstall [pkgname, ...] Uninstalls a list of packages. - build Builds a package. + [-i, --inclDeps] Uninstall package and dependent package(s). + build [opts, ...] [bin] Builds a package. + run [opts, ...] [bin] Builds and runs a package. + Binary needs to be specified after any + compilation options if there are several + binaries defined, any flags after the binary + or -- arg are passed to the binary when it is run. c, cc, js [opts, ...] f.nim Builds a file inside a package. Passes options to the Nim compiler. test Compiles and executes tests + [-c, --continue] Don't stop execution on a failed test. doc, doc2 [opts, ...] f.nim Builds documentation for a file inside a package. Passes options to the Nim compiler. refresh [url] Refreshes the package list. A package list URL @@ -133,6 +160,8 @@ proc parseActionType*(action: string): ActionType = result = actionPath of "build": result = actionBuild + of "run": + result = actionRun of "c", "compile", "js", "cpp", "cc": result = actionCompile of "doc", "doc2": @@ -167,6 +196,7 @@ proc initAction*(options: var Options, key: string) = case options.action.typ of actionInstall, actionPath, actionDevelop, actionUninstall: options.action.packages = @[] + options.action.passNimFlags = @[] of actionCompile, actionDoc, actionBuild: options.action.compileOptions = @[] options.action.file = "" @@ -174,8 +204,11 @@ proc initAction*(options: var Options, key: string) = else: options.action.backend = keyNorm of actionInit: options.action.projName = "" + options.action.vcsOption = "" of actionDump: options.action.projName = "" + options.action.vcsOption = "" + options.forcePrompts = forcePromptYes of actionRefresh: options.action.optionalURL = "" of actionSearch: @@ -184,7 +217,7 @@ proc initAction*(options: var Options, key: string) = options.action.command = key options.action.arguments = @[] options.action.flags = newStringTable() - of actionPublish, actionList, actionTasks, actionCheck, + of actionPublish, actionList, actionTasks, actionCheck, actionRun, actionNil: discard proc prompt*(options: Options, question: string): bool = @@ -208,18 +241,6 @@ proc promptList*(options: Options, question: string, args: openarray[string]): s ## options is selected. return promptList(options.forcePrompts, question, args) -proc renameBabelToNimble(options: Options) {.deprecated.} = - let babelDir = getHomeDir() / ".babel" - let nimbleDir = getHomeDir() / ".nimble" - if dirExists(babelDir): - if options.prompt("Found deprecated babel package directory, would you " & - "like to rename it to nimble?"): - copyDir(babelDir, nimbleDir) - copyFile(babelDir / "babeldata.json", nimbleDir / "nimbledata.json") - - removeDir(babelDir) - removeFile(nimbleDir / "babeldata.json") - proc getNimbleDir*(options: Options): string = result = options.config.nimbleDir if options.nimbleDir.len != 0: @@ -242,9 +263,15 @@ proc getBinDir*(options: Options): string = options.getNimbleDir() / "bin" proc parseCommand*(key: string, result: var Options) = - result.action.typ = parseActionType(key) + result.action = Action(typ: parseActionType(key)) initAction(result, key) +proc setRunOptions(result: var Options, key, val: string, isArg: bool) = + if result.action.runFile.isNone() and (isArg or val == "--"): + result.action.runFile = some(key) + else: + result.action.runFlags.add(val) + proc parseArgument*(key: string, result: var Options) = case result.action.typ of actionNil: @@ -265,23 +292,40 @@ proc parseArgument*(key: string, result: var Options) = result.action.search.add(key) of actionInit, actionDump: if result.action.projName != "": - raise newException(NimbleError, - "Can only initialize one package at a time.") + raise newException( + NimbleError, "Can only perform this action on one package at a time." + ) result.action.projName = key of actionCompile, actionDoc: result.action.file = key - of actionList, actionBuild, actionPublish: + of actionList, actionPublish: result.showHelp = true + of actionBuild: + result.action.file = key + of actionRun: + result.setRunOptions(key, key, true) of actionCustom: result.action.arguments.add(key) else: discard +proc getFlagString(kind: CmdLineKind, flag, val: string): string = + let prefix = + case kind + of cmdShortOption: "-" + of cmdLongOption: "--" + else: "" + if val == "": + return prefix & flag + else: + return prefix & flag & ":" & val + proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = - var wasFlagHandled = true + let f = flag.normalize() # Global flags. + var isGlobalFlag = true case f of "help", "h": result.showHelp = true of "version", "v": result.showVersion = true @@ -292,42 +336,64 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = of "debug": result.verbosity = DebugPriority of "nocolor": result.noColor = true of "disablevalidation": result.disableValidation = true + else: isGlobalFlag = false + + var wasFlagHandled = true # Action-specific flags. - else: - case result.action.typ - of actionSearch, actionList: - case f - of "installed", "i": - result.queryInstalled = true - of "ver": - result.queryVersions = true - else: - wasFlagHandled = false - of actionInstall: - case f - of "depsonly", "d": - result.depsOnly = true - else: - wasFlagHandled = false - of actionCompile, actionDoc, actionBuild: - let prefix = if kind == cmdShortOption: "-" else: "--" - if val == "": - result.action.compileOptions.add(prefix & flag) - else: - result.action.compileOptions.add(prefix & flag & ":" & val) - of actionCustom: - result.action.flags[flag] = val + case result.action.typ + of actionSearch, actionList: + case f + of "installed", "i": + result.queryInstalled = true + of "ver": + result.queryVersions = true else: wasFlagHandled = false + of actionInstall: + case f + of "depsonly", "d": + result.depsOnly = true + of "passnim", "p": + result.action.passNimFlags.add(val) + else: + wasFlagHandled = false + of actionInit: + case f + of "git", "hg": + result.action.vcsOption = f + else: + wasFlagHandled = false + of actionUninstall: + case f + of "incldeps", "i": + result.uninstallRevDeps = true + else: + wasFlagHandled = false + of actionCompile, actionDoc, actionBuild: + if not isGlobalFlag: + result.action.compileOptions.add(getFlagString(kind, flag, val)) + of actionRun: + result.showHelp = false + result.setRunOptions(flag, getFlagString(kind, flag, val), false) + of actionCustom: + if result.action.command.normalize == "test": + if f == "continue" or f == "c": + result.continueTestsOnFailure = true + result.action.flags[flag] = val + else: + wasFlagHandled = false - if not wasFlagHandled: - raise newException(NimbleError, "Unknown option: --" & flag) + if not wasFlagHandled and not isGlobalFlag: + result.unknownFlags.add((kind, flag, val)) proc initOptions*(): Options = - result.action.typ = actionNil - result.pkgInfoCache = newTable[string, PackageInfo]() - result.nimbleDir = "" - result.verbosity = HighPriority + # Exported for choosenim + Options( + action: Action(typ: actionNil), + pkgInfoCache: newTable[string, PackageInfo](), + verbosity: HighPriority, + noColor: not isatty(stdout) + ) proc parseMisc(options: var Options) = # Load nimbledata.json @@ -342,6 +408,29 @@ proc parseMisc(options: var Options) = else: options.nimbleData = %{"reverseDeps": newJObject()} +proc handleUnknownFlags(options: var Options) = + if options.action.typ == actionRun: + # ActionRun uses flags that come before the command as compilation flags + # and flags that come after as run flags. + options.action.compileFlags = + map(options.unknownFlags, x => getFlagString(x[0], x[1], x[2])) + options.unknownFlags = @[] + else: + # For everything else, handle the flags that came before the command + # normally. + let unknownFlags = options.unknownFlags + options.unknownFlags = @[] + for flag in unknownFlags: + parseFlag(flag[1], flag[2], options, flag[0]) + + # Any unhandled flags? + if options.unknownFlags.len > 0: + let flag = options.unknownFlags[0] + raise newException( + NimbleError, + "Unknown option: " & getFlagString(flag[0], flag[1], flag[2]) + ) + proc parseCmdLine*(): Options = result = initOptions() @@ -355,9 +444,11 @@ proc parseCmdLine*(): Options = else: parseArgument(key, result) of cmdLongOption, cmdShortOption: - parseFlag(key, val, result, kind) + parseFlag(key, val, result, kind) of cmdEnd: assert(false) # cannot happen + handleUnknownFlags(result) + # Set verbosity level. setVerbosity(result.verbosity) @@ -373,6 +464,11 @@ proc parseCmdLine*(): Options = if result.action.typ == actionNil and not result.showVersion: result.showHelp = true + if result.action.typ != actionNil and result.showVersion: + # We've got another command that should be handled. For example: + # nimble run foobar -v + result.showVersion = false + proc getProxy*(options: Options): Proxy = ## Returns ``nil`` if no proxy is specified. var url = "" @@ -384,6 +480,10 @@ proc getProxy*(options: Options): Proxy = url = getEnv("http_proxy") elif existsEnv("https_proxy"): url = getEnv("https_proxy") + elif existsEnv("HTTP_PROXY"): + url = getEnv("HTTP_PROXY") + elif existsEnv("HTTPS_PROXY"): + url = getEnv("HTTPS_PROXY") except ValueError: display("Warning:", "Unable to parse proxy from environment: " & getCurrentExceptionMsg(), Warning, HighPriority) @@ -404,5 +504,48 @@ proc briefClone*(options: Options): Options = var newOptions = initOptions() newOptions.config = options.config newOptions.nimbleData = options.nimbleData + newOptions.nimbleDir = options.nimbleDir + newOptions.forcePrompts = options.forcePrompts newOptions.pkgInfoCache = options.pkgInfoCache return newOptions + +proc shouldRemoveTmp*(options: Options, file: string): bool = + result = true + if options.verbosity <= DebugPriority: + let msg = "Not removing temporary path because of debug verbosity: " & file + display("Warning:", msg, Warning, MediumPriority) + return false + +proc getCompilationFlags*(options: var Options): var seq[string] = + case options.action.typ + of actionBuild, actionDoc, actionCompile: + return options.action.compileOptions + of actionRun: + return options.action.compileFlags + else: + assert false + +proc getCompilationFlags*(options: Options): seq[string] = + var opt = options + return opt.getCompilationFlags() + +proc getCompilationBinary*(options: Options, pkgInfo: PackageInfo): Option[string] = + case options.action.typ + of actionBuild, actionDoc, actionCompile: + let file = options.action.file.changeFileExt("") + if file.len > 0: + return some(file) + of actionRun: + let optRunFile = options.action.runFile + let runFile = + if optRunFile.get("").len > 0: + optRunFile.get() + elif pkgInfo.bin.len == 1: + pkgInfo.bin[0] + else: + "" + + if runFile.len > 0: + return some(runFile.changeFileExt(ExeExt)) + else: + discard diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index 10e8bfa..f00c59f 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -3,8 +3,7 @@ # Stdlib imports import system except TResult -import parsecfg, json, streams, strutils, parseutils, os, sets, tables -import httpclient +import hashes, json, strutils, os, sets, tables, httpclient # Local imports import version, tools, common, options, cli, config @@ -107,8 +106,8 @@ proc requiredField(obj: JsonNode, name: string): string = ## Queries ``obj`` for the required ``name`` string. ## ## Aborts execution if the field does not exist or is of invalid json type. - result = optionalField(obj, name, nil) - if result == nil: + result = optionalField(obj, name) + if result.len == 0: raise newException(NimbleError, "Package in packages.json file does not contain a " & name & " field.") @@ -278,7 +277,7 @@ proc getPackage*(pkg: string, options: Options, resPkg: var Package): bool = proc getPackageList*(options: Options): seq[Package] = ## Returns the list of packages found in the downloaded packages.json files. result = @[] - var namesAdded = initSet[string]() + var namesAdded = initHashSet[string]() for name, list in options.config.packageLists: let packages = readPackageList(name, options) for p in packages: @@ -313,14 +312,17 @@ proc findNimbleFile*(dir: string; error: bool): string = # Return the path of the real .nimble file. result = readNimbleLink(result).nimbleFilePath if not fileExists(result): - raiseNimbleError("The .nimble-link file is pointing to a missing" & - " file: " & result) + let msg = "The .nimble-link file is pointing to a missing file: " & result + let hintMsg = + "Remove '$1' or restore the file it points to." % dir + display("Warning:", msg, Warning, HighPriority) + display("Hint:", hintMsg, Warning, HighPriority) proc getInstalledPkgsMin*(libsDir: string, options: Options): seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = ## Gets a list of installed packages. The resulting package info is ## minimal. This has the advantage that it does not depend on the - ## ``packageparser`` module, and so can be used by ``nimscriptsupport``. + ## ``packageparser`` module, and so can be used by ``nimscriptwrapper``. ## ## ``libsDir`` is in most cases: ~/.nimble/pkgs/ (options.getPkgsDir) result = @[] @@ -336,8 +338,17 @@ proc getInstalledPkgsMin*(libsDir: string, options: Options): pkg.specialVersion = version pkg.isMinimal = true pkg.isInstalled = true - pkg.isLinked = - cmpPaths(nimbleFile.splitFile().dir, path) != 0 + let nimbleFileDir = nimbleFile.splitFile().dir + pkg.isLinked = cmpPaths(nimbleFileDir, path) != 0 + + # Read the package's 'srcDir' (this is stored in the .nimble-link so + # we can easily grab it) + if pkg.isLinked: + let nimbleLinkPath = path / name.addFileExt("nimble-link") + let realSrcPath = readNimbleLink(nimbleLinkPath).packageDir + assert realSrcPath.startsWith(nimbleFileDir) + pkg.srcDir = realSrcPath.replace(nimbleFileDir) + pkg.srcDir.removePrefix(DirSep) result.add((pkg, meta)) proc withinRange*(pkgInfo: PackageInfo, verRange: VersionRange): bool = @@ -371,8 +382,7 @@ proc findPkg*(pkglist: seq[tuple[pkgInfo: PackageInfo, meta: MetaData]], if cmpIgnoreStyle(pkg.pkginfo.name, dep.name) != 0 and cmpIgnoreStyle(pkg.meta.url, dep.name) != 0: continue if withinRange(pkg.pkgInfo, dep.ver): - let isNewer = (not r.version.isNil) and - newVersion(r.version) < newVersion(pkg.pkginfo.version) + let isNewer = newVersion(r.version) < newVersion(pkg.pkginfo.version) if not result or isNewer: r = pkg.pkginfo result = true @@ -526,6 +536,15 @@ proc getPkgDest*(pkgInfo: PackageInfo, options: Options): string = let pkgDestDir = options.getPkgsDir() / (pkgInfo.name & versionStr) return pkgDestDir +proc `==`*(pkg1: PackageInfo, pkg2: PackageInfo): bool = + if pkg1.name == pkg2.name and pkg1.myPath == pkg2.myPath: + return true + +proc hash*(x: PackageInfo): Hash = + var h: Hash = 0 + h = h !& hash(x.myPath) + result = !$h + when isMainModule: doAssert getNameVersion("/home/user/.nimble/libs/packagea-0.1") == ("packagea", "0.1") diff --git a/src/nimblepkg/packageinstaller.nim b/src/nimblepkg/packageinstaller.nim index e48a910..4bdc921 100644 --- a/src/nimblepkg/packageinstaller.nim +++ b/src/nimblepkg/packageinstaller.nim @@ -3,7 +3,13 @@ import os, strutils, sets, json # Local imports -import cli, common, options, tools +import cli, options, tools + +when defined(windows): + import version + +when not declared(initHashSet) or not declared(toHashSet): + import common when defined(windows): # This is just for Win XP support. @@ -81,7 +87,7 @@ proc saveNimbleMeta*(pkgDestDir, url, vcsRevision: string, ## ## isLink - Determines whether the installed package is a .nimble-link. var nimblemeta = %{"url": %url} - if not vcsRevision.isNil: + if vcsRevision.len > 0: nimblemeta["vcsRevision"] = %vcsRevision let files = newJArray() nimblemeta["files"] = files @@ -103,4 +109,4 @@ proc saveNimbleMeta*(pkgDestDir, pkgDir, vcsRevision, nimbleLinkPath: string) = ## pkgDir - The directory where the original package files are. ## For example: ~/projects/jester/ saveNimbleMeta(pkgDestDir, "file://" & pkgDir, vcsRevision, - toSet[string]([nimbleLinkPath]), initSet[string](), true) \ No newline at end of file + toHashSet[string]([nimbleLinkPath]), initHashSet[string](), true) \ No newline at end of file diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim index 17f4464..9458074 100644 --- a/src/nimblepkg/packageparser.nim +++ b/src/nimblepkg/packageparser.nim @@ -1,12 +1,12 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import parsecfg, json, streams, strutils, parseutils, os, tables, future +import parsecfg, sets, streams, strutils, os, tables, sugar from sequtils import apply, map -import version, tools, common, nimscriptsupport, options, packageinfo, cli +import version, tools, common, nimscriptwrapper, options, packageinfo, cli ## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` -## because it depends on ``nimscriptsupport`` (``nimscriptsupport`` also +## because it depends on ``nimscriptwrapper`` (``nimscriptwrapper`` also ## depends on other procedures in ``packageinfo``. type @@ -212,7 +212,10 @@ proc multiSplit(s: string): seq[string] = result.del(i) # Huh, nothing to return? Return given input. if len(result) < 1: - return @[s] + if s.strip().len != 0: + return @[s] + else: + return @[] proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = var fs = newFileStream(path, fmRead) @@ -253,12 +256,20 @@ proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = result.installExt.add(ev.value.multiSplit) of "bin": for i in ev.value.multiSplit: + if i.splitFile().ext == ".nim": + raise newException(NimbleError, "`bin` entry should not be a source file: " & i) result.bin.add(i.addFileExt(ExeExt)) of "backend": result.backend = ev.value.toLowerAscii() case result.backend.normalize of "javascript": result.backend = "js" else: discard + of "beforehooks": + for i in ev.value.multiSplit: + result.preHooks.incl(i.normalize) + of "afterhooks": + for i in ev.value.multiSplit: + result.postHooks.incl(i.normalize) else: raise newException(NimbleError, "Invalid field: " & ev.key) of "deps", "dependencies": @@ -277,6 +288,33 @@ proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = else: raise newException(ValueError, "Cannot open package info: " & path) +proc readPackageInfoFromNims(scriptName: string, options: Options, + result: var PackageInfo) = + let + iniFile = getIniFile(scriptName, options) + + if iniFile.fileExists(): + readPackageInfoFromNimble(iniFile, result) + +proc inferInstallRules(pkgInfo: var PackageInfo, options: Options) = + # Binary packages shouldn't install .nim files by default. + # (As long as the package info doesn't explicitly specify what should be + # installed.) + let installInstructions = + pkgInfo.installDirs.len + pkgInfo.installExt.len + pkgInfo.installFiles.len + if installInstructions == 0 and pkgInfo.bin.len > 0: + pkgInfo.skipExt.add("nim") + + # When a package doesn't specify a `srcDir` it's fair to assume that + # the .nim files are in the root of the package. So we can explicitly select + # them and prevent the installation of anything else. The user can always + # override this with `installFiles`. + if pkgInfo.srcDir == "": + if dirExists(pkgInfo.getRealDir() / pkgInfo.name): + pkgInfo.installDirs.add(pkgInfo.name) + if fileExists(pkgInfo.getRealDir() / pkgInfo.name.addFileExt("nim")): + pkgInfo.installFiles.add(pkgInfo.name.addFileExt("nim")) + proc readPackageInfo(nf: NimbleFile, options: Options, onlyMinimalInfo=false): PackageInfo = ## Reads package info from the specified Nimble file. @@ -351,6 +389,9 @@ proc readPackageInfo(nf: NimbleFile, options: Options, if version.kind == verSpecial: result.specialVersion = minimalInfo.version + # Apply rules to infer which files should/shouldn't be installed. See #469. + inferInstallRules(result, options) + if not result.isMinimal: options.pkgInfoCache[nf] = result @@ -446,6 +487,15 @@ proc toFullInfo*(pkg: PackageInfo, options: Options): PackageInfo = else: return pkg +proc getConcreteVersion*(pkgInfo: PackageInfo, options: Options): string = + ## Returns a non-special version from the specified ``pkgInfo``. If the + ## ``pkgInfo`` is minimal it looks it up and retrieves the concrete version. + result = pkgInfo.version + if pkgInfo.isMinimal: + let pkgInfo = pkgInfo.toFullInfo(options) + result = pkgInfo.version + assert(not newVersion(result).isSpecial) + when isMainModule: validatePackageName("foo_bar") validatePackageName("f_oo_b_a_r") diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim index c597aa3..f11b979 100644 --- a/src/nimblepkg/publish.nim +++ b/src/nimblepkg/publish.nim @@ -5,8 +5,8 @@ ## nim-lang/packages automatically. import system except TResult -import httpclient, base64, strutils, rdstdin, json, os, browsers, times, uri -import tools, common, cli, config, options +import httpclient, strutils, json, os, browsers, times, uri +import version, tools, common, cli, config, options type Auth = object @@ -62,7 +62,7 @@ proc getGithubAuth(o: Options): Auth = # try to read from disk, if it cannot be found write a new one try: let apiTokenFilePath = cfg.nimbleDir / ApiKeyFile - result.token = readFile(apiTokenFilePath) + result.token = readFile(apiTokenFilePath).strip() display("Info:", "Using GitHub API Token in file: " & apiTokenFilePath, priority = HighPriority) except IOError: @@ -76,7 +76,7 @@ proc getGithubAuth(o: Options): Auth = proc isCorrectFork(j: JsonNode): bool = # Check whether this is a fork of the nimble packages repo. result = false - if j{"fork"}.getBVal(): + if j{"fork"}.getBool(): result = j{"parent"}{"full_name"}.getStr() == "nim-lang/packages" proc forkExists(a: Auth): bool = @@ -94,11 +94,13 @@ proc createFork(a: Auth) = raise newException(NimbleError, "Unable to create fork. Access token" & " might not have enough permissions.") -proc createPullRequest(a: Auth, packageName, branch: string) = +proc createPullRequest(a: Auth, packageName, branch: string): string = display("Info", "Creating PR", priority = HighPriority) - discard a.http.postContent(ReposUrl & "nim-lang/packages/pulls", + var body = a.http.postContent(ReposUrl & "nim-lang/packages/pulls", body="""{"title": "Add package $1", "head": "$2:$3", "base": "master"}""" % [packageName, a.user, branch]) + var pr = parseJson(body) + return pr{"html_url"}.getStr() proc `%`(s: openArray[string]): JsonNode = result = newJArray() @@ -153,7 +155,7 @@ proc editJson(p: PackageInfo; url, tags, downloadMethod: string) = proc publish*(p: PackageInfo, o: Options) = ## Publishes the package p. let auth = getGithubAuth(o) - var pkgsDir = getTempDir() / "nimble-packages-fork" + var pkgsDir = getNimbleUserTempDir() / "nimble-packages-fork" if not forkExists(auth): createFork(auth) display("Info:", "Waiting 10s to let Github create a fork", @@ -211,14 +213,17 @@ proc publish*(p: PackageInfo, o: Options) = url = promptCustom("Github URL of " & p.name & "?", "") if url.len == 0: userAborted() - let tags = promptCustom("Whitespace separated list of tags?", "") + let tags = promptCustom( + "Whitespace separated list of tags? (For example: web library wrapper)", + "" + ) cd pkgsDir: editJson(p, url, tags, downloadMethod) - let branchName = "add-" & p.name & getTime().getGMTime().format("HHmm") + let branchName = "add-" & p.name & getTime().utc.format("HHmm") doCmd("git checkout -B " & branchName) doCmd("git commit packages.json -m \"Added package " & p.name & "\"") display("Pushing", "to remote of fork.", priority = HighPriority) doCmd("git push https://" & auth.token & "@github.com/" & auth.user & "/packages " & branchName) - createPullRequest(auth, p.name, branchName) - display("Success:", "Pull request successful.", Success, HighPriority) \ No newline at end of file + let prUrl = createPullRequest(auth, p.name, branchName) + display("Success:", "Pull request successful, check at " & prUrl , Success, HighPriority) diff --git a/src/nimblepkg/reversedeps.nim b/src/nimblepkg/reversedeps.nim index 551ee19..45d9940 100644 --- a/src/nimblepkg/reversedeps.nim +++ b/src/nimblepkg/reversedeps.nim @@ -1,7 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import os, json +import os, json, sets import options, common, version, download, packageinfo @@ -58,7 +58,7 @@ proc removeRevDep*(nimbleData: JsonNode, pkg: PackageInfo) = newData[key] = newVal nimbleData["reverseDeps"] = newData -proc getRevDeps*(options: Options, pkg: PackageInfo): seq[PkgTuple] = +proc getRevDepTups*(options: Options, pkg: PackageInfo): seq[PkgTuple] = ## Returns a list of *currently installed* reverse dependencies for `pkg`. result = @[] let thisPkgsDep = @@ -76,6 +76,26 @@ proc getRevDeps*(options: Options, pkg: PackageInfo): seq[PkgTuple] = result.add(pkgTup) +proc getRevDeps*(options: Options, pkg: PackageInfo): HashSet[PackageInfo] = + result.init() + let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) + for rdepTup in getRevDepTups(options, pkg): + for rdepInfo in findAllPkgs(installedPkgs, rdepTup): + result.incl rdepInfo + +proc getAllRevDeps*(options: Options, pkg: PackageInfo, result: var HashSet[PackageInfo]) = + if pkg in result: + return + + let installedPkgs = getInstalledPkgsMin(options.getPkgsDir(), options) + for rdepTup in getRevDepTups(options, pkg): + for rdepInfo in findAllPkgs(installedPkgs, rdepTup): + if rdepInfo in result: + continue + + getAllRevDeps(options, rdepInfo, result) + result.incl pkg + when isMainModule: var nimbleData = %{"reverseDeps": newJObject()} diff --git a/src/nimblepkg/tools.nim b/src/nimblepkg/tools.nim index 7ed5501..a0e6a0e 100644 --- a/src/nimblepkg/tools.nim +++ b/src/nimblepkg/tools.nim @@ -3,7 +3,7 @@ # # Various miscellaneous utility functions reside here. import osproc, pegs, strutils, os, uri, sets, json, parseutils -import version, common, cli +import version, cli proc extractBin(cmd: string): string = if cmd[0] == '"': @@ -11,7 +11,7 @@ proc extractBin(cmd: string): string = else: return cmd.split(' ')[0] -proc doCmd*(cmd: string, showOutput = false) = +proc doCmd*(cmd: string, showOutput = false, showCmd = false) = let bin = extractBin(cmd) if findExe(bin) == "": raise newException(NimbleError, "'" & bin & "' not in PATH.") @@ -20,7 +20,10 @@ proc doCmd*(cmd: string, showOutput = false) = stdout.flushFile() stderr.flushFile() - displayDebug("Executing", cmd) + if showCmd: + display("Executing", cmd, priority = MediumPriority) + else: + displayDebug("Executing", cmd) if showOutput: let exitCode = execCmd(cmd) displayDebug("Finished", "with exit code " & $exitCode) @@ -87,7 +90,7 @@ proc changeRoot*(origRoot, newRoot, path: string): string = ## the trailing separator. This would cause this method to throw during package ## installation. if path.startsWith(origRoot) or path.samePaths(origRoot): - return newRoot / path[origRoot.len .. path.len-1] + return newRoot / path.substr(origRoot.len, path.len-1) else: raise newException(ValueError, "Cannot change root of path: Path does not begin with original root.") @@ -106,6 +109,10 @@ proc copyDirD*(fro, to: string): seq[string] = createDir(changeRoot(fro, to, path.splitFile.dir)) result.add copyFileD(path, changeRoot(fro, to, path)) +proc createDirD*(dir: string) = + display("Creating", "directory $#" % dir, priority = LowPriority) + createDir(dir) + proc getDownloadDirName*(uri: string, verRange: VersionRange): string = ## Creates a directory name based on the specified ``uri`` (url) result = "" @@ -144,6 +151,15 @@ proc contains*(j: JsonNode, elem: tuple[key: string, val: JsonNode]): bool = when not defined(windows): from posix import getpid + +proc getProcessId*(): string = + when defined(windows): + proc GetCurrentProcessId(): int32 {.stdcall, dynlib: "kernel32", + importc: "GetCurrentProcessId".} + result = $GetCurrentProcessId() + else: + result = $getpid() + proc getNimbleTempDir*(): string = ## Returns a path to a temporary directory. ## @@ -151,10 +167,18 @@ proc getNimbleTempDir*(): string = ## different for different runs of it. You have to make sure to create it ## first. In release builds the directory will be removed when nimble finishes ## its work. - result = getTempDir() / "nimble_" - when defined(windows): - proc GetCurrentProcessId(): int32 {.stdcall, dynlib: "kernel32", - importc: "GetCurrentProcessId".} - result.add($GetCurrentProcessId()) + result = getTempDir() / "nimble_" & getProcessId() + +proc getNimbleUserTempDir*(): string = + ## Returns a path to a temporary directory. + ## + ## The returned path will be the same for the duration of the process but + ## different for different runs of it. You have to make sure to create it + ## first. In release builds the directory will be removed when nimble finishes + ## its work. + var tmpdir: string + if existsEnv("TMPDIR") and existsEnv("USER"): + tmpdir = joinPath(getEnv("TMPDIR"), getEnv("USER")) else: - result.add($getpid()) + tmpdir = getTempDir() + return tmpdir diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index 7fa7a73..e4114f1 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -39,8 +39,6 @@ proc `$`*(ver: Version): string {.borrow.} proc hash*(ver: Version): Hash {.borrow.} -proc isNil*(ver: Version): bool {.borrow.} - proc newVersion*(ver: string): Version = doAssert(ver.len == 0 or ver[0] in {'#', '\0'} + Digits, "Wrong version: " & ver) @@ -95,6 +93,11 @@ proc `==`*(ver: Version, ver2: Version): bool = else: return false +proc cmp*(a, b: Version): int = + if a < b: -1 + elif a > b: 1 + else: 0 + proc `<=`*(ver: Version, ver2: Version): bool = return (ver == ver2) or (ver < ver2) @@ -132,46 +135,44 @@ proc contains*(ran: VersionRange, ver: Version): bool = return withinRange(ver, ran) proc makeRange*(version: string, op: string): VersionRange = - new(result) if version == "": raise newException(ParseVersionError, "A version needs to accompany the operator.") case op of ">": - result.kind = verLater + result = VersionRange(kind: verLater) of "<": - result.kind = verEarlier + result = VersionRange(kind: verEarlier) of ">=": - result.kind = verEqLater + result = VersionRange(kind: verEqLater) of "<=": - result.kind = verEqEarlier - of "": - result.kind = verEq + result = VersionRange(kind: verEqEarlier) + of "", "==": + result = VersionRange(kind: verEq) else: raise newException(ParseVersionError, "Invalid operator: " & op) result.ver = Version(version) proc parseVersionRange*(s: string): VersionRange = # >= 1.5 & <= 1.8 - new(result) if s.len == 0: - result.kind = verAny + result = VersionRange(kind: verAny) return if s[0] == '#': - result.kind = verSpecial + result = VersionRange(kind: verSpecial) result.spe = s.Version return var i = 0 var op = "" var version = "" - while true: + while i < s.len: case s[i] of '>', '<', '=': op.add(s[i]) of '&': - result.kind = verIntersect + result = VersionRange(kind: verIntersect) result.verILeft = makeRange(version, op) # Parse everything after & @@ -184,18 +185,14 @@ proc parseVersionRange*(s: string): VersionRange = raise newException(ParseVersionError, "Having more than one `&` in a version range is pointless") - break + return of '0'..'9', '.': version.add(s[i]) - of '\0': - result = makeRange(version, op) - break - of ' ': # Make sure '0.9 8.03' is not allowed. - if version != "" and i < s.len: + if version != "" and i < s.len - 1: if s[i+1] in {'0'..'9', '.'}: raise newException(ParseVersionError, "Whitespace is not allowed in a version literal.") @@ -204,15 +201,16 @@ proc parseVersionRange*(s: string): VersionRange = raise newException(ParseVersionError, "Unexpected char in version range '" & s & "': " & s[i]) inc(i) + result = makeRange(version, op) proc toVersionRange*(ver: Version): VersionRange = ## Converts a version to either a verEq or verSpecial VersionRange. new(result) if ver.isSpecial: - result.kind = verSpecial + result = VersionRange(kind: verSpecial) result.spe = ver else: - result.kind = verEq + result = VersionRange(kind: verEq) result.ver = ver proc parseRequires*(req: string): PkgTuple = @@ -268,21 +266,18 @@ proc getSimpleString*(verRange: VersionRange): string = result = "" proc newVRAny*(): VersionRange = - new(result) - result.kind = verAny + result = VersionRange(kind: verAny) proc newVREarlier*(ver: string): VersionRange = - new(result) - result.kind = verEarlier + result = VersionRange(kind: verEarlier) result.ver = newVersion(ver) proc newVREq*(ver: string): VersionRange = - new(result) - result.kind = verEq + result = VersionRange(kind: verEq) result.ver = newVersion(ver) proc findLatest*(verRange: VersionRange, - versions: Table[Version, string]): tuple[ver: Version, tag: string] = + versions: OrderedTable[Version, string]): tuple[ver: Version, tag: string] = result = (newVersion(""), "") for ver, tag in versions: if not withinRange(ver, verRange): continue @@ -296,16 +291,17 @@ when isMainModule: doAssert(newVersion("1.0") < newVersion("1.4")) doAssert(newVersion("1.0.1") > newVersion("1.0")) doAssert(newVersion("1.0.6") <= newVersion("1.0.6")) - #doAssert(not withinRange(newVersion("0.1.0"), parseVersionRange("> 0.1"))) + doAssert(not withinRange(newVersion("0.1.0"), parseVersionRange("> 0.1"))) doAssert(not (newVersion("0.1.0") < newVersion("0.1"))) doAssert(not (newVersion("0.1.0") > newVersion("0.1"))) doAssert(newVersion("0.1.0") < newVersion("0.1.0.0.1")) doAssert(newVersion("0.1.0") <= newVersion("0.1")) var inter1 = parseVersionRange(">= 1.0 & <= 1.5") + doAssert(inter1.kind == verIntersect) var inter2 = parseVersionRange("1.0") doAssert(inter2.kind == verEq) - #echo(parseVersionRange(">= 0.8 0.9")) + doAssert(parseVersionRange("== 3.4.2") == parseVersionRange("3.4.2")) doAssert(not withinRange(newVersion("1.5.1"), inter1)) doAssert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1)) @@ -319,8 +315,11 @@ when isMainModule: doAssert(newVersion("") < newVersion("1.0.0")) doAssert(newVersion("") < newVersion("0.1.0")) - var versions = toTable[Version, string]({newVersion("0.1.1"): "v0.1.1", - newVersion("0.2.3"): "v0.2.3", newVersion("0.5"): "v0.5"}) + var versions = toOrderedTable[Version, string]({ + newVersion("0.1.1"): "v0.1.1", + newVersion("0.2.3"): "v0.2.3", + newVersion("0.5"): "v0.5" + }) doAssert findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) == (newVersion("0.2.3"), "v0.2.3") @@ -356,4 +355,7 @@ when isMainModule: doAssert toVersionRange(newVersion("#head")).kind == verSpecial doAssert toVersionRange(newVersion("0.2.0")).kind == verEq + # Something raised on IRC + doAssert newVersion("1") == newVersion("1.0") + echo("Everything works!") diff --git a/tests/.gitignore b/tests/.gitignore index 8258bb3..42549a0 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,6 +1,10 @@ +tester +/nimble-test +/buildDir /binaryPackage/v1/binaryPackage /binaryPackage/v2/binaryPackage /develop/dependent/src/dependent +/issue27/issue27 /issue206/issue/issue206bin /issue289/issue289 /issue428/nimbleDir/ @@ -13,3 +17,7 @@ /testCommand/testsPass/tests/one /testCommand/testsPass/tests/three /testCommand/testsPass/tests/two +/nimscript/nimscript +/packageStructure/validBinary/y +/testCommand/testsFail/tests/t2 +/passNimFlags/passNimFlags diff --git a/tests/caching/caching.nimble b/tests/caching/caching.nimble new file mode 100644 index 0000000..d3035a9 --- /dev/null +++ b/tests/caching/caching.nimble @@ -0,0 +1,10 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Test package" +license = "BSD" + +# Dependencies + +requires "nim >= 0.12.1" diff --git a/tests/develop/hybrid/hybrid.nimble b/tests/develop/hybrid/hybrid.nimble index d580fad..ae478e1 100644 --- a/tests/develop/hybrid/hybrid.nimble +++ b/tests/develop/hybrid/hybrid.nimble @@ -6,6 +6,7 @@ description = "hybrid" license = "MIT" bin = @["hybrid"] +installExt = @["nim"] # Dependencies diff --git a/tests/invalidPackage/invalidPackage.nimble b/tests/invalidPackage/invalidPackage.nimble new file mode 100644 index 0000000..08fcfcb --- /dev/null +++ b/tests/invalidPackage/invalidPackage.nimble @@ -0,0 +1,13 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" + +thisFieldDoesNotExist = "hello" + +# Dependencies + +requires "nim >= 0.20.0" diff --git a/tests/issue432/issue432.nimble b/tests/issue432/issue432.nimble new file mode 100644 index 0000000..92937c5 --- /dev/null +++ b/tests/issue432/issue432.nimble @@ -0,0 +1,15 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" + + + +# Dependencies + +requires "nim >= 0.16.0" +requires "https://github.com/nimble-test/packagea#head", + "https://github.com/nimble-test/packagebin2" diff --git a/tests/issue432/src/issue432.nim b/tests/issue432/src/issue432.nim new file mode 100644 index 0000000..4b2a270 --- /dev/null +++ b/tests/issue432/src/issue432.nim @@ -0,0 +1,7 @@ +# This is just an example to get you started. A typical library package +# exports the main API in this file. Note that you cannot rename this file +# but you can remove it if you wish. + +proc add*(x, y: int): int = + ## Adds two files together. + return x + y diff --git a/tests/issue564/issue564.nimble b/tests/issue564/issue564.nimble new file mode 100644 index 0000000..7dc0013 --- /dev/null +++ b/tests/issue564/issue564.nimble @@ -0,0 +1,14 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" +bin = @["issue564/issue564build"] + + + +# Dependencies + +requires "nim >= 0.16.0" diff --git a/tests/issue564/src/issue564/issue564build.nim b/tests/issue564/src/issue564/issue564build.nim new file mode 100644 index 0000000..862d40c --- /dev/null +++ b/tests/issue564/src/issue564/issue564build.nim @@ -0,0 +1,5 @@ +# This is just an example to get you started. A typical binary package +# uses this file as the main entry point of the application. + +when isMainModule: + echo("Hello, World!") diff --git a/tests/issue597/dummy.nimble b/tests/issue597/dummy.nimble new file mode 100644 index 0000000..13bf4be --- /dev/null +++ b/tests/issue597/dummy.nimble @@ -0,0 +1,12 @@ +# Package + +version = "0.1.0" +author = "Author" +description = "dummy" +license = "MIT" + +# Dependencies + +requires "nim >= 0.17.0" + +bin = @["test.nim"] diff --git a/tests/issue597/test.nim b/tests/issue597/test.nim new file mode 100644 index 0000000..e69de29 diff --git a/tests/issue633/issue633.nimble b/tests/issue633/issue633.nimble new file mode 100644 index 0000000..cb786eb --- /dev/null +++ b/tests/issue633/issue633.nimble @@ -0,0 +1,16 @@ +# Package + +version = "0.1.0" +author = "GT" +description = "Package for ensuring that issue #633 is resolved." +license = "MIT" + +# Dependencies + +requires "nim >= 0.19.6" +# to reproduce dependency 2 must be before 1 + +task testTask, "Test": + for i in 0 .. paramCount(): + if paramStr(i) == "--testTask": + echo "Got it" diff --git a/tests/issue678/issue678.nimble b/tests/issue678/issue678.nimble new file mode 100644 index 0000000..20239e7 --- /dev/null +++ b/tests/issue678/issue678.nimble @@ -0,0 +1,12 @@ +# Package + +version = "0.1.0" +author = "Ivan Bobev" +description = "Package for ensuring that issue #678 is resolved." +license = "MIT" + +# Dependencies + +requires "nim >= 0.19.6" +# to reproduce dependency 2 must be before 1 +requires "issue678_dependency_2", "issue678_dependency_1" diff --git a/tests/issue678/packages.json b/tests/issue678/packages.json new file mode 100644 index 0000000..972eb70 --- /dev/null +++ b/tests/issue678/packages.json @@ -0,0 +1,19 @@ +[ + { + "name": "issue678_dependency_1", + "url": "https://github.com/nimble-test/issue678?subdir=dependency_1", + "method": "git", + "tags": [ "test" ], + "description": + "Both first and second level dependency of the issue678 package.", + "license": "MIT" + }, + { + "name": "issue678_dependency_2", + "url": "https://github.com/nimble-test/issue678?subdir=dependency_2", + "method": "git", + "tags": [ "test" ], + "description": "First level dependency of the issue678 package.", + "license": "MIT" + } +] diff --git a/tests/issue708/issue708.nimble b/tests/issue708/issue708.nimble new file mode 100644 index 0000000..ebb079d --- /dev/null +++ b/tests/issue708/issue708.nimble @@ -0,0 +1,17 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" + + + +# Dependencies + +requires "nim >= 0.16.0" + + +echo "hello" +echo "hello2" diff --git a/tests/issue708/src/issue708.nim b/tests/issue708/src/issue708.nim new file mode 100644 index 0000000..4b2a270 --- /dev/null +++ b/tests/issue708/src/issue708.nim @@ -0,0 +1,7 @@ +# This is just an example to get you started. A typical library package +# exports the main API in this file. Note that you cannot rename this file +# but you can remove it if you wish. + +proc add*(x, y: int): int = + ## Adds two files together. + return x + y diff --git a/tests/nimbleVersionDefine/nimbleVersionDefine.nimble b/tests/nimbleVersionDefine/nimbleVersionDefine.nimble new file mode 100644 index 0000000..b47b049 --- /dev/null +++ b/tests/nimbleVersionDefine/nimbleVersionDefine.nimble @@ -0,0 +1,14 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" +bin = @["nimbleVersionDefine"] + + + +# Dependencies + +requires "nim >= 0.16.0" diff --git a/tests/nimbleVersionDefine/src/nimbleVersionDefine b/tests/nimbleVersionDefine/src/nimbleVersionDefine new file mode 100755 index 0000000..af3cc22 Binary files /dev/null and b/tests/nimbleVersionDefine/src/nimbleVersionDefine differ diff --git a/tests/nimbleVersionDefine/src/nimbleVersionDefine.nim b/tests/nimbleVersionDefine/src/nimbleVersionDefine.nim new file mode 100644 index 0000000..572dab8 --- /dev/null +++ b/tests/nimbleVersionDefine/src/nimbleVersionDefine.nim @@ -0,0 +1,3 @@ +when isMainModule: + const NimblePkgVersion {.strdefine.} = "Unknown" + echo(NimblePkgVersion) diff --git a/tests/nimscript/nimscript.nimble b/tests/nimscript/nimscript.nimble index d1adfba..39f3710 100644 --- a/tests/nimscript/nimscript.nimble +++ b/tests/nimscript/nimscript.nimble @@ -2,7 +2,9 @@ version = "0.1.0" author = "Dominik Picheta" -description = "Test package" +description = """Test package +with multi-line description +""" license = "BSD" bin = @["nimscript"] @@ -24,10 +26,13 @@ task cr, "Testing `nimble c -r nimscript.nim` via setCommand": task repeated, "Testing `nimble c nimscript.nim` with repeated flags": --define: foo --define: bar + --define: "quoted" + --define: "quoted\\\"with\\\"quotes" setCommand "c", "nimscript.nim" task api, "Testing nimscriptapi module functionality": - echo(getPkgDir()) + doAssert(findExe("nim").len != 0) + echo("PKG_DIR: ", getPkgDir()) before hooks: echo("First") @@ -43,3 +48,15 @@ before hooks2: task hooks2, "Testing the hooks again": echo("Shouldn't happen") + +before install: + echo("Before PkgDir: ", getPkgDir()) + +after install: + echo("After PkgDir: ", getPkgDir()) + +before build: + echo("Before build") + +after build: + echo("After build") \ No newline at end of file diff --git a/tests/packageStructure/softened/myPkg.nim b/tests/packageStructure/softened/myPkg.nim new file mode 100644 index 0000000..e69de29 diff --git a/tests/packageStructure/softened/myPkg.nimble b/tests/packageStructure/softened/myPkg.nimble new file mode 100644 index 0000000..525080d --- /dev/null +++ b/tests/packageStructure/softened/myPkg.nimble @@ -0,0 +1,15 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Correctly structured package myPkg. See #469." +license = "MIT" + +# This is inferred implicitly by Nimble: +# installDirs = @["myPkg"] +# installFiles = @["myPkg.nim"] + +# Dependencies + +requires "nim >= 0.15.0" + diff --git a/tests/packageStructure/softened/myPkg/submodule.nim b/tests/packageStructure/softened/myPkg/submodule.nim new file mode 100644 index 0000000..e69de29 diff --git a/tests/packageStructure/softened/tests/mytest.nim b/tests/packageStructure/softened/tests/mytest.nim new file mode 100644 index 0000000..e69de29 diff --git a/tests/packageStructure/validBinary/y.nim b/tests/packageStructure/validBinary/y.nim new file mode 100644 index 0000000..e69de29 diff --git a/tests/packageStructure/validBinary/y.nimble b/tests/packageStructure/validBinary/y.nimble new file mode 100644 index 0000000..087141b --- /dev/null +++ b/tests/packageStructure/validBinary/y.nimble @@ -0,0 +1,13 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Correctly structured package Y" +license = "MIT" + +bin = @["y"] + +# Dependencies + +requires "nim >= 0.15.0" + diff --git a/tests/packageStructure/validBinary/yWrong/foobar.nim b/tests/packageStructure/validBinary/yWrong/foobar.nim new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/packageStructure/validBinary/yWrong/foobar.nim @@ -0,0 +1 @@ + diff --git a/tests/packageStructure/y/y.nimble b/tests/packageStructure/y/y.nimble index 0ce52f9..4eb3ba5 100644 --- a/tests/packageStructure/y/y.nimble +++ b/tests/packageStructure/y/y.nimble @@ -5,6 +5,7 @@ author = "Dominik Picheta" description = "Incorrectly structured package Y" license = "MIT" +installExt = @["nim"] bin = @["y"] # Dependencies diff --git a/tests/passNimFlags/passNimFlags.nim b/tests/passNimFlags/passNimFlags.nim new file mode 100644 index 0000000..b4c0b97 --- /dev/null +++ b/tests/passNimFlags/passNimFlags.nim @@ -0,0 +1 @@ +when not defined(passNimIsWorking): {.error: "-d:passNimIsWorking wasn't passed to the compiler"} diff --git a/tests/passNimFlags/passNimFlags.nimble b/tests/passNimFlags/passNimFlags.nimble new file mode 100644 index 0000000..8530524 --- /dev/null +++ b/tests/passNimFlags/passNimFlags.nimble @@ -0,0 +1,11 @@ +# Package + +version = "0.1.0" +author = "SolitudeSF" +description = "Test nimble install flag forwarding" +license = "BSD" +bin = @["passNimFlags"] + +# Dependencies + +requires "nim >= 0.13.0" diff --git a/tests/recursive/recursive.nimble b/tests/recursive/recursive.nimble new file mode 100644 index 0000000..d6b155d --- /dev/null +++ b/tests/recursive/recursive.nimble @@ -0,0 +1,25 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "Test package" +license = "BSD" + +# Dependencies + +requires "nim >= 0.12.1" + +let + callNimble = getEnv("NIMBLE_TEST_BINARY_PATH") +doAssert callNimble.len != 0, "NIMBLE_TEST_BINARY_PATH not set" + +task recurse, "Level 1": + echo 1 + exec callNimble & " recurse2" + +task recurse2, "Level 2": + echo 2 + exec callNimble & " recurse3" + +task recurse3, "Level 3": + echo 3 diff --git a/tests/run/run.nimble b/tests/run/run.nimble new file mode 100644 index 0000000..055bc1e --- /dev/null +++ b/tests/run/run.nimble @@ -0,0 +1,14 @@ +# Package + +version = "0.1.0" +author = "Dominik Picheta" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" +bin = @["run"] + + + +# Dependencies + +requires "nim >= 0.19.0" diff --git a/tests/run/src/run.nim b/tests/run/src/run.nim new file mode 100644 index 0000000..af98995 --- /dev/null +++ b/tests/run/src/run.nim @@ -0,0 +1,4 @@ +import os + +when isMainModule: + echo("Testing `nimble run`: ", commandLineParams()) diff --git a/tests/testCommand/testsCWD/testing123.nimble b/tests/testCommand/testsCWD/testing123.nimble new file mode 100644 index 0000000..00e43bf --- /dev/null +++ b/tests/testCommand/testsCWD/testing123.nimble @@ -0,0 +1,4 @@ +version = "0.1.0" +author = "John Doe" +description = "Nimble Test" +license = "BSD" diff --git a/tests/testCommand/testsCWD/tests/tcwd.nim b/tests/testCommand/testsCWD/tests/tcwd.nim new file mode 100644 index 0000000..444a862 --- /dev/null +++ b/tests/testCommand/testsCWD/tests/tcwd.nim @@ -0,0 +1,2 @@ +import os +echo(getCurrentDir()) \ No newline at end of file diff --git a/tests/testCommand/testsFail/tests/a.nim b/tests/testCommand/testsFail/tests/t1.nim similarity index 100% rename from tests/testCommand/testsFail/tests/a.nim rename to tests/testCommand/testsFail/tests/t1.nim diff --git a/tests/testCommand/testsFail/tests/b.nim b/tests/testCommand/testsFail/tests/t2.nim similarity index 100% rename from tests/testCommand/testsFail/tests/b.nim rename to tests/testCommand/testsFail/tests/t2.nim diff --git a/tests/testCommand/testsFail/tests/c.nim b/tests/testCommand/testsFail/tests/t3.nim similarity index 100% rename from tests/testCommand/testsFail/tests/c.nim rename to tests/testCommand/testsFail/tests/t3.nim diff --git a/tests/testCommand/testsIgnore/testing123.nim b/tests/testCommand/testsIgnore/testing123.nim new file mode 100644 index 0000000..2c96a26 --- /dev/null +++ b/tests/testCommand/testsIgnore/testing123.nim @@ -0,0 +1,4 @@ + +proc myFunc*() = + echo "Executing my func" + diff --git a/tests/testCommand/testsIgnore/testing123.nimble b/tests/testCommand/testsIgnore/testing123.nimble new file mode 100644 index 0000000..00e43bf --- /dev/null +++ b/tests/testCommand/testsIgnore/testing123.nimble @@ -0,0 +1,4 @@ +version = "0.1.0" +author = "John Doe" +description = "Nimble Test" +license = "BSD" diff --git a/tests/testCommand/testsIgnore/tests/foobar/tignored.nim b/tests/testCommand/testsIgnore/tests/foobar/tignored.nim new file mode 100644 index 0000000..9d61174 --- /dev/null +++ b/tests/testCommand/testsIgnore/tests/foobar/tignored.nim @@ -0,0 +1 @@ +echo "Should be ignored" diff --git a/tests/testCommand/testsIgnore/tests/ignore.nim b/tests/testCommand/testsIgnore/tests/ignore.nim new file mode 100644 index 0000000..b63fe4d --- /dev/null +++ b/tests/testCommand/testsIgnore/tests/ignore.nim @@ -0,0 +1 @@ +echo "Should be ignored" \ No newline at end of file diff --git a/tests/testCommand/testsIgnore/tests/taccept.nim b/tests/testCommand/testsIgnore/tests/taccept.nim new file mode 100644 index 0000000..e020a08 --- /dev/null +++ b/tests/testCommand/testsIgnore/tests/taccept.nim @@ -0,0 +1,7 @@ +import testing123, unittest + +test "can accept": + echo "First test" + myFunc() + + diff --git a/tests/testCommand/testsPass/tests/one.nim b/tests/testCommand/testsPass/tests/t1.nim similarity index 100% rename from tests/testCommand/testsPass/tests/one.nim rename to tests/testCommand/testsPass/tests/t1.nim diff --git a/tests/testCommand/testsPass/tests/two.nim b/tests/testCommand/testsPass/tests/t2.nim similarity index 100% rename from tests/testCommand/testsPass/tests/two.nim rename to tests/testCommand/testsPass/tests/t2.nim diff --git a/tests/testCommand/testsPass/tests/three.nim b/tests/testCommand/testsPass/tests/t3.nim similarity index 100% rename from tests/testCommand/testsPass/tests/three.nim rename to tests/testCommand/testsPass/tests/t3.nim diff --git a/tests/tester.nim b/tests/tester.nim index f94ec45..446fecb 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -1,6 +1,6 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import osproc, streams, unittest, strutils, os, sequtils, future +import osproc, unittest, strutils, os, sequtils, sugar, strformat # TODO: Each test should start off with a clean slate. Currently installed # packages are shared between each test which causes a multitude of issues @@ -10,6 +10,10 @@ var rootDir = getCurrentDir().parentDir() var nimblePath = rootDir / "src" / addFileExt("nimble", ExeExt) var installDir = rootDir / "tests" / "nimbleDir" const path = "../src/nimble" +const stringNotFound = -1 + +# Set env var to propagate nimble binary path +putEnv("NIMBLE_TEST_BINARY_PATH", nimblePath) # Clear nimble dir. removeDir(installDir) @@ -32,11 +36,23 @@ template cd*(dir: string, body: untyped) = proc execNimble(args: varargs[string]): tuple[output: string, exitCode: int] = var quotedArgs = @args + quotedArgs.insert("--nimbleDir:" & installDir) quotedArgs.insert(nimblePath) - quotedArgs.add("--nimbleDir:" & installDir) - quotedArgs = quoted_args.map((x: string) => ("\"" & x & "\"")) + quotedArgs = quotedArgs.map((x: string) => x.quoteShell) - result = execCmdEx(quotedArgs.join(" ")) + let path {.used.} = getCurrentDir().parentDir() / "src" + + var cmd = + when not defined(windows): + "PATH=" & path & ":$PATH " & quotedArgs.join(" ") + else: + quotedArgs.join(" ") + when defined(macosx): + # TODO: Yeah, this is really specific to my machine but for my own sanity... + cmd = "DYLD_LIBRARY_PATH=/usr/local/opt/openssl@1.1/lib " & cmd + + result = execCmdEx(cmd) + checkpoint(cmd) checkpoint(result.output) proc execNimbleYes(args: varargs[string]): tuple[output: string, exitCode: int]= @@ -49,12 +65,67 @@ template verify(res: (string, int)) = check r[1] == QuitSuccess proc processOutput(output: string): seq[string] = - output.strip.splitLines().filter((x: string) => (x.len > 0)) + output.strip.splitLines().filter( + (x: string) => ( + x.len > 0 and + "Using env var NIM_LIB_PREFIX" notin x + ) + ) proc inLines(lines: seq[string], line: string): bool = for i in lines: if line.normalize in i.normalize: return true +proc hasLineStartingWith(lines: seq[string], prefix: string): bool = + for line in lines: + if line.strip(trailing = false).startsWith(prefix): + return true + return false + +test "issue 708": + cd "issue708": + # TODO: We need a way to filter out compiler messages from the messages + # written by our nimble scripts. + var (output, exitCode) = execNimble("install", "-y", "--verbose") + check exitCode == QuitSuccess + let lines = output.strip.processOutput() + check(inLines(lines, "hello")) + check(inLines(lines, "hello2")) + +test "issue 564": + cd "issue564": + var (_, exitCode) = execNimble("build") + check exitCode == QuitSuccess + +test "depsOnly + flag order test": + var (output, exitCode) = execNimble( + "--depsOnly", "install", "-y", "https://github.com/nimble-test/packagebin2" + ) + check(not output.contains("Success: packagebin2 installed successfully.")) + check exitCode == QuitSuccess + +test "nimscript evaluation error message": + cd "invalidPackage": + var (output, exitCode) = execNimble("check") + let lines = output.strip.processOutput() + check(lines[^2].endsWith("Error: undeclared identifier: 'thisFieldDoesNotExist'")) + check exitCode == QuitFailure + +test "caching of nims and ini detects changes": + cd "caching": + var (output, exitCode) = execNimble("dump") + check output.contains("0.1.0") + let + nfile = "caching.nimble" + writeFile(nfile, readFile(nfile).replace("0.1.0", "0.2.0")) + (output, exitCode) = execNimble("dump") + check output.contains("0.2.0") + writeFile(nfile, readFile(nfile).replace("0.2.0", "0.1.0")) + +test "tasks can be called recursively": + cd "recursive": + check execNimble("recurse").exitCode == QuitSuccess + test "picks #head when looking for packages": cd "versionClashes" / "aporiaScenario": let (output, exitCode) = execNimble("install", "-y", "--verbose") @@ -80,35 +151,38 @@ test "can build with #head and versioned package (#289)": test "can validate package structure (#144)": # Test that no warnings are produced for correctly structured packages. - for package in ["a", "b", "c"]: + for package in ["a", "b", "c", "validBinary", "softened"]: cd "packageStructure/" & package: let (output, exitCode) = execNimble(["install", "-y"]) check exitCode == QuitSuccess - let lines = output.strip.splitLines() - check(not inLines(lines, "warning")) + let lines = output.strip.processOutput() + check(not lines.hasLineStartingWith("Warning:")) # Test that warnings are produced for the incorrectly structured packages. for package in ["x", "y", "z"]: cd "packageStructure/" & package: let (output, exitCode) = execNimble(["install", "-y"]) check exitCode == QuitSuccess - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() checkpoint(output) case package of "x": - check inLines(lines, "Package 'x' has an incorrect structure. It should" & - " contain a single directory hierarchy for source files," & - " named 'x', but file 'foobar.nim' is in a directory named" & - " 'incorrect' instead.") + check lines.hasLineStartingWith( + "Warning: Package 'x' has an incorrect structure. It should" & + " contain a single directory hierarchy for source files," & + " named 'x', but file 'foobar.nim' is in a directory named" & + " 'incorrect' instead.") of "y": - check inLines(lines, "Package 'y' has an incorrect structure. It should" & - " contain a single directory hierarchy for source files," & - " named 'ypkg', but file 'foobar.nim' is in a directory named" & - " 'yWrong' instead.") + check lines.hasLineStartingWith( + "Warning: Package 'y' has an incorrect structure. It should" & + " contain a single directory hierarchy for source files," & + " named 'ypkg', but file 'foobar.nim' is in a directory named" & + " 'yWrong' instead.") of "z": - check inLines(lines, "Package 'z' has an incorrect structure. The top level" & - " of the package source directory should contain at most one module," & - " named 'z.nim', but a file named 'incorrect.nim' was found.") + check lines.hasLineStartingWith( + "Warning: Package 'z' has an incorrect structure. The top level" & + " of the package source directory should contain at most one module," & + " named 'z.nim', but a file named 'incorrect.nim' was found.") else: assert false @@ -132,7 +206,7 @@ test "issue 113 (uninstallation problems)": # Try to remove c. let (output, exitCode) = execNimble(["remove", "-y", "c"]) - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check exitCode != QuitSuccess check inLines(lines, "cannot uninstall c (0.1.0) because b (0.1.0) depends on it") @@ -186,7 +260,7 @@ test "can refresh with custom urls": let (output, exitCode) = execNimble(["refresh", "--verbose"]) checkpoint(output) - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check exitCode == QuitSuccess check inLines(lines, "config file at") check inLines(lines, "official package list") @@ -201,9 +275,9 @@ test "can refresh with local package list": [PackageList] name = "local" path = "$1" - """.unindent % (getCurrentDir() / "issue368" / "packages.json")) + """.unindent % (getCurrentDir() / "issue368" / "packages.json").replace("\\", "\\\\")) let (output, exitCode) = execNimble(["refresh", "--verbose"]) - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check inLines(lines, "config file at") check inLines(lines, "Copying") check inLines(lines, "Package list copied.") @@ -216,7 +290,7 @@ test "package list source required": name = "local" """) let (output, exitCode) = execNimble(["refresh", "--verbose"]) - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check inLines(lines, "config file at") check inLines(lines, "Package list 'local' requires either url or path") check exitCode == QuitFailure @@ -230,69 +304,97 @@ test "package list can only have one source": url = "http://nim-lang.org/nimble/packages.json" """) let (output, exitCode) = execNimble(["refresh", "--verbose"]) - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check inLines(lines, "config file at") check inLines(lines, "Attempted to specify `url` and `path` for the same package list 'local'") check exitCode == QuitFailure -test "can install nimscript package": - cd "nimscript": - check execNimble(["install", "-y"]).exitCode == QuitSuccess +suite "nimscript": + test "can install nimscript package": + cd "nimscript": + check execNimble(["install", "-y"]).exitCode == QuitSuccess -test "can execute nimscript tasks": - cd "nimscript": - let (output, exitCode) = execNimble("--verbose", "work") - let lines = output.strip.splitLines() - check exitCode == QuitSuccess - check lines[^1] == "10" + test "before/after install pkg dirs are correct": + cd "nimscript": + let (output, exitCode) = execNimble(["install", "-y"]) + check exitCode == QuitSuccess + check output.contains("Before build") + check output.contains("After build") + let lines = output.strip.processOutput() + check lines[0].startsWith("Before PkgDir:") + check lines[0].endsWith("tests" / "nimscript") + check lines[^1].startsWith("After PkgDir:") + check lines[^1].endsWith("tests" / "nimbleDir" / "pkgs" / "nimscript-0.1.0") -test "can use nimscript's setCommand": - cd "nimscript": - let (output, exitCode) = execNimble("--verbose", "cTest") - let lines = output.strip.splitLines() - check exitCode == QuitSuccess - check "Execution finished".normalize in lines[^1].normalize + test "before/after on build": + cd "nimscript": + let (output, exitCode) = execNimble(["build"]) + check exitCode == QuitSuccess + check output.contains("Before build") + check output.contains("After build") -test "can use nimscript's setCommand with flags": - cd "nimscript": - let (output, exitCode) = execNimble("--debug", "cr") - let lines = output.strip.splitLines() - check exitCode == QuitSuccess - check inLines(lines, "Hello World") + test "can execute nimscript tasks": + cd "nimscript": + let (output, exitCode) = execNimble("--verbose", "work") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check lines[^1] == "10" -test "can use nimscript with repeated flags (issue #329)": - cd "nimscript": - let (output, exitCode) = execNimble("--debug", "repeated") - let lines = output.strip.splitLines() - check exitCode == QuitSuccess - var found = false - for line in lines: - if line.contains("--define:foo"): - found = true - check found == true + test "can use nimscript's setCommand": + cd "nimscript": + let (output, exitCode) = execNimble("--verbose", "cTest") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check "Execution finished".normalize in lines[^1].normalize -test "can list nimscript tasks": - cd "nimscript": - let (output, exitCode) = execNimble("tasks") - check "work test description".normalize in output.normalize - check exitCode == QuitSuccess + test "can use nimscript's setCommand with flags": + cd "nimscript": + let (output, exitCode) = execNimble("--debug", "cr") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "Hello World") -test "can use pre/post hooks": - cd "nimscript": - let (output, exitCode) = execNimble("hooks") - let lines = output.strip.splitLines() - check exitCode == QuitSuccess - check inLines(lines, "First") - check inLines(lines, "middle") - check inLines(lines, "last") + test "can use nimscript with repeated flags (issue #329)": + cd "nimscript": + let (output, exitCode) = execNimble("--debug", "repeated") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + var found = false + for line in lines: + if line.contains("--define:foo"): + found = true + check found == true -test "pre hook can prevent action": - cd "nimscript": - let (output, exitCode) = execNimble("hooks2") - let lines = output.strip.splitLines() - check exitCode == QuitSuccess - check(not inLines(lines, "Shouldn't happen")) - check inLines(lines, "Hook prevented further execution") + test "can list nimscript tasks": + cd "nimscript": + let (output, exitCode) = execNimble("tasks") + check "work".normalize in output.normalize + check "test description".normalize in output.normalize + check exitCode == QuitSuccess + + test "can use pre/post hooks": + cd "nimscript": + let (output, exitCode) = execNimble("hooks") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "First") + check inLines(lines, "middle") + check inLines(lines, "last") + + test "pre hook can prevent action": + cd "nimscript": + let (output, exitCode) = execNimble("hooks2") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check(not inLines(lines, "Shouldn't happen")) + check inLines(lines, "Hook prevented further execution") + + test "nimble script api": + cd "nimscript": + let (output, exitCode) = execNimble("api") + let lines = output.strip.processOutput() + check exitCode == QuitSuccess + check inLines(lines, "PKG_DIR: " & getCurrentDir()) test "can install packagebin2": let args = ["install", "-y", "https://github.com/nimble-test/packagebin2.git"] @@ -303,7 +405,7 @@ test "can reject same version dependencies": "install", "-y", "https://github.com/nimble-test/packagebin.git") # We look at the error output here to avoid out-of-order problems caused by # stderr output being generated and flushed without first flushing stdout - let ls = outp.strip.splitLines() + let ls = outp.strip.processOutput() check exitCode != QuitSuccess check "Cannot satisfy the dependency on PackageA 0.2.0 and PackageA 0.5.0" in ls[ls.len-1] @@ -326,20 +428,20 @@ test "issue #27": test "issue #126": cd "issue126/a": let (output, exitCode) = execNimble("install", "-y") - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check exitCode != QuitSuccess # TODO check inLines(lines, "issue-126 is an invalid package name: cannot contain '-'") cd "issue126/b": let (output1, exitCode1) = execNimble("install", "-y") - let lines1 = output1.strip.splitLines() + let lines1 = output1.strip.processOutput() check exitCode1 != QuitSuccess check inLines(lines1, "The .nimble file name must match name specified inside") test "issue #108": cd "issue108": let (output, exitCode) = execNimble("build") - let lines = output.strip.splitLines() + let lines = output.strip.processOutput() check exitCode != QuitSuccess check inLines(lines, "Nothing to build") @@ -382,13 +484,16 @@ test "issue #349": proc checkName(name: string) = let (outp, code) = execNimble("init", "-y", name) - let msg = outp.strip.splitLines() + let msg = outp.strip.processOutput() check code == QuitFailure check inLines(msg, "\"$1\" is an invalid package name: reserved name" % name) - removeFile(name.changeFileExt("nimble")) - removeDir("src") - removeDir("tests") + try: + removeFile(name.changeFileExt("nimble")) + removeDir("src") + removeDir("tests") + except OSError: + discard for reserved in reservedNames: checkName(reserved.toUpperAscii()) @@ -410,10 +515,9 @@ test "can uninstall": block: let (outp, exitCode) = execNimble("uninstall", "-y", "issue27b") - let ls = outp.strip.splitLines() + let ls = outp.strip.processOutput() check exitCode != QuitSuccess - check "Cannot uninstall issue27b (0.1.0) because issue27a (0.1.0) depends" & - " on it" in ls[ls.len-1] + check inLines(ls, "Cannot uninstall issue27b (0.1.0) because issue27a (0.1.0) depends") check execNimble("uninstall", "-y", "issue27").exitCode == QuitSuccess check execNimble("uninstall", "-y", "issue27a").exitCode == QuitSuccess @@ -484,6 +588,9 @@ test "can install diamond deps (#184)": checkpoint(output) check exitCode == 0 +test "issues #280 and #524": + check execNimble("install", "-y", "https://github.com/nimble-test/issue280and524.git").exitCode == 0 + suite "can handle two binary versions": setup: cd "binaryPackage/v1": @@ -492,9 +599,15 @@ suite "can handle two binary versions": cd "binaryPackage/v2": check execNimble("install", "-y").exitCode == QuitSuccess + var + cmd = installDir / "bin" / "binaryPackage" + + when defined(windows): + cmd = "cmd /c " & cmd & ".cmd" + test "can execute v2": let (output, exitCode) = - execCmdEx(installDir / "bin" / "binaryPackage".addFileExt(ExeExt)) + execCmdEx(cmd) check exitCode == QuitSuccess check output.strip() == "v2" @@ -502,7 +615,7 @@ suite "can handle two binary versions": check execNimble("remove", "binaryPackage@2.0", "-y").exitCode==QuitSuccess let (output, exitCode) = - execCmdEx(installDir / "bin" / "binaryPackage".addFileExt(ExeExt)) + execCmdEx(cmd) check exitCode == QuitSuccess check output.strip() == "v1" @@ -510,7 +623,7 @@ suite "can handle two binary versions": check execNimble("remove", "binaryPackage@1.0", "-y").exitCode==QuitSuccess let (output, exitCode) = - execCmdEx(installDir / "bin" / "binaryPackage".addFileExt(ExeExt)) + execCmdEx(cmd) check exitCode == QuitSuccess check output.strip() == "v2" @@ -523,6 +636,12 @@ test "can pass args with spaces to Nim (#351)": checkpoint output check exitCode == QuitSuccess +test "error if `bin` is a source file (#597)": + cd "issue597": + var (output, exitCode) = execNimble("build") + check exitCode != QuitSuccess + check output.contains("entry should not be a source file: test.nim") + suite "reverse dependencies": test "basic test": cd "revdep/mydep": @@ -534,6 +653,27 @@ suite "reverse dependencies": verify execNimbleYes("remove", "pkgA") verify execNimbleYes("remove", "mydep") + test "revdep fail test": + cd "revdep/mydep": + verify execNimbleYes("install") + + cd "revdep/pkgWithDep": + verify execNimbleYes("install") + + let (output, exitCode) = execNimble("uninstall", "mydep") + checkpoint output + check output.processOutput.inLines("cannot uninstall mydep") + check exitCode == QuitFailure + + test "revdep -i test": + cd "revdep/mydep": + verify execNimbleYes("install") + + cd "revdep/pkgWithDep": + verify execNimbleYes("install") + + verify execNimbleYes("remove", "mydep", "-i") + test "issue #373": cd "revdep/mydep": verify execNimbleYes("install") @@ -563,10 +703,10 @@ suite "develop feature": let path = installDir / "pkgs" / "hybrid-#head" / "hybrid.nimble-link" check fileExists(path) - let split = readFile(path).splitLines() + let split = readFile(path).processOutput() check split.len == 2 - check split[0].endsWith("develop/hybrid/hybrid.nimble") - check split[1].endsWith("develop/hybrid") + check split[0].endsWith("develop" / "hybrid" / "hybrid.nimble") + check split[1].endsWith("develop" / "hybrid") test "can develop with srcDir": cd "develop/srcdirtest": @@ -578,13 +718,13 @@ suite "develop feature": let path = installDir / "pkgs" / "srcdirtest-#head" / "srcdirtest.nimble-link" check fileExists(path) - let split = readFile(path).splitLines() + let split = readFile(path).processOutput() check split.len == 2 - check split[0].endsWith("develop/srcdirtest/srcdirtest.nimble") - check split[1].endsWith("develop/srcdirtest/src") + check split[0].endsWith("develop" / "srcdirtest" / "srcdirtest.nimble") + check split[1].endsWith("develop" / "srcdirtest" / "src") cd "develop/dependent": - let (output, exitCode) = execNimble("c", "-r", "src/dependent.nim") + let (output, exitCode) = execNimble("c", "-r", "src" / "dependent.nim") checkpoint output check(output.processOutput.inLines("hello")) check exitCode == QuitSuccess @@ -614,10 +754,20 @@ suite "develop feature": check exitCode == QuitSuccess (output, exitCode) = execNimble("path", "srcdirtest") + checkpoint output check exitCode == QuitSuccess check output.strip() == getCurrentDir() / "src" +suite "path command": + test "can get correct path for srcDir (#531)": + check execNimble("uninstall", "srcdirtest", "-y").exitCode == QuitSuccess + cd "develop/srcdirtest": + let (_, exitCode) = execNimble("install", "-y") + check exitCode == QuitSuccess + let (output, _) = execNimble("path", "srcdirtest") + check output.strip() == installDir / "pkgs" / "srcdirtest-1.0" + suite "test command": test "Runs passing unit tests": cd "testCommand/testsPass": @@ -642,6 +792,19 @@ suite "test command": check exitCode == QuitSuccess check outp.processOutput.inLines("overriden") + test "certain files are ignored": + cd "testCommand/testsIgnore": + let (outp, exitCode) = execNimble("test") + check exitCode == QuitSuccess + check(not outp.processOutput.inLines("Should be ignored")) + check outp.processOutput.inLines("First test") + + test "CWD is root of package": + cd "testCommand/testsCWD": + let (outp, exitCode) = execNimble("test") + check exitCode == QuitSuccess + check outp.processOutput.inLines(getCurrentDir()) + suite "check command": test "can succeed package": cd "binaryPackage/v1": @@ -684,4 +847,199 @@ suite "multi": test "can develop package from git subdir": removeDir("nimble-test/multi") let args = ["develop", "-y", "https://github.com/nimble-test/multi?subdir=beta"] - check execNimble(args).exitCode == QuitSuccess \ No newline at end of file + check execNimble(args).exitCode == QuitSuccess + +suite "Module tests": + test "version": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/version").exitCode == QuitSuccess + + test "reversedeps": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/reversedeps").exitCode == QuitSuccess + + test "packageparser": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/packageparser").exitCode == QuitSuccess + + test "packageinfo": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/packageinfo").exitCode == QuitSuccess + + test "cli": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/cli").exitCode == QuitSuccess + + test "download": + cd "..": + check execCmdEx("nim c -r src/nimblepkg/download").exitCode == QuitSuccess + +test "init does not overwrite existing files (#581)": + createDir("issue581/src") + cd "issue581": + const Src = "echo \"OK\"" + writeFile("src/issue581.nim", Src) + check execNimbleYes("init").exitCode == QuitSuccess + check readFile("src/issue581.nim") == Src + removeDir("issue581") + +test "remove skips packages with revDeps (#504)": + check execNimble("install", "nimboost@0.5.5", "nimfp@0.4.4", "-y").exitCode == QuitSuccess + + var (output, exitCode) = execNimble("uninstall", "nimboost", "nimfp", "-n") + var lines = output.strip.processOutput() + check inLines(lines, "Cannot uninstall nimboost") + + (output, exitCode) = execNimble("uninstall", "nimfp", "nimboost", "-y") + lines = output.strip.processOutput() + check (not inLines(lines, "Cannot uninstall nimboost")) + + check execNimble("path", "nimboost").exitCode != QuitSuccess + check execNimble("path", "nimfp").exitCode != QuitSuccess + +test "pass options to the compiler with `nimble install`": + cd "passNimFlags": + check execNimble("install", "--passNim:-d:passNimIsWorking").exitCode == QuitSuccess + +test "do not install single dependency multiple times (#678)": + # for the test to be correct, the tested package and its dependencies must not + # exist in the local cache + removeDir("nimbleDir") + cd "issue678": + testRefresh(): + writeFile(configFile, """ + [PackageList] + name = "local" + path = "$1" + """.unindent % (getCurrentDir() / "packages.json").replace("\\", "\\\\")) + check execNimble(["refresh"]).exitCode == QuitSuccess + let (output, exitCode) = execNimble("install", "-y") + check exitCode == QuitSuccess + let index = output.find("issue678_dependency_1@0.1.0 already exists") + check index == stringNotFound + +test "Passing command line arguments to a task (#633)": + cd "issue633": + var (output, exitCode) = execNimble("testTask --testTask") + check exitCode == QuitSuccess + check output.contains("Got it") + +suite "nimble run": + test "Invalid binary": + cd "run": + var (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "blahblah", # The command to run + ) + check exitCode == QuitFailure + check output.contains("Binary '$1' is not defined in 'run' package." % + "blahblah".changeFileExt(ExeExt)) + + test "Parameters passed to executable": + cd "run": + var (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "run", # The command to run + "--debug", # First argument passed to the executed command + "check" # Second argument passed to the executed command. + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --debug check" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--debug", "check"]""") + + test "Parameters not passed to single executable": + cd "run": + var (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "--debug" # First argument passed to the executed command + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --debug" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--debug"]""") + + test "Parameters passed to single executable": + cd "run": + var (output, exitCode) = execNimble( + "--debug", # Flag to enable debug verbosity in Nimble + "run", # Run command invokation + "--", # Flag to set run file to "" before next argument + "--debug", # First argument passed to the executed command + "check" # Second argument passed to the executed command. + ) + check exitCode == QuitSuccess + check output.contains("tests$1run$1$2 --debug check" % + [$DirSep, "run".changeFileExt(ExeExt)]) + check output.contains("""Testing `nimble run`: @["--debug", "check"]""") + + test "Executable output is shown even when not debugging": + cd "run": + var (output, exitCode) = + execNimble("run", "run", "--option1", "arg1") + check exitCode == QuitSuccess + check output.contains("""Testing `nimble run`: @["--option1", "arg1"]""") + + test "Quotes and whitespace are well handled": + cd "run": + var (output, exitCode) = execNimble( + "run", "run", "\"", "\'", "\t", "arg with spaces" + ) + check exitCode == QuitSuccess + check output.contains( + """Testing `nimble run`: @["\"", "\'", "\t", "arg with spaces"]""" + ) + + +test "NimbleVersion is defined": + cd "nimbleVersionDefine": + var (output, exitCode) = execNimble("c", "-r", "src/nimbleVersionDefine.nim") + check output.contains("0.1.0") + check exitCode == QuitSuccess + + var (output2, exitCode2) = execNimble("run", "nimbleVersionDefine") + check output2.contains("0.1.0") + check exitCode2 == QuitSuccess + +test "issue 432": + cd "issue432": + check execNimble("install", "-y", "--depsOnly").exitCode == QuitSuccess + check execNimble("install", "-y", "--depsOnly").exitCode == QuitSuccess + +test "compilation without warnings": + const buildDir = "./buildDir/" + const filesToBuild = [ + "../src/nimble.nim", + "../src/nimblepkg/nimscriptapi.nim", + "./tester.nim", + ] + + proc execBuild(fileName: string): tuple[output: string, exitCode: int] = + result = execCmdEx( + fmt"nim c -o:{buildDir/fileName.splitFile.name} {fileName}") + + proc checkOutput(output: string): uint = + const warningsToCheck = [ + "[UnusedImport]", + "[Deprecated]", + "[XDeclaredButNotUsed]", + ] + + for line in output.splitLines(): + for warning in warningsToCheck: + if line.find(warning) != stringNotFound: + once: checkpoint("Detected warnings:") + checkpoint(line) + inc(result) + + removeDir(buildDir) + + var linesWithWarningsCount: uint = 0 + for file in filesToBuild: + let (output, exitCode) = execBuild(file) + check exitCode == QuitSuccess + linesWithWarningsCount += checkOutput(output) + check linesWithWarningsCount == 0