All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mill.main.MainModule.scala Maven / Gradle / Ivy

The newest version!
package mill.main

import mill.api.{Ctx, _}
import mill.define.{BaseModule0, Command, NamedTask, Segments, Target, Task, _}
import mill.eval.{Evaluator, EvaluatorPaths, Terminal}
import mill.moduledefs.Scaladoc
import mill.resolve.SelectMode.Separated
import mill.resolve.{Resolve, SelectMode}
import mill.util.{Util, Watchable}
import pprint.{Renderer, Tree, Truncated}

import java.util.concurrent.LinkedBlockingQueue
import scala.annotation.tailrec
import scala.collection.mutable
import scala.reflect.NameTransformer.decode

object MainModule {

  def resolveTasks[T](
      evaluator: Evaluator,
      targets: Seq[String],
      selectMode: SelectMode,
      resolveToModuleTasks: Boolean = false
  )(f: List[NamedTask[Any]] => T): Result[T] = {
    Resolve.Tasks.resolve(
      evaluator.rootModule,
      targets,
      selectMode,
      resolveToModuleTasks = resolveToModuleTasks
    ) match {
      case Left(err) => Result.Failure(err)
      case Right(tasks) => Result.Success(f(tasks))
    }
  }

  def resolveTasks[T](
      evaluator: Evaluator,
      targets: Seq[String],
      selectMode: SelectMode
  )(f: List[NamedTask[Any]] => T): Result[T] = {
    Resolve.Tasks.resolve(evaluator.rootModule, targets, selectMode) match {
      case Left(err) => Result.Failure(err)
      case Right(tasks) => Result.Success(f(tasks))
    }
  }

  private def show0(
      evaluator: Evaluator,
      targets: Seq[String],
      log: Logger,
      watch0: Watchable => Unit
  )(f: Seq[(Any, Option[(RunScript.TaskName, ujson.Value)])] => ujson.Value)
      : Result[ujson.Value] = {

    // When using `show`, redirect all stdout of the evaluated tasks so the
    // printed JSON is the only thing printed to stdout.
    val redirectLogger = log
      .withOutStream(evaluator.baseLogger.errorStream)
      .asInstanceOf[mill.util.ColorLogger]

    RunScript.evaluateTasksNamed(
      evaluator.withBaseLogger(redirectLogger),
      targets,
      Separated
    ) match {
      case Left(err) => Result.Failure(err)
      case Right((watched, Left(err))) =>
        watched.foreach(watch0)
        Result.Failure(err)

      case Right((watched, Right(res))) =>
        val output = f(res)
        watched.foreach(watch0)
        println(output.render(indent = 2))
        Result.Success(output)
    }
  }
}

/**
 * [[mill.define.Module]] containing all the default tasks that Mill provides: [[resolve]],
 * [[show]], [[inspect]], [[plan]], etc.
 */
trait MainModule extends BaseModule0 {

  object interp extends Interp

  /**
   * Show the mill version.
   */
  def version(): Command[String] = Task.Command(exclusive = true) {
    val res = BuildInfo.millVersion
    println(res)
    res
  }

  /**
   * Resolves a mill query string and prints out the tasks it resolves to.
   */
  def resolve(evaluator: Evaluator, targets: String*): Command[List[String]] =
    Task.Command(exclusive = true) {
      val resolved = Resolve.Segments.resolve(
        evaluator.rootModule,
        targets,
        SelectMode.Multi
      )

      resolved match {
        case Left(err) => Result.Failure(err)
        case Right(resolvedSegmentsList) =>
          val resolvedStrings = resolvedSegmentsList.map(_.render)
          resolvedStrings.sorted.foreach(println)
          Result.Success(resolvedStrings)
      }
    }

  /**
   * Given a set of tasks, prints out the execution plan of what tasks will be
   * executed in what order, without actually executing them.
   */
  def plan(evaluator: Evaluator, targets: String*): Command[Array[String]] =
    Task.Command(exclusive = true) {
      plan0(evaluator, targets) match {
        case Left(err) => Result.Failure(err)
        case Right(success) =>
          val renderedTasks = success.map(_.segments.render)
          renderedTasks.foreach(println)
          Result.Success(renderedTasks)
      }
    }

