# 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, compiler/nversion

from compiler/scriptconfig import setupVM
from compiler/astalgo import strTableGet
import compiler/options as compiler_options

import common, version, options, packageinfo, cli, tools
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}

when declared(NimCompilerApiVersion):
  const finalApi = NimCompilerApiVersion >= 2

  when NimCompilerApiVersion >= 3:
    import compiler / pathutils
else:
  const finalApi = false

proc getGlobal(g: ModuleGraph; ident: PSym): string =
  when finalApi:
    let n = vm.getGlobalValue(PCtx g.vm, ident)
  else:
    let n = vm.globalCtx.getGlobalValue(ident)
  if n.isStrLit:
    result = n.strVal
  else:
    raiseVariableError(ident.name.s, "string")

proc getGlobalAsSeq(g: ModuleGraph; ident: PSym): seq[string] =
  when finalApi:
    let n = vm.getGlobalValue(PCtx g.vm, ident)
  else:
    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(g: ModuleGraph; ident: PSym, result: var seq[PkgTuple]) =
  when finalApi:
    let n = vm.getGlobalValue(PCtx g.vm, ident)
  else:
    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(graph: ModuleGraph; 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(NimCompilerApiVersion):
    result = newCtx(module, identCache, graph)
  elif declared(newIdentCache):
    result = newCtx(module, identCache)
  else:
    result = newCtx(module)
  result.mode = emRepl
  registerAdditionalOps(result)

  # captured vars:
  let conf = graph.config
  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, toUnix(getLastModificationTime(getString(a, 0))))
  cbos findExe:
    setResult(a, os.findExe(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:
    when declared(NimCompilerApiVersion):
      compiler_options.setConfigVar(conf, getString(a, 0), getString(a, 1))
    else:
      compiler_options.setConfigVar(getString(a, 0), getString(a, 1))
  cbconf get:
    when declared(NimCompilerApiVersion):
      setResult(a, compiler_options.getConfigVar(conf, a.getString 0))
    else:
      setResult(a, compiler_options.getConfigVar(a.getString 0))
  cbconf exists:
    when declared(NimCompilerApiVersion):
      setResult(a, compiler_options.existsConfigVar(conf, a.getString 0))
    else:
      setResult(a, compiler_options.existsConfigVar(a.getString 0))
  cbconf nimcacheDir:
    when declared(NimCompilerApiVersion):
      setResult(a, compiler_options.getNimcacheDir(conf))
    else:
      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:
    when declared(NimCompilerApiVersion):
      conf.command = a.getString 0
      let arg = a.getString 1
      if arg.len > 0:
        conf.projectName = arg
        when NimCompilerApiVersion >= 3:
          try:
            conf.projectFull = canonicalizePath(conf,
                conf.projectPath / RelativeFile(conf.projectName))
          except OSError:
            conf.projectFull = AbsoluteFile conf.projectName
        else:
          try:
            conf.projectFull = canonicalizePath(conf, conf.projectPath / conf.projectName)
          except OSError:
            conf.projectFull = conf.projectName
    else:
      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:
    when declared(NimCompilerApiVersion):
      setResult(a, conf.command)
    else:
      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 finalApi:
  var graph = newModuleGraph(identCache, newConfigRef())

elif declared(ModuleGraph):
  var graph = newModuleGraph()

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 finalApi:
    graph = newModuleGraph(graph.cache, graph.config)
  else:
    graph = newModuleGraph(graph.config)

  let conf = graph.config
  when declared(NimCompilerApiVersion):
    if "nimblepkg/nimscriptapi" notin conf.implicitImports:
      conf.implicitImports.add("nimblepkg/nimscriptapi")
  elif 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.
  when declared(NimCompilerApiVersion):
    when NimCompilerApiVersion >= 3:
      conf.prefixDir = AbsoluteDir getNimPrefixDir(options)
      display("Setting", "Nim stdlib prefix to " & conf.prefixDir.string,
              priority=LowPriority)

      template myLibPath(): untyped = conf.libpath.string

    else:
      conf.prefixDir = getNimPrefixDir(options)
      display("Setting", "Nim stdlib prefix to " & conf.prefixDir,
              priority=LowPriority)

      template myLibPath(): untyped = conf.libpath

    # Verify that lib path points to existing stdlib.
    setDefaultLibpath(conf)
  else:
    compiler_options.gPrefixDir = getNimPrefixDir(options)
    display("Setting", "Nim stdlib prefix to " & compiler_options.gPrefixDir,
            priority=LowPriority)

    template myLibPath(): untyped = compiler_options.libpath

    # Verify that lib path points to existing stdlib.
    compiler_options.setDefaultLibpath()

  display("Setting", "Nim stdlib path to " & myLibPath(),
          priority=LowPriority)
  if not isValidLibPath(myLibPath()):
    let msg = "Nimble cannot find Nim's standard library.\nLast try in:\n  - $1" %
                myLibPath()
    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(myLibPath())
  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: $#.") %
              [myLibPath(), $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 = getNimbleUserTempDir() / "nimblecache"
    let tmpNimscriptApiPath = t / "nimblepkg" / "nimscriptapi.nim"
    createDir(tmpNimscriptApiPath.splitFile.dir)
    writeFile(tmpNimscriptApiPath, nimscriptApi)
    when declared(NimCompilerApiVersion):
      when NimCompilerApiVersion >= 3:
        conf.searchPaths.add(AbsoluteDir t)
      else:
        conf.searchPaths.add(t)
    else:
      searchPaths.add(t)

  when declared(NimCompilerApiVersion):
    initDefines(conf.symbols)
    when NimCompilerApiVersion >= 2:
      loadConfigs(DefaultConfig, graph.cache, conf)
    else:
      loadConfigs(DefaultConfig, conf)
      passes.gIncludeFile = includeModule
      passes.gImportModule = importModule

    defineSymbol(conf.symbols, "nimscript")
    defineSymbol(conf.symbols, "nimconfig")
    defineSymbol(conf.symbols, "nimble")
    when NimCompilerApiVersion >= 2:
      registerPass(graph, semPass)
      registerPass(graph, evalPass)
    else:
      registerPass(semPass)
      registerPass(evalPass)

    conf.searchPaths.add(conf.libpath)
  else:
    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:
    result = graph.makeModule(scriptName)

  incl(result.flags, sfMainModule)
  when finalApi:
    graph.vm = setupVM(graph, result, scriptName, flags)

    # Setup builtins defined in nimscriptapi.nim
    template cbApi(name, body) {.dirty.} =
      PCtx(graph.vm).registerCallback "nimscriptapi." & astToStr(name),
        proc (a: VmArgs) =
          body

  else:
    vm.globalCtx = setupVM(graph, result, scriptName, flags)

    # Setup builtins defined in nimscriptapi.nim
    template cbApi(name, body) {.dirty.} =
      vm.globalCtx.registerCallback "nimscriptapi." & astToStr(name),
        proc (a: VmArgs) =
          body

  cbApi getPkgDir:
    setResult(a, scriptName.splitFile.dir)

  when finalApi:
    graph.compileSystemModule()
    when NimCompilerApiVersion >= 3:
      graph.processModule(result, llStreamOpen(AbsoluteFile scriptName, fmRead))
    else:
      graph.processModule(result, llStreamOpen(scriptName, fmRead))
  elif 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:
  when declared(NimCompilerApiVersion):
    let conf = graph.config
    conf.projectName = ""
    conf.command = ""
  else:
    compiler_options.gProjectName = ""
    compiler_options.command = ""
  when declared(NimCompilerApiVersion):
    resetSystemArtifacts(graph)
  elif declared(resetAllModulesHard):
    resetAllModulesHard()
  else:
    resetSystemArtifacts()
  when finalApi:
    clearPasses(graph)
  else:
    clearPasses()
  when declared(NimCompilerApiVersion):
    conf.errorMax = 1
    when NimCompilerApiVersion >= 2:
      conf.writeLnHook = nil
      graph.vm = nil
    else:
      msgs.writeLnHook = nil
      vm.globalCtx = nil
    initDefines(conf.symbols)
  else:
    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.
  when declared(NimCompilerApiVersion):
    let conf = graph.config
    conf.errorMax = high(int)
  else:
    msgs.gErrorMax = high(int)

  template errCounter(): int =
    when declared(NimCompilerApiVersion): conf.errorCounter
    else: msgs.gErrorCounter

  var previousMsg = ""

  proc writelnHook(output: string) =
    # The error counter is incremented after the writeLnHook is invoked.
    if errCounter() > 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

  when finalApi:
    conf.writelnHook = writelnHook
  else:
    msgs.writeLnHook = writelnHook

  when declared(NimCompilerApiVersion):
    conf.command = internalCmd
  else:
    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 errCounter() > 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(identCache, ident))
    if result.isNil:
      raise newException(NimbleError, "Ident not found: " & ident)

  template trivialField(field) =
    result.field = getGlobal(graph, getSym(apiModule, astToStr field))

  template trivialFieldSeq(field) =
    result.field.add getGlobalAsSeq(graph, getSym(apiModule, astToStr field))

  # keep reasonable default:
  let name = getGlobal(graph, apiModule.tab.strTableGet(getIdent(identCache, "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(graph, getSym(apiModule, "requiresData"), result.requires)

  let binSeq = getGlobalAsSeq(graph, getSym(apiModule, "bin"))
  for i in binSeq:
    result.bin.add(i.addFileExt(ExeExt))

  let backend = getGlobal(graph, 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()

when declared(NimCompilerApiVersion):
  template nimCommand(): untyped = conf.command
  template nimProjectName(): untyped = conf.projectName
else:
  template nimCommand(): untyped = compiler_options.command
  template nimProjectName(): untyped = compiler_options.gProjectName

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]]()
  when declared(NimCompilerApiVersion):
    let conf = graph.config
  nimCommand() = internalCmd
  display("Executing",  "task $# in $#" % [taskName, scriptName],
          priority = HighPriority)

  let thisModule = execScript(scriptName, result.flags, options)
  let prc = thisModule.tab.strTableGet(getIdent(identCache, taskName & "Task"))
  if prc.isNil:
    # Procedure not defined in the NimScript module.
    result.success = false
    cleanup()
    return
  when finalApi:
    discard vm.execProc(PCtx(graph.vm), prc, [])
  else:
    discard vm.globalCtx.execProc(prc, [])

  # Read the command, arguments and flags set by the executed task.
  result.command = nimCommand()
  result.arguments = @[]
  for arg in nimProjectName().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.
  when declared(NimCompilerApiVersion):
    let conf = graph.config
  result.success = true
  result.flags = newTable[string, seq[string]]()
  nimCommand() = 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(identCache, hookName))
  if prc.isNil:
    # Procedure not defined in the NimScript module.
    result.success = false
    cleanup()
    return
  when finalApi:
    let returnVal = vm.execProc(PCtx(graph.vm), prc, [])
  else:
    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 = nimCommand()
  result.arguments = @[]
  for arg in nimProjectName().split():
    result.arguments.add(arg)

  cleanup()

proc getNimScriptCommand(): string =
  when declared(NimCompilerApiVersion):
    let conf = graph.config
  nimCommand()

proc setNimScriptCommand(command: string) =
  when declared(NimCompilerApiVersion):
    let conf = graph.config
  nimCommand() = 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()
