Fix generated ProGuard rules not reaching library consumers - #291
Draft
SalvatoreT wants to merge 1 commit into
Draft
Fix generated ProGuard rules not reaching library consumers#291SalvatoreT wants to merge 1 commit into
SalvatoreT wants to merge 1 commit into
Conversation
## 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
addProguardFilesregistered the generated rule file on each build type, but it interleaved that DSL mutation withproject.tasks.withType<T> { ... }calls. The Kotlin DSLwithTypeoverload taking an action is the eagerDomainObjectCollection.withType(Class, Action), so every call realizes all matching tasks immediately. RealizingAndroidLintAnalysisTaskorMergeConsumerProguardFilesTaskreadsOptimizationCreationConfig.consumerProguardFiles, and theLazybehind that property callsOptimizationDslInfo.gatherProguardFiles(CONSUMER), freezing the gathered list.consumerProguardFile()ran after thosewithTypeblocks, so it added the file to a list nothing reads again.The surrounding
buildTypes.configureEachmade it worse. EachwithTyperealizes tasks for every variant, so thedebugiteration frozerelease's list beforereleasewas mutated at all. Release AARs lost the rules almost every time, and consumers got noproguard.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
addProguardFilesruns 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 mirroraddMainJniDir, 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-widegenerateUniffiProguardRulestask, 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
releaseandreleaseCandidate, the latter's tasks matched both iterations.buildType.proguardFilefor applications already ran ahead of thewithTypecalls and was never affected. It keeps that position.Testing
Checked with
:tests:uniffi:coverall-android, temporarily reordering itsplugins {}block to applycom.android.librarybefore the Gobley plugins so it matches the affected setup:proguard.txtat allproguard.txtcarries the generated blocklintReleasesucceeds under both plugin orders, withgenerateUniffiProguardRulesin the task graph. That covers theAndroidLintAnalysisTaskandLintModelWriterTaskwiring that lost its name filters.apiCheckpasses on all four plugin modules, since the delegate is@InternalGobleyGradleApiand 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, onlypublicmembers ofStructuresubclasses survive, and nothing keeps the generated binding package even though JNA mapsLibrarymethod names ontodlsymsymbols. Separate change.Drafted with Claude Code and reviewed by me before opening.