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

Terminal
bash
elide javac -- -d out src/Hello.java
elide java -- -cp out Hello
Compiling and running Java source with Elide
 ~/demo  elide javac -- -d out Greeter.java
✅  1 Java sources compiled to `out` (81ms)
 ~/demo  elide java -- -cp out Greeter
Hello, Ada!
Hello, Linus!
 












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.
--incrementalWorker only: reuse unchanged implementation classes across requests.
--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.
Terminal
bash
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

Terminal
bash
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:

Terminal
bash
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.

Terminal
bash
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:

Terminal
bash
elide build compile-java-main
pkl
jvm {
  target = 25
  java {
    release = 25
    compiler {
      mode = "embedded"
      flags { "-Xlint:all" }
    }
  }
  features {
    graalvm = false
  }
}
utf-8 · pkl · Lines: 13
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.

pkl
sources {
  ["desktopless"] { paths { "src/desktopless/java" } }
}
jvm {
  java {
    sourceSets {
      ["desktopless"] {
        flags { "--limit-modules"; "java.base" }
      }
    }
  }
}
utf-8 · pkl · Lines: 12

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:

c
struct Point { int x; };
int sum(int a, int b);
utf-8 · c · Lines: 2

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

java
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); }
}
utf-8 · java · Lines: 7

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:

pkl
jvm {
  java {
    release = 25
    bindings {
      ["example"] {
        headers { "native/include/**/*.h" }
        packageName = "example.ffi"
        headerClassName = "NativeApi"
        includeDirs { "native/include" }
        libraries { "example" }
      }
    }
    sourceSets {
      ["main"] { bindings { "example" } }
    }
  }
}
utf-8 · pkl · Lines: 17

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.

Terminal
bash
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.

Terminal
bash
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:

Terminal
bash
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.

Start Java incremental workers with elide javac --persistent_worker --incremental. Selective compilation requires explicit -proc:none, -sourcepath "", and a classpath without empty or wildcard entries. Use an empty directory as the classpath when there are no dependencies. Processors, plugins, named modules, and other unsupported configurations use full compilation with the requested flags.

Body-only edits can reuse unchanged classes; declaration, source-set, or dependency changes trigger a full compile. Each successful request publishes complete class, jar, and requested ABI outputs, even if public outputs were deleted between requests. Failed compilation preserves the previous outputs. State belongs to the worker: restarting it starts a cold compile. Pass the complete source list on every request.

Samples

Start from a template:

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

Was this page helpful?