diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d42d89e5a843..0eb23fbf7164 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -154,6 +154,10 @@ jobs: millargs: "'example.androidlib.__.local.daemon'" setup-android: true + - java-version: 17 + millargs: "'example.groovylib.__.local.daemon'" + setup-android: true + - java-version: 17 millargs: "'example.thirdparty[androidtodo].packaged.daemon'" setup-android: true diff --git a/core/api/daemon/src/mill/api/daemon/internal/GroovyModuleApi.scala b/core/api/daemon/src/mill/api/daemon/internal/GroovyModuleApi.scala new file mode 100644 index 000000000000..c64a187cfb27 --- /dev/null +++ b/core/api/daemon/src/mill/api/daemon/internal/GroovyModuleApi.scala @@ -0,0 +1,3 @@ +package mill.api.daemon.internal + +trait GroovyModuleApi extends JavaModuleApi diff --git a/core/api/daemon/src/mill/api/daemon/internal/bsp/BspModuleApi.scala b/core/api/daemon/src/mill/api/daemon/internal/bsp/BspModuleApi.scala index df66de24649c..43ea186bd958 100644 --- a/core/api/daemon/src/mill/api/daemon/internal/bsp/BspModuleApi.scala +++ b/core/api/daemon/src/mill/api/daemon/internal/bsp/BspModuleApi.scala @@ -15,6 +15,7 @@ object BspModuleApi { val Java = "java" val Scala = "scala" val Kotlin = "kotlin" + val Groovy = "groovy" } /** Used to define the [[BspBuildTarget.tags]] field. */ diff --git a/example/groovylib/basic/1-simple/build.mill b/example/groovylib/basic/1-simple/build.mill new file mode 100644 index 000000000000..246277785c36 --- /dev/null +++ b/example/groovylib/basic/1-simple/build.mill @@ -0,0 +1,124 @@ +//// SNIPPET:BUILD + +package build +import mill.*, groovylib.* + +object foo extends GroovyModule { + def groovyVersion = "5.0.1" + + def mvnDeps = Seq( + mvn"org.apache.groovy:groovy-cli-commons", // BOM already loaded by module + mvn"org.apache.groovy:groovy-xml" // BOM already loaded by module + ) + + def mainClass = Some("foo.Foo") + + object test extends GroovyTests with TestModule.Junit5 { + def jupiterVersion = "5.13.4" + + def mvnDeps = Seq( + mvn"org.apache.groovy:groovy-test" // BOM already loaded by module + ) + } +} + +// This is a basic Mill build for a single `GroovyModule`, with one +// third-party dependency and a test suite using the JUnit framework. +//// SNIPPET:TREE +// ---- +// build.mill +// foo/ +// src/ +// foo/Foo.groovy +// resources/ +// ... +// test/ +// src/ +// foo/FooTest.groovy +// out/foo/ +// compile.json +// compile.dest/ +// ... +// test/ +// compile.json +// compile.dest/ +// ... +// ---- +// +// NOTE: The default Mill source folder layout `foo/src/` differs from that of Maven/Gradle's +// `foo/src/main/groovy`. If you wish to use the Maven source folder layout, e.g. for migrating +// an existing codebase, you should use +// xref:#_maven_compatible_modules[Maven-Compatible Modules] +// +//// SNIPPET:DEPENDENCIES +// +// This example project uses two third-party dependencies +// - Groovy-Cli-Commons for CLI argument parsing +// - Groovy-Xml for HTML templating and escaping +// and uses them to wrap a given input string in HTML templates with proper escaping. +// +// Typical usage of a `GroovyModule` is shown below + +/** Usage + +> ./mill resolve foo._ # List what tasks are available to run +foo.assembly +... +foo.compile +... +foo.run +... +*/ +/** Usage +> ./mill inspect foo.compile # Show documentation and inputs of a task +foo.compile(GroovyModule...) + Compiles all the sources to JVM class files. + Compiles the current module to generate compiled classfiles/bytecode. +Inputs: + foo.allJavaSourceFiles + foo.allGroovySourceFiles + foo.compileClasspath + foo.upstreamCompileOutput + foo.javacOptions + foo.zincReportCachedProblems +... +*/ +/** Usage +> ./mill foo.compile # compile sources into classfiles +... +Compiling 1 Groovy sources to... +*/ +/** Usage +> ./mill foo.run # run the main method, if any +error: Error: missing option --text +... +*/ +/** Usage +> ./mill foo.run --text hello +

hello

