Compiling with groovyc & Joint Compilation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every command below was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). groovyc is to Groovy what javac is to Java: it turns source files into .class files ahead of time, so the result can be packaged in a jar, loaded by a Java application or run with plain java. Scripts and experiments are better served by the groovy command, described on Running Scripts & the groovy Command; an application that compiles Groovy at run time is on Integrating Groovy into Java Applications; the build tools that drive groovyc for you are on Build Tools: Gradle, Maven & Ant. The Java side of the build story is on Java Build & Tooling.

Two additions of 6.0 are covered here and named where they appear: --error-format short and --check for tooling that needs one parseable line per diagnostic, and the joint-compilation stub improvements (incubating in 6.0, specified by GEP-21), which make members generated by AST transformations visible to Java code in the same compilation.

The examples compile a small class from the greenhouse scenario:

package greenhouse

class Reading {
    String sensor
    BigDecimal temperature

    String describe(String unit, int decimals) {
        "${sensor}: ${temperature.setScale(decimals)} ${unit}"
    }
}

Compiling a class

groovyc takes one or more source files and writes one .class file per class, in the directory given with -d (the current directory if omitted). Like javac, it mirrors the package structure below that directory. groovyc -v prints the compiler version.

groovyc -v
groovyc -d out src/greenhouse/Reading.groovy
find out -type f
javap -cp out -p greenhouse.Reading
Groovy compiler version 6.0.0
Copyright 2003-2026 The Apache Software Foundation. https://groovy-lang.org/
out/greenhouse/Reading.class
Compiled from "Reading.groovy"
public class greenhouse.Reading implements groovy.lang.GroovyObject {
  private java.lang.String sensor;
  private java.math.BigDecimal temperature;
  private static org.codehaus.groovy.reflection.ClassInfo $staticClassInfo;
  public static transient boolean __$stMC;
  private transient groovy.lang.MetaClass metaClass;
  public greenhouse.Reading();
  public java.lang.String describe(java.lang.String, int);
  protected groovy.lang.MetaClass $getStaticMetaClass();
  public groovy.lang.MetaClass getMetaClass();
  public void setMetaClass(groovy.lang.MetaClass);
  public java.lang.String getSensor();
  public void setSensor(java.lang.String);
  public java.math.BigDecimal getTemperature();
  public void setTemperature(java.math.BigDecimal);
}

The disassembly shows what Groovy adds to a plain class: the field declarations became private fields with generated getters and setters, and the class implements GroovyObject and carries a metaClass field, which is the hook of the runtime metaprogramming described on Runtime Metaprogramming & the MOP. Classes compiled with groovyc still need the Groovy runtime (groovy-6.0.0.jar) on the classpath, and Java code can use them as ordinary classes:

groovyc -d outh src/Hello.groovy        # class Hello with a static main(String[] args)
java -cp outh:groovy-6.0.0.jar Hello bench-1
java -cp outh Hello
Hello from bench-1
Error: Could not find or load main class Hello
Caused by: java.lang.NoClassDefFoundError: groovy/lang/GroovyObject

A source file that contains only statements is a script: groovyc compiles it into a class that extends groovy.lang.Script, named after the file, with a generated main, so java -cp outsc:groovy-6.0.0.jar Scr prints the script’s output (script ok for the one-line script that was compiled here).

Options

groovyc --help prints the options of the installed version. The ones of 6.0.0 are:

Option Meaning

-cp, -classpath, --classpath <path>

Where to find class files. Must be the first argument

-d <dir>

Directory for the generated class files

-sourcepath, --sourcepath <path>

Kept for compatibility; the online documentation says it no longer has any effect

--temp <dir>

Temporary directory for the compiler

--encoding <name>

Encoding of the source files

-w, --warningLevel <n>

Amount of warnings: 0 none, 1 likely errors, 2 possible errors, 3 as many as possible

-pa, --parameters

Store parameter names in the class files so reflection can read them

-pr, --enable-preview

Enable JDK preview features (JDK 12 and newer). Must come after the classpath and before the other arguments

-j, --jointCompilation

Compile .java files together with the Groovy files by calling javac

-s, --stubDirectory <dir>

Directory for the generated Java stubs

-ks, --keepStubs

Keep the stubs instead of deleting them

