summaryrefslogtreecommitdiff
path: root/main/src
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-02-09 00:14:47 -0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-02-09 08:17:47 -0800
commit8ddd2fa054bc8639c28db2e95b7903e2954fdb7d (patch)
treeaa985f1e715f07eb279e6facad61de8a187e316c /main/src
parent90d0a3388d280554eaa51371f666d2f7a965a8af (diff)
downloadmill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.gz
mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.bz2
mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.zip
.
Diffstat (limited to 'main/src')
-rw-r--r--main/src/mill/Main.scala83
-rw-r--r--main/src/mill/main/MagicScopt.scala46
-rw-r--r--main/src/mill/main/MainModule.scala56
-rw-r--r--main/src/mill/main/MainRunner.scala120
-rw-r--r--main/src/mill/main/ReplApplyHandler.scala137
-rw-r--r--main/src/mill/main/Resolve.scala144
-rw-r--r--main/src/mill/main/RunScript.scala240
-rw-r--r--main/src/mill/modules/Jvm.scala257
-rw-r--r--main/src/mill/modules/Util.scala65
-rw-r--r--main/src/mill/package.scala12
10 files changed, 1160 insertions, 0 deletions
diff --git a/main/src/mill/Main.scala b/main/src/mill/Main.scala
new file mode 100644
index 00000000..3025994c
--- /dev/null
+++ b/main/src/mill/Main.scala
@@ -0,0 +1,83 @@
+package mill
+
+import ammonite.main.Cli.{formatBlock, genericSignature, replSignature}
+import ammonite.ops._
+import ammonite.util.Util
+
+object Main {
+ case class Config(home: ammonite.ops.Path = pwd/'out/'ammonite,
+ colored: Option[Boolean] = None,
+ help: Boolean = false,
+ repl: Boolean = false,
+ watch: Boolean = false)
+
+ def main(args: Array[String]): Unit = {
+
+ import ammonite.main.Cli
+
+ var show = false
+ val showCliArg = Cli.Arg[Cli.Config, Unit](
+ "show",
+ None,
+ "Display the json-formatted value of the given target, if any",
+ (x, _) => {
+ show = true
+ x
+ }
+ )
+ val removed = Set("predef-code", "home", "no-home-predef")
+ val millArgSignature = (Cli.genericSignature :+ showCliArg).filter(a => !removed(a.name))
+ Cli.groupArgs(
+ args.toList,
+ millArgSignature,
+ Cli.Config(remoteLogging = false)
+ ) match{
+ case Left(msg) =>
+ System.err.println(msg)
+ System.exit(1)
+ case Right((cliConfig, _)) if cliConfig.help =>
+ val leftMargin = millArgSignature.map(ammonite.main.Cli.showArg(_).length).max + 2
+ System.out.println(
+ s"""Mill Build Tool
+ |usage: mill [mill-options] [target [target-options]]
+ |
+ |${formatBlock(millArgSignature, leftMargin).mkString(Util.newLine)}""".stripMargin
+ )
+ System.exit(0)
+ case Right((cliConfig, leftoverArgs)) =>
+
+ val repl = leftoverArgs.isEmpty
+ val config =
+ if(!repl) cliConfig
+ else cliConfig.copy(
+ predefCode =
+ """import $file.build, build._
+ |implicit val replApplyHandler = mill.main.ReplApplyHandler(
+ | interp.colors(),
+ | repl.pprinter(),
+ | build.millSelf.get,
+ | build.millDiscover
+ |)
+ |repl.pprinter() = replApplyHandler.pprinter
+ |import replApplyHandler.generatedEval._
+ |
+ """.stripMargin,
+ welcomeBanner = None
+ )
+
+ val runner = new mill.main.MainRunner(
+ config, show,
+ System.out, System.err, System.in
+ )
+ if (repl){
+ runner.printInfo("Loading...")
+ runner.runRepl()
+ } else {
+ val result = runner.runScript(pwd / "build.sc", leftoverArgs)
+ System.exit(if(result) 0 else 1)
+ }
+ }
+ }
+}
+
+
diff --git a/main/src/mill/main/MagicScopt.scala b/main/src/mill/main/MagicScopt.scala
new file mode 100644
index 00000000..e18816c8
--- /dev/null
+++ b/main/src/mill/main/MagicScopt.scala
@@ -0,0 +1,46 @@
+package mill.main
+import mill.define.ExternalModule
+import mill.eval.{Evaluator, PathRef}
+import mill.util.ParseArgs
+
+object MagicScopt{
+
+
+ case class Tasks[T](value: Seq[mill.define.NamedTask[T]])
+}
+class EvaluatorScopt[T]()
+ extends scopt.Read[mill.eval.Evaluator[T]]{
+ def arity = 0
+ def reads = s => try{
+ Evaluator.currentEvaluator.get.asInstanceOf[mill.eval.Evaluator[T]]
+ }
+}
+class TargetScopt[T]()
+ extends scopt.Read[MagicScopt.Tasks[T]]{
+ def arity = 0
+ def reads = s => {
+ val rootModule = Evaluator.currentEvaluator.get.rootModule
+ val d = rootModule.millDiscover
+ val (expanded, leftover) = ParseArgs(Seq(s)).fold(e => throw new Exception(e), identity)
+ val resolved = expanded.map{
+ case (Some(scoping), segments) =>
+ val moduleCls = rootModule.getClass.getClassLoader.loadClass(scoping.render + "$")
+ val externalRootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule]
+ val crossSelectors = segments.value.map {
+ case mill.define.Segment.Cross(x) => x.toList.map(_.toString)
+ case _ => Nil
+ }
+ mill.main.Resolve.resolve(segments.value.toList, externalRootModule, d, leftover, crossSelectors.toList, Nil)
+ case (None, segments) =>
+ val crossSelectors = segments.value.map {
+ case mill.define.Segment.Cross(x) => x.toList.map(_.toString)
+ case _ => Nil
+ }
+ mill.main.Resolve.resolve(segments.value.toList, rootModule, d, leftover, crossSelectors.toList, Nil)
+ }
+ mill.util.EitherOps.sequence(resolved) match{
+ case Left(s) => throw new Exception(s)
+ case Right(ts) => MagicScopt.Tasks(ts.flatten).asInstanceOf[MagicScopt.Tasks[T]]
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala
new file mode 100644
index 00000000..012ecce5
--- /dev/null
+++ b/main/src/mill/main/MainModule.scala
@@ -0,0 +1,56 @@
+package mill.main
+
+import mill.util
+import mill.main.RunScript
+import mill.util.Watched
+import pprint.{Renderer, Truncated}
+
+trait MainModule extends mill.Module{
+
+ implicit def millDiscover: mill.define.Discover[_]
+ implicit def millScoptTargetReads[T] = new mill.main.TargetScopt[T]()
+ implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]()
+ mill.define.Ctx.make
+ def resolve(targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ targets.flatMap(_.value).foreach(println)
+ }
+ def describe(evaluator: mill.eval.Evaluator[Any],
+ targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ for{
+ t <- targets
+ target <- t.value
+ tree = ReplApplyHandler.pprintTask(target, evaluator)
+ val defaults = pprint.PPrinter()
+ val renderer = new Renderer(
+ defaults.defaultWidth,
+ defaults.colorApplyPrefix,
+ defaults.colorLiteral,
+ defaults.defaultIndent
+ )
+ val rendered = renderer.rec(tree, 0, 0).iter
+ val truncated = new Truncated(rendered, defaults.defaultWidth, defaults.defaultHeight)
+ str <- truncated ++ Iterator("\n")
+ } {
+ print(str)
+ }
+ }
+ def all(evaluator: mill.eval.Evaluator[Any],
+ targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ val (watched, res) = RunScript.evaluate(
+ evaluator,
+ mill.util.Strict.Agg.from(targets.flatMap(_.value))
+ )
+ Watched((), watched)
+ }
+ def show(evaluator: mill.eval.Evaluator[Any],
+ targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ val (watched, res) = mill.main.RunScript.evaluate(
+ evaluator,
+ mill.util.Strict.Agg.from(targets.flatMap(_.value))
+ )
+ for(json <- res.right.get.flatMap(_._2)){
+ println(json)
+ }
+ Watched((), watched)
+ }
+}
diff --git a/main/src/mill/main/MainRunner.scala b/main/src/mill/main/MainRunner.scala
new file mode 100644
index 00000000..c073e583
--- /dev/null
+++ b/main/src/mill/main/MainRunner.scala
@@ -0,0 +1,120 @@
+package mill.main
+import java.io.{InputStream, OutputStream, PrintStream}
+
+import ammonite.Main
+import ammonite.interp.{Interpreter, Preprocessor}
+import ammonite.ops.Path
+import ammonite.util._
+import mill.define.Discover
+import mill.eval.{Evaluator, PathRef}
+import mill.util.PrintLogger
+import mill.main.RunScript
+import upickle.Js
+
+/**
+ * Customized version of [[ammonite.MainRunner]], allowing us to run Mill
+ * `build.sc` scripts with mill-specific tweaks such as a custom
+ * `scriptCodeWrapper` or with a persistent evaluator between runs.
+ */
+class MainRunner(config: ammonite.main.Cli.Config,
+ show: Boolean,
+ outprintStream: PrintStream,
+ errPrintStream: PrintStream,
+ stdIn: InputStream)
+ extends ammonite.MainRunner(
+ config, outprintStream, errPrintStream,
+ stdIn, outprintStream, errPrintStream
+ ){
+
+ var lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])] = None
+
+ override def runScript(scriptPath: Path, scriptArgs: List[String]) =
+ watchLoop(
+ isRepl = false,
+ printing = true,
+ mainCfg => {
+ val (result, interpWatched) = RunScript.runScript(
+ mainCfg.wd,
+ scriptPath,
+ mainCfg.instantiateInterpreter(),
+ scriptArgs,
+ lastEvaluator,
+ new PrintLogger(
+ colors != ammonite.util.Colors.BlackWhite,
+ colors,
+ if (show) errPrintStream else outprintStream,
+ errPrintStream,
+ errPrintStream
+ )
+ )
+
+ result match{
+ case Res.Success(data) =>
+ val (eval, evaluationWatches, res) = data
+
+ lastEvaluator = Some((interpWatched, eval))
+
+ (Res(res), interpWatched ++ evaluationWatches)
+ case _ => (result, interpWatched)
+ }
+ }
+ )
+
+ override def handleWatchRes[T](res: Res[T], printing: Boolean) = {
+ res match{
+ case Res.Success(value) =>
+// if (show){
+// for(json <- value.asInstanceOf[Seq[Js.Value]]){
+// outprintStream.println(json)
+// }
+// }
+
+ true
+
+ case _ => super.handleWatchRes(res, printing)
+ }
+
+ }
+ override def initMain(isRepl: Boolean) = {
+ super.initMain(isRepl).copy(
+ scriptCodeWrapper = CustomCodeWrapper,
+ // Ammonite does not properly forward the wd from CliConfig to Main, so
+ // force forward it outselves
+ wd = config.wd
+ )
+ }
+ object CustomCodeWrapper extends Preprocessor.CodeWrapper {
+ def top(pkgName: Seq[Name], imports: Imports, indexedWrapperName: Name) = {
+ val wrapName = indexedWrapperName.backticked
+ val literalPath = pprint.Util.literalize(config.wd.toString)
+ s"""
+ |package ${pkgName.head.encoded}
+ |package ${Util.encodeScalaSourcePath(pkgName.tail)}
+ |$imports
+ |import mill._
+ |object $wrapName
+ |extends mill.define.BaseModule(ammonite.ops.Path($literalPath))
+ |with $wrapName{
+ | // Stub to make sure Ammonite has something to call after it evaluates a script,
+ | // even if it does nothing...
+ | def $$main() = Iterator[String]()
+ |
+ | implicit def millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type]
+ | // Need to wrap the returned Module in Some(...) to make sure it
+ | // doesn't get picked up during reflective child-module discovery
+ | val millSelf = Some(this)
+ |}
+ |
+ |sealed trait $wrapName extends mill.main.MainModule{
+ |""".stripMargin
+ }
+
+
+ def bottom(printCode: String, indexedWrapperName: Name, extraCode: String) = {
+ // We need to disable the `$main` method definition inside the wrapper class,
+ // because otherwise it might get picked up by Ammonite and run as a static
+ // class method, which blows up since it's defined as an instance method
+ "\n}"
+ }
+ }
+}
diff --git a/main/src/mill/main/ReplApplyHandler.scala b/main/src/mill/main/ReplApplyHandler.scala
new file mode 100644
index 00000000..76ef354f
--- /dev/null
+++ b/main/src/mill/main/ReplApplyHandler.scala
@@ -0,0 +1,137 @@
+package mill.main
+
+
+import mill.define.Applicative.ApplyHandler
+import mill.define.Segment.Label
+import mill.define._
+import mill.eval.{Evaluator, Result}
+import mill.util.Strict.Agg
+
+import scala.collection.mutable
+object ReplApplyHandler{
+ def apply[T](colors: ammonite.util.Colors,
+ pprinter0: pprint.PPrinter,
+ rootModule: mill.define.BaseModule,
+ discover: Discover[_]) = {
+ new ReplApplyHandler(
+ pprinter0,
+ new Evaluator(
+ ammonite.ops.pwd / 'out,
+ ammonite.ops.pwd / 'out,
+ rootModule,
+ discover,
+ new mill.util.PrintLogger(
+ colors != ammonite.util.Colors.BlackWhite,
+ colors,
+ System.out,
+ System.err,
+ System.err
+ )
+ )
+ )
+ }
+ def pprintCross(c: mill.define.Cross[_], evaluator: Evaluator[_]) = {
+ pprint.Tree.Lazy( ctx =>
+ Iterator(c.millOuterCtx.enclosing , ":", c.millOuterCtx.lineNum.toString, ctx.applyPrefixColor("\nChildren:").toString) ++
+ c.items.iterator.map(x =>
+ "\n (" + x._1.map(pprint.PPrinter.BlackWhite.apply(_)).mkString(", ") + ")"
+ )
+ )
+ }
+ def pprintModule(m: mill.define.Module, evaluator: Evaluator[_]) = {
+ pprint.Tree.Lazy( ctx =>
+ Iterator(m.millInternal.millModuleEnclosing, ":", m.millInternal.millModuleLine.toString) ++
+ (if (m.millInternal.reflect[mill.Module].isEmpty) Nil
+ else
+ ctx.applyPrefixColor("\nChildren:").toString +:
+ m.millInternal.reflect[mill.Module].map("\n ." + _.millOuterCtx.segment.pathSegments.mkString("."))) ++
+ (evaluator.discover.value.get(m.getClass) match{
+ case None => Nil
+ case Some(commands) =>
+ ctx.applyPrefixColor("\nCommands:").toString +: commands.map{c =>
+ "\n ." + c._2.name + "(" +
+ c._2.argSignatures.map(s => s.name + ": " + s.typeString).mkString(", ") +
+ ")()"
+ }
+ }) ++
+ (if (m.millInternal.reflect[Target[_]].isEmpty) Nil
+ else {
+ Seq(ctx.applyPrefixColor("\nTargets:").toString) ++
+ m.millInternal.reflect[Target[_]].sortBy(_.label).map(t =>
+ "\n ." + t.label + "()"
+ )
+ })
+
+ )
+ }
+ def pprintTask(t: NamedTask[_], evaluator: Evaluator[_]) = {
+ val seen = mutable.Set.empty[Task[_]]
+ def rec(t: Task[_]): Seq[Segments] = {
+ if (seen(t)) Nil // do nothing
+ else t match {
+ case t: Target[_] if evaluator.rootModule.millInternal.targets.contains(t) =>
+ Seq(t.ctx.segments)
+ case _ =>
+ seen.add(t)
+ t.inputs.flatMap(rec)
+ }
+ }
+ pprint.Tree.Lazy(ctx =>
+ Iterator(
+ t.toString, "(", t.ctx.fileName, ":", t.ctx.lineNum.toString, ")",
+ t.ctx.lineNum.toString, "\n", ctx.applyPrefixColor("Inputs:").toString
+ ) ++ t.inputs.iterator.flatMap(rec).map("\n " + _.render)
+ )
+ }
+
+}
+class ReplApplyHandler(pprinter0: pprint.PPrinter,
+ evaluator: Evaluator[_]) extends ApplyHandler[Task] {
+ // Evaluate classLoaderSig only once in the REPL to avoid busting caches
+ // as the user enters more REPL commands and changes the classpath
+ val classLoaderSig = Evaluator.classLoaderSig
+ override def apply[V](t: Task[V]) = {
+ val res = evaluator.evaluate(Agg(t))
+ res.values match{
+ case Seq(head: V) => head
+ case Nil =>
+ val msg = new mutable.StringBuilder()
+ msg.append(res.failing.keyCount + " targets failed\n")
+ for((k, vs) <- res.failing.items){
+ msg.append(k match{
+ case Left(t) => "Anonymous Task\n"
+ case Right(k) => k.segments.render + "\n"
+ })
+
+ for(v <- vs){
+ v match{
+ case Result.Failure(m, _) => msg.append(m + "\n")
+ case Result.Exception(t, outerStack) =>
+ msg.append(
+ t.toString +
+ t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString +
+ "\n"
+ )
+
+ }
+ }
+ }
+ throw new Exception(msg.toString)
+ }
+ }
+
+ val generatedEval = new EvalGenerated(evaluator)
+
+ val millHandlers: PartialFunction[Any, pprint.Tree] = {
+ case c: Cross[_] =>
+ ReplApplyHandler.pprintCross(c, evaluator)
+ case m: mill.Module if evaluator.rootModule.millInternal.modules.contains(m) =>
+ ReplApplyHandler.pprintModule(m, evaluator)
+ case t: mill.define.Target[_] if evaluator.rootModule.millInternal.targets.contains(t) =>
+ ReplApplyHandler.pprintTask(t, evaluator)
+
+ }
+ val pprinter = pprinter0.copy(
+ additionalHandlers = millHandlers orElse pprinter0.additionalHandlers
+ )
+}
diff --git a/main/src/mill/main/Resolve.scala b/main/src/mill/main/Resolve.scala
new file mode 100644
index 00000000..c134d70d
--- /dev/null
+++ b/main/src/mill/main/Resolve.scala
@@ -0,0 +1,144 @@
+package mill.main
+
+import mill.define._
+import mill.define.TaskModule
+import ammonite.util.Res
+import mill.util.Router.EntryPoint
+import mill.util.Scripts
+
+object Resolve {
+ def resolve[T, V](remainingSelector: List[Segment],
+ obj: mill.Module,
+ discover: Discover[_],
+ rest: Seq[String],
+ remainingCrossSelectors: List[List[String]],
+ revSelectorsSoFar: List[Segment]): Either[String, Seq[NamedTask[Any]]] = {
+
+ remainingSelector match{
+ case Segment.Cross(_) :: Nil => Left("Selector cannot start with a [cross] segment")
+ case Segment.Label(last) :: Nil =>
+ val target =
+ obj
+ .millInternal
+ .reflect[Target[_]]
+ .find(_.label == last)
+ .map(Right(_))
+
+ def shimArgsig[T](a: mill.util.Router.ArgSig[T, _]) = {
+ ammonite.main.Router.ArgSig[T](
+ a.name,
+ a.typeString,
+ a.doc,
+ a.default
+ )
+ }
+ def invokeCommand(target: mill.Module, name: String) = for{
+ (cls, entryPoints) <- discover.value
+ if cls.isAssignableFrom(target.getClass)
+ ep <- entryPoints
+ if ep._2.name == name
+ } yield Scripts.runMainMethod(
+ target,
+ ep._2.asInstanceOf[EntryPoint[mill.Module]],
+ ammonite.main.Scripts.groupArgs(rest.toList)
+ ) match{
+ case Res.Success(v: Command[_]) => Right(v)
+ case Res.Failure(msg) => Left(msg)
+ case Res.Exception(ex, msg) =>
+ val sw = new java.io.StringWriter()
+ ex.printStackTrace(new java.io.PrintWriter(sw))
+ val prefix = if (msg.nonEmpty) msg + "\n" else msg
+ Left(prefix + sw.toString)
+
+ }
+
+ val runDefault = for{
+ child <- obj.millInternal.reflectNestedObjects[mill.Module]
+ if child.millOuterCtx.segment == Segment.Label(last)
+ res <- child match{
+ case taskMod: TaskModule => Some(invokeCommand(child, taskMod.defaultCommandName()).headOption)
+ case _ => None
+ }
+ } yield res
+
+ val command = invokeCommand(obj, last).headOption
+
+ command orElse target orElse runDefault.flatten.headOption match{
+ case None => Left("Cannot resolve task " +
+ Segments((Segment.Label(last) :: revSelectorsSoFar).reverse:_*).render
+ )
+ // Contents of `either` *must* be a `Task`, because we only select
+ // methods returning `Task` in the discovery process
+ case Some(either) => either.right.map(Seq(_))
+ }
+
+
+ case head :: tail =>
+ val newRevSelectorsSoFar = head :: revSelectorsSoFar
+ head match{
+ case Segment.Label(singleLabel) =>
+ if (singleLabel == "__") {
+
+ val matching =
+ obj.millInternal.modules
+ .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar))
+ .collect{case Right(vs) => vs}.flatten
+
+ if (matching.nonEmpty)Right(matching)
+ else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ } else if (singleLabel == "_") {
+
+ val matching =
+ obj.millModuleDirectChildren
+ .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar))
+ .collect{case Right(vs) => vs}.flatten
+
+ if (matching.nonEmpty)Right(matching)
+ else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }else{
+
+ obj.millInternal.reflectNestedObjects[mill.Module].find{
+ _.millOuterCtx.segment == Segment.Label(singleLabel)
+ } match{
+ case Some(child: mill.Module) => resolve(tail, child, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+ case None => Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }
+ }
+
+ case Segment.Cross(cross) =>
+ obj match{
+ case c: Cross[_] =>
+ if(cross == Seq("__")){
+ val matching =
+ for ((k, v) <- c.items)
+ yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+
+ val results = matching.collect{case Right(res) => res}.flatten
+
+ if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ else Right(results)
+ } else if (cross.contains("_")){
+ val matching = for {
+ (k, v) <- c.items
+ if k.length == cross.length
+ if k.zip(cross).forall { case (l, r) => l == r || r == "_" }
+ } yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+
+ val results = matching.collect{case Right(res) => res}.flatten
+
+ if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ else Right(results)
+ }else{
+ c.itemMap.get(cross.toList) match{
+ case Some(m: mill.Module) => resolve(tail, m, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+ case None => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }
+ }
+ case _ => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }
+ }
+
+ case Nil => Left("Selector cannot be empty")
+ }
+ }
+}
diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala
new file mode 100644
index 00000000..bd21a821
--- /dev/null
+++ b/main/src/mill/main/RunScript.scala
@@ -0,0 +1,240 @@
+package mill.main
+
+import java.nio.file.NoSuchFileException
+
+import ammonite.interp.Interpreter
+import ammonite.ops.{Path, read}
+import ammonite.runtime.SpecialClassLoader
+import ammonite.util.Util.CodeSource
+import ammonite.util.{Name, Res, Util}
+import mill.define
+import mill.define.{Discover, ExternalModule, Segment, Task}
+import mill.eval.{Evaluator, PathRef, Result}
+import mill.util.{EitherOps, Logger, ParseArgs, Watched}
+import mill.util.Strict.Agg
+import upickle.Js
+
+/**
+ * Custom version of ammonite.main.Scripts, letting us run the build.sc script
+ * directly without going through Ammonite's main-method/argument-parsing
+ * subsystem
+ */
+object RunScript{
+ def runScript(wd: Path,
+ path: Path,
+ instantiateInterpreter: => Either[(Res.Failing, Seq[(Path, Long)]), ammonite.interp.Interpreter],
+ scriptArgs: Seq[String],
+ lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])],
+ log: Logger)
+ : (Res[(Evaluator[Any], Seq[(Path, Long)], Either[String, Seq[Js.Value]])], Seq[(Path, Long)]) = {
+
+ val (evalRes, interpWatched) = lastEvaluator match{
+ case Some((prevInterpWatchedSig, prevEvaluator))
+ if watchedSigUnchanged(prevInterpWatchedSig) =>
+
+ (Res.Success(prevEvaluator), prevInterpWatchedSig)
+
+ case _ =>
+ instantiateInterpreter match{
+ case Left((res, watched)) => (res, watched)
+ case Right(interp) =>
+ interp.watch(path)
+ val eval =
+ for((mapping, discover) <- evaluateMapping(wd, path, interp))
+ yield new Evaluator[Any](
+ wd / 'out, wd / 'out, mapping, discover, log,
+ mapping.getClass.getClassLoader.asInstanceOf[SpecialClassLoader].classpathSignature
+ )
+
+ (eval, interp.watchedFiles)
+ }
+ }
+
+ val evaluated = for{
+ evaluator <- evalRes
+ (evalWatches, res) <- Res(evaluateTarget(evaluator, scriptArgs))
+ } yield {
+ val alreadyStale = evalWatches.exists(p => p.sig != new PathRef(p.path, p.quick).sig)
+ // If the file changed between the creation of the original
+ // `PathRef` and the current moment, use random junk .sig values
+ // to force an immediate re-run. Otherwise calculate the
+ // pathSignatures the same way Ammonite would and hand over the
+ // values, so Ammonite can watch them and only re-run if they
+ // subsequently change
+ val evaluationWatches =
+ if (alreadyStale) evalWatches.map(_.path -> util.Random.nextLong())
+ else evalWatches.map(p => p.path -> Interpreter.pathSignature(p.path))
+
+ (evaluator, evaluationWatches, res.map(_.flatMap(_._2)))
+ }
+ (evaluated, interpWatched)
+ }
+
+ def watchedSigUnchanged(sig: Seq[(Path, Long)]) = {
+ sig.forall{case (p, l) => Interpreter.pathSignature(p) == l}
+ }
+
+ def evaluateMapping(wd: Path,
+ path: Path,
+ interp: ammonite.interp.Interpreter): Res[(mill.define.BaseModule, Discover[Any])] = {
+
+ val (pkg, wrapper) = Util.pathToPackageWrapper(Seq(), path relativeTo wd)
+
+ for {
+ scriptTxt <-
+ try Res.Success(Util.normalizeNewlines(read(path)))
+ catch { case e: NoSuchFileException => Res.Failure("Script file not found: " + path) }
+
+ processed <- interp.processModule(
+ scriptTxt,
+ CodeSource(wrapper, pkg, Seq(Name("ammonite"), Name("$file")), Some(path)),
+ autoImport = true,
+ extraCode = "",
+ hardcoded = true
+ )
+
+ buildClsName <- processed.blockInfo.lastOption match {
+ case Some(meta) => Res.Success(meta.id.wrapperPath)
+ case None => Res.Skip
+ }
+
+ buildCls = interp
+ .evalClassloader
+ .loadClass(buildClsName)
+
+ module <- try {
+ Util.withContextClassloader(interp.evalClassloader) {
+ Res.Success(
+ buildCls.getMethod("millSelf")
+ .invoke(null)
+ .asInstanceOf[Some[mill.define.BaseModule]]
+ .get
+ )
+ }
+ } catch {
+ case e: Throwable => Res.Exception(e, "")
+ }
+ discover <- try {
+ Util.withContextClassloader(interp.evalClassloader) {
+ Res.Success(
+ buildCls.getMethod("millDiscover")
+ .invoke(module)
+ .asInstanceOf[Discover[Any]]
+ )
+ }
+ } catch {
+ case e: Throwable => Res.Exception(e, "")
+ }
+// _ <- Res(consistencyCheck(mapping))
+ } yield (module, discover)
+ }
+
+ def evaluateTarget[T](evaluator: Evaluator[T], scriptArgs: Seq[String]) = {
+ for {
+ parsed <- ParseArgs(scriptArgs)
+ (selectors, args) = parsed
+ targets <- {
+ val selected = selectors.map { case (scopedSel, sel) =>
+ val (rootModule, discover) = scopedSel match{
+ case None => (evaluator.rootModule, evaluator.discover)
+ case Some(scoping) =>
+ val moduleCls =
+ evaluator.rootModule.getClass.getClassLoader.loadClass(scoping.render + "$")
+
+ val rootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule]
+ (rootModule, rootModule.millDiscover)
+ }
+ val crossSelectors = sel.value.map {
+ case Segment.Cross(x) => x.toList.map(_.toString)
+ case _ => Nil
+ }
+
+ try {
+ // We inject the `evaluator.rootModule` into the TargetScopt, rather
+ // than the `rootModule`, because even if you are running an external
+ // module we still want you to be able to resolve targets from your
+ // main build. Resolving targets from external builds as CLI arguments
+ // is not currently supported
+ mill.eval.Evaluator.currentEvaluator.set(evaluator)
+ mill.main.Resolve.resolve(
+ sel.value.toList, rootModule,
+ discover,
+ args, crossSelectors.toList, Nil
+ )
+ } finally{
+ mill.eval.Evaluator.currentEvaluator.set(null)
+ }
+ }
+ EitherOps.sequence(selected)
+ }
+ } yield {
+ val (watched, res) = evaluate(
+ evaluator,
+ Agg.from(targets.flatten.distinct)
+ )
+
+ val watched2 = for{
+ x <- res.right.toSeq
+ (Watched(_, extraWatched), _) <- x
+ w <- extraWatched
+ } yield w
+
+ (watched ++ watched2, res)
+ }
+ }
+
+ def evaluate(evaluator: Evaluator[_],
+ targets: Agg[Task[Any]]): (Seq[PathRef], Either[String, Seq[(Any, Option[upickle.Js.Value])]]) = {
+ val evaluated = evaluator.evaluate(targets)
+ val watched = evaluated.results
+ .iterator
+ .collect {
+ case (t: define.Sources, Result.Success(p: Seq[PathRef])) => p
+ }
+ .flatten
+ .toSeq
+
+ val errorStr =
+ (for((k, fs) <- evaluated.failing.items()) yield {
+ val ks = k match{
+ case Left(t) => t.toString
+ case Right(t) => t.segments.render
+ }
+ val fss = fs.map{
+ case Result.Exception(t, outerStack) =>
+ t.toString +
+ t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString
+ case Result.Failure(t, _) => t
+ }
+ s"$ks ${fss.mkString(", ")}"
+ }).mkString("\n")
+
+ evaluated.failing.keyCount match {
+ case 0 =>
+ val json = for(t <- targets.toSeq) yield {
+ t match {
+ case t: mill.define.NamedTask[_] =>
+ val jsonFile = Evaluator
+ .resolveDestPaths(evaluator.outPath, t.ctx.segments)
+ .meta
+ val metadata = upickle.json.read(jsonFile.toIO)
+ Some(metadata(1))
+
+ case _ => None
+ }
+ }
+
+ watched -> Right(evaluated.values.zip(json))
+ case n => watched -> Left(s"$n targets failed\n$errorStr")
+ }
+ }
+
+// def consistencyCheck[T](mapping: Discovered.Mapping[T]): Either[String, Unit] = {
+// val consistencyErrors = Discovered.consistencyCheck(mapping)
+// if (consistencyErrors.nonEmpty) {
+// Left(s"Failed Discovered.consistencyCheck: ${consistencyErrors.map(_.render)}")
+// } else {
+// Right(())
+// }
+// }
+}
diff --git a/main/src/mill/modules/Jvm.scala b/main/src/mill/modules/Jvm.scala
new file mode 100644
index 00000000..297dcf1f
--- /dev/null
+++ b/main/src/mill/modules/Jvm.scala
@@ -0,0 +1,257 @@
+package mill.modules
+
+import java.io.FileOutputStream
+import java.lang.reflect.Modifier
+import java.net.URLClassLoader
+import java.nio.file.attribute.PosixFilePermission
+import java.util.jar.{JarEntry, JarFile, JarOutputStream}
+
+import ammonite.ops._
+import mill.define.Task
+import mill.eval.PathRef
+import mill.util.{Ctx, Loose}
+import mill.util.Ctx.Log
+import mill.util.Loose.Agg
+import upickle.default.{Reader, Writer}
+
+import scala.annotation.tailrec
+import scala.collection.mutable
+import scala.reflect.ClassTag
+
+
+object Jvm {
+
+ def interactiveSubprocess(mainClass: String,
+ classPath: Agg[Path],
+ jvmArgs: Seq[String] = Seq.empty,
+ envArgs: Map[String, String] = Map.empty,
+ mainArgs: Seq[String] = Seq.empty,
+ workingDir: Path = null): Unit = {
+ import ammonite.ops.ImplicitWd._
+ val commandArgs =
+ Vector("java") ++
+ jvmArgs ++
+ Vector("-cp", classPath.mkString(":"), mainClass) ++
+ mainArgs
+
+ %.copy(envArgs = envArgs)(commandArgs)(workingDir)
+ }
+
+ def runLocal(mainClass: String,
+ classPath: Agg[Path],
+ mainArgs: Seq[String] = Seq.empty)
+ (implicit ctx: Ctx): Unit = {
+ inprocess(classPath, classLoaderOverrideSbtTesting = false, cl => {
+ getMainMethod(mainClass, cl).invoke(null, mainArgs.toArray)
+ })
+ }
+
+ private def getMainMethod(mainClassName: String, cl: ClassLoader) = {
+ val mainClass = cl.loadClass(mainClassName)
+ val method = mainClass.getMethod("main", classOf[Array[String]])
+ // jvm allows the actual main class to be non-public and to run a method in the non-public class,
+ // we need to make it accessible
+ method.setAccessible(true)
+ val modifiers = method.getModifiers
+ if (!Modifier.isPublic(modifiers))
+ throw new NoSuchMethodException(mainClassName + ".main is not public")
+ if (!Modifier.isStatic(modifiers))
+ throw new NoSuchMethodException(mainClassName + ".main is not static")
+ method
+ }
+
+
+
+ def inprocess[T](classPath: Agg[Path],
+ classLoaderOverrideSbtTesting: Boolean,
+ body: ClassLoader => T): T = {
+ val cl = if (classLoaderOverrideSbtTesting) {
+ val outerClassLoader = getClass.getClassLoader
+ new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null){
+ override def findClass(name: String) = {
+ if (name.startsWith("sbt.testing.")){
+ outerClassLoader.loadClass(name)
+ }else{
+ super.findClass(name)
+ }
+ }
+ }
+ } else {
+ new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null)
+ }
+ val oldCl = Thread.currentThread().getContextClassLoader
+ Thread.currentThread().setContextClassLoader(cl)
+ try {
+ body(cl)
+ }finally{
+ Thread.currentThread().setContextClassLoader(oldCl)
+ cl.close()
+ }
+ }
+
+ def subprocess(mainClass: String,
+ classPath: Agg[Path],
+ jvmArgs: Seq[String] = Seq.empty,
+ envArgs: Map[String, String] = Map.empty,
+ mainArgs: Seq[String] = Seq.empty,
+ workingDir: Path = null)
+ (implicit ctx: Ctx) = {
+
+ val commandArgs =
+ Vector("java") ++
+ jvmArgs ++
+ Vector("-cp", classPath.mkString(":"), mainClass) ++
+ mainArgs
+
+ val workingDir1 = Option(workingDir).getOrElse(ctx.dest)
+ mkdir(workingDir1)
+ val builder =
+ new java.lang.ProcessBuilder()
+ .directory(workingDir1.toIO)
+ .command(commandArgs:_*)
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .redirectError(ProcessBuilder.Redirect.PIPE)
+
+ for((k, v) <- envArgs) builder.environment().put(k, v)
+ val proc = builder.start()
+ val stdout = proc.getInputStream
+ val stderr = proc.getErrorStream
+ val sources = Seq(
+ (stdout, Left(_: Bytes), ctx.log.outputStream),
+ (stderr, Right(_: Bytes),ctx.log.errorStream )
+ )
+ val chunks = mutable.Buffer.empty[Either[Bytes, Bytes]]
+ while(
+ // Process.isAlive doesn't exist on JDK 7 =/
+ util.Try(proc.exitValue).isFailure ||
+ stdout.available() > 0 ||
+ stderr.available() > 0
+ ){
+ var readSomething = false
+ for ((subStream, wrapper, parentStream) <- sources){
+ while (subStream.available() > 0){
+ readSomething = true
+ val array = new Array[Byte](subStream.available())
+ val actuallyRead = subStream.read(array)
+ chunks.append(wrapper(new ammonite.ops.Bytes(array)))
+ parentStream.write(array, 0, actuallyRead)
+ }
+ }
+ // if we did not read anything sleep briefly to avoid spinning
+ if(!readSomething)
+ Thread.sleep(2)
+ }
+
+ if (proc.exitValue() != 0) throw new InteractiveShelloutException()
+ else ammonite.ops.CommandResult(proc.exitValue(), chunks)
+ }
+
+ private def createManifest(mainClass: Option[String]) = {
+ val m = new java.util.jar.Manifest()
+ m.getMainAttributes.put(java.util.jar.Attributes.Name.MANIFEST_VERSION, "1.0")
+ m.getMainAttributes.putValue( "Created-By", "Scala mill" )
+ mainClass.foreach(
+ m.getMainAttributes.put(java.util.jar.Attributes.Name.MAIN_CLASS, _)
+ )
+ m
+ }
+
+ def createJar(inputPaths: Agg[Path], mainClass: Option[String] = None)
+ (implicit ctx: Ctx.Dest): PathRef = {
+ val outputPath = ctx.dest / "out.jar"
+ rm(outputPath)
+
+ val seen = mutable.Set.empty[RelPath]
+ seen.add("META-INF" / "MANIFEST.MF")
+ val jar = new JarOutputStream(
+ new FileOutputStream(outputPath.toIO),
+ createManifest(mainClass)
+ )
+
+ try{
+ assert(inputPaths.forall(exists(_)))
+ for{
+ p <- inputPaths
+ (file, mapping) <-
+ if (p.isFile) Iterator(p -> empty/p.last)
+ else ls.rec(p).filter(_.isFile).map(sub => sub -> sub.relativeTo(p))
+ if !seen(mapping)
+ } {
+ seen.add(mapping)
+ val entry = new JarEntry(mapping.toString)
+ entry.setTime(file.mtime.toMillis)
+ jar.putNextEntry(entry)
+ jar.write(read.bytes(file))
+ jar.closeEntry()
+ }
+ } finally {
+ jar.close()
+ }
+
+ PathRef(outputPath)
+ }
+
+ def createAssembly(inputPaths: Agg[Path],
+ mainClass: Option[String] = None,
+ prependShellScript: String = "")
+ (implicit ctx: Ctx.Dest): PathRef = {
+ val outputPath = ctx.dest / "out.jar"
+ rm(outputPath)
+
+ if(inputPaths.nonEmpty) {
+
+ val output = new FileOutputStream(outputPath.toIO)
+
+ // Prepend shell script and make it executable
+ if (prependShellScript.nonEmpty) {
+ output.write((prependShellScript + "\n").getBytes)
+ val perms = java.nio.file.Files.getPosixFilePermissions(outputPath.toNIO)
+ perms.add(PosixFilePermission.GROUP_EXECUTE)
+ perms.add(PosixFilePermission.OWNER_EXECUTE)
+ perms.add(PosixFilePermission.OTHERS_EXECUTE)
+ java.nio.file.Files.setPosixFilePermissions(outputPath.toNIO, perms)
+ }
+
+ val jar = new JarOutputStream(
+ output,
+ createManifest(mainClass)
+ )
+
+ val seen = mutable.Set("META-INF/MANIFEST.MF")
+ try{
+
+
+ for{
+ p <- inputPaths
+ if exists(p)
+ (file, mapping) <-
+ if (p.isFile) {
+ val jf = new JarFile(p.toIO)
+ import collection.JavaConverters._
+ for(entry <- jf.entries().asScala if !entry.isDirectory) yield {
+ read.bytes(jf.getInputStream(entry)) -> entry.getName
+ }
+ }
+ else {
+ ls.rec(p).iterator
+ .filter(_.isFile)
+ .map(sub => read.bytes(sub) -> sub.relativeTo(p).toString)
+ }
+ if !seen(mapping)
+ } {
+ seen.add(mapping)
+ val entry = new JarEntry(mapping.toString)
+ jar.putNextEntry(entry)
+ jar.write(file)
+ jar.closeEntry()
+ }
+ } finally {
+ jar.close()
+ output.close()
+ }
+
+ }
+ PathRef(outputPath)
+ }
+
+}
diff --git a/main/src/mill/modules/Util.scala b/main/src/mill/modules/Util.scala
new file mode 100644
index 00000000..cef11859
--- /dev/null
+++ b/main/src/mill/modules/Util.scala
@@ -0,0 +1,65 @@
+package mill.modules
+
+
+import ammonite.ops.{Path, RelPath, empty, mkdir, read}
+import mill.eval.PathRef
+import mill.util.Ctx
+
+object Util {
+ def download(url: String, dest: RelPath = "download")(implicit ctx: Ctx.Dest) = {
+ val out = ctx.dest / dest
+
+ val website = new java.net.URI(url).toURL
+ val rbc = java.nio.channels.Channels.newChannel(website.openStream)
+ try{
+ val fos = new java.io.FileOutputStream(out.toIO)
+ try{
+ fos.getChannel.transferFrom(rbc, 0, java.lang.Long.MAX_VALUE)
+ PathRef(out)
+ } finally{
+ fos.close()
+ }
+ } finally{
+ rbc.close()
+ }
+ }
+
+ def downloadUnpackZip(url: String, dest: RelPath = "unpacked")
+ (implicit ctx: Ctx.Dest) = {
+
+ val tmpName = if (dest == empty / "tmp.zip") "tmp2.zip" else "tmp.zip"
+ val downloaded = download(url, tmpName)
+ unpackZip(downloaded.path, dest)
+ }
+
+ def unpackZip(src: Path, dest: RelPath = "unpacked")
+ (implicit ctx: Ctx.Dest) = {
+
+ val byteStream = read.getInputStream(src)
+ val zipStream = new java.util.zip.ZipInputStream(byteStream)
+ while({
+ zipStream.getNextEntry match{
+ case null => false
+ case entry =>
+ if (!entry.isDirectory) {
+ val entryDest = ctx.dest / dest / RelPath(entry.getName)
+ mkdir(entryDest / ammonite.ops.up)
+ val fileOut = new java.io.FileOutputStream(entryDest.toString)
+ val buffer = new Array[Byte](4096)
+ while ( {
+ zipStream.read(buffer) match {
+ case -1 => false
+ case n =>
+ fileOut.write(buffer, 0, n)
+ true
+ }
+ }) ()
+ fileOut.close()
+ }
+ zipStream.closeEntry()
+ true
+ }
+ })()
+ PathRef(ctx.dest / dest)
+ }
+}
diff --git a/main/src/mill/package.scala b/main/src/mill/package.scala
new file mode 100644
index 00000000..93916c8b
--- /dev/null
+++ b/main/src/mill/package.scala
@@ -0,0 +1,12 @@
+import mill.util.JsonFormatters
+
+package object mill extends JsonFormatters{
+ val T = define.Target
+ type T[T] = define.Target[T]
+ val PathRef = mill.eval.PathRef
+ type PathRef = mill.eval.PathRef
+ type Module = define.Module
+ type Cross[T] = define.Cross[T]
+ type Agg[T] = util.Loose.Agg[T]
+ val Agg = util.Loose.Agg
+}