Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved show output to be valid JSON and added showNamed #1765

Merged
merged 10 commits into from
Mar 7, 2022
64 changes: 57 additions & 7 deletions main/src/mill/main/MainModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -56,6 +57,21 @@ object MainModule {
Result.Success(Watched((), watched))
}
}

@internal
def evaluateTasks1[T](
lefou marked this conversation as resolved.
Show resolved Hide resolved
evaluator: Evaluator,
targets: Seq[String],
selectMode: SelectMode
)(f: Seq[(Any, Option[(String, ujson.Value)])] => T): Result[Watched[Option[T]]] = {
RunScript.evaluateTasks1(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 {
Expand Down Expand Up @@ -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.evaluateTasks1(
evaluator.withBaseLogger(
// When using `show`, redirect all stdout of the evaluated tasks so the
// printed JSON is the only thing printed to stdout.
Expand All @@ -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.evaluateTasks1(
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
}
}

Expand Down
3 changes: 1 addition & 2 deletions main/src/mill/main/MainRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 39 additions & 4 deletions main/src/mill/main/RunScript.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -319,11 +320,45 @@ object RunScript {
}
}

def evaluateTasks1[T](
lefou marked this conversation as resolved.
Show resolved Hide resolved
evaluator: Evaluator,
scriptArgs: Seq[String],
selectMode: SelectMode
): Either[String, (Seq[PathRef], Either[String, Seq[(Any, Option[(String, ujson.Value)])]])] = {
for (targets <- resolveTasks(mill.main.ResolveTasks, evaluator, scriptArgs, selectMode))
yield {
val (watched, res) = evaluate1(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) = evaluate1(evaluator, targets)
// we drop the task name in the inner tuple
(watched, results.map(_.map(p => (p._1, p._2.map(_._2)))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also assign names in the mapand avoid p._1 and p._2

}

/**
*
* @param evaluator
* @param targets
* @return (watched-paths, Either[err-msg, Seq[(task-result, Option[(task-name, task-return-as-json)])]])
*/
def evaluate1(
lefou marked this conversation as resolved.
Show resolved Hide resolved
evaluator: Evaluator,
targets: Agg[Task[Any]]
): (Seq[PathRef], Either[String, Seq[(Any, Option[(String, ujson.Value)])]]) = {
val evaluated: Evaluator.Results = evaluator.evaluate(targets)
val watched = evaluated.results
.iterator
.collect {
Expand All @@ -337,18 +372,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")
}
}
Expand Down
81 changes: 81 additions & 0 deletions main/test/src/main/MainModuleTests.scala
Original file line number Diff line number Diff line change
@@ -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"))
)))
}
}
}
}