Skip to content

Conversation

lostiniceland
Copy link

@lostiniceland lostiniceland commented Aug 26, 2025

I have been working on Groovy support since I would like to use Mill for projects where tests are written in Spock/Groovy.

It is basically a modified version of the Kotlin support.

Whats currently working:

  • Groovy compilation (tested with 4 & 5)
  • Maven project layout with an additional convenience option to configure Groovy for tests only
  • JUnit5 test execution
  • Spock tests
  • CompileStatic
  • Configure Bytecode Version and Preview

If anyone would like to provide feedback I would appreciate it.

Copy link
Member

@lefou lefou left a comment

Choose a reason for hiding this comment

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

Thanks for opening this PR and welcome to Mill.

I had a quick scan and left some comments where I spotted something. Overall, this looks reasonable, but there might be some more places copied from the Kotlin implementation which aren't adequate for Groovy.

override def prepareOffline(all: Flag): Command[Seq[PathRef]] = Task.Command {
(
super.prepareOffline(all)() ++
groovyCompilerClasspath()
Copy link
Member

Choose a reason for hiding this comment

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

Does this include potential compiler plugin dependencies?

Copy link
Author

Choose a reason for hiding this comment

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

Since there is no real Groovy compiler in the sense of Javac, Scala or Kotlin (there is Groovyc, which is just a CLI wrapper) I guess there is no need for this. The core Groovy dependency is able to compile and you can just extends the normal classpath. Guess this is due to Groovy being a dynamic language where everything is resolved at runtime anyways. The only difference could be when you use @CompileStatic (doc). I will add this to the tasks to be evaluated.

@lihaoyi lihaoyi changed the title Intial Groovy setup Initial Groovy setup Aug 27, 2025
Copy link
Member

@lefou lefou left a comment

Choose a reason for hiding this comment

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

I understand, that you copied another language module to start this implementation, but it would be helpful, if you could revise it and delete all tasks that aren't needed, like groovycOptions.

@lostiniceland lostiniceland force-pushed the groovylib branch 2 times, most recently from 21334e0 to 69cb09a Compare September 15, 2025 14:08
@lostiniceland
Copy link
Author

@lefou I've resolved most of the issues and fixed the Spock bug.

I've also extended the use-cases and backed them by tests to support static compilation as well as a 3-stage compilation for setups where Java depends on Groovy and vice-versa.

In the next weeks I will probably find some more time to see about what configuration options are needed and how to support them.

@lefou
Copy link
Member

lefou commented Sep 16, 2025

@lostiniceland Can you resolve the merge conflicts?

@lostiniceland
Copy link
Author

@lefou I would like to pull bomMvnDeps up to JavaModuleBase in order to setup BOMs within the TestModule.Spock utility (I guess other testframeworks provide BOMs as well).
Would this break compatibility? If not, are there other reasons why it is not there?

@lefou
Copy link
Member

lefou commented Sep 16, 2025

@lefou I would like to pull bomMvnDeps up to JavaModuleBase in order to setup BOMs within the TestModule.Spock utility (I guess other testframeworks provide BOMs as well). Would this break compatibility? If not, are there other reasons why it is not there?

You can try, but I think it will break bin-compat.

@lostiniceland
Copy link
Author

@lefou I would like to pull bomMvnDeps up to JavaModuleBase in order to setup BOMs within the TestModule.Spock utility (I guess other testframeworks provide BOMs as well). Would this break compatibility? If not, are there other reasons why it is not there?

You can try, but I think it will break bin-compat.

I gave it a try and no additional test broke. From SemVer perspective it also shouldn't break anything as long as the trait provides a default implementation. So I will keep this for now.

@lostiniceland
Copy link
Author

lostiniceland commented Sep 16, 2025

@lefou Have a look at the latest commit

I managed to manage (pun intended) the JUnit5/Spock dependencies. Since the BOMs are only available at certain versions this will be checked.
For JUnit starting from 5.12 this has the nice effect that the junit-bom manages the platform-launcher version. So there is actually no need to specify this (but removing the property will break compatibility).
So I've implemented it according to these rules:

  1. platform-launcher version set => add launcher with specific version
  2. no launcher version, but junit-bom available => add launcher without version (because managed)
  3. else => don't add launcher
    This should make the usage easier.

What do you think? Do you want to keep this, or should I remove it?

…pile which also fixed spock not being executed
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.
@lostiniceland lostiniceland force-pushed the groovylib branch 2 times, most recently from 32f8111 to bda67ee Compare September 23, 2025 13:44
@lostiniceland
Copy link
Author

lostiniceland commented Sep 23, 2025

Looks like I was wrong about binary compatibility though I am not quite sure what to make of this:
https://github.com/lostiniceland/mill/actions/runs/17948459255/job/51041447311

Lets say Groovylib is released with Mill 1.0.10 then I cannot use this with Mill 1.0.9. Fine. And plugins targeting 1.0.10 will also still be compatible since the new method has a default implementation.
Currently I don't really trust this test.

@lostiniceland
Copy link
Author

@lefou I think this PR is ready to get out of the draft status.

I've added examples and docs (though ./mill docs.fastPages didn't do anything), added most relevant compiler options, and I've been using it for Spock testing in a side project for some weeks now without problems.

There is this outstanding issue with the binary-compatibility caused by adding bomMvnDeps to the TestModule base (see previous message) which I think should actually work. Maybe @lihaoyi can provide insight. If this really breaks compat, then I will have to remove the feature that a BOM is automatically added for certain Test setups.

@lihaoyi
Copy link
Member

lihaoyi commented Sep 24, 2025

I haven't reviewed this in detail, but let's put this in contrib/ rather than libs/. That will send the right signal that it is new and experimental, and if it becomes popular we can move it into libs/ with the Java/Scala/Kotlin toolchains

@lostiniceland
Copy link
Author

I haven't reviewed this in detail, but let's put this in contrib/ rather than libs/. That will send the right signal that it is new and experimental, and if it becomes popular we can move it into libs/ with the Java/Scala/Kotlin toolchains

Do I have to change the package from mill.groovylib to mill.contrib.groovylib? It would be the correct way, but it just creates more work and I am quite confident that the module is in a good shape. I'd rather spent my time on adding tests for the recently added compiler options (currently only tested manually)

@lefou
Copy link
Member

lefou commented Sep 26, 2025

Since we also have experimental pythonolib and javascriptlib not in contrib, I think it's ok to have a groovylib, as long as it is marked as experimental.

We already have the example tooling in place for <lang>lib modules, which is quite different to contrib modules.

@lihaoyi
Copy link
Member

lihaoyi commented Sep 26, 2025

Since we also have experimental pythonolib and javascriptlib not in contrib, I think it's ok to have a groovylib, as long as it is marked as experimental.

We already have the example tooling in place for <lang>lib modules, which is quite different to contrib modules.

Sounds good to me!

@lefou lefou marked this pull request as ready for review September 26, 2025 10:49
Comment on lines 17 to 27
/**
* 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]
Copy link
Member

Choose a reason for hiding this comment

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

What is the output of this? .class files or .java files?

Copy link
Author

Choose a reason for hiding this comment

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

.java though they are cleaned up after the last groovy compilation stage.

Copy link
Member

Choose a reason for hiding this comment

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

I think using CompilationResult in the return isn't appropriate here.

Copy link
Member

@lefou lefou Sep 27, 2025

Choose a reason for hiding this comment

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

Is this stub-handling somewhere documented? Or can you outline the process and what you mean with "cleaned up after the last compilation stage"?

It looks to me, this groovy stubs should reside in it's own cacheable task, since it should not need to be regenerated when just Java files changed.

Copy link
Author

@lostiniceland lostiniceland Sep 27, 2025

Choose a reason for hiding this comment

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

I will document it.

In order for Java to compile with dependencies to Groovy, It only needs the method signatures (headers). This is what the Groovy Stubs are. When you have dependencies Groovy -> Java AND Java -> Groovy you need a 3 stage compile

  1. Groovy compiles to Java stubs (*.java files in the output)
  2. Java compiles with the Stubs available
    2.1 Cleanup stubs after Java compile (I was wrong before, it happens here)
  3. Compile Groovy (now all Java dependencies are also available on the compilepath).

It should actually be very similar in the other toolchains (scala, kotlin) but maybe the external compiler handles this already internally.

Copy link
Member

@lefou lefou Sep 28, 2025

Choose a reason for hiding this comment

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

There is a difference to Scala or Kotlin handling. Both are able to read Java files, so they will always compile their files first and the Java-compiler comes last. Whereas Groovy will first generate stubs, so Java can consume them and succeed with compilation, and then compile Groovy (while consuming the compiled Java classes vie the classpath).

Nevertheless, the only thing needed is a changed classpaths and output paths. There is no need to delete stub files. Especially, since they are needed in the next compile run and don't need any regeneration at all, when nothing has changed to the Groovy files. And even if the Groovy files have changed but the resulting stubs have not, there is no need no recompile Java files at all. Both cases would be automatically handled by Mill, if we properly model it via cached tasks.

Comment on lines +33 to +39
private def isGroovyBomAvailable: T[Boolean] = Task {
if (groovyVersion().isBlank) {
false
} else {
Version.isAtLeast(groovyVersion(), "4.0.26")(using Version.IgnoreQualifierOrdering)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

The name is misleading. It should be useGroovyBom or groovyUseBom, since it's not checking for availability but deciding (based on the version).

* def targetBytecode = Some("17")
* }}}
*/
def targetBytecode: T[Option[String]] = None
Copy link
Member

Choose a reason for hiding this comment

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

Should be groovyCompileTargetBytecode

Comment on lines +6 to +8
/**
* Convenience trait for projects using Java for production and Groovy for tests in a Maven setup
*/
Copy link
Member

Choose a reason for hiding this comment

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

You should add more motivation, why this might be preferable over just adding GroovyModule (e.g. to avoid implicit groovy deps in the module).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants