diff --git a/annotation-processors/build.gradle.kts b/annotation-processors/build.gradle.kts index de7d778ddc..da709dc2ca 100644 --- a/annotation-processors/build.gradle.kts +++ b/annotation-processors/build.gradle.kts @@ -16,6 +16,7 @@ */ import com.itsaky.androidide.build.config.BuildConfig +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -45,5 +46,5 @@ dependencies { } tasks.withType { - kotlinOptions.jvmTarget = "17" + compilerOptions.jvmTarget.set(JvmTarget.JVM_17) } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 34538dfb6b..d9dcb121a5 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,14 +153,29 @@ android { packaging { resources { - excludes.add("META-INF/DEPENDENCIES") - excludes.add("META-INF/gradle/incremental.annotation.processors") + excludes += "META-INF/DEPENDENCIES" + excludes += "META-INF/gradle/incremental.annotation.processors" + + pickFirsts += "kotlin/internal/internal.kotlin_builtins" + pickFirsts += "kotlin/reflect/reflect.kotlin_builtins" + pickFirsts += "kotlin/kotlin.kotlin_builtins" + pickFirsts += "kotlin/coroutines/coroutines.kotlin_builtins" + pickFirsts += "kotlin/ranges/ranges.kotlin_builtins" + pickFirsts += "kotlin/concurrent/atomics/atomics.kotlin_builtins" + pickFirsts += "kotlin/collections/collections.kotlin_builtins" + pickFirsts += "kotlin/annotation/annotation.kotlin_builtins" + + pickFirsts += "META-INF/FastDoubleParser-LICENSE" + pickFirsts += "META-INF/thirdparty-LICENSE" + pickFirsts += "META-INF/FastDoubleParser-NOTICE" + pickFirsts += "META-INF/thirdparty-NOTICE" } jniLibs { useLegacyPackaging = false } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -197,6 +212,10 @@ configurations.matching { it.name.contains("AndroidTest") }.configureEach { exclude(group = "com.google.protobuf", module = "protobuf-lite") } +configurations.configureEach { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-android-extensions-runtime") +} + dependencies { debugImplementation(libs.common.leakcanary) @@ -278,6 +297,7 @@ dependencies { implementation(projects.gradlePluginConfig) implementation(projects.subprojects.aaptcompiler) implementation(projects.subprojects.javacServices) + implementation(projects.subprojects.kotlinAnalysisApi) implementation(projects.subprojects.shizukuApi) implementation(projects.subprojects.shizukuManager) implementation(projects.subprojects.shizukuProvider) diff --git a/composite-builds/build-deps/java-compiler/build.gradle.kts b/composite-builds/build-deps/java-compiler/build.gradle.kts index 3a7714abdb..03743a2d50 100644 --- a/composite-builds/build-deps/java-compiler/build.gradle.kts +++ b/composite-builds/build-deps/java-compiler/build.gradle.kts @@ -16,10 +16,15 @@ */ plugins { - id("java-library") + id("java-library") } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} \ No newline at end of file + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + annotationProcessor(libs.google.auto.service) + implementation(libs.google.auto.service.annotations) +} diff --git a/composite-builds/build-deps/java-compiler/src/main/java/javac/internal/jrtfs/JrtFileSystemProvider.java b/composite-builds/build-deps/java-compiler/src/main/java/javac/internal/jrtfs/JrtFileSystemProvider.java index d122fbb0af..d48a738e5e 100644 --- a/composite-builds/build-deps/java-compiler/src/main/java/javac/internal/jrtfs/JrtFileSystemProvider.java +++ b/composite-builds/build-deps/java-compiler/src/main/java/javac/internal/jrtfs/JrtFileSystemProvider.java @@ -25,6 +25,8 @@ package javac.internal.jrtfs; +import com.google.auto.service.AutoService; + import java.io.*; import java.net.MalformedURLException; import java.net.URL; @@ -53,6 +55,7 @@ * but also compiled and delivered as part of the jrtfs.jar to support access * to the jimage file provided by the shipped JDK by tools running on JDK 8. */ +@AutoService(FileSystemProvider.class) public final class JrtFileSystemProvider extends FileSystemProvider { private volatile FileSystem theFileSystem; diff --git a/composite-builds/build-logic/desugaring/build.gradle.kts b/composite-builds/build-logic/desugaring/build.gradle.kts index bb17148415..1d5bf29482 100644 --- a/composite-builds/build-logic/desugaring/build.gradle.kts +++ b/composite-builds/build-logic/desugaring/build.gradle.kts @@ -1,3 +1,6 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /* * This file is part of AndroidIDE. * @@ -16,26 +19,34 @@ */ plugins { - `kotlin-dsl` + `kotlin-dsl` } dependencies { - implementation(gradleApi()) - implementation(libs.composite.desugaringCore) + implementation(gradleApi()) + implementation(libs.composite.desugaringCore) - compileOnly(libs.android.gradle.plugin) + compileOnly(libs.android.gradle.plugin) - testImplementation(libs.tests.junit) - testImplementation(libs.tests.google.truth) + testImplementation(libs.tests.junit) + testImplementation(libs.tests.google.truth) } gradlePlugin { - plugins { - create("desugaring") { - id = "com.itsaky.androidide.desugaring" - implementationClass = "com.itsaky.androidide.desugaring.DesugarGradlePlugin" - displayName = "AndroidIDE Method Desugaring Plugin" - description = "Gradle plugin for method desugaring in Android projects." - } - } + plugins { + create("desugaring") { + id = "com.itsaky.androidide.desugaring" + implementationClass = "com.itsaky.androidide.desugaring.DesugarGradlePlugin" + displayName = "AndroidIDE Method Desugaring Plugin" + description = "Gradle plugin for method desugaring in Android projects." + } + } +} + +tasks.withType { + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_2_1) + languageVersion.set(KotlinVersion.KOTLIN_2_1) + } } + diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt new file mode 100644 index 0000000000..6eab4658bb --- /dev/null +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/ClassRefReplacingMethodVisitor.kt @@ -0,0 +1,131 @@ +package com.itsaky.androidide.desugaring + +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Type + +/** + * Replaces all bytecode references to one or more classes within a method body. + * + * Covered visit sites: + * - [visitMethodInsn] — owner and embedded descriptor + * - [visitFieldInsn] — owner and field descriptor + * - [visitTypeInsn] — NEW / CHECKCAST / INSTANCEOF / ANEWARRAY operand + * - [visitLdcInsn] — class-literal Type constants + * - [visitLocalVariable] — local variable descriptor and generic signature + * - [visitMultiANewArrayInsn]— array descriptor + * - [visitTryCatchBlock] — caught exception type + * + * @param classReplacements Mapping from source internal name (slash-notation) + * to target internal name (slash-notation). An empty map is a no-op. + * + * @author Akash Yadav + */ +class ClassRefReplacingMethodVisitor( + api: Int, + mv: MethodVisitor?, + private val classReplacements: Map, +) : MethodVisitor(api, mv) { + + override fun visitMethodInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + isInterface: Boolean, + ) { + super.visitMethodInsn( + opcode, + replace(owner), + name, + replaceInDescriptor(descriptor), + isInterface, + ) + } + + override fun visitFieldInsn( + opcode: Int, + owner: String, + name: String, + descriptor: String, + ) { + super.visitFieldInsn( + opcode, + replace(owner), + name, + replaceInDescriptor(descriptor), + ) + } + + override fun visitTypeInsn(opcode: Int, type: String) { + super.visitTypeInsn(opcode, replace(type)) + } + + override fun visitLdcInsn(value: Any?) { + // Replace class-literal constants: Foo.class → Bar.class + if (value is Type && value.sort == Type.OBJECT) { + val replaced = replace(value.internalName) + if (replaced !== value.internalName) { + super.visitLdcInsn(Type.getObjectType(replaced)) + return + } + } + super.visitLdcInsn(value) + } + + override fun visitLocalVariable( + name: String, + descriptor: String, + signature: String?, + start: Label, + end: Label, + index: Int, + ) { + super.visitLocalVariable( + name, + replaceInDescriptor(descriptor), + replaceInSignature(signature), + start, + end, + index, + ) + } + + override fun visitMultiANewArrayInsn(descriptor: String, numDimensions: Int) { + super.visitMultiANewArrayInsn(replaceInDescriptor(descriptor), numDimensions) + } + + override fun visitTryCatchBlock( + start: Label, + end: Label, + handler: Label, + type: String?, + ) { + super.visitTryCatchBlock(start, end, handler, type?.let { replace(it) }) + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Replaces a bare internal class name (slash-notation). */ + private fun replace(internalName: String): String = + classReplacements[internalName] ?: internalName + + /** + * Substitutes every `L;` token in a JVM descriptor or generic + * signature with `L;`. + */ + private fun replaceInDescriptor(descriptor: String): String { + if (classReplacements.isEmpty()) return descriptor + var result = descriptor + for ((from, to) in classReplacements) { + result = result.replace("L$from;", "L$to;") + } + return result + } + + /** Delegates to [replaceInDescriptor]; returns `null` for `null` input. */ + private fun replaceInSignature(signature: String?): String? = + signature?.let { replaceInDescriptor(it) } +} \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt index 0a0e7b10f2..977aa8e7bc 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitor.kt @@ -1,41 +1,106 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring import com.android.build.api.instrumentation.ClassContext import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor import org.objectweb.asm.MethodVisitor /** * [ClassVisitor] implementation for desugaring. * + * Applies two transformations to every method body, in priority order: + * + * 1. **[DesugarMethodVisitor]** (outermost / highest priority) — fine-grained + * per-method-call replacement defined via [DesugarReplacementsContainer.replaceMethod]. + * Its output flows into the next layer. + * + * 2. **[ClassRefReplacingMethodVisitor]** (innermost) — bulk class-reference + * replacement defined via [DesugarReplacementsContainer.replaceClass]. + * Handles every site where a class name can appear in a method body. + * + * Class references that appear in field and method *declarations* (descriptors + * and generic signatures at the class-structure level) are also rewritten here. + * * @author Akash Yadav */ -class DesugarClassVisitor(private val params: DesugarParams, - private val classContext: ClassContext, api: Int, - classVisitor: ClassVisitor +class DesugarClassVisitor( + private val params: DesugarParams, + private val classContext: ClassContext, + api: Int, + classVisitor: ClassVisitor, ) : ClassVisitor(api, classVisitor) { - override fun visitMethod(access: Int, name: String?, descriptor: String?, - signature: String?, exceptions: Array? - ): MethodVisitor { - return DesugarMethodVisitor(params, classContext, api, - super.visitMethod(access, name, descriptor, signature, exceptions)) - } -} + /** + * Class replacement map in ASM internal (slash) notation. + * Derived lazily from the dot-notation map stored in [params]. + */ + private val slashClassReplacements: Map by lazy { + params.classReplacements.get() + .entries.associate { (from, to) -> + from.replace('.', '/') to to.replace('.', '/') + } + } + + // ------------------------------------------------------------------------- + // Class-structure level: rewrite descriptors in field / method declarations + // ------------------------------------------------------------------------- + + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any?, + ): FieldVisitor? = super.visitField( + access, + name, + replaceInDescriptor(descriptor), + replaceInSignature(signature), + value, + ) + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array?, + ): MethodVisitor { + // Rewrite the method's own descriptor/signature at the class-structure level. + val base = super.visitMethod( + access, + name, + descriptor?.let { replaceInDescriptor(it) }, + replaceInSignature(signature), + exceptions, + ) + + // Layer 1 — class-reference replacement inside the method body. + // Skip instantiation entirely when there are no class replacements. + val withClassRefs: MethodVisitor = when { + slashClassReplacements.isNotEmpty() -> + ClassRefReplacingMethodVisitor(api, base, slashClassReplacements) + else -> base + } + + // Layer 2 — fine-grained method-call replacement. + // Runs first; any instruction it emits flows through withClassRefs. + return DesugarMethodVisitor(params, classContext, api, withClassRefs) + } + + // ------------------------------------------------------------------------- + // Descriptor / signature helpers + // ------------------------------------------------------------------------- + + private fun replaceInDescriptor(descriptor: String): String { + if (slashClassReplacements.isEmpty()) return descriptor + var result = descriptor + for ((from, to) in slashClassReplacements) { + result = result.replace("L$from;", "L$to;") + } + return result + } + private fun replaceInSignature(signature: String?): String? = + signature?.let { replaceInDescriptor(it) } +} \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt index 069a5ca142..98f6bde68d 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarClassVisitorFactory.kt @@ -1,20 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring import com.android.build.api.instrumentation.AsmClassVisitorFactory @@ -28,51 +11,49 @@ import org.slf4j.LoggerFactory * * @author Akash Yadav */ -abstract class DesugarClassVisitorFactory : - AsmClassVisitorFactory { - - companion object { - - private val log = - LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java) - } - - override fun createClassVisitor(classContext: ClassContext, - nextClassVisitor: ClassVisitor - ): ClassVisitor { - val params = parameters.orNull - if (params == null) { - log.warn("Could not find desugaring parameters. Disabling desugaring.") - return nextClassVisitor - } - - return DesugarClassVisitor(params, classContext, - instrumentationContext.apiVersion.get(), nextClassVisitor) - } - - override fun isInstrumentable(classData: ClassData): Boolean { - val params = parameters.orNull - if (params == null) { - log.warn("Could not find desugaring parameters. Disabling desugaring.") - return false - } - - val isEnabled = params.enabled.get().also { isEnabled -> - log.debug("Is desugaring enabled: $isEnabled") - } - - if (!isEnabled) { - return false - } - - val includedPackages = params.includedPackages.get() - if (includedPackages.isNotEmpty()) { - val className = classData.className - if (!includedPackages.any { className.startsWith(it) }) { - return false - } - } - - return true - } +abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory { + + companion object { + private val log = + LoggerFactory.getLogger(DesugarClassVisitorFactory::class.java) + } + + private val desugarParams: DesugarParams? + get() = parameters.orNull ?: run { + log.warn("Could not find desugaring parameters. Disabling desugaring.") + null + } + + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor, + ): ClassVisitor { + val params = desugarParams ?: return nextClassVisitor + return DesugarClassVisitor( + params = params, + classContext = classContext, + api = instrumentationContext.apiVersion.get(), + classVisitor = nextClassVisitor, + ) + } + + override fun isInstrumentable(classData: ClassData): Boolean { + val params = desugarParams ?: return false + + val isEnabled = params.enabled.get().also { log.debug("Is desugaring enabled: $it") } + if (!isEnabled) return false + + // Class-reference replacement must scan every class — any class may + // contain a reference to the one being replaced, regardless of package. + if (params.classReplacements.get().isNotEmpty()) return true + + val includedPackages = params.includedPackages.get() + if (includedPackages.isNotEmpty()) { + if (!includedPackages.any { classData.className.startsWith(it) }) { + return false + } + } + + return true + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt index 1e5905b45c..315288e458 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/DesugarParams.kt @@ -1,20 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring import com.android.build.api.instrumentation.InstrumentationParameters @@ -32,33 +15,36 @@ import org.gradle.api.tasks.Input */ interface DesugarParams : InstrumentationParameters { - /** - * Whether the desugaring is enabled. - */ - @get:Input - val enabled: Property - - /** - * The replacement instructions. - */ - @get:Input - val replacements: MapProperty - - @get:Input - val includedPackages: SetProperty - - companion object { - - /** - * Sets [DesugarParams] properties from [DesugarExtension]. - */ - fun DesugarParams.setFrom(extension: DesugarExtension) { - replacements.convention(emptyMap()) - includedPackages.convention(emptySet()) - - enabled.set(extension.enabled) - replacements.set(extension.replacements.instructions) - includedPackages.set(extension.replacements.includePackages) - } - } + /** Whether desugaring is enabled. */ + @get:Input + val enabled: Property + + /** Fine-grained method-call replacement instructions. */ + @get:Input + val replacements: MapProperty + + /** Packages to scan for method-level replacements (empty = all packages). */ + @get:Input + val includedPackages: SetProperty + + /** + * Class-level replacement map: dot-notation source class → dot-notation + * target class. Any class may be instrumented when this is non-empty. + */ + @get:Input + val classReplacements: MapProperty + + companion object { + + fun DesugarParams.setFrom(extension: DesugarExtension) { + replacements.convention(emptyMap()) + includedPackages.convention(emptySet()) + classReplacements.convention(emptyMap()) + + enabled.set(extension.enabled) + replacements.set(extension.replacements.instructions) + includedPackages.set(extension.replacements.includePackages) + classReplacements.set(extension.replacements.classReplacements) + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt index 057fcc1cb9..1ad5f89f32 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/DesugarReplacementsContainer.kt @@ -1,20 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - package com.itsaky.androidide.desugaring.dsl import com.itsaky.androidide.desugaring.internal.parsing.InsnLexer @@ -30,101 +13,126 @@ import javax.inject.Inject /** * Defines replacements for desugaring. * + * Two replacement strategies are supported and can be combined freely: + * + * - **Method-level** ([replaceMethod]): replaces a specific method call with + * another, with full control over opcodes and descriptors. + * - **Class-level** ([replaceClass]): rewrites every bytecode reference to a + * given class (owners, descriptors, type instructions, LDC constants, etc.) + * with a replacement class. This is a broader, structural operation. + * + * When both apply to the same instruction, method-level replacement wins + * because it runs first in the visitor chain. + * * @author Akash Yadav */ abstract class DesugarReplacementsContainer @Inject constructor( - private val objects: ObjectFactory + private val objects: ObjectFactory, ) { - internal val includePackages = TreeSet() - - internal val instructions = - mutableMapOf() - - companion object { - - private val PACKAGE_NAME_REGEX = - Regex("""^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*${'$'}""") - } - - /** - * Adds the given packages to the list of packages that will be scanned for - * the desugaring process. By default, the list of packages is empty. An empty - * list will include all packages. - */ - fun includePackage(vararg packages: String) { - for (pck in packages) { - if (!PACKAGE_NAME_REGEX.matches(pck)) { - throw IllegalArgumentException("Invalid package name: $pck") - } - - includePackages.add(pck) - } - } - - /** - * Removes the given packages from the list of included packages. - */ - fun removePackage(vararg packages: String) { - includePackages.removeAll(packages.toSet()) - } - - /** - * Adds an instruction to replace the given method. - */ - fun replaceMethod(configure: Action) { - val instruction = objects.newInstance(ReplaceMethodInsn::class.java) - configure.execute(instruction) - addReplaceInsns(instruction) - } - - /** - * Replace usage of [sourceMethod] with the [targetMethod]. - */ - @JvmOverloads - fun replaceMethod( - sourceMethod: Method, - targetMethod: Method, - configure: Action = Action {} - ) { - val instruction = ReplaceMethodInsn.forMethods(sourceMethod, targetMethod).build() - configure.execute(instruction) - if (instruction.requireOpcode == MethodOpcode.INVOKEVIRTUAL - && instruction.toOpcode == MethodOpcode.INVOKESTATIC - ) { - ReflectionUtils.validateVirtualToStaticReplacement(sourceMethod, targetMethod) - } - addReplaceInsns(instruction) - } - - /** - * Load instructions from the given file. - */ - fun loadFromFile(file: File) { - val lexer = InsnLexer(file.readText()) - val parser = InsnParser(lexer) - val insns = parser.parse() - addReplaceInsns(insns) - } - - private fun addReplaceInsns(vararg insns: ReplaceMethodInsn - ) { - addReplaceInsns(insns.asIterable()) - } - - private fun addReplaceInsns(insns: Iterable - ) { - for (insn in insns) { - val className = insn.fromClass.replace('/', '.') - val methodName = insn.methodName - val methodDescriptor = insn.methodDescriptor - - insn.requireOpcode ?: run { - insn.requireOpcode = MethodOpcode.ANY - } - - val key = ReplaceMethodInsnKey(className, methodName, methodDescriptor) - this.instructions[key] = insn - } - } + internal val includePackages = TreeSet() + + internal val instructions = + mutableMapOf() + + /** Class-level replacements: dot-notation source → dot-notation target. */ + internal val classReplacements = mutableMapOf() + + companion object { + private val PACKAGE_NAME_REGEX = + Regex("""^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*${'$'}""") + } + + fun includePackage(vararg packages: String) { + for (pck in packages) { + if (!PACKAGE_NAME_REGEX.matches(pck)) { + throw IllegalArgumentException("Invalid package name: $pck") + } + includePackages.add(pck) + } + } + + fun removePackage(vararg packages: String) { + includePackages.removeAll(packages.toSet()) + } + + fun replaceMethod(configure: Action) { + val instruction = objects.newInstance(ReplaceMethodInsn::class.java) + configure.execute(instruction) + addReplaceInsns(instruction) + } + + @JvmOverloads + fun replaceMethod( + sourceMethod: Method, + targetMethod: Method, + configure: Action = Action {}, + ) { + val instruction = ReplaceMethodInsn.forMethods(sourceMethod, targetMethod).build() + configure.execute(instruction) + if (instruction.requireOpcode == MethodOpcode.INVOKEVIRTUAL + && instruction.toOpcode == MethodOpcode.INVOKESTATIC + ) { + ReflectionUtils.validateVirtualToStaticReplacement(sourceMethod, targetMethod) + } + addReplaceInsns(instruction) + } + + /** + * Replaces every bytecode reference to [fromClass] with [toClass]. + * + * This rewrites: + * - Instruction owners (`INVOKEVIRTUAL`, `GETFIELD`, `NEW`, `CHECKCAST`, …) + * - Type descriptors and generic signatures in method bodies + * - Class-literal LDC constants (`Foo.class`) + * - Field and method *declaration* descriptors in the instrumented class + * + * Class names can be provided in dot-notation (`com.example.Foo`) or + * slash-notation (`com/example/Foo`). + * + * Note: unlike [replaceMethod], class-level replacement is applied to + * **all** instrumented classes regardless of [includePackage] filters, + * because any class may contain a reference to the replaced one. + */ + fun replaceClass(fromClass: String, toClass: String) { + require(fromClass.isNotBlank()) { "fromClass must not be blank." } + require(toClass.isNotBlank()) { "toClass must not be blank." } + val from = fromClass.replace('/', '.') + val to = toClass.replace('/', '.') + classReplacements[from] = to + } + + /** + * Replaces every bytecode reference to [fromClass] with [toClass]. + * + * @throws UnsupportedOperationException for array or primitive types. + */ + fun replaceClass(fromClass: Class<*>, toClass: Class<*>) { + require(!fromClass.isArray && !fromClass.isPrimitive) { + "Array and primitive types are not supported for class replacement." + } + require(!toClass.isArray && !toClass.isPrimitive) { + "Array and primitive types are not supported for class replacement." + } + replaceClass(fromClass.name, toClass.name) + } + + fun loadFromFile(file: File) { + val lexer = InsnLexer(file.readText()) + val parser = InsnParser(lexer) + val insns = parser.parse() + addReplaceInsns(insns) + } + + private fun addReplaceInsns(vararg insns: ReplaceMethodInsn) = + addReplaceInsns(insns.asIterable()) + + private fun addReplaceInsns(insns: Iterable) { + for (insn in insns) { + val className = insn.fromClass.replace('/', '.') + insn.requireOpcode = insn.requireOpcode ?: MethodOpcode.ANY + val key = ReplaceMethodInsnKey(className, insn.methodName, insn.methodDescriptor) + instructions[key] = insn + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt index 8317865c2d..bed859bc6d 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/MethodOpcode.kt @@ -24,42 +24,44 @@ import org.objectweb.asm.Opcodes * * @author Akash Yadav */ -enum class MethodOpcode(val insnName: String, val opcode: Int +enum class MethodOpcode( + val insnName: String, + val opcode: Int, ) { - /** - * The opcode for `invokestatic`. - */ - INVOKESTATIC("invoke-static", Opcodes.INVOKESTATIC), + /** + * The opcode for `invokestatic`. + */ + INVOKESTATIC("invoke-static", Opcodes.INVOKESTATIC), - /** - * The opcode for `invokespecial`. - */ - INVOKESPECIAL("invoke-special", Opcodes.INVOKESPECIAL), + /** + * The opcode for `invokespecial`. + */ + INVOKESPECIAL("invoke-special", Opcodes.INVOKESPECIAL), - /** - * The opcode for `invokevirtual`. - */ - INVOKEVIRTUAL("invoke-virtual", Opcodes.INVOKEVIRTUAL), + /** + * The opcode for `invokevirtual`. + */ + INVOKEVIRTUAL("invoke-virtual", Opcodes.INVOKEVIRTUAL), - /** - * The opcode for `invokeinterface`. - */ - INVOKEINTERFACE("invoke-interface", Opcodes.INVOKEINTERFACE), + /** + * The opcode for `invokeinterface`. + */ + INVOKEINTERFACE("invoke-interface", Opcodes.INVOKEINTERFACE), - /** - * Any opcode. This is for internal use only. - */ - ANY("invoke-any", 0); + /** + * Any opcode. This is for internal use only. + */ + ANY("invoke-any", 0); - companion object { + companion object { - /** - * Finds the [MethodOpcode] with the given instruction name. - */ - @JvmStatic - fun find(insn: String): MethodOpcode? { - return MethodOpcode.values().find { it.insnName == insn } - } - } + /** + * Finds the [MethodOpcode] with the given instruction name. + */ + @JvmStatic + fun find(insn: String): MethodOpcode? { + return MethodOpcode.values().find { it.insnName == insn } + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt new file mode 100644 index 0000000000..224ab00ebe --- /dev/null +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceClassRef.kt @@ -0,0 +1,31 @@ +package com.itsaky.androidide.desugaring.dsl + +import java.io.Serializable + +/** + * Describes a full class-reference replacement: every bytecode reference to + * [fromClass] in any instrumented class will be rewritten to [toClass]. + * + * Class names may be given in dot-notation (`com.example.Foo`) or + * slash-notation (`com/example/Foo`); both are normalised internally. + * + * @author Akash Yadav + */ +data class ReplaceClassRef( + /** The class whose references should be replaced (dot-notation). */ + val fromClass: String, + /** The class that should replace all [fromClass] references (dot-notation). */ + val toClass: String, +) : Serializable { + + companion object { + @JvmField + val serialVersionUID = 1L + } + + /** ASM internal name (slash-notation) for [fromClass]. */ + val fromInternal: String get() = fromClass.replace('.', '/') + + /** ASM internal name (slash-notation) for [toClass]. */ + val toInternal: String get() = toClass.replace('.', '/') +} \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt index 09c113f283..da5e59255c 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsn.kt @@ -29,182 +29,189 @@ import java.lang.reflect.Modifier */ interface ReplaceMethodInsn { - /** - * The owner class name for the method to be replaced. The class name must be - * in the form of a fully qualified name or in the binary name format. - */ - var fromClass: String - - /** - * The name of the method to be replaced. - */ - var methodName: String - - /** - * The descriptor of the method to be replaced. This is the method signature - * as it appears in the bytecode. - */ - var methodDescriptor: String - - /** - * The opcode for the method to be replaced. If this is specified, then the - * opcode for the invoked method will be checked against this and the invocation - * will only be replaced of the opcode matches. - * - * This is optional. By default, the invocation will always be replaced. - */ - var requireOpcode: MethodOpcode? - - /** - * The owner class name for the method which will replace the [methodName]. - * The class name must be in the form of a fully qualified name or in the - * binary name format. - */ - var toClass: String - - /** - * The name of the method in [toClass] which will replace the [methodName]. - */ - var toMethod: String - - /** - * The descriptor of the method in [toClass] which will replace the [methodName]. - */ - var toMethodDescriptor: String - - /** - * The opcode for invoking [toMethod] in [toClass]. - */ - var toOpcode: MethodOpcode - - class Builder { - - @JvmField - var fromClass: String = "" - - @JvmField - var methodName: String = "" - - @JvmField - var methodDescriptor: String = "" - - @JvmField - var requireOpcode: MethodOpcode? = null - - @JvmField - var toClass: String = "" - - @JvmField - var toMethod: String = "" - - @JvmField - var toMethodDescriptor: String = "" - - @JvmField - var toOpcode: MethodOpcode = MethodOpcode.ANY - - fun fromMethod(method: Method) = apply { - fromClass(method.declaringClass) - methodName(method.name) - methodDescriptor(ReflectionUtils.describe(method)) - - if (Modifier.isStatic(method.modifiers)) { - requireOpcode(MethodOpcode.INVOKESTATIC) - } else { - requireOpcode(MethodOpcode.INVOKEVIRTUAL) - } - } - - fun fromClass(fromClass: String) = apply { - this.fromClass = fromClass - } - - fun fromClass(klass: Class<*>): Builder { - if (klass.isArray || klass.isPrimitive) { - throw UnsupportedOperationException( - "Array and primitive types are not supported for desugaring") - } - - return fromClass(klass.name) - } - - fun methodName(methodName: String) = apply { - this.methodName = methodName - } - - fun methodDescriptor(methodDescriptor: String) = apply { - this.methodDescriptor = methodDescriptor - } - - fun requireOpcode(requireOpcode: MethodOpcode) = apply { - this.requireOpcode = requireOpcode - } - - fun toClass(toClass: String) = apply { - this.toClass = toClass - } - - fun toClass(klass: Class<*>): Builder { - if (klass.isArray || klass.isPrimitive) { - throw UnsupportedOperationException( - "Array and primitive types are not supported for desugaring") - } - - return toClass(klass.name) - } - - fun toMethod(toMethod: String) = apply { - this.toMethod = toMethod - } - - fun toMethodDescriptor(toMethodDescriptor: String) = apply { - this.toMethodDescriptor = toMethodDescriptor - } - - fun toMethod(method: Method) = apply { - toClass(method.declaringClass) - toMethod(method.name) - toMethodDescriptor(ReflectionUtils.describe(method)) - - if (Modifier.isStatic(method.modifiers)) { - toOpcode(MethodOpcode.INVOKESTATIC) - } else { - toOpcode(MethodOpcode.INVOKEVIRTUAL) - } - } - - fun toOpcode(toOpcode: MethodOpcode) = apply { - this.toOpcode = toOpcode - } - - fun build(): DefaultReplaceMethodInsn { - require(fromClass.isNotBlank()) { "fromClass cannot be blank." } - require(methodName.isNotBlank()) { "methodName cannot be blank." } - require( - methodDescriptor.isNotBlank()) { "methodDescriptor cannot be blank." } - require(toClass.isNotBlank()) { "toClass cannot be blank." } - require(toMethod.isNotBlank()) { "toMethod cannot be blank." } - require( - toMethodDescriptor.isNotBlank()) { "toMethodDescriptor cannot be blank." } - require(toOpcode != MethodOpcode.ANY) { "toOpcode cannot be ANY." } - - return DefaultReplaceMethodInsn(fromClass, methodName, methodDescriptor, - requireOpcode, toClass, toMethod, toMethodDescriptor, toOpcode) - } - } - - companion object { - - @JvmStatic - fun builder(): Builder = Builder() - - /** - * Creates a [Builder] for the given source and target method. - */ - @JvmStatic - fun forMethods(fromMethod: Method, toMethod: Method - ): Builder { - return builder().fromMethod(fromMethod).toMethod(toMethod) - } - } + /** + * The owner class name for the method to be replaced. The class name must be + * in the form of a fully qualified name or in the binary name format. + */ + var fromClass: String + + /** + * The name of the method to be replaced. + */ + var methodName: String + + /** + * The descriptor of the method to be replaced. This is the method signature + * as it appears in the bytecode. + */ + var methodDescriptor: String + + /** + * The opcode for the method to be replaced. If this is specified, then the + * opcode for the invoked method will be checked against this and the invocation + * will only be replaced of the opcode matches. + * + * This is optional. By default, the invocation will always be replaced. + */ + var requireOpcode: MethodOpcode? + + /** + * The owner class name for the method which will replace the [methodName]. + * The class name must be in the form of a fully qualified name or in the + * binary name format. + */ + var toClass: String + + /** + * The name of the method in [toClass] which will replace the [methodName]. + */ + var toMethod: String + + /** + * The descriptor of the method in [toClass] which will replace the [methodName]. + */ + var toMethodDescriptor: String + + /** + * The opcode for invoking [toMethod] in [toClass]. + */ + var toOpcode: MethodOpcode + + class Builder { + + @JvmField + var fromClass: String = "" + + @JvmField + var methodName: String = "" + + @JvmField + var methodDescriptor: String = "" + + @JvmField + var requireOpcode: MethodOpcode? = null + + @JvmField + var toClass: String = "" + + @JvmField + var toMethod: String = "" + + @JvmField + var toMethodDescriptor: String = "" + + @JvmField + var toOpcode: MethodOpcode = MethodOpcode.ANY + + fun fromMethod(method: Method) = apply { + fromClass(method.declaringClass) + methodName(method.name) + methodDescriptor(ReflectionUtils.describe(method)) + + if (Modifier.isStatic(method.modifiers)) { + requireOpcode(MethodOpcode.INVOKESTATIC) + } else { + requireOpcode(MethodOpcode.INVOKEVIRTUAL) + } + } + + fun fromClass(fromClass: String) = apply { + this.fromClass = fromClass + } + + fun fromClass(klass: Class<*>): Builder { + if (klass.isArray || klass.isPrimitive) { + throw UnsupportedOperationException( + "Array and primitive types are not supported for desugaring" + ) + } + + return fromClass(klass.name) + } + + fun methodName(methodName: String) = apply { + this.methodName = methodName + } + + fun methodDescriptor(methodDescriptor: String) = apply { + this.methodDescriptor = methodDescriptor + } + + fun requireOpcode(requireOpcode: MethodOpcode) = apply { + this.requireOpcode = requireOpcode + } + + fun toClass(toClass: String) = apply { + this.toClass = toClass + } + + fun toClass(klass: Class<*>): Builder { + if (klass.isArray || klass.isPrimitive) { + throw UnsupportedOperationException( + "Array and primitive types are not supported for desugaring" + ) + } + + return toClass(klass.name) + } + + fun toMethod(toMethod: String) = apply { + this.toMethod = toMethod + } + + fun toMethodDescriptor(toMethodDescriptor: String) = apply { + this.toMethodDescriptor = toMethodDescriptor + } + + fun toMethod(method: Method) = apply { + toClass(method.declaringClass) + toMethod(method.name) + toMethodDescriptor(ReflectionUtils.describe(method)) + + if (Modifier.isStatic(method.modifiers)) { + toOpcode(MethodOpcode.INVOKESTATIC) + } else { + toOpcode(MethodOpcode.INVOKEVIRTUAL) + } + } + + fun toOpcode(toOpcode: MethodOpcode) = apply { + this.toOpcode = toOpcode + } + + fun build(): DefaultReplaceMethodInsn { + require(fromClass.isNotBlank()) { "fromClass cannot be blank." } + require(methodName.isNotBlank()) { "methodName cannot be blank." } + require( + methodDescriptor.isNotBlank() + ) { "methodDescriptor cannot be blank." } + require(toClass.isNotBlank()) { "toClass cannot be blank." } + require(toMethod.isNotBlank()) { "toMethod cannot be blank." } + require( + toMethodDescriptor.isNotBlank() + ) { "toMethodDescriptor cannot be blank." } + require(toOpcode != MethodOpcode.ANY) { "toOpcode cannot be ANY." } + + return DefaultReplaceMethodInsn( + fromClass, methodName, methodDescriptor, + requireOpcode, toClass, toMethod, toMethodDescriptor, toOpcode + ) + } + } + + companion object { + + @JvmStatic + fun builder(): Builder = Builder() + + /** + * Creates a [Builder] for the given source and target method. + */ + @JvmStatic + fun forMethods( + fromMethod: Method, toMethod: Method + ): Builder { + return builder().fromMethod(fromMethod).toMethod(toMethod) + } + } } \ No newline at end of file diff --git a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt index b3d41fbbc9..b25b487ce9 100644 --- a/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt +++ b/composite-builds/build-logic/desugaring/src/main/java/com/itsaky/androidide/desugaring/dsl/ReplaceMethodInsnKey.kt @@ -25,10 +25,10 @@ import java.io.Serializable * @author Akash Yadav */ data class ReplaceMethodInsnKey( - val className: String, - val methodName: String, - val methodDescriptor: String + val className: String, + val methodName: String, + val methodDescriptor: String ) : Serializable { - @JvmField - val serialVersionUID = 1L + @JvmField + val serialVersionUID = 1L } diff --git a/composite-builds/build-logic/plugins/build.gradle.kts b/composite-builds/build-logic/plugins/build.gradle.kts index 409aac8403..f0a9d2e804 100644 --- a/composite-builds/build-logic/plugins/build.gradle.kts +++ b/composite-builds/build-logic/plugins/build.gradle.kts @@ -97,3 +97,12 @@ gradlePlugin { } } } + +tasks.withType { + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1) + + compilerOptions.freeCompilerArgs.add("-Xuse-fir-lt=false") + } +} diff --git a/git-core/build.gradle.kts b/git-core/build.gradle.kts index 37bab43c9b..d3ae586bb8 100644 --- a/git-core/build.gradle.kts +++ b/git-core/build.gradle.kts @@ -1,49 +1,23 @@ plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { - namespace = "com.itsaky.androidide.git.core" - compileSdk = 35 - - defaultConfig { - minSdk = 27 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - isCoreLibraryDesugaringEnabled = true - } - kotlinOptions { - jvmTarget = "17" - } + namespace = "com.itsaky.androidide.git.core" } dependencies { - implementation(libs.androidx.core.ktx.v1120) - implementation(libs.androidx.appcompat.v171) - implementation(libs.google.material) - implementation(libs.git.jgit) - coreLibraryDesugaring(libs.desugar.jdk.libs.v215) - implementation(libs.common.kotlin.coroutines.core) - implementation(libs.common.kotlin.coroutines.android) - implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.core.ktx.v1120) + implementation(libs.androidx.appcompat.v171) + implementation(libs.google.material) + implementation(libs.git.jgit) + coreLibraryDesugaring(libs.desugar.jdk.libs.v215) + implementation(libs.common.kotlin.coroutines.core) + implementation(libs.common.kotlin.coroutines.android) + implementation(libs.androidx.lifecycle.viewmodel.ktx) - testImplementation(libs.tests.junit) - androidTestImplementation(libs.tests.androidx.junit) - androidTestImplementation(libs.tests.androidx.espresso.core) + testImplementation(libs.tests.junit) + androidTestImplementation(libs.tests.androidx.junit) + androidTestImplementation(libs.tests.androidx.espresso.core) } diff --git a/gradle.properties b/gradle.properties index 925c52d5d5..a398853a74 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1,31 +1,24 @@ -## For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx1024m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -# -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -#Wed Feb 02 13:50:55 IST 2022 - -org.gradle.jvmargs=-Xmx8G -Dkotlin.daemon.jvm.options="-Xmx4096M" -XX:+HeapDumpOnOutOfMemoryError --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED - -# Increase memory for aapt2 to prevent Java heap space issues during asset compression -android.aapt2.daemonHeapSize=8192M -# For CI builds, set worker max to limit memory usage -org.gradle.workers.max=2 -# Use less memory per worker during asset compression -org.gradle.vfs.watch=true -org.gradle.parallel=false -org.gradle.configureondemand=true -org.gradle.caching=true -android.useAndroidX=true -android.enableJetifier=false -android.jetifier.ignorelist=common-30.2.2.jar - -# TODO : Migrate -android.nonTransitiveRClass=false +## For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +#Fri Mar 13 16:37:43 IST 2026 +android.aapt2.daemonHeapSize=8192M +android.enableJetifier=false +android.jetifier.ignorelist=common-30.2.2.jar +android.nonTransitiveRClass=false +android.useAndroidX=true +org.gradle.caching=true +org.gradle.configureondemand=true +org.gradle.jvmargs=-Xmx8192M -Dkotlin.daemon.jvm.options\="-Xmx8192M" -XX\:+HeapDumpOnOutOfMemoryError --add-opens java.base/java.lang\=ALL-UNNAMED --add-opens java.base/java.util\=ALL-UNNAMED --add-opens java.base/java.io\=ALL-UNNAMED +org.gradle.parallel=true +org.gradle.vfs.watch=true +org.gradle.workers.max=30 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 433a98d96f..dff1dee11d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ gson = "2.10.1" junit-jupiter = "5.10.2" anroidx-test-core = "2.2.0" koinAndroid = "4.1.1" -kotlin = "2.1.21" +kotlin = "2.3.0" kotlin-coroutines = "1.9.0" kotlinxCoroutinesCore = "1.10.2" kotlinxSerializationJson = "1.9.0" @@ -35,7 +35,7 @@ editor = "0.23.6" glide = "4.16.0" androidx-vectordrawable = "1.2.0" androidx-navigation = "2.7.7" -ksp = "2.1.21-2.0.2" +ksp = "2.3.6" antlr4 = "4.13.1" androidx-work = "2.10.0" androidx-espresso = "3.5.1" @@ -43,7 +43,7 @@ retrofit = "2.11.0" markwon = "4.6.2" maven-publish-plugin = "0.27.0" logback = "1.5.3" -room = "2.7.2" +room = "2.8.4" utilcodex = "1.31.1" viewpager2 = "1.1.0-beta02" zoomage = "1.3.1" @@ -239,8 +239,8 @@ androidx-work-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = " google-material = { module = "com.google.android.material:material", version = "1.12.0" } google-gson = { module = "com.google.code.gson:gson", version = "2.12.1" } google-guava = { module = "com.google.guava:guava", version = "33.4.0-android" } -google-auto-value-annotations = { module = "com.google.auto.value:auto-value-annotations", version = "1.10.4" } -google-auto-value-ap = { module = "com.google.auto.value:auto-value", version = "1.10.4" } +google-auto-value-annotations = { module = "com.google.auto.value:auto-value-annotations", version = "1.11.0" } +google-auto-value-ap = { module = "com.google.auto.value:auto-value", version = "1.11.0" } google-auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version = "1.1.1" } google-auto-service = { module = "com.google.auto.service:auto-service", version = "1.1.1" } google-protobuf-java = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c0cb293e89..692c2dc230 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/llama-impl/build.gradle.kts b/llama-impl/build.gradle.kts index 6a36589b76..0b1439bcc4 100644 --- a/llama-impl/build.gradle.kts +++ b/llama-impl/build.gradle.kts @@ -1,68 +1,58 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") } android { - namespace = "android.llama.cpp" - compileSdk = 35 + namespace = "android.llama.cpp" - defaultConfig { - minSdk = 33 - consumerProguardFiles("proguard-rules.pro") - ndk { - // Add NDK properties if wanted, e.g. - // abiFilters += listOf("arm64-v8a") - } - externalNativeBuild { - cmake { - arguments += "-DLLAMA_CURL=OFF" - arguments += "-DLLAMA_BUILD_COMMON=ON" - arguments += "-DGGML_LLAMAFILE=OFF" - arguments += "-DCMAKE_BUILD_TYPE=Release" - cppFlags += listOf() - arguments += listOf() + defaultConfig { + minSdk = 33 + consumerProguardFiles("proguard-rules.pro") + ndk { + // Add NDK properties if wanted, e.g. + // abiFilters += listOf("arm64-v8a") + } + externalNativeBuild { + cmake { + arguments += "-DLLAMA_CURL=OFF" + arguments += "-DLLAMA_BUILD_COMMON=ON" + arguments += "-DGGML_LLAMAFILE=OFF" + arguments += "-DCMAKE_BUILD_TYPE=Release" + cppFlags += listOf() + arguments += listOf() - cppFlags("") - } - } - } + cppFlags("") + } + } + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - externalNativeBuild { - cmake { - path("src/main/cpp/CMakeLists.txt") - version = "3.22.1" - } - } - compileOptions { - // It's fine for the library to be compiled with modern features - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = "1.8" - } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } } dependencies { - implementation(project(":llama-api")) - implementation(libs.androidx.core.ktx.v1120) - implementation(libs.androidx.appcompat.v171) - implementation(libs.tooling.slf4j) - + implementation(project(":llama-api")) + implementation(libs.androidx.core.ktx.v1120) + implementation(libs.androidx.appcompat.v171) + implementation(libs.tooling.slf4j) } diff --git a/lsp/kotlin-core/build.gradle.kts b/lsp/kotlin-core/build.gradle.kts index df2bb81208..29489689fc 100644 --- a/lsp/kotlin-core/build.gradle.kts +++ b/lsp/kotlin-core/build.gradle.kts @@ -23,11 +23,11 @@ plugins { android { namespace = "org.appdevforall.codeonthego.lsp.kotlin" +} - kotlinOptions { - freeCompilerArgs += listOf( - "-opt-in=kotlin.contracts.ExperimentalContracts" - ) +kotlin { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.contracts.ExperimentalContracts") } } diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index 9de97e6c31..d111677334 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(projects.lsp.api) implementation(projects.lsp.models) implementation(projects.eventbusEvents) + implementation(projects.subprojects.kotlinAnalysisApi) implementation(projects.shared) implementation(projects.subprojects.projects) implementation(projects.subprojects.projectModels) diff --git a/plugin-api/build.gradle.kts b/plugin-api/build.gradle.kts index 7adab325d9..878e7d7f44 100644 --- a/plugin-api/build.gradle.kts +++ b/plugin-api/build.gradle.kts @@ -1,39 +1,23 @@ - - plugins { - id("com.android.library") - id("kotlin-android") - id("kotlin-parcelize") + id("com.android.library") + id("kotlin-android") + id("kotlin-parcelize") } android { - namespace = "com.itsaky.androidide.plugins.api" - compileSdk = 35 - - defaultConfig { - minSdk = 28 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } + namespace = "com.itsaky.androidide.plugins.api" } dependencies { - // Only include Android context for basic Android functionality - compileOnly("androidx.appcompat:appcompat:1.6.1") - compileOnly("androidx.fragment:fragment-ktx:1.6.2") - compileOnly("com.google.android.material:material:1.11.0") + // Only include Android context for basic Android functionality + compileOnly("androidx.appcompat:appcompat:1.6.1") + compileOnly("androidx.fragment:fragment-ktx:1.6.2") + compileOnly("com.google.android.material:material:1.11.0") } tasks.register("createPluginApiJar") { - dependsOn("assembleRelease") - from(layout.buildDirectory.file("intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar")) - into(layout.buildDirectory.dir("libs")) - rename { "plugin-api-1.0.0.jar" } -} \ No newline at end of file + dependsOn("assembleRelease") + from(layout.buildDirectory.file("intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar")) + into(layout.buildDirectory.dir("libs")) + rename { "plugin-api-1.0.0.jar" } +} diff --git a/plugin-manager/build.gradle.kts b/plugin-manager/build.gradle.kts index 4def49e6da..278fd96d41 100644 --- a/plugin-manager/build.gradle.kts +++ b/plugin-manager/build.gradle.kts @@ -1,48 +1,31 @@ - - import com.itsaky.androidide.build.config.BuildConfig plugins { - id("com.android.library") - id("kotlin-android") + id("com.android.library") + id("kotlin-android") } android { - namespace = "${BuildConfig.PACKAGE_NAME}.plugins.manager" - - compileSdk = BuildConfig.COMPILE_SDK - - defaultConfig { - minSdk = BuildConfig.MIN_SDK - } - - compileOptions { - sourceCompatibility = BuildConfig.JAVA_VERSION - targetCompatibility = BuildConfig.JAVA_VERSION - } + namespace = "${BuildConfig.PACKAGE_NAME}.plugins.manager" - kotlinOptions { - jvmTarget = BuildConfig.JAVA_VERSION.toString() - } - - lint { - abortOnError = false - } + lint { + abortOnError = false + } } dependencies { - api(projects.pluginApi) - - implementation(projects.actions) - implementation(projects.common) - implementation(projects.logger) - implementation(projects.lookup) - implementation(projects.preferences) - implementation(projects.resources) - implementation(projects.idetooltips) - implementation(projects.shared) - implementation(projects.subprojects.projects) - - implementation(libs.androidx.appcompat) - implementation(libs.gson.v2101) -} \ No newline at end of file + api(projects.pluginApi) + + implementation(projects.actions) + implementation(projects.common) + implementation(projects.logger) + implementation(projects.lookup) + implementation(projects.preferences) + implementation(projects.resources) + implementation(projects.idetooltips) + implementation(projects.shared) + implementation(projects.subprojects.projects) + + implementation(libs.androidx.appcompat) + implementation(libs.gson.v2101) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 247903307d..dfb9c6f997 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,25 +38,25 @@ dependencyResolutionManagement { val dependencySubstitutions = mapOf( "build-deps" to - arrayOf( - "appintro", - "fuzzysearch", - "google-java-format", - "java-compiler", - "javac", - "javapoet", - "jaxp", - "jdk-compiler", - "jdk-jdeps", - "jdt", - "layoutlib-api", - "treeview", - ), + arrayOf( + "appintro", + "fuzzysearch", + "google-java-format", + "java-compiler", + "javac", + "javapoet", + "jaxp", + "jdk-compiler", + "jdk-jdeps", + "jdt", + "layoutlib-api", + "treeview", + ), "build-deps-common" to - arrayOf( - "constants", - "desugaring-core", - ), + arrayOf( + "constants", + "desugaring-core", + ), ) for ((build, modules) in dependencySubstitutions) { @@ -123,7 +123,7 @@ include( ":eventbus", ":eventbus-android", ":eventbus-events", - ":git-core", + ":git-core", ":gradle-plugin", ":gradle-plugin-config", ":idetooltips", @@ -155,6 +155,7 @@ include( ":subprojects:flashbar", ":subprojects:framework-stubs", ":subprojects:javac-services", + ":subprojects:kotlin-analysis-api", ":subprojects:libjdwp", ":subprojects:projects", ":subprojects:project-models", @@ -185,12 +186,12 @@ include( ":plugin-api", ":plugin-api:plugin-builder", ":plugin-manager", - ":llama-api", - ":llama-impl", - ":cv-image-to-xml", - ":llama-api", - ":llama-impl", - ":compose-preview" + ":llama-api", + ":llama-impl", + ":cv-image-to-xml", + ":llama-api", + ":llama-impl", + ":compose-preview" ) object FDroidConfig { diff --git a/subprojects/kotlin-analysis-api/.gitignore b/subprojects/kotlin-analysis-api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/subprojects/kotlin-analysis-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts new file mode 100644 index 0000000000..831315b588 --- /dev/null +++ b/subprojects/kotlin-analysis-api/build.gradle.kts @@ -0,0 +1,28 @@ +import com.itsaky.androidide.build.config.BuildConfig +import com.itsaky.androidide.plugins.extension.AssetSource + +plugins { + alias(libs.plugins.android.library) + id("com.itsaky.androidide.build.external-assets") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.kt.analysis" +} + +val ktAndroidRepo = "https://github.com/appdevforall/kotlin-android" +val ktAndroidVersion = "2.3.255" +val ktAndroidTag = "v${ktAndroidVersion}-f047b07" +val ktAndroidJarName = "analysis-api-standalone-embeddable-for-ide-${ktAndroidVersion}-SNAPSHOT.jar" + +externalAssets { + jarDependency("kt-android") { + configuration = "api" + source = + AssetSource.External( + url = uri("$ktAndroidRepo/releases/download/$ktAndroidTag/$ktAndroidJarName"), + sha256Checksum = "c9897c94ae1431fadeb4fa5b05dd4d478a60c4589f38f801e07c72405a7b34b1", + ) + } +} + diff --git a/subprojects/kotlin-analysis-api/consumer-rules.pro b/subprojects/kotlin-analysis-api/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/subprojects/kotlin-analysis-api/proguard-rules.pro b/subprojects/kotlin-analysis-api/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/subprojects/kotlin-analysis-api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file