diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-02-09 00:14:47 -0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-02-09 08:17:47 -0800 |
commit | 8ddd2fa054bc8639c28db2e95b7903e2954fdb7d (patch) | |
tree | aa985f1e715f07eb279e6facad61de8a187e316c /main/src | |
parent | 90d0a3388d280554eaa51371f666d2f7a965a8af (diff) | |
download | mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.gz mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.bz2 mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.zip |
.
Diffstat (limited to 'main/src')
-rw-r--r-- | main/src/mill/Main.scala | 83 | ||||
-rw-r--r-- | main/src/mill/main/MagicScopt.scala | 46 | ||||
-rw-r--r-- | main/src/mill/main/MainModule.scala | 56 | ||||
-rw-r--r-- | main/src/mill/main/MainRunner.scala | 120 | ||||
-rw-r--r-- | main/src/mill/main/ReplApplyHandler.scala | 137 | ||||
-rw-r--r-- | main/src/mill/main/Resolve.scala | 144 | ||||
-rw-r--r-- | main/src/mill/main/RunScript.scala | 240 | ||||
-rw-r--r-- | main/src/mill/modules/Jvm.scala | 257 | ||||
-rw-r--r-- | main/src/mill/modules/Util.scala | 65 | ||||
-rw-r--r-- | main/src/mill/package.scala | 12 |
10 files changed, 1160 insertions, 0 deletions
diff --git a/main/src/mill/Main.scala b/main/src/mill/Main.scala new file mode 100644 index 00000000..3025994c --- /dev/null +++ b/main/src/mill/Main.scala @@ -0,0 +1,83 @@ +package mill + +import ammonite.main.Cli.{formatBlock, genericSignature, replSignature} +import ammonite.ops._ +import ammonite.util.Util + +object Main { + case class Config(home: ammonite.ops.Path = pwd/'out/'ammonite, + colored: Option[Boolean] = None, + help: Boolean = false, + repl: Boolean = false, + watch: Boolean = false) + + def main(args: Array[String]): Unit = { + + import ammonite.main.Cli + + var show = false + val showCliArg = Cli.Arg[Cli.Config, Unit]( + "show", + None, + "Display the json-formatted value of the given target, if any", + (x, _) => { + show = true + x + } + ) + val removed = Set("predef-code", "home", "no-home-predef") + val millArgSignature = (Cli.genericSignature :+ showCliArg).filter(a => !removed(a.name)) + Cli.groupArgs( + args.toList, + millArgSignature, + Cli.Config(remoteLogging = false) + ) match{ + case Left(msg) => + System.err.println(msg) + System.exit(1) + case Right((cliConfig, _)) if cliConfig.help => + val leftMargin = millArgSignature.map(ammonite.main.Cli.showArg(_).length).max + 2 + System.out.println( + s"""Mill Build Tool + |usage: mill [mill-options] [target [target-options]] + | + |${formatBlock(millArgSignature, leftMargin).mkString(Util.newLine)}""".stripMargin + ) + System.exit(0) + case Right((cliConfig, leftoverArgs)) => + + val repl = leftoverArgs.isEmpty + val config = + if(!repl) cliConfig + else cliConfig.copy( + predefCode = + """import $file.build, build._ + |implicit val replApplyHandler = mill.main.ReplApplyHandler( + | interp.colors(), + | repl.pprinter(), + | build.millSelf.get, + | build.millDiscover + |) + |repl.pprinter() = replApplyHandler.pprinter + |import replApplyHandler.generatedEval._ + | + """.stripMargin, + welcomeBanner = None + ) + + val runner = new mill.main.MainRunner( + config, show, + System.out, System.err, System.in + ) + if (repl){ + runner.printInfo("Loading...") + runner.runRepl() + } else { + val result = runner.runScript(pwd / "build.sc", leftoverArgs) + System.exit(if(result) 0 else 1) + } + } + } +} + + diff --git a/main/src/mill/main/MagicScopt.scala b/main/src/mill/main/MagicScopt.scala new file mode 100644 index 00000000..e18816c8 --- /dev/null +++ b/main/src/mill/main/MagicScopt.scala @@ -0,0 +1,46 @@ +package mill.main +import mill.define.ExternalModule +import mill.eval.{Evaluator, PathRef} +import mill.util.ParseArgs + +object MagicScopt{ + + + case class Tasks[T](value: Seq[mill.define.NamedTask[T]]) +} +class EvaluatorScopt[T]() + extends scopt.Read[mill.eval.Evaluator[T]]{ + def arity = 0 + def reads = s => try{ + Evaluator.currentEvaluator.get.asInstanceOf[mill.eval.Evaluator[T]] + } +} +class TargetScopt[T]() + extends scopt.Read[MagicScopt.Tasks[T]]{ + def arity = 0 + def reads = s => { + val rootModule = Evaluator.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/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala new file mode 100644 index 00000000..012ecce5 --- /dev/null +++ b/main/src/mill/main/MainModule.scala @@ -0,0 +1,56 @@ +package mill.main + +import mill.util +import mill.main.RunScript +import mill.util.Watched +import pprint.{Renderer, Truncated} + +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]() + mill.define.Ctx.make + def resolve(targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + targets.flatMap(_.value).foreach(println) + } + def describe(evaluator: mill.eval.Evaluator[Any], + targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + for{ + t <- targets + target <- t.value + tree = ReplApplyHandler.pprintTask(target, evaluator) + val defaults = pprint.PPrinter() + val renderer = new Renderer( + defaults.defaultWidth, + defaults.colorApplyPrefix, + defaults.colorLiteral, + defaults.defaultIndent + ) + val rendered = renderer.rec(tree, 0, 0).iter + val truncated = new Truncated(rendered, defaults.defaultWidth, defaults.defaultHeight) + str <- truncated ++ Iterator("\n") + } { + print(str) + } + } + def all(evaluator: mill.eval.Evaluator[Any], + targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + val (watched, res) = RunScript.evaluate( + evaluator, + mill.util.Strict.Agg.from(targets.flatMap(_.value)) + ) + Watched((), watched) + } + 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(_.value)) + ) + for(json <- res.right.get.flatMap(_._2)){ + println(json) + } + Watched((), watched) + } +} diff --git a/main/src/mill/main/MainRunner.scala b/main/src/mill/main/MainRunner.scala new file mode 100644 index 00000000..c073e583 --- /dev/null +++ b/main/src/mill/main/MainRunner.scala @@ -0,0 +1,120 @@ +package mill.main +import java.io.{InputStream, OutputStream, PrintStream} + +import ammonite.Main +import ammonite.interp.{Interpreter, Preprocessor} +import ammonite.ops.Path +import ammonite.util._ +import mill.define.Discover +import mill.eval.{Evaluator, PathRef} +import mill.util.PrintLogger +import mill.main.RunScript +import upickle.Js + +/** + * Customized version of [[ammonite.MainRunner]], allowing us to run Mill + * `build.sc` scripts with mill-specific tweaks such as a custom + * `scriptCodeWrapper` or with a persistent evaluator between runs. + */ +class MainRunner(config: ammonite.main.Cli.Config, + show: Boolean, + outprintStream: PrintStream, + errPrintStream: PrintStream, + stdIn: InputStream) + extends ammonite.MainRunner( + config, outprintStream, errPrintStream, + stdIn, outprintStream, errPrintStream + ){ + + var lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])] = None + + override def runScript(scriptPath: Path, scriptArgs: List[String]) = + watchLoop( + isRepl = false, + printing = true, + mainCfg => { + val (result, interpWatched) = RunScript.runScript( + mainCfg.wd, + scriptPath, + mainCfg.instantiateInterpreter(), + scriptArgs, + lastEvaluator, + new PrintLogger( + colors != ammonite.util.Colors.BlackWhite, + colors, + if (show) errPrintStream else outprintStream, + errPrintStream, + errPrintStream + ) + ) + + result match{ + case Res.Success(data) => + val (eval, evaluationWatches, res) = data + + lastEvaluator = Some((interpWatched, eval)) + + (Res(res), interpWatched ++ evaluationWatches) + case _ => (result, interpWatched) + } + } + ) + + override def handleWatchRes[T](res: Res[T], printing: Boolean) = { + res match{ + case Res.Success(value) => +// if (show){ +// for(json <- value.asInstanceOf[Seq[Js.Value]]){ +// outprintStream.println(json) +// } +// } + + true + + case _ => super.handleWatchRes(res, printing) + } + + } + override def initMain(isRepl: Boolean) = { + super.initMain(isRepl).copy( + scriptCodeWrapper = CustomCodeWrapper, + // Ammonite does not properly forward the wd from CliConfig to Main, so + // force forward it outselves + wd = config.wd + ) + } + object CustomCodeWrapper extends Preprocessor.CodeWrapper { + def top(pkgName: Seq[Name], imports: Imports, indexedWrapperName: Name) = { + val wrapName = indexedWrapperName.backticked + val literalPath = pprint.Util.literalize(config.wd.toString) + s""" + |package ${pkgName.head.encoded} + |package ${Util.encodeScalaSourcePath(pkgName.tail)} + |$imports + |import mill._ + |object $wrapName + |extends mill.define.BaseModule(ammonite.ops.Path($literalPath)) + |with $wrapName{ + | // Stub to make sure Ammonite has something to call after it evaluates a script, + | // even if it does nothing... + | def $$main() = Iterator[String]() + | + | implicit def millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type] + | // Need to wrap the returned Module in Some(...) to make sure it + | // doesn't get picked up during reflective child-module discovery + | val millSelf = Some(this) + |} + | + |sealed trait $wrapName extends mill.main.MainModule{ + |""".stripMargin + } + + + def bottom(printCode: String, indexedWrapperName: Name, extraCode: String) = { + // We need to disable the `$main` method definition inside the wrapper class, + // because otherwise it might get picked up by Ammonite and run as a static + // class method, which blows up since it's defined as an instance method + "\n}" + } + } +} diff --git a/main/src/mill/main/ReplApplyHandler.scala b/main/src/mill/main/ReplApplyHandler.scala new file mode 100644 index 00000000..76ef354f --- /dev/null +++ b/main/src/mill/main/ReplApplyHandler.scala @@ -0,0 +1,137 @@ +package mill.main + + +import mill.define.Applicative.ApplyHandler +import mill.define.Segment.Label +import mill.define._ +import mill.eval.{Evaluator, Result} +import mill.util.Strict.Agg + +import scala.collection.mutable +object ReplApplyHandler{ + def apply[T](colors: ammonite.util.Colors, + pprinter0: pprint.PPrinter, + rootModule: mill.define.BaseModule, + discover: Discover[_]) = { + new ReplApplyHandler( + pprinter0, + new Evaluator( + ammonite.ops.pwd / 'out, + ammonite.ops.pwd / 'out, + rootModule, + discover, + new mill.util.PrintLogger( + colors != ammonite.util.Colors.BlackWhite, + colors, + System.out, + System.err, + System.err + ) + ) + ) + } + def pprintCross(c: mill.define.Cross[_], evaluator: Evaluator[_]) = { + pprint.Tree.Lazy( ctx => + Iterator(c.millOuterCtx.enclosing , ":", c.millOuterCtx.lineNum.toString, ctx.applyPrefixColor("\nChildren:").toString) ++ + c.items.iterator.map(x => + "\n (" + x._1.map(pprint.PPrinter.BlackWhite.apply(_)).mkString(", ") + ")" + ) + ) + } + def pprintModule(m: mill.define.Module, evaluator: Evaluator[_]) = { + pprint.Tree.Lazy( ctx => + Iterator(m.millInternal.millModuleEnclosing, ":", m.millInternal.millModuleLine.toString) ++ + (if (m.millInternal.reflect[mill.Module].isEmpty) Nil + else + ctx.applyPrefixColor("\nChildren:").toString +: + m.millInternal.reflect[mill.Module].map("\n ." + _.millOuterCtx.segment.pathSegments.mkString("."))) ++ + (evaluator.discover.value.get(m.getClass) match{ + case None => Nil + case Some(commands) => + ctx.applyPrefixColor("\nCommands:").toString +: commands.map{c => + "\n ." + c._2.name + "(" + + c._2.argSignatures.map(s => s.name + ": " + s.typeString).mkString(", ") + + ")()" + } + }) ++ + (if (m.millInternal.reflect[Target[_]].isEmpty) Nil + else { + Seq(ctx.applyPrefixColor("\nTargets:").toString) ++ + m.millInternal.reflect[Target[_]].sortBy(_.label).map(t => + "\n ." + t.label + "()" + ) + }) + + ) + } + def pprintTask(t: NamedTask[_], evaluator: Evaluator[_]) = { + val seen = mutable.Set.empty[Task[_]] + def rec(t: Task[_]): Seq[Segments] = { + if (seen(t)) Nil // do nothing + else t match { + case t: Target[_] if evaluator.rootModule.millInternal.targets.contains(t) => + Seq(t.ctx.segments) + case _ => + seen.add(t) + t.inputs.flatMap(rec) + } + } + pprint.Tree.Lazy(ctx => + Iterator( + t.toString, "(", t.ctx.fileName, ":", t.ctx.lineNum.toString, ")", + t.ctx.lineNum.toString, "\n", ctx.applyPrefixColor("Inputs:").toString + ) ++ t.inputs.iterator.flatMap(rec).map("\n " + _.render) + ) + } + +} +class ReplApplyHandler(pprinter0: pprint.PPrinter, + evaluator: Evaluator[_]) extends ApplyHandler[Task] { + // Evaluate classLoaderSig only once in the REPL to avoid busting caches + // as the user enters more REPL commands and changes the classpath + val classLoaderSig = Evaluator.classLoaderSig + override def apply[V](t: Task[V]) = { + val res = evaluator.evaluate(Agg(t)) + res.values match{ + case Seq(head: V) => head + case Nil => + val msg = new mutable.StringBuilder() + msg.append(res.failing.keyCount + " targets failed\n") + for((k, vs) <- res.failing.items){ + msg.append(k match{ + case Left(t) => "Anonymous Task\n" + case Right(k) => k.segments.render + "\n" + }) + + for(v <- vs){ + v match{ + case Result.Failure(m, _) => msg.append(m + "\n") + case Result.Exception(t, outerStack) => + msg.append( + t.toString + + t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString + + "\n" + ) + + } + } + } + throw new Exception(msg.toString) + } + } + + val generatedEval = new EvalGenerated(evaluator) + + val millHandlers: PartialFunction[Any, pprint.Tree] = { + case c: Cross[_] => + ReplApplyHandler.pprintCross(c, evaluator) + case m: mill.Module if evaluator.rootModule.millInternal.modules.contains(m) => + ReplApplyHandler.pprintModule(m, evaluator) + case t: mill.define.Target[_] if evaluator.rootModule.millInternal.targets.contains(t) => + ReplApplyHandler.pprintTask(t, evaluator) + + } + val pprinter = pprinter0.copy( + additionalHandlers = millHandlers orElse pprinter0.additionalHandlers + ) +} diff --git a/main/src/mill/main/Resolve.scala b/main/src/mill/main/Resolve.scala new file mode 100644 index 00000000..c134d70d --- /dev/null +++ b/main/src/mill/main/Resolve.scala @@ -0,0 +1,144 @@ +package mill.main + +import mill.define._ +import mill.define.TaskModule +import ammonite.util.Res +import mill.util.Router.EntryPoint +import mill.util.Scripts + +object Resolve { + def resolve[T, V](remainingSelector: List[Segment], + obj: mill.Module, + discover: Discover[_], + rest: Seq[String], + remainingCrossSelectors: List[List[String]], + revSelectorsSoFar: List[Segment]): Either[String, Seq[NamedTask[Any]]] = { + + remainingSelector match{ + case Segment.Cross(_) :: Nil => Left("Selector cannot start with a [cross] segment") + case Segment.Label(last) :: Nil => + val target = + obj + .millInternal + .reflect[Target[_]] + .find(_.label == last) + .map(Right(_)) + + def shimArgsig[T](a: mill.util.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 Scripts.runMainMethod( + target, + ep._2.asInstanceOf[EntryPoint[mill.Module]], + ammonite.main.Scripts.groupArgs(rest.toList) + ) match{ + case Res.Success(v: Command[_]) => Right(v) + case Res.Failure(msg) => Left(msg) + case Res.Exception(ex, msg) => + val sw = new java.io.StringWriter() + ex.printStackTrace(new java.io.PrintWriter(sw)) + val prefix = if (msg.nonEmpty) msg + "\n" else msg + Left(prefix + sw.toString) + + } + + val runDefault = for{ + child <- obj.millInternal.reflectNestedObjects[mill.Module] + if child.millOuterCtx.segment == Segment.Label(last) + res <- child match{ + case taskMod: TaskModule => Some(invokeCommand(child, taskMod.defaultCommandName()).headOption) + case _ => None + } + } yield res + + val command = invokeCommand(obj, last).headOption + + command orElse target orElse runDefault.flatten.headOption match{ + case None => Left("Cannot resolve task " + + Segments((Segment.Label(last) :: revSelectorsSoFar).reverse:_*).render + ) + // Contents of `either` *must* be a `Task`, because we only select + // methods returning `Task` in the discovery process + case Some(either) => either.right.map(Seq(_)) + } + + + case head :: tail => + val newRevSelectorsSoFar = head :: revSelectorsSoFar + head match{ + case Segment.Label(singleLabel) => + if (singleLabel == "__") { + + val matching = + obj.millInternal.modules + .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)) + .collect{case Right(vs) => vs}.flatten + + if (matching.nonEmpty)Right(matching) + else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } else if (singleLabel == "_") { + + val matching = + obj.millModuleDirectChildren + .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)) + .collect{case Right(vs) => vs}.flatten + + if (matching.nonEmpty)Right(matching) + else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render) + }else{ + + obj.millInternal.reflectNestedObjects[mill.Module].find{ + _.millOuterCtx.segment == Segment.Label(singleLabel) + } match{ + case Some(child: mill.Module) => resolve(tail, child, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + case None => Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } + } + + case Segment.Cross(cross) => + obj match{ + case c: Cross[_] => + if(cross == Seq("__")){ + val matching = + for ((k, v) <- c.items) + yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + + val results = matching.collect{case Right(res) => res}.flatten + + if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + else Right(results) + } else if (cross.contains("_")){ + val matching = for { + (k, v) <- c.items + if k.length == cross.length + if k.zip(cross).forall { case (l, r) => l == r || r == "_" } + } yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + + val results = matching.collect{case Right(res) => res}.flatten + + if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + else Right(results) + }else{ + c.itemMap.get(cross.toList) match{ + case Some(m: mill.Module) => resolve(tail, m, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + case None => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } + } + case _ => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } + } + + case Nil => Left("Selector cannot be empty") + } + } +} diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala new file mode 100644 index 00000000..bd21a821 --- /dev/null +++ b/main/src/mill/main/RunScript.scala @@ -0,0 +1,240 @@ +package mill.main + +import java.nio.file.NoSuchFileException + +import ammonite.interp.Interpreter +import ammonite.ops.{Path, read} +import ammonite.runtime.SpecialClassLoader +import ammonite.util.Util.CodeSource +import ammonite.util.{Name, Res, Util} +import mill.define +import mill.define.{Discover, ExternalModule, Segment, Task} +import mill.eval.{Evaluator, PathRef, Result} +import mill.util.{EitherOps, Logger, ParseArgs, Watched} +import mill.util.Strict.Agg +import upickle.Js + +/** + * 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{ + def runScript(wd: Path, + path: Path, + instantiateInterpreter: => Either[(Res.Failing, Seq[(Path, Long)]), ammonite.interp.Interpreter], + scriptArgs: Seq[String], + lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])], + log: Logger) + : (Res[(Evaluator[Any], Seq[(Path, Long)], Either[String, Seq[Js.Value]])], Seq[(Path, Long)]) = { + + val (evalRes, interpWatched) = lastEvaluator match{ + case Some((prevInterpWatchedSig, prevEvaluator)) + if watchedSigUnchanged(prevInterpWatchedSig) => + + (Res.Success(prevEvaluator), prevInterpWatchedSig) + + case _ => + instantiateInterpreter match{ + case Left((res, watched)) => (res, watched) + case Right(interp) => + interp.watch(path) + val eval = + for((mapping, discover) <- evaluateMapping(wd, path, interp)) + yield new Evaluator[Any]( + wd / 'out, wd / 'out, mapping, discover, log, + mapping.getClass.getClassLoader.asInstanceOf[SpecialClassLoader].classpathSignature + ) + + (eval, interp.watchedFiles) + } + } + + val evaluated = for{ + evaluator <- evalRes + (evalWatches, res) <- Res(evaluateTarget(evaluator, scriptArgs)) + } yield { + val alreadyStale = evalWatches.exists(p => p.sig != new PathRef(p.path, p.quick).sig) + // If the file changed between the creation of the original + // `PathRef` and the current moment, use random junk .sig values + // to force an immediate re-run. Otherwise calculate the + // pathSignatures the same way Ammonite would and hand over the + // values, so Ammonite can watch them and only re-run if they + // subsequently change + val evaluationWatches = + if (alreadyStale) evalWatches.map(_.path -> util.Random.nextLong()) + else evalWatches.map(p => p.path -> Interpreter.pathSignature(p.path)) + + (evaluator, evaluationWatches, res.map(_.flatMap(_._2))) + } + (evaluated, interpWatched) + } + + def watchedSigUnchanged(sig: Seq[(Path, Long)]) = { + sig.forall{case (p, l) => Interpreter.pathSignature(p) == l} + } + + def evaluateMapping(wd: Path, + path: Path, + interp: ammonite.interp.Interpreter): Res[(mill.define.BaseModule, Discover[Any])] = { + + 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, + extraCode = "", + hardcoded = true + ) + + buildClsName <- processed.blockInfo.lastOption match { + case Some(meta) => Res.Success(meta.id.wrapperPath) + case None => Res.Skip + } + + buildCls = interp + .evalClassloader + .loadClass(buildClsName) + + module <- try { + Util.withContextClassloader(interp.evalClassloader) { + Res.Success( + buildCls.getMethod("millSelf") + .invoke(null) + .asInstanceOf[Some[mill.define.BaseModule]] + .get + ) + } + } catch { + case e: Throwable => Res.Exception(e, "") + } + discover <- try { + Util.withContextClassloader(interp.evalClassloader) { + Res.Success( + buildCls.getMethod("millDiscover") + .invoke(module) + .asInstanceOf[Discover[Any]] + ) + } + } catch { + case e: Throwable => Res.Exception(e, "") + } +// _ <- Res(consistencyCheck(mapping)) + } yield (module, discover) + } + + def evaluateTarget[T](evaluator: Evaluator[T], scriptArgs: Seq[String]) = { + for { + parsed <- ParseArgs(scriptArgs) + (selectors, args) = parsed + targets <- { + val selected = selectors.map { case (scopedSel, sel) => + val (rootModule, discover) = scopedSel match{ + case None => (evaluator.rootModule, evaluator.discover) + case Some(scoping) => + val moduleCls = + evaluator.rootModule.getClass.getClassLoader.loadClass(scoping.render + "$") + + val rootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule] + (rootModule, rootModule.millDiscover) + } + val crossSelectors = sel.value.map { + case Segment.Cross(x) => x.toList.map(_.toString) + case _ => Nil + } + + try { + // We inject the `evaluator.rootModule` into the TargetScopt, rather + // than the `rootModule`, because even if you are running an external + // 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.eval.Evaluator.currentEvaluator.set(evaluator) + mill.main.Resolve.resolve( + sel.value.toList, rootModule, + discover, + args, crossSelectors.toList, Nil + ) + } finally{ + mill.eval.Evaluator.currentEvaluator.set(null) + } + } + EitherOps.sequence(selected) + } + } yield { + val (watched, res) = evaluate( + evaluator, + Agg.from(targets.flatten.distinct) + ) + + val watched2 = for{ + x <- res.right.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[upickle.Js.Value])]]) = { + val evaluated = evaluator.evaluate(targets) + val watched = evaluated.results + .iterator + .collect { + case (t: define.Sources, Result.Success(p: Seq[PathRef])) => p + } + .flatten + .toSeq + + val errorStr = + (for((k, fs) <- evaluated.failing.items()) yield { + val ks = k match{ + case Left(t) => t.toString + case Right(t) => t.segments.render + } + val fss = fs.map{ + case Result.Exception(t, outerStack) => + t.toString + + t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString + case Result.Failure(t, _) => t + } + s"$ks ${fss.mkString(", ")}" + }).mkString("\n") + + evaluated.failing.keyCount match { + case 0 => + val json = for(t <- targets.toSeq) yield { + t match { + case t: mill.define.NamedTask[_] => + val jsonFile = Evaluator + .resolveDestPaths(evaluator.outPath, t.ctx.segments) + .meta + val metadata = upickle.json.read(jsonFile.toIO) + Some(metadata(1)) + + case _ => None + } + } + + watched -> Right(evaluated.values.zip(json)) + case n => watched -> Left(s"$n targets failed\n$errorStr") + } + } + +// def consistencyCheck[T](mapping: Discovered.Mapping[T]): Either[String, Unit] = { +// val consistencyErrors = Discovered.consistencyCheck(mapping) +// if (consistencyErrors.nonEmpty) { +// Left(s"Failed Discovered.consistencyCheck: ${consistencyErrors.map(_.render)}") +// } else { +// Right(()) +// } +// } +} diff --git a/main/src/mill/modules/Jvm.scala b/main/src/mill/modules/Jvm.scala new file mode 100644 index 00000000..297dcf1f --- /dev/null +++ b/main/src/mill/modules/Jvm.scala @@ -0,0 +1,257 @@ +package mill.modules + +import java.io.FileOutputStream +import java.lang.reflect.Modifier +import java.net.URLClassLoader +import java.nio.file.attribute.PosixFilePermission +import java.util.jar.{JarEntry, JarFile, JarOutputStream} + +import ammonite.ops._ +import mill.define.Task +import mill.eval.PathRef +import mill.util.{Ctx, Loose} +import mill.util.Ctx.Log +import mill.util.Loose.Agg +import upickle.default.{Reader, Writer} + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.reflect.ClassTag + + +object Jvm { + + def interactiveSubprocess(mainClass: String, + classPath: Agg[Path], + jvmArgs: Seq[String] = Seq.empty, + envArgs: Map[String, String] = Map.empty, + mainArgs: Seq[String] = Seq.empty, + workingDir: Path = null): Unit = { + import ammonite.ops.ImplicitWd._ + val commandArgs = + Vector("java") ++ + jvmArgs ++ + Vector("-cp", classPath.mkString(":"), mainClass) ++ + mainArgs + + %.copy(envArgs = envArgs)(commandArgs)(workingDir) + } + + def runLocal(mainClass: String, + classPath: Agg[Path], + mainArgs: Seq[String] = Seq.empty) + (implicit ctx: Ctx): Unit = { + inprocess(classPath, classLoaderOverrideSbtTesting = false, cl => { + getMainMethod(mainClass, cl).invoke(null, mainArgs.toArray) + }) + } + + private def getMainMethod(mainClassName: String, cl: ClassLoader) = { + val mainClass = cl.loadClass(mainClassName) + val method = mainClass.getMethod("main", classOf[Array[String]]) + // jvm allows the actual main class to be non-public and to run a method in the non-public class, + // we need to make it accessible + method.setAccessible(true) + val modifiers = method.getModifiers + if (!Modifier.isPublic(modifiers)) + throw new NoSuchMethodException(mainClassName + ".main is not public") + if (!Modifier.isStatic(modifiers)) + throw new NoSuchMethodException(mainClassName + ".main is not static") + method + } + + + + def inprocess[T](classPath: Agg[Path], + classLoaderOverrideSbtTesting: Boolean, + body: ClassLoader => T): T = { + val cl = if (classLoaderOverrideSbtTesting) { + val outerClassLoader = getClass.getClassLoader + new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null){ + override def findClass(name: String) = { + if (name.startsWith("sbt.testing.")){ + outerClassLoader.loadClass(name) + }else{ + super.findClass(name) + } + } + } + } else { + new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null) + } + val oldCl = Thread.currentThread().getContextClassLoader + Thread.currentThread().setContextClassLoader(cl) + try { + body(cl) + }finally{ + Thread.currentThread().setContextClassLoader(oldCl) + cl.close() + } + } + + def subprocess(mainClass: String, + classPath: Agg[Path], + jvmArgs: Seq[String] = Seq.empty, + envArgs: Map[String, String] = Map.empty, + mainArgs: Seq[String] = Seq.empty, + workingDir: Path = null) + (implicit ctx: Ctx) = { + + val commandArgs = + Vector("java") ++ + jvmArgs ++ + Vector("-cp", classPath.mkString(":"), mainClass) ++ + mainArgs + + val workingDir1 = Option(workingDir).getOrElse(ctx.dest) + mkdir(workingDir1) + val builder = + new java.lang.ProcessBuilder() + .directory(workingDir1.toIO) + .command(commandArgs:_*) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + + for((k, v) <- envArgs) builder.environment().put(k, v) + val proc = builder.start() + val stdout = proc.getInputStream + val stderr = proc.getErrorStream + val sources = Seq( + (stdout, Left(_: Bytes), ctx.log.outputStream), + (stderr, Right(_: Bytes),ctx.log.errorStream ) + ) + val chunks = mutable.Buffer.empty[Either[Bytes, Bytes]] + while( + // Process.isAlive doesn't exist on JDK 7 =/ + util.Try(proc.exitValue).isFailure || + stdout.available() > 0 || + stderr.available() > 0 + ){ + var readSomething = false + for ((subStream, wrapper, parentStream) <- sources){ + while (subStream.available() > 0){ + readSomething = true + val array = new Array[Byte](subStream.available()) + val actuallyRead = subStream.read(array) + chunks.append(wrapper(new ammonite.ops.Bytes(array))) + parentStream.write(array, 0, actuallyRead) + } + } + // if we did not read anything sleep briefly to avoid spinning + if(!readSomething) + Thread.sleep(2) + } + + if (proc.exitValue() != 0) throw new InteractiveShelloutException() + else ammonite.ops.CommandResult(proc.exitValue(), chunks) + } + + private def createManifest(mainClass: Option[String]) = { + val m = new java.util.jar.Manifest() + m.getMainAttributes.put(java.util.jar.Attributes.Name.MANIFEST_VERSION, "1.0") + m.getMainAttributes.putValue( "Created-By", "Scala mill" ) + mainClass.foreach( + m.getMainAttributes.put(java.util.jar.Attributes.Name.MAIN_CLASS, _) + ) + m + } + + def createJar(inputPaths: Agg[Path], mainClass: Option[String] = None) + (implicit ctx: Ctx.Dest): PathRef = { + val outputPath = ctx.dest / "out.jar" + rm(outputPath) + + val seen = mutable.Set.empty[RelPath] + seen.add("META-INF" / "MANIFEST.MF") + val jar = new JarOutputStream( + new FileOutputStream(outputPath.toIO), + createManifest(mainClass) + ) + + try{ + assert(inputPaths.forall(exists(_))) + for{ + p <- inputPaths + (file, mapping) <- + if (p.isFile) Iterator(p -> empty/p.last) + else ls.rec(p).filter(_.isFile).map(sub => sub -> sub.relativeTo(p)) + if !seen(mapping) + } { + seen.add(mapping) + val entry = new JarEntry(mapping.toString) + entry.setTime(file.mtime.toMillis) + jar.putNextEntry(entry) + jar.write(read.bytes(file)) + jar.closeEntry() + } + } finally { + jar.close() + } + + PathRef(outputPath) + } + + def createAssembly(inputPaths: Agg[Path], + mainClass: Option[String] = None, + prependShellScript: String = "") + (implicit ctx: Ctx.Dest): PathRef = { + val outputPath = ctx.dest / "out.jar" + rm(outputPath) + + if(inputPaths.nonEmpty) { + + val output = new FileOutputStream(outputPath.toIO) + + // Prepend shell script and make it executable + if (prependShellScript.nonEmpty) { + output.write((prependShellScript + "\n").getBytes) + val perms = java.nio.file.Files.getPosixFilePermissions(outputPath.toNIO) + perms.add(PosixFilePermission.GROUP_EXECUTE) + perms.add(PosixFilePermission.OWNER_EXECUTE) + perms.add(PosixFilePermission.OTHERS_EXECUTE) + java.nio.file.Files.setPosixFilePermissions(outputPath.toNIO, perms) + } + + val jar = new JarOutputStream( + output, + createManifest(mainClass) + ) + + val seen = mutable.Set("META-INF/MANIFEST.MF") + try{ + + + for{ + p <- inputPaths + if exists(p) + (file, mapping) <- + if (p.isFile) { + val jf = new JarFile(p.toIO) + import collection.JavaConverters._ + for(entry <- jf.entries().asScala if !entry.isDirectory) yield { + read.bytes(jf.getInputStream(entry)) -> entry.getName + } + } + else { + ls.rec(p).iterator + .filter(_.isFile) + .map(sub => read.bytes(sub) -> sub.relativeTo(p).toString) + } + if !seen(mapping) + } { + seen.add(mapping) + val entry = new JarEntry(mapping.toString) + jar.putNextEntry(entry) + jar.write(file) + jar.closeEntry() + } + } finally { + jar.close() + output.close() + } + + } + PathRef(outputPath) + } + +} diff --git a/main/src/mill/modules/Util.scala b/main/src/mill/modules/Util.scala new file mode 100644 index 00000000..cef11859 --- /dev/null +++ b/main/src/mill/modules/Util.scala @@ -0,0 +1,65 @@ +package mill.modules + + +import ammonite.ops.{Path, RelPath, empty, mkdir, read} +import mill.eval.PathRef +import mill.util.Ctx + +object Util { + def download(url: String, dest: RelPath = "download")(implicit ctx: Ctx.Dest) = { + val out = ctx.dest / dest + + val website = new java.net.URI(url).toURL + val rbc = java.nio.channels.Channels.newChannel(website.openStream) + try{ + val fos = new java.io.FileOutputStream(out.toIO) + try{ + fos.getChannel.transferFrom(rbc, 0, java.lang.Long.MAX_VALUE) + PathRef(out) + } finally{ + fos.close() + } + } finally{ + rbc.close() + } + } + + def downloadUnpackZip(url: String, dest: RelPath = "unpacked") + (implicit ctx: Ctx.Dest) = { + + val tmpName = if (dest == empty / "tmp.zip") "tmp2.zip" else "tmp.zip" + val downloaded = download(url, tmpName) + unpackZip(downloaded.path, dest) + } + + def unpackZip(src: Path, dest: RelPath = "unpacked") + (implicit ctx: Ctx.Dest) = { + + val byteStream = read.getInputStream(src) + val zipStream = new java.util.zip.ZipInputStream(byteStream) + while({ + zipStream.getNextEntry match{ + case null => false + case entry => + if (!entry.isDirectory) { + val entryDest = ctx.dest / dest / RelPath(entry.getName) + mkdir(entryDest / ammonite.ops.up) + val fileOut = new java.io.FileOutputStream(entryDest.toString) + val buffer = new Array[Byte](4096) + while ( { + zipStream.read(buffer) match { + case -1 => false + case n => + fileOut.write(buffer, 0, n) + true + } + }) () + fileOut.close() + } + zipStream.closeEntry() + true + } + })() + PathRef(ctx.dest / dest) + } +} diff --git a/main/src/mill/package.scala b/main/src/mill/package.scala new file mode 100644 index 00000000..93916c8b --- /dev/null +++ b/main/src/mill/package.scala @@ -0,0 +1,12 @@ +import mill.util.JsonFormatters + +package object mill extends JsonFormatters{ + val T = define.Target + type T[T] = define.Target[T] + val PathRef = mill.eval.PathRef + type PathRef = mill.eval.PathRef + type Module = define.Module + type Cross[T] = define.Cross[T] + type Agg[T] = util.Loose.Agg[T] + val Agg = util.Loose.Agg +} |