Skip to content

Commit 69cb09a

Browse files
committed
3-stage compile to support Groovy <-> Java cycles
1 parent ce19a26 commit 69cb09a

File tree

5 files changed

+123
-80
lines changed

5 files changed

+123
-80
lines changed

libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,28 @@ import mill.api.TaskCtx
44
import mill.api.Result
55
import mill.javalib.api.CompilationResult
66

7+
/**
8+
* Runs the actual compilation.
9+
*
10+
* Supports 3-stage compilation for Java <-> Groovy
11+
* 1. compile Java stubs
12+
* 2. compile Java sources (done externally)
13+
* 3. compile Groovy sources
14+
*/
715
trait GroovyWorker {
816

9-
def compile(sourceFiles: Seq[os.Path], classpath: Seq[os.Path], outputDir: os.Path)(implicit
10-
ctx: TaskCtx
11-
): Result[CompilationResult]
17+
/**
18+
* In a mixed setup this will compile the Groovy sources to Java stubs.
19+
*/
20+
def compileGroovyStubs(sourceFiles: Seq[os.Path], classpath: Seq[os.Path], outputDir: os.Path)
21+
(implicit ctx: TaskCtx)
22+
: Result[CompilationResult]
23+
24+
/**
25+
* Compiles the Groovy sources. In a mixed setup this method assumes that the Java stubs
26+
* are already present in the outputDir.
27+
*/
28+
def compile(sourceFiles: Seq[os.Path], classpath: Seq[os.Path], outputDir: os.Path)
29+
(implicit ctx: TaskCtx)
30+
: Result[CompilationResult]
1231
}

libs/groovylib/src/mill/groovylib/GroovyModule.scala

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer =>
9090
*/
9191
def groovyCompilerMvnDeps: T[Seq[Dep]] = Task {
9292
val gv = groovyVersion()
93-
9493
val compilerDep = mvn"org.apache.groovy:groovy-all:$gv"
95-
// val annotationDep = mvn"org.codehaus.groovy:groovy-all-annotations:$gv"
96-
9794
Seq(compilerDep)
9895
}
9996

@@ -157,36 +154,39 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer =>
157154
reportOldProblems = zincReportCachedProblems()
158155
)
159156
}
157+
158+
if(isMixed){
159+
ctx.log.info("Compiling Groovy stubs for mixed compilation")
160160

161-
if (isMixed || isGroovy) {
161+
val workerStubResult =
162+
GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) {
163+
_.compileGroovyStubs(groovySourceFiles, compileCp, classes)
164+
}
165+
workerStubResult match {
166+
case Result.Success(_) => compileJava
167+
case Result.Failure(reason) => Result.Failure(reason)
168+
}
169+
}
170+
171+
if(isMixed || isGroovy){
162172
ctx.log.info(
163173
s"Compiling ${groovySourceFiles.size} Groovy sources to ${classes} ..."
164174
)
165-
166-
val compileCp = compileClasspath().map(_.path).filter(os.exists)
167-
168-
val workerResult =
175+
176+
val workerGroovyResult =
169177
GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) {
170178
_.compile(groovySourceFiles, compileCp, classes)
171179
}
172180

173181
val analysisFile = dest / "groovy.analysis.dummy" // needed for mills CompilationResult
174182
os.write(target = analysisFile, data = "", createFolders = true)
175183

176-
workerResult match {
184+
workerGroovyResult match {
177185
case Result.Success(_) =>
178-
val cr = CompilationResult(analysisFile, PathRef(classes))
179-
if (!isJava) {
180-
// pure Groovy project
181-
cr
182-
} else {
183-
// also run Java compiler and use it's returned result
184-
compileJava
185-
}
186+
CompilationResult(analysisFile, PathRef(classes))
186187
case Result.Failure(reason) => Result.Failure(reason)
187188
}
188-
} else {
189-
// it's Java only
189+
}else {
190190
compileJava
191191
}
192192
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package hello.maven.tests
2+
3+
//import hello.Hello
4+
import org.junit.jupiter.api.Test
5+
import static org.junit.jupiter.api.Assertions.assertEquals
6+
7+
class HelloMavenTestOnly {
8+
@Test
9+
void testSuccess() {
10+
assertEquals(true, true)
11+
}
12+
}

libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala

