Skip to content

Commit

Permalink
feat: Add auto-installation for sentry-android SDK and integrations (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn committed Mar 9, 2022
1 parent d8f1ab3 commit 0fae1e9
Show file tree
Hide file tree
Showing 24 changed files with 879 additions and 14 deletions.
15 changes: 15 additions & 0 deletions buildSrc/src/main/java/Dependencies.kt
Expand Up @@ -60,4 +60,19 @@ object Samples {
const val compiler = "androidx.room:room-compiler:${version}"
const val rxjava = "androidx.room:room-rxjava2:${version}"
}

object OkHttp {
private const val version = "4.9.3"
const val okhttp = "com.squareup.okhttp3:okhttp:${version}"
}

object Timber {
private const val version = "5.0.1"
const val timber = "com.jakewharton.timber:timber:${version}"
}

object Fragment {
private const val version = "1.3.5"
const val fragmentKtx = "androidx.fragment:fragment-ktx:${version}"
}
}
4 changes: 2 additions & 2 deletions examples/android-instrumentation-sample/build.gradle.kts
Expand Up @@ -50,8 +50,6 @@ android {
// }

dependencies {
implementation(Libs.SENTRY_ANDROID)

implementation(Samples.AndroidX.recyclerView)
implementation(Samples.AndroidX.lifecycle)
implementation(Samples.AndroidX.appcompat)
Expand All @@ -63,6 +61,8 @@ dependencies {
implementation(Samples.Room.ktx)
implementation(Samples.Room.rxjava)

implementation(Samples.Timber.timber)
implementation(Samples.Fragment.fragmentKtx)
implementation(project(":examples:android-room-lib"))

kapt(Samples.Room.compiler)
Expand Down
4 changes: 4 additions & 0 deletions examples/android-room-lib/build.gradle.kts
Expand Up @@ -21,4 +21,8 @@ dependencies {

implementation(Samples.Room.runtime)
implementation(Samples.Room.ktx)

// this is here for test purposes, to ensure that transitive dependencies are also recognized
// by our auto-installation
implementation(Samples.OkHttp.okhttp)
}
Expand Up @@ -16,6 +16,8 @@ import io.sentry.android.gradle.SentryTasksProvider.getPackageBundleTask
import io.sentry.android.gradle.SentryTasksProvider.getPackageProvider
import io.sentry.android.gradle.SentryTasksProvider.getPreBundleTask
import io.sentry.android.gradle.SentryTasksProvider.getTransformerTask
import io.sentry.android.gradle.autoinstall.installDependencies
import io.sentry.android.gradle.extensions.SentryPluginExtension
import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory
import io.sentry.android.gradle.services.SentrySdkStateHolder
import io.sentry.android.gradle.tasks.SentryGenerateProguardUuidTask
Expand Down Expand Up @@ -290,6 +292,8 @@ class SentryPlugin : Plugin<Project> {
project.logger.info { "uploadSentryNativeSymbols won't be executed" }
}
}

project.installDependencies(extension)
}
}

