skip to content

How do you supply dynamic, cacheable compiler arguments to a JavaCompile task using a CommandLineArgumentProvider, and how does it differ from options.compilerArgs?

level: seniorimportance: nice to knowfreq 18%

answer

  1. options.compilerArgs = opaque strings
  2. options.compilerArgumentProviders = @Nested, tracked
  3. annotation-processor -A options pointing at files
  4. @InputFile/@Classpath + @PathSensitive(NONE)
  5. static flags fine as compilerArgs

basics

~10 s

Add a CommandLineArgumentProvider to JavaCompile's options.compilerArgumentProviders. Like raw options.compilerArgs the strings reach javac, but the provider lets you annotate underlying file/value inputs so up-to-date checks and the build cache stay correct.

solid answer

~40 s

`JavaCompile` exposes `options.compilerArgs` (a plain `List<String>`) and `options.compilerArgumentProviders` (a `List<CommandLineArgumentProvider>`). Both feed arguments to `javac`, but they differ in input tracking. `options.compilerArgs` are opaque strings. If a compiler argument references a file — e.g. an annotation-processor config, a module-path entry, or `-Aopt=/path/to/file` — Gradle has no idea that file is an input, so changing it won't recompile and the build cache can return stale `.class` files. With a provider added to `compilerArgumentProviders`, you declare the underlying file/value as an annotated nested input (`@InputFile`, `@Classpath`, `@Input` with proper `@PathSensitive`), and emit the actual `-A.../path` argument from `asArguments()`. Now the compile task's fingerprint includes that input, so edits trigger recompilation and the cache key is correct and relocatable. This is the canonical way to wire annotation-processor parameters that point at files.

code

kotlin · 15 lines
kotlin
abstract class AptArgs : CommandLineArgumentProvider {
    @get:InputFile
    @get:PathSensitive(PathSensitivity.NONE)
    abstract val mappingFile: RegularFileProperty

    override fun asArguments(): Iterable<String> =
        listOf("-Amapping=${mappingFile.get().asFile.absolutePath}")
}

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.add("-Xlint:all")            // static literal: fine
    val apt = objects.newInstance(AptArgs::class.java)
    apt.mappingFile.set(layout.projectDirectory.file("apt/mapping.properties"))
    options.compilerArgumentProviders.add(apt)        // file-backed: tracked
}

go deeper

for a junior

Know JavaCompile has options.compilerArgs for flags; awareness that a provider exists for file-backed args is a bonus.

for a middle

Distinguish compilerArgs (opaque) from compilerArgumentProviders (tracked) and give the APT-options use case.

for a senior

Model processor/module-path via @Classpath, reason about stale-bytecode/relocatability risk, and choose the right knob per argument.

for a principal

Set a standard that any file-dependent compiler argument goes through a provider, protecting cache correctness on the build's heaviest tasks across the org.

## The two knobs on JavaCompile - `options.compilerArgs: List<String>` — literal arguments handed to `javac`. Simple, but opaque to input tracking. - `options.compilerArgumentProviders: List<CommandLineArgumentProvider>` — `@Nested` collection; each provider's annotated properties join the compile task's input fingerprint. Both ultimately append to the `javac` command line. The difference is *correctness*, not capability. ## When compilerArgs is enough Static flags with no file/identity meaning are fine as plain strings: `options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror"))`. These are value inputs Gradle already fingerprints as part of `compilerArgs` itself, and they carry no machine-specific path. ## When you need a provider Reach for `compilerArgumentProviders` whenever an argument depends on a **file** or a **computed value that must be tracked**, most commonly annotation-processor options: ```kotlin abstract class AptArgs : CommandLineArgumentProvider { @get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val mappingFile: RegularFileProperty override fun asArguments(): Iterable<String> = listOf("-Amapping=${mappingFile.get().asFile.absolutePath}") } tasks.withType<JavaCompile>().configureEach { val apt = objects.newInstance(AptArgs::class.java) apt.mappingFile.set(layout.projectDirectory.file("apt/mapping.properties")) options.compilerArgumentProviders.add(apt) } ``` Now editing `mapping.properties` recompiles, and because the file is tracked by content (`@PathSensitive(NONE)`) while the absolute path lives only in the emitted string, the compile task stays cacheable and relocatable across machines. ## Module path / processor path nuance For jar/dir collections feeding `--processor-path` or `--module-path`, prefer `@Classpath` (content-normalized, order-insensitive where appropriate) so the fingerprint is stable. Don't list them in `compilerArgs` as raw absolute paths — that both under-tracks content and over-tracks location. ## Why this matters more for compile tasks `JavaCompile` is one of the heaviest cached tasks in a typical build; a stale cache hit there silently ships wrong bytecode, and a false cache miss costs real time on every CI run. Correct input modeling via providers is what makes incremental + cached compilation trustworthy. ## Summary - Static literal flags → `options.compilerArgs`. - File-dependent or value-tracked args (APT options, generated configs) → `options.compilerArgumentProviders` with annotated inputs. - Jar/dir collections → `@Classpath` inside the provider.

  • Why is a raw -Aconfig=/abs/path string in options.compilerArgs dangerous?
    Gradle treats it as an opaque value: it doesn't see the file as an input (so edits won't recompile and a cache hit can be stale) and the absolute path pollutes the input value, hurting relocatable caching. A provider with @InputFile fixes both.
  • How would you wire a processor path so it's content-tracked?
    Put the jar/dir collection on the provider as a `@Classpath`-annotated `ConfigurableFileCollection` and emit the `--processor-path` argument from asArguments(). @Classpath normalizes by content and is relocatable.
  • Are static flags like -Werror better as compilerArgs or a provider?
    compilerArgs. They're pure literals with no file identity or machine-specific path, so Gradle already fingerprints them correctly and a provider adds no value.

saying these in an interview costs you the question

  • Putting file-referencing -A options as raw strings in options.compilerArgs.
  • Listing processor/module-path jars as absolute-path strings instead of @Classpath inputs.
  • Claiming compilerArgumentProviders changes what javac receives versus compilerArgs (it doesn't — only how inputs are tracked).

context