Skip to content

Fix generated ProGuard rules not reaching library consumers - #291

Draft
SalvatoreT wants to merge 1 commit into
mainfrom
salvatoret/fix/uniffi-consumer-proguard-rules
Draft

Fix generated ProGuard rules not reaching library consumers#291
SalvatoreT wants to merge 1 commit into
mainfrom
salvatoret/fix/uniffi-consumer-proguard-rules

Conversation

@SalvatoreT

Copy link
Copy Markdown
Contributor

Changes

addProguardFiles registered the generated rule file on each build type, but it interleaved that DSL mutation with project.tasks.withType<T> { ... } calls. The Kotlin DSL withType overload taking an action is the eager DomainObjectCollection.withType(Class, Action), so every call realizes all matching tasks immediately. Realizing AndroidLintAnalysisTask or MergeConsumerProguardFilesTask reads OptimizationCreationConfig.consumerProguardFiles, and the Lazy behind that property calls OptimizationDslInfo.gatherProguardFiles(CONSUMER), freezing the gathered list. consumerProguardFile() ran after those withType blocks, so it added the file to a list nothing reads again.

The surrounding buildTypes.configureEach made it worse. Each withType realizes tasks for every variant, so the debug iteration froze release's list before release was mutated at all. Release AARs lost the rules almost every time, and consumers got no proguard.txt.

This only happens when the Android Gradle Plugin is applied before the Gobley plugins. Every test and example here applies it after, so the variant tasks do not exist yet when addProguardFiles runs and there is nothing to realize. That is why CI never caught it.

The fix mutates every build type first, then wires the task dependencies through the lazy withType<T>().configureEach { }.

The name.lowercase().contains(buildType.name.lowercase()) filters go away with the per-build-type helper that required them. They mirror addMainJniDir, where the same shape earns its keep: that method runs once per variant with a different JNI task and directory each time, so each call must wire only its own variant's task. Here the wiring targets a single project-wide generateUniffiProguardRules task, which leaves the per-iteration scoping nothing to separate. The union over the loop is just "every matching task depends on it", which hoisting says directly.

For every real AGP variant task name that union is the same graph. Otherwise it is a superset: a matching task whose name contains no build type name used to get no dependency and now gets one, which can only remove a missing-input race. The generation task has no dependencies of its own, so it cannot create a cycle. The substring match was never strict scoping anyway. With build types release and releaseCandidate, the latter's tasks matched both iterations.

buildType.proguardFile for applications already ran ahead of the withType calls and was never affected. It keeps that position.

Testing

Checked with :tests:uniffi:coverall-android, temporarily reordering its plugins {} block to apply com.android.library before the Gobley plugins so it matches the affected setup:

  • under that order without the fix, the AAR had no proguard.txt at all
  • with the fix, proguard.txt carries the generated block
  • under the original plugin order the AAR is unchanged, still carrying the rules

lintRelease succeeds under both plugin orders, with generateUniffiProguardRules in the task graph. That covers the AndroidLintAnalysisTask and LintModelWriterTask wiring that lost its name filters. apiCheck passes on all four plugin modules, since the delegate is @InternalGobleyGradleApi and stays out of the dumps. :examples:tokio-blake3-app, an Android application using the UniFFI plugin, still configures.

Not covered here: no R8 run against a real consuming app, and no APK build for the application path. The AAR contents stand in for the former.

Notes

Two follow-ups this deliberately leaves alone.

No test covers the configuration that triggers this, because every test and example applies AGP after the Gobley plugins. Reordering one existing Android library test project's plugins {} block would close that gap in a line, at the cost of that project no longer covering the other ordering.

The generated rule set is also narrower than a UniFFI binding needs. com.sun.jna.* misses subpackages, only public members of Structure subclasses survive, and nothing keeps the generated binding package even though JNA maps Library method names onto dlsym symbols. Separate change.

Drafted with Claude Code and reviewed by me before opening.

## Changes

`addProguardFiles` registered the generated rule file on each build type, but it
interleaved that DSL mutation with `project.tasks.withType<T> { ... }` calls. The
Kotlin DSL `withType` overload taking an action is the eager
`DomainObjectCollection.withType(Class, Action)`, so every call realizes all
matching tasks immediately. Realizing `AndroidLintAnalysisTask` or
`MergeConsumerProguardFilesTask` reads
`OptimizationCreationConfig.consumerProguardFiles`, and the `Lazy` behind that
property calls `OptimizationDslInfo.gatherProguardFiles(CONSUMER)`, freezing the
gathered list. `consumerProguardFile()` ran after those `withType` blocks, so it
added the file to a list nothing reads again.

The surrounding `buildTypes.configureEach` made it worse. Each `withType`
realizes tasks for every variant, so the `debug` iteration froze `release`'s list
before `release` was mutated at all. Release AARs lost the rules almost every
time, and consumers got no `proguard.txt`.

This only happens when the Android Gradle Plugin is applied before the Gobley
plugins. Every test and example here applies it after, so the variant tasks do
not exist yet when `addProguardFiles` runs and there is nothing to realize. That
is why CI never caught it.

The fix mutates every build type first, then wires the task dependencies through
the lazy `withType<T>().configureEach { }`.

The `name.lowercase().contains(buildType.name.lowercase())` filters go away with
the per-build-type helper that required them. They mirror `addMainJniDir`, where
the same shape earns its keep: that method runs once per variant with a different
JNI task and directory each time, so each call must wire only its own variant's
task. Here the wiring targets a single project-wide `generateUniffiProguardRules`
task, which leaves the per-iteration scoping nothing to separate. The union over
the loop is just "every matching task depends on it", which hoisting says
directly.

For every real AGP variant task name that union is the same graph. Otherwise it
is a superset: a matching task whose name contains no build type name used to get
no dependency and now gets one, which can only remove a missing-input race. The
generation task has no dependencies of its own, so it cannot create a cycle. The
substring match was never strict scoping anyway. With build types `release` and
`releaseCandidate`, the latter's tasks matched both iterations.

`buildType.proguardFile` for applications already ran ahead of the `withType`
calls and was never affected. It keeps that position.

## Testing

Checked with `:tests:uniffi:coverall-android`, temporarily reordering its
`plugins {}` block to apply `com.android.library` before the Gobley plugins so it
matches the affected setup:

- under that order without the fix, the AAR had no `proguard.txt` at all
- with the fix, `proguard.txt` carries the generated block
- under the original plugin order the AAR is unchanged, still carrying the rules

`lintRelease` succeeds under both plugin orders, with
`generateUniffiProguardRules` in the task graph. That covers the
`AndroidLintAnalysisTask` and `LintModelWriterTask` wiring that lost its name
filters. `apiCheck` passes on all four plugin modules, since the delegate is
`@InternalGobleyGradleApi` and stays out of the dumps.
`:examples:tokio-blake3-app`, an Android application using the UniFFI plugin,
still configures.
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