skip to content

If you write project(path: ':lib', configuration: 'reports') and resolution fails, what is likely wrong on the producer side and how do you make a configuration targetable this way?

level: middleimportance: should knowfreq 30%

answer

  1. two role flags: canBeResolved / canBeConsumed
  2. consumable target needs canBeConsumed = true
  3. outgoing config -> canBeResolved = false
  4. attach via artifacts { add(...) } or outgoing.artifact
  5. bucket/resolvable-only -> not targetable

basics

~10 s

The named configuration on :lib probably isn't consumable. Mark it isCanBeConsumed = true (and usually isCanBeResolved = false) and attach artifacts to it, then it can be targeted.

solid answer

~40 s

Consuming a named configuration cross-project requires the producer to actually *expose* one. Every Gradle configuration has two role flags: `canBeConsumed` (other projects may depend on it) and `canBeResolved` (it can resolve a dependency graph itself). To be a valid target of `project(configuration: 'reports')`, the producer's `reports` configuration must have `isCanBeConsumed = true`. It typically also sets `isCanBeResolved = false`, because a pure *outgoing* configuration shouldn't double as a resolution sink. Then you attach the outputs via `artifacts { add("reports", someTask) }` or `outgoing.artifact(...)`. If the configuration is only a bucket (both false) or only resolvable, Gradle rejects the cross-project request — often with a message that the configuration is not meant to be consumed. So the failure is usually a producer-side role/visibility problem, not consumer syntax.

code

kotlin · 11 lines
kotlin
// in :lib (the producer)
val reports by configurations.creating {
    isCanBeConsumed = true
    isCanBeResolved = false
}
artifacts { add("reports", tasks.named("genReports")) }

// in :app (the consumer)
dependencies {
    implementation(project(path = ":lib", configuration = "reports"))
}

go deeper

for a junior

Know that the producer configuration must be 'consumable' (canBeConsumed = true) and have artifacts attached.

for a middle

Explain the two role flags, the typical consumable setup (canBeConsumed true, canBeResolved false), and how to attach artifacts.

for a senior

Diagnose failures by reasoning about roles, explain why dual-role configs are discouraged, and relate to how java plugin's apiElements/runtimeElements are defined.

for a principal

Set conventions for exposing custom outgoing configurations, prefer attribute/capability modeling for reuse, and govern against ad-hoc consumable configs proliferating across modules.

## Configuration roles, briefly Every Gradle `Configuration` carries two booleans that define its **role**: - `canBeResolved` — it can be *resolved* into a concrete dependency graph + files (a consumer/resolution sink). - `canBeConsumed` — it can be *exposed* to other projects as a dependency target (a producer/outgoing surface). Three common combinations: | Role | canBeResolved | canBeConsumed | example | |------|---------------|---------------|---------| | Bucket (declarable) | false | false | `implementation`, `api` | | Resolvable | true | false | `compileClasspath`, `runtimeClasspath` | | Consumable | false | true | `apiElements`, `runtimeElements` | ## Why your cross-project target fails `project(path = ":lib", configuration = "reports")` asks `:lib` to hand over the artifacts of *its* `reports` configuration. Gradle only allows this if `reports` is **consumable** (`canBeConsumed = true`). If `reports` is a bucket or resolvable-only, the request is invalid and you get an error to the effect that the target configuration is not intended to be consumed. ## Making a configuration targetable ```kotlin val reports by configurations.creating { isCanBeConsumed = true // other projects may depend on it isCanBeResolved = false // it is purely outgoing } val genReports = tasks.register<Zip>("genReports") { /* ... */ } artifacts { add("reports", genReports) } ``` Now a sibling can do `implementation(project(path = ":lib", configuration = "reports"))` and receive the zip. ## Attributes are optional here Because you named the configuration explicitly, Gradle does **not** run attribute matching to pick it. You can still add attributes (`attributes { attribute(Usage.USAGE_ATTRIBUTE, ...) }`) for documentation or so the same configuration also participates in attribute-driven resolution, but they aren't required for the explicit-name path. ## Common producer-side mistakes - Forgetting `isCanBeConsumed = true` (left at the default false). - Adding artifacts to a *bucket* configuration like `implementation` and trying to consume that. - Marking it both resolvable and consumable — discouraged; one configuration shouldn't be both ends.

  • Why shouldn't a single configuration be both resolvable and consumable?
    It conflates the producer (outgoing) and consumer (resolution) roles, leading to ambiguous semantics and accidental dependency leakage; Gradle warns against and is deprecating this dual role.
  • Do you need attributes on the producer configuration for project(configuration:) to work?
    No — explicitly naming the configuration skips attribute matching. Attributes are optional here and mostly useful if the same configuration also participates in attribute-driven resolution.

saying these in an interview costs you the question

  • Blaming consumer syntax when the real issue is a non-consumable producer configuration.
  • Attaching artifacts to a bucket like implementation and expecting it to be consumable.

context