From 7df09212c5ba3fe549449f78249498072b6a9259 Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Mon, 15 Sep 2025 17:59:08 -0700 Subject: [PATCH 1/7] Adding normalization on invoke --- README.md | 10 ++ .../workarounds/JdkImageWorkaround.groovy | 4 + .../android/JdkImageWorkaroundTest.groovy | 99 +++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/README.md b/README.md index a877598f..14992902 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,16 @@ To work around this issue, please apply the [Room Gradle Plugin](https://develop ## Implementation Notes +### JdkImageWorkaround +This workaround addresses issues with the JdkImageInput compiler argument, introduced in Android Gradle Plugin 7.1.0 and later. The JdkImageInput argument can introduce non-determinism in the build process, leading to cache misses. + +Additionally, the workaround applies runtime classpath normalization by ignoring `metaInf` attributes and ignoring `**/java/lang/invoke/**`. The latter exclusion is primarily based on our observations when comparing builds across different architectures, and it was also discussed in the related issue https://github.com/gradle/android-cache-fix-gradle-plugin/issues/341. + +You can opt out of this normalization by adding the following property to your gradle.properties file: +``` +org.gradle.android.cache-fix.JdkImageWorkaround.normalization.enabled=false +``` + ### MergeNativeLibs, StripDebugSymbols, MergeJavaResources, MergeSourceSetFolders, BundleLibraryClassesJar, DataBindingMergeDependencyArtifacts, LibraryJniLibs and ZipMerging Workarounds It has been observed that caching the `MergeNativeLibsTask`, `StripDebugSymbols`, `MergeSourceSetFolders`, `BundleLibraryClassesJar`, `DataBindingMergeDependencyArtifacts`, `LibraryJniLibs` and `ZipMergingTask` tasks rarely provide any significant positive avoidance savings. In fact, they frequently provide negative savings, especially when fetched from a remote cache node. As such, these workarounds disable caching for these tasks. diff --git a/src/main/groovy/org/gradle/android/workarounds/JdkImageWorkaround.groovy b/src/main/groovy/org/gradle/android/workarounds/JdkImageWorkaround.groovy index 4a41f337..ddf54f8a 100644 --- a/src/main/groovy/org/gradle/android/workarounds/JdkImageWorkaround.groovy +++ b/src/main/groovy/org/gradle/android/workarounds/JdkImageWorkaround.groovy @@ -47,6 +47,7 @@ import java.util.stream.Stream @AndroidIssue(introducedIn = "7.1.0", link = "https://issuetracker.google.com/issues/267213045") class JdkImageWorkaround implements Workaround { static final String WORKAROUND_ENABLED_PROPERTY = "org.gradle.android.cache-fix.JdkImageWorkaround.enabled" + static final String WORKAROUND_INVOKE_NORMALIZATION_PROPERTY = "org.gradle.android.cache-fix.JdkImageWorkaround.normalization.enabled" static final String JDK_IMAGE = "_internal_android_jdk_image" static final String JDK_IMAGE_EXTRACTED = "_internal_android_jdk_image_extracted" @@ -90,6 +91,9 @@ class JdkImageWorkaround implements Workaround { static def applyRuntimeClasspathNormalization(Project project) { project.normalization { handler -> handler.runtimeClasspath { + if (SystemPropertiesCompat.getBoolean(WORKAROUND_INVOKE_NORMALIZATION_PROPERTY, project, true)) { + it.ignore '**/java/lang/invoke/**' + } it.metaInf { metaInfNormalization -> metaInfNormalization.ignoreAttribute('Implementation-Version') metaInfNormalization.ignoreAttribute('Implementation-Vendor') diff --git a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy index f88ab426..67e89c1d 100644 --- a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy +++ b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy @@ -308,4 +308,103 @@ class JdkImageWorkaroundTest extends AbstractTest { where: androidVersion << TestVersions.latestAndroidVersions } + + def "Invoke normalization property is enabled and normalizes **/java/lang/invoke/**"() { + def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() + def gradleVersion = TestVersions.latestSupportedGradleVersionFor(androidVersion) + Assume.assumeTrue(androidVersion >= VersionNumber.parse("7.1.0-alpha01")) + SimpleAndroidApp.builder(temporaryFolder.root, cacheDir) + .withAndroidVersion(androidVersion) + .withKotlinDisabled() + .withDatabindingDisabled() + .build() + .writeProject() + createLibJavaClass("firstBuild") + + when: + BuildResult buildResult = withGradleVersion(gradleVersion.version) + .withProjectDir(temporaryFolder.root) + .withArguments( + "clean", "test", "assemble", + "--build-cache" + ).build() + then: + buildResult.task(':app:compileDebugJavaWithJavac').outcome == TaskOutcome.SUCCESS + buildResult.task(':library:compileDebugJavaWithJavac').outcome == TaskOutcome.SUCCESS + + buildResult.task(':app:compileDebugUnitTestJavaWithJavac').outcome == TaskOutcome.SUCCESS + buildResult.task(':library:compileDebugUnitTestJavaWithJavac').outcome == TaskOutcome.SUCCESS + + when: + createLibJavaClass("secondBuild") + buildResult = withGradleVersion(gradleVersion.version) + .withProjectDir(temporaryFolder.root) + .withArguments( + "clean", "test", "assemble", + "--build-cache" + ).build() + + then: + buildResult.task(':app:testDebugUnitTest').outcome == TaskOutcome.FROM_CACHE + } + + def "Invoke normalization property is disable and doesn't normalize **/java/lang/invoke/**"() { + def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() + def gradleVersion = TestVersions.latestSupportedGradleVersionFor(androidVersion) + Assume.assumeTrue(androidVersion >= VersionNumber.parse("7.1.0-alpha01")) + SimpleAndroidApp.builder(temporaryFolder.root, cacheDir) + .withAndroidVersion(androidVersion) + .withKotlinDisabled() + .withDatabindingDisabled() + .build() + .writeProject() + createLibJavaClass("firstBuild") + + when: + BuildResult buildResult = withGradleVersion(gradleVersion.version) + .withProjectDir(temporaryFolder.root) + .withArguments( + "clean", "test", "assemble", + "--build-cache", + "-D${JdkImageWorkaround.WORKAROUND_INVOKE_NORMALIZATION_PROPERTY}=false" + ).build() + then: + buildResult.task(':app:compileDebugJavaWithJavac').outcome == TaskOutcome.SUCCESS + buildResult.task(':library:compileDebugJavaWithJavac').outcome == TaskOutcome.SUCCESS + + buildResult.task(':app:compileDebugUnitTestJavaWithJavac').outcome == TaskOutcome.SUCCESS + buildResult.task(':library:compileDebugUnitTestJavaWithJavac').outcome == TaskOutcome.SUCCESS + + when: + createLibJavaClass("secondBuild") + + buildResult = withGradleVersion(gradleVersion.version) + .withProjectDir(temporaryFolder.root) + .withArguments( + "clean", "test", "assemble", + "--build-cache", + "-D${JdkImageWorkaround.WORKAROUND_INVOKE_NORMALIZATION_PROPERTY}=false" + ).build() + + then: + buildResult.task(':app:testDebugUnitTest').outcome == TaskOutcome.SUCCESS + } + + def createLibJavaClass(String label) { + def barClass = file("library/src/main/java/com/foo/java/lang/invoke/Bar.java") + if (barClass.exists()) { + barClass.delete() + } + barClass.parentFile.mkdirs() + barClass << """ + + package com.foo.java.lang.invoke; + + public class Bar { + public static String bar() { + return "$label"; + } + } + """ + } } From 4aef51d442bd80f4acf44164545d3174f6108a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Villar?= Date: Mon, 15 Sep 2025 18:04:42 -0700 Subject: [PATCH 2/7] Update JdkImageWorkaroundTest.groovy --- .../groovy/org/gradle/android/JdkImageWorkaroundTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy index 67e89c1d..e4e5ca36 100644 --- a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy +++ b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy @@ -348,7 +348,7 @@ class JdkImageWorkaroundTest extends AbstractTest { buildResult.task(':app:testDebugUnitTest').outcome == TaskOutcome.FROM_CACHE } - def "Invoke normalization property is disable and doesn't normalize **/java/lang/invoke/**"() { + def "Invoke normalization property is disabled and doesn't normalize **/java/lang/invoke/**"() { def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() def gradleVersion = TestVersions.latestSupportedGradleVersionFor(androidVersion) Assume.assumeTrue(androidVersion >= VersionNumber.parse("7.1.0-alpha01")) From cde0b778c2b2cdb07a264600d471cfc6ce7bf342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Villar?= Date: Thu, 18 Sep 2025 15:08:31 -0700 Subject: [PATCH 3/7] Update src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gabriel Féo --- .../groovy/org/gradle/android/JdkImageWorkaroundTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy index e4e5ca36..93fa77c0 100644 --- a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy +++ b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy @@ -309,7 +309,7 @@ class JdkImageWorkaroundTest extends AbstractTest { androidVersion << TestVersions.latestAndroidVersions } - def "Invoke normalization property is enabled and normalizes **/java/lang/invoke/**"() { + def "**/java/lang/invoke/** is normalized by default"() { def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() def gradleVersion = TestVersions.latestSupportedGradleVersionFor(androidVersion) Assume.assumeTrue(androidVersion >= VersionNumber.parse("7.1.0-alpha01")) From a711003efc84e18ab1f3870d39a0db5c2bcbb4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Villar?= Date: Thu, 18 Sep 2025 15:08:40 -0700 Subject: [PATCH 4/7] Update src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gabriel Féo --- .../groovy/org/gradle/android/JdkImageWorkaroundTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy index 93fa77c0..576acf67 100644 --- a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy +++ b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy @@ -348,7 +348,7 @@ class JdkImageWorkaroundTest extends AbstractTest { buildResult.task(':app:testDebugUnitTest').outcome == TaskOutcome.FROM_CACHE } - def "Invoke normalization property is disabled and doesn't normalize **/java/lang/invoke/**"() { + def "**/java/lang/invoke/** is not normalized when normalization property is false"() { def androidVersion = TestVersions.latestAndroidVersionForCurrentJDK() def gradleVersion = TestVersions.latestSupportedGradleVersionFor(androidVersion) Assume.assumeTrue(androidVersion >= VersionNumber.parse("7.1.0-alpha01")) From a5011ae5e53540664f902e63f8dae3451a0ea03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Villar?= Date: Thu, 18 Sep 2025 15:09:57 -0700 Subject: [PATCH 5/7] Update src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gabriel Féo --- .../android/JdkImageWorkaroundTest.groovy | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy index 576acf67..2ef39d4c 100644 --- a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy +++ b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy @@ -391,20 +391,17 @@ class JdkImageWorkaroundTest extends AbstractTest { } def createLibJavaClass(String label) { - def barClass = file("library/src/main/java/com/foo/java/lang/invoke/Bar.java") - if (barClass.exists()) { - barClass.delete() - } - barClass.parentFile.mkdirs() - barClass << """ - - package com.foo.java.lang.invoke; - - public class Bar { - public static String bar() { - return "$label"; - } + file("library/src/main/java/com/foo/java/lang/invoke/Bar.java") + .createParentDirectories() + .newWriter { w -> + w << """ + package com.foo.java.lang.invoke; + public class Bar { + public static String bar() { + return "$label"; + } + } + """ } - """ } } From e0ec586071244823957bd330e44e6266bb9f2bfa Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Fri, 19 Sep 2025 09:24:07 -0700 Subject: [PATCH 6/7] using correct method --- .../groovy/org/gradle/android/JdkImageWorkaroundTest.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy index 2ef39d4c..89d8296e 100644 --- a/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy +++ b/src/test/groovy/org/gradle/android/JdkImageWorkaroundTest.groovy @@ -393,7 +393,7 @@ class JdkImageWorkaroundTest extends AbstractTest { def createLibJavaClass(String label) { file("library/src/main/java/com/foo/java/lang/invoke/Bar.java") .createParentDirectories() - .newWriter { w -> + .withWriter { w -> w << """ package com.foo.java.lang.invoke; public class Bar { From 1a01118114a5d1258a51ccb00001d9ffcd167e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Villar?= Date: Fri, 19 Sep 2025 17:15:58 -0700 Subject: [PATCH 7/7] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gabriel Féo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14992902..4265bdbb 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Additionally, the workaround applies runtime classpath normalization by ignoring You can opt out of this normalization by adding the following property to your gradle.properties file: ``` -org.gradle.android.cache-fix.JdkImageWorkaround.normalization.enabled=false +systemProp.org.gradle.android.cache-fix.JdkImageWorkaround.normalization.enabled=false ``` ### MergeNativeLibs, StripDebugSymbols, MergeJavaResources, MergeSourceSetFolders, BundleLibraryClassesJar, DataBindingMergeDependencyArtifacts, LibraryJniLibs and ZipMerging Workarounds