442 lines
14 KiB
Nim
442 lines
14 KiB
Nim
# 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/lists, 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, times, osproc, sets
|
|
|
|
when not declared(resetAllModulesHard):
|
|
import compiler/modulegraphs
|
|
|
|
type
|
|
ExecutionResult*[T] = object
|
|
success*: bool
|
|
command*: string
|
|
arguments*: seq[string]
|
|
flags*: StringTableRef
|
|
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: StringTableRef): 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:
|
|
flags[a.getString 0] = a.getString 1
|
|
|
|
proc getNimPrefixDir(): string = splitPath(findExe("nim")).head.parentDir
|
|
|
|
when declared(ModuleGraph):
|
|
var graph: ModuleGraph
|
|
|
|
proc execScript(scriptName: string, flags: StringTableRef,
|
|
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()
|
|
|
|
let pkgName = scriptName.splitFile.name
|
|
|
|
# Ensure that "nimblepkg/nimscriptapi" is in the PATH.
|
|
# TODO: put this in a more isolated directory.
|
|
let tmpNimscriptApiPath = getTempDir() / "nimblepkg" / "nimscriptapi.nim"
|
|
createDir(tmpNimscriptApiPath.splitFile.dir)
|
|
if not existsFile(tmpNimscriptApiPath):
|
|
writeFile(tmpNimscriptApiPath, nimscriptApi)
|
|
appendStr(searchPaths, getTempDir())
|
|
|
|
initDefines()
|
|
loadConfigs(DefaultConfig)
|
|
passes.gIncludeFile = includeModule
|
|
passes.gImportModule = importModule
|
|
|
|
defineSymbol("nimscript")
|
|
defineSymbol("nimconfig")
|
|
defineSymbol("nimble")
|
|
registerPass(semPass)
|
|
registerPass(evalPass)
|
|
|
|
appendStr(searchPaths, 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 = HighPriority)
|
|
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..<graph.modules.len:
|
|
if graph.modules[i] != nil and
|
|
graph.modules[i].name.s == "nimscriptapi":
|
|
apiModule = graph.modules[i]
|
|
break
|
|
doAssert apiModule != nil
|
|
|
|
# Check whether an error has occurred.
|
|
if msgs.gErrorCounter > 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 = newStringTable()
|
|
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 = newStringTable()
|
|
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: Make the 'task' template generate explicit data structure containing
|
|
# all the task names + descriptions.
|
|
cleanup()
|