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.

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 it on javac's -processorpath. ServiceLoader picks the processor class. @Data's getName(), setName(), toString(), etc. are generated as expected.

Three knobs

FieldDefaultPurpose
scanForAnnotationProcessors: BooleanfalseOpt-in to scan resolved-dependencies jars for processors. Matches found jars get added to -processorpath.
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.
  • 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/generated-sources/<crossName>/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.

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/generated-sources/<crossName>/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.

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.