From 541652c2ed9b79b6700b526a48257d74b842c1d2 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Wed, 20 Aug 2025 20:03:53 +0200 Subject: [PATCH 01/23] Intial Groovy setup --- .../api/daemon/internal/GroovyModuleApi.scala | 3 + .../daemon/internal/bsp/BspModuleApi.scala | 1 + .../groovylib/worker/api/GroovyWorker.scala | 12 + libs/groovylib/package.mill | 50 ++++ .../mill/groovylib/GroovyMavenModule.scala | 20 ++ .../src/mill/groovylib/GroovyModule.scala | 269 ++++++++++++++++++ .../mill/groovylib/GroovyWorkerManager.scala | 40 +++ .../groovylib/TestGroovyMavenModule.scala | 25 ++ .../src/mill/groovylib/exports.scala | 13 + .../src/mill/groovylib/groovylib.scala | 7 + .../src/mill/groovylib/publish/exports.scala | 35 +++ .../main/script/src/HelloScript.groovy | 8 + .../main/spock/src/SpockTest.groovy | 21 ++ .../hello-groovy/main/src/Hello.groovy | 16 ++ .../main/staticcompile/src/HelloStatic.groovy | 26 ++ .../main/test/src/HelloTest.groovy | 12 + .../src/mill/groovylib/HelloGroovyTests.scala | 185 ++++++++++++ .../worker/impl/GroovyWorkerImpl.scala | 43 +++ .../javalib/src/mill/javalib/TestModule.scala | 39 ++- .../src/mill/kotlinlib/publish/exports.scala | 2 +- .../src/mill/kotlinlib/HelloKotlinTests.scala | 1 + libs/package.mill | 1 + mill-build/src/millbuild/Deps.scala | 2 + 23 files changed, 828 insertions(+), 3 deletions(-) create mode 100644 core/api/daemon/src/mill/api/daemon/internal/GroovyModuleApi.scala create mode 100644 libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala create mode 100644 libs/groovylib/package.mill create mode 100644 libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala create mode 100644 libs/groovylib/src/mill/groovylib/GroovyModule.scala create mode 100644 libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala create mode 100644 libs/groovylib/src/mill/groovylib/TestGroovyMavenModule.scala create mode 100644 libs/groovylib/src/mill/groovylib/exports.scala create mode 100644 libs/groovylib/src/mill/groovylib/groovylib.scala create mode 100644 libs/groovylib/src/mill/groovylib/publish/exports.scala create mode 100644 libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy create mode 100644 libs/groovylib/test/resources/hello-groovy/main/spock/src/SpockTest.groovy create mode 100644 libs/groovylib/test/resources/hello-groovy/main/src/Hello.groovy create mode 100644 libs/groovylib/test/resources/hello-groovy/main/staticcompile/src/HelloStatic.groovy create mode 100644 libs/groovylib/test/resources/hello-groovy/main/test/src/HelloTest.groovy create mode 100644 libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala create mode 100644 libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala 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/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..9e4a6c5cf4fc --- /dev/null +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -0,0 +1,12 @@ +package mill.groovylib.worker.api + +import mill.api.TaskCtx +import mill.api.Result +import mill.javalib.api.CompilationResult + +trait GroovyWorker { + + def compile(sourceFiles: Seq[os.Path], classpath: Seq[os.Path], outputDir: os.Path)(implicit + ctx: TaskCtx + ): Result[CompilationResult] +} diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill new file mode 100644 index 000000000000..e8b390db59d3 --- /dev/null +++ b/libs/groovylib/package.mill @@ -0,0 +1,50 @@ +package build.libs.groovylib + +// imports +import mill.* +import mill.contrib.buildinfo.BuildInfo +import mill.scalalib.* +import millbuild.* + +object `package` extends MillPublishScalaModule with BuildInfo { + + def moduleDeps = Seq(build.libs.javalib, build.libs.javalib.testrunner, api) + def localTestExtraModules = + super.localTestExtraModules ++ Seq(worker) + + def buildInfoPackageName = "mill.groovylib" + def buildInfoObjectName = "Versions" + def buildInfoMembers = Seq( + BuildInfo.Value("groovyVersion", Deps.groovyVersion, "Version of Groovy") + ) + + trait MillGroovyModule extends MillPublishScalaModule { + override def javacOptions = super.javacOptions() ++ { + val release = + if (scala.util.Properties.isJavaAtLeast(11)) Seq("-release", "8") + else Seq("-source", "1.8", "-target", "1.8") + release ++ Seq("-encoding", "UTF-8", "-deprecation") + } + } + + object api extends MillGroovyModule { + def moduleDeps = Seq(build.libs.javalib.testrunner) + + override def compileMvnDeps: T[Seq[Dep]] = Seq( + Deps.osLib + ) + } + + object worker extends MillGroovyModule { + 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..bfcabb3bee9e --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala @@ -0,0 +1,20 @@ +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. + */ +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..08ef7115e95c --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -0,0 +1,269 @@ +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.javalib.api.internal.{JavaCompilerOptions, JvmWorkerApi, ZincCompileJava} + +/** + * Core configuration required to compile a single Groovy module + */ +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(".") } + + override def bomMvnDeps: T[Seq[Dep]] = super.bomMvnDeps() ++ Seq( + mvn"org.apache.groovy:groovy-bom:${groovyVersion()}" + ) + + /** + * All individual source files fed into the compiler. + */ + override def allSourceFiles: T[Seq[PathRef]] = Task { + Lib.findSourceFiles(allSources(), Seq("groovy", "java")).map(PathRef(_)) + } + + /** + * All individual Java source files fed into the compiler. + * Subset of [[allSourceFiles]]. + */ + private def allJavaSourceFiles: T[Seq[PathRef]] = Task { + allSourceFiles().filter(_.path.ext.toLowerCase() == "java") + } + + /** + * All individual Groovy source files fed into the compiler. + * Subset of [[allSourceFiles]]. + */ + private def allGroovySourceFiles: T[Seq[PathRef]] = Task { + allSourceFiles().filter(path => Seq("groovy").contains(path.path.ext.toLowerCase())) + } + + /** + * The dependencies of this module. + * Defaults to add the groovy dependency matching the [[groovyVersion]]. + */ + override def mandatoryMvnDeps: T[Seq[Dep]] = Task { + super.mandatoryMvnDeps() ++ Seq( + mvn"org.apache.groovy:groovy:${groovyVersion()}" + ) + } + + private 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 Ivy/Coursier dependencies resembling the Groovy compiler. + * + * Default is derived from [[groovyCompilerVersion]]. + */ + def groovyCompilerMvnDeps: T[Seq[Dep]] = Task { + val gv = groovyVersion() + + val compilerDep = mvn"org.apache.groovy:groovy:$gv" + + Seq(compilerDep) + } + + /** + * Compiler Plugin dependencies. + */ + def groovyCompilerPluginMvnDeps: T[Seq[Dep]] = Task { Seq.empty[Dep] } + + /** + * The resolved plugin jars + */ + def groovyCompilerPluginJars: T[Seq[PathRef]] = Task { + val jars = defaultResolver().classpath( + groovycPluginMvnDeps() + // Don't resolve transitive jars + .map(d => d.exclude("*" -> "*")), + resolutionParamsMapOpt = None + ) + jars.toSeq + } + + /** + * Compiles all the sources to JVM class files. + */ + override def compile: T[CompilationResult] = Task { + groovyCompileTask()() + } + + /** + * The actual Groovy compile task (used by [[compile]] and [[groovycHelp]]). + */ + 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 isMixed = isGroovy && isJava + + val compileCp = compileClasspath().map(_.path).filter(os.exists) + val updateCompileOutput = upstreamCompileOutput() + + def compileJava: Result[CompilationResult] = { + ctx.log.info( + s"Compiling ${javaSourceFiles.size} Java sources to ${classes} ..." + ) + // The compile step is lazy, but its dependencies are not! + internalCompileJavaFiles( + worker = jvmWorkerRef().internalWorker(), + upstreamCompileOutput = updateCompileOutput, + javaSourceFiles = javaSourceFiles, + compileCp = compileCp, + javaHome = javaHome().map(_.path), + javacOptions = javacOptions(), + compileProblemReporter = ctx.reporter(hashCode), + reportOldProblems = internalReportOldProblems() + ) + } + + if (isMixed || isGroovy) { + ctx.log.info( + s"Compiling ${groovySourceFiles.size} Groovy sources to ${classes} ..." + ) + + val compileCp = compileClasspath().map(_.path).filter(os.exists) + + val workerResult = + GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { + _.compile(groovySourceFiles, compileCp, classes) + } + + val analysisFile = dest / "groovy.analysis.dummy" // needed for mills CompilationResult + os.write(target = analysisFile, data = "", createFolders = true) + + workerResult match { + case Result.Success(_) => + val cr = CompilationResult(analysisFile, PathRef(classes)) + if (!isJava) { + // pure Groovy project + cr + } else { + // also run Java compiler and use it's returned result + compileJava + } + case Result.Failure(reason) => Result.Failure(reason) + } + } else { + // it's Java only + compileJava + } + } + + /** + * Additional Groovy compiler options to be used by [[compile]]. + */ + def groovycOptions: T[Seq[String]] = Task { Seq.empty[String] } + + /** + * Aggregation of all the options passed to the Groovy compiler. + * In most cases, instead of overriding this target you want to override `groovycOptions` instead. + */ + def allGroovycOptions: T[Seq[String]] = Task { + groovycOptions() + } + + 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 + ) + } + + private[groovylib] def internalReportOldProblems: Task[Boolean] = zincReportCachedProblems + + @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 sub-module 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 groovycPluginMvnDeps: T[Seq[Dep]] = + Task { outer.groovycPluginMvnDeps() } + // TODO: make Xfriend-path an explicit setting + override def groovycOptions: T[Seq[String]] = Task { + // FIXME + outer.groovycOptions().filterNot(_.startsWith("-Xcommon-sources")) ++ + Seq(s"-Xfriend-paths=${outer.compile().classes.path.toString()}") + } + } + +} + +// TODO maybe an StandaloneGroovyTestsModule diff --git a/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala new file mode 100644 index 000000000000..9ccdc7845054 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala @@ -0,0 +1,40 @@ +package mill.groovylib + +import mill.* +import mill.api.{Discover, ExternalModule, TaskCtx} +import mill.groovylib.worker.api.GroovyWorker +import mill.util.ClassLoaderCachedFactory + +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 = { + 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/TestGroovyMavenModule.scala b/libs/groovylib/src/mill/groovylib/TestGroovyMavenModule.scala new file mode 100644 index 000000000000..1ad30eb67678 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/TestGroovyMavenModule.scala @@ -0,0 +1,25 @@ +package mill.groovylib + +import mill.* + +/** + * A [[GroovyModule]] for tests with a Maven compatible directory layout: + * `src/test/groovy`, `src/test/resources`, etc. + * This is useful when only the tests are written in Groovy while the production code remains in Java or other. + * + * Requires an outer module (like JavaModule) otherwise [[moduleDir]] must be overridden manually. + */ +trait TestGroovyMavenModule extends GroovyMavenModule { + + /** + * The name of this module's folder within `src/`: e.g. `src/test/`, `src/integration/`, + * etc. Defaults to the name of the module object, but can be overridden by users + */ + def testModuleName: String = moduleCtx.segments.last.value + + private def testSources = Task.Sources(moduleDir / os.up / "src" / testModuleName / "groovy") + override def sources: T[Seq[PathRef]] = testSources() + + private def testResources = Task.Sources(moduleDir / os.up / "src" / testModuleName / "resources") + override def resources: T[Seq[PathRef]] = testResources() +} diff --git a/libs/groovylib/src/mill/groovylib/exports.scala b/libs/groovylib/src/mill/groovylib/exports.scala new file mode 100644 index 000000000000..9657cb08d604 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/exports.scala @@ -0,0 +1,13 @@ +package mill.groovylib + +export mill.javalib.DepSyntax + +export mill.javalib.Dep + +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..8224e34c4956 --- /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.publish.SonatypePublisher + +export mill.javalib.publish.VersionControl + +export mill.javalib.publish.VersionControlConnection + +export mill.javalib.publish.VersionScheme 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..2a709b574b4e --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy @@ -0,0 +1,8 @@ + +println "This is a Groovy Script" +println getHelloString() + +static String getHelloString() { + return "Hello, world!" +} + diff --git a/libs/groovylib/test/resources/hello-groovy/main/spock/src/SpockTest.groovy b/libs/groovylib/test/resources/hello-groovy/main/spock/src/SpockTest.groovy new file mode 100644 index 000000000000..6a4ef42f4a76 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/main/spock/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/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/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala new file mode 100644 index 000000000000..3ec2314fedaa --- /dev/null +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -0,0 +1,185 @@ +package mill +package groovylib + +import mill.javalib.{JavaModule, MavenModule, TestModule} +import mill.api.{ExecResult, Task} +import mill.api.Discover +import mill.testkit.{TestRootModule, UnitTester} +import utest.* + +object HelloGroovyTests extends TestSuite { + + val groovy4Version = "4.0.28" + val junit5Version = sys.props.getOrElse("TEST_JUNIT5_VERSION", "5.13.4") + + object HelloGroovy extends TestRootModule { + + lazy val millDiscover = Discover[this.type] + + // needed for a special test where only the tests are written in Groovy while appcode remains Java + object `mixed-compile` extends JavaModule with MavenModule { + + object `test` extends TestGroovyMavenModule with TestModule.Junit5 { + + override def moduleDeps: Seq[JavaModule] = Seq( + HelloGroovy.`mixed-compile`, // TODO improve: TestOnly does not inherit outer deps + ) + + override def groovyVersion = groovy4Version + override def depManagement = Seq( + mvn"org.junit.jupiter:junit-jupiter-engine:$junit5Version" + ) + override def jupiterVersion = junit5Version + override def junitPlatformVersion = "1.13.4" + } + + } + + trait Test extends GroovyModule { + + override def mainClass = Some("hello.Hello") + + object test extends GroovyTests with TestModule.Junit5 { + + override def depManagement = Seq( + mvn"org.junit.jupiter:junit-jupiter-engine:5.13.4" + ) + + override def jupiterVersion = "5.13.4" + override def junitPlatformVersion = "1.13.4" + } + + object script extends GroovyModule { + override def groovyVersion = "4.0.28" + override def mainClass = Some("HelloScript") + } + + object staticcompile extends GroovyModule { + override def groovyVersion = "4.0.28" + override def mainClass = Some("hellostatic.HelloStatic") + } + + object spock extends GroovyTests with TestModule.Junit5 { + override def junitPlatformVersion = "1.13.4" + def spockVersion: T[String] = "2.3-groovy-4.0" + override def groovyVersion = "4.0.28" + + def bomMvnDeps = Seq( + mvn"org.junit:junit-bom:5.13.4", + mvn"org.apache.groovy:groovy-bom:${groovyVersion()}", + mvn"org.spockframework:spock-bom:${spockVersion()}" + ) + + def mvnDeps = Seq( + mvn"org.spockframework:spock-core" + ) + } + } + object main extends Test { + override def groovyVersion: T[String] = groovy4Version + } + } + + 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 m = HelloGroovy.main + def mixed = HelloGroovy.`mixed-compile` + + test("running a Groovy script") { + testEval().scoped { eval => + val Right(_) = eval.apply(m.script.run()): @unchecked + } + } + + test("compile & run Groovy module") { + testEval().scoped { eval => + val Right(compiler) = eval.apply(m.groovyCompilerMvnDeps): @unchecked + + assert( + compiler.value.map(_.dep.module) + .map(m => m.organization.value -> m.name.value) + .contains("org.apache.groovy" -> "groovy") + ) + + 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 => + + 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("compiling & running a statically compiled Groovy") { +// testEval().scoped { eval => +// val Right(_) = eval.apply(m.staticcompile.showMvnDepsTree()): @unchecked +// 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 & run test-only Maven JUnit5 test") { + testEval().scoped { eval => + + val Right(resultCompile) = eval.apply(mixed.compile): @unchecked + assert( + os.walk(resultCompile.value.classes.path).exists(_.last == "Greeter.class") + ) + + val Right(_) = eval.apply(mixed.test.compile): @unchecked + val Right(discovered) = eval.apply(mixed.test.discoveredTestClasses): @unchecked + assert(discovered.value == Seq("tests.GreeterTests")) + + val Right(_) = eval.apply(mixed.test.testForked()): @unchecked + } + } + +// test("compile Spock test") { +// testEval().scoped { eval => +// +// val Right(result1) = eval.apply(m.spock.compile): @unchecked +// assert( +// os.walk(result1.value.classes.path).exists(_.last == "SpockTest.class") +// ) +// } +// } + +// test("run Spock test") { +// testEval().scoped { eval => +// +// val Right(discovered) = eval.apply(m.spock.discoveredTestClasses): @unchecked +// assert(discovered.value == Seq("hello.spock.SpockTest")) +// +// val Right(_) = eval.apply(m.spock.testForked()): @unchecked +// } +// } + + } +} 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..2ea7f3e0dc80 --- /dev/null +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -0,0 +1,43 @@ +package mill.groovylib.worker.impl + +import mill.api.Result +import mill.api.TaskCtx +import mill.javalib.api.CompilationResult +import mill.groovylib.worker.api.GroovyWorker +import org.codehaus.groovy.control.{CompilationUnit, CompilerConfiguration, Phases} + +import scala.jdk.CollectionConverters.* +import scala.util.Try + +class GroovyWorkerImpl extends GroovyWorker { + + def compile( + sourceFiles: Seq[os.Path], + classpath: Seq[os.Path], + outputDir: os.Path + )(implicit + ctx: TaskCtx + ): Result[CompilationResult] = { + + val config = new CompilerConfiguration() + config.setTargetDirectory(outputDir.toIO) + config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) + + val unit = new CompilationUnit(config) + + // Add source files to compile + sourceFiles.foreach { sourceFile => + unit.addSource(sourceFile.toIO) + } + + return 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..7c20b7e2ccaf 100644 --- a/libs/javalib/src/mill/javalib/TestModule.scala +++ b/libs/javalib/src/mill/javalib/TestModule.scala @@ -2,8 +2,7 @@ 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 @@ -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. */ @@ -600,6 +600,41 @@ object TestModule { } } + /** + * TestModule that uses Spock Test Framework to run tests. + * You can override the [[spockTestVersion]] task or provide the Spock-dependency yourself. + */ + 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 { + "" + } + + /** The Groovy version to use, or the empty string, if you want to provide the Groovy Test-dependency yourself. */ + def groovyVersion: T[String] = Task { + "" + } + + override def mandatoryMvnDeps: T[Seq[Dep]] = Task { + super.mandatoryMvnDeps() ++ + Seq(spockVersion()) + .filter(!_.isBlank()) + .flatMap(v => + Seq( + mvn"org.spockframework:spock-core:${v.trim()}" + ) + ) ++ + Seq(groovyVersion()) + .filter(!_.isBlank()) + .flatMap(v => + Seq( + mvn"org.apache.groovy:groovy:${v.trim()}" + ) + ) + } + } + def handleResults( doneMsg: String, results: Seq[TestResult], 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 { From 51d62665c178257c31c7d2bdbf0a5be9adb344d1 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Thu, 11 Sep 2025 21:27:18 +0200 Subject: [PATCH 02/23] adding more test cases and fixing classloader issue during groovy compile which also fixed spock not being executed --- libs/groovylib/package.mill | 6 +- .../src/mill/groovylib/GroovyModule.scala | 53 +++++---------- .../mill/groovylib/GroovyWorkerManager.scala | 1 + .../joint-compile/src/GroovyGreeter.groovy | 20 ++++++ .../joint-compile/src/JavaMain.java | 12 ++++ .../joint-compile/src/JavaPrinter.java | 7 ++ .../main/script/src/HelloScript.groovy | 3 +- .../src/mill/groovylib/HelloGroovyTests.scala | 68 ++++++++----------- .../worker/impl/GroovyWorkerImpl.scala | 19 ++++-- 9 files changed, 100 insertions(+), 89 deletions(-) create mode 100644 libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy create mode 100644 libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java create mode 100644 libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaPrinter.java diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index e8b390db59d3..bf7ee642f9a7 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -1,6 +1,5 @@ package build.libs.groovylib -// imports import mill.* import mill.contrib.buildinfo.BuildInfo import mill.scalalib.* @@ -20,10 +19,7 @@ object `package` extends MillPublishScalaModule with BuildInfo { trait MillGroovyModule extends MillPublishScalaModule { override def javacOptions = super.javacOptions() ++ { - val release = - if (scala.util.Properties.isJavaAtLeast(11)) Seq("-release", "8") - else Seq("-source", "1.8", "-target", "1.8") - release ++ Seq("-encoding", "UTF-8", "-deprecation") + Seq("-release", "8", "-encoding", "UTF-8", "-deprecation") } } diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 08ef7115e95c..4d8df7301af9 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -26,9 +26,8 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => */ def groovyLanguageVersion: T[String] = Task { groovyVersion().split("[.]").take(2).mkString(".") } - override def bomMvnDeps: T[Seq[Dep]] = super.bomMvnDeps() ++ Seq( - mvn"org.apache.groovy:groovy-bom:${groovyVersion()}" - ) + override def bomMvnDeps: T[Seq[Dep]] = super.bomMvnDeps() ++ + Seq(mvn"org.apache.groovy:groovy-bom:${groovyVersion()}") /** * All individual source files fed into the compiler. @@ -58,12 +57,15 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => * Defaults to add the groovy dependency matching the [[groovyVersion]]. */ override def mandatoryMvnDeps: T[Seq[Dep]] = Task { - super.mandatoryMvnDeps() ++ Seq( - mvn"org.apache.groovy:groovy:${groovyVersion()}" - ) + super.mandatoryMvnDeps() + ++ + groovyCompilerMvnDeps() +// Seq( +// mvn"org.apache.groovy:groovy:${groovyVersion()}" +// ) } - private def jvmWorkerRef: ModuleRef[JvmWorkerModule] = jvmWorker + def jvmWorkerRef: ModuleRef[JvmWorkerModule] = jvmWorker override def checkGradleModules: T[Boolean] = true @@ -89,7 +91,8 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => def groovyCompilerMvnDeps: T[Seq[Dep]] = Task { val gv = groovyVersion() - val compilerDep = mvn"org.apache.groovy:groovy:$gv" + val compilerDep = mvn"org.apache.groovy:groovy-all:$gv" +// val annotationDep = mvn"org.codehaus.groovy:groovy-all-annotations:$gv" Seq(compilerDep) } @@ -104,9 +107,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => */ def groovyCompilerPluginJars: T[Seq[PathRef]] = Task { val jars = defaultResolver().classpath( - groovycPluginMvnDeps() - // Don't resolve transitive jars - .map(d => d.exclude("*" -> "*")), + allMvnDeps(), resolutionParamsMapOpt = None ) jars.toSeq @@ -122,6 +123,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => /** * The actual Groovy compile task (used by [[compile]] and [[groovycHelp]]). */ + // TODO joint compilation: generate groovy-stubs -> compile java -> compile groovy -> delete stubs (or keep for debugging) protected def groovyCompileTask(): Task[CompilationResult] = Task.Anon { val ctx = Task.ctx() @@ -152,7 +154,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => javaHome = javaHome().map(_.path), javacOptions = javacOptions(), compileProblemReporter = ctx.reporter(hashCode), - reportOldProblems = internalReportOldProblems() + reportOldProblems = zincReportCachedProblems() ) } @@ -189,18 +191,6 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => } } - /** - * Additional Groovy compiler options to be used by [[compile]]. - */ - def groovycOptions: T[Seq[String]] = Task { Seq.empty[String] } - - /** - * Aggregation of all the options passed to the Groovy compiler. - * In most cases, instead of overriding this target you want to override `groovycOptions` instead. - */ - def allGroovycOptions: T[Seq[String]] = Task { - groovycOptions() - } private[groovylib] def internalCompileJavaFiles( worker: JvmWorkerApi, @@ -228,8 +218,6 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => ) } - private[groovylib] def internalReportOldProblems: Task[Boolean] = zincReportCachedProblems - @internal override def bspBuildTarget: BspBuildTarget = super.bspBuildTarget.copy( languageIds = Seq( @@ -254,16 +242,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => override def groovyLanguageVersion: T[String] = outer.groovyLanguageVersion() override def groovyVersion: T[String] = Task { outer.groovyVersion() } - override def groovycPluginMvnDeps: T[Seq[Dep]] = - Task { outer.groovycPluginMvnDeps() } - // TODO: make Xfriend-path an explicit setting - override def groovycOptions: T[Seq[String]] = Task { - // FIXME - outer.groovycOptions().filterNot(_.startsWith("-Xcommon-sources")) ++ - Seq(s"-Xfriend-paths=${outer.compile().classes.path.toString()}") - } + override def bomMvnDeps: T[Seq[Dep]] = outer.bomMvnDeps() + override def mandatoryMvnDeps: Task.Simple[Seq[Dep]] = outer.mandatoryMvnDeps } - } - -// TODO maybe an StandaloneGroovyTestsModule diff --git a/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala index 9ccdc7845054..292d5290dd4b 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala @@ -17,6 +17,7 @@ object GroovyWorkerManager extends ExternalModule { } def get(toolsClassLoader: ClassLoader)(implicit ctx: TaskCtx): GroovyWorker = { + // TODO why not use ServiceLoader...investigate val className = classOf[GroovyWorker].getPackage().getName().split("\\.").dropRight(1).mkString( "." diff --git a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy new file mode 100644 index 000000000000..f23aee4b2224 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy @@ -0,0 +1,20 @@ +package jointcompile + +import jointCompile.JavaPrinter + +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/joint-compile/src/JavaMain.java b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java new file mode 100644 index 000000000000..14ae5384840d --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java @@ -0,0 +1,12 @@ +package jointcompile; + +import jointcompile.GroovyGreeter; + + +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/joint-compile/src/JavaPrinter.java b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaPrinter.java new file mode 100644 index 000000000000..eb6dcfd64e19 --- /dev/null +++ b/libs/groovylib/test/resources/hello-groovy/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 index 2a709b574b4e..ccdd2a9a93e9 100644 --- a/libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy +++ b/libs/groovylib/test/resources/hello-groovy/main/script/src/HelloScript.groovy @@ -1,8 +1,7 @@ -println "This is a Groovy Script" println getHelloString() static String getHelloString() { - return "Hello, world!" + return "Hello, Scripting!" } diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 3ec2314fedaa..77a65bf2d01c 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -35,16 +35,18 @@ object HelloGroovyTests extends TestSuite { } + object `joint-compile` extends GroovyModule { + override def groovyVersion: T[String] = groovy4Version + } + trait Test extends GroovyModule { override def mainClass = Some("hello.Hello") object test extends GroovyTests with TestModule.Junit5 { - override def depManagement = Seq( mvn"org.junit.jupiter:junit-jupiter-engine:5.13.4" ) - override def jupiterVersion = "5.13.4" override def junitPlatformVersion = "1.13.4" } @@ -100,14 +102,6 @@ object HelloGroovyTests extends TestSuite { test("compile & run Groovy module") { testEval().scoped { eval => - val Right(compiler) = eval.apply(m.groovyCompilerMvnDeps): @unchecked - - assert( - compiler.value.map(_.dep.module) - .map(m => m.organization.value -> m.name.value) - .contains("org.apache.groovy" -> "groovy") - ) - val Right(result) = eval.apply(m.compile): @unchecked assert( @@ -134,16 +128,15 @@ object HelloGroovyTests extends TestSuite { } } -// test("compiling & running a statically compiled Groovy") { -// testEval().scoped { eval => -// val Right(_) = eval.apply(m.staticcompile.showMvnDepsTree()): @unchecked -// 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("compiling & running a statically compiled Groovy") { + testEval().scoped { eval => + 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 & run test-only Maven JUnit5 test") { testEval().scoped { eval => @@ -161,25 +154,22 @@ object HelloGroovyTests extends TestSuite { } } -// test("compile Spock test") { -// testEval().scoped { eval => -// -// val Right(result1) = eval.apply(m.spock.compile): @unchecked -// assert( -// os.walk(result1.value.classes.path).exists(_.last == "SpockTest.class") -// ) -// } -// } - -// test("run Spock test") { -// testEval().scoped { eval => -// -// val Right(discovered) = eval.apply(m.spock.discoveredTestClasses): @unchecked -// assert(discovered.value == Seq("hello.spock.SpockTest")) -// -// val Right(_) = eval.apply(m.spock.testForked()): @unchecked -// } -// } + test("compile & run Spock test") { + testEval().scoped { eval => + + val Right(result1) = eval.apply(m.spock.compile): @unchecked + assert( + os.walk(result1.value.classes.path).exists(_.last == "SpockTest.class") + ) + + val Right(discovered) = eval.apply(m.spock.discoveredTestClasses): @unchecked + assert(discovered.value == Seq("hello.spock.SpockTest")) + + val Right(_) = eval.apply(m.spock.testForked()): @unchecked + } + } + + } } diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala index 2ea7f3e0dc80..cd7e26d06cf9 100644 --- a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -1,5 +1,6 @@ package mill.groovylib.worker.impl +import groovy.lang.GroovyClassLoader import mill.api.Result import mill.api.TaskCtx import mill.javalib.api.CompilationResult @@ -22,22 +23,28 @@ class GroovyWorkerImpl extends GroovyWorker { val config = new CompilerConfiguration() config.setTargetDirectory(outputDir.toIO) config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) + // TODO +// config.setDisabledGlobalASTTransformations() +// config.setJointCompilationOptions() +// config.setSourceEncoding() + + // we need to set the classloader for groovy to use the worker classloader + val parentCl: ClassLoader = this.getClass.getClassLoader + // config in the GroovyClassLoader is needed when the CL itself is compiling classes + val gcl = new GroovyClassLoader(parentCl, config) + // config for actual compilation + val unit = new CompilationUnit(config, null, gcl) - val unit = new CompilationUnit(config) - - // Add source files to compile sourceFiles.foreach { sourceFile => unit.addSource(sourceFile.toIO) } - return Try { + 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) ) } - } From 4bcb5b198c912551791e8c02c1033e12771b1839 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Fri, 12 Sep 2025 13:12:03 +0200 Subject: [PATCH 03/23] improve test-only-groovy to not rely on an existing outer module --- .../JavaMavenModuleWithGroovyTests.scala | 16 ++++++++++++ .../groovylib/TestGroovyMavenModule.scala | 25 ------------------- .../src/mill/groovylib/exports.scala | 4 +++ .../joint-compile/src/GroovyGreeter.groovy | 2 -- .../joint-compile/src/JavaMain.java | 3 --- 5 files changed, 20 insertions(+), 30 deletions(-) create mode 100644 libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala delete mode 100644 libs/groovylib/src/mill/groovylib/TestGroovyMavenModule.scala diff --git a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala new file mode 100644 index 000000000000..c7018dcca885 --- /dev/null +++ b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala @@ -0,0 +1,16 @@ +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 + */ +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/TestGroovyMavenModule.scala b/libs/groovylib/src/mill/groovylib/TestGroovyMavenModule.scala deleted file mode 100644 index 1ad30eb67678..000000000000 --- a/libs/groovylib/src/mill/groovylib/TestGroovyMavenModule.scala +++ /dev/null @@ -1,25 +0,0 @@ -package mill.groovylib - -import mill.* - -/** - * A [[GroovyModule]] for tests with a Maven compatible directory layout: - * `src/test/groovy`, `src/test/resources`, etc. - * This is useful when only the tests are written in Groovy while the production code remains in Java or other. - * - * Requires an outer module (like JavaModule) otherwise [[moduleDir]] must be overridden manually. - */ -trait TestGroovyMavenModule extends GroovyMavenModule { - - /** - * The name of this module's folder within `src/`: e.g. `src/test/`, `src/integration/`, - * etc. Defaults to the name of the module object, but can be overridden by users - */ - def testModuleName: String = moduleCtx.segments.last.value - - private def testSources = Task.Sources(moduleDir / os.up / "src" / testModuleName / "groovy") - override def sources: T[Seq[PathRef]] = testSources() - - private def testResources = Task.Sources(moduleDir / os.up / "src" / testModuleName / "resources") - override def resources: T[Seq[PathRef]] = testResources() -} diff --git a/libs/groovylib/src/mill/groovylib/exports.scala b/libs/groovylib/src/mill/groovylib/exports.scala index 9657cb08d604..89d445c8033c 100644 --- a/libs/groovylib/src/mill/groovylib/exports.scala +++ b/libs/groovylib/src/mill/groovylib/exports.scala @@ -4,6 +4,10 @@ export mill.javalib.DepSyntax export mill.javalib.Dep +export mill.javalib.JavaModule + +export mill.javalib.MavenModule + export mill.javalib.TestModule export mill.javalib.PublishModule diff --git a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy index f23aee4b2224..d876ed9310c3 100644 --- a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy +++ b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy @@ -1,7 +1,5 @@ package jointcompile -import jointCompile.JavaPrinter - class GroovyGreeter{ private final String toGreet; diff --git a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java index 14ae5384840d..6522149af6c4 100644 --- a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java +++ b/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java @@ -1,8 +1,5 @@ package jointcompile; -import jointcompile.GroovyGreeter; - - public class JavaMain { public static void main(String[] args) { From 45dfd9bc69da10af42c95c81f4bc43cef64d01ee Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Mon, 15 Sep 2025 11:03:25 +0200 Subject: [PATCH 04/23] 3-stage compile to support Groovy <-> Java cycles --- .../groovylib/worker/api/GroovyWorker.scala | 27 +++++- libs/groovylib/package.mill | 2 +- .../src/mill/groovylib/GroovyModule.scala | 85 +++++++------------ .../src/test/groovy/HelloMavenTestOnly.groovy | 12 +++ .../src/mill/groovylib/HelloGroovyTests.scala | 54 +++++++----- .../worker/impl/GroovyWorkerImpl.scala | 50 ++++++++++- .../javalib/src/mill/javalib/TestModule.scala | 15 ++-- 7 files changed, 157 insertions(+), 88 deletions(-) create mode 100644 libs/groovylib/test/resources/hello-groovy/groovy-tests/src/test/groovy/HelloMavenTestOnly.groovy diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala index 9e4a6c5cf4fc..d92e90463e7a 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -4,9 +4,34 @@ import mill.api.TaskCtx import mill.api.Result 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 + */ 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 + )(implicit + ctx: TaskCtx + ) + : Result[CompilationResult] + + /** + * 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)(implicit ctx: TaskCtx - ): Result[CompilationResult] + ) + : Result[CompilationResult] } diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index bf7ee642f9a7..483692461a40 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -8,7 +8,7 @@ import millbuild.* object `package` extends MillPublishScalaModule with BuildInfo { def moduleDeps = Seq(build.libs.javalib, build.libs.javalib.testrunner, api) - def localTestExtraModules = + def localTestExtraModules: Seq[MillJavaModule] = super.localTestExtraModules ++ Seq(worker) def buildInfoPackageName = "mill.groovylib" diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 4d8df7301af9..21d40f42e733 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -12,7 +12,9 @@ import mill.api.daemon.internal.bsp.{BspBuildTarget, BspModuleApi} import mill.javalib.api.internal.{JavaCompilerOptions, JvmWorkerApi, ZincCompileJava} /** - * Core configuration required to compile a single Groovy module + * Core configuration required to compile a single Groovy module. + * + * Resolves */ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => @@ -33,7 +35,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => * All individual source files fed into the compiler. */ override def allSourceFiles: T[Seq[PathRef]] = Task { - Lib.findSourceFiles(allSources(), Seq("groovy", "java")).map(PathRef(_)) + allGroovySourceFiles() ++ allJavaSourceFiles() } /** @@ -41,7 +43,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => * Subset of [[allSourceFiles]]. */ private def allJavaSourceFiles: T[Seq[PathRef]] = Task { - allSourceFiles().filter(_.path.ext.toLowerCase() == "java") + Lib.findSourceFiles(allSources(), Seq("java")).map(PathRef(_)) } /** @@ -49,20 +51,15 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => * Subset of [[allSourceFiles]]. */ private def allGroovySourceFiles: T[Seq[PathRef]] = Task { - allSourceFiles().filter(path => Seq("groovy").contains(path.path.ext.toLowerCase())) + Lib.findSourceFiles(allSources(), Seq("groovy")).map(PathRef(_)) } /** * The dependencies of this module. - * Defaults to add the groovy dependency matching the [[groovyVersion]]. + * Defaults to add the Groovy dependency matching the [[groovyVersion]]. */ override def mandatoryMvnDeps: T[Seq[Dep]] = Task { - super.mandatoryMvnDeps() - ++ - groovyCompilerMvnDeps() -// Seq( -// mvn"org.apache.groovy:groovy:${groovyVersion()}" -// ) + super.mandatoryMvnDeps() ++ groovyCompilerMvnDeps() } def jvmWorkerRef: ModuleRef[JvmWorkerModule] = jvmWorker @@ -86,31 +83,11 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => /** * The Ivy/Coursier dependencies resembling the Groovy compiler. * - * Default is derived from [[groovyCompilerVersion]]. + * Default is derived from [[groovyVersion]]. */ def groovyCompilerMvnDeps: T[Seq[Dep]] = Task { val gv = groovyVersion() - - val compilerDep = mvn"org.apache.groovy:groovy-all:$gv" -// val annotationDep = mvn"org.codehaus.groovy:groovy-all-annotations:$gv" - - Seq(compilerDep) - } - - /** - * Compiler Plugin dependencies. - */ - def groovyCompilerPluginMvnDeps: T[Seq[Dep]] = Task { Seq.empty[Dep] } - - /** - * The resolved plugin jars - */ - def groovyCompilerPluginJars: T[Seq[PathRef]] = Task { - val jars = defaultResolver().classpath( - allMvnDeps(), - resolutionParamsMapOpt = None - ) - jars.toSeq + Seq(mvn"org.apache.groovy:groovy:$gv") } /** @@ -121,9 +98,8 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => } /** - * The actual Groovy compile task (used by [[compile]] and [[groovycHelp]]). + * The actual Groovy compile task (used by [[compile]]). */ - // TODO joint compilation: generate groovy-stubs -> compile java -> compile groovy -> delete stubs (or keep for debugging) protected def groovyCompileTask(): Task[CompilationResult] = Task.Anon { val ctx = Task.ctx() @@ -143,9 +119,9 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => def compileJava: Result[CompilationResult] = { ctx.log.info( - s"Compiling ${javaSourceFiles.size} Java sources to ${classes} ..." + s"Compiling ${javaSourceFiles.size} Java sources to $classes ..." ) - // The compile step is lazy, but its dependencies are not! + // The compiler step is lazy, but its dependencies are not! internalCompileJavaFiles( worker = jvmWorkerRef().internalWorker(), upstreamCompileOutput = updateCompileOutput, @@ -158,40 +134,43 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => ) } + if (isMixed) { + ctx.log.info("Compiling Groovy stubs for mixed compilation") + + val workerStubResult = + GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { + _.compileGroovyStubs(groovySourceFiles, compileCp, classes) + } + workerStubResult match { + case Result.Success(_) => compileJava + case Result.Failure(reason) => Result.Failure(reason) + } + } + if (isMixed || isGroovy) { ctx.log.info( - s"Compiling ${groovySourceFiles.size} Groovy sources to ${classes} ..." + s"Compiling ${groovySourceFiles.size} Groovy sources to $classes ..." ) - val compileCp = compileClasspath().map(_.path).filter(os.exists) - - val workerResult = + val workerGroovyResult = GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { _.compile(groovySourceFiles, compileCp, classes) } + // 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) - workerResult match { + workerGroovyResult match { case Result.Success(_) => - val cr = CompilationResult(analysisFile, PathRef(classes)) - if (!isJava) { - // pure Groovy project - cr - } else { - // also run Java compiler and use it's returned result - compileJava - } + CompilationResult(analysisFile, PathRef(classes)) case Result.Failure(reason) => Result.Failure(reason) } } else { - // it's Java only compileJava } } - private[groovylib] def internalCompileJavaFiles( worker: JvmWorkerApi, upstreamCompileOutput: Seq[CompilationResult], @@ -236,7 +215,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => } /** - * A test sub-module linked to its parent module best suited for unit-tests. + * A test submodule linked to its parent module best suited for unit-tests. */ trait GroovyTests extends JavaTests with GroovyModule { 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/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 77a65bf2d01c..9a3818f79227 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -1,8 +1,8 @@ package mill package groovylib -import mill.javalib.{JavaModule, MavenModule, TestModule} -import mill.api.{ExecResult, Task} +import mill.javalib.{JavaModule, TestModule} +import mill.api.{Task} import mill.api.Discover import mill.testkit.{TestRootModule, UnitTester} import utest.* @@ -17,12 +17,12 @@ object HelloGroovyTests extends TestSuite { lazy val millDiscover = Discover[this.type] // needed for a special test where only the tests are written in Groovy while appcode remains Java - object `mixed-compile` extends JavaModule with MavenModule { + object `groovy-tests` extends JavaMavenModuleWithGroovyTests { - object `test` extends TestGroovyMavenModule with TestModule.Junit5 { + object `test` extends GroovyMavenTests with TestModule.Junit5 { override def moduleDeps: Seq[JavaModule] = Seq( - HelloGroovy.`mixed-compile`, // TODO improve: TestOnly does not inherit outer deps + HelloGroovy.`groovy-tests` ) override def groovyVersion = groovy4Version @@ -37,6 +37,7 @@ object HelloGroovyTests extends TestSuite { object `joint-compile` extends GroovyModule { override def groovyVersion: T[String] = groovy4Version + override def mainClass = Some("jointcompile.JavaMain") } trait Test extends GroovyModule { @@ -61,20 +62,16 @@ object HelloGroovyTests extends TestSuite { override def mainClass = Some("hellostatic.HelloStatic") } - object spock extends GroovyTests with TestModule.Junit5 { + object spock extends GroovyTests with TestModule.Spock { override def junitPlatformVersion = "1.13.4" - def spockVersion: T[String] = "2.3-groovy-4.0" + override def spockVersion = "2.3-groovy-4.0" override def groovyVersion = "4.0.28" def bomMvnDeps = Seq( mvn"org.junit:junit-bom:5.13.4", - mvn"org.apache.groovy:groovy-bom:${groovyVersion()}", +// mvn"org.apache.groovy:groovy-bom:${groovyVersion()}", mvn"org.spockframework:spock-bom:${spockVersion()}" ) - - def mvnDeps = Seq( - mvn"org.spockframework:spock-core" - ) } } object main extends Test { @@ -92,7 +89,14 @@ object HelloGroovyTests extends TestSuite { def tests: Tests = Tests { def m = HelloGroovy.main - def mixed = HelloGroovy.`mixed-compile` + def mixed = HelloGroovy.`groovy-tests` + def joint = HelloGroovy.`joint-compile` + + test("running a Groovy script") { + testEval().scoped { eval => + val Right(_) = eval.apply(m.script.run()): @unchecked + } + } test("running a Groovy script") { testEval().scoped { eval => @@ -128,7 +132,7 @@ object HelloGroovyTests extends TestSuite { } } - test("compiling & running a statically compiled Groovy") { + test("compile & run a statically compiled Groovy") { testEval().scoped { eval => val Right(result) = eval.apply(m.staticcompile.compile): @unchecked assert( @@ -138,17 +142,12 @@ object HelloGroovyTests extends TestSuite { } } - test("compile & run test-only Maven JUnit5 test") { + test("compile & test module (only test uses Groovy)") { testEval().scoped { eval => - val Right(resultCompile) = eval.apply(mixed.compile): @unchecked - assert( - os.walk(resultCompile.value.classes.path).exists(_.last == "Greeter.class") - ) - val Right(_) = eval.apply(mixed.test.compile): @unchecked val Right(discovered) = eval.apply(mixed.test.discoveredTestClasses): @unchecked - assert(discovered.value == Seq("tests.GreeterTests")) + assert(discovered.value == Seq("hello.maven.tests.HelloMavenTestOnly")) val Right(_) = eval.apply(mixed.test.testForked()): @unchecked } @@ -169,7 +168,20 @@ object HelloGroovyTests extends TestSuite { } } + test("compile joint (groovy <-> java cycle) & run") { + testEval().scoped { eval => + val Right(result) = eval.apply(joint.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(joint.run()): @unchecked + } + } } } diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala index cd7e26d06cf9..391ac9a702dd 100644 --- a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -6,12 +6,48 @@ import mill.api.TaskCtx import mill.javalib.api.CompilationResult import mill.groovylib.worker.api.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 + )(implicit ctx: TaskCtx): Result[CompilationResult] = { + val config = new CompilerConfiguration() + config.setTargetDirectory(outputDir.toIO) + config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) + config.setJointCompilationOptions(Map( + "stubDir" -> outputDir.toIO, + "keepStubs" -> false + ).asJava) + + // we need to set the classloader for groovy to use the worker classloader + val parentCl: ClassLoader = this.getClass.getClassLoader + // config in the GroovyClassLoader is needed when the CL itself is compiling classes + val gcl = new GroovyClassLoader(parentCl, config) + // config for actual compilation + val stubUnit = JavaStubCompilationUnit(config, gcl) + + sourceFiles.foreach { sourceFile => + stubUnit.addSource(sourceFile.toIO) + } + + Try { + stubUnit.compile(Phases.CONVERSION) + CompilationResult(outputDir, mill.api.PathRef(outputDir)) + }.fold( + exception => Result.Failure(s"Groovy stub generation failed: ${exception.getMessage}"), + result => Result.Success(result) + ) + + } + def compile( sourceFiles: Seq[os.Path], classpath: Seq[os.Path], @@ -20,12 +56,13 @@ class GroovyWorkerImpl extends GroovyWorker { ctx: TaskCtx ): Result[CompilationResult] = { + val extendedClasspath = classpath :+ outputDir + val config = new CompilerConfiguration() config.setTargetDirectory(outputDir.toIO) - config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) + config.setClasspathList(extendedClasspath.map(_.toIO.getAbsolutePath).asJava) // TODO // config.setDisabledGlobalASTTransformations() -// config.setJointCompilationOptions() // config.setSourceEncoding() // we need to set the classloader for groovy to use the worker classloader @@ -41,10 +78,19 @@ class GroovyWorkerImpl extends GroovyWorker { Try { unit.compile(Phases.OUTPUT) + removeAllJavaFiles(outputDir) CompilationResult(outputDir, mill.api.PathRef(outputDir)) }.fold( exception => Result.Failure(s"Groovy compilation failed: ${exception.getMessage}"), result => Result.Success(result) ) } + + private def removeAllJavaFiles(outputDir: os.Path): Unit = { + if (os.exists(outputDir)) { + os.walk(outputDir) + .filter(_.ext == "java") + .foreach(os.remove) + } + } } diff --git a/libs/javalib/src/mill/javalib/TestModule.scala b/libs/javalib/src/mill/javalib/TestModule.scala index 7c20b7e2ccaf..24f1e9805df3 100644 --- a/libs/javalib/src/mill/javalib/TestModule.scala +++ b/libs/javalib/src/mill/javalib/TestModule.scala @@ -602,20 +602,22 @@ object TestModule { /** * TestModule that uses Spock Test Framework to run tests. - * You can override the [[spockTestVersion]] task or provide the Spock-dependency yourself. + * You can override the [[spockVersion]] task or provide the Spock dependency yourself. */ 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. */ + /** 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 { "" } - /** The Groovy version to use, or the empty string, if you want to provide the Groovy Test-dependency yourself. */ + /** The Groovy version to use, or the empty string, if you want to provide the Groovy test dependency yourself. */ def groovyVersion: T[String] = Task { "" } + // TODO currently bomMvnDeps not in JavaModuleBase so we cannot pull in the Spock-BOM + override def mandatoryMvnDeps: T[Seq[Dep]] = Task { super.mandatoryMvnDeps() ++ Seq(spockVersion()) @@ -624,13 +626,6 @@ object TestModule { Seq( mvn"org.spockframework:spock-core:${v.trim()}" ) - ) ++ - Seq(groovyVersion()) - .filter(!_.isBlank()) - .flatMap(v => - Seq( - mvn"org.apache.groovy:groovy:${v.trim()}" - ) ) } } From cacf2c7e5b296b8e23885fb845f85b500bc6bc4a Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Tue, 16 Sep 2025 15:21:30 +0200 Subject: [PATCH 05/23] manage BOMs in Tests Starting with JUnit 5.12 and Spock 2.3 a BOM is available on Maven Central. If the version matches the BOMs will be automatically added. For JUnit, this has the effect that the platform-launcher is also managed in the BOM so it is actually not necessary to specify a platform-launcher-version. Hence the launcher is also added when the version is not set and the minimum version is met. --- libs/groovylib/package.mill | 2 +- .../src/mill/groovylib/GroovyModule.scala | 13 ++- .../src/mill/groovylib/HelloGroovyTests.scala | 109 ++++++++++++++---- .../javalib/src/mill/javalib/TestModule.scala | 65 +++++++++-- .../src/mill/javalib/junit5/JUnit5Tests.scala | 62 +++++++++- 5 files changed, 216 insertions(+), 35 deletions(-) diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index 483692461a40..e9720db4ff65 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -7,7 +7,7 @@ import millbuild.* object `package` extends MillPublishScalaModule with BuildInfo { - def moduleDeps = Seq(build.libs.javalib, build.libs.javalib.testrunner, api) + def moduleDeps = Seq(build.libs.util, build.libs.javalib, build.libs.javalib.testrunner, api) def localTestExtraModules: Seq[MillJavaModule] = super.localTestExtraModules ++ Seq(worker) diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 21d40f42e733..284315fac4d2 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -10,6 +10,7 @@ import mill.* import mainargs.Flag import mill.api.daemon.internal.bsp.{BspBuildTarget, BspModuleApi} import mill.javalib.api.internal.{JavaCompilerOptions, JvmWorkerApi, ZincCompileJava} +import mill.util.Version /** * Core configuration required to compile a single Groovy module. @@ -28,8 +29,18 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => */ def groovyLanguageVersion: T[String] = Task { groovyVersion().split("[.]").take(2).mkString(".") } + private def isGroovyBomAvailable: 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(mvn"org.apache.groovy:groovy-bom:${groovyVersion()}") + Seq(groovyVersion()) + .filter(_.nonEmpty && isGroovyBomAvailable()) + .map(v => mvn"org.apache.groovy:groovy-bom:$v") /** * All individual source files fed into the compiler. diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 9a3818f79227..8a0e1be24db4 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -2,7 +2,7 @@ package mill package groovylib import mill.javalib.{JavaModule, TestModule} -import mill.api.{Task} +import mill.api.Task import mill.api.Discover import mill.testkit.{TestRootModule, UnitTester} import utest.* @@ -11,6 +11,7 @@ object HelloGroovyTests extends TestSuite { val groovy4Version = "4.0.28" val junit5Version = sys.props.getOrElse("TEST_JUNIT5_VERSION", "5.13.4") + val spockGroovy4Version = "2.3-groovy-4.0" object HelloGroovy extends TestRootModule { @@ -25,12 +26,8 @@ object HelloGroovyTests extends TestSuite { HelloGroovy.`groovy-tests` ) - override def groovyVersion = groovy4Version - override def depManagement = Seq( - mvn"org.junit.jupiter:junit-jupiter-engine:$junit5Version" - ) - override def jupiterVersion = junit5Version - override def junitPlatformVersion = "1.13.4" + override def groovyVersion: T[String] = groovy4Version + override def jupiterVersion: T[String] = junit5Version } } @@ -40,38 +37,53 @@ object HelloGroovyTests extends TestSuite { override def mainClass = Some("jointcompile.JavaMain") } + 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 GroovyModule { override def mainClass = Some("hello.Hello") object test extends GroovyTests with TestModule.Junit5 { - override def depManagement = Seq( - mvn"org.junit.jupiter:junit-jupiter-engine:5.13.4" - ) - override def jupiterVersion = "5.13.4" + override def jupiterVersion: T[String] = junit5Version override def junitPlatformVersion = "1.13.4" } object script extends GroovyModule { - override def groovyVersion = "4.0.28" + override def groovyVersion: T[String] = groovy4Version override def mainClass = Some("HelloScript") } object staticcompile extends GroovyModule { - override def groovyVersion = "4.0.28" + override def groovyVersion: T[String] = groovy4Version override def mainClass = Some("hellostatic.HelloStatic") } object spock extends GroovyTests with TestModule.Spock { - override def junitPlatformVersion = "1.13.4" - override def spockVersion = "2.3-groovy-4.0" - override def groovyVersion = "4.0.28" - - def bomMvnDeps = Seq( - mvn"org.junit:junit-bom:5.13.4", -// mvn"org.apache.groovy:groovy-bom:${groovyVersion()}", - mvn"org.spockframework:spock-bom:${spockVersion()}" - ) + override def jupiterVersion: T[String] = junit5Version + override def spockVersion: T[String] = spockGroovy4Version + override def groovyVersion: T[String] = groovy4Version } } object main extends Test { @@ -91,6 +103,7 @@ object HelloGroovyTests extends TestSuite { def m = HelloGroovy.main def mixed = HelloGroovy.`groovy-tests` def joint = HelloGroovy.`joint-compile` + def deps = HelloGroovy.deps test("running a Groovy script") { testEval().scoped { eval => @@ -183,5 +196,57 @@ object HelloGroovyTests extends TestSuite { } } + 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/javalib/src/mill/javalib/TestModule.scala b/libs/javalib/src/mill/javalib/TestModule.scala index 24f1e9805df3..02d826dd0ade 100644 --- a/libs/javalib/src/mill/javalib/TestModule.scala +++ b/libs/javalib/src/mill/javalib/TestModule.scala @@ -9,7 +9,7 @@ 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.{ @@ -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 isJupiterBomAvailable: 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() && isJupiterBomAvailable()) + .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 (isJupiterBomAvailable()) { + 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()}") @@ -603,6 +632,9 @@ 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 { @@ -611,12 +643,24 @@ object TestModule { "" } - /** The Groovy version to use, or the empty string, if you want to provide the Groovy test dependency yourself. */ - def groovyVersion: T[String] = Task { - "" + private def isSpockBomAvailable: T[Boolean] = Task { + if (spockVersion().isBlank) { + false + } else { + Version.isAtLeast(spockVersion(), "2.3")(using Version.IgnoreQualifierOrdering) + } } - // TODO currently bomMvnDeps not in JavaModuleBase so we cannot pull in the Spock-BOM + 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() ++ @@ -650,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) + ) + } + } + } } } From eb13bc65bcc2e355b8974ce70969b4abf15639d5 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Tue, 23 Sep 2025 12:34:02 +0200 Subject: [PATCH 06/23] test with cross-compile Groovy 4 & 5 --- .../joint-compile/src/GroovyGreeter.groovy | 0 .../joint-compile/src/JavaMain.java | 0 .../joint-compile/src/JavaPrinter.java | 0 .../hello-groovy/spock/src/Hello.groovy | 16 ++ .../tests}/src/SpockTest.groovy | 0 .../src/mill/groovylib/HelloGroovyTests.scala | 138 ++++++++++-------- 6 files changed, 97 insertions(+), 57 deletions(-) rename libs/groovylib/test/resources/hello-groovy/{ => main}/joint-compile/src/GroovyGreeter.groovy (100%) rename libs/groovylib/test/resources/hello-groovy/{ => main}/joint-compile/src/JavaMain.java (100%) rename libs/groovylib/test/resources/hello-groovy/{ => main}/joint-compile/src/JavaPrinter.java (100%) create mode 100644 libs/groovylib/test/resources/hello-groovy/spock/src/Hello.groovy rename libs/groovylib/test/resources/hello-groovy/{main/spock => spock/tests}/src/SpockTest.groovy (100%) diff --git a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/GroovyGreeter.groovy similarity index 100% rename from libs/groovylib/test/resources/hello-groovy/joint-compile/src/GroovyGreeter.groovy rename to libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/GroovyGreeter.groovy diff --git a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaMain.java similarity index 100% rename from libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaMain.java rename to libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaMain.java diff --git a/libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaPrinter.java b/libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaPrinter.java similarity index 100% rename from libs/groovylib/test/resources/hello-groovy/joint-compile/src/JavaPrinter.java rename to libs/groovylib/test/resources/hello-groovy/main/joint-compile/src/JavaPrinter.java 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/main/spock/src/SpockTest.groovy b/libs/groovylib/test/resources/hello-groovy/spock/tests/src/SpockTest.groovy similarity index 100% rename from libs/groovylib/test/resources/hello-groovy/main/spock/src/SpockTest.groovy rename to libs/groovylib/test/resources/hello-groovy/spock/tests/src/SpockTest.groovy diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 8a0e1be24db4..8421fdf56cf7 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -10,11 +10,17 @@ import utest.* 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 @@ -32,11 +38,22 @@ object HelloGroovyTests extends TestSuite { } - object `joint-compile` extends GroovyModule { + /** + * 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 - override def mainClass = Some("jointcompile.JavaMain") + + 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 { @@ -61,34 +78,31 @@ object HelloGroovyTests extends TestSuite { } - trait Test extends GroovyModule { + trait Test extends GroovyVersionCross { override def mainClass = Some("hello.Hello") - object test extends GroovyTests with TestModule.Junit5 { - override def jupiterVersion: T[String] = junit5Version - override def junitPlatformVersion = "1.13.4" - } - object script extends GroovyModule { - override def groovyVersion: T[String] = groovy4Version + override def groovyVersion: T[String] = crossValue override def mainClass = Some("HelloScript") } object staticcompile extends GroovyModule { - override def groovyVersion: T[String] = groovy4Version + override def groovyVersion: T[String] = crossValue override def mainClass = Some("hellostatic.HelloStatic") } - object spock extends GroovyTests with TestModule.Spock { + 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 spockVersion: T[String] = spockGroovy4Version - override def groovyVersion: T[String] = groovy4Version + override def junitPlatformVersion = "1.13.4" } } - object main extends Test { - override def groovyVersion: T[String] = groovy4Version - } + object main extends Cross[Test](groovyVersions) } val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "hello-groovy" @@ -100,58 +114,84 @@ object HelloGroovyTests extends TestSuite { def tests: Tests = Tests { - def m = HelloGroovy.main + def main = HelloGroovy.main + def spock = HelloGroovy.spock def mixed = HelloGroovy.`groovy-tests` - def joint = HelloGroovy.`joint-compile` def deps = HelloGroovy.deps test("running a Groovy script") { testEval().scoped { eval => - val Right(_) = eval.apply(m.script.run()): @unchecked + main.crossModules.foreach(m => { + val Right(_) = eval.apply(m.script.run()): @unchecked + }) } } test("running a Groovy script") { testEval().scoped { eval => - val Right(_) = eval.apply(m.script.run()): @unchecked + main.crossModules.foreach(m => { + val Right(_) = eval.apply(m.script.run()): @unchecked + }) } } test("compile & run Groovy module") { testEval().scoped { eval => - val Right(result) = eval.apply(m.compile): @unchecked + main.crossModules.foreach(m => { + val Right(result) = eval.apply(m.compile): @unchecked - assert( - os.walk(result.value.classes.path).exists(_.last == "Hello.class") - ) + assert( + os.walk(result.value.classes.path).exists(_.last == "Hello.class") + ) - val Right(_) = eval.apply(m.run()): @unchecked + 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 - val Right(result) = eval.apply(m.test.compile): @unchecked + assert( + os.walk(result.value.classes.path).exists(_.last == "HelloTest.class") + ) - 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(discovered) = eval.apply(m.test.discoveredTestClasses): @unchecked + assert(discovered.value == Seq("hello.tests.HelloTest")) - val Right(_) = eval.apply(m.test.testForked()): @unchecked + val Right(_) = eval.apply(m.test.testForked()): @unchecked + }) } } test("compile & run a statically compiled Groovy") { testEval().scoped { eval => - 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 + 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 + }) } } @@ -168,31 +208,15 @@ object HelloGroovyTests extends TestSuite { test("compile & run Spock test") { testEval().scoped { eval => - - val Right(result1) = eval.apply(m.spock.compile): @unchecked + 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(m.spock.discoveredTestClasses): @unchecked + val Right(discovered) = eval.apply(spock.tests.discoveredTestClasses): @unchecked assert(discovered.value == Seq("hello.spock.SpockTest")) - val Right(_) = eval.apply(m.spock.testForked()): @unchecked - } - } - - test("compile joint (groovy <-> java cycle) & run") { - testEval().scoped { eval => - val Right(result) = eval.apply(joint.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(joint.run()): @unchecked + val Right(_) = eval.apply(spock.tests.testForked()): @unchecked } } From 13c7ec9be8a4a3d35cca55e97b5effd7d3373636 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Tue, 23 Sep 2025 15:43:35 +0200 Subject: [PATCH 07/23] samples --- example/groovylib/basic/1-simple/build.mill | 124 ++++++++++++++++++ .../basic/1-simple/foo/src/foo/Foo.groovy | 32 +++++ .../1-simple/foo/test/src/foo/FooTest.groovy | 15 +++ .../basic/2-compat-modules/build.mill | 59 +++++++++ .../groovy/foo/FooIntegrationTests.groovy | 12 ++ .../foo/src/main/groovy/foo/Foo.groovy | 9 ++ .../foo/src/main/java/foo/Foo2.java | 5 + .../foo/src/test/groovy/foo/FooTests.groovy | 13 ++ .../1-test-suite/bar/src/bar/Bar.groovy | 9 ++ .../bar/test/src/bar/BarTests.groovy | 29 ++++ .../groovylib/testing/1-test-suite/build.mill | 32 +++++ .../1-test-suite/foo/src/foo/Foo.groovy | 11 ++ .../foo/test/src/foo/FooTests.groovy | 29 ++++ .../testing/2-integration-suite/build.mill | 20 +++ .../src/qux/QuxIntegrationTests.groovy | 18 +++ .../qux/src/qux/Qux.groovy | 11 ++ .../qux/test/src/qux/QuxTests.groovy | 18 +++ example/groovylib/testing/3-spock/build.mill | 15 +++ .../testing/3-spock/src/foo/Calculator.groovy | 8 ++ .../3-spock/test/foo/CalculatorSpec.groovy | 17 +++ .../testing/4-spock-for-java/build.mill | 23 ++++ .../src/main/java/foo/Calculator.java | 8 ++ .../src/test/groovy/foo/CalculatorSpec.groovy | 12 ++ 23 files changed, 529 insertions(+) create mode 100644 example/groovylib/basic/1-simple/build.mill create mode 100644 example/groovylib/basic/1-simple/foo/src/foo/Foo.groovy create mode 100644 example/groovylib/basic/1-simple/foo/test/src/foo/FooTest.groovy create mode 100644 example/groovylib/basic/2-compat-modules/build.mill create mode 100644 example/groovylib/basic/2-compat-modules/foo/src/integration/groovy/foo/FooIntegrationTests.groovy create mode 100644 example/groovylib/basic/2-compat-modules/foo/src/main/groovy/foo/Foo.groovy create mode 100644 example/groovylib/basic/2-compat-modules/foo/src/main/java/foo/Foo2.java create mode 100644 example/groovylib/basic/2-compat-modules/foo/src/test/groovy/foo/FooTests.groovy create mode 100644 example/groovylib/testing/1-test-suite/bar/src/bar/Bar.groovy create mode 100644 example/groovylib/testing/1-test-suite/bar/test/src/bar/BarTests.groovy create mode 100644 example/groovylib/testing/1-test-suite/build.mill create mode 100644 example/groovylib/testing/1-test-suite/foo/src/foo/Foo.groovy create mode 100644 example/groovylib/testing/1-test-suite/foo/test/src/foo/FooTests.groovy create mode 100644 example/groovylib/testing/2-integration-suite/build.mill create mode 100644 example/groovylib/testing/2-integration-suite/qux/integration/src/qux/QuxIntegrationTests.groovy create mode 100644 example/groovylib/testing/2-integration-suite/qux/src/qux/Qux.groovy create mode 100644 example/groovylib/testing/2-integration-suite/qux/test/src/qux/QuxTests.groovy create mode 100644 example/groovylib/testing/3-spock/build.mill create mode 100644 example/groovylib/testing/3-spock/src/foo/Calculator.groovy create mode 100644 example/groovylib/testing/3-spock/test/foo/CalculatorSpec.groovy create mode 100644 example/groovylib/testing/4-spock-for-java/build.mill create mode 100644 example/groovylib/testing/4-spock-for-java/src/main/java/foo/Calculator.java create mode 100644 example/groovylib/testing/4-spock-for-java/src/test/groovy/foo/CalculatorSpec.groovy diff --git a/example/groovylib/basic/1-simple/build.mill b/example/groovylib/basic/1-simple/build.mill new file mode 100644 index 000000000000..fc9fc75f966b --- /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..1c1c0712a8c7 --- /dev/null +++ b/example/groovylib/basic/2-compat-modules/build.mill @@ -0,0 +1,59 @@ +//// 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..6bcfb5588f54 --- /dev/null +++ b/example/groovylib/testing/1-test-suite/build.mill @@ -0,0 +1,32 @@ +//// 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..b8eae997aab1 --- /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 \ No newline at end of file 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..ba8a4b5f2af0 --- /dev/null +++ b/example/groovylib/testing/3-spock/build.mill @@ -0,0 +1,15 @@ +//// 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..38a628b4d648 --- /dev/null +++ b/example/groovylib/testing/4-spock-for-java/build.mill @@ -0,0 +1,23 @@ +//// 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 From ea34a49d2dd52c9aa901eed8e101d9ad0c2b46cf Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Wed, 24 Sep 2025 11:42:01 +0200 Subject: [PATCH 08/23] docs --- .../modules/ROOT/pages/groovylib/intro.adoc | 19 +++++++++++++ .../modules/ROOT/pages/groovylib/testing.adoc | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 website/docs/modules/ROOT/pages/groovylib/intro.adoc create mode 100644 website/docs/modules/ROOT/pages/groovylib/testing.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..0538fa008d79 --- /dev/null +++ b/website/docs/modules/ROOT/pages/groovylib/intro.adoc @@ -0,0 +1,19 @@ + += Building Groovy with Mill +:page-aliases: Groovy_Intro_to_Mill.adoc +:language: Groovy +:language-small: groovy + +include::partial$Intro_Header.adoc[] + +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..47296507879b --- /dev/null +++ b/website/docs/modules/ROOT/pages/groovylib/testing.adoc @@ -0,0 +1,28 @@ += 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-java-spock.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 From 9a26ea33cee394b453898b0607de0bd68558c02e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:24:25 +0000 Subject: [PATCH 09/23] [autofix.ci] apply automated fixes --- example/groovylib/basic/1-simple/build.mill | 4 ++-- example/groovylib/basic/2-compat-modules/build.mill | 6 ++---- example/groovylib/testing/1-test-suite/build.mill | 1 - .../groovylib/testing/2-integration-suite/build.mill | 2 +- example/groovylib/testing/3-spock/build.mill | 2 -- example/groovylib/testing/4-spock-for-java/build.mill | 1 - .../test/src/mill/groovylib/HelloGroovyTests.scala | 10 +++++----- libs/javalib/src/mill/javalib/TestModule.scala | 2 +- 8 files changed, 11 insertions(+), 17 deletions(-) diff --git a/example/groovylib/basic/1-simple/build.mill b/example/groovylib/basic/1-simple/build.mill index fc9fc75f966b..246277785c36 100644 --- a/example/groovylib/basic/1-simple/build.mill +++ b/example/groovylib/basic/1-simple/build.mill @@ -10,7 +10,7 @@ object foo extends GroovyModule { 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 { @@ -52,7 +52,7 @@ object foo extends GroovyModule { // //// SNIPPET:DEPENDENCIES // -// This example project uses two third-party 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. diff --git a/example/groovylib/basic/2-compat-modules/build.mill b/example/groovylib/basic/2-compat-modules/build.mill index 1c1c0712a8c7..144115e09071 100644 --- a/example/groovylib/basic/2-compat-modules/build.mill +++ b/example/groovylib/basic/2-compat-modules/build.mill @@ -12,10 +12,8 @@ object foo extends GroovyMavenModule { def groovyVersion = "5.0.1" - object test extends GroovyMavenTests with TestModule.Junit5 { - } - object integration extends GroovyMavenTests with TestModule.Junit5 { - } + object test extends GroovyMavenTests with TestModule.Junit5 {} + object integration extends GroovyMavenTests with TestModule.Junit5 {} } // `GroovyMavenModule` is a variant of `GroovyModule` diff --git a/example/groovylib/testing/1-test-suite/build.mill b/example/groovylib/testing/1-test-suite/build.mill index 6bcfb5588f54..652533e72b59 100644 --- a/example/groovylib/testing/1-test-suite/build.mill +++ b/example/groovylib/testing/1-test-suite/build.mill @@ -29,4 +29,3 @@ object bar extends GroovyModule { ) } } - diff --git a/example/groovylib/testing/2-integration-suite/build.mill b/example/groovylib/testing/2-integration-suite/build.mill index b8eae997aab1..04c4f0f0a3da 100644 --- a/example/groovylib/testing/2-integration-suite/build.mill +++ b/example/groovylib/testing/2-integration-suite/build.mill @@ -17,4 +17,4 @@ object qux extends GroovyModule { } } -// The integration suite is just another regular test module within the parent GroovyModule \ No newline at end of file +// The integration suite is just another regular test module within the parent GroovyModule diff --git a/example/groovylib/testing/3-spock/build.mill b/example/groovylib/testing/3-spock/build.mill index ba8a4b5f2af0..331a776db914 100644 --- a/example/groovylib/testing/3-spock/build.mill +++ b/example/groovylib/testing/3-spock/build.mill @@ -11,5 +11,3 @@ object `package` extends GroovyModule { def jupiterVersion = "5.13.4" } } - - diff --git a/example/groovylib/testing/4-spock-for-java/build.mill b/example/groovylib/testing/4-spock-for-java/build.mill index 38a628b4d648..d6be164c3b44 100644 --- a/example/groovylib/testing/4-spock-for-java/build.mill +++ b/example/groovylib/testing/4-spock-for-java/build.mill @@ -20,4 +20,3 @@ object `package` extends JavaMavenModuleWithGroovyTests { // 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/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 8421fdf56cf7..740751799d52 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -17,10 +17,10 @@ object HelloGroovyTests extends TestSuite { object HelloGroovy extends TestRootModule { - trait GroovyVersionCross extends GroovyModule with Cross.Module[String]{ + 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 @@ -44,7 +44,7 @@ object HelloGroovyTests extends TestSuite { */ 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 @@ -102,7 +102,7 @@ object HelloGroovyTests extends TestSuite { override def junitPlatformVersion = "1.13.4" } } - object main extends Cross[Test](groovyVersions) + object main extends Cross[Test](groovyVersions) } val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "hello-groovy" @@ -122,7 +122,7 @@ object HelloGroovyTests extends TestSuite { test("running a Groovy script") { testEval().scoped { eval => main.crossModules.foreach(m => { - val Right(_) = eval.apply(m.script.run()): @unchecked + val Right(_) = eval.apply(m.script.run()): @unchecked }) } } diff --git a/libs/javalib/src/mill/javalib/TestModule.scala b/libs/javalib/src/mill/javalib/TestModule.scala index 02d826dd0ade..f71196898b3b 100644 --- a/libs/javalib/src/mill/javalib/TestModule.scala +++ b/libs/javalib/src/mill/javalib/TestModule.scala @@ -633,7 +633,7 @@ 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) + * 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 { From b8d28f7f24d4426f8ba60bc7d6ad8ad2c14943bd Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Wed, 24 Sep 2025 14:47:00 +0200 Subject: [PATCH 10/23] Groovy Compiler options for bytecode version, preview features and ast transoformations --- .../api/GroovyCompilerConfiguration.scala | 6 +++ .../groovylib/worker/api/GroovyWorker.scala | 13 +++-- .../src/mill/groovylib/GroovyModule.scala | 36 ++++++++++++-- .../src/mill/groovylib/publish/exports.scala | 2 +- .../worker/impl/GroovyWorkerImpl.scala | 48 +++++++++++-------- 5 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala 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..414bfbe6e13d --- /dev/null +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala @@ -0,0 +1,6 @@ +package mill.groovylib.worker.api + +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 index d92e90463e7a..32a7493f44d2 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -1,7 +1,6 @@ package mill.groovylib.worker.api -import mill.api.TaskCtx -import mill.api.Result +import mill.api.{Result, TaskCtx} import mill.javalib.api.CompilationResult /** @@ -20,7 +19,8 @@ trait GroovyWorker { def compileGroovyStubs( sourceFiles: Seq[os.Path], classpath: Seq[os.Path], - outputDir: os.Path + outputDir: os.Path, + config: GroovyCompilerConfiguration )(implicit ctx: TaskCtx ) @@ -30,7 +30,12 @@ trait GroovyWorker { * 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)(implicit + 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/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 284315fac4d2..555747846885 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -9,6 +9,7 @@ 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 @@ -49,6 +50,29 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => allGroovySourceFiles() ++ allJavaSourceFiles() } + /** + * Specifiy the bytecode version for the Groovy compiler. + * {{{ + * def targetBytecode = Some("17") + * }}} + */ + def targetBytecode: T[Option[String]] = None + + /** + * Specify if the Groovy compiler should enable preview features. + */ + def enablePreview: 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]]. @@ -92,7 +116,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => } /** - * The Ivy/Coursier dependencies resembling the Groovy compiler. + * The Coursier dependencies resembling the Groovy compiler. * * Default is derived from [[groovyVersion]]. */ @@ -128,6 +152,12 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val compileCp = compileClasspath().map(_.path).filter(os.exists) val updateCompileOutput = upstreamCompileOutput() + val config = GroovyCompilerConfiguration( + enablePreview = enablePreview(), + targetBytecode = targetBytecode(), + disabledGlobalAstTransformations = disabledGlobalAstTransformations(), + ) + def compileJava: Result[CompilationResult] = { ctx.log.info( s"Compiling ${javaSourceFiles.size} Java sources to $classes ..." @@ -150,7 +180,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val workerStubResult = GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { - _.compileGroovyStubs(groovySourceFiles, compileCp, classes) + _.compileGroovyStubs(groovySourceFiles, compileCp, classes, config) } workerStubResult match { case Result.Success(_) => compileJava @@ -165,7 +195,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val workerGroovyResult = GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { - _.compile(groovySourceFiles, compileCp, classes) + _.compile(groovySourceFiles, compileCp, classes, config) } // TODO figure out if there is a better way to do this diff --git a/libs/groovylib/src/mill/groovylib/publish/exports.scala b/libs/groovylib/src/mill/groovylib/publish/exports.scala index 8224e34c4956..0c01da54c719 100644 --- a/libs/groovylib/src/mill/groovylib/publish/exports.scala +++ b/libs/groovylib/src/mill/groovylib/publish/exports.scala @@ -26,7 +26,7 @@ export mill.javalib.publish.PackagingType export mill.javalib.publish.SonatypeHelpers export mill.javalib.publish.SonatypeHttpApi -export mill.javalib.publish.SonatypePublisher +export mill.javalib.SonatypeCentralPublisher export mill.javalib.publish.VersionControl diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala index 391ac9a702dd..c4f60b525b8d 100644 --- a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -4,7 +4,7 @@ import groovy.lang.GroovyClassLoader import mill.api.Result import mill.api.TaskCtx import mill.javalib.api.CompilationResult -import mill.groovylib.worker.api.GroovyWorker +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 @@ -17,22 +17,26 @@ class GroovyWorkerImpl extends GroovyWorker { override def compileGroovyStubs( sourceFiles: Seq[Path], classpath: Seq[Path], - outputDir: Path + outputDir: Path, + config: GroovyCompilerConfiguration, )(implicit ctx: TaskCtx): Result[CompilationResult] = { - val config = new CompilerConfiguration() - config.setTargetDirectory(outputDir.toIO) - config.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) - config.setJointCompilationOptions(Map( + val compilerConfig = new CompilerConfiguration() + compilerConfig.setTargetDirectory(outputDir.toIO) + compilerConfig.setClasspathList(classpath.map(_.toIO.getAbsolutePath).asJava) + compilerConfig.setJointCompilationOptions(Map( "stubDir" -> outputDir.toIO, "keepStubs" -> false ).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 - // config in the GroovyClassLoader is needed when the CL itself is compiling classes - val gcl = new GroovyClassLoader(parentCl, config) - // config for actual compilation - val stubUnit = JavaStubCompilationUnit(config, gcl) + // 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) @@ -51,26 +55,28 @@ class GroovyWorkerImpl extends GroovyWorker { def compile( sourceFiles: Seq[os.Path], classpath: Seq[os.Path], - outputDir: os.Path + outputDir: os.Path, + config: GroovyCompilerConfiguration, )(implicit ctx: TaskCtx ): Result[CompilationResult] = { val extendedClasspath = classpath :+ outputDir - val config = new CompilerConfiguration() - config.setTargetDirectory(outputDir.toIO) - config.setClasspathList(extendedClasspath.map(_.toIO.getAbsolutePath).asJava) - // TODO -// config.setDisabledGlobalASTTransformations() -// config.setSourceEncoding() + 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 - // config in the GroovyClassLoader is needed when the CL itself is compiling classes - val gcl = new GroovyClassLoader(parentCl, config) - // config for actual compilation - val unit = new CompilationUnit(config, null, gcl) + // 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) From 507b11b5697ed9b5105a4677b5e2e935754e24a2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:56:28 +0000 Subject: [PATCH 11/23] [autofix.ci] apply automated fixes --- .../worker/api/GroovyCompilerConfiguration.scala | 9 +++++---- libs/groovylib/src/mill/groovylib/GroovyModule.scala | 3 +-- .../groovylib/worker/impl/GroovyWorkerImpl.scala | 12 ++++++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala index 414bfbe6e13d..7c4d7dee5bd2 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala @@ -1,6 +1,7 @@ package mill.groovylib.worker.api -case class GroovyCompilerConfiguration ( - enablePreview: Boolean = false, - disabledGlobalAstTransformations: Set[String]= Set.empty, - targetBytecode: Option[String]= None) +case class GroovyCompilerConfiguration( + enablePreview: Boolean = false, + disabledGlobalAstTransformations: Set[String] = Set.empty, + targetBytecode: Option[String] = None +) diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 555747846885..87f957cd5240 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -72,7 +72,6 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => */ def disabledGlobalAstTransformations: T[Set[String]] = Set.empty - /** * All individual Java source files fed into the compiler. * Subset of [[allSourceFiles]]. @@ -155,7 +154,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val config = GroovyCompilerConfiguration( enablePreview = enablePreview(), targetBytecode = targetBytecode(), - disabledGlobalAstTransformations = disabledGlobalAstTransformations(), + disabledGlobalAstTransformations = disabledGlobalAstTransformations() ) def compileJava: Result[CompilationResult] = { diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala index c4f60b525b8d..14cb20330fd3 100644 --- a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -18,7 +18,7 @@ class GroovyWorkerImpl extends GroovyWorker { sourceFiles: Seq[Path], classpath: Seq[Path], outputDir: Path, - config: GroovyCompilerConfiguration, + config: GroovyCompilerConfiguration )(implicit ctx: TaskCtx): Result[CompilationResult] = { val compilerConfig = new CompilerConfiguration() compilerConfig.setTargetDirectory(outputDir.toIO) @@ -27,7 +27,9 @@ class GroovyWorkerImpl extends GroovyWorker { "stubDir" -> outputDir.toIO, "keepStubs" -> false ).asJava) - compilerConfig.setDisabledGlobalASTTransformations(config.disabledGlobalAstTransformations.asJava) + compilerConfig.setDisabledGlobalASTTransformations( + config.disabledGlobalAstTransformations.asJava + ) compilerConfig.setPreviewFeatures(config.enablePreview) config.targetBytecode.foreach(compilerConfig.setTargetBytecode) @@ -56,7 +58,7 @@ class GroovyWorkerImpl extends GroovyWorker { sourceFiles: Seq[os.Path], classpath: Seq[os.Path], outputDir: os.Path, - config: GroovyCompilerConfiguration, + config: GroovyCompilerConfiguration )(implicit ctx: TaskCtx ): Result[CompilationResult] = { @@ -66,7 +68,9 @@ class GroovyWorkerImpl extends GroovyWorker { val compilerConfig = new CompilerConfiguration() compilerConfig.setTargetDirectory(outputDir.toIO) compilerConfig.setClasspathList(extendedClasspath.map(_.toIO.getAbsolutePath).asJava) - compilerConfig.setDisabledGlobalASTTransformations(config.disabledGlobalAstTransformations.asJava) + compilerConfig.setDisabledGlobalASTTransformations( + config.disabledGlobalAstTransformations.asJava + ) compilerConfig.setPreviewFeatures(config.enablePreview) config.targetBytecode.foreach(compilerConfig.setTargetBytecode) From e218ec2710e74bc1696f5e92d5e1c48b786058b7 Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Fri, 26 Sep 2025 11:59:19 +0200 Subject: [PATCH 12/23] Added example tests to CI setup --- .github/workflows/run-tests.yml | 4 ++++ example/package.mill | 5 +++++ 2 files changed, 9 insertions(+) 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/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) From 485a8c37465b01c4fb006831ad8e4f3c67409419 Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Fri, 26 Sep 2025 14:29:47 +0200 Subject: [PATCH 13/23] Rudimentarily fix webpage generation --- website/docs/modules/ROOT/nav.adoc | 2 ++ .../modules/ROOT/pages/groovylib/intro.adoc | 25 +++++++++++++++++-- .../modules/ROOT/pages/groovylib/testing.adoc | 3 +-- 3 files changed, 26 insertions(+), 4 deletions(-) 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 index 0538fa008d79..cab3270bd64f 100644 --- a/website/docs/modules/ROOT/pages/groovylib/intro.adoc +++ b/website/docs/modules/ROOT/pages/groovylib/intro.adoc @@ -1,10 +1,31 @@ - = Building Groovy with Mill :page-aliases: Groovy_Intro_to_Mill.adoc :language: Groovy :language-small: groovy -include::partial$Intro_Header.adoc[] +// 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. diff --git a/website/docs/modules/ROOT/pages/groovylib/testing.adoc b/website/docs/modules/ROOT/pages/groovylib/testing.adoc index 47296507879b..c8577c468e96 100644 --- a/website/docs/modules/ROOT/pages/groovylib/testing.adoc +++ b/website/docs/modules/ROOT/pages/groovylib/testing.adoc @@ -2,7 +2,6 @@ :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 @@ -19,7 +18,7 @@ include::partial$example/groovylib/testing/3-spock.adoc[] == Using Spock in Java projects with Maven layout -include::partial$example/groovylib/testing/4-java-spock.adoc[] +include::partial$example/groovylib/testing/4-spock-for-java.adoc[] == Github Actions Test Reports From f0cc0b8645dc8c79099c37f317d918a92442dd3f Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Fri, 26 Sep 2025 15:43:12 +0200 Subject: [PATCH 14/23] Cleanup and Testcase for targetBytecode and previewEnabled --- libs/groovylib/package.mill | 4 ++ .../src/mill/groovylib/GroovyModule.scala | 26 ++++++---- .../src/HelloCompilerOptions.groovy | 12 +++++ .../src/mill/groovylib/HelloGroovyTests.scala | 52 +++++++++++++++++++ 4 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 libs/groovylib/test/resources/hello-groovy/main/compileroptions/src/HelloCompilerOptions.groovy diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index e9720db4ff65..dad2483fef1a 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -11,6 +11,10 @@ object `package` extends MillPublishScalaModule with BuildInfo { 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( diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 87f957cd5240..f0941da4c306 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -174,24 +174,17 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => ) } - if (isMixed) { + def compileGroovyStubs(): Result[CompilationResult] = { ctx.log.info("Compiling Groovy stubs for mixed compilation") - - val workerStubResult = - GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { - _.compileGroovyStubs(groovySourceFiles, compileCp, classes, config) - } - workerStubResult match { - case Result.Success(_) => compileJava - case Result.Failure(reason) => Result.Failure(reason) + GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { + _.compileGroovyStubs(groovySourceFiles, compileCp, classes, config) } } - if (isMixed || isGroovy) { + 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) @@ -206,6 +199,17 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => CompilationResult(analysisFile, PathRef(classes)) case Result.Failure(reason) => Result.Failure(reason) } + } + + val firstAndSecondStage = if (isMixed) { + // only compile Java if Stubs are successfully generated + compileGroovyStubs().flatMap(_ => compileJava) + }else{ + Result.Success + } + + if (isMixed || isGroovy) { + firstAndSecondStage.flatMap(_ => compileGroovy()) } else { compileJava } 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/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 740751799d52..06033420e132 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -7,6 +7,8 @@ import mill.api.Discover import mill.testkit.{TestRootModule, UnitTester} import utest.* +import java.io.FileInputStream + object HelloGroovyTests extends TestSuite { val groovy4Version = "4.0.28" @@ -101,6 +103,13 @@ object HelloGroovyTests extends TestSuite { override def jupiterVersion: T[String] = junit5Version override def junitPlatformVersion = "1.13.4" } + + object compileroptions extends GroovyModule { + override def groovyVersion: T[String] = crossValue + override def targetBytecode: Task.Simple[Option[String]] = Some("11") + override def enablePreview: Task.Simple[Boolean] = true + override def mainClass = Some("compileroptions.HelloCompilerOptions") + } } object main extends Cross[Test](groovyVersions) } @@ -195,6 +204,49 @@ object HelloGroovyTests extends TestSuite { } } + 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 + + // 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.major == 55) + assert(bytecodeVersion.is11PreviewEnabled) + + val Right(_) = eval.apply(m.compileroptions.run()): @unchecked + }) + } + } + test("compile & test module (only test uses Groovy)") { testEval().scoped { eval => From 48b6020702ac578ce0c652d2970d1a13a09e1247 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:52:15 +0000 Subject: [PATCH 15/23] [autofix.ci] apply automated fixes --- libs/groovylib/package.mill | 2 +- .../groovylib/src/mill/groovylib/GroovyModule.scala | 2 +- .../test/src/mill/groovylib/HelloGroovyTests.scala | 13 +++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index dad2483fef1a..e79c1a7737a4 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -14,7 +14,7 @@ object `package` extends MillPublishScalaModule with BuildInfo { override def testMvnDeps: T[Seq[Dep]] = super.testMvnDeps() ++ Seq( Deps.asmTree ) - + def buildInfoPackageName = "mill.groovylib" def buildInfoObjectName = "Versions" def buildInfoMembers = Seq( diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index f0941da4c306..784e5673dd09 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -204,7 +204,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val firstAndSecondStage = if (isMixed) { // only compile Java if Stubs are successfully generated compileGroovyStubs().flatMap(_ => compileJava) - }else{ + } else { Result.Success } diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 06033420e132..dcc3d4c0cc26 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -103,7 +103,7 @@ object HelloGroovyTests extends TestSuite { override def jupiterVersion: T[String] = junit5Version override def junitPlatformVersion = "1.13.4" } - + object compileroptions extends GroovyModule { override def groovyVersion: T[String] = crossValue override def targetBytecode: Task.Simple[Option[String]] = Some("11") @@ -221,8 +221,8 @@ object HelloGroovyTests extends TestSuite { val buffer = classReader.b // 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) + val minor = ((buffer(4) & 0xff) << 8) | (buffer(5) & 0xff) + val major = ((buffer(6) & 0xff) << 8) | (buffer(7) & 0xff) BytecodeVersion(major, minor) } @@ -231,8 +231,9 @@ object HelloGroovyTests extends TestSuite { 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") - + val compiledClassFile = + os.walk(result.value.classes.path).find(_.last == "HelloCompilerOptions.class") + assert( compiledClassFile.isDefined ) @@ -241,7 +242,7 @@ object HelloGroovyTests extends TestSuite { assert(bytecodeVersion.major == 55) assert(bytecodeVersion.is11PreviewEnabled) - + val Right(_) = eval.apply(m.compileroptions.run()): @unchecked }) } From 21cf8f746cc92ad83f752e0d95a0a30ec008517d Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Fri, 26 Sep 2025 16:07:50 +0200 Subject: [PATCH 16/23] minor test improvement --- .../test/src/mill/groovylib/HelloGroovyTests.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index dcc3d4c0cc26..504aae783aad 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -105,8 +105,9 @@ object HelloGroovyTests extends TestSuite { } object compileroptions extends GroovyModule { + def javaVersion = "11" override def groovyVersion: T[String] = crossValue - override def targetBytecode: Task.Simple[Option[String]] = Some("11") + override def targetBytecode: Task.Simple[Option[String]] = Some(javaVersion) override def enablePreview: Task.Simple[Boolean] = true override def mainClass = Some("compileroptions.HelloCompilerOptions") } @@ -220,6 +221,7 @@ object HelloGroovyTests extends TestSuite { 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) @@ -240,7 +242,7 @@ object HelloGroovyTests extends TestSuite { val bytecodeVersion = getBytecodeVersion(compiledClassFile.get) - assert(bytecodeVersion.major == 55) + assert(bytecodeVersion.javaVersion == m.compileroptions.javaVersion) assert(bytecodeVersion.is11PreviewEnabled) val Right(_) = eval.apply(m.compileroptions.run()): @unchecked From 0df8825b943e657d20c3f325b767274c0efa472e Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Sat, 27 Sep 2025 10:03:32 +0200 Subject: [PATCH 17/23] Mark API as experimental --- .../worker/api/GroovyCompilerConfiguration.scala | 1 + .../src/mill/groovylib/worker/api/GroovyWorker.scala | 1 + libs/groovylib/package.mill | 12 +++--------- .../src/mill/groovylib/GroovyMavenModule.scala | 1 + libs/groovylib/src/mill/groovylib/GroovyModule.scala | 1 + .../src/mill/groovylib/GroovyWorkerManager.scala | 1 + .../groovylib/JavaMavenModuleWithGroovyTests.scala | 1 + 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala index 7c4d7dee5bd2..0206c3d2d4fe 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyCompilerConfiguration.scala @@ -1,5 +1,6 @@ package mill.groovylib.worker.api +@mill.api.experimental case class GroovyCompilerConfiguration( enablePreview: Boolean = false, disabledGlobalAstTransformations: Set[String] = Set.empty, diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala index 32a7493f44d2..8a2f40849ffd 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -11,6 +11,7 @@ import mill.javalib.api.CompilationResult * 2. compile Java sources (done externally) * 3. compile Groovy sources */ +@mill.api.experimental trait GroovyWorker { /** diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index e79c1a7737a4..028e1a97dba9 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -5,7 +5,7 @@ import mill.contrib.buildinfo.BuildInfo import mill.scalalib.* import millbuild.* -object `package` extends MillPublishScalaModule with BuildInfo { +object `package` extends MillStableScalaModule with BuildInfo { def moduleDeps = Seq(build.libs.util, build.libs.javalib, build.libs.javalib.testrunner, api) def localTestExtraModules: Seq[MillJavaModule] = @@ -21,13 +21,7 @@ object `package` extends MillPublishScalaModule with BuildInfo { BuildInfo.Value("groovyVersion", Deps.groovyVersion, "Version of Groovy") ) - trait MillGroovyModule extends MillPublishScalaModule { - override def javacOptions = super.javacOptions() ++ { - Seq("-release", "8", "-encoding", "UTF-8", "-deprecation") - } - } - - object api extends MillGroovyModule { + object api extends MillStableScalaModule { def moduleDeps = Seq(build.libs.javalib.testrunner) override def compileMvnDeps: T[Seq[Dep]] = Seq( @@ -35,7 +29,7 @@ object `package` extends MillPublishScalaModule with BuildInfo { ) } - object worker extends MillGroovyModule { + object worker extends MillPublishScalaModule { override def compileModuleDeps = Seq(api) def mandatoryMvnDeps = Seq.empty[Dep] diff --git a/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala b/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala index bfcabb3bee9e..46c7ec366602 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyMavenModule.scala @@ -7,6 +7,7 @@ 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() diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 784e5673dd09..681f26f70a3d 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -18,6 +18,7 @@ import mill.util.Version * * Resolves */ +@mill.api.experimental trait GroovyModule extends JavaModule with GroovyModuleApi { outer => /** diff --git a/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala index 292d5290dd4b..41a85025df9c 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyWorkerManager.scala @@ -5,6 +5,7 @@ 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) { diff --git a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala index c7018dcca885..6b7f48e81edd 100644 --- a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala +++ b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala @@ -6,6 +6,7 @@ import mill.javalib.{JavaModule, MavenModule} /** * Convenience trait for projects using Java for production and Groovy for tests in a Maven setup */ +@mill.api.experimental trait JavaMavenModuleWithGroovyTests extends JavaModule with MavenModule { trait GroovyMavenTests extends JavaTests with MavenTests with GroovyModule { From 2c80e4b4a03e455a6062583ce63c5a3e2595e982 Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Sat, 27 Sep 2025 10:41:08 +0200 Subject: [PATCH 18/23] Fix mima setup --- libs/groovylib/package.mill | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index 028e1a97dba9..711fdcd29530 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -21,7 +21,12 @@ object `package` extends MillStableScalaModule with BuildInfo { BuildInfo.Value("groovyVersion", Deps.groovyVersion, "Version of Groovy") ) - object api extends MillStableScalaModule { + // this module wasn't released in these versions + def mimaPreviousVersions: T[Seq[String]] = super.mimaPreviousVersions().filterNot( + Seq("1.0.0", "1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.0.5").contains + ) + + object api extends MillPublishScalaModule { def moduleDeps = Seq(build.libs.javalib.testrunner) override def compileMvnDeps: T[Seq[Dep]] = Seq( From d2326842c548d80924a96a9c0ec3397034d9a857 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Mon, 29 Sep 2025 09:33:59 +0200 Subject: [PATCH 19/23] minor review remarks --- .../src/mill/groovylib/GroovyModule.scala | 14 ++++++------- .../JavaMavenModuleWithGroovyTests.scala | 20 ++++++++++++++++++- .../src/mill/groovylib/HelloGroovyTests.scala | 4 ++-- .../javalib/src/mill/javalib/TestModule.scala | 6 +++--- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 681f26f70a3d..8bcb27074913 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -31,7 +31,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => */ def groovyLanguageVersion: T[String] = Task { groovyVersion().split("[.]").take(2).mkString(".") } - private def isGroovyBomAvailable: T[Boolean] = Task { + private def useGroovyBom: T[Boolean] = Task { if (groovyVersion().isBlank) { false } else { @@ -41,7 +41,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => override def bomMvnDeps: T[Seq[Dep]] = super.bomMvnDeps() ++ Seq(groovyVersion()) - .filter(_.nonEmpty && isGroovyBomAvailable()) + .filter(_.nonEmpty && useGroovyBom()) .map(v => mvn"org.apache.groovy:groovy-bom:$v") /** @@ -54,15 +54,15 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => /** * Specifiy the bytecode version for the Groovy compiler. * {{{ - * def targetBytecode = Some("17") + * def groovyCompileTargetBytecode = Some("17") * }}} */ - def targetBytecode: T[Option[String]] = None + def groovyCompileTargetBytecode: T[Option[String]] = None /** * Specify if the Groovy compiler should enable preview features. */ - def enablePreview: T[Boolean] = false + def groovyCompileEnablePreview: T[Boolean] = false /** * Specify which global AST transformations should be disabled. Be aware that transformations @@ -153,8 +153,8 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val updateCompileOutput = upstreamCompileOutput() val config = GroovyCompilerConfiguration( - enablePreview = enablePreview(), - targetBytecode = targetBytecode(), + enablePreview = groovyCompileEnablePreview(), + targetBytecode = groovyCompileTargetBytecode(), disabledGlobalAstTransformations = disabledGlobalAstTransformations() ) diff --git a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala index 6b7f48e81edd..958483a1936a 100644 --- a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala +++ b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala @@ -4,7 +4,25 @@ import mill.* import mill.javalib.{JavaModule, MavenModule} /** - * Convenience trait for projects using Java for production and Groovy for tests in a Maven setup + * 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 { diff --git a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala index 504aae783aad..6a07da866c30 100644 --- a/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala +++ b/libs/groovylib/test/src/mill/groovylib/HelloGroovyTests.scala @@ -107,8 +107,8 @@ object HelloGroovyTests extends TestSuite { object compileroptions extends GroovyModule { def javaVersion = "11" override def groovyVersion: T[String] = crossValue - override def targetBytecode: Task.Simple[Option[String]] = Some(javaVersion) - override def enablePreview: Task.Simple[Boolean] = true + override def groovyCompileTargetBytecode: Task.Simple[Option[String]] = Some(javaVersion) + override def groovyCompileEnablePreview: Task.Simple[Boolean] = true override def mainClass = Some("compileroptions.HelloCompilerOptions") } } diff --git a/libs/javalib/src/mill/javalib/TestModule.scala b/libs/javalib/src/mill/javalib/TestModule.scala index f71196898b3b..8bcba0688842 100644 --- a/libs/javalib/src/mill/javalib/TestModule.scala +++ b/libs/javalib/src/mill/javalib/TestModule.scala @@ -372,7 +372,7 @@ object TestModule { /** The JUnit Jupiter version to use, or empty, if you want to provide the dependencies yourself. */ def jupiterVersion: T[String] = Task { "" } - private def isJupiterBomAvailable: T[Boolean] = Task { + private def useJupiterBom: T[Boolean] = Task { if (jupiterVersion().isBlank) { false } else { @@ -385,7 +385,7 @@ object TestModule { override def bomMvnDeps: T[Seq[Dep]] = Task { super.bomMvnDeps() ++ Seq(jupiterVersion()) - .filter(!_.isBlank() && isJupiterBomAvailable()) + .filter(!_.isBlank() && useJupiterBom()) .flatMap(v => Seq( mvn"org.junit:junit-bom:${v.trim()}" @@ -399,7 +399,7 @@ object TestModule { Seq(junitPlatformVersion()).flatMap(v => { if (!v.isBlank) { Some(mvn"org.junit.platform:junit-platform-launcher:${v.trim()}") - } else if (isJupiterBomAvailable()) { + } else if (useJupiterBom()) { Some(mvn"org.junit.platform:junit-platform-launcher") } else { None From 7fac9ce2f9abfd63c9b4aaa37272476a92650b9e Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Mon, 29 Sep 2025 14:41:35 +0200 Subject: [PATCH 20/23] use Mill caching for stub folder --- .../groovylib/worker/api/GroovyWorker.scala | 4 +- .../src/mill/groovylib/GroovyModule.scala | 69 +++++++++++++------ .../worker/impl/GroovyWorkerImpl.scala | 19 ++--- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala index 8a2f40849ffd..e4acb8198594 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -1,6 +1,6 @@ package mill.groovylib.worker.api -import mill.api.{Result, TaskCtx} +import mill.api.{PathRef, Result, TaskCtx} import mill.javalib.api.CompilationResult /** @@ -25,7 +25,7 @@ trait GroovyWorker { )(implicit ctx: TaskCtx ) - : Result[CompilationResult] + : Result[Unit] /** * Compiles the Groovy sources. In a mixed setup this method assumes that the Java stubs diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index 8bcb27074913..cd747f615adc 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -132,6 +132,29 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => 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]]). */ @@ -147,11 +170,21 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => val isGroovy = groovySourceFiles.nonEmpty val isJava = javaSourceFiles.nonEmpty - val isMixed = isGroovy && isJava - 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(), @@ -167,7 +200,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => worker = jvmWorkerRef().internalWorker(), upstreamCompileOutput = updateCompileOutput, javaSourceFiles = javaSourceFiles, - compileCp = compileCp, + compileCp = compileCp :+ compileGeneratedGroovyStubs(), javaHome = javaHome().map(_.path), javacOptions = javacOptions(), compileProblemReporter = ctx.reporter(hashCode), @@ -175,13 +208,6 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => ) } - def compileGroovyStubs(): Result[CompilationResult] = { - ctx.log.info("Compiling Groovy stubs for mixed compilation") - GroovyWorkerManager.groovyWorker().withValue(groovyCompilerClasspath()) { - _.compileGroovyStubs(groovySourceFiles, compileCp, classes, config) - } - } - def compileGroovy(): Result[CompilationResult] = { ctx.log.info( s"Compiling ${groovySourceFiles.size} Groovy sources to $classes ..." @@ -192,7 +218,7 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => } // TODO figure out if there is a better way to do this - val analysisFile = dest / "groovy.analysis.dummy" // needed for mills CompilationResult + val analysisFile = dest / "groovy.analysis.dummy" // needed for Mills CompilationResult os.write(target = analysisFile, data = "", createFolders = true) workerGroovyResult match { @@ -202,18 +228,19 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => } } - val firstAndSecondStage = if (isMixed) { - // only compile Java if Stubs are successfully generated - compileGroovyStubs().flatMap(_ => compileJava) - } else { - Result.Success - } + strategy match { + case JavaOnly => + compileJava - if (isMixed || isGroovy) { - firstAndSecondStage.flatMap(_ => compileGroovy()) - } else { - compileJava + case GroovyOnly => + compileGroovy() + + case Mixed => + compileGroovyStubs() + .flatMap(_ => compileJava) + .flatMap(_ => compileGroovy()) } + } private[groovylib] def internalCompileJavaFiles( diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala index 14cb20330fd3..28dbc2ecc888 100644 --- a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -1,8 +1,7 @@ package mill.groovylib.worker.impl import groovy.lang.GroovyClassLoader -import mill.api.Result -import mill.api.TaskCtx +import mill.api.{PathRef, Result, TaskCtx} import mill.javalib.api.CompilationResult import mill.groovylib.worker.api.{GroovyCompilerConfiguration, GroovyWorker} import org.codehaus.groovy.control.{CompilationUnit, CompilerConfiguration, Phases} @@ -19,13 +18,13 @@ class GroovyWorkerImpl extends GroovyWorker { classpath: Seq[Path], outputDir: Path, config: GroovyCompilerConfiguration - )(implicit ctx: TaskCtx): Result[CompilationResult] = { + )(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" -> false + "keepStubs" -> true ).asJava) compilerConfig.setDisabledGlobalASTTransformations( config.disabledGlobalAstTransformations.asJava @@ -46,10 +45,9 @@ class GroovyWorkerImpl extends GroovyWorker { Try { stubUnit.compile(Phases.CONVERSION) - CompilationResult(outputDir, mill.api.PathRef(outputDir)) }.fold( exception => Result.Failure(s"Groovy stub generation failed: ${exception.getMessage}"), - result => Result.Success(result) + result => Result.Success(()) ) } @@ -88,19 +86,10 @@ class GroovyWorkerImpl extends GroovyWorker { Try { unit.compile(Phases.OUTPUT) - removeAllJavaFiles(outputDir) CompilationResult(outputDir, mill.api.PathRef(outputDir)) }.fold( exception => Result.Failure(s"Groovy compilation failed: ${exception.getMessage}"), result => Result.Success(result) ) } - - private def removeAllJavaFiles(outputDir: os.Path): Unit = { - if (os.exists(outputDir)) { - os.walk(outputDir) - .filter(_.ext == "java") - .foreach(os.remove) - } - } } From b5177adaee368ee9c0ad8038e5650997eb924b9e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:53:06 +0000 Subject: [PATCH 21/23] [autofix.ci] apply automated fixes --- .../api/src/mill/groovylib/worker/api/GroovyWorker.scala | 4 ++-- libs/groovylib/src/mill/groovylib/GroovyModule.scala | 6 ++++-- .../mill/groovylib/JavaMavenModuleWithGroovyTests.scala | 8 ++++---- .../src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala index e4acb8198594..ef1e8916e510 100644 --- a/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala +++ b/libs/groovylib/api/src/mill/groovylib/worker/api/GroovyWorker.scala @@ -1,6 +1,6 @@ package mill.groovylib.worker.api -import mill.api.{PathRef, Result, TaskCtx} +import mill.api.{Result, TaskCtx} import mill.javalib.api.CompilationResult /** @@ -25,7 +25,7 @@ trait GroovyWorker { )(implicit ctx: TaskCtx ) - : Result[Unit] + : Result[Unit] /** * Compiles the Groovy sources. In a mixed setup this method assumes that the Java stubs diff --git a/libs/groovylib/src/mill/groovylib/GroovyModule.scala b/libs/groovylib/src/mill/groovylib/GroovyModule.scala index cd747f615adc..25c5af0817ba 100644 --- a/libs/groovylib/src/mill/groovylib/GroovyModule.scala +++ b/libs/groovylib/src/mill/groovylib/GroovyModule.scala @@ -132,10 +132,12 @@ trait GroovyModule extends JavaModule with GroovyModuleApi { outer => groovyCompileTask()() } - def compileGroovyStubs: T[Result[Unit]] = Task(persistent = true){ + 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 ...") + 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( diff --git a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala index 958483a1936a..6f2c4ce7f391 100644 --- a/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala +++ b/libs/groovylib/src/mill/groovylib/JavaMavenModuleWithGroovyTests.scala @@ -5,13 +5,13 @@ 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 + * 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 { * @@ -21,7 +21,7 @@ import mill.javalib.{JavaModule, MavenModule} * } * } * }}} - * + * * Note: for non-Maven layouts this is not necessary, since the test module can just be a [[GroovyModule]]. */ @mill.api.experimental diff --git a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala index 28dbc2ecc888..85c48f3f05cc 100644 --- a/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala +++ b/libs/groovylib/worker/src/mill/groovylib/worker/impl/GroovyWorkerImpl.scala @@ -1,7 +1,7 @@ package mill.groovylib.worker.impl import groovy.lang.GroovyClassLoader -import mill.api.{PathRef, Result, TaskCtx} +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} From eed6b588ad271204a117f06f681a1465af5156f5 Mon Sep 17 00:00:00 2001 From: Marc Schlegel Date: Mon, 13 Oct 2025 07:34:53 +0200 Subject: [PATCH 22/23] Revert back to experimental module --- libs/groovylib/package.mill | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index 711fdcd29530..6b52ab41e193 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -5,7 +5,7 @@ import mill.contrib.buildinfo.BuildInfo import mill.scalalib.* import millbuild.* -object `package` extends MillStableScalaModule with BuildInfo { +object `package` extends MillPublishScalaModule with BuildInfo { def moduleDeps = Seq(build.libs.util, build.libs.javalib, build.libs.javalib.testrunner, api) def localTestExtraModules: Seq[MillJavaModule] = @@ -21,10 +21,6 @@ object `package` extends MillStableScalaModule with BuildInfo { BuildInfo.Value("groovyVersion", Deps.groovyVersion, "Version of Groovy") ) - // this module wasn't released in these versions - def mimaPreviousVersions: T[Seq[String]] = super.mimaPreviousVersions().filterNot( - Seq("1.0.0", "1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.0.5").contains - ) object api extends MillPublishScalaModule { def moduleDeps = Seq(build.libs.javalib.testrunner) From 00b68933753d35592954137e687f0a41c053759c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 05:44:04 +0000 Subject: [PATCH 23/23] [autofix.ci] apply automated fixes --- libs/groovylib/package.mill | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/groovylib/package.mill b/libs/groovylib/package.mill index 6b52ab41e193..281ab876a443 100644 --- a/libs/groovylib/package.mill +++ b/libs/groovylib/package.mill @@ -21,7 +21,6 @@ object `package` extends MillPublishScalaModule with BuildInfo { BuildInfo.Value("groovyVersion", Deps.groovyVersion, "Version of Groovy") ) - object api extends MillPublishScalaModule { def moduleDeps = Seq(build.libs.javalib.testrunner)