Skip to main content

Annotation processing (Java + Kotlin)

Bleep runs Java annotation processors (Lombok, MapStruct, Immutables, Dagger, …) via three composable fields under java:. The default is off, nothing scans, nothing runs unless you opt in.

Kotlin annotation processing: bleep ships KSP1 support as the standalone Analysis-API runner. See the Kotlin section below for the user surface, scan vs. explicit mode, version pairing, and current limitations. KAPT is not supported; migrate to KSP first.

The java: fields below also work in a Kotlin project: they run on its .java sources, which javac compiles after kotlinc. Kotlin code can use what they generate from the second compile on, since kotlinc runs first.

Resolution and processor-jar discovery happen lazily during compile as a first-class DAG task (bleep:resolve-annotation-processors:<project>), not at bootstrap. Failures are isolated per project and surface as a build failure with the project name in the message.

Quick start: Lombok​

Lombok ships annotations and a processor in a single jar. List it as a dependency, opt in to scanning, done:

projects:
myapp:
dependencies:
- org.projectlombok:lombok:1.18.46
java:
scanForAnnotationProcessors: true

Bleep finds Lombok's META-INF/services/javax.annotation.processing.Processor entry inside the jar at compile time and puts the classpath on javac's -processorpath, as javac does itself without one: a processor usually needs other jars to run, JMH's needs jmh-generator-core. ServiceLoader picks the processor class. @Data's getName(), setName(), toString(), etc. are generated as expected.

Three knobs​

FieldDefaultPurpose
scanForAnnotationProcessors: true | false | if-presentfalseOpt-in to scan resolved-dependencies jars for processors. When it finds any, the classpath goes on -processorpath. true fails when it finds none, if-present doesn't.
annotationProcessors: [Dep][]Explicit processor-only deps. Always added to -processorpath; never on the runtime/compile classpath of the project.
annotationProcessorOptions: {key: value}{}-A<key>=<value> flags. Visible to every configured processor (JSR 269 has no per-processor -A).

The two collections compose. You can scan AND list explicit extras in the same project.

MapStruct, AutoValue, Immutables, Dagger (split-jar processors)​

These ship their API as a small annotation jar (runtime-needed) and a separate processor jar (compile-time only). To keep the runtime classpath lean, put the annotations in dependencies and the processor in annotationProcessors. Each processor jar has a standard META-INF/services/javax.annotation.processing.Processor entry, so javac's ServiceLoader picks the classes. Bleep doesn't have to name them.

# MapStruct
projects:
myapp:
dependencies:
- org.mapstruct:mapstruct:1.5.5.Final
java:
annotationProcessors:
- org.mapstruct:mapstruct-processor:1.5.5.Final
annotationProcessorOptions:
mapstruct.suppressGeneratorTimestamp: "true"

# AutoValue
projects:
myapp:
dependencies:
- com.google.auto.value:auto-value-annotations:1.10.4
java:
annotationProcessors:
- com.google.auto.value:auto-value:1.10.4

scanForAnnotationProcessors is omitted, pure-explicit mode. Lombok-style jars in dependencies would not be auto-detected here.

This matches Maven's <annotationProcessorPaths> and Gradle's annotationProcessor configurations. The Maven idiom of <scope>provided</scope> for the processor jar is the equivalent of annotationProcessors: here, both keep the heavy processor JAR off the runtime classpath.

Composed: scan + explicit​

For projects that mix Lombok-style auto-discovery with an explicit processor-only dep:

projects:
myapp:
dependencies:
- org.projectlombok:lombok:1.18.46
- org.mapstruct:mapstruct:1.5.5.Final
java:
scanForAnnotationProcessors: true
annotationProcessors:
- org.mapstruct:mapstruct-processor:1.5.5.Final

Both processors run in a single javac invocation. Generated source files land under .bleep/generated-sources/<crossName>/annotations/ and are added to the project's compile source path.

Power-user mode: pure-explicit, no scanning​

Build engineers who want full reproducibility (no surprises from scanning) omit the boolean and pin every processor by hand:

projects:
myapp:
dependencies:
- org.projectlombok:lombok:1.18.46 # for @Data references on compile classpath
java:
annotationProcessors:
- org.projectlombok:lombok:1.18.46 # also pinned as a processor

Dependency upgrades cannot silently introduce new processors here, only what's in the explicit list runs.

Errors and escape hatches​

  • No-op opt-in fails loud. scanForAnnotationProcessors: true with empty annotationProcessors and zero processor-bearing jars in dependencies makes the AP DAG task fail at compile time, propagating up as commands.compile/commands.run throwing a BleepException with "Annotation processor resolution failed for N project(s)". The user typed true for a reason; we don't silently disable.
  • if-present is javac's own default. Without a processor path, javac runs whatever processors it finds on the classpath, and finding none is fine. scanForAnnotationProcessors: if-present does exactly that, and it's what bleep import-maven writes for modules which don't list <annotationProcessorPaths>: a Maven build relying on Lombok being on the classpath keeps working. Write true in your own build, so a processor which goes missing is an error.
  • Manual -proc:none is honored. Putting -proc:none in java.options skips all auto-wiring. Use this when you want to disable processing for a project that has processor-bearing dependencies.
  • Conflicting flags rejected. Manual -processorpath, -A, -proc:*, or -s in java.options raise an error when bleep is also wiring annotation processing, choose one or the other.
  • -processor (the comma-separated class-name list) is allowed in java.options. Bleep never emits -processor itself; use it to pin a subset (e.g. one processor from a multi-processor jar) or to work around the rare jar that ships its processor classes without a META-INF/services/javax.annotation.processing.Processor registration.

Where generated sources go​

.bleep/projects/<crossName>/generated-sources/annotations/, a single directory per project per cross-id. All processors share this -s directory (per JSR 269). Output files separate naturally by Java package.