-J<name>=<value>

Pass a name=value option to javac (joint compilation)

-F<flag>

Pass a flag to javac (joint compilation)

-b, --basescript <class>

Base class for scripts; it must extend groovy.lang.Script

-cf, --configscript <script>

Run a script that adjusts the CompilerConfiguration

-t, --tolerance <n>

Number of non-fatal errors to collect before giving up (default 10, 0 for no limit)

-e, --exception

Print the stack trace of a compilation failure

-de, --debug

Print a little more information when errors occur

--compile-static, --type-checked

Compile every class with @CompileStatic or @TypeChecked

--error-format <full|short>

How diagnostics are rendered (6.0). full is the default

--check

Run all the checks without writing class files (6.0)

@<file>

Argument file: a list of source files

The classpath option has to come first, and javac-style paths are separated with the platform path separator. The online documentation describes @argfile as holding "options and source files". In 6.0.0 only source files worked: the help text itself says "@-files containing a list of source files", and an option on a line of an argument file was reported as a missing file.

printf 'src/greenhouse/Reading.groovy\nsrc/Hello.groovy\n' > files.txt
groovyc --parameters -d outa @files.txt
find outa -type f | sort

printf -- '-d\noutb\nsrc/Hello.groovy\n' > args2.txt
groovyc @args2.txt
outa/Hello.class
outa/greenhouse/Reading.class

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
-d: -d (No such file or directory)

Options therefore go on the command line, and the argument file holds the list of sources, which is its useful role anyway when the list is long.

Parameter names with --parameters

By default Groovy, like Java, does not store the names of method parameters in the class file, so reflection reports arg0, arg1. The --parameters option (-pa) writes the MethodParameters attribute that java.lang.reflect.Parameter reads. Frameworks that bind request or command-line values by parameter name need it, and so do tools that print signatures.

import greenhouse.Reading

println Reading.getMethod('describe', String, int).parameters*.name
groovyc -d out src/greenhouse/Reading.groovy
groovy -cp out src/Main.groovy
groovyc --parameters -d outp src/greenhouse/Reading.groovy
groovy -cp outp src/Main.groovy
[arg0, arg1]
[unit, decimals]

The same switch exists on the groovy command, and Gradle and Maven expose it as a compiler option (see Build Tools: Gradle, Maven & Ant).

Target bytecode

groovyc has no --release or -target flag. The bytecode level is the targetBytecode property of the CompilerConfiguration, which a configuration script sets. A configuration script is a Groovy script whose binding has the variable configuration, the same mechanism used with groovy --configscript and described with compilation customizers on Compilation Customizers & Secure Scripting.

// cfg.groovy
configuration.targetBytecode = '17'
groovyc -d out src/greenhouse/Reading.groovy
javap -v -cp out greenhouse.Reading | grep major
groovyc --configscript cfg.groovy -d out17 src/greenhouse/Reading.groovy
javap -v -cp out17 greenhouse.Reading | grep major
  major version: 70
  major version: 61

Without the setting, the class file followed the JDK that ran the compiler (class file version 70 is JDK 26); with it, version 61 is JDK 17, the minimum JDK of Groovy 6.0. The generated class files keep calling the Groovy runtime, so the runtime jar of the same Groovy line is required either way. The Ant task offers the same control through its targetBytecode attribute (not run here).

Errors and --error-format short

Without options the compiler prints each diagnostic over four lines: the file and line, the message with its column, the source line and a caret. That is comfortable for a person at a terminal. Groovy 6.0 adds --error-format short, which prints one file:line:column: severity: message line per diagnostic: the shape editors, CI log parsers and coding agents already understand. The setting is also available as CompilerConfiguration.errorFormat and, for the Ant task, as the errorFormat attribute of <groovyc>. A source with one unresolved class, compiled both ways:

class Order {
    Custmer owner
    int total() { 'abc'.toInt() }
}
groovyc -d bad Broken.groovy
groovyc --error-format short -d bad Broken.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Broken.groovy: 2: unable to resolve class Custmer
 @ line 2, column 5.
       Custmer owner
       ^

1 error

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Broken.groovy:2:5: error: unable to resolve class Custmer
1 error

