From bf86cc74f302b3ce65b083e9e3185ea4a12a154b Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Mon, 26 Aug 2024 15:47:27 +0200 Subject: [PATCH] Add `ResultFailureException` and `T.fail` convenience API Removed `Exception` from `Result.Failing` hierarchy. It was added after the last release, so this should be considered a binary compatible change. The `T.fail` shortcut is an experiment. Pease comment WDYT. This change was discussed in https://github.com/com-lihaoyi/mill/pull/3406 --- build.sc | 1 - main/api/src/mill/api/Ctx.scala | 15 ++++++++++++++- main/api/src/mill/api/MillException.scala | 17 ++++++++++++++++- main/api/src/mill/api/Result.scala | 11 +++++++++-- main/define/src/mill/define/Task.scala | 6 ++++++ main/eval/src/mill/eval/Evaluator.scala | 11 +++++++++-- .../src/mill/scalalib/api/ZincWorkerUtil.scala | 7 ++++--- scalalib/src/mill/scalalib/PublishModule.scala | 4 ++-- .../src/mill/scalalib/ZincWorkerModule.scala | 8 ++++---- 9 files changed, 64 insertions(+), 16 deletions(-) diff --git a/build.sc b/build.sc index 743505015c0..b8c3f556776 100644 --- a/build.sc +++ b/build.sc @@ -4,7 +4,6 @@ import com.github.lolgab.mill.mima.Mima import coursier.maven.MavenRepository import de.tobiasroeser.mill.vcs.version.VcsVersion import com.goyeau.mill.scalafix.ScalafixModule -import example.millSourcePath import mill._ import mill.api.JarManifest import mill.define.NamedTask diff --git a/main/api/src/mill/api/Ctx.scala b/main/api/src/mill/api/Ctx.scala index 6ba07731d9f..106323b2396 100644 --- a/main/api/src/mill/api/Ctx.scala +++ b/main/api/src/mill/api/Ctx.scala @@ -97,6 +97,18 @@ object Ctx { def workspace: os.Path } + /** + * Provide some convenience API to fail a task. + */ + trait Fail { + + /** + * Fail the current task. This behaves equivalent to `return Result.Failure(msg)`. + * @param msg The failure message + */ + def fail(msg: String): Nothing = throw new ResultFailureException(msg) + } + def defaultHome: os.Path = os.home / ".mill" / "ammonite" /** @@ -123,7 +135,8 @@ class Ctx( with Ctx.Args with Ctx.Home with Ctx.Env - with Ctx.Workspace { + with Ctx.Workspace + with Ctx.Fail { def dest: os.Path = dest0() def arg[T](index: Int): T = { diff --git a/main/api/src/mill/api/MillException.scala b/main/api/src/mill/api/MillException.scala index aa3054a911d..1c4b156aff7 100644 --- a/main/api/src/mill/api/MillException.scala +++ b/main/api/src/mill/api/MillException.scala @@ -4,7 +4,10 @@ package mill.api * This exception is specifically handled in [[mill.runner.MillMain]] and [[mill.runner.MillServerMain]]. You can use it, if you need to exit Mill with a nice error message. * @param msg The error message, to be displayed to the user. */ -class MillException(msg: String) extends Exception(msg) +class MillException protected[api] (msg: String, cause: Throwable = null) + extends Exception(msg, cause) { + def this(msg: String) = this(msg, null) +} class BuildScriptException(msg: String, script: Option[String]) extends MillException( @@ -12,3 +15,15 @@ class BuildScriptException(msg: String, script: Option[String]) ) { def this(msg: String) = this(msg, None) } + +/** + * This exception is meant to specifically exit a task evaluation with a [[Result.Failure]]. + * The actual catch and lift logic need to be applied in the evaluator. + * @param msg The error message, to be displayed to the user. + * @param cause A potential cause or `null` + */ +class ResultFailureException(msg: String, cause: Throwable = null) + extends MillException(msg, cause) +// { +// def toFailure[T]: Result.Failure[T] = Result.Failure(msg) +//} diff --git a/main/api/src/mill/api/Result.scala b/main/api/src/mill/api/Result.scala index cbe27b004d8..e73f4c0caab 100644 --- a/main/api/src/mill/api/Result.scala +++ b/main/api/src/mill/api/Result.scala @@ -14,7 +14,11 @@ sealed trait Result[+T] { def asFailing: Option[Result.Failing[T]] = None def getOrThrow: T = this match { case Result.Success(v) => v - case f: Result.Failing[_] => throw f + case Result.Failure(msg, _) => + // TODO: once, we have an original exception, reuse it + throw new ResultFailureException(msg) + case Result.Exception(e, _) => + throw e } } @@ -22,6 +26,9 @@ object Result { implicit def create[T](t: => T): Result[T] = { try Success(t) catch { + case e: ResultFailureException => + // TODO: add cause when Result.Failure supports it + Result.Failure(e.getMessage, None) case e: Throwable => Exception(e, new OuterStack(new java.lang.Exception().getStackTrace().toIndexedSeq)) } @@ -58,7 +65,7 @@ object Result { * A failed task execution. * @tparam T The result type of the computed task. */ - sealed trait Failing[+T] extends java.lang.Exception with Result[T] { + sealed trait Failing[+T] extends Result[T] { def map[V](f: T => V): Failing[V] def flatMap[V](f: T => Result[V]): Failing[V] override def asFailing: Option[Result.Failing[T]] = Some(this) diff --git a/main/define/src/mill/define/Task.scala b/main/define/src/mill/define/Task.scala index c8c41d746e8..1f14f173716 100644 --- a/main/define/src/mill/define/Task.scala +++ b/main/define/src/mill/define/Task.scala @@ -192,6 +192,12 @@ object Target extends Applicative.Applyer[Task, Task, Result, mill.api.Ctx] { */ def workspace(implicit ctx: mill.api.Ctx): os.Path = ctx.workspace + /** + * Fail the current task. This behaves equivalent to `return Result.Failure(msg)`. + * @param msg The failure message + */ + def fail(msg: String)(implicit ctx: mill.api.Ctx): Nothing = ctx.fail(msg) + /** * A target is the most common [[Task]] a user would encounter, commonly * defined using the `def foo = T{...}` syntax. [[TargetImpl]]s require that their diff --git a/main/eval/src/mill/eval/Evaluator.scala b/main/eval/src/mill/eval/Evaluator.scala index 0ff9e0814a6..1cda92801cb 100644 --- a/main/eval/src/mill/eval/Evaluator.scala +++ b/main/eval/src/mill/eval/Evaluator.scala @@ -1,6 +1,13 @@ package mill.eval -import mill.api.{CompileProblemReporter, DummyTestReporter, Result, TestReporter, Val} +import mill.api.{ + CompileProblemReporter, + DummyTestReporter, + Result, + ResultFailureException, + TestReporter, + Val +} import mill.api.Strict.Agg import mill.define.{BaseModule, BaseModuleTree, Segments, Task} import mill.eval.Evaluator.{Results, formatFailing} @@ -82,7 +89,7 @@ object Evaluator { yield { val fss = fs.map { case Result.Failure(t, _) => t - case Result.Exception(Result.Failure(t, _), _) => t + case Result.Exception(e: ResultFailureException, _) => e.getMessage case ex: Result.Exception => ex.toString } s"${k.render} ${fss.iterator.mkString(", ")}" diff --git a/scalalib/api/src/mill/scalalib/api/ZincWorkerUtil.scala b/scalalib/api/src/mill/scalalib/api/ZincWorkerUtil.scala index 699506c1a39..fa7951f5222 100644 --- a/scalalib/api/src/mill/scalalib/api/ZincWorkerUtil.scala +++ b/scalalib/api/src/mill/scalalib/api/ZincWorkerUtil.scala @@ -1,7 +1,8 @@ package mill.scalalib.api import mill.api.Loose.Agg -import mill.api.PathRef +import mill.api.{PathRef, ResultFailureException} + import scala.util.matching.Regex trait ZincWorkerUtil { @@ -68,7 +69,7 @@ trait ZincWorkerUtil { def scalaJSBinaryVersion(scalaJSVersion: String): String = scalaJSVersion match { case _ if scalaJSVersion.startsWith("0.6.") => - throw new Exception("Scala.js 0.6 is not supported") + throw new ResultFailureException("Scala.js 0.6 is not supported") case ScalaJSFullVersion(major, minor, patch, suffix) => if (suffix != null && minor == "0" && patch == "0") s"$major.$minor$suffix" @@ -78,7 +79,7 @@ trait ZincWorkerUtil { def scalaJSWorkerVersion(scalaJSVersion: String): String = scalaJSVersion match { case _ if scalaJSVersion.startsWith("0.6.") => - throw new Exception("Scala.js 0.6 is not supported") + throw new ResultFailureException("Scala.js 0.6 is not supported") case ScalaJSFullVersion(major, _, _, _) => major } diff --git a/scalalib/src/mill/scalalib/PublishModule.scala b/scalalib/src/mill/scalalib/PublishModule.scala index edc92dbdeab..747537d62ed 100644 --- a/scalalib/src/mill/scalalib/PublishModule.scala +++ b/scalalib/src/mill/scalalib/PublishModule.scala @@ -2,7 +2,7 @@ package mill package scalalib import mill.define.{Command, ExternalModule, Target, Task} -import mill.api.{JarManifest, PathRef, Result} +import mill.api.{BuildScriptException, JarManifest, PathRef, Result} import mill.scalalib.PublishModule.checkSonatypeCreds import mill.scalalib.publish.SonatypeHelpers.{ PASSWORD_ENV_VARIABLE_NAME, @@ -20,7 +20,7 @@ trait PublishModule extends JavaModule { outer => override def moduleDeps: Seq[PublishModule] = super.moduleDeps.map { case m: PublishModule => m case other => - throw new Exception( + throw new BuildScriptException( s"PublishModule moduleDeps need to be also PublishModules. $other is not a PublishModule" ) } diff --git a/scalalib/src/mill/scalalib/ZincWorkerModule.scala b/scalalib/src/mill/scalalib/ZincWorkerModule.scala index 0ce66219da3..97495e81824 100644 --- a/scalalib/src/mill/scalalib/ZincWorkerModule.scala +++ b/scalalib/src/mill/scalalib/ZincWorkerModule.scala @@ -4,11 +4,11 @@ import coursier.Repository import mainargs.Flag import mill.Agg import mill._ -import mill.api.{Ctx, FixSizedCache, KeyedLockedCache, PathRef, Result} -import mill.define.{ExternalModule, Discover} +import mill.api.{Ctx, FixSizedCache, KeyedLockedCache, PathRef, Result, ResultFailureException} +import mill.define.{Discover, ExternalModule} import mill.scalalib.Lib.resolveDependencies import mill.scalalib.api.ZincWorkerUtil.{isBinaryBridgeAvailable, isDotty, isDottyOrScala3} -import mill.scalalib.api.{ZincWorkerApi, ZincWorkerUtil, Versions} +import mill.scalalib.api.{Versions, ZincWorkerApi, ZincWorkerUtil} import mill.util.Util.millProjectModule /** @@ -76,7 +76,7 @@ trait ZincWorkerModule extends mill.Module with OfflineSupportModule { self: Cou scalaCompilerBridgeJar(x, y, repositoriesTask()) .asSuccess .getOrElse( - throw new Exception(s"Failed to load compiler bridge for $x $y") + throw new ResultFailureException(s"Failed to load compiler bridge for $x $y") ) .value )),