diff --git a/build.sc b/build.sc index b2745404d92..633376c030e 100755 --- a/build.sc +++ b/build.sc @@ -232,7 +232,10 @@ object contrib extends MillModule { object twirllib extends MillModule { def moduleDeps = Seq(scalalib) + } + object playlib extends MillModule { + def moduleDeps = Seq(scalalib) } object scalapblib extends MillModule { diff --git a/ci/test-mill-0.sh b/ci/test-mill-0.sh index 92dc34c58c3..b1dd7e49f09 100755 --- a/ci/test-mill-0.sh +++ b/ci/test-mill-0.sh @@ -6,4 +6,4 @@ set -eux git clean -xdf # Run tests -mill -i all {main,scalalib,scalajslib,contrib.twirllib,main.client,contrib.scalapblib}.test +mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,main.client,contrib.scalapblib}.test diff --git a/ci/test-mill-bootstrap.sh b/ci/test-mill-bootstrap.sh index df8d086d15d..f95c06465cd 100755 --- a/ci/test-mill-bootstrap.sh +++ b/ci/test-mill-bootstrap.sh @@ -27,4 +27,4 @@ git clean -xdf rm -rf ~/.mill # Use second build to run tests using Mill -~/mill-2 -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.scalapblib}.test +~/mill-2 -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,contrib.scalapblib}.test diff --git a/ci/test-mill-dev.sh b/ci/test-mill-dev.sh index ab4a4d197bb..ccd03beb8f3 100755 --- a/ci/test-mill-dev.sh +++ b/ci/test-mill-dev.sh @@ -11,5 +11,5 @@ mill -i dev.assembly rm -rf ~/.mill # Second build & run tests -out/dev/assembly/dest/mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.scalapblib}.test +out/dev/assembly/dest/mill -i all {main,scalalib,scalajslib,contrib.twirllib,contrib.playlib,contrib.scalapblib}.test diff --git a/contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala b/contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala new file mode 100644 index 00000000000..a246bf2a0d2 --- /dev/null +++ b/contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala @@ -0,0 +1,117 @@ +package mill +package playlib + +import java.io.File +import java.net.URLClassLoader + +import ammonite.ops.Path +import mill.eval.PathRef +import mill.scalalib.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 + p.mtime.toMillis).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 new file mode 100644 index 00000000000..947e895be6a --- /dev/null +++ b/contrib/playlib/src/mill/playlib/RouterModule.scala @@ -0,0 +1,52 @@ +package mill +package playlib + +import coursier.{Cache, MavenRepository} +import mill.scalalib.Lib.resolveDependencies +import mill.scalalib._ +import mill.util.Loose + +trait RouterModule extends mill.Module { + + def playVersion: T[String] + + def routesFile: T[PathRef] = T { + val routesPath = millSourcePath / "conf" / "routes" + PathRef(routesPath) + } + + def routerClasspath: T[Loose.Agg[PathRef]] = T { + resolveDependencies( + Seq( + Cache.ivy2Local, + MavenRepository("https://repo1.maven.org/maven2") + ), + Lib.depToDependency(_, "2.12.4"), + Seq( + ivy"com.typesafe.play::routes-compiler:${playVersion()}" + ) + ) + } + + def routesAdditionalImport: Seq[String] = Seq( + "controllers.Assets.Asset", + "play.libs.F" + ) + + def generateForwardsRouter: Boolean = true + + def generateReverseRouter: Boolean = true + + def namespaceReverseRouter: Boolean = false + + def compileRouter: T[CompilationResult] = T.persistent { + RouterGeneratorWorkerApi.routerGeneratorWorker + .compile(routerClasspath().map(_.path), + routesFile().path, + routesAdditionalImport, + generateForwardsRouter, + generateReverseRouter, + namespaceReverseRouter, + T.ctx().dest) + } +} \ No newline at end of file diff --git a/contrib/playlib/test/resources/hello-world/core/conf/routes b/contrib/playlib/test/resources/hello-world/core/conf/routes new file mode 100644 index 00000000000..7d5e54984c4 --- /dev/null +++ b/contrib/playlib/test/resources/hello-world/core/conf/routes @@ -0,0 +1,2 @@ +GET / controllers.HomeController.index +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/contrib/playlib/test/src/mill/playlib/HelloWorldTests.scala b/contrib/playlib/test/src/mill/playlib/HelloWorldTests.scala new file mode 100644 index 00000000000..e4dc73990e3 --- /dev/null +++ b/contrib/playlib/test/src/mill/playlib/HelloWorldTests.scala @@ -0,0 +1,77 @@ +package mill.playlib + +import ammonite.ops.{Path, cp, ls, mkdir, pwd, rm, _} +import mill.util.{TestEvaluator, TestUtil} +import utest.framework.TestPath +import utest.{TestSuite, Tests, assert, _} + +object HelloWorldTests extends TestSuite { + + trait HelloBase extends TestUtil.BaseModule { + override def millSourcePath: Path = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.') + } + + trait HelloWorldModule extends mill.playlib.RouterModule { + def playVersion = "2.6.15" + } + + object HelloWorld extends HelloBase { + + object core extends HelloWorldModule { + override def playVersion = "2.6.14" + } + } + + val resourcePath: Path = pwd / 'contrib / 'playlib / 'test / 'resources / "hello-world" + + def workspaceTest[T, M <: TestUtil.BaseModule](m: M, resourcePath: Path = resourcePath) + (t: TestEvaluator[M] => T) + (implicit tp: TestPath): T = { + val eval = new TestEvaluator(m) + rm(m.millSourcePath) + rm(eval.outPath) + mkdir(m.millSourcePath / up) + cp(resourcePath, m.millSourcePath) + t(eval) + } + + def tests: Tests = Tests { + 'playVersion - { + + 'fromBuild - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.playVersion) + + assert( + result == "2.6.14", + evalCount > 0 + ) + } + } + 'compileRouter - workspaceTest(HelloWorld) { eval => + val Right((result, evalCount)) = eval.apply(HelloWorld.core.compileRouter) + + val outputFiles = ls.rec(result.classes.path).filter(_.isFile) + val expectedClassfiles = Seq[RelPath]( + RelPath("controllers/ReverseRoutes.scala"), + RelPath("controllers/routes.java"), + RelPath("router/Routes.scala"), + RelPath("router/RoutesPrefix.scala"), + RelPath("controllers/javascript/JavaScriptReverseRoutes.scala") + ).map( + eval.outPath / 'core / 'compileRouter / 'dest / _ + ) + assert( + result.classes.path == eval.outPath / 'core / 'compileRouter / 'dest, + outputFiles.nonEmpty, + outputFiles.forall(expectedClassfiles.contains), + outputFiles.size == 5, + evalCount > 0 + ) + + // don't recompile if nothing changed + val Right((_, unchangedEvalCount)) = eval.apply(HelloWorld.core.compileRouter) + + assert(unchangedEvalCount == 0) + } + } +}