The directory is reserved at bootstrap whenever a project has any annotation-processor configuration, and added to the project's source set so downstream compile picks up the generated .java files. bleep clean wipes it.

Not part of the cache key​

What a processor generates is a function of your sources and the processor coordinates, and both of those already go into the remote cache digest. So the generated output itself is left out — the same treatment a Scala macro's expansion has always had, since that is never written anywhere to hash. It also stays out of what --watch and bleep build invalidated look at.

The practical effect: a processor that stamps a date into what it generates — MapStruct does, unless mapstruct.suppressGeneratorTimestamp is set — no longer moves the digest on every build and leaves the project, and everything downstream of it, permanently uncached. The option is still worth keeping on if you want byte-identical sources jars.

The flip side is a trust assumption worth knowing: bleep assumes a processor depends only on what it is given. One that reads a file, an environment variable or the network at compile time will not invalidate the cache when that changes. That is the same assumption bleep makes of macros, and the same reason a generator that genuinely needs outside input belongs in a sourcegen script, whose output is keyed.

Kotlin: KSP​

Kotlin annotation processing in bleep goes through KSP, the Kotlin Symbol Processing API. Three composable fields under kotlin:. Default off, nothing runs unless you opt in:

projects:
myapp:
source-layout: kotlin
kotlin:
version: 2.1.20
kspVersion: 1.0.32
symbolProcessors:
- com.squareup.moshi:moshi-kotlin-codegen:1.15.0
symbolProcessorOptions:
room.schemaLocation: ./schemas
dependencies:
- com.squareup.moshi:moshi:1.15.0
platform:
name: jvm

KSP runs as a separate process before kotlinc. It reads your Kotlin and Java sources, invokes the configured processors, and emits generated files under .bleep/generated-sources/<crossName>/ksp/. The next kotlinc step picks those generated files up via the project's source set automatically.

Versioning​

kspVersion is the KSP-side suffix only. Bleep concatenates it with kotlin.version to form the full Maven coord com.google.devtools.ksp:symbol-processing-aa-embeddable:<kotlin>-<ksp>, e.g. 2.1.20-1.0.32. KSP releases are pinned 1:1 to exact kotlinc versions, so the kotlin prefix is forced; you only pick the KSP-side release. Browse KSP releases to find a release paired with your kotlin.version.

Required when symbolProcessors is non-empty or scanForSymbolProcessors: true. No default. Bleep fails loud at compile time if missing.

Two ways to declare processors​

Explicit (recommended):

kotlin:
kspVersion: 1.0.32
symbolProcessors:
- androidx.room:room-compiler:2.7.0
- com.google.dagger:hilt-compiler:2.51

Each entry resolves with its full transitive closure and is passed to KSP via the processor classpath. Processor jars never leak onto the runtime classpath.

Scan-mode:

kotlin:
kspVersion: 1.0.32
scanForSymbolProcessors: true
dependencies:
- com.squareup.moshi:moshi:1.15.0
- com.squareup.moshi:moshi-kotlin-codegen:1.15.0

When scanForSymbolProcessors: true, bleep scans the resolved dependency JARs for META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider and treats any matches as KSP processors. Useful when a single library ships both runtime and processor classes (Moshi codegen ships in the same artifact graph as Moshi itself).

Failing loud: scan-mode without any matched processors raises at compile time. The opt-in had to mean something.

Processor options​

symbolProcessorOptions is a string-to-string map. Bleep encodes the entries into KSP's -processor-options=k1=v1:k2=v2:... argument; any processor in the run can read them via SymbolProcessorEnvironment.options.

Where generated sources go​

.bleep/projects/<crossName>/generated-sources/ksp/
├── kotlin/ # KSP-emitted .kt files (read by kotlinc on the next compile)
├── java/ # KSP-emitted .java files (read by javac in mixed compilation)
├── classes/ # pre-compiled .class files (rare; some processors emit these)
├── resources/ # KSP-emitted resources (packaged with the project)
└── caches/ # KSP's own incremental state (today: unused; see below)

The kotlin/, java/, and resources/ dirs are added to the project's source / resource set automatically. bleep clean wipes them. Like annotation-processor output, they are compiled but not part of the cache key.

Incrementality (current state)​

KSP runs from scratch on every compile in bleep today. KSP1's incremental mode requires the caller to track and pass changed-source file lists explicitly. Bleep doesn't yet track per-file changes between runs, so we run with -incremental=false. The trade-off is slower KSP runs on large modules in exchange for not silently missing new files. Wiring up per-file change tracking is a planned follow-up.

What works​

  • Real-world JVM KSP processors: Moshi codegen, Room (JVM-pure 2.7+), Hilt, Dagger, Koin KSP, kotlinx.serialization (KSP variant), kotlin-inject, Ktorfit, Wire, Moshi IR, and similar.
  • Kotlin compiler plugins compose with KSP: kotlin.compilerPlugins: [allopen, jpa, spring, noarg, serialization] runs alongside KSP processors without conflict.
  • Multi-project builds: when project A depends on project B, KSP for A waits on B's compile so KSP can resolve cross-project types.

What's not supported​

  • KAPT. Kotlin's older annotation-processing model. Migrate to KSP1; every major KAPT processor has a KSP equivalent.
  • Kotlin/JS, Kotlin/Native. KSP supports them; bleep wires only the JVM target.
  • Tracked-incremental KSP. As above, runs are full each compile.

Path-separator caveat​

KSP's CLI accepts list arguments as :-separated on Unix and ;-separated on Windows. A project directory whose path itself contains the platform separator (a : on macOS / Linux) will break KSP. This is rare for normal filesystem layouts; flagged here because the underlying tool can't escape.