Skip to content

Replace SetProperty<Configuration> with ConfigurableFileCollection on ShadowJar#2003

Draft
tresat wants to merge 1 commit intoGradleUp:mainfrom
tresat:tt/avoid-property-of-configuration
Draft

Replace SetProperty<Configuration> with ConfigurableFileCollection on ShadowJar#2003
tresat wants to merge 1 commit intoGradleUp:mainfrom
tresat:tt/avoid-property-of-configuration

Conversation

@tresat
Copy link
Copy Markdown

@tresat tresat commented Apr 10, 2026

Replacing SetProperty<Configuration> with ConfigurableFileCollection

Summary of Changes

This change replaces the configurations property on ShadowJar from SetProperty<Configuration> to ConfigurableFileCollection, and introduces three new public methods as the sole API for managing which Gradle configurations are shadowed:

  • addConfiguration(config: Configuration) — add a configuration to shadow
  • setConfigurations(configs: Iterable<Configuration>) — replace all configurations
  • clearConfigurations() — remove all configurations

An internal MutableList<Configuration> (_sourceConfigurations) tracks the actual Configuration objects for dependency-level filtering, while the ConfigurableFileCollection serves as the configuration-cache-safe input property.

Files Modified
File Change
ShadowJar.kt Replaced property type, added mutation methods, updated toMinimize and includedDependencies providers to reference internal list
ShadowJavaPlugin.kt task.configurations.convention(...) replaced with task.addConfiguration(runtimeConfiguration)
ShadowKmpPlugin.kt Same pattern — task.addConfiguration(...) replaces lazy convention
ShadowPropertiesTest.kt Updated assertion from configurations.get() to configurations.files
JavaPluginsTest.kt 4 Groovy DSL sites migrated to setConfigurations([...])
RelocationTest.kt 1 Groovy DSL site migrated to clearConfigurations()
PublishingTest.kt 1 Groovy DSL site migrated to setConfigurations([...])

The Problem: Configuration Cannot Survive a CC Round-Trip

Configuration cannot survive a configuration cache serialization round-trip. When the CC serializes a Configuration object, Gradle's codec produces a FileCollection (ResolvingFileCollection) on deserialization — not a Configuration. This causes a task reconstruction failure at CC load time, before the task ever executes.

The failure occurs when Gradle tries to restore a previously cached task. The CC deserializes the stored value (a ResolvingFileCollection) and attempts to inject it back into the SetProperty<Configuration>. The property performs a type check: "Is ResolvingFileCollection assignable to Configuration?" It is not — ResolvingFileCollection implements FileCollection but not Configuration. The build fails immediately:

Could not load the value of field 'configurations' of task ':shadowJar'
  > Cannot set the value of a property of type org.gradle.api.artifacts.Configuration
    using a provider of type
    org.gradle.api.internal.file.DefaultFileCollectionFactory$ResolvingFileCollection.

This is not a .get() call failure or a ClassCastException. The CC cannot even reconstruct the task object. The deserialization process fails during task restoration, which happens before any task action runs. Even if reconstruction somehow succeeded, calling .get() would return FileCollection objects where Configuration is expected — Shadow's DependencyFilter relies on Configuration.resolvedConfiguration.firstLevelModuleDependencies to filter by module coordinates, which would fail immediately.

Since Gradle 8.0, the configuration cache always performs a store-then-load round-trip, even on the first build (a cache miss). Gradle configures the build, serializes the task graph, and then runs from the deserialized state — never from the live configuration objects. A type that cannot survive a round-trip will cause the build to fail on the very first invocation with the CC enabled.


The Gradle PR: gradle/gradle#37466

gradle/gradle#37466 addresses gradle/gradle#19122 by moving the failure from a confusing load-time type mismatch to a clear store-time rejection.

The PR adds validation in the property codec encode paths (PropertyCodec.encodeThis(), SetPropertyCodec.encodeThis(), ListPropertyCodec.encodeThis(), MapPropertyCodec.encodeThis()) that checks whether the property's type argument is Configuration or SourceDirectorySet. If so, the build fails immediately at CC store time with:

Cannot serialize SetProperty<Configuration> in task :shadowJar of type ShadowJar.
The value type of this property (org.gradle.api.artifacts.Configuration)
is not supported with the configuration cache.

> Use a @InputFiles ConfigurableFileCollection instead.