  private def plan0(evaluator: Evaluator, targets: Seq[String]) = {
    Resolve.Tasks.resolve(
      evaluator.rootModule,
      targets,
      SelectMode.Multi
    ) match {
      case Left(err) => Left(err)
      case Right(rs) =>
        val (sortedGroups, _) = evaluator.plan(rs)
        Right(sortedGroups.keys().collect { case r: Terminal.Labelled[_] => r }.toArray)
    }
  }

  /**
   * Prints out some dependency path from the `src` task to the `dest` task.
   *
   * If there are multiple dependency paths between `src` and `dest`, the path
   * chosen is arbitrary.
   */

  def path(
      evaluator: Evaluator,
      @mainargs.arg(positional = true) src: String,
      @mainargs.arg(positional = true) dest: String
  ): Command[List[String]] =
    Task.Command(exclusive = true) {
      val resolved = Resolve.Tasks.resolve(
        evaluator.rootModule,
        List(src, dest),
        SelectMode.Multi
      )

      resolved match {
        case Left(err) => Result.Failure(err)
        case Right(Seq(src1, dest1)) =>
          val queue = collection.mutable.Queue[List[Task[_]]](List(src1))
          var found = Option.empty[List[Task[_]]]
          val seen = collection.mutable.Set.empty[Task[_]]
          while (queue.nonEmpty && found.isEmpty) {
            val current = queue.dequeue()
            if (current.head == dest1) found = Some(current)
            else {
              for {
                next <- current.head.inputs
                if !seen.contains(next)
              } {
                seen.add(next)
                queue.enqueue(next :: current)
              }
            }
          }
          found match {
            case None =>
              Result.Failure(s"No path found between $src and $dest")
            case Some(list) =>
              val labels = list
                .collect { case n: NamedTask[_] => n.ctx.segments.render }

              labels.foreach(println)
              Result.Success(labels)
          }
      }
    }

  private lazy val inspectItemIndent = "    "

