All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.sentry.android.gradle.SentryCliProvider.kt Maven / Gradle / Ivy

@file:Suppress("UnstableApiUsage")

package io.sentry.android.gradle

import io.sentry.BuildConfig
import io.sentry.android.gradle.SentryCliValueSource.Params
import io.sentry.android.gradle.SentryPlugin.Companion.logger
import io.sentry.android.gradle.util.GradleVersions
import io.sentry.android.gradle.util.error
import io.sentry.android.gradle.util.info
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.Locale
import java.util.Properties
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.api.tasks.Input

internal object SentryCliProvider {

    @field:Volatile
    private var memoizedCliPath: String? = null

    /**
     * Return the correct sentry-cli executable path to use for the given project.  This
     * will look for a sentry-cli executable in a local node_modules in case it was put
     * there by sentry-react-native or others before falling back to the global installation.
     * In case there's no global installation, and a matching cli is packaged in the resources
     * it will provide a temporary path, without actually extracting it.
     */
    @JvmStatic
    @Synchronized
    fun getSentryCliPath(projectDir: File, projectBuildDir: File, rootDir: File): String {
        val cliPath = memoizedCliPath
        if (!cliPath.isNullOrEmpty() && File(cliPath).exists()) {
            logger.info { "Using memoized cli path: $cliPath" }
            return cliPath
        }
        // If a path is provided explicitly use that first.
        logger.info { "Searching cli from sentry.properties file..." }

        searchCliInPropertiesFile(projectDir, rootDir)?.let {
            logger.info { "cli Found: $it" }
            memoizedCliPath = it
            return@getSentryCliPath it
        } ?: logger.info { "sentry-cli not found in sentry.properties file" }

        // next up try a packaged version of sentry-cli
        val cliResLocation = getCliLocationInResources()
        if (!cliResLocation.isNullOrBlank()) {
            logger.info { "cli present in resources: $cliResLocation" }
            // just provide the target extraction path
            // actual extraction will be done prior to task execution
            val extractedResourcePath = getCliResourcesExtractionPath(projectBuildDir)
                .absolutePath
            memoizedCliPath = extractedResourcePath
            return extractedResourcePath
        }

        logger.error { "Falling back to invoking `sentry-cli` from shell" }
        return "sentry-cli".also { memoizedCliPath = it }
    }

    private fun getCliLocationInResources(): String? {
        val cliSuffix = getCliSuffix()
        logger.info { "cliSuffix is $cliSuffix" }

        if (!cliSuffix.isNullOrBlank()) {
            val resourcePath = "/bin/sentry-cli-$cliSuffix"

            // if we are not in a jar, we can use the file directly
            logger.info { "Searching for $resourcePath in resources folder..." }

            getResourceUrl(resourcePath)?.let {
                logger.info { "cli found in resources: $it" }

                // still return the resource path, as it's the one we can use for extraction later
                return resourcePath
            } ?: logger.info { "Failed to load sentry-cli from resource folder" }
        }

        return null
    }

    internal fun getSentryPropertiesPath(projectDir: File, rootDir: File): String? =
        listOf(
            File(projectDir, "sentry.properties"),
            File(rootDir, "sentry.properties")
        ).firstOrNull(File::exists)?.path

    internal fun searchCliInPropertiesFile(projectDir: File, rootDir: File): String? {
        return getSentryPropertiesPath(projectDir, rootDir)?.let { propertiesFile ->
            runCatching {
                Properties()
                    .apply { load(FileInputStream(propertiesFile)) }
                    .getProperty("cli.executable")
            }.getOrNull()
        }
    }

    internal fun getResourceUrl(resourcePath: String): String? =
        javaClass.getResource(resourcePath)?.toString()

    internal fun getCliResourcesExtractionPath(projectBuildDir: File): File {
        // usually /build/tmp/
        return File(
            File(projectBuildDir, "tmp"),
            "sentry-cli-${BuildConfig.CliVersion}.exe"
        )
    }

    internal fun extractCliFromResources(resourcePath: String, outputPath: File): String? {
        val resourceStream = javaClass.getResourceAsStream(resourcePath)
        return if (resourceStream != null) {
            val baseFolder = outputPath.parentFile
            logger.info { "sentry-cli base folder: ${baseFolder.absolutePath}" }

            if (!baseFolder.exists() && !baseFolder.mkdirs()) {
                logger.error { "sentry-cli base folder could not be created!" }
                return null
            }

            FileOutputStream(outputPath).use { output ->
                resourceStream.use { input ->
                    input.copyTo(output)
                }
            }
            outputPath.setExecutable(true)
            outputPath.deleteOnExit()

            outputPath.absolutePath
        } else {
            return null
        }
    }

    internal fun getCliSuffix(): String? {
        // TODO: change to .lowercase(Locale.ROOT) when using Kotlin 1.6
        val osName = System.getProperty("os.name").toLowerCase(Locale.ROOT)
        val osArch = System.getProperty("os.arch")
        return when {
            "mac" in osName -> "Darwin-universal"
            "linux" in osName -> if (osArch == "amd64") "Linux-x86_64" else "Linux-$osArch"
            "win" in osName -> "Windows-i686.exe"
            else -> null
        }
    }

    /**
     * Tries to extract the sentry-cli from resources if the computedCliPath does not exist.
     */
    @Synchronized
    internal fun maybeExtractFromResources(buildDir: File, cliPath: String): String {
        val cli = File(cliPath)
        if (!cli.exists()) {
            // we only want to auto-extract if the path matches the pre-computed one
            if (File(cliPath).absolutePath.equals(
                    getCliResourcesExtractionPath(buildDir).absolutePath
                )
            ) {
                val cliResPath = getCliLocationInResources()
                if (!cliResPath.isNullOrBlank()) {
                    return extractCliFromResources(cliResPath, cli) ?: cliPath
                }
            }
        }
        return cliPath
    }
}

abstract class SentryCliValueSource : ValueSource {
    interface Params : ValueSourceParameters {
        @get:Input
        val projectDir: Property

        @get:Input
        val projectBuildDir: Property

        @get:Input
        val rootProjDir: Property
    }

    override fun obtain(): String? {
        return SentryCliProvider.getSentryCliPath(
            parameters.projectDir.get(),
            parameters.projectBuildDir.get(),
            parameters.rootProjDir.get()
        )
    }
}

fun Project.cliExecutableProvider(): Provider {
    return if (GradleVersions.CURRENT >= GradleVersions.VERSION_7_5) {
        // config-cache compatible way to retrieve the cli path, it properly gets invalidated when
        // e.g. switching branches
        providers.of(SentryCliValueSource::class.java) {
            it.parameters.projectDir.set(project.projectDir)
            it.parameters.projectBuildDir.set(project.layout.buildDirectory.asFile.get())
            it.parameters.rootProjDir.set(project.rootDir)
        }
    } else {
        return provider {
            SentryCliProvider.getSentryCliPath(
                project.projectDir,
                project.layout.buildDirectory.asFile.get(),
                project.rootDir
            )
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy