From 90d0a3388d280554eaa51371f666d2f7a965a8af Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 8 Feb 2018 05:35:34 -0800 Subject: vendor ammonite.main code so we can properly handle arity-0 CLI args, fix GenIdea by making it take an Evaluator as an argument --- ci/test-mill-built.sh | 2 +- ci/test-mill-release.sh | 4 +- ci/test-sbt-built.sh | 2 +- ci/test_all.sh | 4 +- core/src/mill/define/BaseModule.scala | 43 +-- core/src/mill/define/Ctx.scala | 4 +- core/src/mill/define/Discover.scala | 6 +- core/src/mill/define/Module.scala | 2 +- core/src/mill/define/Router.scala | 434 ------------------------ core/src/mill/eval/Evaluator.scala | 12 +- core/src/mill/main/MagicScopt.scala | 49 +++ core/src/mill/main/MainModule.scala | 27 ++ core/src/mill/main/MainRunner.scala | 24 +- core/src/mill/main/Resolve.scala | 21 +- core/src/mill/main/Router.scala | 442 +++++++++++++++++++++++++ core/src/mill/main/RunScript.scala | 6 +- core/src/mill/main/Scopt.scala | 19 -- core/src/mill/main/Scripts.scala | 330 ++++++++++++++++++ core/test/src/mill/define/CacherTests.scala | 1 - core/test/src/mill/eval/CrossTests.scala | 3 +- core/test/src/mill/eval/FailureTests.scala | 2 +- core/test/src/mill/eval/ModuleTests.scala | 1 - core/test/src/mill/eval/TaskTests.scala | 1 - core/test/src/mill/main/MainTests.scala | 1 - core/test/src/mill/util/TestUtil.scala | 2 +- scalalib/src/mill/scalalib/GenIdea.scala | 18 +- scalalib/src/mill/scalalib/PublishModule.scala | 5 +- 27 files changed, 898 insertions(+), 567 deletions(-) delete mode 100644 core/src/mill/define/Router.scala create mode 100644 core/src/mill/main/MagicScopt.scala create mode 100644 core/src/mill/main/MainModule.scala create mode 100644 core/src/mill/main/Router.scala delete mode 100644 core/src/mill/main/Scopt.scala create mode 100644 core/src/mill/main/Scripts.scala diff --git a/ci/test-mill-built.sh b/ci/test-mill-built.sh index 5b3b1a28..52be8851 100755 --- a/ci/test-mill-built.sh +++ b/ci/test-mill-built.sh @@ -13,7 +13,7 @@ target/bin/mill devAssembly # Second build & run tests using Mill -out/devAssembly/dest/out.jar --all {core,scalalib,scalajslib}.test devAssembly +out/devAssembly/dest/out.jar all {core,scalalib,scalajslib}.test devAssembly out/devAssembly/dest/out.jar integration.test mill.integration.AmmoniteTests out/devAssembly/dest/out.jar integration.test "mill.integration.{AcyclicTests,BetterFilesTests,JawnTests}" out/devAssembly/dest/out.jar devAssembly diff --git a/ci/test-mill-release.sh b/ci/test-mill-release.sh index 5e19bcdc..e3755e93 100755 --- a/ci/test-mill-release.sh +++ b/ci/test-mill-release.sh @@ -9,7 +9,7 @@ git clean -xdf sbt bin/test:assembly # Build Mill using SBT -target/bin/mill --all _.publishLocal releaseAssembly +target/bin/mill all __.publishLocal releaseAssembly mv out/releaseAssembly/dest/out.jar ~/mill-release @@ -17,7 +17,7 @@ git clean -xdf # Second build & run tests using Mill -~/mill-release --all {core,scalalib,scalajslib}.test devAssembly +~/mill-release all {core,scalalib,scalajslib}.test devAssembly ~/mill-release integration.test mill.integration.AmmoniteTests ~/mill-release integration.test "mill.integration.{AcyclicTests,BetterFilesTests,JawnTests}" ~/mill-release devAssembly diff --git a/ci/test-sbt-built.sh b/ci/test-sbt-built.sh index ebb3ddb5..3a60cbd3 100755 --- a/ci/test-sbt-built.sh +++ b/ci/test-sbt-built.sh @@ -8,7 +8,7 @@ git clean -xdf sbt bin/test:assembly # Run tests using Mill built using SBT -target/bin/mill --all {core,scalalib,scalajslib}.test devAssembly +target/bin/mill all {core,scalalib,scalajslib}.test devAssembly target/bin/mill integration.test mill.integration.AmmoniteTests target/bin/mill integration.test "mill.integration.{AcyclicTests,BetterFilesTests,JawnTests}" target/bin/mill devAssembly diff --git a/ci/test_all.sh b/ci/test_all.sh index 74b6d203..a04f7119 100755 --- a/ci/test_all.sh +++ b/ci/test_all.sh @@ -9,7 +9,7 @@ git clean -xdf sbt core/test scalalib/test scalajslib/test integration/test bin/test:assembly # Run tests using Mill built using SBT -target/bin/mill --all {core,scalalib,scalajslib,integration}.test devAssembly +target/bin/mill all {core,scalalib,scalajslib,integration}.test devAssembly # Second build & run tests using Mill -out/devAssembly/dest/out.jar --all {core,scalalib,scalajslib,integration}.test devAssembly \ No newline at end of file +out/devAssembly/dest/out.jar all {core,scalalib,scalajslib,integration}.test devAssembly \ No newline at end of file diff --git a/core/src/mill/define/BaseModule.scala b/core/src/mill/define/BaseModule.scala index cedfcef7..5253e691 100644 --- a/core/src/mill/define/BaseModule.scala +++ b/core/src/mill/define/BaseModule.scala @@ -1,6 +1,6 @@ package mill.define -import ammonite.main.Router.Overrides +import mill.main.Router.Overrides import ammonite.ops.Path import mill.main.ParseArgs @@ -31,7 +31,6 @@ abstract class BaseModule(millSourcePath0: Path, external0: Boolean = false) override implicit def millModuleBasePath: BasePath = BasePath(millSourcePath) implicit def millImplicitBaseModule: BaseModule.Implicit = BaseModule.Implicit(this) def millDiscover: Discover[this.type] - implicit def millScoptTargetReads[T] = new TargetScopt[T]() } @@ -49,43 +48,3 @@ abstract class ExternalModule(implicit millModuleEnclosing0: sourcecode.Enclosin Segments(millModuleEnclosing0.value.split('.').map(Segment.Label):_*) } } - -object TargetScopt{ - case class Targets[T](items: Seq[mill.define.Target[T]]) - implicit def millScoptTargetReads[T] = new TargetScopt[T]() - // This needs to be a ThreadLocal because we need to pass it into the body of - // the TargetScopt#read call, which does not accept additional parameters. - // Until we migrate our CLI parsing off of Scopt (so we can pass the BaseModule - // in directly) we are forced to pass it in via a ThreadLocal - val currentRootModule = new ThreadLocal[BaseModule] -} -class TargetScopt[T]() - extends scopt.Read[TargetScopt.Targets[T]]{ - def arity = 1 - def reads = s => try{ - val rootModule = TargetScopt.currentRootModule.get - val d = rootModule.millDiscover - val (expanded, Nil) = ParseArgs(Seq("--all", 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, Nil, 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, Nil, crossSelectors.toList, Nil) - } - mill.util.EitherOps.sequence(resolved) match{ - case Left(s) => throw new Exception(s) - case Right(ts) => TargetScopt.Targets(ts.flatten.collect{case t: mill.define.Target[T] => t}) - } - }catch{case e => e.printStackTrace(); throw e} -} diff --git a/core/src/mill/define/Ctx.scala b/core/src/mill/define/Ctx.scala index 1e85d8b3..11e9e1f5 100644 --- a/core/src/mill/define/Ctx.scala +++ b/core/src/mill/define/Ctx.scala @@ -1,6 +1,6 @@ package mill.define -import ammonite.main.Router.Overrides +import mill.main.Router.Overrides import ammonite.ops.{Path, RelPath} import scala.annotation.implicitNotFound @@ -67,7 +67,7 @@ object Ctx{ millName0: sourcecode.Name, millModuleBasePath0: BasePath, segments0: Segments, - overrides0: Overrides, + overrides0: mill.main.Router.Overrides, external0: External): Ctx = { Ctx( millModuleEnclosing0.value, diff --git a/core/src/mill/define/Discover.scala b/core/src/mill/define/Discover.scala index b213d9f3..fd5bd449 100644 --- a/core/src/mill/define/Discover.scala +++ b/core/src/mill/define/Discover.scala @@ -1,6 +1,6 @@ package mill.define import language.experimental.macros -import ammonite.main.Router.{EntryPoint, Overrides} +import mill.main.Router.{EntryPoint, Overrides} import sourcecode.Compat.Context import scala.collection.mutable @@ -41,7 +41,7 @@ object Discover { } rec(weakTypeOf[T]) - val router = new mill.define.Router(c) + val router = new mill.main.Router(c) val mapping = for{ discoveredModuleType <- seen val curCls = discoveredModuleType.asInstanceOf[router.c.Type] @@ -57,7 +57,7 @@ object Discover { val (overrides, routes) = overridesRoutes.unzip val lhs = q"classOf[${discoveredModuleType.typeSymbol.asClass}]" val clsType = discoveredModuleType.typeSymbol.asClass - val rhs = q"scala.Seq[(Int, ammonite.main.Router.EntryPoint[_])](..$overridesRoutes)" + val rhs = q"scala.Seq[(Int, mill.main.Router.EntryPoint[_])](..$overridesRoutes)" q"$lhs -> $rhs" } diff --git a/core/src/mill/define/Module.scala b/core/src/mill/define/Module.scala index 66132fa9..a53ed345 100644 --- a/core/src/mill/define/Module.scala +++ b/core/src/mill/define/Module.scala @@ -2,7 +2,7 @@ package mill.define import java.lang.reflect.Modifier -import ammonite.main.Router.{EntryPoint, Overrides} +import mill.main.Router.{EntryPoint, Overrides} import ammonite.ops.Path import scala.language.experimental.macros diff --git a/core/src/mill/define/Router.scala b/core/src/mill/define/Router.scala deleted file mode 100644 index 4a9c3ffb..00000000 --- a/core/src/mill/define/Router.scala +++ /dev/null @@ -1,434 +0,0 @@ -package mill.define - - -import ammonite.main.Compat -import sourcecode.Compat.Context - -import scala.annotation.StaticAnnotation -import scala.collection.mutable -import scala.language.experimental.macros -import scala.reflect.macros.blackbox.Context -/** - * More or less a minimal version of Autowire's Server that lets you generate - * a set of "routes" from the methods defined in an object, and call them - * using passing in name/args/kwargs via Java reflection, without having to - * generate/compile code or use Scala reflection. This saves us spinning up - * the Scala compiler and greatly reduces the startup time of cached scripts. - */ -object Router{ - /** - * Allows you to query how many things are overriden by the enclosing owner. - */ - case class Overrides(value: Int) - object Overrides{ - def apply()(implicit c: Overrides) = c.value - implicit def generate: Overrides = macro impl - def impl(c: Context): c.Tree = { - import c.universe._ - q"new _root_.ammonite.main.Router.Overrides(${c.internal.enclosingOwner.overrides.length})" - } - } - - class doc(s: String) extends StaticAnnotation - class main extends StaticAnnotation - def generateRoutes[T]: Seq[Router.EntryPoint[T]] = macro generateRoutesImpl[T] - def generateRoutesImpl[T: c.WeakTypeTag](c: Context): c.Expr[Seq[EntryPoint[T]]] = { - import c.universe._ - val r = new Router(c) - val allRoutes = r.getAllRoutesForClass( - weakTypeOf[T].asInstanceOf[r.c.Type] - ).asInstanceOf[Iterable[c.Tree]] - - c.Expr[Seq[EntryPoint[T]]](q"_root_.scala.Seq(..$allRoutes)") - } - - /** - * Models what is known by the router about a single argument: that it has - * a [[name]], a human-readable [[typeString]] describing what the type is - * (just for logging and reading, not a replacement for a `TypeTag`) and - * possible a function that can compute its default value - */ - case class ArgSig[T](name: String, - typeString: String, - doc: Option[String], - default: Option[T => Any]) - - def stripDashes(s: String) = { - if (s.startsWith("--")) s.drop(2) - else if (s.startsWith("-")) s.drop(1) - else s - } - /** - * What is known about a single endpoint for our routes. It has a [[name]], - * [[argSignatures]] for each argument, and a macro-generated [[invoke0]] - * that performs all the necessary argument parsing and de-serialization. - * - * Realistically, you will probably spend most of your time calling [[invoke]] - * instead, which provides a nicer API to call it that mimmicks the API of - * calling a Scala method. - */ - case class EntryPoint[T](name: String, - argSignatures: Seq[ArgSig[T]], - doc: Option[String], - varargs: Boolean, - invoke0: (T, Map[String, String], Seq[String]) => Result[Any], - overrides: Int){ - def invoke(target: T, groupedArgs: Seq[(String, Option[String])]): Result[Any] = { - var remainingArgSignatures = argSignatures.toList - - - val accumulatedKeywords = mutable.Map.empty[ArgSig[T], mutable.Buffer[String]] - val keywordableArgs = if (varargs) argSignatures.dropRight(1) else argSignatures - - for(arg <- keywordableArgs) accumulatedKeywords(arg) = mutable.Buffer.empty - - val leftoverArgs = mutable.Buffer.empty[String] - - val lookupArgSig = argSignatures.map(x => (x.name, x)).toMap - - var incomplete: Option[ArgSig[T]] = None - - for(group <- groupedArgs){ - - group match{ - case (value, None) => - if (value(0) == '-' && !varargs){ - lookupArgSig.get(stripDashes(value)) match{ - case None => leftoverArgs.append(value) - case Some(sig) => incomplete = Some(sig) - } - - } else remainingArgSignatures match { - case Nil => leftoverArgs.append(value) - case last :: Nil if varargs => leftoverArgs.append(value) - case next :: rest => - accumulatedKeywords(next).append(value) - remainingArgSignatures = rest - } - case (rawKey, Some(value)) => - val key = stripDashes(rawKey) - lookupArgSig.get(key) match{ - case Some(x) if accumulatedKeywords.contains(x) => - if (accumulatedKeywords(x).nonEmpty && varargs){ - leftoverArgs.append(rawKey, value) - }else{ - accumulatedKeywords(x).append(value) - remainingArgSignatures = remainingArgSignatures.filter(_.name != key) - } - case _ => - leftoverArgs.append(rawKey, value) - } - } - } - - val missing0 = remainingArgSignatures.filter(_.default.isEmpty) - val missing = if(varargs) { - missing0.filter(_ != argSignatures.last) - } else { - missing0.filter(x => incomplete != Some(x)) - } - val duplicates = accumulatedKeywords.toSeq.filter(_._2.length > 1) - - if ( - incomplete.nonEmpty || - missing.nonEmpty || - duplicates.nonEmpty || - (leftoverArgs.nonEmpty && !varargs) - ){ - Result.Error.MismatchedArguments( - missing = missing, - unknown = leftoverArgs, - duplicate = duplicates, - incomplete = incomplete - - ) - } else { - val mapping = accumulatedKeywords - .iterator - .collect{case (k, Seq(single)) => (k.name, single)} - .toMap - - try invoke0(target, mapping, leftoverArgs) - catch{case e: Throwable => - Result.Error.Exception(e) - } - } - } - } - - def tryEither[T](t: => T, error: Throwable => Result.ParamError) = { - try Right(t) - catch{ case e: Throwable => Left(error(e))} - } - def readVarargs[T](arg: ArgSig[_], - values: Seq[String], - thunk: String => T) = { - val attempts = - for(item <- values) - yield tryEither(thunk(item), Result.ParamError.Invalid(arg, item, _)) - - - val bad = attempts.collect{ case Left(x) => x} - if (bad.nonEmpty) Left(bad) - else Right(attempts.collect{case Right(x) => x}) - } - def read[T](dict: Map[String, String], - default: => Option[Any], - arg: ArgSig[_], - thunk: String => T): FailMaybe = { - dict.get(arg.name) match{ - case None => - tryEither(default.get, Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) - - case Some(x) => - tryEither(thunk(x), Result.ParamError.Invalid(arg, x, _)).left.map(Seq(_)) - } - } - - /** - * Represents what comes out of an attempt to invoke an [[EntryPoint]]. - * Could succeed with a value, but could fail in many different ways. - */ - sealed trait Result[+T] - object Result{ - - /** - * Invoking the [[EntryPoint]] was totally successful, and returned a - * result - */ - case class Success[T](value: T) extends Result[T] - - /** - * Invoking the [[EntryPoint]] was not successful - */ - sealed trait Error extends Result[Nothing] - object Error{ - - /** - * Invoking the [[EntryPoint]] failed with an exception while executing - * code within it. - */ - case class Exception(t: Throwable) extends Error - - /** - * Invoking the [[EntryPoint]] failed because the arguments provided - * did not line up with the arguments expected - */ - case class MismatchedArguments(missing: Seq[ArgSig[_]], - unknown: Seq[String], - duplicate: Seq[(ArgSig[_], Seq[String])], - incomplete: Option[ArgSig[_]]) extends Error - /** - * Invoking the [[EntryPoint]] failed because there were problems - * deserializing/parsing individual arguments - */ - case class InvalidArguments(values: Seq[ParamError]) extends Error - } - - sealed trait ParamError - object ParamError{ - /** - * Something went wrong trying to de-serialize the input parameter; - * the thrown exception is stored in [[ex]] - */ - case class Invalid(arg: ArgSig[_], value: String, ex: Throwable) extends ParamError - /** - * Something went wrong trying to evaluate the default value - * for this input parameter - */ - case class DefaultFailed(arg: ArgSig[_], ex: Throwable) extends ParamError - } - } - - - type FailMaybe = Either[Seq[Result.ParamError], Any] - type FailAll = Either[Seq[Result.ParamError], Seq[Any]] - - def validate(args: Seq[FailMaybe]): Result[Seq[Any]] = { - val lefts = args.collect{case Left(x) => x}.flatten - - if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts) - else { - val rights = args.collect{case Right(x) => x} - Result.Success(rights) - } - } - - def makeReadCall[T: scopt.Read](dict: Map[String, String], - default: => Option[Any], - arg: ArgSig[_]) = { - read[T](dict, default, arg, implicitly[scopt.Read[T]].reads(_)) - } - def makeReadVarargsCall[T: scopt.Read](arg: ArgSig[_], - values: Seq[String]) = { - readVarargs[T](arg, values, implicitly[scopt.Read[T]].reads(_)) - } -} - -class Router [C <: Context](val c: C) { - import c.universe._ - def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = { - def isAMemberOfAnyRef(member: Symbol) = { - // AnyRef is an alias symbol, we go to the real "owner" of these methods - val anyRefSym = c.mirror.universe.definitions.ObjectClass - member.owner == anyRefSym - } - val extractableMembers = for { - member <- curCls.members.toList.reverse - if !isAMemberOfAnyRef(member) - if !member.isSynthetic - if member.isPublic - if member.isTerm - memTerm = member.asTerm - if memTerm.isMethod - if !memTerm.isModule - } yield memTerm.asMethod - - extractableMembers flatMap { case memTerm => - if (memTerm.isSetter || memTerm.isConstructor || memTerm.isGetter) Nil - else Seq(memTerm) - - } - } - - - - def extractMethod(meth: MethodSymbol, curCls: c.universe.Type): c.universe.Tree = { - val baseArgSym = TermName(c.freshName()) - val flattenedArgLists = meth.paramss.flatten - def hasDefault(i: Int) = { - val defaultName = s"${meth.name}$$default$$${i + 1}" - if (curCls.members.exists(_.name.toString == defaultName)) Some(defaultName) - else None - } - val argListSymbol = q"${c.fresh[TermName]("argsList")}" - val extrasSymbol = q"${c.fresh[TermName]("extras")}" - val defaults = for ((arg, i) <- flattenedArgLists.zipWithIndex) yield { - val arg = TermName(c.freshName()) - hasDefault(i).map(defaultName => q"($arg: $curCls) => $arg.${newTermName(defaultName)}") - } - - def getDocAnnotation(annotations: List[Annotation]) = { - val (docTrees, remaining) = annotations.partition(_.tpe =:= typeOf[Router.doc]) - val docValues = for { - doc <- docTrees - if doc.scalaArgs.head.isInstanceOf[Literal] - l = doc.scalaArgs.head.asInstanceOf[Literal] - if l.value.value.isInstanceOf[String] - } yield l.value.value.asInstanceOf[String] - (remaining, docValues.headOption) - } - - def unwrapVarargType(arg: Symbol) = { - val vararg = arg.typeSignature.typeSymbol == definitions.RepeatedParamClass - val unwrappedType = - if (!vararg) arg.typeSignature - else arg.typeSignature.asInstanceOf[TypeRef].args(0) - - (vararg, unwrappedType) - } - - - val (_, methodDoc) = getDocAnnotation(meth.annotations) - val readArgSigs = for( - ((arg, defaultOpt), i) <- flattenedArgLists.zip(defaults).zipWithIndex - ) yield { - - val (vararg, varargUnwrappedType) = unwrapVarargType(arg) - - val default = - if (vararg) q"scala.Some(scala.Nil)" - else defaultOpt match { - case Some(defaultExpr) => q"scala.Some($defaultExpr($baseArgSym))" - case None => q"scala.None" - } - - val (docUnwrappedType, docOpt) = varargUnwrappedType match{ - case t: AnnotatedType => - - val (remaining, docValue) = getDocAnnotation(t.annotations) - if (remaining.isEmpty) (t.underlying, docValue) - else (Compat.copyAnnotatedType(c)(t, remaining), docValue) - - case t => (t, None) - } - - val docTree = docOpt match{ - case None => q"scala.None" - case Some(s) => q"scala.Some($s)" - } - val argSig = q""" - ammonite.main.Router.ArgSig( - ${arg.name.toString}, - ${docUnwrappedType.toString + (if(vararg) "*" else "")}, - $docTree, - $defaultOpt - ) - """ - - val reader = - if(vararg) q""" - ammonite.main.Router.makeReadVarargsCall[$docUnwrappedType]( - $argSig, - $extrasSymbol - ) - """ else q""" - ammonite.main.Router.makeReadCall[$docUnwrappedType]( - $argListSymbol, - $default, - $argSig - ) - """ - c.internal.setPos(reader, meth.pos) - (reader, argSig, vararg) - } - - val (readArgs, argSigs, varargs) = readArgSigs.unzip3 - val (argNames, argNameCasts) = flattenedArgLists.map { arg => - val (vararg, unwrappedType) = unwrapVarargType(arg) - ( - pq"${arg.name.toTermName}", - if (!vararg) q"${arg.name.toTermName}.asInstanceOf[$unwrappedType]" - else q"${arg.name.toTermName}.asInstanceOf[Seq[$unwrappedType]]: _*" - - ) - }.unzip - - - q""" - ammonite.main.Router.EntryPoint[$curCls]( - ${meth.name.toString}, - scala.Seq(..$argSigs), - ${methodDoc match{ - case None => q"scala.None" - case Some(s) => q"scala.Some($s)" - }}, - ${varargs.contains(true)}, - ($baseArgSym: $curCls, $argListSymbol: Map[String, String], $extrasSymbol: Seq[String]) => - ammonite.main.Router.validate(Seq(..$readArgs)) match{ - case ammonite.main.Router.Result.Success(List(..$argNames)) => - ammonite.main.Router.Result.Success( - $baseArgSym.${meth.name.toTermName}(..$argNameCasts) - ) - case x: ammonite.main.Router.Result.Error => x - }, - ammonite.main.Router.Overrides() - ) - """ - } - - def hasMainAnnotation(t: MethodSymbol) = { - t.annotations.exists(_.tpe =:= typeOf[Router.main]) - } - def getAllRoutesForClass(curCls: Type, - pred: MethodSymbol => Boolean = hasMainAnnotation) - : Iterable[c.universe.Tree] = { - for{ - t <- getValsOrMeths(curCls) - if pred(t) - } yield { - extractMethod(t, curCls) - } - } -} - diff --git a/core/src/mill/eval/Evaluator.scala b/core/src/mill/eval/Evaluator.scala index 3d8e82b8..347ad321 100644 --- a/core/src/mill/eval/Evaluator.scala +++ b/core/src/mill/eval/Evaluator.scala @@ -2,7 +2,7 @@ package mill.eval import java.net.URLClassLoader -import ammonite.main.Router.EntryPoint +import mill.main.Router.EntryPoint import ammonite.ops._ import ammonite.runtime.SpecialClassLoader import mill.define.{Ctx => _, _} @@ -58,13 +58,9 @@ class Evaluator[T](val outPath: Path, } } } - pprint.log(discover.value.keySet) - pprint.log(c.cls) findMatching(c.cls) match{ case Some(v) => - pprint.log(v) - pprint.log(c.ctx.segment.pathSegments) v.find(_._2.name == c.ctx.segment.pathSegments.head).get._1 // For now we don't properly support overrides for external modules // that do not appear in the Evaluator's main Discovered listing @@ -334,12 +330,6 @@ class Evaluator[T](val outPath: Path, object Evaluator{ - class Scopt extends scopt.Read[Evaluator[_]] { - def arity = 0 - def reads = _ => dynamicScopt.get - } - val dynamicScopt = new ThreadLocal[Evaluator[_]] - implicit def evaluatorScopt = new Scopt case class Paths(out: Path, dest: Path, meta: Path, diff --git a/core/src/mill/main/MagicScopt.scala b/core/src/mill/main/MagicScopt.scala new file mode 100644 index 00000000..acba57cb --- /dev/null +++ b/core/src/mill/main/MagicScopt.scala @@ -0,0 +1,49 @@ +package mill.main +import mill.define.ExternalModule +import mill.main.ParseArgs + +object MagicScopt{ + // This needs to be a ThreadLocal because we need to pass it into the body of + // the TargetScopt#read call, which does not accept additional parameters. + // Until we migrate our CLI parsing off of Scopt (so we can pass the BaseModule + // in directly) we are forced to pass it in via a ThreadLocal + val currentEvaluator = new ThreadLocal[mill.eval.Evaluator[_]] + + case class Tasks[T](items: Seq[mill.define.NamedTask[T]]) +} +class EvaluatorScopt[T]() + extends scopt.Read[mill.eval.Evaluator[T]]{ + def arity = 0 + def reads = s => try{ + MagicScopt.currentEvaluator.get.asInstanceOf[mill.eval.Evaluator[T]] + } +} +class TargetScopt[T]() + extends scopt.Read[MagicScopt.Tasks[T]]{ + def arity = 0 + def reads = s => { + val rootModule = MagicScopt.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/core/src/mill/main/MainModule.scala b/core/src/mill/main/MainModule.scala new file mode 100644 index 00000000..fd46fb77 --- /dev/null +++ b/core/src/mill/main/MainModule.scala @@ -0,0 +1,27 @@ +package mill.main + +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]() + def resolve(targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + targets.flatMap(_.items).foreach(println) + } + def all(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(_.items)) + ) + } + 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(_.items)) + ) + for(json <- res.right.get.flatMap(_._2)){ + println(json) + } + } +} diff --git a/core/src/mill/main/MainRunner.scala b/core/src/mill/main/MainRunner.scala index b61ab450..9004de39 100644 --- a/core/src/mill/main/MainRunner.scala +++ b/core/src/mill/main/MainRunner.scala @@ -91,7 +91,6 @@ class MainRunner(config: ammonite.main.Cli.Config, |package ${Util.encodeScalaSourcePath(pkgName.tail)} |$imports |import mill._ - |import mill.eval.Evaluator.evaluatorScopt |object $wrapName |extends mill.define.BaseModule(ammonite.ops.Path($literalPath)) |with $wrapName{ @@ -105,28 +104,7 @@ class MainRunner(config: ammonite.main.Cli.Config, | val millSelf = Some(this) |} | - |sealed trait $wrapName extends mill.Module{this: mill.define.BaseModule => - | def resolve(targets: mill.define.TargetScopt.Targets[Any]*) = mill.T.command{ - | targets.flatMap(_.items).foreach(println) - | } - | def all(evaluator: mill.eval.Evaluator[_], - | targets: mill.define.TargetScopt.Targets[Any]*) = mill.T.command{ - | val (watched, res) = mill.main.RunScript.evaluate( - | evaluator, - | mill.util.Strict.Agg.from(targets.flatMap(_.items)) - | ) - | } - | def show(evaluator: mill.eval.Evaluator[_], - | targets: mill.define.TargetScopt.Targets[Any]*) = mill.T.command{ - | val (watched, res) = mill.main.RunScript.evaluate( - | evaluator, - | mill.util.Strict.Agg.from(targets.flatMap(_.items)) - | ) - | for(json <- res.right.get.flatMap(_._2)){ - | println(json) - | } - | } - | implicit def millDiscover: mill.define.Discover[_] + |sealed trait $wrapName extends mill.main.MainModule{ |""".stripMargin } diff --git a/core/src/mill/main/Resolve.scala b/core/src/mill/main/Resolve.scala index 7ecd4ac8..1932c241 100644 --- a/core/src/mill/main/Resolve.scala +++ b/core/src/mill/main/Resolve.scala @@ -2,9 +2,8 @@ package mill.main import mill.define._ import mill.define.TaskModule -import ammonite.main.Router -import ammonite.main.Router.EntryPoint -import ammonite.util.Res +import mill.main.Router.EntryPoint +import ammonite.util.{Res} object Resolve { def resolve[T, V](remainingSelector: List[Segment], @@ -12,7 +11,7 @@ object Resolve { discover: Discover[_], rest: Seq[String], remainingCrossSelectors: List[List[String]], - revSelectorsSoFar: List[Segment]): Either[String, Seq[Task[Any]]] = { + revSelectorsSoFar: List[Segment]): Either[String, Seq[NamedTask[Any]]] = { remainingSelector match{ case Segment.Cross(_) :: Nil => Left("Selector cannot start with a [cross] segment") @@ -24,17 +23,25 @@ object Resolve { .find(_.label == last) .map(Right(_)) + def shimArgsig[T](a: mill.main.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 ammonite.main.Scripts.runMainMethod( + } yield mill.main.Scripts.runMainMethod( target, ep._2.asInstanceOf[EntryPoint[mill.Module]], ammonite.main.Scripts.groupArgs(rest.toList) ) match{ - case Res.Success(v) => Right(v) + case Res.Success(v: Command[_]) => Right(v) case Res.Failure(msg) => Left(msg) case Res.Exception(ex, msg) => val sw = new java.io.StringWriter() @@ -61,7 +68,7 @@ object Resolve { ) // Contents of `either` *must* be a `Task`, because we only select // methods returning `Task` in the discovery process - case Some(either) => either.right.map{ case x: Task[Any] => Seq(x) } + case Some(either) => either.right.map(Seq(_)) } diff --git a/core/src/mill/main/Router.scala b/core/src/mill/main/Router.scala new file mode 100644 index 00000000..935ffc72 --- /dev/null +++ b/core/src/mill/main/Router.scala @@ -0,0 +1,442 @@ +package mill.main + + +import ammonite.main.Compat + +import scala.annotation.StaticAnnotation +import scala.collection.mutable +import scala.language.experimental.macros +import scala.reflect.macros.blackbox.Context +/** + * More or less a minimal version of Autowire's Server that lets you generate + * a set of "routes" from the methods defined in an object, and call them + * using passing in name/args/kwargs via Java reflection, without having to + * generate/compile code or use Scala reflection. This saves us spinning up + * the Scala compiler and greatly reduces the startup time of cached scripts. + */ +object Router{ + /** + * Allows you to query how many things are overriden by the enclosing owner. + */ + case class Overrides(value: Int) + object Overrides{ + def apply()(implicit c: Overrides) = c.value + implicit def generate: Overrides = macro impl + def impl(c: Context): c.Tree = { + import c.universe._ + q"new _root_.mill.main.Router.Overrides(${c.internal.enclosingOwner.overrides.length})" + } + } + + class doc(s: String) extends StaticAnnotation + class main extends StaticAnnotation + def generateRoutes[T]: Seq[Router.EntryPoint[T]] = macro generateRoutesImpl[T] + def generateRoutesImpl[T: c.WeakTypeTag](c: Context): c.Expr[Seq[EntryPoint[T]]] = { + import c.universe._ + val r = new Router(c) + val allRoutes = r.getAllRoutesForClass( + weakTypeOf[T].asInstanceOf[r.c.Type] + ).asInstanceOf[Iterable[c.Tree]] + + c.Expr[Seq[EntryPoint[T]]](q"_root_.scala.Seq(..$allRoutes)") + } + + /** + * Models what is known by the router about a single argument: that it has + * a [[name]], a human-readable [[typeString]] describing what the type is + * (just for logging and reading, not a replacement for a `TypeTag`) and + * possible a function that can compute its default value + */ + case class ArgSig[T, V](name: String, + typeString: String, + doc: Option[String], + default: Option[T => V]) + (implicit val reads: scopt.Read[V]) + + def stripDashes(s: String) = { + if (s.startsWith("--")) s.drop(2) + else if (s.startsWith("-")) s.drop(1) + else s + } + /** + * What is known about a single endpoint for our routes. It has a [[name]], + * [[argSignatures]] for each argument, and a macro-generated [[invoke0]] + * that performs all the necessary argument parsing and de-serialization. + * + * Realistically, you will probably spend most of your time calling [[invoke]] + * instead, which provides a nicer API to call it that mimmicks the API of + * calling a Scala method. + */ + case class EntryPoint[T](name: String, + argSignatures: Seq[ArgSig[T, _]], + doc: Option[String], + varargs: Boolean, + invoke0: (T, Map[String, String], Seq[String]) => Result[Any], + overrides: Int){ + def invoke(target: T, groupedArgs: Seq[(String, Option[String])]): Result[Any] = { + var remainingArgSignatures = argSignatures.toList.filter(_.reads.arity > 0) + + val accumulatedKeywords = mutable.Map.empty[ArgSig[T, _], mutable.Buffer[String]] + val keywordableArgs = if (varargs) argSignatures.dropRight(1) else argSignatures + + for(arg <- keywordableArgs) accumulatedKeywords(arg) = mutable.Buffer.empty + + val leftoverArgs = mutable.Buffer.empty[String] + + val lookupArgSig = Map(argSignatures.map(x => (x.name, x)):_*) + + var incomplete: Option[ArgSig[T, _]] = None + + for(group <- groupedArgs){ + + group match{ + case (value, None) => + if (value(0) == '-' && !varargs){ + lookupArgSig.get(stripDashes(value)) match{ + case None => leftoverArgs.append(value) + case Some(sig) => incomplete = Some(sig) + } + + } else remainingArgSignatures match { + case Nil => leftoverArgs.append(value) + case last :: Nil if varargs => leftoverArgs.append(value) + case next :: rest => + accumulatedKeywords(next).append(value) + remainingArgSignatures = rest + } + case (rawKey, Some(value)) => + val key = stripDashes(rawKey) + lookupArgSig.get(key) match{ + case Some(x) if accumulatedKeywords.contains(x) => + if (accumulatedKeywords(x).nonEmpty && varargs){ + leftoverArgs.append(rawKey, value) + }else{ + accumulatedKeywords(x).append(value) + remainingArgSignatures = remainingArgSignatures.filter(_.name != key) + } + case _ => + leftoverArgs.append(rawKey, value) + } + } + } + + val missing0 = remainingArgSignatures + .filter(_.default.isEmpty) + + val missing = if(varargs) { + missing0.filter(_ != argSignatures.last) + } else { + missing0.filter(x => incomplete != Some(x)) + } + val duplicates = accumulatedKeywords.toSeq.filter(_._2.length > 1) + + if ( + incomplete.nonEmpty || + missing.nonEmpty || + duplicates.nonEmpty || + (leftoverArgs.nonEmpty && !varargs) + ){ + Result.Error.MismatchedArguments( + missing = missing, + unknown = leftoverArgs, + duplicate = duplicates, + incomplete = incomplete + + ) + } else { + val mapping = accumulatedKeywords + .iterator + .collect{case (k, Seq(single)) => (k.name, single)} + .toMap + + try invoke0(target, mapping, leftoverArgs) + catch{case e: Throwable => + Result.Error.Exception(e) + } + } + } + } + + def tryEither[T](t: => T, error: Throwable => Result.ParamError) = { + try Right(t) + catch{ case e: Throwable => Left(error(e))} + } + def readVarargs(arg: ArgSig[_, _], + values: Seq[String], + thunk: String => Any) = { + val attempts = + for(item <- values) + yield tryEither(thunk(item), Result.ParamError.Invalid(arg, item, _)) + + + val bad = attempts.collect{ case Left(x) => x} + if (bad.nonEmpty) Left(bad) + else Right(attempts.collect{case Right(x) => x}) + } + def read(dict: Map[String, String], + default: => Option[Any], + arg: ArgSig[_, _], + thunk: String => Any): FailMaybe = { + arg.reads.arity match{ + case 0 => + tryEither(thunk(null), Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) + case 1 => + dict.get(arg.name) match{ + case None => + tryEither(default.get, Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) + + case Some(x) => + tryEither(thunk(x), Result.ParamError.Invalid(arg, x, _)).left.map(Seq(_)) + } + } + + } + + /** + * Represents what comes out of an attempt to invoke an [[EntryPoint]]. + * Could succeed with a value, but could fail in many different ways. + */ + sealed trait Result[+T] + object Result{ + + /** + * Invoking the [[EntryPoint]] was totally successful, and returned a + * result + */ + case class Success[T](value: T) extends Result[T] + + /** + * Invoking the [[EntryPoint]] was not successful + */ + sealed trait Error extends Result[Nothing] + object Error{ + + /** + * Invoking the [[EntryPoint]] failed with an exception while executing + * code within it. + */ + case class Exception(t: Throwable) extends Error + + /** + * Invoking the [[EntryPoint]] failed because the arguments provided + * did not line up with the arguments expected + */ + case class MismatchedArguments(missing: Seq[ArgSig[_, _]], + unknown: Seq[String], + duplicate: Seq[(ArgSig[_, _], Seq[String])], + incomplete: Option[ArgSig[_, _]]) extends Error + /** + * Invoking the [[EntryPoint]] failed because there were problems + * deserializing/parsing individual arguments + */ + case class InvalidArguments(values: Seq[ParamError]) extends Error + } + + sealed trait ParamError + object ParamError{ + /** + * Something went wrong trying to de-serialize the input parameter; + * the thrown exception is stored in [[ex]] + */ + case class Invalid(arg: ArgSig[_, _], value: String, ex: Throwable) extends ParamError + /** + * Something went wrong trying to evaluate the default value + * for this input parameter + */ + case class DefaultFailed(arg: ArgSig[_, _], ex: Throwable) extends ParamError + } + } + + + type FailMaybe = Either[Seq[Result.ParamError], Any] + type FailAll = Either[Seq[Result.ParamError], Seq[Any]] + + def validate(args: Seq[FailMaybe]): Result[Seq[Any]] = { + val lefts = args.collect{case Left(x) => x}.flatten + + if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts) + else { + val rights = args.collect{case Right(x) => x} + Result.Success(rights) + } + } + + def makeReadCall(dict: Map[String, String], + default: => Option[Any], + arg: ArgSig[_, _]) = { + read(dict, default, arg, arg.reads.reads(_)) + } + def makeReadVarargsCall(arg: ArgSig[_, _], values: Seq[String]) = { + readVarargs(arg, values, arg.reads.reads(_)) + } +} + +class Router [C <: Context](val c: C) { + import c.universe._ + def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = { + def isAMemberOfAnyRef(member: Symbol) = { + // AnyRef is an alias symbol, we go to the real "owner" of these methods + val anyRefSym = c.mirror.universe.definitions.ObjectClass + member.owner == anyRefSym + } + val extractableMembers = for { + member <- curCls.members.toList.reverse + if !isAMemberOfAnyRef(member) + if !member.isSynthetic + if member.isPublic + if member.isTerm + memTerm = member.asTerm + if memTerm.isMethod + if !memTerm.isModule + } yield memTerm.asMethod + + extractableMembers flatMap { case memTerm => + if (memTerm.isSetter || memTerm.isConstructor || memTerm.isGetter) Nil + else Seq(memTerm) + + } + } + + + + def extractMethod(meth: MethodSymbol, curCls: c.universe.Type): c.universe.Tree = { + val baseArgSym = TermName(c.freshName()) + val flattenedArgLists = meth.paramss.flatten + def hasDefault(i: Int) = { + val defaultName = s"${meth.name}$$default$$${i + 1}" + if (curCls.members.exists(_.name.toString == defaultName)) Some(defaultName) + else None + } + val argListSymbol = q"${c.fresh[TermName]("argsList")}" + val extrasSymbol = q"${c.fresh[TermName]("extras")}" + val defaults = for ((arg, i) <- flattenedArgLists.zipWithIndex) yield { + val arg = TermName(c.freshName()) + hasDefault(i).map(defaultName => q"($arg: $curCls) => $arg.${newTermName(defaultName)}") + } + + def getDocAnnotation(annotations: List[Annotation]) = { + val (docTrees, remaining) = annotations.partition(_.tpe =:= typeOf[Router.doc]) + val docValues = for { + doc <- docTrees + if doc.scalaArgs.head.isInstanceOf[Literal] + l = doc.scalaArgs.head.asInstanceOf[Literal] + if l.value.value.isInstanceOf[String] + } yield l.value.value.asInstanceOf[String] + (remaining, docValues.headOption) + } + + def unwrapVarargType(arg: Symbol) = { + val vararg = arg.typeSignature.typeSymbol == definitions.RepeatedParamClass + val unwrappedType = + if (!vararg) arg.typeSignature + else arg.typeSignature.asInstanceOf[TypeRef].args(0) + + (vararg, unwrappedType) + } + + + val (_, methodDoc) = getDocAnnotation(meth.annotations) + val readArgSigs = for( + ((arg, defaultOpt), i) <- flattenedArgLists.zip(defaults).zipWithIndex + ) yield { + + val (vararg, varargUnwrappedType) = unwrapVarargType(arg) + + val default = + if (vararg) q"scala.Some(scala.Nil)" + else defaultOpt match { + case Some(defaultExpr) => q"scala.Some($defaultExpr($baseArgSym))" + case None => q"scala.None" + } + + val (docUnwrappedType, docOpt) = varargUnwrappedType match{ + case t: AnnotatedType => + + val (remaining, docValue) = getDocAnnotation(t.annotations) + if (remaining.isEmpty) (t.underlying, docValue) + else (Compat.copyAnnotatedType(c)(t, remaining), docValue) + + case t => (t, None) + } + + val docTree = docOpt match{ + case None => q"scala.None" + case Some(s) => q"scala.Some($s)" + } + + val argSig = q""" + mill.main.Router.ArgSig[$curCls, $docUnwrappedType]( + ${arg.name.toString}, + ${docUnwrappedType.toString + (if(vararg) "*" else "")}, + $docTree, + $defaultOpt + ) + """ + + val reader = + if(vararg) q""" + mill.main.Router.makeReadVarargsCall( + $argSig, + $extrasSymbol + ) + """ else q""" + mill.main.Router.makeReadCall( + $argListSymbol, + $default, + $argSig + ) + """ + c.internal.setPos(reader, meth.pos) + (reader, argSig, vararg) + } + + val (readArgs, argSigs, varargs) = readArgSigs.unzip3 + val (argNames, argNameCasts) = flattenedArgLists.map { arg => + val (vararg, unwrappedType) = unwrapVarargType(arg) + ( + pq"${arg.name.toTermName}", + if (!vararg) q"${arg.name.toTermName}.asInstanceOf[$unwrappedType]" + else q"${arg.name.toTermName}.asInstanceOf[Seq[$unwrappedType]]: _*" + + ) + }.unzip + + + val res = q""" + mill.main.Router.EntryPoint[$curCls]( + ${meth.name.toString}, + scala.Seq(..$argSigs), + ${methodDoc match{ + case None => q"scala.None" + case Some(s) => q"scala.Some($s)" + }}, + ${varargs.contains(true)}, + ($baseArgSym: $curCls, $argListSymbol: Map[String, String], $extrasSymbol: Seq[String]) => + mill.main.Router.validate(Seq(..$readArgs)) match{ + case mill.main.Router.Result.Success(List(..$argNames)) => + mill.main.Router.Result.Success( + $baseArgSym.${meth.name.toTermName}(..$argNameCasts) + ) + case x: mill.main.Router.Result.Error => x + }, + ammonite.main.Router.Overrides() + ) + """ + res + } + + def hasMainAnnotation(t: MethodSymbol) = { + t.annotations.exists(_.tpe =:= typeOf[Router.main]) + } + def getAllRoutesForClass(curCls: Type, + pred: MethodSymbol => Boolean = hasMainAnnotation) + : Iterable[c.universe.Tree] = { + for{ + t <- getValsOrMeths(curCls) + if pred(t) + } yield { + extractMethod(t, curCls) + } + } +} + diff --git a/core/src/mill/main/RunScript.scala b/core/src/mill/main/RunScript.scala index 3aaeb44f..17d520e7 100644 --- a/core/src/mill/main/RunScript.scala +++ b/core/src/mill/main/RunScript.scala @@ -155,16 +155,14 @@ object RunScript{ // 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.define.TargetScopt.currentRootModule.set(evaluator.rootModule) - mill.eval.Evaluator.dynamicScopt.set(evaluator) + mill.main.MagicScopt.currentEvaluator.set(evaluator) mill.main.Resolve.resolve( sel.value.toList, rootModule, discover, args, crossSelectors.toList, Nil ) } finally{ - mill.eval.Evaluator.dynamicScopt.set(null) - mill.define.TargetScopt.currentRootModule.set(null) + mill.main.MagicScopt.currentEvaluator.set(null) } } EitherOps.sequence(selected) diff --git a/core/src/mill/main/Scopt.scala b/core/src/mill/main/Scopt.scala deleted file mode 100644 index 57d6529d..00000000 --- a/core/src/mill/main/Scopt.scala +++ /dev/null @@ -1,19 +0,0 @@ -//package mill.main -// -//import mill.define.{BaseModule, Discover, ExternalModule} -// -////class ModuleScopt[T <: mill.Module, M <: BaseModule](rootModule: M, d: => Discover[M]) -//// extends scopt.Read[Seq[T]]{ -//// def arity = 1 -//// def reads = s => { -//// val (expanded, Nil) = ParseArgs(Seq(s)).fold(e => throw new Exception(e), identity) -//// expanded.map{ -//// case (Some(scoping), segments) => -//// val moduleCls = rootModule.getClass.getClassLoader.loadClass(scoping.render + "$") -//// val externalRootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule] -//// externalRootModule.millInternal.segmentsToModules(segments).asInstanceOf[T] -//// case (None, segments) => -//// rootModule.millInternal.segmentsToModules(segments).asInstanceOf[T] -//// } -//// } -////} diff --git a/core/src/mill/main/Scripts.scala b/core/src/mill/main/Scripts.scala new file mode 100644 index 00000000..334c610f --- /dev/null +++ b/core/src/mill/main/Scripts.scala @@ -0,0 +1,330 @@ +package mill.main +import java.nio.file.NoSuchFileException + + +import mill.main.Router.{ArgSig, EntryPoint} +import ammonite.ops._ +import ammonite.runtime.Evaluator.AmmoniteExit +import ammonite.util.Name.backtickWrap +import ammonite.util.Util.CodeSource +import ammonite.util.{Name, Res, Util} +import fastparse.utils.Utils._ + +/** + * Logic around using Ammonite as a script-runner; invoking scripts via the + * macro-generated [[Router]], and pretty-printing any output or error messages + */ +object Scripts { + def groupArgs(flatArgs: List[String]): Seq[(String, Option[String])] = { + var keywordTokens = flatArgs + var scriptArgs = Vector.empty[(String, Option[String])] + + while(keywordTokens.nonEmpty) keywordTokens match{ + case List(head, next, rest@_*) if head.startsWith("-") => + scriptArgs = scriptArgs :+ (head, Some(next)) + keywordTokens = rest.toList + case List(head, rest@_*) => + scriptArgs = scriptArgs :+ (head, None) + keywordTokens = rest.toList + + } + scriptArgs + } + + def runScript(wd: Path, + path: Path, + interp: ammonite.interp.Interpreter, + scriptArgs: Seq[(String, Option[String])] = Nil) = { + interp.watch(path) + 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, + // Not sure why we need to wrap this in a separate `$routes` object, + // but if we don't do it for some reason the `generateRoutes` macro + // does not see the annotations on the methods of the outer-wrapper. + // It can inspect the type and its methods fine, it's just the + // `methodsymbol.annotations` ends up being empty. + extraCode = Util.normalizeNewlines( + s""" + |val $$routesOuter = this + |object $$routes + |extends scala.Function0[scala.Seq[ammonite.main.Router.EntryPoint[$$routesOuter.type]]]{ + | def apply() = ammonite.main.Router.generateRoutes[$$routesOuter.type] + |} + """.stripMargin + ), + hardcoded = true + ) + + routeClsName <- processed.blockInfo.lastOption match{ + case Some(meta) => Res.Success(meta.id.wrapperPath) + case None => Res.Skip + } + + mainCls = + interp + .evalClassloader + .loadClass(processed.blockInfo.last.id.wrapperPath + "$") + + routesCls = + interp + .evalClassloader + .loadClass(routeClsName + "$$routes$") + + scriptMains = + routesCls + .getField("MODULE$") + .get(null) + .asInstanceOf[() => Seq[Router.EntryPoint[Any]]] + .apply() + + + mainObj = mainCls.getField("MODULE$").get(null) + + res <- Util.withContextClassloader(interp.evalClassloader){ + scriptMains match { + // If there are no @main methods, there's nothing to do + case Seq() => + if (scriptArgs.isEmpty) Res.Success(()) + else { + val scriptArgString = + scriptArgs.flatMap{case (a, b) => Seq(a) ++ b}.map(literalize(_)) + .mkString(" ") + + Res.Failure("Script " + path.last + " does not take arguments: " + scriptArgString) + } + + // If there's one @main method, we run it with all args + case Seq(main) => runMainMethod(mainObj, main, scriptArgs) + + // If there are multiple @main methods, we use the first arg to decide + // which method to run, and pass the rest to that main method + case mainMethods => + val suffix = formatMainMethods(mainObj, mainMethods) + scriptArgs match{ + case Seq() => + Res.Failure( + s"Need to specify a subcommand to call when running " + path.last + suffix + ) + case Seq((head, Some(_)), tail @ _*) => + Res.Failure( + "To select a subcommand to run, you don't need --s." + Util.newLine + + s"Did you mean `${head.drop(2)}` instead of `$head`?" + ) + case Seq((head, None), tail @ _*) => + mainMethods.find(_.name == head) match{ + case None => + Res.Failure( + s"Unable to find subcommand: " + backtickWrap(head) + suffix + ) + case Some(main) => + runMainMethod(mainObj, main, tail) + } + } + } + } + } yield res + } + def formatMainMethods[T](base: T, mainMethods: Seq[Router.EntryPoint[T]]) = { + if (mainMethods.isEmpty) "" + else{ + val leftColWidth = getLeftColWidth(mainMethods.flatMap(_.argSignatures)) + + val methods = + for(main <- mainMethods) + yield formatMainMethodSignature(base, main, 2, leftColWidth) + + Util.normalizeNewlines( + s""" + | + |Available subcommands: + | + |${methods.mkString(Util.newLine)}""".stripMargin + ) + } + } + def getLeftColWidth[T](items: Seq[ArgSig[T, _]]) = { + items.map(_.name.length + 2) match{ + case Nil => 0 + case x => x.max + } + } + def formatMainMethodSignature[T](base: T, + main: Router.EntryPoint[T], + leftIndent: Int, + leftColWidth: Int) = { + // +2 for space on right of left col + val args = main.argSignatures.map(renderArg(base, _, leftColWidth + leftIndent + 2 + 2, 80)) + + val leftIndentStr = " " * leftIndent + val argStrings = + for((lhs, rhs) <- args) + yield { + val lhsPadded = lhs.padTo(leftColWidth, ' ') + val rhsPadded = rhs.lines.mkString(Util.newLine) + s"$leftIndentStr $lhsPadded $rhsPadded" + } + val mainDocSuffix = main.doc match{ + case Some(d) => Util.newLine + leftIndentStr + softWrap(d, leftIndent, 80) + case None => "" + } + + s"""$leftIndentStr${main.name}$mainDocSuffix + |${argStrings.map(_ + Util.newLine).mkString}""".stripMargin + } + def runMainMethod[T](base: T, + mainMethod: Router.EntryPoint[T], + scriptArgs: Seq[(String, Option[String])]): Res[Any] = { + val leftColWidth = getLeftColWidth(mainMethod.argSignatures) + + def expectedMsg = formatMainMethodSignature(base: T, mainMethod, 0, leftColWidth) + + def pluralize(s: String, n: Int) = { + if (n == 1) s else s + "s" + } + + mainMethod.invoke(base, scriptArgs) match{ + case Router.Result.Success(x) => Res.Success(x) + case Router.Result.Error.Exception(x: AmmoniteExit) => Res.Success(x.value) + case Router.Result.Error.Exception(x) => Res.Exception(x, "") + case Router.Result.Error.MismatchedArguments(missing, unknown, duplicate, incomplete) => + val missingStr = + if (missing.isEmpty) "" + else { + val chunks = + for (x <- missing) + yield "--" + x.name + ": " + x.typeString + + val argumentsStr = pluralize("argument", chunks.length) + s"Missing $argumentsStr: (${chunks.mkString(", ")})" + Util.newLine + } + + + val unknownStr = + if (unknown.isEmpty) "" + else { + val argumentsStr = pluralize("argument", unknown.length) + s"Unknown $argumentsStr: " + unknown.map(literalize(_)).mkString(" ") + Util.newLine + } + + val duplicateStr = + if (duplicate.isEmpty) "" + else { + val lines = + for ((sig, options) <- duplicate) + yield { + s"Duplicate arguments for (--${sig.name}: ${sig.typeString}): " + + options.map(literalize(_)).mkString(" ") + Util.newLine + } + + lines.mkString + + } + val incompleteStr = incomplete match{ + case None => "" + case Some(sig) => + s"Option (--${sig.name}: ${sig.typeString}) is missing a corresponding value" + + Util.newLine + + } + + Res.Failure( + Util.normalizeNewlines( + s"""$missingStr$unknownStr$duplicateStr$incompleteStr + |Arguments provided did not match expected signature: + | + |$expectedMsg + |""".stripMargin + ) + ) + + case Router.Result.Error.InvalidArguments(x) => + val argumentsStr = pluralize("argument", x.length) + val thingies = x.map{ + case Router.Result.ParamError.Invalid(p, v, ex) => + val literalV = literalize(v) + val rendered = {renderArgShort(p)} + s"$rendered: ${p.typeString} = $literalV failed to parse with $ex" + case Router.Result.ParamError.DefaultFailed(p, ex) => + s"${renderArgShort(p)}'s default value failed to evaluate with $ex" + } + + Res.Failure( + Util.normalizeNewlines( + s"""The following $argumentsStr failed to parse: + | + |${thingies.mkString(Util.newLine)} + | + |expected signature: + | + |$expectedMsg + """.stripMargin + ) + ) + } + } + + def softWrap(s: String, leftOffset: Int, maxWidth: Int) = { + val oneLine = s.lines.mkString(" ").split(' ') + + lazy val indent = " " * leftOffset + + val output = new StringBuilder(oneLine.head) + var currentLineWidth = oneLine.head.length + for(chunk <- oneLine.tail){ + val addedWidth = currentLineWidth + chunk.length + 1 + if (addedWidth > maxWidth){ + output.append(Util.newLine + indent) + output.append(chunk) + currentLineWidth = chunk.length + } else{ + currentLineWidth = addedWidth + output.append(' ') + output.append(chunk) + } + } + output.mkString + } + def renderArgShort[T](arg: ArgSig[T, _]) = "--" + backtickWrap(arg.name) + def renderArg[T](base: T, + arg: ArgSig[T, _], + leftOffset: Int, + wrappedWidth: Int): (String, String) = { + val suffix = arg.default match{ + case Some(f) => " (default " + f(base) + ")" + case None => "" + } + val docSuffix = arg.doc match{ + case Some(d) => ": " + d + case None => "" + } + val wrapped = softWrap( + arg.typeString + suffix + docSuffix, + leftOffset, + wrappedWidth - leftOffset + ) + (renderArgShort(arg), wrapped) + } + + + def mainMethodDetails[T](ep: EntryPoint[T]) = { + ep.argSignatures.collect{ + case ArgSig(name, tpe, Some(doc), default) => + Util.newLine + name + " // " + doc + }.mkString + } + + /** + * Additional [[scopt.Read]] instance to teach it how to read Ammonite paths + */ + implicit def pathScoptRead: scopt.Read[Path] = scopt.Read.stringRead.map(Path(_, pwd)) + +} diff --git a/core/test/src/mill/define/CacherTests.scala b/core/test/src/mill/define/CacherTests.scala index 077fea8d..606de846 100644 --- a/core/test/src/mill/define/CacherTests.scala +++ b/core/test/src/mill/define/CacherTests.scala @@ -9,7 +9,6 @@ import utest._ import utest.framework.TestPath import mill.util.TestEvaluator.implicitDisover -import TargetScopt.millScoptTargetReads object CacherTests extends TestSuite{ object Base extends Base trait Base extends TestUtil.BaseModule{ diff --git a/core/test/src/mill/eval/CrossTests.scala b/core/test/src/mill/eval/CrossTests.scala index 4e772a40..aa12e180 100644 --- a/core/test/src/mill/eval/CrossTests.scala +++ b/core/test/src/mill/eval/CrossTests.scala @@ -1,12 +1,11 @@ package mill.eval import ammonite.ops._ -import mill.define.{Discover, TargetScopt} +import mill.define.Discover import mill.util.TestEvaluator import mill.util.TestEvaluator.implicitDisover import mill.util.TestGraphs.{crossResolved, doubleCross, nestedCrosses, singleCross} import utest._ -import TargetScopt.millScoptTargetReads object CrossTests extends TestSuite{ val tests = Tests{ 'singleCross - { diff --git a/core/test/src/mill/eval/FailureTests.scala b/core/test/src/mill/eval/FailureTests.scala index 6bf53f7c..90cff686 100644 --- a/core/test/src/mill/eval/FailureTests.scala +++ b/core/test/src/mill/eval/FailureTests.scala @@ -6,7 +6,7 @@ import mill.eval.Result.OuterStack import utest._ import utest.framework.TestPath import mill.util.TestEvaluator.implicitDisover -import mill.define.TargetScopt.millScoptTargetReads + object FailureTests extends TestSuite{ val tests = Tests{ diff --git a/core/test/src/mill/eval/ModuleTests.scala b/core/test/src/mill/eval/ModuleTests.scala index 278b46cc..c6125b32 100644 --- a/core/test/src/mill/eval/ModuleTests.scala +++ b/core/test/src/mill/eval/ModuleTests.scala @@ -6,7 +6,6 @@ import mill.T import mill.define.Discover import mill.util.TestEvaluator.implicitDisover import utest._ -import mill.define.TargetScopt.millScoptTargetReads object ModuleTests extends TestSuite{ object ExternalModule extends mill.define.ExternalModule { diff --git a/core/test/src/mill/eval/TaskTests.scala b/core/test/src/mill/eval/TaskTests.scala index ea82677d..114a2910 100644 --- a/core/test/src/mill/eval/TaskTests.scala +++ b/core/test/src/mill/eval/TaskTests.scala @@ -5,7 +5,6 @@ import ammonite.ops._ import mill.T import mill.util.TestEvaluator.implicitDisover import mill.util.TestEvaluator -import mill.define.TargetScopt.millScoptTargetReads object TaskTests extends TestSuite{ val tests = Tests{ object build extends mill.util.TestUtil.BaseModule{ diff --git a/core/test/src/mill/main/MainTests.scala b/core/test/src/mill/main/MainTests.scala index e1a419cb..22f93ae0 100644 --- a/core/test/src/mill/main/MainTests.scala +++ b/core/test/src/mill/main/MainTests.scala @@ -4,7 +4,6 @@ import mill.define.{Discover, Segment, Task} import mill.util.TestGraphs._ import mill.util.TestEvaluator.implicitDisover import utest._ -import mill.define.TargetScopt.millScoptTargetReads object MainTests extends TestSuite{ def check[T <: mill.Module](module: T)( diff --git a/core/test/src/mill/util/TestUtil.scala b/core/test/src/mill/util/TestUtil.scala index 9a5baf11..7ef43943 100644 --- a/core/test/src/mill/util/TestUtil.scala +++ b/core/test/src/mill/util/TestUtil.scala @@ -1,6 +1,6 @@ package mill.util -import ammonite.main.Router.Overrides +import mill.main.Router.Overrides import ammonite.ops.pwd import mill.define._ import mill.eval.Result diff --git a/scalalib/src/mill/scalalib/GenIdea.scala b/scalalib/src/mill/scalalib/GenIdea.scala index 2f76b666..4b283e7c 100644 --- a/scalalib/src/mill/scalalib/GenIdea.scala +++ b/scalalib/src/mill/scalalib/GenIdea.scala @@ -12,15 +12,23 @@ import mill.util.Strict.Agg object GenIdeaModule extends ExternalModule { - def idea() = T.command{ mill.scalalib.GenIdea() } + + def idea(ev: Evaluator[Any]) = T.command{ + mill.scalalib.GenIdea( + implicitly, + ev.rootModule, + ev.discover + ) + } + + implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]() def millDiscover = Discover[this.type] } object GenIdea { - def apply()(implicit ctx: Log, - rootModule0: BaseModule.Implicit, - discover: Discover[_]): Unit = { - val rootModule = rootModule0.value + def apply(ctx: Log, + rootModule: BaseModule, + discover: Discover[_]): Unit = { val pp = new scala.xml.PrettyPrinter(999, 4) rm! pwd/".idea" rm! pwd/".idea_modules" diff --git a/scalalib/src/mill/scalalib/PublishModule.scala b/scalalib/src/mill/scalalib/PublishModule.scala index 993fe7e6..e7f36609 100644 --- a/scalalib/src/mill/scalalib/PublishModule.scala +++ b/scalalib/src/mill/scalalib/PublishModule.scala @@ -2,7 +2,7 @@ package mill package scalalib import ammonite.ops._ -import mill.define.{ExternalModule, TargetScopt, Task} +import mill.define.{ExternalModule, Task} import mill.eval.{PathRef, Result} import mill.scalalib.publish.{Artifact, SonatypePublisher} import mill.util.Loose.Agg @@ -88,7 +88,7 @@ trait PublishModule extends ScalaModule { outer => object PublishModule extends ExternalModule{ def publishAll(sonatypeCreds: String, gpgPassphrase: String, - publishArtifacts: TargetScopt.Targets[(mill.scalalib.publish.Artifact, Seq[(PathRef, String)])], + publishArtifacts: mill.main.MagicScopt.Tasks[(mill.scalalib.publish.Artifact, Seq[(PathRef, String)])], sonatypeUri: String = "https://oss.sonatype.org/service/local", sonatypeSnapshotUri: String = "https://oss.sonatype.org/content/repositories/snapshots") = T.command{ val x: Seq[(Seq[(Path, String)], Artifact)] = Task.sequence(publishArtifacts.items)().map{ @@ -104,6 +104,7 @@ object PublishModule extends ExternalModule{ x:_* ) } + implicit def millScoptTargetReads[T] = new mill.main.MagicScopt[T]() def millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type] } \ No newline at end of file -- cgit v1.2.3