Other JVM languages
Jenesis is not a Java-only build tool. Drop ., . or . sources into a module and it resolves
the matching compiler and builds them, mixed freely with Java, into one modular jar and its generated POM, with
no build script to write. You do not turn a language on. Jenesis detects it from the file extensions
present and wires the right compiler into the same step graph Core concepts
described.
This chapter covers what changes when a module holds more than Java: how two compilers share one module, the one rule that decides which packages you can export, the standard-library dependency each language needs, and how to run a compiler plugin.
One module, two compilers
A mixed module is compiled by two compilers in sequence. Jenesis runs the other-language compiler
and javac in a fixed order over the same sources, each producing its own classes into the module. Because both
write into one module, the exported jar looks exactly like a single-language one - a consumer never sees the
seam.
The order is not arbitrary, and it differs by language:
| Language | Order | Why |
|---|---|---|
| Kotlin | kotlinc, then javac |
kotlinc reads . as source for symbol resolution but emits only Kotlin classes |
| Scala | scalac, then javac |
scalac reads . as source for symbol resolution but emits only Scala classes |
| Groovy | javac, then groovyc |
groovyc cannot read . as source; it resolves Java from the compiled class path |
For Kotlin and Scala, the language compiler runs first: it reads your . files only to resolve symbols
(so Sample. can call a Java Greeter) but leaves the Java output to javac, which compiles the .
sources and module-info. afterwards. For Groovy it is the mirror image: javac runs first and owns all
Java output, then groovyc compiles the . sources, reaching Java types through the classes javac
already produced.
module-info.java. Neither kotlinc nor scalac
is handed the module declaration; javac always owns it. For scalac that is a
necessity, since its Java parser trips over the dotted module name. Both compilers still receive your other
.java sources for resolution.
Which packages you can export
That order decides one thing you need to know: whether a package holding only non-Java code can be exported.
When javac validates an exports <package> directive, it checks that the package is populated in the module's
output. For Kotlin and Scala this just works: their compiler already ran, so javac sees the Kotlin or Scala
classes through --patch-module and accepts the export. You can export a package that contains a single Kotlin
(or Scala) class and no Java type at all.
For Groovy you cannot. groovyc runs after javac, so at export-check time the package's Groovy classes
do not exist yet and only Java types count. The rule is:
exports directive must contain at least one Java type when
the module also uses Groovy. Add a Java type to a Groovy package you want to export, or keep the package
internal (a requires with no exports). This is a permanent Groovy restriction, not a
Jenesis one - it follows from groovyc resolving Java only from the compiled class path.
The standard-library dependency
Each language needs its standard library on the module path. You declare it as a normal requires in
module-info., and Jenesis resolves it through Maven like any other dependency:
| Language | requires |
Resolves to |
|---|---|---|
| Kotlin | requires kotlin. |
org. (plus its transitives) |
| Scala | requires scala. |
org. |
| Groovy | requires org. |
org. |
Groovy's jar carries an Automatic-Module-Name, so org. resolves as an automatic module. The
Scala standard library lives entirely in one module - scala-library - with scala3-library_ being an empty
aggregator, so nothing splits package scala across two modules and the Java Module System accepts it on the
module path with no hand-written wiring.
Pinning the compiler
Every language compiler resolves in its own dependency group - kotlinc, scalac or groovyc - kept
completely apart from your module's own main-group dependencies. That separation matters: the running compiler
is locked independently of the standard library your module ships against, so pinning a different kotlin-stdlib
for your code can never downgrade the kotlinc that compiles it.
The compilers float a latest version by default. Run the pin step to record each resolved compiler jar with its
version and SHA-256, exactly as it pins your Java compilers and dependencies (see Pinning & bills of materials):
java build/jenesis/Make.java pin
-RC or a Groovy -alpha - so an unpinned build can drift
onto one. Pinning keeps the module on a stable compiler while you upgrade deliberately.
A compiler plugin
A Kotlin or Scala compiler plugin is declared exactly like a Java annotation processor
(Building & running),
with one addition: name the compiler first, so the plugin resolves in that compiler's own group rather than
on javac's processor path.
/**
* @jenesis.plugin kotlinc maven/org.jetbrains.kotlin/kotlin-serialization-compiler-plugin
*/
module demo.serialize {
requires kotlinx.serialization.core;
}
Jenesis resolves the plugin under the kotlinc group, and the Kotlin compiler picks up the jar and loads it
itself - you never name an entry point. Above, the kotlinx. plugin generates a @Serializable
class's serializer(), and the requires provides the annotation and the runtime types the generated code
references. The Scala path is identical: @jenesis. resolves in the scalac
group, and scalac loads the plugin the same way.
@jenesis.plugin line and
the plugin is no longer handed to the compiler - the generated code is never produced and the build fails to
compile whatever referenced it. Nothing on the class or module path is scanned for plugins implicitly.
The plugin's version is pinned the usual way, coordinated to the compiler - the pin step writes back its
@jenesis. line for you.
API documentation
When you build documentation jars (jenesis., Building & running), each language uses
its own documentation tool: Dokka for Kotlin, scaladoc for Scala, groovydoc for Groovy, javadoc for
Java. You do not configure this; Jenesis scans the sources and picks tools to cover the languages present.
When one tool can document every language in the module it runs alone and renders a single document:
Java + Kotlin is one Dokka document, Java + Groovy one groovydoc document. Any mix that includes Scala, or
Kotlin together with Groovy, splits the output, with javadoc rendering the Java at the archive root and
each remaining language in its own subfolder. Either way the produced -javadoc. always has a root
index., so it satisfies a repository like Maven Central.