diff --git a/firebase-crashlytics-gradle/CHANGELOG.md b/firebase-crashlytics-gradle/CHANGELOG.md index 5a91821f691..06809268e73 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` 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 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..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 @@ -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,143 @@ class TypicalAppFunctionalTests { ) } + @Test + fun `injectCrashlyticsMappingFileIdRelease is UP_TO_DATE on second invocation`() { + val first = + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) + + assertThat(first.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + + val second = + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) + + assertThat(second.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(UP_TO_DATE) + } + + @Test + fun `release mapping file id is preserved across rebuilds`() { + buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + + val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") + val idBefore = idFile.readText() + assertThat(idBefore).isNotEmpty() + + buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + + assertThat(idFile.readText()).isEqualTo(idBefore) + } + + @Test + fun `task re-runs after clean`() { + buildGradleRunner(projectDir, ":injectCrashlyticsMappingFileIdRelease", "--configuration-cache") + + val idFile = File(projectDir, "build/crashlytics/release/mappingFileId.txt") + + val rerun = + buildGradleRunner( + projectDir, + ":clean", + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache", + ) + + assertThat(rerun.task(":injectCrashlyticsMappingFileIdRelease")?.outcome).isEqualTo(SUCCESS) + assertThat(idFile.exists()).isTrue() + assertThat(idFile.readText()).isNotEmpty() + } + + @Test + fun `toggling mappingFileUploadEnabled invalidates the task`() { + val first = + buildGradleRunner( + projectDir, + ":injectCrashlyticsMappingFileIdRelease", + "--configuration-cache" + ) + + 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 = + 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") 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..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 @@ -66,32 +77,45 @@ 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. - */ - private fun blankMappingFileIdExists(): Boolean { - val file: File = mappingFileIdFile.get().asFile - return file.exists() && file.readText() == CrashlyticsBuildtools.BLANK_MAPPING_FILE_ID - } - 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 { useBlankMappingFileId.get() && blankMappingFileIdExists() } + // 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*/**") + } + ) + } } // It is not possible to disable Android resources in the AGP app plugin.