The summary and the exit status (1) are the same; only the diagnostic itself changes. The saving grows with the number of errors, because messages that span several lines are joined into one. A class with two static type errors shows it:

@groovy.transform.CompileStatic
class Typed {
    int a() { 'x' }
    void b() { undefinedVar++ ; String s = 5 }
}
Typed.groovy:3:15: error: [Static type checking] - Cannot return value of type java.lang.String for method returning int
Typed.groovy:4:16: error: [Static type checking] - The variable [undefinedVar] is undeclared.
2 errors

The -t option sets how many errors are collected before the compiler stops, 10 by default. With -t 2 and three broken classes, two diagnostics were printed:

Many.groovy:1:11: error: unable to resolve class Foo
Many.groovy:2:11: error: unable to resolve class Bar
2 errors

Syntax errors benefit from the clearer messages of 6.0, which report an unclosed string where it started:

Syn.groovy:1:9: error: Unclosed string literal
1 error

The stack trace stays out of the picture unless asked for: -e prints it for failures that carry one, and -de adds debugging information.

Checking without compiling

--check (6.0) runs every check, including resolution, static type checking, class verification and bytecode generation, but writes no class files. Together with --error-format short it gives editors, pre-commit hooks and CI jobs a fast parseable check. The exit status is 0 for a clean source and 1 when there is a diagnostic, and no output directory is created:

groovyc --check -d chk Ok.groovy; echo "exit=$?"
groovyc --check --error-format short -d chk Typed.groovy; echo "exit=$?"
ls chk
exit=0
Typed.groovy:3:15: error: [Static type checking] - Cannot return value of type java.lang.String for method returning int
Typed.groovy:4:16: error: [Static type checking] - The variable [undefinedVar] is undeclared.
2 errors
exit=1
ls: chk: No such file or directory

Programmatically, the same mode is the targetPhase property of CompilerConfiguration: setting it to an early phase such as Phases.INSTRUCTION_SELECTION reports parse, resolution and static type-checking errors without generating classes.

Static compilation from the command line

--compile-static and --type-checked apply @CompileStatic or @TypeChecked to every class of the compilation, which is a way to hold a whole code base to the rules described on Static Type Checking & @CompileStatic without annotating each class. A method call on Object that dynamic Groovy would resolve at run time is an error when compiled statically:

class Dyn { int total(Object o) { o.size() } }
groovyc --compile-static -d outs Dyn.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Dyn.groovy: 1: [Static type checking] - Cannot find matching method java.lang.Object#size(). Please check if the declared type is correct and if the method exists.
 @ line 1, column 35.
   class Dyn { int total(Object o) { o.size() } }
                                     ^

1 error

Warnings

-w (--warningLevel) chooses how many warnings are printed. Until 6.0 the collected warnings were shown only when the compilation failed, so the option had little visible effect. Groovy 6.0 prints them to standard error after a successful compilation as well (GROOVY-12132), which also applies to the in-process <groovyc> Ant task; the groovy command is deliberately left unchanged. The value of 3 is the most talkative. Most everyday code produces no warnings, and none of the sources compiled for this page did at any level, so there is no warning output to show here; the compilers that do report warnings are javac for the Java side of a joint compilation, as the next section shows, and the Groovy transformations that warn about questionable declarations.

Joint compilation

A project that mixes the two languages has a circular problem. Java code may call Groovy classes, Groovy code may call Java classes, and neither compiler understands the other’s sources. Joint compilation solves it with stubs. With -j, the Groovy compiler:

  1. parses all the Groovy sources and generates a Java stub for each class (signatures, no bodies);

  2. calls javac on those stubs together with the Java sources, so Java code can see the Groovy classes;

  3. continues the normal Groovy compilation, now with the compiled Java classes available.

flowchart LR G["Groovy sources Zone.groovy, Planner.groovy"] --> P["Parse and generate Java stubs"] P --> S["Stubs Zone.java (signatures only)"] S --> J["javac compiles stubs and Java sources"] JS["Java sources ZoneReport.java"] --> J J --> JC["Java class files ZoneReport.class"] JC --> GC["Groovy compilation continues"] G --> GC GC --> OUT["All class files in the -d directory"]