  /**
   * Displays metadata about the given task without actually running it.
   */
  def inspect(evaluator: Evaluator, tasks: String*): Command[String] =
    Task.Command(exclusive = true) {

      /** Find a parent classes of the given class queue. */
      @tailrec
      def resolveParents(queue: List[Class[_]], seen: Seq[Class[_]] = Seq()): Seq[Class[_]] = {
        queue match {
          case Nil => seen
          case cand :: rest if seen.contains(cand) => resolveParents(rest, seen)
          case cand :: rest =>
            val sups = Option(cand.getSuperclass).toList ++ cand.getInterfaces.toList
            resolveParents(sups ::: rest, seen ++ Seq(cand))
        }
      }

      def renderFileName(t: NamedTask[_]) = {
        // handle both Windows or Unix separators
        val fullFileName = t.ctx.fileName.replaceAll(raw"\\", "/")
        val basePath = WorkspaceRoot.workspaceRoot.toString().replaceAll(raw"\\", "/") + "/"
        val name =
          if (fullFileName.startsWith(basePath)) {
            fullFileName.drop(basePath.length)
          } else {
            fullFileName.split('/').last
          }
        s"${name}:${t.ctx.lineNum}"
      }

      def pprintTask(t: NamedTask[_], evaluator: Evaluator): Tree.Lazy = {
        val seen = mutable.Set.empty[Task[_]]

        def rec(t: Task[_]): Seq[Segments] = {
          if (seen(t)) Nil // do nothing
          else t match {
            case t: mill.define.Target[_]
                if evaluator.rootModule.millInternal.targets.contains(t) =>
              Seq(t.ctx.segments)
            case _ =>
              seen.add(t)
              t.inputs.flatMap(rec)
          }
        }

        val annots = for {
          c <- resolveParents(List(t.ctx.enclosingCls))
          m <- c.getMethods
          if m.getName == t.ctx.segment.pathSegments.head
          a = m.getAnnotation(classOf[mill.moduledefs.Scaladoc])
          if a != null
        } yield a

        val allDocs =
          for (a <- annots.distinct)
            yield Util.cleanupScaladoc(a.value).map("\n" + inspectItemIndent + _).mkString

        pprint.Tree.Lazy { ctx =>
          val mainMethodSig =
            if (t.asCommand.isEmpty) List()
            else {
              val mainDataOpt = evaluator
                .rootModule
                .millDiscover
                .value
                .get(t.ctx.enclosingCls)
                .flatMap(_._2.find(_.name == t.ctx.segments.parts.last))
                .headOption

              mainDataOpt match {
                case Some(mainData) if mainData.renderedArgSigs.nonEmpty =>
                  val rendered = mainargs.Renderer.formatMainMethodSignature(
                    mainDataOpt.get,
                    leftIndent = 2,
                    totalWidth = 100,
                    leftColWidth = mainargs.Renderer.getLeftColWidth(mainData.renderedArgSigs),
                    docsOnNewLine = false,
                    customName = None,
                    customDoc = None,
                    sorted = true,
                    nameMapper = mainargs.Util.kebabCaseNameMapper
                  )

                  // trim first line containing command name, since we already render
                  // the command name below with the filename and line num
                  val trimmedRendered = rendered
                    .linesIterator
                    .drop(1)
                    .mkString("\n")

                  List("\n", trimmedRendered, "\n")

                case _ => List()
              }
            }

          Iterator(
            ctx.applyPrefixColor(t.toString).toString,
            "(",
            renderFileName(t),
            ")",
            allDocs.mkString("\n"),
            "\n"
          ) ++
            mainMethodSig.iterator ++
            Iterator(
              "\n",
              ctx.applyPrefixColor("Inputs").toString,
              ":"
            ) ++ t.inputs.iterator.flatMap(rec).map("\n" + inspectItemIndent + _.render).distinct
        }
      }

      def pprintModule(t: ModuleTask[_], evaluator: Evaluator): Tree.Lazy = {
        val cls = t.module.getClass
        val annotation = cls.getAnnotation(classOf[Scaladoc])
        val scaladocOpt = Option(annotation).map(annotation =>
          Util.cleanupScaladoc(annotation.value).map("\n" + inspectItemIndent + _).mkString
        )

        def parentFilter(parent: Class[_]) =
          classOf[Module].isAssignableFrom(parent) && classOf[Module] != parent

        val parents = (Option(cls.getSuperclass).toSeq ++ cls.getInterfaces).distinct

        val inheritedModules = parents.filter(parentFilter)

        val allInheritedModules = Option.when(Target.log.debugEnabled)(
          resolveParents(parents.toList)
            .filter(parentFilter)
            .filterNot(inheritedModules.contains)
        ).toSeq.flatten

        def getModuleDeps(methodName: String): Seq[Module] = cls
          .getMethods
          .find(m => decode(m.getName) == methodName)
          .toSeq
          .map(_.invoke(t.module).asInstanceOf[Seq[Module]])
          .flatten

        val javaModuleDeps = getModuleDeps("moduleDeps")
        val javaCompileModuleDeps = getModuleDeps("compileModuleDeps")
        val javaRunModuleDeps = getModuleDeps("runModuleDeps")
        val hasModuleDeps =
          javaModuleDeps.nonEmpty || javaCompileModuleDeps.nonEmpty || javaRunModuleDeps.nonEmpty

        val defaultTaskOpt = t.module match {
          case taskMod: TaskModule => Some(s"${t.module}.${taskMod.defaultCommandName()}")
          case _ => None
        }

        val methodMap = evaluator.rootModule.millDiscover.value
        val tasks = methodMap.get(cls).map {
          case (_, _, tasks) => tasks.map(task => s"${t.module}.$task")
        }.toSeq.flatten
        pprint.Tree.Lazy { ctx =>
          Iterator(
            // module name(module/file:line)
            Iterator(
              ctx.applyPrefixColor(t.module.toString).toString,
              s"(${renderFileName(t)})"
            ),
            // Scaladoc
            Iterator(scaladocOpt).flatten,
            // Inherited Modules:
            Iterator(
              "\n\n",
              ctx.applyPrefixColor("Inherited Modules").toString,
              ":"
            ),
            inheritedModules.map("\n" + inspectItemIndent + _.getName),
            // Indirect Inherited Modules:
            if (allInheritedModules.isEmpty) Iterator.empty[String]
            else Iterator(
              "\n\n",
              ctx.applyPrefixColor("Indirect Inherited Modules").toString,
              ":\n",
              inspectItemIndent,
              allInheritedModules.map(_.getName).mkString("\n" + inspectItemIndent)
            ),
            // Module Dependencies: (JavaModule)
            if (hasModuleDeps) Iterator(
              "\n\n",
              ctx.applyPrefixColor("Module Dependencies").toString,
              ":"
            )
            else Iterator.empty[String],
            javaModuleDeps.map("\n" + inspectItemIndent + _.toString),
            javaCompileModuleDeps.map("\n" + inspectItemIndent + _.toString + " (compile)"),
            javaRunModuleDeps.map("\n" + inspectItemIndent + _.toString + " (runtime)"),
            // Default Task:
            defaultTaskOpt.fold(Iterator.empty[String])(task =>
              Iterator("\n\n", ctx.applyPrefixColor("Default Task").toString, ": ", task)
            ),
            // Tasks (re-/defined):
            if (tasks.isEmpty) Iterator.empty[String]
            else Iterator(
              "\n\n",
              ctx.applyPrefixColor("Tasks (re-/defined)").toString,
              ":\n",
              inspectItemIndent,
              tasks.mkString("\n" + inspectItemIndent)
            )
          ).flatten
        }
      }

      MainModule.resolveTasks(evaluator, tasks, SelectMode.Multi, resolveToModuleTasks = true) {
        tasks =>
          val output = (for {
            task <- tasks
            tree = task match {
              case t: ModuleTask[_] => pprintModule(t, evaluator)
              case t => pprintTask(t, evaluator)
            }
            defaults = pprint.PPrinter()
            renderer = new Renderer(
              defaults.defaultWidth,
              defaults.colorApplyPrefix,
              defaults.colorLiteral,
              defaults.defaultIndent
            )
            rendered = renderer.rec(tree, 0, 0).iter
            truncated = new Truncated(rendered, defaults.defaultWidth, defaults.defaultHeight)
          } yield {
            val sb = new StringBuilder()
            for { str <- truncated ++ Iterator("\n") } sb.append(str)
            sb.toString()
          }).mkString("\n")
          println(output)
          fansi.Str(output).plainText
      }
    }

