Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(wip) Create a Play! module to compile the router #421

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion ci/test-mill-0.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion ci/test-mill-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion ci/test-mill-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

117 changes: 117 additions & 0 deletions contrib/playlib/src/mill/playlib/RouterGeneratorWorker.scala
Original file line number Diff line number Diff line change
@@ -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("")))))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure about the return type... Do we want to return a CompilationResult ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it failed, we can either return a Option, Either, or mill.eval.Result.Error/mill.eval.Result.Exception. I don't care much either way, except returning an invalid CompilationResult is probably unacceptable since anyone trying to use it will find garbage inside

}
}
}
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()
}
52 changes: 52 additions & 0 deletions contrib/playlib/src/mill/playlib/RouterModule.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions contrib/playlib/test/resources/hello-world/core/conf/routes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GET / controllers.HomeController.index
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
77 changes: 77 additions & 0 deletions contrib/playlib/test/src/mill/playlib/HelloWorldTests.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object HelloWorldTests extends TestSuite {
override def millSourcePath: Path = TestUtil.getSrcPathBase() / millOuterCtx.enclosing.split('.')
}

trait HelloWorldModule extends mill.twirllib.TwirlModule {
trait HelloWorldModule extends TwirlModule {
def twirlVersion = "1.0.0"
}

Expand Down