Without -j, the Groovy compiler treats .java files as if they were Groovy, which sometimes works because much of the syntax is shared but can change the meaning of the code. The example is a Groovy value class used by a Java class, which a Groovy main then calls back:

// src/greenhouse/Zone.groovy
package greenhouse

import groovy.transform.Immutable

@Immutable
class Zone {
    String name
    int benches
}
// src/greenhouse/ZoneReport.java
package greenhouse;

public class ZoneReport {
    public static String summary(Zone zone) {
        return zone.getName() + " has " + zone.getBenches() + " benches";
    }

    public static Zone sample() {
        return new Zone("north", 6);
    }
}
// src/greenhouse/Planner.groovy
package greenhouse

class Planner {
    static void main(String[] args) {
        println ZoneReport.summary(ZoneReport.sample())
        println ZoneReport.sample()
    }
}

Plain javac cannot compile the Java file on its own, because the Groovy class does not exist as a class file yet:

src/greenhouse/ZoneReport.java:4: error: cannot find symbol
    public static String summary(Zone zone) {
                                 ^
  symbol:   class Zone
  location: class ZoneReport

Joint compilation compiles all three. One detail matters on the command line: the output directory has to exist and be on the classpath (-cp, which must be the first argument), because the Groovy code is resolved against the Java classes that javac has just written there. The build tools take care of this for you. With -d alone, or with a directory that does not exist, the Groovy file fails with an error that never mentions classpaths:

src/greenhouse/Planner.groovy: 5: Apparent variable 'ZoneReport' was found in a static scope but doesn't refer to a local variable, static field or class. Possible causes:
You attempted to reference a variable in the binding or an instance variable from a static context.
...

The working command and its result:

mkdir classes
groovyc -cp classes -j -d classes src/greenhouse/*.groovy src/greenhouse/*.java
find classes -type f | sort
java -cp classes:groovy-6.0.0.jar greenhouse.Planner
classes/greenhouse/Planner.class
classes/greenhouse/Zone.class
classes/greenhouse/ZoneReport.class
north has 6 benches
greenhouse.Zone(north, 6)

The Java code calls the constructor new Zone("north", 6) and the getters of a class whose source contains neither: they are generated by @Immutable, an AST transformation (see AST Transformations: Code Generation & Class Design). The javac options pass through with -J and -F. For instance, -Jsource=17 reaches javac, whose own warning is logged at INFO level by the joint compiler:

INFO: warning: [options] location of system modules is not set in conjunction with -source 17
  not setting the location of system modules may lead to class files that cannot run on JDK 17
    --release 17 is recommended instead of -source 17 because it sets the location of system modules automatically
1 warning

Stubs and AST-generated members

Historically the stubs were generated before AST transformations ran, so a member contributed by a transformation such as @TupleConstructor, @Immutable or @Builder was absent from the stub, and Java code relying on it failed to compile. Groovy 6.0 (incubating, GEP-21) lets transformations opt in to contributing constructor, method and field signatures to the stubs. The change is additive: transformations that do not opt in behave as before. The release notes list the built-in transformations that opted in, among them @AutoClone, @AutoImplement, @Bindable, @Builder, @Delegate, @EqualsAndHashCode, @Immutable, @InheritConstructors, @Lazy, @MapConstructor, @NamedVariant, @Singleton, @Sortable, @ToString, @TupleConstructor and @Vetoable. Native records appear as records in the stubs, static methods of traits are visible to Java callers, and sealed types carry correct sealed, permits and non-sealed declarations. A custom transformation can opt in with an annotation attribute, a marker interface or a split-transform class pair; the specification is GEP-21.

The stub of the Zone class above shows the members that Java saw. The -s and -ks options are meant for this (-s names a stub directory, -ks keeps the stubs). In the 6.0.0 command-line compiler used here, -s failed with a ClassCastException (a String cast to File) in JavaAwareCompilationUnit, whatever the arguments, and with -ks alone no stub could be found in --temp either. The stub below was written through the compiler API, which accepts a File for the stub directory:

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.tools.javac.JavaAwareCompilationUnit

def config = new CompilerConfiguration()
config.targetDirectory = new File('cl5')
config.jointCompilationOptions = [stubDir: new File('stubs5'), keepStubs: true]
config.classpath = 'cl5'
def unit = new JavaAwareCompilationUnit(config)
unit.addSources(new File('src/greenhouse').listFiles().findAll {
    it.name.endsWith('.groovy') || it.name.endsWith('.java')
} as File[])
unit.compile()

The generated stubs5/greenhouse/Zone.java (the annotation line is shortened here; the real file lists all the transformation annotations on one line):

package greenhouse;

@groovy.transform.ToString(cache=true, includeSuperProperties=true) @groovy.transform.EqualsAndHashCode(cache=true) ... public final class Zone
  extends java.lang.Object  implements
    groovy.lang.GroovyObject {
;
@groovy.transform.Generated() public Zone(java.lang.String name, int benches) {}
@groovy.transform.Generated() public Zone(java.util.Map args) {}
@groovy.transform.Generated() public Zone() {}
@groovy.transform.Generated() public  java.lang.String getName() { return null; }
@groovy.transform.Generated() public  int getBenches() { return 0; }
@groovy.transform.Generated() public  java.lang.String toString() { return null; }
@groovy.transform.Generated() public  int hashCode() { return 0; }
@groovy.transform.Generated() public  boolean equals(java.lang.Object other) { return false; }
}

The constructors, getters, toString, hashCode and equals are the generated members. Stub bodies are empty because only the view of the signatures that javac has matters; the real code comes from the Groovy compilation that follows. Stub generation is also where joint compilation reaches its limits: anything that depends on a generated member that does not appear in the stub, or on generic information that stubs cannot express, fails in javac with an error about the stub rather than the Groovy source. When that happens, keeping the stubs and reading them, as above, is the way to find out what Java was shown.

Build tool entry points

Most projects do not call groovyc directly. What follows is where each tool stands, in brief; the details and complete build files are on Build Tools: Gradle, Maven & Ant. These tools were not run for this page.

  • Ant. The groovyc task (class org.codehaus.groovy.ant.Groovyc, in groovy-ant) calls the compiler in process. A nested javac element enables joint compilation (per the Ant task documentation). It accepts errorFormat="short" from 6.0.

  • Gradle. The groovy plugin compiles src/main/groovy with groovyc, and compiles Java and Groovy sources in the same source set jointly, so Java may call Groovy and the reverse. Gradle embeds its own Groovy line for its build scripts, which is not the one it compiles your code with; the project’s Groovy dependency decides that.

  • Maven. GMavenPlus is the most flexible plugin and supports recent Groovy versions, invokedynamic, Groovy on Android, GroovyDoc and configuration scripts. Its joint compilation uses stubs, so it has the limits of stubs. The Groovy-Eclipse compiler plugin avoids stubs altogether ("stubless" joint compilation), which sidesteps the stub problems at the cost of using a different compiler. GMaven, its predecessor, is no longer supported. A third option, the maven-antrun-plugin calling the Ant task, still works.

  • Gant is the legacy way of scripting Ant tasks with Groovy and has the same features as the Ant task.

Android support

A Groovy application can run on Android, but the regular groovyc tool cannot target Android bytecode. Groovy publishes dedicated jars with the classifier grooid, and a Gradle plugin (the groovy-android-gradle-plugin) adds Groovy support to the Android Gradle toolchain. A dependency without a grooid variant is directly compatible with Android and is added with transitive = false so that it does not drag in the normal Groovy jar. Android is not run here, and the online page still shows the Groovy 2.4 coordinates and a very old Android Gradle plugin; check the plugin home page for the versions that match Groovy 6.0 before relying on an example.

Where the book is outdated

The book this guide was checked against, the first edition of Groovy in Action, covers groovyc as the Groovy 1.0 compiler, and later books written for Groovy 2.x describe the 2.x one. Since then:

  • Groovy 6.0 adds --error-format short, --check and warning output after successful compilation, and extends the stubs of joint compilation so that AST-generated members are visible to Java.

  • --parameters and --enable-preview are later additions to the command line, and the minimum JDK is now 17.

  • The Maven group id is org.apache.groovy since Groovy 4.0 (it was org.codehaus.groovy), while the compiler classes keep their legacy package names, for example org.codehaus.groovy.tools.FileSystemCompiler.

  • Gradle and Maven are now the usual entry points, with the Gradle groovy plugin and GMavenPlus more common than the Ant task.