diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AGP74Compat.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AGP74Compat.kt index 5f60b60da..4d435a76b 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AGP74Compat.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AGP74Compat.kt @@ -14,7 +14,6 @@ import com.android.build.api.variant.impl.VariantImpl import io.sentry.android.gradle.SentryTasksProvider.getComposeMappingMergeTask import io.sentry.android.gradle.SentryTasksProvider.getMinifyTask import io.sentry.android.gradle.tasks.SentryGenerateProguardUuidTask -import io.sentry.android.gradle.util.AgpVersions import io.sentry.gradle.common.SentryVariant import io.sentry.gradle.common.filterBuildConfig import org.gradle.api.Project @@ -32,9 +31,7 @@ data class AndroidVariant74(private val variant: Variant) : SentryVariant { override val flavorName: String? = variant.flavorName override val buildTypeName: String? = variant.buildType override val productFlavors: List = variant.productFlavors.map { it.second } - override val isMinifyEnabled: Boolean = - (variant as? CanMinifyCode)?.isMinifyEnabled == true || - variant.isApplicationOptimizationEnabled() + override val isMinifyEnabled: Boolean = (variant as? CanMinifyCode)?.isMinifyEnabled == true // TODO: replace this eventually (when targeting AGP 8.3.0) with // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-api/src/main/java/com/android/build/api/variant/Component.kt;l=103-104;bpv=1 @@ -117,24 +114,6 @@ data class AndroidVariant74(private val variant: Variant) : SentryVariant { } } -private fun Variant.isApplicationOptimizationEnabled(): Boolean { - if (!AgpVersions.isAGP93(AgpVersions.CURRENT)) { - return false - } - - // AGP 9.3's optimization.enable DSL does not update CanMinifyCode.isMinifyEnabled. The merged - // value is only exposed through an internal creation config, so use reflection to keep this - // plugin binary-compatible with older AGP versions. - return runCatching { - val optimizationCreationConfig = - javaClass.getMethod("getOptimizationCreationConfig").invoke(this) - optimizationCreationConfig.javaClass - .getMethod("getApplicationOptimizationEnabled") - .invoke(optimizationCreationConfig) as Boolean - } - .getOrDefault(false) -} - fun configureInstrumentationFor74( variant: Variant, classVisitorFactoryImplClass: Class>, diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt index 32c7123ca..718b5506a 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt @@ -38,7 +38,6 @@ import io.sentry.android.gradle.telemetry.SentryTelemetryService import io.sentry.android.gradle.util.AgpVersions import io.sentry.android.gradle.util.GroovyCompat import io.sentry.android.gradle.util.SentryModules -import io.sentry.android.gradle.util.SentryPluginUtils.isMinificationEnabled import io.sentry.android.gradle.util.SentryPluginUtils.isVariantAllowed import io.sentry.android.gradle.util.collectModules import io.sentry.android.gradle.util.hookWithAssembleTasks @@ -351,9 +350,8 @@ private fun ApplicationVariant.configureProguardMappingsTasks( } val sentryProps = getPropertiesFilePath(project, variant) val dexguardEnabled = extension.dexguardEnabled.get() - val isMinifyEnabled = isMinificationEnabled(project, variant, dexguardEnabled) - if (isMinifyEnabled && extension.includeProguardMapping.get()) { + if (extension.includeProguardMapping.get()) { val mappings = getMappingFileProvider(project, variant, dexguardEnabled) val generateUuidTask = SentryGenerateProguardUuidTask.register( diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTask.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTask.kt index 0acd26b83..70caf0f87 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTask.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTask.kt @@ -5,7 +5,9 @@ import io.sentry.android.gradle.telemetry.SentryTelemetryService import io.sentry.android.gradle.telemetry.withSentryTelemetry import io.sentry.android.gradle.util.contentHash import io.sentry.android.gradle.util.info +import java.io.File import java.util.UUID +import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.Directory @@ -36,24 +38,34 @@ abstract class SentryGenerateProguardUuidTask : PropertiesFileOutputTask() { // Used by AGP 8.3+ with toListenTo API - this property is wired to the mapping artifact @get:Internal abstract val mappingFile: RegularFileProperty + internal fun findMappingFile(): File? = + try { + mappingFile.orNull?.asFile?.takeIf { it.exists() } + ?: fallbackMappingFiles.files.firstOrNull { it.exists() } + } catch (_: GradleException) { + // AGP uses a provider with no value for non-minified variants; treat it as no mapping. + null + } catch (_: IllegalStateException) { + // Gradle may realize a missing artifact provider before wrapping it in a GradleException. + null + } + @TaskAction fun generateProperties() { val outputDir = output.get().asFile outputDir.mkdirs() - // Prefer mappingFile (set via toListenTo on AGP 8.3+) over fallbackMappingFiles - val mappingFile = - if (mappingFile.isPresent) { - mappingFile.get().asFile.takeIf { it.exists() } - } else { - // Fallback for AGP < 8.3: use conventional file paths - fallbackMappingFiles.files.firstOrNull { it.exists() } - } + val mappingFile = findMappingFile() + val outputFile = outputFile.get().asFile + if (mappingFile == null) { + // The task may have produced a UUID in an earlier minified build. Remove it so a later + // non-minified build cannot inject or upload that stale UUID. + outputFile.delete() + return + } - val uuid = - mappingFile?.let { UUID.nameUUIDFromBytes(it.contentHash().toByteArray()) } - ?: UUID.randomUUID() - outputFile.get().asFile.writer().use { writer -> + val uuid = UUID.nameUUIDFromBytes(mappingFile.contentHash().toByteArray()) + outputFile.writer().use { writer -> writer.appendLine("$SENTRY_PROGUARD_MAPPING_UUID_PROPERTY=$uuid") } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadProguardMappingsTask.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadProguardMappingsTask.kt index eb1c88f24..7411d4bb2 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadProguardMappingsTask.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadProguardMappingsTask.kt @@ -15,6 +15,7 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskProvider @@ -36,7 +37,10 @@ abstract class SentryUploadProguardMappingsTask : SentryCliExecTask() { outputs.upToDateWhen { true } } - @get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val uuidFile: RegularFileProperty + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.NONE) + abstract val uuidFile: RegularFileProperty @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) @@ -44,13 +48,6 @@ abstract class SentryUploadProguardMappingsTask : SentryCliExecTask() { @get:Input abstract val autoUploadProguardMapping: Property - override fun exec() { - if (!mappingsFiles.isPresent || mappingsFiles.get().isEmpty) { - error("[sentry] Mapping files are missing!") - } - super.exec() - } - override fun getArguments(args: MutableList) { val uuid = readUuidFromFile(uuidFile.get().asFile) val firstExistingFile = mappingsFiles.get().files.firstOrNull { it.exists() } @@ -118,6 +115,10 @@ abstract class SentryUploadProguardMappingsTask : SentryCliExecTask() { sentryTelemetryProvider?.let { task.sentryTelemetryService.set(it) } task.asSentryCliExec() task.withSentryTelemetry(extension, sentryTelemetryProvider) + task.onlyIf("a ProGuard mapping file and matching UUID were produced") { + task.uuidFile.asFile.orNull?.exists() == true && + task.mappingsFiles.orNull?.files?.any { it.exists() } == true + } } return uploadSentryProguardMappingsTask } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt index 7509d9921..9568f4246 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt @@ -374,9 +374,10 @@ class SentryPluginTest : @Test fun `does not include a UUID in the APK`() { - // isMinifyEnabled is disabled by default in debug builds - runner.appendArguments(":app:assembleDebug").build() + val build = runner.appendArguments(":app:assembleDebug").build() + assertEquals(TaskOutcome.SUCCESS, build.task(":app:generateSentryProguardUuidDebug")?.outcome) + assertEquals(TaskOutcome.SKIPPED, build.task(":app:uploadSentryProguardMappingsDebug")?.outcome) assertThrows(AssertionError::class.java) { verifyProguardUuid(testProjectDir.root, variant = "debug", signed = false) } @@ -1119,25 +1120,17 @@ class SentryPluginTest : ) val result1 = runner.appendArguments(":app:testReleaseUnitTest").build() - val uuid1 = verifyProguardUuid(testProjectDir.root, inGeneratedFolder = true) assertFalse { "minifyReleaseWithR8" in result1.output } + assertEquals( + TaskOutcome.SUCCESS, + result1.task(":app:generateSentryProguardUuidRelease")?.outcome, + ) val result2 = runner.build() - val uuid2 = verifyProguardUuid(testProjectDir.root, inGeneratedFolder = true) assertFalse { "minifyReleaseWithR8" in result2.output } - - // UUIDs may differ between runs when minify doesn't run, because there's no mapping file - // to generate a deterministic hash from. The important assertion is that minify doesn't run. - // When minify DOES run (in production builds), the UUID will be deterministic. - assertNotEquals( - "00000000-0000-0000-0000-000000000000", - uuid1.toString(), - "UUID should be generated", - ) - assertNotEquals( - "00000000-0000-0000-0000-000000000000", - uuid2.toString(), - "UUID should be generated", + assertEquals( + TaskOutcome.SUCCESS, + result2.task(":app:generateSentryProguardUuidRelease")?.outcome, ) } diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateDebugMetaPropertiesTaskTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateDebugMetaPropertiesTaskTest.kt index a9a9ee3a0..752b39974 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateDebugMetaPropertiesTaskTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateDebugMetaPropertiesTaskTest.kt @@ -9,6 +9,8 @@ import java.io.File import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Provider import org.gradle.api.tasks.TaskProvider import org.gradle.testfixtures.ProjectBuilder import org.junit.Test @@ -36,7 +38,7 @@ class SentryGenerateDebugMetaPropertiesTaskTest { project.extensions.findByName("sentry") as SentryPluginExtension, null, project.layout.buildDirectory.dir("dummy/folder/"), - null, + createMappingFileProvider(project), "test", ) val idGenerationTasks = listOf(bundleIdTask, proguardIdTask) @@ -81,7 +83,7 @@ class SentryGenerateDebugMetaPropertiesTaskTest { project.extensions.findByName("sentry") as SentryPluginExtension, null, project.layout.buildDirectory.dir("dummy/folder/"), - null, + createMappingFileProvider(project), "test", ) val idGenerationTasks = listOf(bundleIdTask, proguardIdTask) @@ -121,6 +123,16 @@ class SentryGenerateDebugMetaPropertiesTaskTest { assertNotNull(bundleId2) } + private fun createMappingFileProvider(project: Project): Provider = + project.provider { + project.files( + File(project.buildDir, "mapping.txt").also { + it.parentFile.mkdirs() + it.writeText("mapping") + } + ) + } + private fun createProject(): Project { with(ProjectBuilder.builder().build()) { plugins.apply("io.sentry.android.gradle") diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTaskTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTaskTest.kt index f8e0bcb5a..82d1f9403 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTaskTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryGenerateProguardUuidTaskTest.kt @@ -4,6 +4,7 @@ import io.sentry.android.gradle.tasks.SentryGenerateProguardUuidTask.Companion.S import io.sentry.android.gradle.util.PropertiesUtil import java.io.File import java.util.UUID +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.gradle.api.Project @@ -22,6 +23,12 @@ class SentryGenerateProguardUuidTaskTest { SentryGenerateProguardUuidTask::class.java, ) { it.output.set(project.layout.buildDirectory.dir("dummy/folder/")) + it.fallbackMappingFiles.from( + File(project.buildDir, "mapping.txt").also { file -> + file.parentFile.mkdirs() + file.writeText("mapping") + } + ) } task.get().generateProperties() @@ -35,6 +42,27 @@ class SentryGenerateProguardUuidTaskTest { UUID.fromString(uuid) } + @Test + fun `generate proguard UUID removes stale output when no mapping exists`() { + val project = createProject() + val task = + project.tasks.register( + "testRemoveStaleProguardUuid", + SentryGenerateProguardUuidTask::class.java, + ) { + it.output.set(project.layout.buildDirectory.dir("dummy/folder/")) + } + val staleOutput = + File(project.buildDir, "dummy/folder/sentry-proguard-uuid.properties").also { + it.parentFile.mkdirs() + it.writeText("$SENTRY_PROGUARD_MAPPING_UUID_PROPERTY=stale") + } + + task.get().generateProperties() + + assertFalse(staleOutput.exists()) + } + private fun createProject(): Project { with(ProjectBuilder.builder().build()) { plugins.apply("io.sentry.android.gradle")