Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add auto-installation for sentry-android SDK and integrations #282

Merged
merged 18 commits into from Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
romtsn marked this conversation as resolved.
Show resolved Hide resolved
const val okhttp = "com.squareup.okhttp3:okhttp:${version}"
}

object Timber {
private const val version = "5.0.1"
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
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"
romtsn marked this conversation as resolved.
Show resolved Hide resolved

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 ->
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
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
) {}
}
}
}