diff --git a/docs/antora/modules/ROOT/pages/Intro_to_Mill.adoc b/docs/antora/modules/ROOT/pages/Intro_to_Mill.adoc index ae25cec0b3f..aee5db21165 100644 --- a/docs/antora/modules/ROOT/pages/Intro_to_Mill.adoc +++ b/docs/antora/modules/ROOT/pages/Intro_to_Mill.adoc @@ -660,6 +660,46 @@ $ mill show foo.compileClasspath `show` is also useful for interacting with Mill from external tools, since the JSON it outputs is structured and easily parsed and manipulated. +When `show` is used with multiple targets, its output will slightly change to a JSON array, containing all the results of the given targets. + +[source,bash] +---- +$ mill show "foo.{sources,compileClasspath}" +[1/1] show +[2/11] foo.resources +[ + [ + "ref:8befb7a8:/Users/lihaoyi/Dropbox/Github/test/foo/src" + ], + [ + "ref:c984eca8:/Users/lihaoyi/Dropbox/Github/test/foo/resources", + ".../org/scala-lang/scala-library/2.13.1/scala-library-2.13.1.jar" + ] +] +---- + +=== showNamed + +Same as `show`, but the output will always be structured in a JSON dictionary, with the task names as key and the task results as JSON values. + +[source,bash] +---- +$ mill showNamed "foo.{sources,compileClasspath}" +[1/1] show +[2/11] foo.resources +{ + "foo.sources": + [ + "ref:8befb7a8:/Users/lihaoyi/Dropbox/Github/test/foo/src" + ], + "foo.compileClasspath": + [ + "ref:c984eca8:/Users/lihaoyi/Dropbox/Github/test/foo/resources", + ".../org/scala-lang/scala-library/2.13.1/scala-library-2.13.1.jar" + ] +} +---- + === path [source,bash] diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index af89a12892e..6ed984a7e29 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -2,12 +2,13 @@ package mill.main import java.util.concurrent.LinkedBlockingQueue import mill.{BuildInfo, T} -import mill.api.{Ctx, PathRef, Result} +import mill.api.{Ctx, PathRef, Result, internal} import mill.define.{Command, NamedTask, Task} import mill.eval.{Evaluator, EvaluatorPaths} import mill.util.{PrintLogger, Watched} import mill.define.SelectMode import pprint.{Renderer, Truncated} +import ujson.Value object MainModule { @deprecated( @@ -56,6 +57,21 @@ object MainModule { Result.Success(Watched((), watched)) } } + + @internal + def evaluateTasksNamed[T]( + evaluator: Evaluator, + targets: Seq[String], + selectMode: SelectMode + )(f: Seq[(Any, Option[(RunScript.TaskName, ujson.Value)])] => T): Result[Watched[Option[T]]] = { + RunScript.evaluateTasksNamed(evaluator, targets, selectMode) match { + case Left(err) => Result.Failure(err) + case Right((watched, Left(err))) => Result.Failure(err, Some(Watched(None, watched))) + case Right((watched, Right(res))) => + val fRes = f(res) + Result.Success(Watched(Some(fRes), watched)) + } + } } trait MainModule extends mill.Module { @@ -235,8 +251,8 @@ trait MainModule extends mill.Module { * 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*) = T.command { - MainModule.evaluateTasks( + def show(evaluator: Evaluator, targets: String*): Command[Value] = T.command { + MainModule.evaluateTasksNamed( evaluator.withBaseLogger( // When using `show`, redirect all stdout of the evaluated tasks so the // printed JSON is the only thing printed to stdout. @@ -248,10 +264,44 @@ trait MainModule extends mill.Module { ), targets, SelectMode.Separated - ) { res => - for (json <- res.flatMap(_._2)) { - T.log.outputStream.println(json.render(indent = 4)) - } + ) { res: Seq[(Any, Option[(String, ujson.Value)])] => + val jsons = res.flatMap(_._2).map(_._2) + val output: ujson.Value = + if (jsons.size == 1) jsons.head + else { ujson.Arr.from(jsons) } + T.log.outputStream.println(output.render(indent = 2)) + output + }.map { res => + val Watched(Some(json), _) = res + json + } + } + + /** + * 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[Value] = T.command { + MainModule.evaluateTasksNamed( + evaluator.withBaseLogger( + // When using `show`, redirect all stdout of the evaluated tasks so the + // printed JSON is the only thing printed to stdout. + evaluator.baseLogger match { + case PrintLogger(c1, d, c2, c3, _, i, e, in, de, uc) => + PrintLogger(c1, d, c2, c3, e, i, e, in, de, uc) + case l => l + } + ), + targets, + SelectMode.Separated + ) { res: Seq[(Any, Option[(String, ujson.Value)])] => + val nameAndJson = res.flatMap(_._2) + val output: ujson.Value = ujson.Obj.from(nameAndJson) + T.log.outputStream.println(output.render(indent = 2)) + output + }.map { res => + val Watched(Some(json), _) = res + json } } diff --git a/main/src/mill/main/MainRunner.scala b/main/src/mill/main/MainRunner.scala index 9bad7f35f8a..63955c14cf5 100644 --- a/main/src/mill/main/MainRunner.scala +++ b/main/src/mill/main/MainRunner.scala @@ -49,7 +49,6 @@ class MainRunner( var stateCache = stateCache0 override def watchAndWait(watched: Seq[(ammonite.interp.Watchable, Long)]) = { - setIdle(true) super.watchAndWait(watched) setIdle(false) @@ -86,7 +85,7 @@ class MainRunner( val colored = config.core.color.getOrElse(mainInteractive) override val colors = if (colored) Colors.Default else Colors.BlackWhite - override def runScript(scriptPath: os.Path, scriptArgs: List[String]) = + override def runScript(scriptPath: os.Path, scriptArgs: List[String]): Boolean = watchLoop2( isRepl = false, printing = true, diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala index ae7ded10d08..9474a3f810e 100644 --- a/main/src/mill/main/RunScript.scala +++ b/main/src/mill/main/RunScript.scala @@ -17,6 +17,7 @@ import mill.internal.AmmoniteUtils import scala.collection.mutable import scala.reflect.ClassTag import mill.define.ParseArgs.TargetsWithParams +import ujson.Value /** * Custom version of ammonite.main.Scripts, letting us run the build.sc script @@ -24,6 +25,9 @@ import mill.define.ParseArgs.TargetsWithParams * subsystem */ object RunScript { + + type TaskName = String + def runScript( home: os.Path, wd: os.Path, @@ -319,11 +323,45 @@ object RunScript { } } + def evaluateTasksNamed[T]( + evaluator: Evaluator, + scriptArgs: Seq[String], + selectMode: SelectMode + ): Either[String, (Seq[PathRef], Either[String, Seq[(Any, Option[(TaskName, ujson.Value)])]])] = { + for (targets <- resolveTasks(mill.main.ResolveTasks, evaluator, scriptArgs, selectMode)) + yield { + val (watched, res) = evaluateNamed(evaluator, Agg.from(targets.distinct)) + + val watched2 = for { + x <- res.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[ujson.Value])]]) = { - val evaluated = evaluator.evaluate(targets) + val (watched, results) = evaluateNamed(evaluator, targets) + // we drop the task name in the inner tuple + (watched, results.map(_.map(p => (p._1, p._2.map(_._2))))) + } + + /** + * + * @param evaluator + * @param targets + * @return (watched-paths, Either[err-msg, Seq[(task-result, Option[(task-name, task-return-as-json)])]]) + */ + def evaluateNamed( + evaluator: Evaluator, + targets: Agg[Task[Any]] + ): (Seq[PathRef], Either[String, Seq[(Any, Option[(TaskName, ujson.Value)])]]) = { + val evaluated: Evaluator.Results = evaluator.evaluate(targets) val watched = evaluated.results .iterator .collect { @@ -337,18 +375,18 @@ object RunScript { evaluated.failing.keyCount match { case 0 => - val json = for (t <- targets.toSeq) yield { + val nameAndJson = for (t <- targets.toSeq) yield { t match { case t: mill.define.NamedTask[_] => val jsonFile = EvaluatorPaths.resolveDestPaths(evaluator.outPath, t).meta val metadata = upickle.default.read[Evaluator.Cached](ujson.read(jsonFile.toIO)) - Some(metadata.value) + Some(t.toString, metadata.value) case _ => None } } - watched -> Right(evaluated.values.zip(json)) + watched -> Right(evaluated.values.zip(nameAndJson)) case n => watched -> Left(s"$n targets failed\n$errorStr") } } diff --git a/main/test/src/main/MainModuleTests.scala b/main/test/src/main/MainModuleTests.scala new file mode 100644 index 00000000000..2911cf5c0d1 --- /dev/null +++ b/main/test/src/main/MainModuleTests.scala @@ -0,0 +1,81 @@ +package mill.main + +import mill.api.Result +import mill.{Agg, T} +import mill.util.{TestEvaluator, TestUtil} +import utest.{TestSuite, Tests, test} + +object MainModuleTests extends TestSuite { + + object mainModule extends TestUtil.BaseModule with MainModule { + def hello = T { Seq("hello", "world") } + def hello2 = T { Map("1" -> "hello", "2" -> "world") } + } + + override def tests: Tests = Tests { + test("show") { + val evaluator = new TestEvaluator(mainModule) + test("single") { + val results = + evaluator.evaluator.evaluate(Agg(mainModule.show(evaluator.evaluator, "hello"))) + + assert(results.failing.keyCount == 0) + + val Result.Success(value) = results.rawValues.head + + assert(value == ujson.Arr.from(Seq("hello", "world"))) + } + test("multi") { + val results = + evaluator.evaluator.evaluate(Agg(mainModule.show( + evaluator.evaluator, + "hello", + "+", + "hello2" + ))) + + assert(results.failing.keyCount == 0) + + val Result.Success(value) = results.rawValues.head + + assert(value == ujson.Arr.from(Seq( + ujson.Arr.from(Seq("hello", "world")), + ujson.Obj.from(Map("1" -> "hello", "2" -> "world")) + ))) + } + } + test("showNamed") { + val evaluator = new TestEvaluator(mainModule) + test("single") { + val results = + evaluator.evaluator.evaluate(Agg(mainModule.showNamed(evaluator.evaluator, "hello"))) + + assert(results.failing.keyCount == 0) + + val Result.Success(value) = results.rawValues.head + + assert(value == ujson.Obj.from(Map( + "hello" -> ujson.Arr.from(Seq("hello", "world")) + ))) + } + test("multi") { + val results = + evaluator.evaluator.evaluate(Agg(mainModule.showNamed( + evaluator.evaluator, + "hello", + "+", + "hello2" + ))) + + assert(results.failing.keyCount == 0) + + val Result.Success(value) = results.rawValues.head + + assert(value == ujson.Obj.from(Map( + "hello" -> ujson.Arr.from(Seq("hello", "world")), + "hello2" -> ujson.Obj.from(Map("1" -> "hello", "2" -> "world")) + ))) + } + } + } +}