diff --git a/plugin-build/build.gradle.kts b/plugin-build/build.gradle.kts index 9aaf9b856..7e9b87dea 100644 --- a/plugin-build/build.gradle.kts +++ b/plugin-build/build.gradle.kts @@ -168,6 +168,10 @@ gradlePlugin { implementationClass = "io.sentry.android.gradle.snapshot.metadata.SentrySnapshotMetadataPlugin" } + register("sentrySettingsPlugin") { + id = "io.sentry.android.gradle.settings" + implementationClass = "io.sentry.android.gradle.SentrySettingsPlugin" + } } } @@ -203,6 +207,9 @@ distributions { create("sentrySnapshotMetadataPluginMarker") { contents { from("build${sep}publications${sep}sentrySnapshotMetadataPluginPluginMarkerMaven") } } + create("sentrySettingsPluginMarker") { + contents { from("build${sep}publications${sep}sentrySettingsPluginPluginMarkerMaven") } + } } tasks.named("distZip") { @@ -257,6 +264,14 @@ tasks.named("sentrySnapshotMetadataPluginMarkerDistZip").configure { dependsOn("generatePomFileForSentrySnapshotMetadataPluginPluginMarkerMavenPublication") } +tasks.named("sentrySettingsPluginMarkerDistTar").configure { + dependsOn("generatePomFileForSentrySettingsPluginPluginMarkerMavenPublication") +} + +tasks.named("sentrySettingsPluginMarkerDistZip").configure { + dependsOn("generatePomFileForSentrySettingsPluginPluginMarkerMavenPublication") +} + tasks.withType().configureEach { testLogging { events = setOf(TestLogEvent.SKIPPED, TestLogEvent.PASSED, TestLogEvent.FAILED) 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 4fcb35d83..9d2d6c7e7 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 @@ -26,6 +26,7 @@ import io.sentry.android.gradle.services.SentryModulesService import io.sentry.android.gradle.snapshot.GenerateSnapshotTestsTask import io.sentry.android.gradle.sourcecontext.OutputPaths import io.sentry.android.gradle.sourcecontext.SourceContext +import io.sentry.android.gradle.sourcecontext.resolveDependencySources import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask import io.sentry.android.gradle.tasks.InjectSentryMetaPropertiesIntoAssetsTask import io.sentry.android.gradle.tasks.PropertiesFileOutputTask @@ -44,6 +45,7 @@ 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 +import io.sentry.gradle.common.filterBuildConfig import java.io.File import org.gradle.api.Project import org.gradle.api.file.Directory @@ -103,7 +105,19 @@ fun ApplicationAndroidComponentsExtension.configure( project.layout.projectDirectory.dir(it) } } - val sourceFiles = sentryVariant.sources(project, additionalSourcesProvider) + val moduleSourceFiles = sentryVariant.sources(project, additionalSourcesProvider) + + val sourceFiles = + if (extension.includeSourceContext.get()) { + val dependencySources = resolveDependencySources(project, variant.name) + moduleSourceFiles?.map { currentSources -> + val depDirs = + dependencySources.files.map { project.layout.projectDirectory.dir(it.absolutePath) } + (currentSources + depDirs).filterBuildConfig().toSet() + } + } else { + moduleSourceFiles + } val tasksGeneratingProperties = mutableListOf>() val sourceContextTasks = diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt index 2582c78f2..c0fc92f1e 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentryPlugin.kt @@ -4,6 +4,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension import io.sentry.BuildConfig import io.sentry.android.gradle.autoinstall.installDependencies import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.sourcecontext.registerSentrySourceElements import io.sentry.android.gradle.util.AgpVersions import java.io.File import javax.inject.Inject @@ -30,11 +31,14 @@ constructor(private val buildEvents: BuildEventListenerRegistryInternal) : Plugi .trimIndent() ) } - if (!project.plugins.hasPlugin("com.android.application")) { + if ( + !project.plugins.hasPlugin("com.android.application") && + !project.plugins.hasPlugin("com.android.library") + ) { project.logger.warn( """ - WARNING: Using 'io.sentry.android.gradle' is only supported for the app module. - Please make sure that you apply the Sentry gradle plugin alongside 'com.android.application' on the _module_ level, and not on the root project level. + WARNING: Using 'io.sentry.android.gradle' is only supported for app and library modules. + Please make sure that you apply the Sentry gradle plugin alongside 'com.android.application' or 'com.android.library' on the _module_ level, and not on the root project level. https://docs.sentry.io/platforms/android/configuration/gradle/ """ .trimIndent() @@ -67,6 +71,10 @@ constructor(private val buildEvents: BuildEventListenerRegistryInternal) : Plugi project.installDependencies(extension, true) } + + project.pluginManager.withPlugin("com.android.library") { + registerSentrySourceElements(project) + } } companion object { diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt new file mode 100644 index 000000000..84b8bafb8 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/SentrySettingsPlugin.kt @@ -0,0 +1,18 @@ +package io.sentry.android.gradle + +import org.gradle.api.Plugin +import org.gradle.api.initialization.Settings + +class SentrySettingsPlugin : Plugin { + + override fun apply(settings: Settings) { + settings.gradle.beforeProject { project -> + project.pluginManager.withPlugin("com.android.library") { + project.pluginManager.apply("io.sentry.android.gradle") + } + project.pluginManager.withPlugin("java-library") { + project.pluginManager.apply("io.sentry.android.gradle") + } + } + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt new file mode 100644 index 000000000..f4f2837d5 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/sourcecontext/SourceContextArtifacts.kt @@ -0,0 +1,41 @@ +package io.sentry.android.gradle.sourcecontext + +import io.sentry.android.gradle.util.SentryPluginUtils.capitalizeUS +import org.gradle.api.Project +import org.gradle.api.attributes.Attribute +import org.gradle.api.file.FileCollection + +val SENTRY_ARTIFACT_ATTR: Attribute = Attribute.of("io.sentry.artifact", String::class.java) + +const val SENTRY_SOURCES_VALUE = "sentry-sources" + +fun registerSentrySourceElements(project: Project) { + val config = + project.configurations.create("sentrySourceElements") { + it.isCanBeConsumed = true + it.isCanBeResolved = false + it.attributes { attrs -> attrs.attribute(SENTRY_ARTIFACT_ATTR, SENTRY_SOURCES_VALUE) } + } + listOf("src/main/java", "src/main/kotlin").forEach { path -> + val dir = project.file(path) + if (dir.isDirectory) { + config.outgoing.artifact(dir) + } + } +} + +fun resolveDependencySources(project: Project, variantName: String): FileCollection { + val sentrySourcesPath = + project.configurations.create("sentrySourcesFor${variantName.capitalizeUS()}") { + it.isCanBeConsumed = false + it.isCanBeResolved = true + it.attributes { attrs -> attrs.attribute(SENTRY_ARTIFACT_ATTR, SENTRY_SOURCES_VALUE) } + } + + val runtimeClasspath = project.configurations.getByName("${variantName}RuntimeClasspath") + runtimeClasspath.extendsFrom + .filter { !it.isCanBeResolved && !it.isCanBeConsumed } + .forEach { sentrySourcesPath.extendsFrom(it) } + + return sentrySourcesPath.incoming.artifactView { view -> view.lenient(true) }.files +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt index 4be98743b..4e4264037 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginSourceContextTest.kt @@ -348,6 +348,116 @@ class SentryPluginSourceContextTest : assertTrue(subsequentBuild.output) { "BUILD SUCCESSFUL" in subsequentBuild.output } } + @Test + fun `bundles source context from library module dependencies`() { + // Set up settings.gradle to include the :library module + File(testProjectDir.root, "settings.gradle") + .writeText( + // language=Groovy + """ + include ':app', ':module', ':library' + """ + .trimIndent() + ) + + // Create the library module with the sentry plugin to publish source elements + val libraryDir = File(testProjectDir.root, "library").apply { mkdirs() } + File(libraryDir, "build.gradle") + .writeText( + // language=Groovy + """ + plugins { + id 'com.android.library' + id 'io.sentry.android.gradle' + } + + android { + namespace 'com.example.library' + compileSdkVersion 35 + defaultConfig { + minSdkVersion 21 + } + } + + sentry { + autoInstallation.enabled = false + telemetry = false + } + """ + .trimIndent() + ) + + val libSrcDir = File(libraryDir, "src/main/java/com/example/library") + libSrcDir.mkdirs() + val libContents = + // language=java + """ + package com.example.library; + + public class LibHelper { + public static int add(int a, int b) { return a + b; } + } + """ + .trimIndent() + File(libSrcDir, "LibHelper.java").writeText(libContents) + + appBuildFile.writeText( + // language=Groovy + """ + plugins { + id "com.android.application" + id "io.sentry.android.gradle" + } + + android { + namespace 'com.example' + + buildFeatures { + buildConfig false + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + } + + dependencies { + implementation project(':library') + } + + sentry { + debug = true + includeSourceContext = true + autoUploadSourceContext = false + autoUploadProguardMapping = false + org = "sentry-sdks" + projectName = "sentry-android" + } + """ + .trimIndent() + ) + + sentryPropertiesFile.writeText("") + val ktContents = testProjectDir.withDummyKtFile() + + val result = runner.appendArguments("app:assembleRelease").build() + + assertTrue(result.output) { "BUILD SUCCESSFUL" in result.output } + + // App module sources should be bundled + verifySourceBundleContents(testProjectDir.root, "files/_/_/com/example/Example.jvm", ktContents) + + // Library module sources should also be bundled + verifySourceBundleContents( + testProjectDir.root, + "files/_/_/com/example/library/LibHelper.jvm", + libContents, + ) + } + @Test fun `uploadSourceBundle task is not up-to-date on subsequent builds if cli path changes`() { val sentryCli = SentryCliProvider.getSentryCliPath(File(""), File("build"), File(""))