diff options
Diffstat (limited to 'main/src/mill/main/RunScript.scala')
-rw-r--r-- | main/src/mill/main/RunScript.scala | 240 |
1 files changed, 240 insertions, 0 deletions
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(()) +// } +// } +} |