+*/ +/** Usage +> ./mill foo.test +... +Test foo.FooTest testSimple finished, ... +Test foo.FooTest testEscaping finished, ... +Test foo.FooTest finished, ... +Test run finished: 0 failed, 0 ignored, 2 total, ... +*/ +/** Usage +> ./mill foo.assembly # bundle classfiles and libraries into a jar for deployment + +> ./mill show foo.assembly # show the output of the assembly task +".../out/foo/assembly.dest/out.jar" + +> java -jar ./out/foo/assembly.dest/out.jar --text hello +

hello

+ +> ./out/foo/assembly.dest/out.jar --text hello # mac/linux +

hello

+ +> cp ./out/foo/assembly.dest/out.jar out.bat # windows + +> ./out.bat --text hello # windows +

hello

+*/ diff --git a/example/groovylib/basic/1-simple/foo/src/foo/Foo.groovy b/example/groovylib/basic/1-simple/foo/src/foo/Foo.groovy new file mode 100644 index 000000000000..10e0cc77b475 --- /dev/null +++ b/example/groovylib/basic/1-simple/foo/src/foo/Foo.groovy @@ -0,0 +1,32 @@ +package foo + +import groovy.xml.MarkupBuilder +import groovy.cli.commons.CliBuilder + +class Foo { + static String generateHtml(String text) { + def writer = new StringWriter() + new MarkupBuilder(writer).h1 { + mkp.yield text + } + writer.toString() + } + + static void main(String[] args) { + def cli = new CliBuilder(usage:'help') + cli.t(longOpt:'text', args: 1, 'Passes text to the HTML generation') + def options = cli.parse(args) + + if (!options) { + return + } + + if (options.h) { + cli.usage() + return + } + + String textToProcess = options.t ?: "hello from main" + println generateHtml(textToProcess) + } +} diff --git a/example/groovylib/basic/1-simple/foo/test/src/foo/FooTest.groovy b/example/groovylib/basic/1-simple/foo/test/src/foo/FooTest.groovy new file mode 100644 index 000000000000..6e8f048873c7 --- /dev/null +++ b/example/groovylib/basic/1-simple/foo/test/src/foo/FooTest.groovy @@ -0,0 +1,15 @@ +package foo + +import org.junit.jupiter.api.Test + +class FooTest { + @Test + void "generate html created properly"() { + assert Foo.generateHtml("hello") == "

hello

" + } + + @Test + void "generated html is properly escaped"() { + assert Foo.generateHtml("") == "

<hello>

" + } +} diff --git a/example/groovylib/basic/2-compat-modules/build.mill b/example/groovylib/basic/2-compat-modules/build.mill new file mode 100644 index 000000000000..144115e09071 --- /dev/null +++ b/example/groovylib/basic/2-compat-modules/build.mill @@ -0,0 +1,57 @@ +//// SNIPPET:ALL +// Mill's default folder layout of `foo/src/` and `foo/test/src` differs from that +// of Maven or Gradle's `foo/src/main/groovy/` and `foo/src/test/groovy/`. If you are +// migrating an existing codebase, you can use Mill's `GroovyMavenModule` and +// `GroovyMavenTests` as shown below to preserve filesystem compatibility with an existing +// Maven or Gradle build: + +package build +import mill.*, groovylib.* + +object foo extends GroovyMavenModule { + + def groovyVersion = "5.0.1" + + object test extends GroovyMavenTests with TestModule.Junit5 {} + object integration extends GroovyMavenTests with TestModule.Junit5 {} +} + +// `GroovyMavenModule` is a variant of `GroovyModule` +// that uses the more verbose folder layout of Maven, `sbt`, and other tools: +// +// - `foo/src/main/java` +// - `foo/src/main/groovy` +// - `foo/src/test/java` +// - `foo/src/test/groovy` +// - `foo/src/integration/java` +// - `foo/src/integration/groovy` +// +// Rather than Mill's +// +// - `foo/src` +// - `foo/test/src` +// +// This is especially useful if you are migrating from Maven to Mill (or vice +// versa), during which a particular module may be built using both Maven and +// Mill at the same time + +/** Usage + +> ./mill foo.compile +Compiling 1 Groovy source... + +> ./mill foo.test.compile +Compiling 1 Groovy source... + +> ./mill foo.test.testForked +...foo.FooTests hello ... + +> ./mill foo.test +...foo.FooTests hello ... + +> ./mill foo.integration +...foo.FooIntegrationTests hello ... + +*/ + +// For more details on migrating from other build tools, see xref:migrating/migrating.adoc[] diff --git a/example/groovylib/basic/2-compat-modules/foo/src/integration/groovy/foo/FooIntegrationTests.groovy b/example/groovylib/basic/2-compat-modules/foo/src/integration/groovy/foo/FooIntegrationTests.groovy new file mode 100644 index 000000000000..a5c50328c1ff --- /dev/null +++ b/example/groovylib/basic/2-compat-modules/foo/src/integration/groovy/foo/FooIntegrationTests.groovy @@ -0,0 +1,12 @@ +package foo + +import org.junit.jupiter.api.Test + +class FooIntegrationTests { + @Test + void "hello should print correct greeting"() { + // Groovy creates an implicit class for the script named after the file + def foo = new Foo() + assert foo.hello() == "Hello World, Earth" + } +} diff --git a/example/groovylib/basic/2-compat-modules/foo/src/main/groovy/foo/Foo.groovy b/example/groovylib/basic/2-compat-modules/foo/src/main/groovy/foo/Foo.groovy new file mode 100644 index 000000000000..0054d20ec1b1 --- /dev/null +++ b/example/groovylib/basic/2-compat-modules/foo/src/main/groovy/foo/Foo.groovy @@ -0,0 +1,9 @@ +package foo + +def run(){ + println(hello()) +} + +def hello() { + "Hello World, ${Foo2.VALUE}" +} diff --git a/example/groovylib/basic/2-compat-modules/foo/src/main/java/foo/Foo2.java b/example/groovylib/basic/2-compat-modules/foo/src/main/java/foo/Foo2.java new file mode 100644 index 000000000000..47684927c22e --- /dev/null +++ b/example/groovylib/basic/2-compat-modules/foo/src/main/java/foo/Foo2.java @@ -0,0 +1,5 @@ +package foo; + +public class Foo2 { + public static final String VALUE = "Earth"; +} diff --git a/example/groovylib/basic/2-compat-modules/foo/src/test/groovy/foo/FooTests.groovy b/example/groovylib/basic/2-compat-modules/foo/src/test/groovy/foo/FooTests.groovy new file mode 100644 index 000000000000..80ba076e8959 --- /dev/null +++ b/example/groovylib/basic/2-compat-modules/foo/src/test/groovy/foo/FooTests.groovy @@ -0,0 +1,13 @@ +package foo + +import org.junit.jupiter.api.Test + +class FooTest { + + @Test + void "hello should print correct greeting"() { + // Groovy creates an implicit class for the script named after the file + def foo = new Foo() + assert foo.hello() == "Hello World, Earth" + } +} \ No newline at end of file diff --git a/example/groovylib/testing/1-test-suite/bar/src/bar/Bar.groovy b/example/groovylib/testing/1-test-suite/bar/src/bar/Bar.groovy new file mode 100644 index 000000000000..9ea74e14b77b --- /dev/null +++ b/example/groovylib/testing/1-test-suite/bar/src/bar/Bar.groovy @@ -0,0 +1,9 @@ +package bar + +def hello() { + "Hello World" +} + +def run(){ + println new Bar().hello() +} diff --git a/example/groovylib/testing/1-test-suite/bar/test/src/bar/BarTests.groovy b/example/groovylib/testing/1-test-suite/bar/test/src/bar/BarTests.groovy new file mode 100644 index 000000000000..3f9c8202d182 --- /dev/null +++ b/example/groovylib/testing/1-test-suite/bar/test/src/bar/BarTests.groovy @@ -0,0 +1,29 @@ +package bar + +import org.junit.jupiter.api.Test +import groovy.mock.interceptor.MockFor + +class BarTests{ + @Test + void "hello"() { + def result = new Bar().hello() + assert result == "Hello World" + } + + @Test + void "world"() { + def result = new Bar().hello() + assert result.endsWith("World") + } + + @Test + void "using groovy mocks"() { + def mockBar = new MockFor(Bar) + mockBar.demand.hello { "Hello GroovyMock World" } + + mockBar.use{ + def result = new Bar().hello() + assert result == "Hello GroovyMock World" + } + } +} diff --git a/example/groovylib/testing/1-test-suite/build.mill b/example/groovylib/testing/1-test-suite/build.mill new file mode 100644 index 000000000000..652533e72b59 --- /dev/null +++ b/example/groovylib/testing/1-test-suite/build.mill @@ -0,0 +1,31 @@ +//// SNIPPET:BUILD1 +package build +import mill.*, groovylib.* + +object foo extends GroovyModule { + + def groovyVersion = "5.0.1" + + object test extends GroovyTests with TestModule.Junit5 { + def jupiterVersion = "5.13.4" + + def mvnDeps = Seq( + mvn"org.apache.groovy:groovy-test" // BOM already loaded by module + ) + } +} + +object bar extends GroovyModule { + + def mainClass = Some("bar.Bar") + + def groovyVersion = "5.0.1" + + object test extends GroovyTests, TestModule.Junit5 { + def jupiterVersion = "5.13.4" + + def mvnDeps = Seq( + mvn"org.apache.groovy:groovy-test" // BOM already loaded by module + ) + } +} diff --git a/example/groovylib/testing/1-test-suite/foo/src/foo/Foo.groovy b/example/groovylib/testing/1-test-suite/foo/src/foo/Foo.groovy new file mode 100644 index 000000000000..3145fff350e6 --- /dev/null +++ b/example/groovylib/testing/1-test-suite/foo/src/foo/Foo.groovy @@ -0,0 +1,11 @@ +package foo + +class Foo { + def hello() { + "Hello World" + } + + static void main(String[] args){ + println Foo().hello() + } +} diff --git a/example/groovylib/testing/1-test-suite/foo/test/src/foo/FooTests.groovy b/example/groovylib/testing/1-test-suite/foo/test/src/foo/FooTests.groovy new file mode 100644 index 000000000000..5390008ede06 --- /dev/null +++ b/example/groovylib/testing/1-test-suite/foo/test/src/foo/FooTests.groovy @@ -0,0 +1,29 @@ +package foo + +import org.junit.jupiter.api.Test +import groovy.mock.interceptor.MockFor + +class FooMoreTests{ + @Test + void "hello"() { + def result = new Foo().hello() + assert result == "Hello World" + } + + @Test + void "world"() { + def result = new Foo().hello() + assert result.endsWith("World") + } + + @Test + void "using groovy mocks"() { + def mockFoo = new MockFor(Foo) + mockFoo.demand.hello { "Hello GroovyMock World" } + + mockFoo.use{ + def result = new Foo().hello() + assert result == "Hello GroovyMock World" + } + } +} diff --git a/example/groovylib/testing/2-integration-suite/build.mill b/example/groovylib/testing/2-integration-suite/build.mill new file mode 100644 index 000000000000..04c4f0f0a3da --- /dev/null +++ b/example/groovylib/testing/2-integration-suite/build.mill @@ -0,0 +1,20 @@ +//// SNIPPET:BUILD2 +package build +import mill.*, groovylib.* +object qux extends GroovyModule { + + def groovyVersion = "5.0.1" + + object test extends GroovyTests with TestModule.Junit5 { + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.apache.groovy:groovy-test" + ) + } + object integration extends GroovyTests with TestModule.Junit5 { + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"org.apache.groovy:groovy-test" + ) + } +} + +// The integration suite is just another regular test module within the parent GroovyModule diff --git a/example/groovylib/testing/2-integration-suite/qux/integration/src/qux/QuxIntegrationTests.groovy b/example/groovylib/testing/2-integration-suite/qux/integration/src/qux/QuxIntegrationTests.groovy new file mode 100644 index 000000000000..c04f366bd689 --- /dev/null +++ b/example/groovylib/testing/2-integration-suite/qux/integration/src/qux/QuxIntegrationTests.groovy @@ -0,0 +1,18 @@ +package qux + +import org.junit.jupiter.api.Test + +class QuxIntegrationTests { + + @Test + void "hello"() { + def result = new Qux().hello() + assert result == "Hello World" + } + + @Test + void "world"() { + def result = new Qux().hello() + assert result.endsWith("World") + } +} diff --git a/example/groovylib/testing/2-integration-suite/qux/src/qux/Qux.groovy b/example/groovylib/testing/2-integration-suite/qux/src/qux/Qux.groovy new file mode 100644 index 000000000000..c04e966f9105 --- /dev/null +++ b/example/groovylib/testing/2-integration-suite/qux/src/qux/Qux.groovy @@ -0,0 +1,11 @@ +package qux + +class Qux { + static void main(String[] args){ + println(hello()) + } + + String hello(){ + "Hello World" + } +} diff --git a/example/groovylib/testing/2-integration-suite/qux/test/src/qux/QuxTests.groovy b/example/groovylib/testing/2-integration-suite/qux/test/src/qux/QuxTests.groovy new file mode 100644 index 000000000000..b97ae1a45cde --- /dev/null +++ b/example/groovylib/testing/2-integration-suite/qux/test/src/qux/QuxTests.groovy @@ -0,0 +1,18 @@ +package qux + +import org.junit.jupiter.api.Test + +class QuxTests { + + @Test + void "hello"() { + def result = new Qux().hello() + assert result == "Hello World" + } + + @Test + void "world"() { + def result = new Qux().hello() + assert result.endsWith("World") + } +} \ No newline at end of file diff --git a/example/groovylib/testing/3-spock/build.mill b/example/groovylib/testing/3-spock/build.mill new file mode 100644 index 000000000000..331a776db914 --- /dev/null +++ b/example/groovylib/testing/3-spock/build.mill @@ -0,0 +1,13 @@ +//// SNIPPET:BUILD4 +package build +import mill.*, groovylib.* + +object `package` extends GroovyModule { + + def groovyVersion = "4.0.28" + + object test extends GroovyTests with TestModule.Spock { + def spockVersion = "2.3-groovy-4.0" + def jupiterVersion = "5.13.4" + } +} diff --git a/example/groovylib/testing/3-spock/src/foo/Calculator.groovy b/example/groovylib/testing/3-spock/src/foo/Calculator.groovy new file mode 100644 index 000000000000..015c700376b9 --- /dev/null +++ b/example/groovylib/testing/3-spock/src/foo/Calculator.groovy @@ -0,0 +1,8 @@ +package foo + +class Calculator { + + int add(int a, int b) { + return a + b + } +} diff --git a/example/groovylib/testing/3-spock/test/foo/CalculatorSpec.groovy b/example/groovylib/testing/3-spock/test/foo/CalculatorSpec.groovy new file mode 100644 index 000000000000..35f4535ab7f6 --- /dev/null +++ b/example/groovylib/testing/3-spock/test/foo/CalculatorSpec.groovy @@ -0,0 +1,17 @@ +package foo + +def "Calculator add: #a + #b = #expected"(){ + given: + def sut = new Calculator() + + when: + def result = sut.add(a, b) + + then: + result == expected + + where: + a | b | expected + 2 | 2 | 4 + -2 | -2 | -4 +} \ No newline at end of file diff --git a/example/groovylib/testing/4-spock-for-java/build.mill b/example/groovylib/testing/4-spock-for-java/build.mill new file mode 100644 index 000000000000..d6be164c3b44 --- /dev/null +++ b/example/groovylib/testing/4-spock-for-java/build.mill @@ -0,0 +1,22 @@ +//// SNIPPET:BUILD5 +package build +import mill.*, groovylib.* + +object `package` extends JavaMavenModuleWithGroovyTests { + + object test extends GroovyMavenTests with TestModule.Spock { + def spockVersion = "2.3-groovy-4.0" + def groovyVersion = "4.0.28" + def jupiterVersion = "5.13.4" + } +} + +// `JavaMavenModuleWithGroovyTests` is a variant of `JavaModule` which also +// includes `MavenModule` and has a special `test` trait for Groovy tests: +// +// - `foo/src/main/java` +// - `foo/src/test/groovy` +// +// This is usefull for Java projects which use Groovy only for testing. +// For non Maven-layout projects, this convenience trait is not necessary +// because you can just use `GroovyModule` for your test object instead. diff --git a/example/groovylib/testing/4-spock-for-java/src/main/java/foo/Calculator.java b/example/groovylib/testing/4-spock-for-java/src/main/java/foo/Calculator.java new file mode 100644 index 000000000000..bee08ac9ad6e --- /dev/null +++ b/example/groovylib/testing/4-spock-for-java/src/main/java/foo/Calculator.java @@ -0,0 +1,8 @@ +package foo; + +public class Calculator { + + public static int add(int a, int b) { + return a + b; + } +} diff --git a/example/groovylib/testing/4-spock-for-java/src/test/groovy/foo/CalculatorSpec.groovy b/example/groovylib/testing/4-spock-for-java/src/test/groovy/foo/CalculatorSpec.groovy new file mode 100644 index 000000000000..465b2bb4cd5e --- /dev/null +++ b/example/groovylib/testing/4-spock-for-java/src/test/groovy/foo/CalculatorSpec.groovy @@ -0,0 +1,12 @@ +package foo + +def "Calculator add: #a + #b = #result"(){ + expect: + Calculator.add(a, b) == result + + where: + a | b + 2 | 2 + -2 | -2 + result = a + b +} \ No newline at end of file diff --git a/example/package.mill b/example/package.mill index df36b154cdcb..197641a1cbcf 100644 --- a/example/package.mill +++ b/example/package.mill @@ -80,6 +80,11 @@ object `package` extends Module { object testing extends Cross[ExampleCrossModule](build.listCross) } + object groovylib extends Module { + object basic extends Cross[ExampleCrossModule](build.listCross) + object testing extends Cross[ExampleCrossModule](build.listCross) + } + object cli extends Module { object builtins extends Cross[ExampleCrossModule](build.listCross) object header extends Cross[ExampleCrossModule](build.listCross) diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala new file mode 100644 index 000000000000..0206c3d2d4fe --- /dev/null +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala @@ -0,0 +1,8 @@ +package mill.groovylib.worker.api + +@mill.api.experimental +case class GroovyCompilerConfiguration( + enablePreview: Boolean = false, + disabledGlobalAstTransformations: Set[String] = Set.empty, + targetBytecode: Option[String] = None +) diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala new file mode 100644 index 000000000000..ef1e8916e510 --- /dev/null +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -0,0 +1,43 @@ +package mill.groovylib.worker.api + +import mill.api.{Result, TaskCtx} +import mill.javalib.api.CompilationResult + +/** + * Runs the actual compilation. + * + * Supports 3-stage compilation for Java <-> Groovy + * 1. compile Java stubs + * 2. compile Java sources (done externally) + * 3. compile Groovy sources + */ +@mill.api.experimental +trait GroovyWorker { + + /** + * In a mixed setup this will compile the Groovy sources to Java stubs. + */ + def compileGroovyStubs( + sourceFiles: Seq[os.Path], + classpath: Seq[os.Path], + outputDir: os.Path, + config: GroovyCompilerConfiguration + )(implicit + ctx: TaskCtx + ) + : Result[Unit] + + /** + * Compiles the Groovy sources. In a mixed setup this method assumes that the Java stubs + * are already present in the outputDir. + */ + def compile( + sourceFiles: Seq[os.Path], + classpath: Seq[os.Path], + outputDir: os.Path, + config: GroovyCompilerConfiguration + )(implicit + ctx: TaskCtx + ) + : Result[CompilationResult] +} diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill new file mode 100644 index 000000000000..281ab876a443 --- /dev/null +++ b/libs/groovylib/package.mill @@ -0,0 +1,44 @@ +package build.libs.groovylib + +import mill.* +import mill.contrib.buildinfo.BuildInfo +import mill.scalalib.* +import millbuild.* + +object `package` extends MillPublishScalaModule with BuildInfo { + + def moduleDeps = Seq(build.libs.util, build.libs.javalib, build.libs.javalib.testrunner, api) + def localTestExtraModules: Seq[MillJavaModule] = + super.localTestExtraModules ++ Seq(worker) + + override def testMvnDeps: T[Seq[Dep]] = super.testMvnDeps() ++ Seq( + Deps.asmTree + ) + + def buildInfoPackageName = "mill.groovylib" + def buildInfoObjectName = "Versions" + def buildInfoMembers = Seq( + BuildInfo.Value("groovyVersion", Deps.groovyVersion, "Version of Groovy") + ) + + object api extends MillPublishScalaModule { + def moduleDeps = Seq(build.libs.javalib.testrunner) + + override def compileMvnDeps: T[Seq[Dep]] = Seq( + Deps.osLib + ) + } + + object worker extends MillPublishScalaModule { + override def compileModuleDeps = Seq(api) + + def mandatoryMvnDeps = Seq.empty[Dep] + + override def compileMvnDeps: T[Seq[Dep]] = + super.mandatoryMvnDeps() ++ Seq( + Deps.osLib, + Deps.groovyCompiler + ) + } + +} diff --git a/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala b/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala new file mode 100644 index 000000000000..46c7ec366602 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala @@ -0,0 +1,21 @@ +package mill.groovylib + +import mill.Task +import mill.javalib.MavenModule + +/** + * A [[GroovyModule]] with a Maven compatible directory layout: + * `src/main/groovy`, `src/main/resources`, etc. + */ +@mill.api.experimental +trait GroovyMavenModule extends GroovyModule with MavenModule { + private def sources0 = Task.Sources("src/main/groovy") + override def sources = super.sources() ++ sources0() + + trait GroovyMavenTests extends GroovyTests with MavenTests { + override def sources = Task.Sources( + moduleDir / "src" / testModuleName / "java", + moduleDir / "src" / testModuleName / "groovy" + ) + } +} diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala new file mode 100644 index 000000000000..25c5af0817ba --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -0,0 +1,301 @@ +package mill +package groovylib + +import mill.api.{ModuleRef, Result} +import mill.javalib.api.CompilationResult +import mill.javalib.api.JvmWorkerApi as PublicJvmWorkerApi +import mill.api.daemon.internal.{CompileProblemReporter, GroovyModuleApi, internal} +import mill.javalib.{Dep, JavaModule, JvmWorkerModule, Lib} +import mill.* +import mainargs.Flag +import mill.api.daemon.internal.bsp.{BspBuildTarget, BspModuleApi} +import mill.groovylib.worker.api.GroovyCompilerConfiguration +import mill.javalib.api.internal.{JavaCompilerOptions, JvmWorkerApi, ZincCompileJava} +import mill.util.Version + +/** + * Core configuration required to compile a single Groovy module. + * + * Resolves + */ +@mill.api.experimental +trait GroovyModule extends JavaModule with GroovyModuleApi { outer => + + /** + * The Groovy version to be used. + */ + def groovyVersion: T[String] + + /** + * The compiler language version. Default is derived from [[groovyVersion]]. + */ + def groovyLanguageVersion: T[String] = Task { groovyVersion().split("[.]").take(2).mkString(".") } + + private def useGroovyBom: T[Boolean] = Task { + if (groovyVersion().isBlank) { + false + } else { + Version.isAtLeast(groovyVersion(), "4.0.26")(using Version.IgnoreQualifierOrdering) + } + } + + override def bomMvnDeps: T[Seq[Dep]] = super.bomMvnDeps() ++ + Seq(groovyVersion()) + .filter(_.nonEmpty && useGroovyBom()) + .map(v => mvn"org.apache.groovy:groovy-bom:$v") + + /** + * All individual source files fed into the compiler. + */ + override def allSourceFiles: T[Seq[PathRef]] = Task { + allGroovySourceFiles() ++ allJavaSourceFiles() + } + + /** + * Specifiy the bytecode version for the Groovy compiler. + * {{{ + * def groovyCompileTargetBytecode = Some("17") + * }}} + */ + def groovyCompileTargetBytecode: T[Option[String]] = None + + /** + * Specify if the Groovy compiler should enable preview features. + */ + def groovyCompileEnablePreview: T[Boolean] = false + + /** + * Specify which global AST transformations should be disabled. Be aware that transformations + * like [[groovy.transform.Immutable]] are so-called "local" transformations and will not be + * affected. + * + * see [[https://docs.groovy-lang.org/latest/html/api/org/codehaus/groovy/control/CompilerConfiguration.html#setDisabledGlobalASTTransformations(java.util.Set) Groovy-Docs]] + */ + def disabledGlobalAstTransformations: T[Set[String]] = Set.empty + + /** + * All individual Java source files fed into the compiler. + * Subset of [[allSourceFiles]]. + */ + private def allJavaSourceFiles: T[Seq[PathRef]] = Task { + Lib.findSourceFiles(allSources(), Seq("java")).map(PathRef(_)) + } + + /** + * All individual Groovy source files fed into the compiler. + * Subset of [[allSourceFiles]]. + */ + private def allGroovySourceFiles: T[Seq[PathRef]] = Task { + Lib.findSourceFiles(allSources(), Seq("groovy")).map(PathRef(_)) + } + + /** + * The dependencies of this module. + * Defaults to add the Groovy dependency matching the [[groovyVersion]]. + */ + override def mandatoryMvnDeps: T[Seq[Dep]] = Task { + super.mandatoryMvnDeps() ++ groovyCompilerMvnDeps() + } + + def jvmWorkerRef: ModuleRef[JvmWorkerModule] = jvmWorker + + override def checkGradleModules: T[Boolean] = true + + /** + * The Java classpath resembling the Groovy compiler. + * Default is derived from [[groovyCompilerMvnDeps]]. + */ + def groovyCompilerClasspath: T[Seq[PathRef]] = Task { + val deps = groovyCompilerMvnDeps() ++ Seq( + Dep.millProjectModule("mill-libs-groovylib-worker") + ) + defaultResolver().classpath( + deps, + resolutionParamsMapOpt = None + ) + } + + /** + * The Coursier dependencies resembling the Groovy compiler. + * + * Default is derived from [[groovyVersion]]. + */ + def groovyCompilerMvnDeps: T[Seq[Dep]] = Task { + val gv = groovyVersion() + Seq(mvn"org.apache.groovy:groovy:$gv") + } + + /** + * Compiles all the sources to JVM class files. + */ + override def compile: T[CompilationResult] = Task { + groovyCompileTask()() + } + + def compileGroovyStubs: T[Result[Unit]] = Task(persistent = true) { + val groovySourceFiles = allGroovySourceFiles().map(_.path) + val stubDir = compileGeneratedGroovyStubs() + Task.ctx().log.info( + s"Generating Java stubs for ${groovySourceFiles.size} Groovy sources to $stubDir ..." + ) + + val compileCp = compileClasspath().map(_.path).filter(os.exists) + val config = GroovyCompilerConfiguration( + enablePreview = groovyCompileEnablePreview(), + targetBytecode = groovyCompileTargetBytecode(), + disabledGlobalAstTransformations = disabledGlobalAstTransformations() + ) + + GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { + _.compileGroovyStubs(groovySourceFiles, compileCp, stubDir, config) + } + } + + /** + * Path to Java stub sources as part of the `compile` step. Stubs are generated + * by the Groovy compiler and later used by the Java compiler. + */ + def compileGeneratedGroovyStubs: T[os.Path] = Task(persistent = true) { Task.dest } + + /** + * The actual Groovy compile task (used by [[compile]]). + */ + protected def groovyCompileTask(): Task[CompilationResult] = + Task.Anon { + val ctx = Task.ctx() + val dest = ctx.dest + val classes = dest / "classes" + os.makeDir.all(classes) + + val javaSourceFiles = allJavaSourceFiles().map(_.path) + val groovySourceFiles = allGroovySourceFiles().map(_.path) + + val isGroovy = groovySourceFiles.nonEmpty + val isJava = javaSourceFiles.nonEmpty + val compileCp = compileClasspath().map(_.path).filter(os.exists) + val updateCompileOutput = upstreamCompileOutput() + + sealed trait CompilationStrategy + case object JavaOnly extends CompilationStrategy + case object GroovyOnly extends CompilationStrategy + case object Mixed extends CompilationStrategy + + val strategy: CompilationStrategy = (isJava, isGroovy) match { + case (true, false) => JavaOnly + case (false, true) => GroovyOnly + case (true, true) => Mixed + case (false, false) => JavaOnly // fallback, though this shouldn't happen + } + + val config = GroovyCompilerConfiguration( + enablePreview = groovyCompileEnablePreview(), + targetBytecode = groovyCompileTargetBytecode(), + disabledGlobalAstTransformations = disabledGlobalAstTransformations() + ) + + def compileJava: Result[CompilationResult] = { + ctx.log.info( + s"Compiling ${javaSourceFiles.size} Java sources to $classes ..." + ) + // The compiler step is lazy, but its dependencies are not! + internalCompileJavaFiles( + worker = jvmWorkerRef().internalWorker(), + upstreamCompileOutput = updateCompileOutput, + javaSourceFiles = javaSourceFiles, + compileCp = compileCp :+ compileGeneratedGroovyStubs(), + javaHome = javaHome().map(_.path), + javacOptions = javacOptions(), + compileProblemReporter = ctx.reporter(hashCode), + reportOldProblems = zincReportCachedProblems() + ) + } + + def compileGroovy(): Result[CompilationResult] = { + ctx.log.info( + s"Compiling ${groovySourceFiles.size} Groovy sources to $classes ..." + ) + val workerGroovyResult = + GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { + _.compile(groovySourceFiles, compileCp, classes, config) + } + + // TODO figure out if there is a better way to do this + val analysisFile = dest / "groovy.analysis.dummy" // needed for Mills CompilationResult + os.write(target = analysisFile, data = "", createFolders = true) + + workerGroovyResult match { + case Result.Success(_) => + CompilationResult(analysisFile, PathRef(classes)) + case Result.Failure(reason) => Result.Failure(reason) + } + } + + strategy match { + case JavaOnly => + compileJava + + case GroovyOnly => + compileGroovy() + + case Mixed => + compileGroovyStubs() + .flatMap(_ => compileJava) + .flatMap(_ => compileGroovy()) + } + + } + + private[groovylib] def internalCompileJavaFiles( + worker: JvmWorkerApi, + upstreamCompileOutput: Seq[CompilationResult], + javaSourceFiles: Seq[os.Path], + compileCp: Seq[os.Path], + javaHome: Option[os.Path], + javacOptions: Seq[String], + compileProblemReporter: Option[CompileProblemReporter], + reportOldProblems: Boolean + )(implicit ctx: PublicJvmWorkerApi.Ctx): Result[CompilationResult] = { + val jOpts = JavaCompilerOptions(javacOptions) + worker.compileJava( + ZincCompileJava( + upstreamCompileOutput = upstreamCompileOutput, + sources = javaSourceFiles, + compileClasspath = compileCp, + javacOptions = jOpts.compiler, + incrementalCompilation = true + ), + javaHome = javaHome, + javaRuntimeOptions = jOpts.runtime, + reporter = compileProblemReporter, + reportCachedProblems = reportOldProblems + ) + } + + @internal + override def bspBuildTarget: BspBuildTarget = super.bspBuildTarget.copy( + languageIds = Seq( + BspModuleApi.LanguageId.Java, + BspModuleApi.LanguageId.Groovy + ), + canCompile = true, + canRun = true + ) + + override def prepareOffline(all: Flag): Command[Seq[PathRef]] = Task.Command { + ( + super.prepareOffline(all)() ++ + groovyCompilerClasspath() + ).distinct + } + + /** + * A test submodule linked to its parent module best suited for unit-tests. + */ + trait GroovyTests extends JavaTests with GroovyModule { + + override def groovyLanguageVersion: T[String] = outer.groovyLanguageVersion() + override def groovyVersion: T[String] = Task { outer.groovyVersion() } + override def bomMvnDeps: T[Seq[Dep]] = outer.bomMvnDeps() + override def mandatoryMvnDeps: Task.Simple[Seq[Dep]] = outer.mandatoryMvnDeps + } +} diff --git a/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala new file mode 100644 index 000000000000..41a85025df9c --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala @@ -0,0 +1,42 @@ +package mill.groovylib + +import mill.* +import mill.api.{Discover, ExternalModule, TaskCtx} +import mill.groovylib.worker.api.GroovyWorker +import mill.util.ClassLoaderCachedFactory + +@mill.api.experimental +class GroovyWorkerManager()(implicit ctx: TaskCtx) + extends ClassLoaderCachedFactory[GroovyWorker](ctx.jobs) { + + def getValue(cl: ClassLoader) = GroovyWorkerManager.get(cl) +} + +object GroovyWorkerManager extends ExternalModule { + def groovyWorker: Worker[GroovyWorkerManager] = Task.Worker { + new GroovyWorkerManager() + } + + def get(toolsClassLoader: ClassLoader)(implicit ctx: TaskCtx): GroovyWorker = { + // TODO why not use ServiceLoader...investigate + val className = + classOf[GroovyWorker].getPackage().getName().split("\\.").dropRight(1).mkString( + "." + ) + ".impl." + classOf[GroovyWorker].getSimpleName() + "Impl" + + val impl = toolsClassLoader.loadClass(className) + val worker = impl.getConstructor().newInstance().asInstanceOf[GroovyWorker] + if (worker.getClass().getClassLoader() != toolsClassLoader) { + ctx.log.warn( + """Worker not loaded from worker classloader. + |You should not add the mill-groovy-worker JAR to the mill build classpath""".stripMargin + ) + } + if (worker.getClass().getClassLoader() == classOf[GroovyWorker].getClassLoader()) { + ctx.log.warn("Worker classloader used to load interface and implementation") + } + worker + } + + override protected def millDiscover: Discover = Discover[this.type] +} diff --git a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala new file mode 100644 index 000000000000..6f2c4ce7f391 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala @@ -0,0 +1,35 @@ +package mill.groovylib + +import mill.* +import mill.javalib.{JavaModule, MavenModule} + +/** + * Convenience trait for projects using Java for production and Groovy for tests in a Maven setup. + * + * Since [[GroovyModule.GroovyTests]] is only available as a child-trait, it is necessary to have + * the main module as a [[GroovyModule]], which would implicitly add Groovy dependencies to the + * module. + * This trait explicitly uses Java with a Maven layout for the main module and enables `src/test/groovy` + * as a source folder for Groovy tests. + * + * {{{ + * object `package` extends JavaMavenModuleWithGroovyTests { + * + * object `test` extends GroovyMavenTests with TestModule.Spock { + * override def groovyVersion: T[String] = "4.0.28" + * override def spockVersion: T[String] = "2.3-groovy-4" + * } + * } + * }}} + * + * Note: for non-Maven layouts this is not necessary, since the test module can just be a [[GroovyModule]]. + */ +@mill.api.experimental +trait JavaMavenModuleWithGroovyTests extends JavaModule with MavenModule { + + trait GroovyMavenTests extends JavaTests with MavenTests with GroovyModule { + private def groovyTestSources = Task.Sources(moduleDir / "src" / testModuleName / "groovy") + override def sources: T[Seq[PathRef]] = super[MavenTests].sources() ++ groovyTestSources() + override def resources: T[Seq[PathRef]] = super[MavenTests].resources() + } +} diff --git a/libs/groovylib/src/mill/groovylib/exports.scala b/libs/groovylib/src/mill/groovylib/exports.scala new file mode 100644 index 000000000000..89d445c8033c --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/exports.scala @@ -0,0 +1,17 @@ +package mill.groovylib + +export mill.javalib.DepSyntax + +export mill.javalib.Dep + +export mill.javalib.JavaModule + +export mill.javalib.MavenModule + +export mill.javalib.TestModule + +export mill.javalib.PublishModule + +export mill.javalib.NativeImageModule + +export mill.javalib.JvmWorkerModule diff --git a/libs/groovylib/src/mill/groovylib/groovylib.scala b/libs/groovylib/src/mill/groovylib/groovylib.scala new file mode 100644 index 000000000000..436a35a541bd --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/groovylib.scala @@ -0,0 +1,7 @@ +package mill + +/** + * Groovy toolchain containing [[GroovyModule]] and other functionality related to building + * Groovy projects. + */ +package object groovylib diff --git a/libs/groovylib/src/mill/groovylib/publish/exports.scala b/libs/groovylib/src/mill/groovylib/publish/exports.scala new file mode 100644 index 000000000000..0c01da54c719 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/publish/exports.scala @@ -0,0 +1,35 @@ +package mill.groovylib.publish + +export mill.javalib.publish.Ivy + +export mill.javalib.publish.License + +export mill.javalib.publish.LocalIvyPublisher + +export mill.javalib.publish.LocalM2Publisher + +export mill.javalib.publish.Pom + +export mill.javalib.publish.PublishInfo + +export mill.javalib.publish.Artifact + +export mill.javalib.publish.Scope + +export mill.javalib.publish.Dependency + +export mill.javalib.publish.Developer + +export mill.javalib.publish.PomSettings + +export mill.javalib.publish.PackagingType + +export mill.javalib.publish.SonatypeHelpers +export mill.javalib.publish.SonatypeHttpApi +export mill.javalib.SonatypeCentralPublisher + +export mill.javalib.publish.VersionControl + +export mill.javalib.publish.VersionControlConnection + +export mill.javalib.publish.VersionScheme diff --git a/libs/groovylib/test/resources/hello-groovy/groovy-tests/src/test/groovy/HelloMavenTestOnly.groovy b/libs/groovylib/test/resources/hello-groovy/groovy-tests/src/test/groovy/HelloMavenTestOnly.groovy new file mode 100644 index 000000000000..3f456065bfa4 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/groovy-tests/src/test/groovy/HelloMavenTestOnly.groovy @@ -0,0 +1,12 @@ +package hello.maven.tests + +//import hello.Hello +import org.junit.jupiter.api.Test +import static org.junit.jupiter.api.Assertions.assertEquals + +class HelloMavenTestOnly { + @Test + void testSuccess() { + assertEquals(true, true) + } +} diff --git a/libs/groovylib/test/resources/hello-groovy/main/compileroptions/src/HelloCompilerOptions.groovy b/libs/groovylib/test/resources/hello-groovy/main/compileroptions/src/HelloCompilerOptions.groovy new file mode 100644 index 000000000000..4033f7ae1fde --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/compileroptions/src/HelloCompilerOptions.groovy @@ -0,0 +1,12 @@ +package compileroptions + +class HelloCompilerOptions { + + static String getHelloString() { + return "Hello, Java 11 Preview!" + } + + static void main(String[] args) { + println(getHelloString()) + } +} \ No newline at end of file diff --git a/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/GroovyGreeter.groovy b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/GroovyGreeter.groovy new file mode 100644 index 000000000000..d876ed9310c3 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/GroovyGreeter.groovy @@ -0,0 +1,18 @@ +package jointcompile + +class GroovyGreeter{ + + private final String toGreet; + private final JavaPrinter printer; + + GroovyGreeter(String toGreet){ + this.toGreet = toGreet + this.printer = new JavaPrinter() + } + + + void greet(){ + printer.print("Hello $toGreet"); + } + +} \ No newline at end of file diff --git a/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaMain.java b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaMain.java new file mode 100644 index 000000000000..6522149af6c4 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaMain.java @@ -0,0 +1,9 @@ +package jointcompile; + +public class JavaMain { + + public static void main(String[] args) { + var greeter = new GroovyGreeter("JointCompile"); + greeter.greet(); + } +} diff --git a/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaPrinter.java b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaPrinter.java new file mode 100644 index 000000000000..eb6dcfd64e19 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaPrinter.java @@ -0,0 +1,7 @@ +package jointcompile; + +public class JavaPrinter { + public void print(String s) { + System.out.println(s); + } +} diff --git a/libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy b/libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy new file mode 100644 index 000000000000..ccdd2a9a93e9 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy @@ -0,0 +1,7 @@ + +println getHelloString() + +static String getHelloString() { + return "Hello, Scripting!" +} + diff --git a/libs/groovylib/test/resources/hello-groovy/main/src/Hello.groovy b/libs/groovylib/test/resources/hello-groovy/main/src/Hello.groovy new file mode 100644 index 000000000000..c6d16c431c32 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/src/Hello.groovy @@ -0,0 +1,16 @@ +package hello + +class Hello { + + static String getHelloString() { + return "Hello, world!" + } + + static String sayHello(String name){ + return "Hello, $name" + } + + static void main(String[] args) { + println(getHelloString()) + } +} \ No newline at end of file diff --git a/libs/groovylib/test/resources/hello-groovy/main/staticcompile/src/HelloStatic.groovy b/libs/groovylib/test/resources/hello-groovy/main/staticcompile/src/HelloStatic.groovy new file mode 100644 index 000000000000..fe56e2263888 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/staticcompile/src/HelloStatic.groovy @@ -0,0 +1,26 @@ +package hellostatic + +import groovy.transform.CompileStatic + +@CompileStatic +class HelloStatic { + + static void main(String[] args) { + def x = new Some("Hello World") + x.doStuff() + } +} + +@CompileStatic +class Some { + + private String toPrint; + + Some(String toPrint){ + this.toPrint = toPrint + } + + void doStuff(){ + println(toPrint) + } +} \ No newline at end of file diff --git a/libs/groovylib/test/resources/hello-groovy/main/test/src/HelloTest.groovy b/libs/groovylib/test/resources/hello-groovy/main/test/src/HelloTest.groovy new file mode 100644 index 000000000000..5d9833fe9585 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/test/src/HelloTest.groovy @@ -0,0 +1,12 @@ +package hello.tests + +import hello.Hello +import org.junit.jupiter.api.Test +import static org.junit.jupiter.api.Assertions.assertEquals + +class HelloTest { + @Test + void testSuccess() { + assertEquals("Hello, world!", Hello.getHelloString()) + } +} diff --git a/libs/groovylib/test/resources/hello-groovy/spock/src/Hello.groovy b/libs/groovylib/test/resources/hello-groovy/spock/src/Hello.groovy new file mode 100644 index 000000000000..c6d16c431c32 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/spock/src/Hello.groovy @@ -0,0 +1,16 @@ +package hello + +class Hello { + + static String getHelloString() { + return "Hello, world!" + } + + static String sayHello(String name){ + return "Hello, $name" + } + + static void main(String[] args) { + println(getHelloString()) + } +} \ No newline at end of file diff --git a/libs/groovylib/test/resources/hello-groovy/spock/tests/src/SpockTest.groovy b/libs/groovylib/test/resources/hello-groovy/spock/tests/src/SpockTest.groovy new file mode 100644 index 000000000000..6a4ef42f4a76 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/spock/tests/src/SpockTest.groovy @@ -0,0 +1,21 @@ +package hello.spock + +import spock.lang.Specification +import static hello.Hello.* + +class SpockTest extends Specification { + + def "test succeeds"() { + expect: + getHelloString() == "Hello, world!" + } + + def "sayHello to '#name' equals '#expected'"() { + expect: + sayHello(name) == expected + + where: + name << ["Foo", "Bar"] + expected = "Hello, $name" + } +} diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala new file mode 100644 index 000000000000..6a07da866c30 --- /dev/null +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -0,0 +1,331 @@ +package mill +package groovylib + +import mill.javalib.{JavaModule, TestModule} +import mill.api.Task +import mill.api.Discover +import mill.testkit.{TestRootModule, UnitTester} +import utest.* + +import java.io.FileInputStream + +object HelloGroovyTests extends TestSuite { + + val groovy4Version = "4.0.28" + val groovy5Version = "5.0.1" + val groovyVersions = Seq(groovy4Version, groovy5Version) + val junit5Version = sys.props.getOrElse("TEST_JUNIT5_VERSION", "5.13.4") + val spockGroovy4Version = "2.3-groovy-4.0" + + object HelloGroovy extends TestRootModule { + + trait GroovyVersionCross extends GroovyModule with Cross.Module[String] { + override def groovyVersion: Task.Simple[String] = crossValue + } + + lazy val millDiscover = Discover[this.type] + + // needed for a special test where only the tests are written in Groovy while appcode remains Java + object `groovy-tests` extends JavaMavenModuleWithGroovyTests { + + object `test` extends GroovyMavenTests with TestModule.Junit5 { + + override def moduleDeps: Seq[JavaModule] = Seq( + HelloGroovy.`groovy-tests` + ) + + override def groovyVersion: T[String] = groovy4Version + override def jupiterVersion: T[String] = junit5Version + } + + } + + /** + * Currently Spock does not support Groovy 5, so that's why it's currently + * pulled out of the cross compilation. + */ + object spock extends GroovyModule { + override def groovyVersion: T[String] = groovy4Version + + object tests extends GroovyTests with TestModule.Spock { + override def jupiterVersion: T[String] = junit5Version + override def spockVersion: T[String] = spockGroovy4Version + } + } + + /** + * Test to verify BOM-resolution only done starting with the minimal version + */ + object deps extends Module { + + object groovyBom extends GroovyModule { + override def groovyVersion: T[String] = groovy4Version + } + + object groovyNoBom extends GroovyModule { + // Groovy-BOM available starting with 4.0.26 + override def groovyVersion: T[String] = "4.0.25" + } + + object `spockBom` extends GroovyModule with TestModule.Spock { + override def spockVersion: T[String] = spockGroovy4Version + override def groovyVersion: T[String] = groovy4Version + } + + object `spockNoBom` extends GroovyModule with TestModule.Spock { + // Groovy-BOM available starting with 2.3 + override def spockVersion: T[String] = "2.2-groovy-4.0" + override def groovyVersion: T[String] = groovy4Version + } + + } + + trait Test extends GroovyVersionCross { + + override def mainClass = Some("hello.Hello") + + object script extends GroovyModule { + override def groovyVersion: T[String] = crossValue + override def mainClass = Some("HelloScript") + } + + object staticcompile extends GroovyModule { + override def groovyVersion: T[String] = crossValue + override def mainClass = Some("hellostatic.HelloStatic") + } + + object `joint-compile` extends GroovyModule { + override def groovyVersion: T[String] = crossValue + override def mainClass = Some("jointcompile.JavaMain") + } + + object test extends GroovyTests with TestModule.Junit5 { + override def jupiterVersion: T[String] = junit5Version + override def junitPlatformVersion = "1.13.4" + } + + object compileroptions extends GroovyModule { + def javaVersion = "11" + override def groovyVersion: T[String] = crossValue + override def groovyCompileTargetBytecode: Task.Simple[Option[String]] = Some(javaVersion) + override def groovyCompileEnablePreview: Task.Simple[Boolean] = true + override def mainClass = Some("compileroptions.HelloCompilerOptions") + } + } + object main extends Cross[Test](groovyVersions) + } + + val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "hello-groovy" + + /** + * Compiles test files located within resources + */ + def testEval() = UnitTester(HelloGroovy, resourcePath) + + def tests: Tests = Tests { + + def main = HelloGroovy.main + def spock = HelloGroovy.spock + def mixed = HelloGroovy.`groovy-tests` + def deps = HelloGroovy.deps + + test("running a Groovy script") { + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(_) = eval.apply(m.script.run()): @unchecked + }) + } + } + + test("running a Groovy script") { + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(_) = eval.apply(m.script.run()): @unchecked + }) + } + } + + test("compile & run Groovy module") { + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(result) = eval.apply(m.compile): @unchecked + + assert( + os.walk(result.value.classes.path).exists(_.last == "Hello.class") + ) + + val Right(_) = eval.apply(m.run()): @unchecked + }) + } + } + + test("compile & run Groovy JUnit5 test") { + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(result) = eval.apply(m.test.compile): @unchecked + + assert( + os.walk(result.value.classes.path).exists(_.last == "HelloTest.class") + ) + + val Right(discovered) = eval.apply(m.test.discoveredTestClasses): @unchecked + assert(discovered.value == Seq("hello.tests.HelloTest")) + + val Right(_) = eval.apply(m.test.testForked()): @unchecked + }) + } + } + + test("compile & run a statically compiled Groovy") { + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(result) = eval.apply(m.staticcompile.compile): @unchecked + assert( + os.walk(result.value.classes.path).exists(_.last == "HelloStatic.class") + ) + val Right(_) = eval.apply(m.staticcompile.run()): @unchecked + }) + } + } + + test("compile joint (groovy <-> java cycle) & run") { + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(result) = eval.apply(m.`joint-compile`.compile): @unchecked + + assert( + os.walk(result.value.classes.path).exists(_.last == "JavaPrinter.class") + ) + assert( + os.walk(result.value.classes.path).exists(_.last == "GroovyGreeter.class") + ) + + val Right(_) = eval.apply(m.`joint-compile`.run()): @unchecked + }) + } + } + + test("compiles to Java 11 with Preview enabled") { + import org.objectweb.asm.ClassReader + + case class BytecodeVersion(major: Int, minor: Int) { + def javaVersion: String = major match { + case 55 => "11" + case _ => "Irrelevant" + } + + def is11PreviewEnabled: Boolean = minor == 65535 // 0xFFFF + } + + def getBytecodeVersion(classFilePath: os.Path): BytecodeVersion = { + val classReader = new ClassReader(new FileInputStream(classFilePath.toIO)) + val buffer = classReader.b + + // see https://en.wikipedia.org/wiki/Java_class_file#General_layout + // Class file format: magic(4) + minor(2) + major(2) + ... + val minor = ((buffer(4) & 0xff) << 8) | (buffer(5) & 0xff) + val major = ((buffer(6) & 0xff) << 8) | (buffer(7) & 0xff) + + BytecodeVersion(major, minor) + } + + testEval().scoped { eval => + main.crossModules.foreach(m => { + val Right(result) = eval.apply(m.compileroptions.compile): @unchecked + + val compiledClassFile = + os.walk(result.value.classes.path).find(_.last == "HelloCompilerOptions.class") + + assert( + compiledClassFile.isDefined + ) + + val bytecodeVersion = getBytecodeVersion(compiledClassFile.get) + + assert(bytecodeVersion.javaVersion == m.compileroptions.javaVersion) + assert(bytecodeVersion.is11PreviewEnabled) + + val Right(_) = eval.apply(m.compileroptions.run()): @unchecked + }) + } + } + + test("compile & test module (only test uses Groovy)") { + testEval().scoped { eval => + + val Right(_) = eval.apply(mixed.test.compile): @unchecked + val Right(discovered) = eval.apply(mixed.test.discoveredTestClasses): @unchecked + assert(discovered.value == Seq("hello.maven.tests.HelloMavenTestOnly")) + + val Right(_) = eval.apply(mixed.test.testForked()): @unchecked + } + } + + test("compile & run Spock test") { + testEval().scoped { eval => + val Right(result1) = eval.apply(spock.tests.compile): @unchecked + assert( + os.walk(result1.value.classes.path).exists(_.last == "SpockTest.class") + ) + + val Right(discovered) = eval.apply(spock.tests.discoveredTestClasses): @unchecked + assert(discovered.value == Seq("hello.spock.SpockTest")) + + val Right(_) = eval.apply(spock.tests.testForked()): @unchecked + } + } + + test("dependency management") { + + test("groovy") { + + val groovyBom = mvn"org.apache.groovy:groovy-bom:$groovy4Version" + + test("groovy bom is added when version is at least 4.0.26") { + testEval().scoped { eval => + val Right(result) = eval.apply(deps.groovyBom.bomMvnDeps): @unchecked + + assert( + result.value.contains(groovyBom) + ) + } + } + + test("groovy bom is NOT added when version is below 4.0.26") { + testEval().scoped { eval => + val Right(result) = eval.apply(deps.groovyNoBom.bomMvnDeps): @unchecked + + assert( + !result.value.contains(groovyBom) + ) + } + } + } + + test("spock") { + + val spockBom = mvn"org.spockframework:spock-bom:$spockGroovy4Version" + + test("spock bom is added when version is at least 2.3") { + testEval().scoped { eval => + val Right(result) = eval.apply(deps.spockBom.bomMvnDeps): @unchecked + + assert( + result.value.contains(spockBom) + ) + } + } + + test("spock bom is NOT added when version is below 2.3") { + testEval().scoped { eval => + val Right(result) = eval.apply(deps.spockNoBom.bomMvnDeps): @unchecked + + assert( + !result.value.contains(spockBom) + ) + } + } + } + } + } +} diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala new file mode 100644 index 000000000000..85c48f3f05cc --- /dev/null +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -0,0 +1,95 @@ +package mill.groovylib.worker.impl + +import groovy.lang.GroovyClassLoader +import mill.api.{Result, TaskCtx} +import mill.javalib.api.CompilationResult +import mill.groovylib.worker.api.{GroovyCompilerConfiguration, GroovyWorker} +import org.codehaus.groovy.control.{CompilationUnit, CompilerConfiguration, Phases} +import org.codehaus.groovy.tools.javac.JavaStubCompilationUnit +import os.Path + +import scala.jdk.CollectionConverters.* +import scala.util.Try + +class GroovyWorkerImpl extends GroovyWorker { + + override def compileGroovyStubs( + sourceFiles: Seq[Path], + classpath: Seq[Path], + outputDir: Path, + config: GroovyCompilerConfiguration + )(implicit ctx: TaskCtx): Result[Unit] = { + val compilerConfig = new CompilerConfiguration() + compilerConfig.setTargetDirectory(outputDir.toIO) + compilerConfig.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) + compilerConfig.setJointCompilationOptions(Map( + "stubDir" -> outputDir.toIO, + "keepStubs" -> true + ).asJava) + compilerConfig.setDisabledGlobalASTTransformations( + config.disabledGlobalAstTransformations.asJava + ) + compilerConfig.setPreviewFeatures(config.enablePreview) + config.targetBytecode.foreach(compilerConfig.setTargetBytecode) + + // we need to set the classloader for groovy to use the worker classloader + val parentCl: ClassLoader = this.getClass.getClassLoader + // compilerConfig in the GroovyClassLoader is needed when the CL itself is compiling classes + val gcl = new GroovyClassLoader(parentCl, compilerConfig) + // compilerConfig for actual compilation + val stubUnit = JavaStubCompilationUnit(compilerConfig, gcl) + + sourceFiles.foreach { sourceFile => + stubUnit.addSource(sourceFile.toIO) + } + + Try { + stubUnit.compile(Phases.CONVERSION) + }.fold( + exception => Result.Failure(s"Groovy stub generation failed: ${exception.getMessage}"), + result => Result.Success(()) + ) + + } + + def compile( + sourceFiles: Seq[os.Path], + classpath: Seq[os.Path], + outputDir: os.Path, + config: GroovyCompilerConfiguration + )(implicit + ctx: TaskCtx + ): Result[CompilationResult] = { + + val extendedClasspath = classpath :+ outputDir + + val compilerConfig = new CompilerConfiguration() + compilerConfig.setTargetDirectory(outputDir.toIO) + compilerConfig.setClasspathList(extendedClasspath.map(_.toIO.getAbsolutePath).asJava) + compilerConfig.setDisabledGlobalASTTransformations( + config.disabledGlobalAstTransformations.asJava + ) + compilerConfig.setPreviewFeatures(config.enablePreview) + config.targetBytecode.foreach(compilerConfig.setTargetBytecode) + + // we need to set the classloader for groovy to use the worker classloader + val parentCl: ClassLoader = this.getClass.getClassLoader + // compilerConfig in the GroovyClassLoader is needed when the CL itself is compiling classes + val gcl = new GroovyClassLoader(parentCl, compilerConfig) + + // compilerConfig for actual compilation + val unit = new CompilationUnit(compilerConfig, null, gcl) + + sourceFiles.foreach { sourceFile => + unit.addSource(sourceFile.toIO) + } + + Try { + unit.compile(Phases.OUTPUT) + CompilationResult(outputDir, mill.api.PathRef(outputDir)) + }.fold( + exception => Result.Failure(s"Groovy compilation failed: ${exception.getMessage}"), + result => Result.Success(result) + ) + } +} diff --git a/libs/javalib/src/mill/javalib/TestModule.scala b/libs/javalib/src/mill/javalib/TestModule.scala index 9f0a0a2fdb86..8bcba0688842 100644 --- a/libs/javalib/src/mill/javalib/TestModule.scala +++ b/libs/javalib/src/mill/javalib/TestModule.scala @@ -2,15 +2,14 @@ package mill.javalib import mill.T import mill.api.Result -import mill.api.daemon.internal.TestModuleApi -import mill.api.daemon.internal.TestReporter +import mill.api.daemon.internal.{TestModuleApi, TestReporter} import mill.api.daemon.internal.bsp.{BspBuildTarget, BspModuleApi} import mill.api.PathRef import mill.api.Task import mill.api.TaskCtx import mill.api.DefaultTaskModule import mill.javalib.bsp.BspModule -import mill.util.Jvm +import mill.util.{Jvm, Version} import mill.api.JsonFormatters.given import mill.constants.EnvVars import mill.javalib.testrunner.{ @@ -58,6 +57,7 @@ trait TestModule * - [[TestModule.Utest]] * - [[TestModule.Weaver]] * - [[TestModule.ZioTest]] + * - [[TestModule.Spock]] * * Most of these provide additional `xxxVersion` tasks, to manage the test framework dependencies for you. */ @@ -358,24 +358,53 @@ object TestModule { * You can override the [[junitPlatformVersion]] and [[jupiterVersion]] task * or provide the JUnit 5-dependencies yourself. * + * In case the [[jupiterVersion]] is set (and it is > 5.12), it pulls in JUnit-BOM in [[bomMvnDeps]]. If this is + * true, then there is no need to specify the [[junitPlatformVersion]] anymore, because this is managed by the + * BOM. + * * See: https://junit.org/junit5/ */ trait Junit5 extends TestModule { - /** The JUnit 5 Platfrom version to use, or empty, if you want to provide the dependencies yourself. */ + /** The JUnit 5 Platform version to use, or empty, if you want to provide the dependencies yourself. */ def junitPlatformVersion: T[String] = Task { "" } - /** The JUnit Jupiter version to use, or empty, if you want to provide the dependencie yourself. */ + /** The JUnit Jupiter version to use, or empty, if you want to provide the dependencies yourself. */ def jupiterVersion: T[String] = Task { "" } + private def useJupiterBom: T[Boolean] = Task { + if (jupiterVersion().isBlank) { + false + } else { + Version.isAtLeast(jupiterVersion(), "5.12.0")(using Version.IgnoreQualifierOrdering) + } + } + override def testFramework: T[String] = "com.github.sbt.junit.jupiter.api.JupiterFramework" + override def bomMvnDeps: T[Seq[Dep]] = Task { + super.bomMvnDeps() ++ + Seq(jupiterVersion()) + .filter(!_.isBlank() && useJupiterBom()) + .flatMap(v => + Seq( + mvn"org.junit:junit-bom:${v.trim()}" + ) + ) + } + override def mandatoryMvnDeps: T[Seq[Dep]] = Task { super.mandatoryMvnDeps() ++ Seq(mvn"${mill.javalib.api.Versions.jupiterInterface}") ++ - Seq(junitPlatformVersion()) - .filter(!_.isBlank()) - .map(v => mvn"org.junit.platform:junit-platform-launcher:${v.trim()}") ++ + Seq(junitPlatformVersion()).flatMap(v => { + if (!v.isBlank) { + Some(mvn"org.junit.platform:junit-platform-launcher:${v.trim()}") + } else if (useJupiterBom()) { + Some(mvn"org.junit.platform:junit-platform-launcher") + } else { + None + } + }) ++ Seq(jupiterVersion()) .filter(!_.isBlank()) .map(v => mvn"org.junit.jupiter:junit-jupiter-api:${v.trim()}") @@ -600,6 +629,51 @@ object TestModule { } } + /** + * TestModule that uses Spock Test Framework to run tests. + * You can override the [[spockVersion]] task or provide the Spock dependency yourself. + * + * In case the version is set, it pulls in Spock-BOM in [[bomMvnDeps]] (only for 2.3 onwards) + * and Spock-Core in [[mvnDeps]] + */ + trait Spock extends TestModule.Junit5 { + + /** The Spock Test version to use, or the empty string, if you want to provide the Spock test dependency yourself. */ + def spockVersion: T[String] = Task { + "" + } + + private def isSpockBomAvailable: T[Boolean] = Task { + if (spockVersion().isBlank) { + false + } else { + Version.isAtLeast(spockVersion(), "2.3")(using Version.IgnoreQualifierOrdering) + } + } + + override def bomMvnDeps: T[Seq[Dep]] = Task { + super.bomMvnDeps() ++ + Seq(spockVersion()) + .filter(!_.isBlank() && isSpockBomAvailable()) + .flatMap(v => + Seq( + mvn"org.spockframework:spock-bom:${v.trim()}" + ) + ) + } + + override def mandatoryMvnDeps: T[Seq[Dep]] = Task { + super.mandatoryMvnDeps() ++ + Seq(spockVersion()) + .filter(!_.isBlank()) + .flatMap(v => + Seq( + mvn"org.spockframework:spock-core:${v.trim()}" + ) + ) + } + } + def handleResults( doneMsg: String, results: Seq[TestResult], @@ -620,6 +694,7 @@ object TestModule { def mvnDeps: T[Seq[Dep]] = Seq() def mandatoryMvnDeps: T[Seq[Dep]] = Seq() def resources: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + def bomMvnDeps: T[Seq[Dep]] = Seq() } trait ScalaModuleBase extends mill.Module { diff --git a/libs/javalib/test/src/mill/javalib/junit5/JUnit5Tests.scala b/libs/javalib/test/src/mill/javalib/junit5/JUnit5Tests.scala index 227b5c4aaf09..35d28e33bed0 100644 --- a/libs/javalib/test/src/mill/javalib/junit5/JUnit5Tests.scala +++ b/libs/javalib/test/src/mill/javalib/junit5/JUnit5Tests.scala @@ -1,17 +1,34 @@ package mill.javalib.junit5 -import mill.api.Discover +import mill.api.Task.Simple +import mill.api.{Discover, Module, Task} import mill.javalib.JavaModule import mill.javalib.TestModule +import mill.javalib.DepSyntax import mill.testkit.{TestRootModule, UnitTester} import utest.* import mill.util.TokenReaders._ object JUnit5Tests extends TestSuite { + val junitVersion = "5.13.4" + object module extends TestRootModule with JavaModule { object test extends JavaTests with TestModule.Junit5 lazy val millDiscover = Discover[this.type] + + object deps extends Module { + + object junitBom extends JavaTests with TestModule.Junit5 { + override def jupiterVersion: Simple[String] = junitVersion + } + + object junitNoBom extends JavaTests with TestModule.Junit5 { + // JUnit-BOM available starting with 5.12.0 + override def jupiterVersion: Simple[String] = "5.11.0" + } + } + } val testModuleSourcesPath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "junit5" @@ -32,5 +49,48 @@ object JUnit5Tests extends TestSuite { assert(qualifiedNames.forall(_.fullyQualifiedName == "qux.QuxTests")) } } + test("dependency management") { + def testEval() = UnitTester(module, testModuleSourcesPath) + val junitBom = mvn"org.junit:junit-bom:$junitVersion" + val jupiter = mvn"org.junit.jupiter:junit-jupiter-api:$junitVersion" + val junitPlatformLauncher = mvn"org.junit.platform:junit-platform-launcher" + + test("jupiter added when version is set") { + testEval().scoped { eval => + val Right(resultDeps) = eval.apply(module.deps.junitBom.mandatoryMvnDeps): @unchecked + assert( + resultDeps.value.contains(jupiter) + ) + } + } + + test("junit bom & platform are added when version is at least 5.12.0") { + testEval().scoped { eval => + val Right(resultBom) = eval.apply(module.deps.junitBom.bomMvnDeps): @unchecked + assert( + resultBom.value.contains(junitBom) + ) + + val Right(resultDeps) = eval.apply(module.deps.junitBom.mandatoryMvnDeps): @unchecked + assert( + resultDeps.value.contains(junitPlatformLauncher) + ) + } + } + + test("junit bom & platform are NOT added when version is below 5.11.0") { + testEval().scoped { eval => + val Right(resultBom) = eval.apply(module.deps.junitNoBom.bomMvnDeps): @unchecked + assert( + !resultBom.value.contains(junitBom) + ) + + val Right(resultDeps) = eval.apply(module.deps.junitNoBom.mandatoryMvnDeps): @unchecked + assert( + !resultDeps.value.contains(junitPlatformLauncher) + ) + } + } + } } } diff --git a/libs/kotlinlib/src/mill/kotlinlib/publish/exports.scala b/libs/kotlinlib/src/mill/kotlinlib/publish/exports.scala index c0104ef760f2..8224e34c4956 100644 --- a/libs/kotlinlib/src/mill/kotlinlib/publish/exports.scala +++ b/libs/kotlinlib/src/mill/kotlinlib/publish/exports.scala @@ -1,4 +1,4 @@ -package mill.kotlinlib.publish +package mill.groovylib.publish export mill.javalib.publish.Ivy diff --git a/libs/kotlinlib/test/src/mill/kotlinlib/HelloKotlinTests.scala b/libs/kotlinlib/test/src/mill/kotlinlib/HelloKotlinTests.scala index 8793905681e3..0e81d2b12114 100644 --- a/libs/kotlinlib/test/src/mill/kotlinlib/HelloKotlinTests.scala +++ b/libs/kotlinlib/test/src/mill/kotlinlib/HelloKotlinTests.scala @@ -40,6 +40,7 @@ object HelloKotlinTests extends TestSuite { object main extends Cross[KotlinVersionCross](crossMatrix) lazy val millDiscover = Discover[this.type] + println(millDiscover) } val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "hello-kotlin" diff --git a/libs/package.mill b/libs/package.mill index 603b5a5e7c36..98b6a978bede 100644 --- a/libs/package.mill +++ b/libs/package.mill @@ -10,6 +10,7 @@ import millbuild.* object `package` extends MillStableScalaModule { def moduleDeps = Seq( build.libs.kotlinlib, + build.libs.groovylib, build.libs.androidlib, build.libs.scalajslib, build.libs.scalanativelib, diff --git a/mill-build/src/millbuild/Deps.scala b/mill-build/src/millbuild/Deps.scala index 05936b3b95e8..f1c344f291f5 100644 --- a/mill-build/src/millbuild/Deps.scala +++ b/mill-build/src/millbuild/Deps.scala @@ -197,6 +197,8 @@ object Deps { val kotlinBuildToolsApi = mvn"org.jetbrains.kotlin:kotlin-build-tools-api:$kotlinVersion" val kotlinBuildToolsImpl = mvn"org.jetbrains.kotlin:kotlin-build-tools-impl:$kotlinVersion" val kotlinStdlib = mvn"org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + val groovyVersion = "4.0.28" + val groovyCompiler = mvn"org.apache.groovy:groovy:$groovyVersion" /** Used for the `mill init` from a Maven project. */ object MavenInit { diff --git a/website/docs/modules/ROOT/nav.adoc b/website/docs/modules/ROOT/nav.adoc index f45eb00f8263..00fe57a1a365 100644 --- a/website/docs/modules/ROOT/nav.adoc +++ b/website/docs/modules/ROOT/nav.adoc @@ -57,6 +57,8 @@ *** xref:javascriptlib/linting.adoc[] *** xref:javascriptlib/publishing.adoc[] *** xref:javascriptlib/build-examples.adoc[] +** xref:groovylib/intro.adoc[] +*** xref:groovylib/testing.adoc[] * xref:comparisons/why-mill.adoc[] ** xref:comparisons/maven.adoc[] ** xref:comparisons/gradle.adoc[] diff --git a/website/docs/modules/ROOT/pages/groovylib/intro.adoc b/website/docs/modules/ROOT/pages/groovylib/intro.adoc new file mode 100644 index 000000000000..cab3270bd64f --- /dev/null +++ b/website/docs/modules/ROOT/pages/groovylib/intro.adoc @@ -0,0 +1,40 @@ += Building Groovy with Mill +:page-aliases: Groovy_Intro_to_Mill.adoc +:language: Groovy +:language-small: groovy + +// START-COPY-AND-PASTE +// Instead of this include +// include::partial$Intro_Header.adoc[] +// We copy the text here, as we currently have no publishing page + +This page contains a quick introduction to getting start with using Mill to build +a simple {language} program. It walks through a series of Mill builds of increasing +complexity to show you the key features and usage of the Mill build tool. +The other pages of this section of the docs go into more depth into individual Mill features. +These aren't intended to be read comprehensively top-to-bottom, but +rather looked up when you have a particular interest e.g. in +xref:{language-small}lib/testing.adoc[testing], +xref:javalib/publishing.adoc[publishing], and so on. + +The API reference for Mill's {language} toolchain can be found at: + +* {mill-doc-url}/api/latest/mill/{language-small}lib.html[mill.{language-small}lib] + +If you aren't sure why you would use Mill, see xref:comparisons/why-mill.adoc[] for +a discussion on the motivations of the project. If you are migrating an existing project, +see xref:migrating/migrating.adoc[Migrating to Mill]. + +// END-COPY-AND-PASTE + +NOTE: Mill Groovy support is currently still under active development. +It is expected to continue evolving over time. + +== Simple Groovy Module + +include::partial$example/groovylib/basic/1-simple.adoc[] + +== Maven-Compatible Modules + +include::partial$example/groovylib/basic/2-compat-modules.adoc[] + diff --git a/website/docs/modules/ROOT/pages/groovylib/testing.adoc b/website/docs/modules/ROOT/pages/groovylib/testing.adoc new file mode 100644 index 000000000000..c8577c468e96 --- /dev/null +++ b/website/docs/modules/ROOT/pages/groovylib/testing.adoc @@ -0,0 +1,27 @@ += Testing Groovy Projects +:page-aliases: Testing_Groovy_Projects.adoc + + +This page will discuss common topics around working with test suites using the Mill build tool + +== Defining Unit Test Suites + +include::partial$example/groovylib/testing/1-test-suite.adoc[] + +== Defining Integration Test Suites + +include::partial$example/groovylib/testing/2-integration-suite.adoc[] + +== Using Spock + +include::partial$example/groovylib/testing/3-spock.adoc[] + +== Using Spock in Java projects with Maven layout + +include::partial$example/groovylib/testing/4-spock-for-java.adoc[] + +== Github Actions Test Reports + +If you use Github Actions for CI, you can use https://github.com/mikepenz/action-junit-report in +your pipeline to render the generated `test-report.xml` files nicely on Github. See +https://github.com/com-lihaoyi/mill/pull/4218/files for an example integration \ No newline at end of file