From dec0bb69a2dd38954377b2c71808b5a6567417c8 Mon Sep 17 00:00:00 2001 From: carloscoria-bfly Date: Mon, 18 May 2026 13:26:38 -0500 Subject: [PATCH] [AND][MERC-5813] Update transformer --- realm-transformer/build.gradle | 108 -------- realm-transformer/build.gradle.kts | 48 ++++ realm-transformer/settings.gradle | 1 - realm-transformer/settings.gradle.kts | 1 + .../java/io/realm/transformer/Version.java | 10 + .../io/realm/transformer/RealmTransformer.kt | 2 +- .../io/realm/transformer/ext/ProjectExt.kt | 65 +++-- .../src/main/templates/Version.java | 6 - .../realm/transformer/ByteCodeModifierTest.kt | 238 ------------------ 9 files changed, 90 insertions(+), 389 deletions(-) delete mode 100644 realm-transformer/build.gradle create mode 100644 realm-transformer/build.gradle.kts delete mode 100644 realm-transformer/settings.gradle create mode 100644 realm-transformer/settings.gradle.kts create mode 100644 realm-transformer/src/main/java/io/realm/transformer/Version.java delete mode 100644 realm-transformer/src/main/templates/Version.java delete mode 100644 realm-transformer/src/test/kotlin/io/realm/transformer/ByteCodeModifierTest.kt diff --git a/realm-transformer/build.gradle b/realm-transformer/build.gradle deleted file mode 100644 index 7982fcbb5f..0000000000 --- a/realm-transformer/build.gradle +++ /dev/null @@ -1,108 +0,0 @@ -buildscript { - def properties = new Properties() - properties.load(new FileInputStream("${projectDir}/../dependencies.list")) - ext.kotlin_version = properties.get('KOTLIN') - repositories { - google() - jcenter() - maven { - url "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath "io.github.gradle-nexus:publish-plugin:${properties.get("GRADLE_NEXUS_PLUGIN")}" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -apply plugin: 'kotlin' -apply plugin: 'java' -apply plugin: 'maven-publish' - -group = 'io.realm' -version = file("${projectDir}/../version.txt").text.trim() - -def properties = new Properties() -properties.load(new FileInputStream("${projectDir}/../dependencies.list")) - -def coreVersion = properties.getProperty('REALM_CORE') - -sourceCompatibility = JavaVersion.VERSION_11 -targetCompatibility = JavaVersion.VERSION_11 - -repositories { - mavenLocal() - google() - jcenter() -} - -configurations { - provided - compile.extendsFrom provided -} - -// Include the generated Version file -sourceSets { - main { - compileClasspath += configurations.provided - java.srcDirs += ['build/generated-src/main/java', 'src/main/kotlin'] - } -} - -dependencies { - implementation gradleApi() - implementation "io.realm:realm-annotations:${version}" - compileOnly "com.android.tools.build:gradle:${properties.get("GRADLE_BUILD_TOOLS")}" - implementation 'org.javassist:javassist:3.25.0-GA' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" - implementation "com.google.guava:guava:31.0.1-jre" - // JAX-B dependencies for JDK 9+ (this is not available in JVM env 'java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter' - // and it was removed in Java 11 https://stackoverflow.com/a/43574427 - implementation("javax.xml.bind:jaxb-api:2.3.1") - - testImplementation 'junit:junit:4.12' -} - -// for Ant filter -import org.apache.tools.ant.filters.ReplaceTokens - -task generateVersionClass(type: Copy) { - from 'src/main/templates/Version.java' - into 'build/generated-src/main/java/io/realm/transformer' - filter(ReplaceTokens, tokens: [version: version, coreVersion: coreVersion]) - outputs.upToDateWhen { false } -} - -compileKotlin.dependsOn generateVersionClass - -apply from: "${rootDir}/../mavencentral-publications.gradle" -apply from: "${rootDir}/../mavencentral-publish.gradle" - -publishing { - publications { - realmPublication(MavenPublication) { - groupId 'io.realm' - artifactId = 'realm-transformer' - from components.java - populatePom( - it, - 'realm-transformer', - 'Android Gradle Transformer for Realm. Realm is a mobile database: Build better apps, faster.' - ) - } - } -} - -java { - withSourcesJar() - withJavadocJar() - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} - -compileKotlin { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - freeCompilerArgs = ["-Xinline-classes", "-Xjvm-default=all-compatibility"] - } -} diff --git a/realm-transformer/build.gradle.kts b/realm-transformer/build.gradle.kts new file mode 100644 index 0000000000..7dc403767d --- /dev/null +++ b/realm-transformer/build.gradle.kts @@ -0,0 +1,48 @@ +// Local fork of io.realm:realm-transformer:10.19.0 with AGP 9 compatibility patches. +// Published to the composite build under the same coordinates so dependency substitution +// kicks in automatically for any consumer that pulls `io.realm:realm-gradle-plugin:10.19.0`. + +plugins { + kotlin("jvm") version "2.2.21" + `java-library` + `maven-publish` +} + +group = "io.realm" +version = "10.19.0" + +repositories { + google() + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + } +} + +dependencies { + implementation(gradleApi()) + implementation("io.realm:realm-annotations:10.19.0") + compileOnly("com.android.tools.build:gradle:9.0.0") + implementation("org.javassist:javassist:3.25.0-GA") + implementation("com.google.guava:guava:31.0.1-jre") + implementation("javax.xml.bind:jaxb-api:2.3.1") +} + +publishing { + publications { + create("realmPublication") { + groupId = "io.realm" + artifactId = "realm-transformer" + version = "10.19.0" + from(components["java"]) + } + } +} diff --git a/realm-transformer/settings.gradle b/realm-transformer/settings.gradle deleted file mode 100644 index dfae22fab0..0000000000 --- a/realm-transformer/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'realm-transformer' diff --git a/realm-transformer/settings.gradle.kts b/realm-transformer/settings.gradle.kts new file mode 100644 index 0000000000..516d17170c --- /dev/null +++ b/realm-transformer/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "realm-transformer" diff --git a/realm-transformer/src/main/java/io/realm/transformer/Version.java b/realm-transformer/src/main/java/io/realm/transformer/Version.java new file mode 100644 index 0000000000..d4aaa2edb4 --- /dev/null +++ b/realm-transformer/src/main/java/io/realm/transformer/Version.java @@ -0,0 +1,10 @@ +package io.realm.transformer; + +/** + * Hand-authored replacement for the original Ant-templated Version.java. Coordinates pinned + * to upstream Realm Java 10.19.0 (and its Realm Core dependency) so this fork is a drop-in. + */ +public class Version { + public static final String VERSION = "10.19.0"; + public static final String SYNC_VERSION = "13.26.0"; +} diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt index b84f7d181a..4bf1fc60a6 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt @@ -252,7 +252,7 @@ abstract class RealmTransformerTask : DefaultTask() { val jarFileOutput: FileSystem = output.get().let { jarFile -> // Workaround to create the Jar if does not exist, as FileSystems fails to do so. touchJarFile(jarFile) - FileSystems.newFileSystem(output.get().asFile.toPath(), null) + FileSystems.newFileSystem(output.get().asFile.toPath(), null as ClassLoader?) } val build: BuildTemplate = diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt index ed2db09b1d..b6b3338709 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt @@ -12,11 +12,19 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * -- + * Modified by Butterfly Network for AGP 9 compatibility. The legacy + * `com.android.build.gradle.BaseExtension` API was removed in AGP 9; this file now + * reads `bootClasspath` from `AndroidComponentsExtension.sdkComponents` and treats the + * SDK-version analytics fields as best-effort (returning "unknown" if the modern DSL + * shape ever changes again). */ package io.realm.transformer.ext -import com.android.build.gradle.BaseExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.variant.AndroidComponentsExtension import org.gradle.api.Project import java.io.File @@ -25,69 +33,56 @@ import java.io.File */ fun Project.getAppId(): String { // Use the Root project name, usually set in `settings.gradle` - // This means that we don't treat apps with multiple flavours as different, nor - // if a project contains more than one app (probably unlikely). - // This seems acceptable. These cases would just show up as more builds for the - // same AppId. return this.rootProject.name } /** * Returns the `targetSdk` property for this project if it is available. + * Reflectively reads `defaultConfig.targetSdk` so the same code works against both + * `ApplicationExtension` (which has it) and `LibraryExtension` (which does not). */ -fun Project.getTargetSdk(): String { - return getAndroidExtension(this).defaultConfig.targetSdkVersion?.apiString ?: "unknown" -} +fun Project.getTargetSdk(): String = readDefaultConfigInt("getTargetSdk") /** * Returns the `minSdk` property for this project if it is available. */ -fun Project.getMinSdk(): String { - return getAndroidExtension(this).defaultConfig.minSdkVersion?.apiString ?: "unknown" -} +fun Project.getMinSdk(): String = readDefaultConfigInt("getMinSdk") + +private fun Project.readDefaultConfigInt(getter: String): String = + runCatching { + @Suppress("UnstableApiUsage") + val ext = extensions.getByType(CommonExtension::class.java) + val defaultConfig: Any = ext.defaultConfig + val method = defaultConfig.javaClass.methods.firstOrNull { it.name == getter && it.parameterCount == 0 } + method?.invoke(defaultConfig)?.toString() ?: "unknown" + }.getOrDefault("unknown") /** * Returns the version of the Android Gradle Plugin that is used. */ fun Project.getAgpVersion(): String { - // This API is only available from AGP 7.0.0. And it is a bit unclear exactly which part of - // this is actually stable. Also, there appear to be problems with depending on AGP 7.* on - // the compile classpath (it cannot load BaseExtension). - // - // So for now, this code assumes that we are compiling against AGP 4.1 and uses reflection - // to try to grap the AGP version. - // - // This is done with a best-effort, but we just - // accept finding the version isn't possible if anything goes wrong. return try { val extension = this.extensions.getByName("androidComponents") as Object val method = extension.`class`.getMethod("getPluginVersion") val version = method.invoke(extension) - if (version != null) { - return version.toString() - } else { - return "unknown" - } + version?.toString() ?: "unknown" } catch (e: Exception) { "unknown" } } /** - * Returns the `bootClasspath` for this project + * Returns the `bootClasspath` for this project. AGP 9 exposes this via the modern + * `AndroidComponentsExtension.sdkComponents.bootClasspath` provider; we resolve it + * eagerly here because the transformer task needs a `List`. */ fun Project.getBootClasspath(): List { - return getAndroidExtension(this).bootClasspath -} - -private fun getAndroidExtension(project: Project): BaseExtension { - // This will always be present, otherwise the android build would not be able to - // trigger the transformer code in the first place. - return project.extensions.getByName("android") as BaseExtension + val androidComponents = extensions.getByType(AndroidComponentsExtension::class.java) + return androidComponents.sdkComponents.bootClasspath.get().map { it.asFile } } fun Project.areIncrementalBuildsDisabled() = - if(extensions.extraProperties.has("io.realm.disableIncrementalBuilds")){ + if (extensions.extraProperties.has("io.realm.disableIncrementalBuilds")) { extensions.extraProperties["io.realm.disableIncrementalBuilds"] == "true" } else { false @@ -103,4 +98,4 @@ fun Project.targetType(): String = with(project.plugins) { fun Project.usesKotlin(): Boolean { return project.pluginManager.hasPlugin("kotlin-kapt") -} \ No newline at end of file +} diff --git a/realm-transformer/src/main/templates/Version.java b/realm-transformer/src/main/templates/Version.java deleted file mode 100644 index 9ea6a82503..0000000000 --- a/realm-transformer/src/main/templates/Version.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.realm.transformer; - -public class Version { - public static final String VERSION = "@version@"; - public static final String SYNC_VERSION = "@coreVersion@"; -} diff --git a/realm-transformer/src/test/kotlin/io/realm/transformer/ByteCodeModifierTest.kt b/realm-transformer/src/test/kotlin/io/realm/transformer/ByteCodeModifierTest.kt deleted file mode 100644 index 0abb726cec..0000000000 --- a/realm-transformer/src/test/kotlin/io/realm/transformer/ByteCodeModifierTest.kt +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright 2018 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.realm.transformer - -import io.realm.annotations.Ignore -import javassist.* -import javassist.bytecode.AnnotationsAttribute -import javassist.bytecode.ConstPool -import javassist.bytecode.Opcode -import javassist.bytecode.annotation.Annotation -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import java.lang.reflect.Modifier - -class ByteCodeModifierTest { - - @Test - fun addRealmAccessors() { - // Generate an empty class - val classPool = ClassPool.getDefault() - val ctClass = classPool.makeClass("testClass") - - // Add a field - val ctField = CtField(CtClass.intType, "age", ctClass) - ctClass.addField(ctField) - - // The accessors are added - BytecodeModifier.addRealmAccessors(ctClass) - - // The accessors are generated - val ctMethods = ctClass.declaredMethods - val methodNames = ctMethods.map { it.name } - assertTrue(methodNames.contains("realmGet\$age")) - assertTrue(methodNames.contains("realmSet\$age")) - - // The accessors are public - ctMethods.forEach { - assertTrue(it.modifiers == Modifier.PUBLIC) - } - } - - // https://github.com/realm/realm-java/issues/3469 - @Test - fun addRealmAccessors_duplicateSetter() { - // Generate an empty class - val classPool = ClassPool.getDefault() - val ctClass = classPool.makeClass("testClass") - - // Add a field - val ctField = CtField(CtClass.intType, "age", ctClass) - ctClass.addField(ctField) - - // Add a setter - val setter = CtNewMethod.setter("realmSet\$age", ctField) - ctClass.addMethod(setter) - - // addRealmAccessors is called - BytecodeModifier.addRealmAccessors(ctClass) - - // a getter for the field is generated - val ctMethods = ctClass.declaredMethods - val methodNames = ctMethods.map { it.name } - assertTrue(methodNames.contains("realmGet\$age")) - - // the setter is not changed - assertTrue(ctMethods.find { it.name == "realmSet\$age" } == setter) - - // accessors are public - ctMethods.forEach { - assertTrue(it.modifiers == Modifier.PUBLIC) - } - } - - // https://github.com/realm/realm-java/issues/3469 - @Test - fun addRealmAccessors_duplicateGetter() { - // Generate an empty class - val classPool = ClassPool.getDefault() - val ctClass = classPool.makeClass("testClass") - - // Add a field - val ctField = CtField(CtClass.intType, "age", ctClass) - ctClass.addField(ctField) - - // Add a getter - val setter = CtNewMethod.setter("realmGet\$age", ctField) - ctClass.addMethod(setter) - - // addRealmAccessors is called - BytecodeModifier.addRealmAccessors(ctClass) - - // a setter for the field is generated - val ctMethods = ctClass.declaredMethods - val methodNames = ctMethods.map { it.name } - assertTrue(methodNames.contains("realmSet\$age")) - - // the getter is not changed - assertTrue(ctMethods.find { it.name == "realmGet\$age" } == setter) - - // accessors are public - ctMethods.forEach { - assertTrue(it.modifiers == Modifier.PUBLIC) - } - } - - @Test - fun addRealmAccessors_ignoreAnnotation() { - // Generate an empty class - val classPool = ClassPool.getDefault() - val ctClass = classPool.makeClass("testClass") - val constPool = ConstPool("TestClass") - - // Add a field with @Ignore - val ctField = CtField(CtClass.intType, "age", ctClass) - ctClass.addField(ctField) - val attr = AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag) - val ignoreAnnotation = Annotation(Ignore::class.java.name, constPool) - attr.addAnnotation(ignoreAnnotation) - ctField.fieldInfo.addAttribute(attr) - - // Try to add the accessor - BytecodeModifier.addRealmAccessors(ctClass) - - // the accessor should not be generated - // a setter for the field is generated - val ctMethods = ctClass.declaredMethods - val methodNames = ctMethods.map { it.name } - assertFalse(methodNames.contains("realmSet\$age")) - assertFalse(methodNames.contains("realmGet\$age")) - } - - @Test - fun userRealmAccessors() { - // Generate an empty class - val classPool = ClassPool.getDefault() - val ctClass = classPool.makeClass("testClass") - - // Add a field - val ctField = CtField(CtClass.intType, "age", ctClass) - ctClass.addField(ctField) - - // Add a method that uses such field - val ctMethod = CtNewMethod.make("public boolean canDrive() { return this.age >= 18; }", ctClass) - ctClass.addMethod(ctMethod) - - // Realm accessors are called - BytecodeModifier.addRealmAccessors(ctClass) - - // the field use is replaced by the accessor - BytecodeModifier.useRealmAccessors(classPool, ctClass, listOf(ctField)) - - // the field is not used and getter is called in the method - assertTrue(!isFieldRead(ctMethod) && hasMethodCall(ctMethod)) - } - - fun userRealmAccessors_fieldAccessConstructorIsTransformed() { - // Generate an empty class - val classPool = ClassPool.getDefault() - val ctClass = classPool.makeClass("testClass") - val constPool = ConstPool("TestClass") - - // Add a field with @Ignore - val ctField = CtField(CtClass.intType, "age", ctClass) - ctClass.addField(ctField) - - // Add a method sets such field - val ctMethod = CtNewMethod.make("private void setupAge(int age) { this.age = age; }", ctClass) - ctClass.addMethod(ctMethod) - - // Add a default constructor that uses the method - val ctDefaultConstructor = CtNewConstructor.make("public TestClass() { int myAge = this.age; }", ctClass) - ctClass.addConstructor(ctDefaultConstructor) - - // Add a non-default constructor that uses the method - val ctNonDefaultConstructor = CtNewConstructor.make("public TestClass(TestClass other) { int otherAge = other.age; }", ctClass) - ctClass.addConstructor(ctNonDefaultConstructor) - - // Realm accessors are added - BytecodeModifier.addRealmAccessors(ctClass) - - // the field use is replaced by the accessor - BytecodeModifier.useRealmAccessors(classPool, ctClass, listOf(ctField)) - - // the field is not used in the method anymore - assertTrue(!isFieldRead(ctDefaultConstructor) - && hasMethodCall(ctDefaultConstructor) - && !isFieldRead(ctNonDefaultConstructor) - && hasMethodCall(ctNonDefaultConstructor)) - } - - private fun isFieldRead(behavior: CtBehavior): Boolean { - val methodInfo = behavior.methodInfo - val codeAttribute = methodInfo.codeAttribute - - val it = codeAttribute.iterator() - var index = 0; - while (it.hasNext()) { - val op: Int = it.byteAt(index) - index = it.next() - if (op == Opcode.GETFIELD) { - return true; - } - } - return false - } - - private fun hasMethodCall(behavior: CtBehavior): Boolean { - val methodInfo = behavior.methodInfo - val codeAttribute = methodInfo.codeAttribute - - val it = codeAttribute.iterator() - var index = 0; - while (it.hasNext()) { - val op: Int = it.byteAt(index) - index = it.next() - if (op == Opcode.INVOKEVIRTUAL) { - return true; - } - } - return false - } - -}