# 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.

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

## Compile source files

```bash
elide javac -- -d out src/Hello.java
elide java -- -cp out Hello
```

<img src="https://elide.help/assets/demos/javac.gif" alt="Compiling and running Java source with Elide" width="1591" height="728" loading="lazy" decoding="async" class="rounded-lg border border-border my-6 w-full" />

`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 `--`:

| Option | Purpose |
| --- | --- |
| `--jar PATH` | Pack the output into a JAR at `PATH`. |
| `--manifest FILE` | Manifest for the `--jar` output. |
| `--max-errors N` | Errors shown before the rest are counted; `0` shows all. Warnings are de-duplicated and never limited. |
| `--report-used-deps PATH` | Write the classpath entries the compiler read, supertypes included (Bazel strict deps). |
| `--jdeps PATH` | Write a Bazel `.jdeps` proto: each classpath entry explicit, implicit or unused, with the classes used from it and their SHA-256 hashes. |
| `--target-label LABEL` | Target label recorded in the `--jdeps` proto. |
| `--unused-inputs PATH` | Write the classpath entries the compile neither used nor searched (Bazel `unused_inputs_list`). |
| `--capabilities` | Print compiler capabilities as JSON. |
| `--persistent_worker` | Run as a Bazel persistent worker, singleplex or multiplex. |
| `--classpath-cache` | Worker only: reuse classpath state across compiles. `ELIDE_WORKER_CLASSPATH_CACHE=0` turns it off. |
| `--abi-only` | Write only an ABI jar, to `--emit-abi`, `--jar` or `-d`, without compiling bodies. |
| `--emit-abi PATH` | Also write an ABI jar (or directory) next to the full compile. |

```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

```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:

```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.

```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](https://elide.help/docs/toolchain/source-sets) that
`elide.pkl` declares. To compile only `main`:

```bash
elide build compile-java-main
```

```pkl
jvm {
  target = 25
  java {
    release = 25
    compiler {
      mode = "embedded"
      flags { "-Xlint:all" }
    }
  }
  features {
    graalvm = false
  }
}
```

| Property | Meaning | Default |
| --- | --- | --- |
| `jvm.target` | Bytecode 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.source` | Use `--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.flags` | Extra `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>"].flags` | `javac` flags for that source set, after `compiler.flags`. | none |
| `jvm.javaHome` | Java home for external compiles and forked JVMs. | Elide's own |
| `jvm.debug` | Compile with `-g`. | `true` |
| `jvm.features.graalvm` | Put `org.graalvm.nativeimage`, `word` and `collections` on the compile classpath. | `false` |
| `jvm.features.automodules` | Put 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" }
      }
    }
  }
}
```

Each compile writes its arguments to `javac.args` beside its output. Annotation processors: [Maven Dependencies](https://elide.help/docs/toolchain/jvm/maven#annotation-processors).

## 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](https://elide.help/docs/toolchain/native/jextract).

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

```c
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:

```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); }
}
```

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](https://elide.help/docs/toolchain/native/cc) 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" } }
    }
  }
}
```

`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.

```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`.

```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:

```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](https://elide.help/docs/build-tools-bazel#persistent-workers).

## Samples

- [Java records and streams](https://elide.help/docs/code-samples#java-records-and-streams)

Start from a template:

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