Lines changed: 23 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package mill
22
package groovylib
33

4-
import mill.javalib.{JavaModule, MavenModule, TestModule}
5-
import mill.api.{ExecResult, Task}
4+
import mill.javalib.{JavaModule, TestModule}
5+
import mill.api.{ Task}
66
import mill.api.Discover
77
import mill.testkit.{TestRootModule, UnitTester}
88
import utest.*
@@ -17,12 +17,12 @@ object HelloGroovyTests extends TestSuite {
1717
lazy val millDiscover = Discover[this.type]
1818

1919
// needed for a special test where only the tests are written in Groovy while appcode remains Java
20-
object `mixed-compile` extends JavaModule with MavenModule {
20+
object `groovy-tests` extends JavaMavenModuleWithGroovyTests {
2121

22-
object `test` extends TestGroovyMavenModule with TestModule.Junit5 {
22+
object `test` extends GroovyMavenTests with TestModule.Junit5 {
2323

2424
override def moduleDeps: Seq[JavaModule] = Seq(
25-
HelloGroovy.`mixed-compile`, // TODO improve: TestOnly does not inherit outer deps
25+
HelloGroovy.`groovy-tests`,
2626
)
2727

2828
override def groovyVersion = groovy4Version
@@ -37,6 +37,7 @@ object HelloGroovyTests extends TestSuite {
3737

3838
object `joint-compile` extends GroovyModule {
3939
override def groovyVersion: T[String] = groovy4Version
40+
override def mainClass = Some("jointcompile.JavaMain")
4041
}
4142

4243
trait Test extends GroovyModule {
@@ -92,63 +93,16 @@ object HelloGroovyTests extends TestSuite {
9293
def tests: Tests = Tests {
9394

9495
def m = HelloGroovy.main
95-
def mixed = HelloGroovy.`mixed-compile`
96+
def mixed = HelloGroovy.`groovy-tests`
97+
def joint = HelloGroovy.`joint-compile`
9698

97-
test("running a Groovy script") {
98-
testEval().scoped { eval =>
99-
val Right(_) = eval.apply(m.script.run()): @unchecked
100-
}
101-
}
102-
103-
test("compile & run Groovy module") {
104-
testEval().scoped { eval =>
105-
val Right(result) = eval.apply(m.compile): @unchecked
106-
107-
assert(
108-
os.walk(result.value.classes.path).exists(_.last == "Hello.class")
109-
)
110-
111-
val Right(_) = eval.apply(m.run()): @unchecked
112-
}
113-
}
114-
115-
test("compile & run Groovy JUnit5 test") {
116-
testEval().scoped { eval =>
117-
118-
val Right(result) = eval.apply(m.test.compile): @unchecked
119-
120-
assert(
121-
os.walk(result.value.classes.path).exists(_.last == "HelloTest.class")
122-
)
123-
124-
val Right(discovered) = eval.apply(m.test.discoveredTestClasses): @unchecked
125-
assert(discovered.value == Seq("hello.tests.HelloTest"))
126-
127-
val Right(_) = eval.apply(m.test.testForked()): @unchecked
128-
}
129-
}
130-
131-
test("compiling & running a statically compiled Groovy") {
132-
testEval().scoped { eval =>
133-
val Right(result) = eval.apply(m.staticcompile.compile): @unchecked
134-
assert(
135-
os.walk(result.value.classes.path).exists(_.last == "HelloStatic.class")
136-
)
137-
val Right(_) = eval.apply(m.staticcompile.run()): @unchecked
138-
}
139-
}
14099

141-
test("compile & run test-only Maven JUnit5 test") {
100+
test("compile & test module (only test uses Groovy)") {
142101
testEval().scoped { eval =>
143102

144-
val Right(resultCompile) = eval.apply(mixed.compile): @unchecked
145-
assert(
146-
os.walk(resultCompile.value.classes.path).exists(_.last == "Greeter.class")
147-
)
148-
149103
val Right(_) = eval.apply(mixed.test.compile): @unchecked
150104
val Right(discovered) = eval.apply(mixed.test.discoveredTestClasses): @unchecked
151-
assert(discovered.value == Seq("tests.GreeterTests"))
105+
assert(discovered.value == Seq("hello.maven.tests.HelloMavenTestOnly"))
152106

153107
val Right(_) = eval.apply(mixed.test.testForked()): @unchecked
154108
}
@@ -169,7 +123,20 @@ object HelloGroovyTests extends TestSuite {
169123
}
170124
}
171125

126+
test("compile joint (groovy <-> java cycle) & run") {
127+
testEval().scoped { eval =>
128+
val Right(result) = eval.apply(joint.compile): @unchecked
172129

130+
assert(
131+
os.walk(result.value.classes.path).exists(_.last == "JavaPrinter.class")
132+
)
133+
assert(
134+
os.walk(result.value.classes.path).exists(_.last == "GroovyGreeter.class")
135+
)
136+
137+
val Right(_) = eval.apply(joint.run()): @unchecked
138+
}
139+
}
173140

174141
}
175142
}

libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,46 @@ import mill.api.TaskCtx
66
import mill.javalib.api.CompilationResult
77
import mill.groovylib.worker.api.GroovyWorker
88
import org.codehaus.groovy.control.{CompilationUnit, CompilerConfiguration, Phases}
9+
import org.codehaus.groovy.tools.javac.JavaStubCompilationUnit
10+
import os.Path
911

1012
import scala.jdk.CollectionConverters.*
1113
import scala.util.Try
1214

1315
class GroovyWorkerImpl extends GroovyWorker {
1416

17+
18+
override def compileGroovyStubs(sourceFiles: Seq[Path], classpath: Seq[Path], outputDir: Path)
19+
(implicit ctx: TaskCtx): Result[CompilationResult] = {
20+
val config = new CompilerConfiguration()
21+
config.setTargetDirectory(outputDir.toIO)
22+
config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava)
23+
config.setJointCompilationOptions(Map(
24+
"stubDir" -> outputDir.toIO,
25+
"keepStubs" -> false
26+
).asJava)
27+
28+
// we need to set the classloader for groovy to use the worker classloader
29+
val parentCl: ClassLoader = this.getClass.getClassLoader
30+
// config in the GroovyClassLoader is needed when the CL itself is compiling classes
31+
val gcl = new GroovyClassLoader(parentCl, config)
32+
// config for actual compilation
33+
val stubUnit = JavaStubCompilationUnit(config, gcl)
34+
35+
sourceFiles.foreach { sourceFile =>
36+
stubUnit.addSource(sourceFile.toIO)
37+
}
38+
39+
Try {
40+
stubUnit.compile(Phases.CONVERSION)
41+
CompilationResult(outputDir, mill.api.PathRef(outputDir))
42+
}.fold(
43+
exception => Result.Failure(s"Groovy stub generation failed: ${exception.getMessage}"),
44+
result => Result.Success(result)
45+
)
46+
47+
}
48+
1549
def compile(
1650
sourceFiles: Seq[os.Path],
1751
classpath: Seq[os.Path],
@@ -20,12 +54,13 @@ class GroovyWorkerImpl extends GroovyWorker {
2054
ctx: TaskCtx
2155
): Result[CompilationResult] = {
2256

57+
val extendedClasspath = classpath :+ outputDir
58+
2359
val config = new CompilerConfiguration()
2460
config.setTargetDirectory(outputDir.toIO)
25-
config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava)
61+
config.setClasspathList(extendedClasspath.map(_.toIO.getAbsolutePath).asJava)
2662
// TODO
2763
// config.setDisabledGlobalASTTransformations()
28-
// config.setJointCompilationOptions()
2964
// config.setSourceEncoding()
3065

3166
// we need to set the classloader for groovy to use the worker classloader
@@ -41,10 +76,20 @@ class GroovyWorkerImpl extends GroovyWorker {
4176

4277
Try {
4378
unit.compile(Phases.OUTPUT)
79+
removeAllJavaFiles(outputDir)
4480
CompilationResult(outputDir, mill.api.PathRef(outputDir))
4581
}.fold(
4682
exception => Result.Failure(s"Groovy compilation failed: ${exception.getMessage}"),
4783
result => Result.Success(result)
4884
)
4985
}
86+
87+
88+
private def removeAllJavaFiles(outputDir: os.Path): Unit = {
89+
if (os.exists(outputDir)) {
90+
os.walk(outputDir)
91+
.filter(_.ext == "java")
92+
.foreach(os.remove)
93+
}
94+
}
5095
}

0 commit comments

Comments
 (0)