  /**
   * Runs a given task and prints the JSON result to stdout. This is useful
   * to integrate Mill into external scripts and tooling.
   */
  def show(evaluator: Evaluator, targets: String*): Command[ujson.Value] =
    Task.Command(exclusive = true) {
      MainModule.show0(evaluator, targets, Target.log, interp.evalWatch0) { res =>
        res.flatMap(_._2) match {
          case Seq((k, singleValue)) => singleValue
          case multiple => ujson.Obj.from(multiple)
        }
      }
    }

  /**
   * Runs a given task and prints the results as JSON dictionary to stdout. This is useful
   * to integrate Mill into external scripts and tooling.
   */
  def showNamed(evaluator: Evaluator, targets: String*): Command[ujson.Value] =
    Task.Command(exclusive = true) {
      MainModule.show0(evaluator, targets, Target.log, interp.evalWatch0) { res =>
        ujson.Obj.from(res.flatMap(_._2))
      }
    }

  /**
   * Deletes the given targets from the out directory. Providing no targets
   * will clean everything.
   */
  def clean(evaluator: Evaluator, targets: String*): Command[Seq[PathRef]] =
    Task.Command(exclusive = true) {
      val rootDir = evaluator.outPath

      val KeepPattern = "(mill-.+)".r.anchored

      def keepPath(path: os.Path) = path.last match {
        case KeepPattern(_) => true
        case _ => false
      }

      val pathsToRemove =
        if (targets.isEmpty)
          Right((os.list(rootDir).filterNot(keepPath), List(mill.define.Segments())))
        else
          mill.resolve.Resolve.Segments.resolve(
            evaluator.rootModule,
            targets,
            SelectMode.Multi
          ).map { ts =>
            val allPaths = ts.flatMap { segments =>
              val evPaths = EvaluatorPaths.resolveDestPaths(rootDir, segments)
              val paths = Seq(evPaths.dest, evPaths.meta, evPaths.log)
              val potentialModulePath = rootDir / EvaluatorPaths.makeSegmentStrings(segments)
              if (os.exists(potentialModulePath)) {
                // this is either because of some pre-Mill-0.10 files lying around
                // or most likely because the segments denote a module but not a task
                // in which case we want to remove the module and all its sub-modules
                // (If this logic is later found to be too harsh, we could further guard it,
                // to when none of the other paths exists.)
                paths :+ potentialModulePath
              } else paths
            }
            (allPaths, ts)
          }

      pathsToRemove match {
        case Left(err) =>
          Result.Failure(err)
        case Right((paths, allSegments)) =>
          for {
            workerSegments <- evaluator.workerCache.keys.toList
            if allSegments.exists(workerSegments.startsWith)
            case (_, Val(closeable: AutoCloseable)) <-
              evaluator.mutableWorkerCache.remove(workerSegments)
          } {
            closeable.close()
          }

          val existing = paths.filter(p => os.exists(p))
          Target.log.debug(s"Cleaning ${existing.size} paths ...")
          existing.foreach(os.remove.all)
          Result.Success(existing.map(PathRef(_)))
      }
    }

