Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Configuration Cache support (Gradle 9+, AGP 9+) for `featured-gradle-plugin`
- `NSUserDefaultsConfigValueProvider` for iOS/macOS local storage (#104)
- `clear()` method on `LocalConfigValueProvider` interface (#101)
- Graceful error handling when a provider fails (#100)
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Release build optimization](#release-build-optimization)
- [iOS integration](#ios-integration)
- [Multi-module setup](#multi-module-setup)
- [Configuration cache](#configuration-cache)
- [API reference](#api-reference)

---
Expand Down Expand Up @@ -499,6 +500,42 @@ Declare a single shared `ConfigValues` in your app module and inject it into fea

---

## Configuration cache

`featured-gradle-plugin` officially supports the Gradle [Configuration Cache](https://docs.gradle.org/current/userguide/configuration_cache.html) on **Gradle 9+** and **AGP 9+**. Every task registered by the plugin (`resolveFeatureFlags`, `generateProguardRules`, `generateConfigParam`, `generateFlagRegistrar`, `generateIosConstVal`, `generateXcconfig`) stores and reuses CC entries without violations.

### Enabling

Add the following to `gradle.properties`:

```properties
org.gradle.configuration-cache=true
```

### Known gap — AGP 9.x `proguardFiles` provider propagation

AGP 9.x exposes `variant.proguardFiles` as a `ListProperty<RegularFile>`, but on the AGP releases verified during the 1.0.0-Beta cycle (9.1.0) the provider's dependency does **not** propagate to the underlying R8 / minification tasks. As a result, wiring the plugin's generated `proguard-featured.pro` purely through `variant.proguardFiles.add(...)` is insufficient — the R8 task will not see the file as an input dependency and will run before the rules are generated.

`featured-gradle-plugin` retains a `tasks.configureEach { … }` fallback inside [`AndroidProguardWiring.kt`](featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/AndroidProguardWiring.kt) that explicitly establishes the task dependency. The fallback is CC-safe (no `Project` reference at execution time, no eager configuration). It will be revisited on every AGP minor and removed when the upstream provider propagation gap is fixed.

Audit artefact: [`docs/cc-verification/agp-propagation-check-2026-05-16.md`](docs/cc-verification/agp-propagation-check-2026-05-16.md).
Comment on lines +519 to +521

### Upstream limitations

No known upstream Configuration Cache limitations attributable to third-party plugins were observed at time of release across the sample modules (`:sample:android-app`, `:sample:desktop`, `:sample:shared`).

### Verification artefacts

All verification artefacts live under `docs/cc-verification/`:

- [`fixture-report-2026-05-17.md`](docs/cc-verification/fixture-report-2026-05-17.md) — plugin test fixture audit (AC-3).
- [`sample-report-2026-05-17.md`](docs/cc-verification/sample-report-2026-05-17.md) — sample modules audit (AC-4).
- [`agp-propagation-check-2026-05-16.md`](docs/cc-verification/agp-propagation-check-2026-05-16.md) — AGP `proguardFiles` provider propagation audit (AC-5a).

Isolated-projects support is tracked separately — see [`docs/known-limitations.md`](docs/known-limitations.md).

---

## Running the sample app

The `sample` module is a Kotlin Multiplatform app (Android + iOS + Desktop) that demonstrates
Expand Down
216 changes: 216 additions & 0 deletions docs/cc-verification/agp-propagation-check-2026-05-16.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# AC-5a — AGP `variant.proguardFiles` propagation check (Featured plugin)

Date: 2026-05-17
Spec: `docs/specs/2026-05-16-gradle-plugin-cc-support.md` (AC-5a, AC-5b)
Plugin under test: `featured-gradle-plugin` @ branch `feat/cc-support`
Gradle: 9.4.1 (project wrapper)
AGP: 9.1.0 (fixture `build.gradle.kts`)

---

## Question

Does AGP 9.1's `Variant.proguardFiles.add(Provider<RegularFile>)` propagate the
file's producing task as an implicit Gradle dependency of the R8 minify task
(`:minify<Variant>WithR8`), and does it propagate the file itself as an R8
configuration input?

The `featured-gradle-plugin` plugin needs both — without (a) the task dependency,
R8 may run before the rules are generated; without (b) the file actually reaching
R8, our generated `-assumevalues` rules have no effect.

`AndroidProguardWiring.kt` currently wires the provider via the Variant API
(line 23–27) AND adds a defensive `tasks.configureEach { dependsOn(proguardTask) }`
fallback (line 30–34). AC-5a asks whether the fallback is still necessary on
AGP 9.1.

---

## Preflight 1 — provider exposure shape

The call site:

```kotlin
// AndroidProguardWiring.kt:23–27
androidComponents.onVariants { variant ->
variant.proguardFiles.add(
proguardTask.flatMap { it.outputFile },
)
}
```

`proguardTask` is `TaskProvider<GenerateProguardRulesTask>`, registered in
`FeaturedPlugin.kt:104–115`. The value passed to `variant.proguardFiles.add(...)`
is `TaskProvider.flatMap(...) -> Provider<RegularFile>`. This is the documented
Gradle-friendly shape: the provider carries the producing task identity, so AGP
*could* wire an implicit `Task.dependsOn` from any task that resolves the
provider.

**Preflight 1 result:** PASS — exposure shape is `flatMap` over `TaskProvider`,
not a raw `Provider<RegularFile>`. No fix to `FeaturedPlugin.kt` is needed.
Proceed to Preflight 2 and the experiment.

---

## Preflight 2 — fixture R8 re-validation

Ran `:featured-gradle-plugin:test --tests "...FeaturedPluginIntegrationTest"` end
to end. `assembleRelease` succeeded on the fixture under
`src/test/fixtures/android-project/`. Inspecting the fixture build dir after the
AC-5a experiment run confirmed:

```
build/outputs/mapping/release/mapping.txt present
build/outputs/mapping/release/seeds.txt present
build/outputs/mapping/release/usage.txt present
build/outputs/mapping/release/configuration.txt present
```

`mapping.txt` presence is the canonical signal that R8 actually ran. Fixture has
`isMinifyEnabled = true` and a non-empty `proguardFiles(...)` configuration on
the release build type, so R8 is invoked.

**Preflight 2 result:** PASS — R8 runs on the fixture as expected.

---

## Experiment

### Setup

Temporary, non-committed mutations applied to working tree, then reverted via
targeted edits (working tree returned to clean state after capturing artifacts):

1. `AndroidProguardWiring.kt` lines 30–34 (`tasks.configureEach { … dependsOn }`
fallback) removed — replaced with a comment marker.
2. `ProguardRulesGenerator.generate(...)` modified to emit an extra header line:
`# featured-cc-marker: AC5A-EXPERIMENT-MARKER-c0ffee-2026-05-17`.
3. `FeaturedPluginIntegrationTest` extended with a dedicated experiment test
`AC5A experiment - assembleRelease with fallback removed and marker injected`
that runs `assembleRelease` in a fixed scratch directory
(`featured-gradle-plugin/build/ac5a-experiment-fixture`) and persists the
executed task graph and full `--info` log for inspection.

### Command

```
./gradlew :featured-gradle-plugin:test \
--tests "dev.androidbroadcast.featured.gradle.FeaturedPluginIntegrationTest.AC5A experiment - assembleRelease with fallback removed and marker injected"
```

The experiment test internally invoked TestKit with:

```
assembleRelease --configuration-cache --info --stacktrace
```

Build result: `BUILD SUCCESSFUL in 30s`.

### Assertion 1 — task-graph ordering

From the captured `ac5a-task-graph.txt`, the relevant slice is:

```
:resolveFeatureFlags SUCCESS (position 2)
:extractProguardFiles SUCCESS (position 5)
:generateProguardRules SUCCESS (position 23)
:mergeReleaseGeneratedProguardFiles SUCCESS (position 46)
:minifyReleaseWithR8 SUCCESS (position 47)
```

`:generateProguardRules` executed BEFORE `:mergeReleaseGeneratedProguardFiles`
and `:minifyReleaseWithR8`. With the fallback `dependsOn` removed, the task
still ran in time. Two plausible explanations: (a) some other path
(e.g. `wireToRootAggregator → scanAllLocalFlags`) realized the task; (b) AGP did
propagate the task dependency via `variant.proguardFiles`. Assertion 1 alone is
INSUFFICIENT to discriminate — the spec explicitly requires Assertion 2 also.

**Assertion 1 result:** PASS (task-graph order is correct).

### Assertion 2 — marker-string presence in R8 input

The marker string `featured-cc-marker: AC5A-EXPERIMENT-MARKER-c0ffee-2026-05-17`
was confirmed present in the plugin's output file:

```
build/featured/proguard-featured.pro:
# featured-cc-marker: AC5A-EXPERIMENT-MARKER-c0ffee-2026-05-17
```

R8's own record of every configuration file it consumed is the merged
`configuration.txt` artifact:

```
build/outputs/mapping/release/configuration.txt
```

It is structured as concatenated sections, each prefixed with
`# The proguard configuration file for the following section is <path>`.
`grep -F "featured-cc-marker"` against this file: **0 matches**. The only
sections present are AGP's own `aapt_rules.txt` and the default
`proguard-android-optimize.txt-9.1.0`. No section for
`build/featured/proguard-featured.pro` appears.

Cross-check on the merge-task output directory:

```
build/intermediates/generated_proguard_file/release/mergeReleaseGeneratedProguardFiles/
(empty — no files produced)
```

The empty merge output corroborates the absence of our file in
`configuration.txt`: AGP's `mergeReleaseGeneratedProguardFiles` task — which is
the documented sink for `variant.proguardFiles` additions — emitted nothing.

Cross-check on the `--info` log: `grep` for `proguard-featured` returned exactly
one hit, the plugin's own lifecycle log line announcing generation. No line
showing R8 read the file.

**Assertion 2 result:** FAIL — the marker did not appear in any
AGP-observable R8 input location. R8 did not consume our generated rules.

### Combined verdict

Assertion 1 PASS but Assertion 2 FAIL. The spec demands BOTH for a positive
conclusion. Therefore: **propagation does NOT work on AGP 9.1**.

A plausible interpretation, consistent with the empty
`mergeReleaseGeneratedProguardFiles` output and the missing section in
`configuration.txt`: `Variant.proguardFiles.add(Provider<RegularFile>)` on
AGP 9.1 may register the provider for some downstream consumer (build features /
plugin metadata) but does NOT register it as an input of the
`mergeReleaseGeneratedProguardFiles` → R8 pipeline. The exact AGP-internal reason
is out of scope for this audit; the observable behaviour is sufficient to make
the AC-5b call.

---

## Outcome

**Propagation does NOT work on AGP 9.1.** The `tasks.configureEach { …
dependsOn(proguardTask) }` fallback in `AndroidProguardWiring.kt:30–34` is
LOAD-BEARING — removing it produces a green build whose R8 invocation silently
ignores our `-assumevalues` rules.

Per spec AC-5b: **keep fallback unchanged**, and document the AGP 9.x gap in the
README "Configuration cache" section (AC-6).

Comment on lines +190 to +197
Note on the resolution path forbidden by the spec: narrowing to
`tasks.withType<R8Task>` is explicitly OUT OF SCOPE — `R8Task` lives under
`com.android.build.gradle.internal.tasks`, which is excluded from AGP's public
API contract. The current name-pattern fallback (`minify*WithR8`) is the
defensible boundary for this plugin until AGP exposes a public hook.

A separate follow-up — to revisit propagation on each AGP minor and replace the
fallback with a Variant-API-only wiring as soon as AGP fixes the gap — is
captured in the README workaround note. No GitHub issue is required by this spec
for the AGP-side gap (the spec asked only for AC-8 isolated-projects issue).

## Reproducibility

Working-tree mutations used during the experiment have been reverted; the
captured artifacts (`ac5a-task-graph.txt`, `ac5a-info.log`, intermediate
inspection) live under `featured-gradle-plugin/build/ac5a-experiment-fixture/`
in the local build directory (gitignored) and are not committed. To re-run the
experiment, re-apply the three mutations above and execute the listed Gradle
command — they are deterministic.
80 changes: 80 additions & 0 deletions docs/cc-verification/fixture-report-2026-05-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# AC-3 — Manual Configuration Cache verification (test fixture)

Date: 2026-05-17
Spec: `docs/specs/2026-05-16-gradle-plugin-cc-support.md` (AC-3)
Target: `featured-gradle-plugin/src/test/fixtures/android-project/`

---

## Versions

- **Gradle**: 9.4.1 (standalone, Homebrew `gradle` — output of `gradle --version`).
The spec asks for Gradle 9.1; this machine ships 9.4.1 in the 9.x line. AGP 9.1.0 requires
Gradle 9.0 minimum and is forward-compatible with 9.x — this is the closest available
standalone Gradle in the 9.x line and matches the AGP 9+ / Gradle 9+ floor stated in the spec.
- **AGP**: 9.1.0 (declared in the fixture `build.gradle.kts`).

## Procedure

The fixture under `featured-gradle-plugin/src/test/fixtures/android-project/` is wired for
TestKit (`GradleRunner.withPluginClasspath()` injects the plugin); a standalone Gradle
invocation has no plugin classpath injection, so the fixture was copied to a scratch
directory and adjusted to resolve the plugin from the local Maven repository.

1. `./gradlew :featured-gradle-plugin:publishToMavenLocal -x signMavenPublication` publishes
the plugin under coordinates `dev.androidbroadcast.featured:featured-gradle-plugin:0.1.0-SNAPSHOT`
and the plugin marker `dev.androidbroadcast.featured.gradle.plugin:0.1.0-SNAPSHOT` into `~/.m2`.
2. `cp -r featured-gradle-plugin/src/test/fixtures/android-project /tmp/featured-fixture-ac3`.
3. In `/tmp/featured-fixture-ac3/settings.gradle.kts`, prepend `mavenLocal()` to
`pluginManagement.repositories` and `dependencyResolutionManagement.repositories`.
4. In `/tmp/featured-fixture-ac3/build.gradle.kts`, change the Featured plugin declaration
to `id("dev.androidbroadcast.featured") version "0.1.0-SNAPSHOT"`.
5. Add `local.properties` with `sdk.dir=/Users/krozov/dev/android-sdk`.

This isolates the fixture from the parent build (no `includeBuild`, no project wrapper),
satisfying the spec's "exercised in isolation" intent.

## Command

```
cd /tmp/featured-fixture-ac3
gradle assembleRelease --configuration-cache --configuration-cache-problems=fail
```

## Result

```
BUILD SUCCESSFUL in 26s
42 actionable tasks: 42 executed
Configuration cache entry stored.
```

Configuration-cache report:

```
build/reports/configuration-cache/8lulc5p6c95dikv6updqjlgbg/388cxpk2r9ch33znubyiiu37k/configuration-cache-report.html
```

Report header field `"totalProblemCount":0`.

## Total violation count

**0**

## Per-violation table

| Source plugin | Violation category | Upstream issue link |
|---|---|---|
| _none_ | _none_ | _n/a_ |

## Conclusion

AC-3 **passes**. `assembleRelease --configuration-cache --configuration-cache-problems=fail`
completed successfully against the test fixture with zero Configuration Cache violations.
The plugin's six tasks (`resolveFeatureFlags`, `generateProguardRules`,
`generateConfigParam`, `generateFlagRegistrar`, `generateIosConstVal`, `generateXcconfig`)
participated in the build without any CC-attributable issues. The `--problems=fail` flag
would have aborted the build on any single violation; since the build succeeded, no
violations attributable to `featured-gradle-plugin` (or any other source) were emitted.

Raw HTML report is NOT committed per spec Technical Constraints — Markdown summary only.
Loading
Loading