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

io.sentry.android.gradle.transforms.MetaInfStripTransform.kt Maven / Gradle / Ivy

The newest version!
package io.sentry.android.gradle.transforms

import io.sentry.android.gradle.SentryPlugin
import io.sentry.android.gradle.util.warn
import java.io.File
import java.nio.file.Files
import java.util.jar.Attributes
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.transform.CacheableTransform
import org.gradle.api.artifacts.transform.InputArtifact
import org.gradle.api.artifacts.transform.TransformAction
import org.gradle.api.artifacts.transform.TransformOutputs
import org.gradle.api.artifacts.transform.TransformParameters
import org.gradle.api.attributes.Attribute
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity

/**
 * Gradle's [TransformAction] that strips out unsupported Java classes from
 * resources/META-INF/versions folder of a jar. This is the case for [Multi-Release JARs](https://openjdk.java.net/jeps/238),
 * when a single jar contains classes for different Java versions.
 *
 * For Android it may have bad consequences, as the min supported Java version is 11 at the moment,
 * and this can cause incompatibilities, if AGP or other plugins erroneously consider .class files
 * under the META-INF folder during build-time transformations/instrumentation.
 *
 * The minimum supported Java version of the latest AGP is 11 -> https://developer.android.com/studio/releases/gradle-plugin#7-1-0
 */
@CacheableTransform
abstract class MetaInfStripTransform : TransformAction {

    @get:PathSensitive(PathSensitivity.RELATIVE)
    @get:InputArtifact
    abstract val inputArtifact: Provider

    interface Parameters : TransformParameters {
        // only used for development purposes
        @get:Input
        @get:Optional
        val invalidate: Property
    }

    override fun transform(outputs: TransformOutputs) {
        val input = inputArtifact.get().asFile
        val jarFile = JarFile(input)
        if (jarFile.isMultiRelease) {
            val outputFilename = "${input.nameWithoutExtension}-meta-inf-stripped.jar"
            val tmpOutputFile = File.createTempFile(
                "sentry-transformed-${input.nameWithoutExtension}",
                ".jar"
            )
            var isStillMultiRelease = false
            tmpOutputFile.jarOutputStream().use { outStream ->
                val entries = jarFile.entries()
                // copy each .jar entry, except those that are under META-INF/versions/${unsupported_java_version}
                while (entries.hasMoreElements()) {
                    val jarEntry = entries.nextElement()
                    if (jarEntry.name.equals(JarFile.MANIFEST_NAME, ignoreCase = true)) {
                        // we deal with the manifest as a last step, since we need to know if there
                        // any multi-release entries remained
                        continue
                    }

                    if (jarEntry.isSignatureEntry) {
                        SentryPlugin.logger.warn {
                            """
                            Signed Multirelease Jar (${jarFile.name}) found, skipping transform.
                            This might lead to auto-instrumentation issues due to a bug in AGP (https://issuetracker.google.com/issues/206655905).
                            Please update to AGP >= 7.1.2 (https://developer.android.com/studio/releases/gradle-plugin) in order to keep using `autoInstrumentation` option.
                            """.trimIndent()
                        }
                        tmpOutputFile.delete()
                        outputs.file(inputArtifact)
                        return
                    }

                    if (jarEntry.name.startsWith(versionsDir, ignoreCase = true)) {
                        val javaVersion = jarEntry.javaVersion
                        if (javaVersion > MIN_SUPPORTED_JAVA_VERSION) {
                            continue
                        } else if (javaVersion > 0) {
                            isStillMultiRelease = true
                        }
                    }
                    outStream.putNextEntry(jarEntry)
                    jarFile.getInputStream(jarEntry).buffered().copyTo(outStream)
                    outStream.closeEntry()
                }

                val manifest = jarFile.manifest
                if (manifest != null) {
                    // write MANIFEST.MF as a last entry and modify Multi-Release attribute accordingly
                    manifest.mainAttributes.put(
                        Attributes.Name.MULTI_RELEASE,
                        isStillMultiRelease.toString()
                    )
                    outStream.putNextEntry(ZipEntry(JarFile.MANIFEST_NAME))
                    manifest.write(outStream.buffered())
                    outStream.closeEntry()
                }
            }
            val transformedOutput = outputs.file(outputFilename)
            Files.move(tmpOutputFile.toPath(), transformedOutput.toPath())
        } else {
            outputs.file(inputArtifact)
        }
    }

    private val JarEntry.isSignatureEntry get() = signatureFileRegex.matches(name)

    private val JarEntry.javaVersion: Int get() = regex.find(name)?.value?.toIntOrNull() ?: 0

    private fun File.jarOutputStream(): JarOutputStream = JarOutputStream(outputStream())

    companion object {
        private val regex = "(?<=/)([0-9]*)(?=/)".toRegex()
        private val signatureFileRegex = "^META-INF/.*\\.(SF|DSA|RSA|EC)|^META-INF/SIG-.*"
            .toRegex()
        private const val versionsDir = "META-INF/versions/"
        private const val MIN_SUPPORTED_JAVA_VERSION = 11

        internal val artifactType: Attribute =
            Attribute.of("artifactType", String::class.java)
        internal val metaInfStripped: Attribute =
            Attribute.of("meta-inf-stripped", Boolean::class.javaObjectType)

        internal fun register(dependencies: DependencyHandler, forceInstrument: Boolean) {
            // add our attribute to schema
            dependencies.attributesSchema { schema ->
                schema.attribute(metaInfStripped) { matchingStrategy ->
                    /*
                    * This is necessary so our transform is selected before the AGP's one.
                    * AGP's transform chain looks roughly like that:
                    *
                    * IdentityTransform (jar to processed-jar) -> IdentityTransform (processed-jar to classes-jar)
                    * -> AsmClassesTransform (classes-jar to asm-transformed-classes-jar)
                    *
                    * We want out transform to run before the first IdentityTransform, so the chain
                    * would look like that:
                    *
                    * MetaInfStripTransform (jar to processed-jar) -> IdentityTransform (jar to processed-jar)
                    * -> IdentityTransform (...) -> AsmClassesTransform (...)
                    *
                    * Since the first two transforms have conflicting attributes, we define a
                    * disambiguation rule to make Gradle select our transform first.
                    */
                    matchingStrategy.disambiguationRules
                        .pickFirst { o1, o2 -> if (o1 > o2) -1 else if (o1 < o2) 1 else 0 }
                }
            }
            // sets meta-inf-stripped attr to false for all .jar artifacts
            dependencies.artifactTypes.named("jar") {
                it.attributes.attribute(metaInfStripped, false)
            }
            // registers a transform
            dependencies.registerTransform(MetaInfStripTransform::class.java) {
                it.from.attribute(artifactType, "jar").attribute(metaInfStripped, false)
                it.to.attribute(artifactType, "processed-jar").attribute(metaInfStripped, true)

                if (forceInstrument) {
                    it.parameters.invalidate.set(System.currentTimeMillis())
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy