From ca99ad7d21b1a735ccc100cf2d202668e2b236ba Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 28 Dec 2015 16:33:34 +0000 Subject: [PATCH] Moved Nimble-specific Nimscript definitions to nimscriptapi module. * Moved package info reading procedures to a packageparser module. This is to prevent recu rsive dependencies between nimscriptsupport and packageinfo modules. * Passed the Options object to all procedures which read package info (necessary for nimscriptsupport module, to find the path of the nimscriptapi module) * Introduced an ``isInstalled`` field to the ``PackageInfo`` type. This is a bug fix for g etRealDir: it no longer uses the ``src`` field in the path it returns for installed packag es' PackageInfo objects. * Improved error reporting from NimScript evaluator. * Renamed compiler/options import to ``compiler_options`` in ``nimscriptsupport`` module. * Backward compatibility for .babel packages in getNameVersion proc. * Introduced a getInstalledPkgsMin procedure which does not read package info, but only pr ovides minimal info instead. --- src/nimble.nim | 40 +++-- src/nimblepkg/download.nim | 6 +- src/nimblepkg/nimbletypes.nim | 1 + src/nimblepkg/nimscriptapi.nim | 30 ++++ src/nimblepkg/nimscriptsupport.nim | 134 ++++++++++----- src/nimblepkg/options.nim | 2 +- src/nimblepkg/packageinfo.nim | 254 ++--------------------------- src/nimblepkg/packageparser.nim | 254 +++++++++++++++++++++++++++++ tests/nimscript/nimscript.nimble | 5 +- 9 files changed, 426 insertions(+), 300 deletions(-) create mode 100644 src/nimblepkg/nimscriptapi.nim create mode 100644 src/nimblepkg/packageparser.nim diff --git a/src/nimble.nim b/src/nimble.nim index d813b20..ea626ba 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -8,7 +8,7 @@ from sequtils import toSeq import nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, nimblepkg/download, nimblepkg/config, nimblepkg/nimbletypes, - nimblepkg/publish, nimblepkg/options + nimblepkg/publish, nimblepkg/options, nimblepkg/packageparser import nimblepkg/nimscriptsupport @@ -227,7 +227,7 @@ proc processDeps(pkginfo: PackageInfo, options: Options): seq[string] = ## ## Returns the list of paths to pass to the compiler during build phase. result = @[] - let pkglist = getInstalledPkgs(options.getPkgsDir()) + let pkglist = getInstalledPkgs(options.getPkgsDir(), options) var reverseDeps: seq[tuple[name, version: string]] = @[] for dep in pkginfo.requires: if dep.name == "nimrod" or dep.name == "nim": @@ -328,7 +328,7 @@ proc installFromDir(dir: string, latest: bool, 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. - var pkgInfo = getPkgInfo(dir) + var pkgInfo = getPkgInfo(dir, options) let realDir = pkgInfo.getRealDir() let binDir = options.getBinDir() let pkgsDir = options.getPkgsDir() @@ -440,7 +440,8 @@ proc getNimbleTempDir(): string = result.add($getpid()) proc downloadPkg(url: string, verRange: VersionRange, - downMethod: DownloadMethod): (string, VersionRange) = + downMethod: DownloadMethod, + options: Options): (string, VersionRange) = ## Downloads the repository as specified by ``url`` and ``verRange`` using ## the download method specified. ## @@ -449,7 +450,10 @@ proc downloadPkg(url: string, verRange: VersionRange, let downloadDir = (getNimbleTempDir() / getDownloadDirName(url, verRange)) createDir(downloadDir) echo("Downloading ", url, " into ", downloadDir, " using ", downMethod, "...") - result = (downloadDir, doDownload(url, downloadDir, verRange, downMethod)) + result = ( + downloadDir, + doDownload(url, downloadDir, verRange, downMethod, options) + ) proc getDownloadInfo*(pv: PkgTuple, options: Options, doPrompt: bool): (DownloadMethod, string) = @@ -488,7 +492,8 @@ proc install(packages: seq[PkgTuple], # Install each package. for pv in packages: let (meth, url) = getDownloadInfo(pv, options, doPrompt) - let (downloadDir, downloadVersion) = downloadPkg(url, pv.ver, meth) + let (downloadDir, downloadVersion) = + downloadPkg(url, pv.ver, meth, options) try: result = installFromDir(downloadDir, false, options, url) except BuildFailed: @@ -512,13 +517,13 @@ proc install(packages: seq[PkgTuple], raise proc build(options: Options) = - var pkgInfo = getPkgInfo(getCurrentDir()) + var pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) let paths = processDeps(pkginfo, options) buildFromDir(pkgInfo, paths, false) proc compile(options: Options) = - var pkgInfo = getPkgInfo(getCurrentDir()) + var pkgInfo = getPkgInfo(getCurrentDir(), options) nimScriptHint(pkgInfo) let paths = processDeps(pkginfo, options) let realDir = pkgInfo.getRealDir() @@ -587,7 +592,7 @@ proc list(options: Options) = proc listInstalled(options: Options) = var h = initTable[string, seq[string]]() - let pkgs = getInstalledPkgs(options.getPkgsDir()) + let pkgs = getInstalledPkgs(options.getPkgsDir(), options) for x in pkgs.items(): let pName = x.pkginfo.name @@ -627,7 +632,7 @@ proc listPaths(options: Options) = hasSpec = nimScriptFile.existsFile or nimbleFile.existsFile or babelFile.existsFile if hasSpec: - var pkgInfo = getPkgInfo(path) + var pkgInfo = getPkgInfo(path, options) var v: VersionAndPath v.version = newVersion(pkgInfo.version) v.path = options.getPkgsDir / (pkgInfo.name & '-' & pkgInfo.version) @@ -654,8 +659,8 @@ proc join(x: seq[PkgTuple]; y: string): string = proc dump(options: Options) = let proj = addFileExt(options.action.projName, "nimble") - let p = if fileExists(proj): readPackageInfo(proj) - else: getPkgInfo(os.getCurrentDir()) + let p = if fileExists(proj): readPackageInfo(proj, options) + else: getPkgInfo(os.getCurrentDir(), options) echo "name: ", p.name.escape echo "version: ", p.version.escape echo "author: ", p.author.escape @@ -755,7 +760,7 @@ proc uninstall(options: Options) = # Do some verification. for pkgTup in options.action.packages: echo("Looking for ", pkgTup.name, " (", $pkgTup.ver, ")...") - let installedPkgs = getInstalledPkgs(options.getPkgsDir()) + let installedPkgs = getInstalledPkgs(options.getPkgsDir(), options) var pkgList = findAllPkgs(installedPkgs, pkgTup) if pkgList.len == 0: raise newException(NimbleError, "Package not found") @@ -805,7 +810,7 @@ proc uninstall(options: Options) = proc listTasks(options: Options) = let nimbleFile = findNimbleFile(getCurrentDir(), true) - nimscriptsupport.listTasks(nimbleFile) + nimscriptsupport.listTasks(nimbleFile, options) proc doAction(options: Options) = if not existsDir(options.getNimbleDir()): @@ -836,7 +841,7 @@ proc doAction(options: Options) = of actionInit: init(options) of actionPublish: - var pkgInfo = getPkgInfo(getCurrentDir()) + var pkgInfo = getPkgInfo(getCurrentDir(), options) publish(pkgInfo) of actionDump: dump(options) @@ -849,10 +854,11 @@ proc doAction(options: Options) = of actionCustom: # Custom command. Attempt to call a NimScript task. let nimbleFile = findNimbleFile(getCurrentDir(), true) - if not nimbleFile.isNimScript(): + ## TODO: Optimise this, there are two calls to readPackageInfo here. + if not nimbleFile.isNimScript(options): writeHelp() - let execResult = execTask(nimbleFile, options.action.command) + let execResult = execTask(nimbleFile, options.action.command, options) if not execResult.success: echo("FAILURE: Could not find task ", options.action.command, " in ", nimbleFile) diff --git a/src/nimblepkg/download.nim b/src/nimblepkg/download.nim index a7ebd34..1310886 100644 --- a/src/nimblepkg/download.nim +++ b/src/nimblepkg/download.nim @@ -3,7 +3,7 @@ import parseutils, os, osproc, strutils, tables, pegs -import packageinfo, version, tools, nimbletypes +import packageinfo, packageparser, version, tools, nimbletypes, options type DownloadMethod* {.pure.} = enum @@ -136,7 +136,7 @@ proc isURL*(name: string): bool = name.startsWith(peg" @'://' ") proc doDownload*(url: string, downloadDir: string, verRange: VersionRange, - downMethod: DownloadMethod): VersionRange = + downMethod: DownloadMethod, options: Options): VersionRange = ## Downloads the repository specified by ``url`` using the specified download ## method. ## @@ -161,7 +161,7 @@ proc doDownload*(url: string, downloadDir: string, verRange: VersionRange, proc verifyClone() = ## Makes sure that the downloaded package's version satisfies the requested ## version range. - let pkginfo = getPkgInfo(downloadDir) + let pkginfo = getPkgInfo(downloadDir, options) if pkginfo.version.newVersion notin verRange: raise newException(NimbleError, "Downloaded package's version does not satisfy requested version " & diff --git a/src/nimblepkg/nimbletypes.nim b/src/nimblepkg/nimbletypes.nim index f5530b9..aa6d35b 100644 --- a/src/nimblepkg/nimbletypes.nim +++ b/src/nimblepkg/nimbletypes.nim @@ -13,6 +13,7 @@ type mypath*: string ## The path of this .nimble file isNimScript*: bool ## Determines if this pkg info was read from a nims file isMinimal*: bool + isInstalled*: bool ## Determines if the pkg this info belongs to is installed name*: string version*: string author*: string diff --git a/src/nimblepkg/nimscriptapi.nim b/src/nimblepkg/nimscriptapi.nim new file mode 100644 index 0000000..c996e1a --- /dev/null +++ b/src/nimblepkg/nimscriptapi.nim @@ -0,0 +1,30 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. + +## This module is implicitly imported in NimScript .nimble files. + +var + packageName* = "" ## Set this to the package name. It + ## is usually not required to do that, nims' filename is + ## the default. + version*: string ## The package's version. + 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. + binDir*: string ## The package's binary directory. + backend*: string ## The package's backend. + + skipDirs*, skipFiles*, skipExt*, installDirs*, installFiles*, + installExt*, bin*: seq[string] = @[] ## Nimble metadata. + requiresData*: seq[string] = @[] ## The package's dependencies. + +proc requires*(deps: varargs[string]) = + ## Call this to set the list of requirements of your Nimble + ## package. + for d in deps: requiresData.add(d) + +template builtin = discard + +proc getPkgDir*(): string = + builtin \ No newline at end of file diff --git a/src/nimblepkg/nimscriptsupport.nim b/src/nimblepkg/nimscriptsupport.nim index 1c78f7d..850cbb1 100644 --- a/src/nimblepkg/nimscriptsupport.nim +++ b/src/nimblepkg/nimscriptsupport.nim @@ -6,15 +6,16 @@ import compiler/ast, compiler/modules, compiler/passes, compiler/passaux, - compiler/condsyms, compiler/options, compiler/sem, compiler/semdata, + compiler/condsyms, compiler/sem, compiler/semdata, compiler/llstream, compiler/vm, compiler/vmdef, compiler/commands, compiler/msgs, compiler/magicsys, compiler/lists from compiler/scriptconfig import setupVM from compiler/idents import getIdent from compiler/astalgo import strTableGet +import compiler/options as compiler_options -import nimbletypes, version +import nimbletypes, version, options, packageinfo import os, strutils, strtabs, times, osproc type @@ -33,27 +34,27 @@ proc raiseVariableError(ident, typ: string) {.noinline.} = proc isStrLit(n: PNode): bool = n.kind in {nkStrLit..nkTripleStrLit} -proc getGlobal(ident: string): string = - let n = vm.globalCtx.getGlobalValue(getSysSym ident) +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, "string") + raiseVariableError(ident.name.s, "string") -proc getGlobalAsSeq(ident: string): seq[string] = - let n = vm.globalCtx.getGlobalValue(getSysSym ident) +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, "seq[string]") + raiseVariableError(ident.name.s, "seq[string]") else: - raiseVariableError(ident, "seq[string]") + raiseVariableError(ident.name.s, "seq[string]") -proc extractRequires(result: var seq[PkgTuple]) = - let n = vm.globalCtx.getGlobalValue(getSysSym "requiresData") +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: @@ -140,13 +141,13 @@ proc setupVM(module: PSym; scriptName: string, cbconf thisDir: setResult(a, vthisDir) cbconf put: - options.setConfigVar(getString(a, 0), getString(a, 1)) + compiler_options.setConfigVar(getString(a, 0), getString(a, 1)) cbconf get: - setResult(a, options.getConfigVar(a.getString 0)) + setResult(a, compiler_options.getConfigVar(a.getString 0)) cbconf exists: - setResult(a, options.existsConfigVar(a.getString 0)) + setResult(a, compiler_options.existsConfigVar(a.getString 0)) cbconf nimcacheDir: - setResult(a, options.getNimcacheDir()) + setResult(a, compiler_options.getNimcacheDir()) cbconf paramStr: setResult(a, os.paramStr(int a.getInt 0)) cbconf paramCount: @@ -156,7 +157,7 @@ proc setupVM(module: PSym; scriptName: string, cbconf cmpIgnoreCase: setResult(a, strutils.cmpIgnoreCase(a.getString 0, a.getString 1)) cbconf setCommand: - options.command = a.getString 0 + compiler_options.command = a.getString 0 let arg = a.getString 1 if arg.len > 0: gProjectName = arg @@ -165,15 +166,40 @@ proc setupVM(module: PSym; scriptName: string, except OSError: gProjectFull = gProjectName cbconf getCommand: - setResult(a, options.command) + setResult(a, compiler_options.command) cbconf switch: if not flags.isNil: flags[a.getString 0] = a.getString 1 -proc execScript(scriptName: string, flags: StringTableRef) = +proc findNimscriptApi(options: Options): string = + ## Returns the directory containing ``nimscriptapi.nim`` + var inPath = false + let pkgs = getInstalledPkgsMin(options.getPkgsDir(), options) + var pkg: PackageInfo + if pkgs.findPkg(("nimble", newVRAny()), pkg): + let pkgDir = pkg.getRealDir() + if fileExists(pkgDir / "nimblepkg" / "nimscriptapi.nim"): + result = pkgDir + inPath = true + if not inPath: + # Try finding it in exe's path + if fileExists(getAppDir() / "nimblepkg" / "nimscriptapi.nim"): + result = getAppDir() + inPath = true + if not inPath: + raise newException(NimbleError, "Cannot find nimscriptapi.nim") + +proc execScript(scriptName: string, flags: StringTableRef, options: Options) = ## Executes the specified script. ## ## No clean up is performed and must be done manually! + if "nimblepkg/nimscriptapi" notin compiler_options.implicitIncludes: + compiler_options.implicitIncludes.add("nimblepkg/nimscriptapi") + + # Ensure that "nimblepkg/nimscriptapi" is in the PATH. + let nimscriptApiPath = findNimscriptApi(options) + appendStr(searchPaths, nimscriptApiPath) + setDefaultLibpath() passes.gIncludeFile = includeModule passes.gImportModule = importModule @@ -185,19 +211,28 @@ proc execScript(scriptName: string, flags: StringTableRef) = registerPass(semPass) registerPass(evalPass) - appendStr(searchPaths, options.libpath) + appendStr(searchPaths, compiler_options.libpath) var m = makeModule(scriptName) incl(m.flags, sfMainModule) vm.globalCtx = setupVM(m, scriptName, flags) + # Setup builtins defined in nimscriptapi.nim + template cbApi(name, body) {.dirty.} = + vm.globalCtx.registerCallback "stdlib.system." & astToStr(name), + proc (a: VmArgs) = + body + # TODO: This doesn't work. + cbApi getPkgDir: + setResult(a, "FOOBAR") + compileSystemModule() processModule(m, llStreamOpen(scriptName, fmRead), nil) proc cleanup() = # ensure everything can be called again: - options.gProjectName = "" - options.command = "" + compiler_options.gProjectName = "" + compiler_options.command = "" resetAllModulesHard() clearPasses() msgs.gErrorMax = 1 @@ -205,7 +240,8 @@ proc cleanup() = vm.globalCtx = nil initDefines() -proc readPackageInfoFromNims*(scriptName: string; result: var PackageInfo) = +proc readPackageInfoFromNims*(scriptName: string, options: Options, + result: var PackageInfo) = ## Executes the `scriptName` nimscript file. Reads the package information ## that it populates. @@ -217,20 +253,35 @@ proc readPackageInfoFromNims*(scriptName: string; result: var PackageInfo) = # The error counter is incremented after the writeLnHook is invoked. if msgs.gErrorCounter > 0: raise newException(NimbleError, previousMsg) + elif previousMsg.len > 0: + echo(previousMsg) + if output.normalize.startsWith("error"): + raise newException(NimbleError, output) previousMsg = output + compiler_options.command = internalCmd + # Execute the nimscript file. - execScript(scriptName, nil) + execScript(scriptName, nil, options) # Extract all the necessary fields populated by the nimscript file. + proc getSym(thisModule: PSym, ident: string): PSym = + thisModule.tab.strTableGet(getIdent(ident)) + template trivialField(field) = - result.field = getGlobal(astToStr field) + result.field = getGlobal(getSym(thisModule, astToStr field)) template trivialFieldSeq(field) = - result.field.add getGlobalAsSeq(astToStr field) + result.field.add getGlobalAsSeq(getSym(thisModule, astToStr field)) + + # Grab the module Sym for .nimble file (nimscriptapi is included in it). + let idx = fileInfoIdx(scriptName) + let thisModule = getModule(idx) + assert(not thisModule.isNil) + assert thisModule.kind == skModule # keep reasonable default: - let name = getGlobal"packageName" + let name = getGlobal(thisModule.tab.strTableGet(getIdent"packageName")) if name.len > 0: result.name = name trivialField version @@ -246,13 +297,13 @@ proc readPackageInfoFromNims*(scriptName: string; result: var PackageInfo) = trivialFieldSeq installFiles trivialFieldSeq installExt - extractRequires result.requires + extractRequires(getSym(thisModule, "requiresData"), result.requires) - let binSeq = getGlobalAsSeq("bin") + let binSeq = getGlobalAsSeq(getSym(thisModule, "bin")) for i in binSeq: result.bin.add(i.addFileExt(ExeExt)) - let backend = getGlobal("backend") + let backend = getGlobal(getSym(thisModule, "backend")) if backend.len == 0: result.backend = "c" elif cmpIgnoreStyle(backend, "javascript") == 0: @@ -262,19 +313,22 @@ proc readPackageInfoFromNims*(scriptName: string; result: var PackageInfo) = cleanup() -proc execTask*(scriptName, taskName: string): ExecutionResult = +proc execTask*(scriptName, taskName: string, + options: Options): ExecutionResult = ## Executes the specified task in the specified script. ## ## `scriptName` should be a filename pointing to the nimscript file. result.success = true result.flags = newStringTable() - options.command = internalCmd + compiler_options.command = internalCmd echo("Executing task ", taskName, " in ", scriptName) - execScript(scriptName, result.flags) + execScript(scriptName, result.flags, options) # Explicitly execute the task procedure, instead of relying on hack. - assert vm.globalCtx.module.kind == skModule - let prc = vm.globalCtx.module.tab.strTableGet(getIdent(taskName & "Task")) + let idx = fileInfoIdx(scriptName) + let thisModule = getModule(idx) + assert thisModule.kind == skModule + let prc = thisModule.tab.strTableGet(getIdent(taskName & "Task")) if prc.isNil: # Procedure not defined in the NimScript module. result.success = false @@ -282,27 +336,27 @@ proc execTask*(scriptName, taskName: string): ExecutionResult = discard vm.globalCtx.execProc(prc, []) # Read the command, arguments and flags set by the executed task. - result.command = options.command + result.command = compiler_options.command result.arguments = @[] - for arg in options.gProjectName.split(): + for arg in compiler_options.gProjectName.split(): result.arguments.add(arg) cleanup() proc getNimScriptCommand(): string = - options.command + compiler_options.command proc setNimScriptCommand(command: string) = - options.command = command + 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) = +proc listTasks*(scriptName: string, options: Options) = setNimScriptCommand("help") - execScript(scriptName, nil) + execScript(scriptName, nil, options) # TODO: Make the 'task' template generate explicit data structure containing # all the task names + descriptions. cleanup() diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 74e843e..41f22f9 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -3,7 +3,7 @@ import json, strutils, os, parseopt, strtabs -import nimblepkg/config, nimblepkg/version, nimblepkg/nimscriptsupport, +import nimblepkg/config, nimblepkg/version, nimblepkg/tools type diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index b118df7..ead94a6 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -1,10 +1,7 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. import parsecfg, json, streams, strutils, parseutils, os -import version, tools, nimbletypes, nimscriptsupport - -when not declared(system.map): - from sequtils import map +import version, tools, nimbletypes, options type Package* = object @@ -23,12 +20,7 @@ type MetaData* = object url*: string - NimbleFile* = string - - ValidationError* = object of NimbleError - warnInstalled*: bool # Determines whether to show a warning for installed pkgs - -proc initPackageInfo(path: string): PackageInfo = +proc initPackageInfo*(path: string): PackageInfo = result.mypath = path # reasonable default: result.name = path.splitFile.name @@ -48,37 +40,6 @@ proc initPackageInfo(path: string): PackageInfo = result.binDir = "" result.backend = "c" -proc newValidationError(msg: string, warnInstalled: bool): ref ValidationError = - result = newException(ValidationError, msg) - result.warnInstalled = warnInstalled - -proc validatePackageName*(name: string) = - ## Raises an error if specified package name contains invalid characters. - ## - ## A valid package name is one which is a valid nim module name. So only - ## underscores, letters and numbers allowed. - if name.len == 0: return - - if name[0] in {'0'..'9'}: - raise newValidationError(name & - "\"$1\" is an invalid package name: cannot begin with $2" % - [name, $name[0]], true) - - var prevWasUnderscore = false - for c in name: - case c - of '_': - if prevWasUnderscore: - raise newValidationError( - "$1 is an invalid package name: cannot contain \"__\"" % name, true) - prevWasUnderscore = true - of AllChars - IdentChars: - raise newValidationError( - "$1 is an invalid package name: cannot contain '$2'" % [name, $c], - true) - else: - prevWasUnderscore = false - proc toValidPackageName*(name: string): string = result = "" for c in name: @@ -88,128 +49,6 @@ proc toValidPackageName*(name: string): string = of AllChars - IdentChars - {'-'}: discard else: result.add(c) -proc validateVersion*(ver: string) = - for c in ver: - if c notin ({'.'} + Digits): - raise newValidationError( - "Version may only consist of numbers and the '.' character " & - "but found '" & c & "'.", false) - -proc validatePackageInfo(pkgInfo: PackageInfo, path: string) = - if pkgInfo.name == "": - raise newValidationError("Incorrect .nimble file: " & path & - " does not contain a name field.", false) - - if pkgInfo.name.normalize != path.splitFile.name.normalize: - raise newValidationError( - "The .nimble file name must match name specified inside " & path, true) - - if pkgInfo.version == "": - raise newValidationError("Incorrect .nimble file: " & path & - " does not contain a version field.", false) - - if not pkgInfo.isMinimal: - if pkgInfo.author == "": - raise newValidationError("Incorrect .nimble file: " & path & - " does not contain an author field.", false) - if pkgInfo.description == "": - raise newValidationError("Incorrect .nimble file: " & path & - " does not contain a description field.", false) - if pkgInfo.license == "": - raise newValidationError("Incorrect .nimble file: " & path & - " does not contain a license field.", false) - if pkgInfo.backend notin ["c", "cc", "objc", "cpp", "js"]: - raise newValidationError("'" & pkgInfo.backend & - "' is an invalid backend.", false) - - validateVersion(pkgInfo.version) - -proc nimScriptHint*(pkgInfo: PackageInfo) = - if not pkgInfo.isNimScript: - # TODO: Turn this into a warning. - # TODO: Add a URL explaining more. - echo("NOTE: The .nimble file for this project could make use of " & - "additional features, if converted into the new NimScript format.") - -proc multiSplit(s: string): seq[string] = - ## Returns ``s`` split by newline and comma characters. - ## - ## Before returning, all individual entries are stripped of whitespace and - ## also empty entries are purged from the list. If after all the cleanups are - ## done no entries are found in the list, the proc returns a sequence with - ## the original string as the only entry. - result = split(s, {char(0x0A), char(0x0D), ','}) - map(result, proc(x: var string) = x = x.strip()) - for i in countdown(result.len()-1, 0): - if len(result[i]) < 1: - result.del(i) - # Huh, nothing to return? Return given input. - if len(result) < 1: - return @[s] - -proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = - var fs = newFileStream(path, fmRead) - if fs != nil: - var p: CfgParser - open(p, fs, path) - var currentSection = "" - while true: - var ev = next(p) - case ev.kind - of cfgEof: - break - of cfgSectionStart: - currentSection = ev.section - of cfgKeyValuePair: - case currentSection.normalize - of "package": - case ev.key.normalize - of "name": result.name = ev.value - of "version": result.version = ev.value - of "author": result.author = ev.value - of "description": result.description = ev.value - of "license": result.license = ev.value - of "srcdir": result.srcDir = ev.value - of "bindir": result.binDir = ev.value - of "skipdirs": - result.skipDirs.add(ev.value.multiSplit) - of "skipfiles": - result.skipFiles.add(ev.value.multiSplit) - of "skipext": - result.skipExt.add(ev.value.multiSplit) - of "installdirs": - result.installDirs.add(ev.value.multiSplit) - of "installfiles": - result.installFiles.add(ev.value.multiSplit) - of "installext": - result.installExt.add(ev.value.multiSplit) - of "bin": - for i in ev.value.multiSplit: - result.bin.add(i.addFileExt(ExeExt)) - of "backend": - result.backend = ev.value.toLower() - case result.backend.normalize - of "javascript": result.backend = "js" - else: discard - else: - raise newException(NimbleError, "Invalid field: " & ev.key) - of "deps", "dependencies": - case ev.key.normalize - of "requires": - for v in ev.value.multiSplit: - result.requires.add(parseRequires(v.strip)) - else: - raise newException(NimbleError, "Invalid field: " & ev.key) - else: raise newException(NimbleError, - "Invalid section: " & currentSection) - of cfgOption: raise newException(NimbleError, - "Invalid package info, should not contain --" & ev.value) - of cfgError: - raise newException(NimbleError, "Error parsing .nimble file: " & ev.msg) - close(p) - else: - raise newException(ValueError, "Cannot open package info: " & path) - proc getNameVersion*(pkgpath: string): tuple[name, version: string] = ## Splits ``pkgpath`` in the format ``/home/user/.nimble/pkgs/package-0.1`` ## into ``(packagea, 0.1)`` @@ -217,7 +56,7 @@ proc getNameVersion*(pkgpath: string): tuple[name, version: string] = ## Also works for file paths like: ## ``/home/user/.nimble/pkgs/package-0.1/package.nimble`` - if pkgPath.splitFile.ext == ".nimble": + if pkgPath.splitFile.ext == ".nimble" or pkgPath.splitFile.ext == ".babel": return getNameVersion(pkgPath.splitPath.head) result.name = "" @@ -233,51 +72,6 @@ proc getNameVersion*(pkgpath: string): tuple[name, version: string] = result.version = tail[i+1 .. tail.len-1] break -proc readPackageInfo*(nf: NimbleFile; onlyMinimalInfo=false): PackageInfo = - ## Reads package info from the specified Nimble file. - ## - ## Attempts to read it using the "old" Nimble ini format first, if that - ## fails attempts to evaluate it as a nimscript file. - ## - ## If both fail then returns an error. - ## - ## When ``onlyMinimalInfo`` is true, only the `name` and `version` fields are - ## populated. The isNimScript field can also be relied on. - result = initPackageInfo(nf) - - validatePackageName(nf.splitFile.name) - - var success = false - var iniError: ref NimbleError - # Attempt ini-format first. - try: - readPackageInfoFromNimble(nf, result) - success = true - result.isNimScript = false - except NimbleError: - iniError = (ref NimbleError)(getCurrentException()) - - if not success: - if onlyMinimalInfo: - let tmp = getNameVersion(nf) - result.name = tmp.name - result.version = tmp.version - result.isNimScript = true - result.isMinimal = true - else: - try: - readPackageInfoFromNims(nf, result) - result.isNimScript = true - except NimbleError: - let msg = "Could not read package info file in " & nf & ";\n" & - " Reading as ini file failed with: \n" & - " " & iniError.msg & ".\n" & - " Evaluating as NimScript file failed with: \n" & - " " & getCurrentExceptionMsg() & "." - raise newException(NimbleError, msg) - - validatePackageInfo(result, nf) - proc optionalField(obj: JsonNode, name: string, default = ""): string = ## Queries ``obj`` for the optional ``name`` string. ## @@ -353,7 +147,7 @@ proc getPackageList*(packagesPath: string): seq[Package] = let pkg: Package = p.fromJson() result.add(pkg) -proc findNimbleFile*(dir: string; error: bool): NimbleFile = +proc findNimbleFile*(dir: string; error: bool): string = result = "" var hits = 0 for kind, path in walkDir(dir): @@ -375,14 +169,11 @@ proc findNimbleFile*(dir: string; error: bool): NimbleFile = # TODO: Abstract logging. echo("WARNING: No .nimble file found for ", dir) -proc getPkgInfo*(dir: string): PackageInfo = - ## Find the .nimble file in ``dir`` and parses it, returning a PackageInfo. - let nimbleFile = findNimbleFile(dir, true) - result = readPackageInfo(nimbleFile) - -proc getInstalledPkgs*(libsDir: string): +proc getInstalledPkgsMin*(libsDir: string, options: Options): seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = - ## Gets a list of installed packages. + ## 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``. ## ## ``libsDir`` is in most cases: ~/.nimble/pkgs/ result = @[] @@ -391,23 +182,13 @@ proc getInstalledPkgs*(libsDir: string): let nimbleFile = findNimbleFile(path, false) if nimbleFile != "": let meta = readMetaData(path) - try: - result.add((readPackageInfo(nimbleFile, true), meta)) - except ValidationError: - let exc = (ref ValidationError)(getCurrentException()) - if exc.warnInstalled: - echo("WARNING: Unable to read package info for " & path & "\n" & - " Package did not pass validation: " & exc.msg) - else: - exc.msg = "Unable to read package info for " & path & "\n" & - " Package did not pass validation: " & exc.msg - raise exc - except: - let exc = getCurrentException() - exc.msg = "Unable to read package info for " & path & "\n" & - " Error: " & exc.msg - raise exc - + let (name, version) = getNameVersion(nimbleFile) + var pkg = initPackageInfo(nimbleFile) + pkg.name = name + pkg.version = version + pkg.isMinimal = true + pkg.isInstalled = true + result.add((pkg, meta)) proc findPkg*(pkglist: seq[tuple[pkginfo: PackageInfo, meta: MetaData]], dep: PkgTuple, @@ -441,7 +222,7 @@ proc findAllPkgs*(pkglist: seq[tuple[pkginfo: PackageInfo, meta: MetaData]], proc getRealDir*(pkgInfo: PackageInfo): string = ## Returns the ``pkgInfo.srcDir`` or the .mypath directory if package does ## not specify the src dir. - if pkgInfo.srcDir != "": + if pkgInfo.srcDir != "" and not pkgInfo.isInstalled: result = pkgInfo.mypath.splitFile.dir / pkgInfo.srcDir else: result = pkgInfo.mypath.splitFile.dir @@ -469,9 +250,6 @@ proc getDownloadDirName*(pkg: Package, verRange: VersionRange): string = result.add "_" result.add verSimple -proc isNimScript*(nf: NimbleFile): bool = - result = readPackageInfo(nf).isNimScript - when isMainModule: doAssert getNameVersion("/home/user/.nimble/libs/packagea-0.1") == ("packagea", "0.1") diff --git a/src/nimblepkg/packageparser.nim b/src/nimblepkg/packageparser.nim new file mode 100644 index 0000000..0e0f03d --- /dev/null +++ b/src/nimblepkg/packageparser.nim @@ -0,0 +1,254 @@ +# Copyright (C) Dominik Picheta. All rights reserved. +# BSD License. Look at license.txt for more info. +import parsecfg, json, streams, strutils, parseutils, os +import version, tools, nimbletypes, nimscriptsupport, options, packageinfo + +## Contains procedures for parsing .nimble files. Moved here from ``packageinfo`` +## because it depends on ``nimscriptsupport`` (``nimscriptsupport`` also +## depends on other procedures in ``packageinfo``. + +when not declared(system.map): + from sequtils import map + +type + NimbleFile* = string + + ValidationError* = object of NimbleError + warnInstalled*: bool # Determines whether to show a warning for installed pkgs + +proc newValidationError(msg: string, warnInstalled: bool): ref ValidationError = + result = newException(ValidationError, msg) + result.warnInstalled = warnInstalled + +proc validatePackageName*(name: string) = + ## Raises an error if specified package name contains invalid characters. + ## + ## A valid package name is one which is a valid nim module name. So only + ## underscores, letters and numbers allowed. + if name.len == 0: return + + if name[0] in {'0'..'9'}: + raise newValidationError(name & + "\"$1\" is an invalid package name: cannot begin with $2" % + [name, $name[0]], true) + + var prevWasUnderscore = false + for c in name: + case c + of '_': + if prevWasUnderscore: + raise newValidationError( + "$1 is an invalid package name: cannot contain \"__\"" % name, true) + prevWasUnderscore = true + of AllChars - IdentChars: + raise newValidationError( + "$1 is an invalid package name: cannot contain '$2'" % [name, $c], + true) + else: + prevWasUnderscore = false + +proc validateVersion*(ver: string) = + for c in ver: + if c notin ({'.'} + Digits): + raise newValidationError( + "Version may only consist of numbers and the '.' character " & + "but found '" & c & "'.", false) + +proc validatePackageInfo(pkgInfo: PackageInfo, path: string) = + if pkgInfo.name == "": + raise newValidationError("Incorrect .nimble file: " & path & + " does not contain a name field.", false) + + if pkgInfo.name.normalize != path.splitFile.name.normalize: + raise newValidationError( + "The .nimble file name must match name specified inside " & path, true) + + if pkgInfo.version == "": + raise newValidationError("Incorrect .nimble file: " & path & + " does not contain a version field.", false) + + if not pkgInfo.isMinimal: + if pkgInfo.author == "": + raise newValidationError("Incorrect .nimble file: " & path & + " does not contain an author field.", false) + if pkgInfo.description == "": + raise newValidationError("Incorrect .nimble file: " & path & + " does not contain a description field.", false) + if pkgInfo.license == "": + raise newValidationError("Incorrect .nimble file: " & path & + " does not contain a license field.", false) + if pkgInfo.backend notin ["c", "cc", "objc", "cpp", "js"]: + raise newValidationError("'" & pkgInfo.backend & + "' is an invalid backend.", false) + + validateVersion(pkgInfo.version) + +proc nimScriptHint*(pkgInfo: PackageInfo) = + if not pkgInfo.isNimScript: + # TODO: Turn this into a warning. + # TODO: Add a URL explaining more. + echo("NOTE: The .nimble file for this project could make use of " & + "additional features, if converted into the new NimScript format.") + +proc multiSplit(s: string): seq[string] = + ## Returns ``s`` split by newline and comma characters. + ## + ## Before returning, all individual entries are stripped of whitespace and + ## also empty entries are purged from the list. If after all the cleanups are + ## done no entries are found in the list, the proc returns a sequence with + ## the original string as the only entry. + result = split(s, {char(0x0A), char(0x0D), ','}) + map(result, proc(x: var string) = x = x.strip()) + for i in countdown(result.len()-1, 0): + if len(result[i]) < 1: + result.del(i) + # Huh, nothing to return? Return given input. + if len(result) < 1: + return @[s] + +proc readPackageInfoFromNimble(path: string; result: var PackageInfo) = + var fs = newFileStream(path, fmRead) + if fs != nil: + var p: CfgParser + open(p, fs, path) + var currentSection = "" + while true: + var ev = next(p) + case ev.kind + of cfgEof: + break + of cfgSectionStart: + currentSection = ev.section + of cfgKeyValuePair: + case currentSection.normalize + of "package": + case ev.key.normalize + of "name": result.name = ev.value + of "version": result.version = ev.value + of "author": result.author = ev.value + of "description": result.description = ev.value + of "license": result.license = ev.value + of "srcdir": result.srcDir = ev.value + of "bindir": result.binDir = ev.value + of "skipdirs": + result.skipDirs.add(ev.value.multiSplit) + of "skipfiles": + result.skipFiles.add(ev.value.multiSplit) + of "skipext": + result.skipExt.add(ev.value.multiSplit) + of "installdirs": + result.installDirs.add(ev.value.multiSplit) + of "installfiles": + result.installFiles.add(ev.value.multiSplit) + of "installext": + result.installExt.add(ev.value.multiSplit) + of "bin": + for i in ev.value.multiSplit: + result.bin.add(i.addFileExt(ExeExt)) + of "backend": + result.backend = ev.value.toLower() + case result.backend.normalize + of "javascript": result.backend = "js" + else: discard + else: + raise newException(NimbleError, "Invalid field: " & ev.key) + of "deps", "dependencies": + case ev.key.normalize + of "requires": + for v in ev.value.multiSplit: + result.requires.add(parseRequires(v.strip)) + else: + raise newException(NimbleError, "Invalid field: " & ev.key) + else: raise newException(NimbleError, + "Invalid section: " & currentSection) + of cfgOption: raise newException(NimbleError, + "Invalid package info, should not contain --" & ev.value) + of cfgError: + raise newException(NimbleError, "Error parsing .nimble file: " & ev.msg) + close(p) + else: + raise newException(ValueError, "Cannot open package info: " & path) + +proc readPackageInfo*(nf: NimbleFile, options: Options, + onlyMinimalInfo=false): PackageInfo = + ## Reads package info from the specified Nimble file. + ## + ## Attempts to read it using the "old" Nimble ini format first, if that + ## fails attempts to evaluate it as a nimscript file. + ## + ## If both fail then returns an error. + ## + ## When ``onlyMinimalInfo`` is true, only the `name` and `version` fields are + ## populated. The isNimScript field can also be relied on. + result = initPackageInfo(nf) + + validatePackageName(nf.splitFile.name) + + var success = false + var iniError: ref NimbleError + # Attempt ini-format first. + try: + readPackageInfoFromNimble(nf, result) + success = true + result.isNimScript = false + except NimbleError: + iniError = (ref NimbleError)(getCurrentException()) + + if not success: + if onlyMinimalInfo: + let tmp = getNameVersion(nf) + result.name = tmp.name + result.version = tmp.version + result.isNimScript = true + result.isMinimal = true + else: + try: + readPackageInfoFromNims(nf, options, result) + result.isNimScript = true + except NimbleError: + let msg = "Could not read package info file in " & nf & ";\n" & + " Reading as ini file failed with: \n" & + " " & iniError.msg & ".\n" & + " Evaluating as NimScript file failed with: \n" & + " " & getCurrentExceptionMsg() & "." + raise newException(NimbleError, msg) + + validatePackageInfo(result, nf) + +proc getPkgInfo*(dir: string, options: Options): PackageInfo = + ## Find the .nimble file in ``dir`` and parses it, returning a PackageInfo. + let nimbleFile = findNimbleFile(dir, true) + result = readPackageInfo(nimbleFile, options) + +proc getInstalledPkgs*(libsDir: string, options: Options): + seq[tuple[pkginfo: PackageInfo, meta: MetaData]] = + ## Gets a list of installed packages. + ## + ## ``libsDir`` is in most cases: ~/.nimble/pkgs/ + result = @[] + for kind, path in walkDir(libsDir): + if kind == pcDir: + let nimbleFile = findNimbleFile(path, false) + if nimbleFile != "": + let meta = readMetaData(path) + try: + var pkg = readPackageInfo(nimbleFile, options, true) + pkg.isInstalled = true + result.add((pkg, meta)) + except ValidationError: + let exc = (ref ValidationError)(getCurrentException()) + if exc.warnInstalled: + echo("WARNING: Unable to read package info for " & path & "\n" & + " Package did not pass validation: " & exc.msg) + else: + exc.msg = "Unable to read package info for " & path & "\n" & + " Package did not pass validation: " & exc.msg + raise exc + except: + let exc = getCurrentException() + exc.msg = "Unable to read package info for " & path & "\n" & + " Error: " & exc.msg + raise exc + +proc isNimScript*(nf: string, options: Options): bool = + result = readPackageInfo(nf, options).isNimScript \ No newline at end of file diff --git a/tests/nimscript/nimscript.nimble b/tests/nimscript/nimscript.nimble index 4a71b56..ab56b06 100644 --- a/tests/nimscript/nimscript.nimble +++ b/tests/nimscript/nimscript.nimble @@ -19,4 +19,7 @@ task c_test, "Testing `setCommand \"c\", \"nimscript.nim\"`": task cr, "Testing `nimble c -r nimscript.nim` via setCommand": --r - setCommand "c", "nimscript.nim" \ No newline at end of file + setCommand "c", "nimscript.nim" + +task api, "Testing nimscriptapi module functionality": + echo(getPkgDir()) \ No newline at end of file