From 3fe65a95534aaafb6ce60ac387f45171929f26b5 Mon Sep 17 00:00:00 2001 From: Jean Helou Date: Mon, 11 Feb 2019 22:21:18 +0100 Subject: Reworks the toplevel playlib module This is the first commit of a redesign of the play lib module. The new design was massively inspired from the `scalajslib` module. It adds a specialized worker for each version of play, both workers implement a common api from an `api`. The main module delegates to a `loader` which dynamically looks up the bridge instance through reflection then triggers the generation. - adds a `RouteCompilerWorkerApi` trait which establishes the bridge to the actual `RouteCompilerWorker`. - drops the existing `RouterGeneratorWorker` (it is specialized by versions of play and extracted to its own submodule). - updates the `RouterModule` with improved settings and documentation. --- .../src/mill/playlib/RouteCompilerWorkerApi.scala | 68 ++++++++++++ .../src/mill/playlib/RouterGeneratorWorker.scala | 117 --------------------- .../playlib/src/mill/playlib/RouterModule.scala | 100 ++++++++++++++---- 3 files changed, 146 insertions(+), 139 deletions(-) create mode 100644 contrib/playlib/src/mill/playlib/RouteCompilerWorkerApi.scala delete mode 100644 contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala (limited to 'contrib/playlib') diff --git a/contrib/playlib/src/mill/playlib/RouteCompilerWorkerApi.scala b/contrib/playlib/src/mill/playlib/RouteCompilerWorkerApi.scala new file mode 100644 index 00000000..6e9f68ed --- /dev/null +++ b/contrib/playlib/src/mill/playlib/RouteCompilerWorkerApi.scala @@ -0,0 +1,68 @@ +package mill.playlib + +import ammonite.ops.Path +import mill._ +import mill.api.{Ctx, Result} +import mill.define.{Discover, ExternalModule, Worker} +import mill.playlib.api.RouteCompilerType +import mill.scalalib.api.CompilationResult + +class RouteCompilerWorker { + private var routeCompilerInstanceCache = Option.empty[(Long, mill.playlib.api.RouteCompilerWorkerApi)] + + private def bridge(toolsClasspath: Agg[os.Path]) + (implicit ctx: Ctx) = { + val classloaderSig = + toolsClasspath.map(p => p.toString().hashCode + os.mtime(p)).sum + routeCompilerInstanceCache match { + case Some((sig, bridge)) if sig == classloaderSig => bridge + case _ => + val toolsClassPath = toolsClasspath.map(_.toIO.toURI.toURL).toVector + ctx.log.debug("Loading classes from\n"+toolsClassPath.mkString("\n")) + val cl = mill.api.ClassLoader.create( + toolsClassPath, + getClass.getClassLoader + ) + val bridge = cl + .loadClass("mill.playlib.worker.RouteCompilerWorker") + .getDeclaredConstructor() + .newInstance() + .asInstanceOf[mill.playlib.api.RouteCompilerWorkerApi] + routeCompilerInstanceCache = Some((classloaderSig, bridge)) + bridge + } + } + + + def compile(routerClasspath: Agg[Path], + file: Path, + additionalImports: Seq[String], + forwardsRouter: Boolean, + reverseRouter: Boolean, + namespaceReverseRouter: Boolean, + generatorType: RouteCompilerType, + dest: Path)(implicit ctx: Ctx) + : Result[CompilationResult] = { + bridge(routerClasspath) + .compile( + file, + additionalImports, + forwardsRouter, + reverseRouter, + namespaceReverseRouter, + generatorType, + dest + )(ctx) + } + + +} + +object RouteCompilerWorkerModule extends ExternalModule { + def routeCompilerWorker: Worker[RouteCompilerWorker] = T.worker { + new RouteCompilerWorker() + } + + lazy val millDiscover = Discover[this.type] +} + diff --git a/contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala b/contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala deleted file mode 100644 index 35eaf047..00000000 --- a/contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala +++ /dev/null @@ -1,117 +0,0 @@ -package mill -package playlib - -import java.io.File -import java.net.URLClassLoader - -import ammonite.ops.Path -import mill.eval.PathRef -import mill.scalalib.api.CompilationResult - -import scala.collection.JavaConverters._ - -class RouterGeneratorWorker { - - private var routerGeneratorInstances = Option.empty[(Long, RouterGeneratorWorkerApi)] - - private def router(routerClasspath: Agg[Path]) = { - val classloaderSig = routerClasspath.map(p => p.toString().hashCode + os.mtime(p)).sum - routerGeneratorInstances match { - case Some((sig, instance)) if sig == classloaderSig => instance - case _ => - val cl = new URLClassLoader(routerClasspath.map(_.toIO.toURI.toURL).toArray, null) - val routerCompilerClass = cl.loadClass("play.routes.compiler.RoutesCompiler") - val routesCompilerTaskClass = cl.loadClass("play.routes.compiler.RoutesCompiler$RoutesCompilerTask") - val routerCompilerTaskConstructor = routesCompilerTaskClass.getConstructor( - classOf[File], - cl.loadClass("scala.collection.Seq"), - classOf[Boolean], - classOf[Boolean], - classOf[Boolean]) - val staticRoutesGeneratorModule = cl.loadClass("play.routes.compiler.StaticRoutesGenerator$").getField("MODULE$") - val injectedRoutesGeneratorModule = cl.loadClass("play.routes.compiler.InjectedRoutesGenerator$").getField("MODULE$") - val compileMethod = routerCompilerClass.getMethod("compile", - routesCompilerTaskClass, - cl.loadClass("play.routes.compiler.RoutesGenerator"), - classOf[java.io.File]) - val instance = new RouterGeneratorWorkerApi { - override def compile(task: RoutesCompilerTask, generatorType: String = "injected", generatedDir: File): Either[Seq[CompilationResult], Seq[File]] = { - // Since the classloader is isolated, we do not share any classes with the Mill classloader. - // Thus both classloaders have different copies of "scala.collection.Seq" which are not compatible. - val additionalImports = cl.loadClass("scala.collection.mutable.WrappedArray$ofRef") - .getConstructors()(0) - .newInstance(task.additionalImports.toArray) - .asInstanceOf[AnyRef] - val args = Array[AnyRef](task.file, - additionalImports, - Boolean.box(task.forwardsRouter), - Boolean.box(task.reverseRouter), - Boolean.box(task.namespaceReverseRouter)) - val routesCompilerTaskInstance = routerCompilerTaskConstructor.newInstance(args: _*).asInstanceOf[Object] - val routesGeneratorInstance = generatorType match { - case "injected" => injectedRoutesGeneratorModule.get(null) - case "static" => staticRoutesGeneratorModule.get(null) - case _ => throw new Exception(s"Unrecognized generator type: $generatorType. Use injected or static") - } - val result = compileMethod.invoke(null, - routesCompilerTaskInstance, - routesGeneratorInstance, - generatedDir) - // compile method returns an object of type Either[Seq[RoutesCompilationError], Seq[File]] - result.getClass.getName match { - case "scala.util.Right" => - val files = cl.loadClass("scala.util.Right") - .getMethod("value") - .invoke(result) - val asJavaMethod = cl.loadClass("scala.collection.convert.DecorateAsJava") - .getMethod("seqAsJavaListConverter", cl.loadClass("scala.collection.Seq")) - val javaConverters = cl.loadClass("scala.collection.JavaConverters$") - val javaConvertersInstance = javaConverters.getField("MODULE$").get(javaConverters) - val filesJava = cl.loadClass("scala.collection.convert.Decorators$AsJava") - .getMethod("asJava") - .invoke(asJavaMethod.invoke(javaConvertersInstance, files)) - .asInstanceOf[java.util.List[File]] - Right(filesJava.asScala) - case "scala.util.Left" => - // TODO: convert the error of type RoutesCompilationError to a CompilationResult - Left(Seq(CompilationResult(Path(""), PathRef(Path(""))))) - } - } - } - routerGeneratorInstances = Some((classloaderSig, instance)) - instance - } - } - - def compile(routerClasspath: Agg[Path], - file: Path, - additionalImports: Seq[String], - forwardsRouter: Boolean, - reverseRouter: Boolean, - namespaceReverseRouter: Boolean, - dest: Path) - (implicit ctx: mill.util.Ctx): mill.eval.Result[CompilationResult] = { - val compiler = router(routerClasspath) - - val result = compiler.compile(RoutesCompilerTask(file.toIO, additionalImports, forwardsRouter, reverseRouter, namespaceReverseRouter), generatedDir = dest.toIO) - - result match { - case Right(_) => - val zincFile = ctx.dest / 'zinc - mill.eval.Result.Success(CompilationResult(zincFile, PathRef(ctx.dest))) - case Left(_) => mill.eval.Result.Failure("Unable to compile the routes") // FIXME: convert the error to a Failure - } - } -} - -trait RouterGeneratorWorkerApi { - - def compile(task: RoutesCompilerTask, generatorType: String = "injected", generatedDir: File): Either[Seq[CompilationResult], Seq[File]] -} - -case class RoutesCompilerTask(file: File, additionalImports: Seq[String], forwardsRouter: Boolean, reverseRouter: Boolean, namespaceReverseRouter: Boolean) - -object RouterGeneratorWorkerApi { - - def routerGeneratorWorker = new RouterGeneratorWorker() -} diff --git a/contrib/playlib/src/mill/playlib/RouterModule.scala b/contrib/playlib/src/mill/playlib/RouterModule.scala index a7073492..ac1f71ff 100644 --- a/contrib/playlib/src/mill/playlib/RouterModule.scala +++ b/contrib/playlib/src/mill/playlib/RouterModule.scala @@ -2,53 +2,109 @@ package mill package playlib import coursier.{Cache, MavenRepository} -import scalalib.Lib -import scalalib.Lib.resolveDependencies -import scalalib.api._ -import scalalib._ import mill.api.Loose +import mill.playlib.api.RouteCompilerType +import mill.scalalib.Lib.resolveDependencies +import mill.scalalib._ +import mill.scalalib.api._ -trait RouterModule extends mill.Module { +trait RouterModule extends mill.Module with ScalaModule { + /** + * Defines the version of playframework to be used to compile this projects + * routes. + */ def playVersion: T[String] + override def generatedSources = T{ super.generatedSources() ++ Seq(compileRouter().classes) } + + /** + * The [[PathRef]] to the main routes file. + * + * This is the default path for play projects and it should be fine but you + * can override it if needed. + */ def routesFile: T[PathRef] = T { val routesPath = millSourcePath / "conf" / "routes" PathRef(routesPath) } + /** + * A [[Seq]] of additional imports to be added to the routes file. + * Defaults to : + * + */ + def routesAdditionalImport: Seq[String] = Seq( + "controllers.Assets.Asset", + "play.libs.F" + ) + + def generateForwardsRouter: Boolean = true + + def generateReverseRouter: Boolean = true + + def namespaceReverseRouter: Boolean = false + + /** + * The routes compiler type to be used. Can only be one of: + * + * @return + */ + def generatorType: RouteCompilerType = RouteCompilerType.InjectedGenerator + def routerClasspath: T[Loose.Agg[PathRef]] = T { resolveDependencies( Seq( Cache.ivy2Local, MavenRepository("https://repo1.maven.org/maven2") ), - Lib.depToDependency(_, "2.12.4"), + Lib.depToDependency(_, scalaVersion()), Seq( ivy"com.typesafe.play::routes-compiler:${playVersion()}" ) ) } - def routesAdditionalImport: Seq[String] = Seq( - "controllers.Assets.Asset", - "play.libs.F" - ) + final def compileRouter: T[CompilationResult] = T { + T.ctx().log.debug(s"compiling play routes with ${playVersion()} worker") + RouteCompilerWorkerModule.routeCompilerWorker().compile( + toolsClasspath().map(_.path), + routesFile().path, + routesAdditionalImport, + generateForwardsRouter, + generateReverseRouter, + namespaceReverseRouter, + generatorType, + T.ctx().dest) + } - def generateForwardsRouter: Boolean = true + private def playMinorVersion: T[String] = T { + playVersion().split("\\.").take(2).mkString("", ".", ".0") + } - def generateReverseRouter: Boolean = true + private def playRouteCompilerWorkerClasspath = T { + val workerKey = "MILL_CONTRIB_PLAYLIB_ROUTECOMPILER_WORKER_" + playMinorVersion().replace(".", + "_") + T.ctx.log.debug(s"classpath worker key: $workerKey") - def namespaceReverseRouter: Boolean = false + //While the following seems to work (tests pass), I am not completely + //confident that the strings I used for artifact and resolveFilter are + //actually correct + mill.modules.Util.millProjectModule( + workerKey, + s"mill-contrib-playlib-worker-${playVersion()}", + repositories, + resolveFilter = _.toString.contains("mill-contrib-playlib-worker") + ) + } - def compileRouter: T[CompilationResult] = T.persistent { - RouterGeneratorWorkerApi.routerGeneratorWorker - .compile(routerClasspath().map(_.path), - routesFile().path, - routesAdditionalImport, - generateForwardsRouter, - generateReverseRouter, - namespaceReverseRouter, - T.ctx().dest) + private def toolsClasspath = T { + playRouteCompilerWorkerClasspath() ++ routerClasspath() } } \ No newline at end of file -- cgit v1.2.3