  /**
   * Renders the dependencies between the given tasks as a SVG for you to look at
   */
  def visualize(evaluator: Evaluator, targets: String*): Command[Seq[PathRef]] =
    Task.Command(exclusive = true) {
      visualize0(evaluator, targets, Target.ctx(), mill.main.VisualizeModule.worker())
    }

  /**
   * Renders the dependencies between the given tasks, and all their dependencies, as a SVG
   */
  def visualizePlan(evaluator: Evaluator, targets: String*): Command[Seq[PathRef]] =
    Task.Command(exclusive = true) {
      plan0(evaluator, targets) match {
        case Left(err) => Result.Failure(err)
        case Right(planResults) => visualize0(
            evaluator,
            targets,
            Target.ctx(),
            mill.main.VisualizeModule.worker(),
            Some(planResults.toList.map(_.task))
          )
      }
    }

  /**
   * Shuts down mill's background server
   */
  def shutdown(): Command[Unit] = Task.Command(exclusive = true) {
    Target.log.info("Shutting down Mill server...")
    Target.ctx().systemExit(0)
    ()
  }

  /**
   * The `init` allows you to quickly generate a starter project.
   *
   * If you run it without arguments, it displays the list of available examples.
   *
   * If you pass one of listed examples, it downloads specified example from mill releases page and extracts it to working directory.
   *
   * If you pass a g8 template, it will generate a project based on a Giter8 template.
   * It prompts you to enter project name and creates a folder with that name.
   * There are lots of templates out there for many frameworks and tools!
   */
  def init(evaluator: Evaluator, args: String*): Command[ujson.Value] =
    Task.Command(exclusive = true) {
      val evaluated =
        if (os.exists(os.pwd / "pom.xml"))
          RunScript.evaluateTasksNamed(
            evaluator,
            Seq("mill.init.InitMavenModule/init") ++ args,
            SelectMode.Separated
          )
        else if (args.headOption.exists(_.toLowerCase.endsWith(".g8")))
          RunScript.evaluateTasksNamed(
            evaluator,
            Seq("mill.scalalib.giter8.Giter8Module/init") ++ args,
            SelectMode.Separated
          )
        else
          RunScript.evaluateTasksNamed(
            evaluator,
            Seq("mill.init.InitModule/init") ++ args,
            SelectMode.Separated
          )
      evaluated match {
        case Left(failStr) => throw new Exception(failStr)
        case Right((_, Right(Seq((_, Some((_, jsonableResult))))))) => jsonableResult
        case Right((_, Left(failStr))) => throw new Exception(failStr)
      }
    }

  private type VizWorker = (
      LinkedBlockingQueue[(scala.Seq[NamedTask[Any]], scala.Seq[NamedTask[Any]], os.Path)],
      LinkedBlockingQueue[Result[scala.Seq[PathRef]]]
  )

  private def visualize0(
      evaluator: Evaluator,
      targets: Seq[String],
      ctx: Ctx,
      vizWorker: VizWorker,
      planTasks: Option[List[NamedTask[_]]] = None
  ): Result[Seq[PathRef]] = {
    def callVisualizeModule(
        tasks: List[NamedTask[Any]],
        transitiveTasks: List[NamedTask[Any]]
    ): Result[Seq[PathRef]] = {
      val (in, out) = vizWorker
      in.put((tasks, transitiveTasks, ctx.dest))
      val res = out.take()
      res.map { v =>
        println(upickle.default.write(v.map(_.path.toString()), indent = 2))
        v
      }
    }

    Resolve.Tasks.resolve(
      evaluator.rootModule,
      targets,
      SelectMode.Multi
    ) match {
      case Left(err) => Result.Failure(err)
      case Right(rs) => planTasks match {
          case Some(allRs) => callVisualizeModule(rs, allRs)
          case None => callVisualizeModule(rs, rs)
        }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy