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
40 changes: 40 additions & 0 deletions docs/antora/modules/ROOT/pages/Intro_to_Mill.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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, it's output will slightly change to a JSON array, containing all the results of the given targets.
lefou marked this conversation as resolved.
Show resolved Hide resolved

[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]
Expand Down
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 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 {
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.evaluateTasksNamed(
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.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
}
}

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
46 changes: 42 additions & 4 deletions main/src/mill/main/RunScript.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ 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
* directly without going through Ammonite's main-method/argument-parsing
* subsystem
*/
object RunScript {

type TaskName = String

def runScript(
home: os.Path,
wd: os.Path,
Expand Down Expand Up @@ -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)))))
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 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 {
Expand All @@ -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")
}
}
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"))
)))
}
}
}
}