Skip to content

Commit

Permalink
Add PreviewTransform to aggregate methods
Browse files Browse the repository at this point in the history
  • Loading branch information
runningcode committed Mar 4, 2025
1 parent f71e77c commit b6629e4
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ data class ArtifactMetadata(
val testArtifactZipPath: String? = null,
val proguardMappingsZipPath: String? = null,
val dependencyMetadataZipPath: String? = null,
val snapshotConfigFile: String? = null,
val ciDebugData: CIDebugData? = null,
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ import com.emergetools.android.gradle.tasks.base.ArtifactMetadata
import com.emergetools.android.gradle.tasks.base.BasePreflightTask.Companion.setPreflightTaskInputs
import com.emergetools.android.gradle.tasks.base.BaseUploadTask.Companion.setTagFromProductOptions
import com.emergetools.android.gradle.tasks.base.BaseUploadTask.Companion.setUploadTaskInputs
import com.emergetools.android.gradle.tasks.snapshots.transform.AggregatePreviewMethodsTask
import com.emergetools.android.gradle.tasks.snapshots.transform.PreviewAnalyzerTransform
import com.emergetools.android.gradle.tasks.snapshots.transform.TransformProjectClasses
import com.emergetools.android.gradle.util.AgpVersions
import com.emergetools.android.gradle.util.capitalize
import com.emergetools.android.gradle.util.hasDependency
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.attributes.Attribute
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.TaskProvider

const val EMERGE_SNAPSHOTS_TASK_GROUP = "Emerge snapshots"
Expand All @@ -34,14 +40,50 @@ fun registerSnapshotTasks(

registerSnapshotPreflightTask(appProject, extension, variant)

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 flagEnabled = true
if (flagEnabled) {

val artifactType = Attribute.of("artifactType", String::class.java)
appProject.dependencies.registerTransform(PreviewAnalyzerTransform::class.java) { transform ->
transform.from.attribute(artifactType, "android-classes")
transform.to.attribute(artifactType, "transformed-preview")
}
val name = variant.name
val transformProjectClasses = appProject.tasks.register(name.transformProjectClassesTaskName, TransformProjectClasses::class.java) {
it.outputFile.set(appProject.layout.buildDirectory.dir("transformed-classes-$name").map { it.file("output.txt") })
// it.inputDir.set(appProject.tasks.named(name.kotlinCompileTaskName, KotlinCompile::class.java).flatMap { it.ou })
// get the KotlinCompile task reflectively
val kotlinCompile = appProject.tasks.named(name.kotlinCompileTaskName)
// get the destination dir property of the KotlinCompile task reflectively
val destinationDir = kotlinCompile.get().javaClass.getDeclaredMethod("getDestinationDirectory").invoke(kotlinCompile.get()) as DirectoryProperty
it.inputDir.set(destinationDir)
// Set input dir to the output dir of the kotlin compile task
// it.inputDir.set(appProject.layout.buildDirectory.dir("intermediates/javac/$name"))
}
appProject.tasks.register(name.aggregatePreviewMethodsName, AggregatePreviewMethodsTask::class.java) { task ->
task.outputFile.set(appProject.layout.buildDirectory.file("aggregated-$name/foo.txt"))


task.inputFiles.from(appProject.configurations.named("${name}RuntimeClasspath").map { configuration ->

configuration.incoming.artifactView { view ->
view.componentFilter { component ->
component is ProjectComponentIdentifier
}
view.attributes.attribute(artifactType, "transformed-preview")
}.files
}, transformProjectClasses)
}
} else {
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())
}
}
}
}
Expand Down Expand Up @@ -173,3 +215,12 @@ private fun registerSnapshotUploadTask(
fun getSnapshotUploadTaskName(variantName: String): String {
return "${EMERGE_TASK_PREFIX}UploadSnapshotBundle${variantName.capitalize()}"
}

private val String.transformProjectClassesTaskName : String
get() = "transform${capitalize()}ProjectClasses"

private val String.kotlinCompileTaskName
get() = "compile${capitalize()}Kotlin"

private val String.aggregatePreviewMethodsName
get() = "aggregate${capitalize()}PreviewMethods"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.emergetools.android.gradle.tasks.snapshots.transform

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

@OptIn(ExperimentalSerializationApi::class)
abstract class AggregatePreviewMethodsTask : DefaultTask() {
@get:InputFiles
abstract val inputFiles: ConfigurableFileCollection

@get:OutputFile
abstract val outputFile: RegularFileProperty

@TaskAction
fun aggregate() {
val output = outputFile.get().asFile
output.parentFile.mkdirs()

output.writeText("")

// Aggregate all preview method files
val list = inputFiles.flatMap { Json.decodeFromStream<List<ComposePreviewSnapshotConfig>>(it.inputStream()) }
output.writeText(Json.encodeToString(list))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.emergetools.android.gradle.tasks.snapshots.transform

import kotlinx.serialization.Serializable

@Serializable
data class ComposePreviewSnapshotConfig(
val fullyQualifiedClassName: String? = null,
val originalFqn: String,
val sourceFileName: String? = null,
var isAppStoreSnapshot: Boolean? = null,
val previewParameter: PreviewParameter? = null,
var name: String? = null,
var group: String? = null,
var uiMode: Int? = null,
var locale: String? = null,
var fontScale: Float? = null,
var heightDp: Int? = null,
var widthDp: Int? = null,
var showBackground: Boolean? = null,
var backgroundColor: Int? = null,
var showSystemUi: Boolean? = null,
var device: String? = null,
var apiLevel: Int? = null,
var wallpaper: Int? = null,
)

@Serializable
data class PreviewParameter(
val parameterName: String,
val providerClassFqn: String,
val limit: Int? = null,
val index: Int? = null
)

fun String.cleanName(): String {
var newName = this.replace("/", ".")
// Strip .class suffix
if (newName.endsWith(".class")) {
newName = newName.substring(0, newName.length - 6)
}
return newName
}

fun String.removeClassName(): String {
return this.substringBeforeLast(".")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.emergetools.android.gradle.tasks.snapshots.transform

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
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.file.FileSystemLocation
import org.gradle.api.provider.Provider
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
import java.io.File
import java.util.jar.JarFile

abstract class PreviewAnalyzerTransform : TransformAction<TransformParameters.None> {
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>

override fun transform(outputs: TransformOutputs) {
val input = inputArtifact.get().asFile
if (!input.name.endsWith(".jar") && !input.name.endsWith(".aar") && !input.isDirectory) {
return
}

val outputFile = outputs.file("output.txt")

val previewMethods = when {
input.isDirectory -> findMethodsInDirectory(input)
input.name.endsWith(".jar") -> analyzeJarFile(input)
input.name.endsWith(".aar") -> analyzeAarFile(input)
else -> throw IllegalArgumentException("Unsupported input type: $input")
}

val json = Json.encodeToString(previewMethods.toList())

outputFile.writeText(json)
}

private fun analyzeJarFile(inputJar: File): Sequence<ComposePreviewSnapshotConfig> {
val methods = mutableListOf<ComposePreviewSnapshotConfig>()
JarFile(inputJar).use { jarFile ->
jarFile.entries().asSequence()
.filter { it.name.endsWith(".class") }
.forEach { classEntry ->
jarFile.getInputStream(classEntry).use { inputStream ->
methods.addAll(extractPreviewMethodsFromBytes(classEntry.realName , inputStream.readBytes()))
}
}
}
return methods.asSequence()
}

private fun findMethodsInDirectory(directory: File) : Sequence<ComposePreviewSnapshotConfig> {
return directory.findPreviewMethodsInDirectory()
}

private fun analyzeAarFile(aarFile: File) : Sequence<ComposePreviewSnapshotConfig> {
TODO("analzying aar file not yet implemented")
// Implementation of AAR analysis using ZipFile to avoid copying to temp dir
// This is more configuration cache friendly than using Project.zipTree
}



companion object {
fun File.findPreviewMethodsInDirectory(): Sequence<ComposePreviewSnapshotConfig> {
return walk()
.filter { it.name.endsWith(".class") }
.flatMap { classFile ->
extractPreviewMethodsFromBytes(classFile.name, classFile.readBytes())
}
}

fun extractPreviewMethodsFromBytes(fileName: String, byteStream: ByteArray): List<ComposePreviewSnapshotConfig> {
val classReader = ClassReader(byteStream)
val methodNames = mutableListOf<ComposePreviewSnapshotConfig>()

val visitor = SnapshotAggregatorClassVisitor(Opcodes.ASM9, fileName, classReader.className, methodNames)

classReader.accept(visitor, ClassReader.EXPAND_FRAMES)

return methodNames
}
}
}

Loading

0 comments on commit b6629e4

Please sign in to comment.