From 8e04c7c597f49ea33a8e2aed643c73da2ff919d4 Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 19 May 2026 15:58:21 -0600 Subject: [PATCH 1/7] Crashlytics: keep release mapping file id stable across builds Reuse the on-disk id when present and valid for the current mode; only generate a new UUID on first build, after clean, or when the mode flips. This lets the task report UP_TO_DATE and stops invalidating downstream release tasks (mergeResources, R8, packaging, bundling) on every build. Fixes #6770 --- .../gradle/tasks/InjectMappingFileIdTask.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt b/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt index 7b155c61f66..207c88f73c6 100644 --- a/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt +++ b/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt @@ -56,7 +56,11 @@ abstract class InjectMappingFileIdTask : DefaultTask() { if (useBlankMappingFileId.get()) { CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID } else { - CrashlyticsBuildtools.generateMappingFileId() + // Reuse the on-disk id when valid so release builds stay reproducible and don't invalidate + // downstream tasks (mergeResources, R8, packaging, bundling). A fresh UUID is only minted + // on the first build or after `clean`. + existingMappingFileId()?.takeUnless { it == CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID } + ?: CrashlyticsBuildtools.generateMappingFileId() } mappingFileIdFile.get().asFile.writeText(mappingFileId) @@ -67,12 +71,14 @@ abstract class InjectMappingFileIdTask : DefaultTask() { } /** - * Check if a mapping file id file already exists, and that the mapping file id is blank - meaning - * no obfuscation is enabled. The Crashlytics SDK always needs a mapping file id. + * Returns the mapping file id currently stored on disk, or null if the file is missing or empty. + * Used to keep the task up-to-date across builds and to avoid generating a new random id on + * every release build. */ - private fun blankMappingFileIdExists(): Boolean { + private fun existingMappingFileId(): String? { val file: File = mappingFileIdFile.get().asFile - return file.exists() && file.readText() == CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID + if (!file.exists()) return null + return file.readText().trim().ifEmpty { null } } internal companion object { @@ -91,7 +97,14 @@ abstract class InjectMappingFileIdTask : DefaultTask() { ) this.mappingFileIdFile.set(buildFile(project, variant, "mappingFileId.txt")) - outputs.upToDateWhen { useBlankMappingFileId.get() && blankMappingFileIdExists() } + outputs.upToDateWhen { + val existing = existingMappingFileId() + if (useBlankMappingFileId.get()) { + existing == CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID + } else { + existing != null && existing != CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID + } + } } // It is not possible to disable Android resources in the AGP app plugin. From 6520b465da8c91e8cb8045016bcb6f777d5ef1f9 Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 19 May 2026 15:58:26 -0600 Subject: [PATCH 2/7] Crashlytics: functional tests for stable mapping file id behavior Covers: UP_TO_DATE on second run, resource preserved across rebuilds, re-run after clean, mode-toggle invalidates the task. --- .../gradle/TypicalAppFunctionalTests.kt | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt b/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt index 89ce22ad36c..fdfe8f87222 100644 --- a/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt +++ b/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt @@ -21,6 +21,7 @@ import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPluginTest.C import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsPluginTest.Companion.pluginVersion import java.io.File import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE import org.gradle.testkit.runner.UnexpectedBuildFailure import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -217,6 +218,132 @@ class TypicalAppFunctionalTests { ) } + @Test + fun `injectCrashlyticsMappingFileIdRelease is UP_TO_DATE on second invocation`() { + val first = + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + assertThat(first.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + + val second = + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + assertThat(second.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(UP_TO_DATE) + } + + @Test + fun `release mapping file id resource is preserved across rebuilds`() { + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") + val resourceFile = + File( + projectDir, + "build/generated/res/injectCrashlyticsMappingFileIdRelease/values/" + + "com_google_firebase_crashlytics_mappingfileid.xml", + ) + + val idBefore = idFile.readText() + val resourceBefore = resourceFile.readText() + + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + assertThat(idFile.readText()).isEqualTo(idBefore) + assertThat(resourceFile.readText()).isEqualTo(resourceBefore) + } + + @Test + fun `task re-runs after clean`() { + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") + + val rerun = + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments( + ":clean", + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache", + ) + .build() + + assertThat(rerun.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + assertThat(idFile.exists()).isTrue() + assertThat(idFile.readText()).isNotEmpty() + } + + @Test + fun `toggling mappingFileUploadEnabled invalidates the task`() { + val first = + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + assertThat(first.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") + assertThat(idFile.readText()).isEqualTo("test321") + + buildFile.writeText( + """ + import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension + + plugins { + id("com.android.application") version "8.1.4" + id("com.google.gms.google-services") version "4.4.1" + id("com.google.firebase.crashlytics") version "$pluginVersion" + } + + android { + compileSdk = 33 + namespace = "com.google.firebase.testing.crashlytics" + + buildTypes { + release { + configure { + mappingFileUploadEnabled = false + } + isMinifyEnabled = true + } + } + } + """ + ) + + val second = + GradleRunner.create() + .withGradleVersion("8.1") + .withProjectDir(projectDir) + .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + .build() + + assertThat(second.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + assertThat(idFile.readText()).isEqualTo("00000000000000000000000000000000") + } + @BeforeEach fun setup() { settingsFile = File(projectDir, "settings.gradle.kts") From 6918ab91adbbba41d7f794f8879342d3b144ad54 Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 19 May 2026 19:03:22 -0600 Subject: [PATCH 3/7] Crashlytics: spotlessApply on InjectMappingFileIdTask Wrap the existingMappingFileId() KDoc to satisfy ktfmt. --- .../buildtools/gradle/tasks/InjectMappingFileIdTask.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt b/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt index 207c88f73c6..36167730f12 100644 --- a/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt +++ b/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt @@ -72,8 +72,8 @@ abstract class InjectMappingFileIdTask : DefaultTask() { /** * Returns the mapping file id currently stored on disk, or null if the file is missing or empty. - * Used to keep the task up-to-date across builds and to avoid generating a new random id on - * every release build. + * Used to keep the task up-to-date across builds and to avoid generating a new random id on every + * release build. */ private fun existingMappingFileId(): String? { val file: File = mappingFileIdFile.get().asFile From 7e1c779fdc857219b3a3b0dc96a79a06010cc406 Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 19 May 2026 19:11:32 -0600 Subject: [PATCH 4/7] Crashlytics: changelog entry for #6770 --- firebase-crashlytics-gradle/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-crashlytics-gradle/CHANGELOG.md b/firebase-crashlytics-gradle/CHANGELOG.md index 5a91821f691..a9096893492 100644 --- a/firebase-crashlytics-gradle/CHANGELOG.md +++ b/firebase-crashlytics-gradle/CHANGELOG.md @@ -1,6 +1,7 @@ ### Unreleased - [fixed] Fixed an incompatibility between Crashlytics Gradle plugin and Gradle isolated projects when enabling nativeSymbolUploadEnabled. [#8037] +- [fixed] Stopped regenerating the mapping file id on every minified release build. `injectCrashlyticsMappingFileId` now reuses the on-disk id when valid for the current mode, so release builds can be `UP-TO-DATE` and downstream tasks (`mergeResources`, `processResources`, R8, packaging) are no longer invalidated on every build. A new id is still generated on the first build, after `clean`, or when `mappingFileUploadEnabled` is toggled. [#6770] ### Crashlytics Gradle plugin version 3.0.7 From e7499f3e3d1a5b3afc0343aab953c95d46291435 Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 26 May 2026 20:54:00 -0600 Subject: [PATCH 5/7] Crashlytics: content-driven mapping file id Source-file fingerprint drives UP_TO_DATE; id regenerates on edits. --- .../gradle/tasks/InjectMappingFileIdTask.kt | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt b/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt index 36167730f12..96457f18d48 100644 --- a/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt +++ b/firebase-crashlytics-gradle/src/main/kotlin/com/google/firebase/crashlytics/buildtools/gradle/tasks/InjectMappingFileIdTask.kt @@ -26,13 +26,17 @@ import com.google.firebase.crashlytics.buildtools.mappingfiles.MappingFileIdWrit import java.io.File import org.gradle.api.DefaultTask import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.register @@ -40,7 +44,14 @@ import org.gradle.kotlin.dsl.register /** Inject mapping file id task. */ @CacheableTask abstract class InjectMappingFileIdTask : DefaultTask() { - @get:Internal abstract val useBlankMappingFileId: Property + @get:Input abstract val useBlankMappingFileId: Property + + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val obfuscatableSources: ConfigurableFileCollection + + @get:[InputFiles PathSensitive(PathSensitivity.NONE)] + abstract val obfuscatableClasspath: ConfigurableFileCollection + @get:OutputFile abstract val mappingFileIdFile: RegularFileProperty @get:OutputDirectory abstract val resourceDir: DirectoryProperty @@ -56,11 +67,7 @@ abstract class InjectMappingFileIdTask : DefaultTask() { if (useBlankMappingFileId.get()) { CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID } else { - // Reuse the on-disk id when valid so release builds stay reproducible and don't invalidate - // downstream tasks (mergeResources, R8, packaging, bundling). A fresh UUID is only minted - // on the first build or after `clean`. - existingMappingFileId()?.takeUnless { it == CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID } - ?: CrashlyticsBuildtools.generateMappingFileId() + CrashlyticsBuildtools.generateMappingFileId() } mappingFileIdFile.get().asFile.writeText(mappingFileId) @@ -70,40 +77,44 @@ abstract class InjectMappingFileIdTask : DefaultTask() { ) } - /** - * Returns the mapping file id currently stored on disk, or null if the file is missing or empty. - * Used to keep the task up-to-date across builds and to avoid generating a new random id on every - * release build. - */ - private fun existingMappingFileId(): String? { - val file: File = mappingFileIdFile.get().asFile - if (!file.exists()) return null - return file.readText().trim().ifEmpty { null } - } - internal companion object { - @Suppress("UnstableApiUsage") // isMinifyEnabled + @Suppress("UnstableApiUsage") // isMinifyEnabled, compileClasspath fun register( project: Project, variant: ApplicationVariant, crashlyticsExtension: CrashlyticsVariantExtension, ): TaskProvider { + val useBlank = + !crashlyticsExtension.mappingFileUploadEnabled.getOrElse(variant.isMinifyEnabled) + val injectMappingFileIdTaskProvider = project.tasks.register( "injectCrashlyticsMappingFileId${variant.name.capitalized()}" ) { - this.useBlankMappingFileId.set( - !crashlyticsExtension.mappingFileUploadEnabled.getOrElse(variant.isMinifyEnabled) - ) + this.useBlankMappingFileId.set(useBlank) this.mappingFileIdFile.set(buildFile(project, variant, "mappingFileId.txt")) - outputs.upToDateWhen { - val existing = existingMappingFileId() - if (useBlankMappingFileId.get()) { - existing == CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID - } else { - existing != null && existing != CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID - } + // Only fingerprint inputs when obfuscation is on. In blank-id mode the id is constant + // and source/classpath changes are irrelevant to the mapping handle. + // + // Discover user source files via project.fileTree("src") rather than + // variant.sources.java/kotlin.all. AGP's accessor includes generated source dirs + // (R.java, deeplinks, view-binding, etc.) whose producer tasks depend on the same + // mergeResources pipeline that consumes THIS task's output, which would close a cycle. + // AGP 8.1.4 has no `static` getter (added in 8.6) that would expose the non-generated + // subset, so we hand-roll the discovery from on-disk source-set conventions. + if (!useBlank) { + this.obfuscatableSources.from( + project.fileTree("src").matching { patterns -> + patterns.include( + "**/java/**/*.java", + "**/java/**/*.kt", + "**/kotlin/**/*.java", + "**/kotlin/**/*.kt", + ) + patterns.exclude("test/**", "androidTest/**", "test*/**", "androidTest*/**") + } + ) } } From dcfd1228b3f509f4910822445b479fa09da91d9d Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 26 May 2026 20:54:07 -0600 Subject: [PATCH 6/7] Crashlytics: source-change test and #6770 harness fix Adds source-invalidation test; rewires #6770 tests to buildGradleRunner. --- .../gradle/TypicalAppFunctionalTests.kt | 119 ++++++++++-------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt b/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt index fdfe8f87222..8e6c2a36444 100644 --- a/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt +++ b/firebase-crashlytics-gradle/src/functionalTest/kotlin/com/google/firebase/crashlytics/buildtools/gradle/TypicalAppFunctionalTests.kt @@ -221,73 +221,50 @@ class TypicalAppFunctionalTests { @Test fun `injectCrashlyticsMappingFileIdRelease is UP_TO_DATE on second invocation`() { val first = - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) assertThat(first.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) val second = - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) assertThat(second.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(UP_TO_DATE) } @Test - fun `release mapping file id resource is preserved across rebuilds`() { - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + fun `release mapping file id is preserved across rebuilds`() { + buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") - val resourceFile = - File( - projectDir, - "build/generated/res/injectCrashlyticsMappingFileIdRelease/values/" + - "com_google_firebase_crashlytics_mappingfileid.xml", - ) - val idBefore = idFile.readText() - val resourceBefore = resourceFile.readText() + assertThat(idBefore).isNotEmpty() - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") assertThat(idFile.readText()).isEqualTo(idBefore) - assertThat(resourceFile.readText()).isEqualTo(resourceBefore) } @Test fun `task re-runs after clean`() { - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") val rerun = - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments( - ":clean", - ":injectCrashlyticsMappingFileIdRelease", - "--configuration-cache", - ) - .build() + buildGradleRunner( + projectDir, + ":clean", + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache", + ) assertThat(rerun.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) assertThat(idFile.exists()).isTrue() @@ -297,11 +274,11 @@ class TypicalAppFunctionalTests { @Test fun `toggling mappingFileUploadEnabled invalidates the task`() { val first = - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) assertThat(first.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") @@ -334,16 +311,50 @@ class TypicalAppFunctionalTests { ) val second = - GradleRunner.create() - .withGradleVersion("8.1") - .withProjectDir(projectDir) - .withArguments(":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") - .build() + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) assertThat(second.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) assertThat(idFile.readText()).isEqualTo("00000000000000000000000000000000") } + @Test + fun `release mapping file id task is invalidated when source code changes`() { + val sourceFile = + File(projectDir, "src/main/kotlin/com/google/firebase/testing/crashlytics/Greeter.kt") + sourceFile.parentFile.mkdirs() + sourceFile.writeText( + """ + package com.google.firebase.testing.crashlytics + class Greeter { fun hello() = "hello" } + """ + ) + + val first = buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease") + assertThat(first.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + + // Re-running without changing inputs must NOT regenerate the id (would invalidate downstream + // R8, packaging, etc. — the bug PR #8185 originally fixed). + val noChange = buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease") + assertThat(noChange.task(":injectCrashlyticsMappingFileIdRelease")?.outcome) + .isEqualTo(UP_TO_DATE) + + // Editing the source MUST invalidate the task so a new id is minted: the on-device id is the + // handle Crashlytics uses to match crashes to the right uploaded mapping.txt, and R8 will + // produce a different mapping.txt after this edit. + sourceFile.writeText( + """ + package com.google.firebase.testing.crashlytics + class Greeter { fun hello() = "hello world" } + """ + ) + val afterEdit = buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease") + assertThat(afterEdit.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + } + @BeforeEach fun setup() { settingsFile = File(projectDir, "settings.gradle.kts") From 0502746af9ac3316db0d4332b52eb935566b8c5c Mon Sep 17 00:00:00 2001 From: Joseph Rodiz Date: Tue, 26 May 2026 20:54:12 -0600 Subject: [PATCH 7/7] Crashlytics: amend #6770 changelog for content-driven behavior --- firebase-crashlytics-gradle/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-crashlytics-gradle/CHANGELOG.md b/firebase-crashlytics-gradle/CHANGELOG.md index a9096893492..06809268e73 100644 --- a/firebase-crashlytics-gradle/CHANGELOG.md +++ b/firebase-crashlytics-gradle/CHANGELOG.md @@ -1,7 +1,7 @@ ### Unreleased - [fixed] Fixed an incompatibility between Crashlytics Gradle plugin and Gradle isolated projects when enabling nativeSymbolUploadEnabled. [#8037] -- [fixed] Stopped regenerating the mapping file id on every minified release build. `injectCrashlyticsMappingFileId` now reuses the on-disk id when valid for the current mode, so release builds can be `UP-TO-DATE` and downstream tasks (`mergeResources`, `processResources`, R8, packaging) are no longer invalidated on every build. A new id is still generated on the first build, after `clean`, or when `mappingFileUploadEnabled` is toggled. [#6770] +- [fixed] Stopped regenerating the mapping file id on every minified release build. `injectCrashlyticsMappingFileId` is now content-driven: the task stays `UP-TO-DATE` (and downstream `mergeResources`, `processResources`, R8, packaging tasks stay cached) while the variant's source files (`src/**/java`, `src/**/kotlin`, excluding tests) are unchanged. A new id is minted when those sources change — which is when R8 would produce a different `mapping.txt` — and on first build, after `clean`, or when `mappingFileUploadEnabled` is toggled. [#6770] ### Crashlytics Gradle plugin version 3.0.7