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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create SnapshotsPreviewRuntimeRetentionTransform to change Previews to runtime retention #139

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.emergetools.android.gradle

import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.dsl.TestExtension
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.api.variant.AndroidTest
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.ApplicationVariant
Expand All @@ -11,6 +12,7 @@ import com.android.build.api.variant.Variant
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.android.build.gradle.internal.utils.KOTLIN_ANDROID_PLUGIN_ID
import com.emergetools.android.gradle.instrumentation.snapshots.SnapshotsPreviewRuntimeRetentionTransformFactory
import com.emergetools.android.gradle.tasks.internal.SaveExtensionConfigTask
import com.emergetools.android.gradle.tasks.perf.GeneratePerfProject
import com.emergetools.android.gradle.tasks.perf.LocalPerfTest
Expand Down Expand Up @@ -94,11 +96,14 @@ class EmergePlugin : Plugin<Project> {
appProject: Project,
emergeExtension: EmergePluginExtension,
) {
appProject.afterEvaluate {
configureAppProjectSnapshots(
appProject = appProject,
emergeExtension = emergeExtension
)
// Only configure KSP for snapshot generation if the experimental transform is disabled
if (!emergeExtension.snapshotOptions.experimentalTransformEnabled.getOrElse(false)) {
appProject.afterEvaluate {
configureAppProjectSnapshots(
appProject = appProject,
emergeExtension = emergeExtension
)
}
}

appProject.pluginManager.withPlugin(ANDROID_APPLICATION_PLUGIN_ID) { _ ->
Expand Down Expand Up @@ -300,6 +305,20 @@ class EmergePlugin : Plugin<Project> {
variant: ApplicationVariant,
androidTest: AndroidTest,
) {
if (extension.snapshotOptions.experimentalTransformEnabled.getOrElse(false)) {
variant.instrumentation.let { instrumentation ->
instrumentation.transformClassesWith(
SnapshotsPreviewRuntimeRetentionTransformFactory::class.java,
InstrumentationScope.ALL,
) { params ->
// Force invalidate/reinstrument classes if debug option is set
if (extension.debugOptions.forceInstrumentation.getOrElse(false)) {
params.invalidate.set(System.currentTimeMillis())
}
}
}
}

val snapshotPackageTask = registerSnapshotPackageTask(appProject, variant, androidTest)
registerSnapshotLocalTask(appProject, extension, variant, androidTest, snapshotPackageTask)
registerSnapshotUploadTask(appProject, extension, variant, snapshotPackageTask)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ abstract class EmergePluginExtension @Inject constructor(objects: ObjectFactory)
action.execute(vcsOptions)
}

@get:Nested
abstract val debugOptions: DebugOptions

fun debug(action: Action<DebugOptions>) {
action.execute(debugOptions)
}

/**
* Optional inputs.
*/
Expand Down Expand Up @@ -140,5 +147,11 @@ abstract class SnapshotOptions : ProductOptions() {

abstract val experimentalInternalSnapshotsEnabled: Property<Boolean>

abstract val experimentalTransformEnabled: Property<Boolean>

abstract val apiVersion: Property<Int>
}

abstract class DebugOptions : ProductOptions() {
abstract val forceInstrumentation: Property<Boolean>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.emergetools.android.gradle.instrumentation.snapshots

import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.slf4j.Logger
import org.slf4j.LoggerFactory

abstract class SnapshotsPreviewRuntimeRetentionTransformFactory : AsmClassVisitorFactory<SnapshotsPreviewRuntimeRetentionTransformFactory.Params> {

interface Params : InstrumentationParameters {
@get:Input
val invalidate: Property<Long>
}

companion object {
private val logger by lazy {
LoggerFactory.getLogger(SnapshotsPreviewRuntimeRetentionTransformFactory::class.java)
}
}

override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor,
): ClassVisitor {
return SnapshotsPreviewRuntimeRetentionTransform(
instrumentationContext.apiVersion.get(),
nextClassVisitor,
logger,
)
}

override fun isInstrumentable(classData: ClassData): Boolean {
// Need to instrument all classes to ensure that the annotations are visible at runtime
return true
}
}

class SnapshotsPreviewRuntimeRetentionTransform(
api: Int,
classVisitor: ClassVisitor?,
private val logger: Logger,
) : ClassVisitor(api, classVisitor) {

companion object {
const val TAG = "SnapshotRuntimePreviewClassVisitor"

const val PREVIEW_ANNOTATION_DESC = "Landroidx/compose/ui/tooling/preview/Preview;"
const val PREVIEW_CONTAINER_ANNOTATION_DESC =
"Landroidx/compose/ui/tooling/preview/Preview\$Container;"
}

override fun visitMethod(
access: Int,
name: String,
descriptor: String,
signature: String?,
exceptions: Array<String>?,
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)

return object : MethodVisitor(api, mv) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the need for this? Does the other override fun visitAnnotation() not cover all cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, that will only cover the annotations on the class-level. We need a separate method visitor to visit the annotations on each method, which is what this is doing.

Without this, we'd just be marking class-level Preview annotations as runtime, instead of class and method level which is what we want.

The class annotation visitor handles multipreview annotation classes that have the preview annotation, and the method annotation visitor handles actual preview methods annotated with Preview.

override fun visitAnnotation(
desc: String,
visible: Boolean,
): AnnotationVisitor? {
if (desc == PREVIEW_ANNOTATION_DESC || desc == PREVIEW_CONTAINER_ANNOTATION_DESC) {
logger.info(
"$TAG: Modifying method annotation visible at runtime to true for annotation $desc $visible"
)

// Force the annotation to be visible at runtime
return super.visitAnnotation(desc, true)
}

return super.visitAnnotation(desc, visible)
}
}
}

override fun visitAnnotation(
desc: String,
visible: Boolean,
): AnnotationVisitor? {
if (desc == PREVIEW_ANNOTATION_DESC || desc == PREVIEW_CONTAINER_ANNOTATION_DESC) {
logger.info(
"$TAG: Modifying class annotation visible at runtime to true for annotation $desc $visible"
)

// Force the annotation to be visible at runtime
return super.visitAnnotation(desc, true)
}

return super.visitAnnotation(desc, visible)
}
}
Loading