Skip to content

Java compiler

Compile Java 25 source with elide javac or in an Elide project build, without a JDK install: ABI headers, C header bindings, JAR output and compiler options.

On this page

Elide bundles javac for Java 25; no separate JDK is needed.

Compile source files

elide javac -- -d out src/Hello.java
elide java -- -cp out Hello
0:12 / 0:12

javac options go after --. Without -d, class files land beside their sources. elide javac -- -help lists every javac option.

Elide options

Elide options go before --:

OptionPurpose
--jar PATHPack the output into a JAR at PATH.
--manifest FILEManifest for the --jar output.
--max-errors NErrors shown before the rest are counted; 0 shows all. Warnings are de-duplicated and never limited.
--report-used-deps PATHWrite the classpath entries the compiler read, supertypes included (Bazel strict deps).
--jdeps PATHWrite a Bazel .jdeps proto: each classpath entry explicit, implicit or unused, with the classes used from it and their SHA-256 hashes.
--target-label LABELTarget label recorded in the --jdeps proto.
--unused-inputs PATHWrite the classpath entries the compile neither used nor searched (Bazel unused_inputs_list).
--capabilitiesPrint compiler capabilities as JSON.
--persistent_workerRun as a Bazel persistent worker, singleplex or multiplex.
--classpath-cacheWorker only: reuse classpath state across compiles. ELIDE_WORKER_CLASSPATH_CACHE=0 turns it off.
--abi-onlyWrite only an ABI jar, to --emit-abi, --jar or -d, without compiling bodies.
--emit-abi PATHAlso write an ABI jar (or directory) next to the full compile.
elide javac --jar target/app.jar -- src/Main.java src/Util.java
elide javac --jar target/app.jar --manifest src/MANIFEST.MF -- src/Main.java

ABI jars

elide javac --abi-only --emit-abi header.jar -- src/Main.java src/Util.java
elide javac --emit-abi header.jar -- -d out src/Main.java src/Util.java

An ABI (header) jar holds the public, protected and package-private signatures, constants and annotations that dependents compile against. It contains no implementation bodies and is a compile-time input, not a runnable JAR. Private implementation details are omitted. A body-only edit leaves the header byte-identical unless an annotation processor changes the generated API.

--abi-only compiles headers directly from source without checking method bodies. --emit-abi runs a header compile alongside the full javac compile; the full compile still checks bodies and produces the runnable classes. Use separate output paths for implementation and header output. A .jar destination writes an archive; another destination writes class files to a directory.

For a library and its consumer:

elide javac --emit-abi lib-abi.jar -- -d lib-classes src/library/Api.java
elide javac -- -cp lib-abi.jar -d app-classes src/app/Main.java
elide java -- -cp app-classes:lib-classes app.Main

The consumer compiles against lib-abi.jar and runs against lib-classes. Classpath examples use : on macOS/Linux; use ; on Windows. Both JAR and class-directory dependency inputs work. Pass the dependency classpath explicitly with -cp/--class-path, or set CLASSPATH; the header compiler does not use the current directory as an implicit classpath.

--release, -source and -target set the header’s language/class-file level; the default is Java 25. Header compilation resolves platform types against the bundled JDK, including when --release names an older JDK. It does not validate that an API exists in that older release. Run the full javac compile with --release to enforce that API boundary and check method bodies.

Annotation processing in headers

Explicitly enabled annotation processors run during header compilation, so their generated types can be included. Use -processor to name processors, or -processorpath/--processor-path to discover them. With -proc:full and no processor path, discovery uses the classpath. -proc:none disables processing.

elide javac --abi-only --emit-abi lib-abi.jar -- \
  -cp annotations.jar --processor-path processor.jar -processor example.GenerateApi \
  src/library/Api.java

The processor path must include the processor’s dependencies. Header processing presents declarations without implementation bodies; processors that inspect javac implementation details or method bodies require separate compatibility checks. --emit-abi runs processors independently in the header and full compiles, so a processor may execute twice. Java annotation processing support also depends on the distribution: inspect elide javac --capabilities before wiring a processor-dependent build.

Build projects

elide build compiles src/main/java and src/test/java, or the source sets that elide.pkl declares. To compile only main:

elide build compile-java-main
jvm {
  target = 25
  java {
    release = 25
    compiler {
      mode = "embedded"
      flags { "-Xlint:all" }
    }
  }
  features {
    graalvm = false
  }
}
PropertyMeaningDefault
jvm.targetBytecode target; the --release level when java.release is unset."auto" (21)
jvm.java.release--release level: compile against that JDK’s API only."auto"
jvm.java.sourceUse --source/--target instead of --release; ignored when release is set."auto"
jvm.java.compiler.mode"embedded" uses Elide’s javac; "external" runs the one in jvm.javaHome."embedded"
jvm.java.compiler.flagsExtra javac flags; --release, -source or -target here replaces Elide’s.none
jvm.java.sourceSets["<set>"].release--release level for that source set alone."auto" (the project’s)
jvm.java.sourceSets["<set>"].flagsjavac flags for that source set, after compiler.flags.none
jvm.javaHomeJava home for external compiles and forked JVMs.Elide’s own
jvm.debugCompile with -g.true
jvm.features.graalvmPut org.graalvm.nativeimage, word and collections on the compile classpath.false
jvm.features.automodulesPut JARs without a module descriptor on the module path as automatic modules.true

A source set that needs different settings, such as one compiled against java.base alone, names them under sourceSets. Naming a source set the project does not have fails the build.

sources {
  ["desktopless"] { paths { "src/desktopless/java" } }
}
jvm {
  java {
    sourceSets {
      ["desktopless"] {
        flags { "--limit-modules"; "java.base" }
      }
    }
  }
}

Each compile writes its arguments to javac.args beside its output. Annotation processors: Maven Dependencies.

C header bindings

From 1.6.2, embedded javac can generate Java FFM bindings from C headers and compile them alongside application sources. Generated Java stays in memory unless source export is requested. For separate source generation, filtering options and native library loading, see jextract.

For example, put this declaration in native/include/example.h:

struct Point { int x; };
int sum(int a, int b);

Save the application as src/main/java/NativeClient.java; it can import generated types in the same compile:

import example.ffi.NativeApi;
import example.ffi.Point;

public class NativeClient {
  public long pointSize() { return Point.sizeof(); }
  public int sum(int a, int b) { return NativeApi.sum(a, b); }
}

Generating and compiling these bindings needs the header, not a built implementation of sum. Calling sum at runtime needs the native library and native access enabled. C compiler describes the separate C compilation surface.

Declare named groups in elide.pkl and select each group in its owning Java source set:

jvm {
  java {
    release = 25
    bindings {
      ["example"] {
        headers { "native/include/**/*.h" }
        packageName = "example.ffi"
        headerClassName = "NativeApi"
        includeDirs { "native/include" }
        libraries { "example" }
      }
    }
    sourceSets {
      ["main"] { bindings { "example" } }
    }
  }
}

headers, packageName, and headerClassName are required. includeDirs, defines, and libraries default to empty lists; useSystemLoadLibrary defaults to false. Paths belong to the declaring project, including workspace members. Globs expand in sorted order and must match a file. A group has one owning source set; tests and other source sets inherit its classes through dependsOn. Declarations alone do not select groups.

elide build
elide javac -- --bindings example -d out src/main/java/NativeClient.java
elide javac -- --jextract 'native/include/*.h' \
  --jextract-package example.ffi --jextract-class NativeApi \
  --jextract-include-dir native/include --jextract-library example \
  -d out src/main/java/NativeClient.java

Repeat --bindings to combine named groups. Repeat --jextract, --jextract-include-dir, --jextract-define, and --jextract-library to add headers, search directories, preprocessor definitions, and library names to an ad hoc group. --jextract-use-system-load-library selects System.loadLibrary-based loading for that ad hoc group; the corresponding Pkl property is useSystemLoadLibrary. Named and ad hoc groups can be combined; ad hoc flags do not alter named groups. These options also work inside javac argument files. Quote globs so Elide expands them. Ad hoc javac paths resolve against the working directory.

Builds accept the same options using --option=value, for example --bindings=example. Add --bindings-source-set=<name> to choose the source set receiving CLI additions; it can be omitted only when there is one production source set. Build CLI paths resolve against the selected project root, including when using --project.

elide build --bindings=example --bindings-source-set=main
elide build --jextract='native/include/*.h' --jextract-package=example.ffi \
  --jextract-class=NativeApi --jextract-include-dir=native/include \
  --jextract-library=example --bindings-source-set=main

A bindings-only Java source set is valid. Keep an owning source set’s selections in Pkl for normal builds; CLI selections add to the configured groups for that invocation. Unknown groups, unmatched globs and conflicting generated type names fail before application compilation. Generated Java participates in normal javac annotation processing, and compiling it with -proc:none still works.

To export sources for an IDE:

elide build --emit-bindings
elide build bindings-main --emit-bindings
elide javac -- --bindings example --emit-bindings generated-bindings -d out src/main/java/NativeClient.java

The bindings-main target generates and exports bindings without compiling the application. Build exports live under the project’s build directory at jvm/generated-sources/<source-set>/jextract/<group>/. IntelliJ export marks emitted roots as generated sources, preserving each source set’s test status. Source JARs include the generated Java. Successful exports remove stale owned files and preserve unrelated files; failed generation retains the previous export. A direct javac export uses <directory>/<group>/…, with adhoc as the group name for CLI header selections. Exported sources are for IDEs and source artifacts; Elide compiles the in-memory sources rather than reading that export back in.

Each build prepares bindings again before deciding whether Java compilation is up to date. Changes in transitive headers or preprocessor results therefore affect compilation even when the top-level header has not changed. Unchanged generated sources allow the Java compile to remain cached.

Bindings require Java 23 or later, embedded javac, and a build with native tools enabled. Classpath projects and single-module compilation are supported; external javac and multi-module source-path compilation are rejected. The owning source set must contain no Kotlin: put bindings in a separate Java source set and depend on it from Kotlin. Elide does not raise the configured Java release, package native application libraries, or enable runtime native access automatically.

Bazel persistent workers

elide javac --persistent_worker and elide kotlinc --persistent_worker keep one compiler warm across Bazel actions. Rules, setup and a worked example: Bazel.

Samples

Start from a template:

elide init --template java        # Java app with a JUnit test

Was this page helpful?