Expand All @@ -307,6 +311,7 @@ class SentryPlugin : Plugin<Project> {
companion object {
const val SENTRY_ORG_PARAMETER = "sentryOrg"
const val SENTRY_PROJECT_PARAMETER = "sentryProject"
internal const val SENTRY_SDK_VERSION = "5.6.1"

internal val sep = File.separator

Expand Down
@@ -0,0 +1,48 @@
package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.util.SemVer
import io.sentry.android.gradle.util.info
import io.sentry.android.gradle.util.warn
import org.gradle.api.artifacts.ComponentMetadataContext
import org.gradle.api.artifacts.ComponentMetadataRule
import org.slf4j.Logger

abstract class AbstractInstallStrategy : ComponentMetadataRule {

protected lateinit var logger: Logger

protected abstract val moduleId: String

protected abstract val shouldInstallModule: Boolean

protected open val minSupportedVersion: SemVer = SemVer(0, 0, 0)

override fun execute(context: ComponentMetadataContext) {
val autoInstallState = AutoInstallState.getInstance()
if (!shouldInstallModule) {
logger.info {
"$moduleId won't be installed because it was already installed directly"
}
return
}
val semVer = SemVer.parse(context.details.id.version)
if (semVer < minSupportedVersion) {
logger.warn {
"$moduleId won't be installed because the current version is " +
"lower than the minimum supported version ($minSupportedVersion)"
}
return
}

context.details.allVariants { metadata ->
metadata.withDependencies { dependencies ->
val sentryVersion = autoInstallState.sentryVersion
dependencies.add("$SENTRY_GROUP:$moduleId:$sentryVersion")

logger.info {
"$moduleId was successfully installed with version: $sentryVersion"
}
}
}
}
}
@@ -0,0 +1,75 @@
package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy
import io.sentry.android.gradle.autoinstall.fragment.FragmentInstallStrategy.Registrar.SENTRY_FRAGMENT_ID
import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy
import io.sentry.android.gradle.autoinstall.okhttp.OkHttpInstallStrategy.Registrar.SENTRY_OKHTTP_ID
import io.sentry.android.gradle.autoinstall.timber.TimberInstallStrategy
import io.sentry.android.gradle.autoinstall.timber.TimberInstallStrategy.Registrar.SENTRY_TIMBER_ID
import io.sentry.android.gradle.extensions.SentryPluginExtension
import io.sentry.android.gradle.util.info
import org.gradle.api.Project
import org.gradle.api.artifacts.DependencySet

internal const val SENTRY_GROUP = "io.sentry"
private const val SENTRY_ANDROID_ID = "sentry-android"
private const val SENTRY_ANDROID_CORE_ID = "sentry-android-core"

private val strategies = listOf(
OkHttpInstallStrategy.Registrar,
TimberInstallStrategy.Registrar,
FragmentInstallStrategy.Registrar
)

fun Project.installDependencies(extension: SentryPluginExtension) {
configurations.named("implementation").configure { configuration ->
configuration.withDependencies { dependencies ->
// if autoInstallation is disabled, the autoInstallState will contain initial values
// which all default to false, hence, the integrations won't be installed as well
if (extension.autoInstallation.enabled.get()) {
val sentryVersion = dependencies.findSentryAndroidVersion()
with(AutoInstallState.getInstance(gradle)) {
this.sentryVersion = installSentrySdk(sentryVersion, dependencies, extension)

installOkHttp = !dependencies.isModuleAvailable(SENTRY_OKHTTP_ID)
installTimber = !dependencies.isModuleAvailable(SENTRY_TIMBER_ID)
installFragment = !dependencies.isModuleAvailable(SENTRY_FRAGMENT_ID)
}
}
}
}
project.dependencies.components { component ->
strategies.forEach { it.register(component) }
}
}

private fun Project.installSentrySdk(
sentryVersion: String?,
dependencies: DependencySet,
extension: SentryPluginExtension
): String {
return if (sentryVersion == null) {
val userDefinedVersion = extension.autoInstallation.sentryVersion.get()
val sentryAndroidDep =
this.dependencies.create("$SENTRY_GROUP:$SENTRY_ANDROID_ID:$userDefinedVersion")
dependencies.add(sentryAndroidDep)
logger.info {
"sentry-android was successfully installed with version: $userDefinedVersion"
}
userDefinedVersion
} else {
logger.info {
"sentry-android won't be installed because it was already installed directly"
}
sentryVersion
}
}

private fun DependencySet.findSentryAndroidVersion(): String? =
find {
it.group == SENTRY_GROUP &&
(it.name == SENTRY_ANDROID_ID || it.name == SENTRY_ANDROID_CORE_ID)
}?.version

private fun DependencySet.isModuleAvailable(id: String): Boolean =
any { it.group == SENTRY_GROUP && it.name == id }
@@ -0,0 +1,77 @@
package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.SentryPlugin.Companion.SENTRY_SDK_VERSION
import java.io.Serializable
import org.gradle.api.invocation.Gradle

class AutoInstallState private constructor() : Serializable {

@get:Synchronized
@set:Synchronized
var sentryVersion: String = SENTRY_SDK_VERSION

@get:Synchronized
@set:Synchronized
var installOkHttp: Boolean = false

@get:Synchronized
@set:Synchronized
var installFragment: Boolean = false

@get:Synchronized
@set:Synchronized
var installTimber: Boolean = false

override fun toString(): String {
return "AutoInstallState(sentryVersion='$sentryVersion', " +
"installOkHttp=$installOkHttp, " +
"installFragment=$installFragment, " +
"installTimber=$installTimber)"
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as AutoInstallState

if (sentryVersion != other.sentryVersion) return false
if (installOkHttp != other.installOkHttp) return false
if (installFragment != other.installFragment) return false
if (installTimber != other.installTimber) return false

return true
}

override fun hashCode(): Int {
var result = sentryVersion.hashCode()
result = 31 * result + installOkHttp.hashCode()
result = 31 * result + installFragment.hashCode()
result = 31 * result + installTimber.hashCode()
return result
}

// We can't use Kotlin object because we need new instance on each Gradle rebuild
// But if we're inside Gradle daemon, Kotlin object will be shared between builds
companion object {
@field:Volatile
private var instance: AutoInstallState? = null

@JvmStatic
@Synchronized
fun getInstance(gradle: Gradle? = null): AutoInstallState {
if (instance != null) {
return instance!!
}

val state = AutoInstallState()
instance = state

if (gradle != null) {
BuildFinishedListenerService.getInstance(gradle).onClose { instance = null }
}

return state
}
}
}
@@ -0,0 +1,35 @@
@file:Suppress("UnstableApiUsage")

package io.sentry.android.gradle.autoinstall

import io.sentry.android.gradle.util.getBuildServiceName
import org.gradle.api.invocation.Gradle
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

abstract class BuildFinishedListenerService :
BuildService<BuildServiceParameters.None>,
AutoCloseable {

private val actionsOnClose = mutableListOf<() -> Unit>()

fun onClose(action: () -> Unit) {
actionsOnClose.add(action)
}

override fun close() {
for (action in actionsOnClose) {
action()
}
actionsOnClose.clear()
}

companion object {
fun getInstance(gradle: Gradle): BuildFinishedListenerService {
return gradle.sharedServices.registerIfAbsent(
getBuildServiceName(BuildFinishedListenerService::class.java),
BuildFinishedListenerService::class.java
) {}.get()
}
}
}
@@ -0,0 +1,7 @@
package io.sentry.android.gradle.autoinstall

import org.gradle.api.artifacts.dsl.ComponentMetadataHandler

interface InstallStrategyRegistrar {
fun register(component: ComponentMetadataHandler)
}
@@ -0,0 +1,38 @@
package io.sentry.android.gradle.autoinstall.fragment

import io.sentry.android.gradle.SentryPlugin
import io.sentry.android.gradle.autoinstall.AbstractInstallStrategy
import io.sentry.android.gradle.autoinstall.AutoInstallState
import io.sentry.android.gradle.autoinstall.InstallStrategyRegistrar
import javax.inject.Inject
import org.gradle.api.artifacts.dsl.ComponentMetadataHandler
import org.slf4j.Logger

// @CacheableRule // TODO: make it cacheable somehow (probably depends on parameters)
abstract class FragmentInstallStrategy : AbstractInstallStrategy {

constructor(logger: Logger) : super() {
this.logger = logger
}

@Suppress("unused") // used by Gradle
@Inject // inject is needed to avoid Gradle error
constructor() : this(SentryPlugin.logger)

override val moduleId: String get() = SENTRY_FRAGMENT_ID

override val shouldInstallModule: Boolean get() = AutoInstallState.getInstance().installFragment

companion object Registrar : InstallStrategyRegistrar {
private const val FRAGMENT_GROUP = "androidx.fragment"
private const val FRAGMENT_ID = "fragment"
internal const val SENTRY_FRAGMENT_ID = "sentry-android-fragment"

override fun register(component: ComponentMetadataHandler) {
component.withModule(
"$FRAGMENT_GROUP:$FRAGMENT_ID",
FragmentInstallStrategy::class.java
) {}
}
}
}

0 comments on commit 0fae1e9

Please sign in to comment.