Key details

  • The validation fires for all annotations — the PR tests @Input, @Internal, @InputFiles, and @Classpath. The @Classpath annotation does not bypass the check.
  • Unset properties are exempt — if the SetProperty<Configuration> has never been set, its backing field is null, a null marker is written directly, and SetPropertyCodec.encodeThis() is never invoked.
  • The failure shifts from load to store — this is strictly an improvement in error reporting. The underlying type-mismatch problem already existed; the PR just makes it impossible to write a poisoned cache entry.
Aspect Before PR #37466 After PR #37466
CC Store Succeeds silently Fails with UnsupportedPropertyValueException
CC Load Fails with confusing type mismatch Never reached (poisoned entry not written)
Error message "Cannot set the value of a property of type Configuration using a provider of type ResolvingFileCollection" "Cannot serialize SetProperty<Configuration>... Use a @InputFiles ConfigurableFileCollection instead."

Why Shadow Works Today

Despite the round-trip problem described above, Gradle's own Shadow plugin smoke test runs a full CC store-then-load cycle (build shadowJar, clean, build shadowJar again) and passes with the configuration cache enabled. The smoke test uses the plugin's default configuration — it does not set the configurations property explicitly.

The most likely explanation is that the @Classpath annotation causes the CC to handle this property through the file-input serialization path rather than through SetPropertyCodec. In this path, each Configuration (which extends FileCollection) is resolved to its constituent file paths at store time. On load, only the file-input tracking is restored — the CC does not attempt to reconstruct the SetProperty<Configuration> value by injecting a deserialized FileCollection back into it, so the type mismatch never occurs.

This is consistent with the PR's design: it adds rejection logic inside SetPropertyCodec.encodeThis(). If @Classpath properties bypass this codec entirely, the new rejection would not fire for Shadow's current property — but the PR's intent is to reject this pattern regardless, and future Gradle changes may close this bypass.


How This Change Makes Shadow Compatible

The property replacement

// Before
@get:Classpath
public open val configurations: SetProperty<Configuration> = objectFactory.setProperty()

// After
@get:Classpath
public open val configurations: ConfigurableFileCollection = objectFactory.fileCollection()

ConfigurableFileCollection is the type Gradle's own error message recommends. The configuration cache serializes it as resolved file paths — no type mismatch, no round-trip problem. This resolves both the existing latent CC load failure and the new store-time rejection that gradle/gradle#37466 introduces.

Preserving dependency filtering

The DependencyFilter contract requires actual Configuration objects to walk the dependency tree:

// DependencyFilter.kt
override fun resolve(configuration: Configuration): FileCollection {
  // Uses configuration.resolvedConfiguration.firstLevelModuleDependencies
  // to filter by group/name/version �� needs the real Configuration object
}

An internal MutableList<Configuration> (_sourceConfigurations) tracks the Configuration objects. The three public methods (addConfiguration, setConfigurations, clearConfigurations) serve as a chokepoint that keeps the file collection and the internal list in sync:

public open fun addConfiguration(config: Configuration) {
  _sourceConfigurations.add(config)    // for DependencyFilter
  configurations.from(config)           // for @Classpath / CC
}

Why the internal list is safe on cache restore

_sourceConfigurations is annotated @Transient and is not a Gradle managed property — the configuration cache does not serialize it. After a cache restore, it's an empty list. This is safe because:

  1. includedDependencies and toMinimize are ConfigurableFileCollection properties whose providers reference _sourceConfigurations
  2. On the first run (cache store), these providers evaluate, call DependencyFilter.resolve(_sourceConfigurations), and produce resolved file sets
  3. The configuration cache serializes these ConfigurableFileCollections with their resolved files
  4. On cache restore, the resolved files are used directly — the providers are never re-evaluated, so _sourceConfigurations is never accessed

DSL migration

The old SetProperty supported Groovy's property assignment syntax and Gradle's provider-aware set(Provider<Iterable<T>>):

// Old Groovy DSL
configurations = [project.configurations.runtimeClasspath]
configurations = project.configurations.named('testRuntimeClasspath').map { [it] }

The new API uses explicit method calls:

// New Groovy DSL
setConfigurations([project.configurations.runtimeClasspath])
addConfiguration(project.configurations.testRuntimeClasspath)
clearConfigurations()
// New Kotlin DSL
setConfigurations(listOf(project.configurations.runtimeClasspath))
addConfiguration(project.configurations["testRuntimeClasspath"])
clearConfigurations()

This is a breaking public API change for users who configure configurations directly in their build scripts.

… ShadowJar

Add addConfiguration(), setConfigurations(), clearConfigurations() methods
that maintain both the CC-safe ConfigurableFileCollection and an internal
Configuration list for DependencyFilter compatibility.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant