diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7cfae0b..ca28f45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,22 @@ [versions] agp = "8.1.4" +apache-ant = "1.10.14" asm-commons = "9.6" android-tools = "31.1.4" kotlin = "2.0.0" shadow = "8.3.0" publisher = "0.14.0" +kotlinx-metadata = "0.9.0" [libraries] +apache-ant = { module = "org.apache.ant:ant", version.ref = "apache-ant" } asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm-commons" } gradle-android-common = { module = "com.android.tools:common", version.ref = "android-tools" } gradle-android-sdk-common = { module = "com.android.tools:sdk-common", version.ref = "android-tools" } gradle-android-layoutlib = { module = "com.android.tools.layoutlib:layoutlib-api", version.ref = "android-tools" } gradle-android-build = { module = "com.android.tools.build:gradle", version.ref = "agp" } gradle-shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow" } +kotlinx-metadata-jvm = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version.ref = "kotlinx-metadata" } [bundles] gradle-android = ["gradle-android-sdk-common", "gradle-android-build", "gradle-android-common", "gradle-android-layoutlib"] diff --git a/grease/build.gradle.kts b/grease/build.gradle.kts index 1f7f6ab..143f1f8 100644 --- a/grease/build.gradle.kts +++ b/grease/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { implementation(libs.asm.commons) implementation(libs.gradle.shadow) implementation(libs.bundles.gradle.android) + implementation(libs.kotlinx.metadata.jvm) + implementation(libs.apache.ant) } deployer { diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt index b23770d..3af5679 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt @@ -1,8 +1,5 @@ -@file:Suppress("UnstableApiUsage") - package io.deepmedia.tools.grease -import com.android.build.api.component.analytics.AnalyticsEnabledLibraryVariant import com.android.build.api.component.analytics.AnalyticsEnabledVariant import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.Variant @@ -31,12 +28,13 @@ import com.android.ide.common.resources.CopyToOutputDirectoryResourceCompilation import com.android.manifmerger.ManifestMerger2 import com.android.manifmerger.ManifestProvider import com.android.utils.StdLogger -import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration -import org.gradle.kotlin.dsl.get +import org.gradle.api.publish.maven.internal.publication.DefaultMavenPublication +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.kotlin.dsl.support.unzipTo import org.gradle.kotlin.dsl.support.zipTo import java.io.File @@ -54,42 +52,40 @@ open class GreasePlugin : Plugin { private val defaultIssueReporter = DefaultIssueReporter(StdLogger(StdLogger.Level.WARNING)) - @Suppress("NAME_SHADOWING") override fun apply(target: Project) { - require(target.plugins.hasPlugin("com.android.library")) { - "Grease must be applied after the com.android.library plugin." - } - val log = Logger(target, "grease") - val android = target.extensions["android"] as LibraryExtension - val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java) - val greaseExtension = target.extensions.create("grease", GreaseExtension::class.java) - - debugGreasyConfigurationHierarchy(target, log) - - // Create the configurations. - fun createConfigurations(isTransitive: Boolean) { - target.createRootConfiguration(isTransitive, log) - target.createProductFlavorConfigurations(androidComponents, isTransitive, log) - target.createBuildTypeConfigurations(android.buildTypes, isTransitive, log) - target.createVariantConfigurations(androidComponents, isTransitive, log) - } - createConfigurations(false) - createConfigurations(true) - - fun configure(variant: Variant, vararg configurations: Configuration) { - configureVariantManifest(target, variant, configurations, log) - configureVariantJniLibs(target, variant, configurations, log) - configureVariantResources(target, variant, configurations, log) - configureVariantSources(target, variant, configurations, greaseExtension, log) - configureVariantAssets(target, variant, configurations, log) - configureVariantProguardFiles(target, variant, configurations, log) - } - // Configure all variants. - androidComponents.onVariants { variant -> - val log = log.child("configureVariant") - log.d { "Configuring variant ${variant.name}..." } - target.afterEvaluate { - configure(variant, target.greaseOf(variant), target.greaseOf(variant, true)) + target.plugins.withId("com.android.library") { + val log = Logger(target, "grease") + val android = target.extensions.getByType(LibraryExtension::class.java) + val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java) + val greaseExtension = target.extensions.create("grease", GreaseExtension::class.java) + + debugGreasyConfigurationHierarchy(target, log) + + // Create the configurations. + fun createConfigurations(isTransitive: Boolean) { + target.createRootConfiguration(isTransitive, log) + target.createProductFlavorConfigurations(androidComponents, isTransitive, log) + target.createBuildTypeConfigurations(android.buildTypes, isTransitive, log) + target.createVariantConfigurations(androidComponents, isTransitive, log) + } + createConfigurations(false) + createConfigurations(true) + + fun configure(variant: Variant, vararg configurations: Configuration) { + configureVariantManifest(target, variant, configurations, log) + configureVariantJniLibs(target, variant, configurations, log) + configureVariantResources(target, variant, configurations, log) + configureVariantSources(target, variant, configurations, greaseExtension, log) + configureVariantAssets(target, variant, configurations, log) + configureVariantProguardFiles(target, variant, configurations, log) + } + // Configure all variants. + androidComponents.onVariants { variant -> + val childLog = log.child("configureVariant") + childLog.d { "Configuring variant ${variant.name}..." } + target.afterEvaluate { + configure(variant, target.greaseOf(variant), target.greaseOf(variant, true)) + } } } } @@ -141,7 +137,6 @@ open class GreasePlugin : Plugin { // To retrieve the secondary files, we must query the configuration artifacts. val primaryManifest = processManifestTask.manifestOutputFile.asFile // overwrite - @Suppress("DEPRECATION") val mergedFlavor = componentConfig.oldVariantApiLegacySupport?.mergedFlavor log.d { "Merging manifests... primary=${primaryManifest.get()}, secondary=${extraManifests.files.joinToString()}" } @@ -432,7 +427,7 @@ open class GreasePlugin : Plugin { fun injectClasses(inputJar: File) { log.d { "Processing inputJar=$inputJar outputDir=${jarExtractWorkdir}..." } - val inputFiles = target.zipTree(inputJar).matching { include("**/*.class") } + val inputFiles = target.zipTree(inputJar).matching { include("**/*.class", "**/*.kotlin_module") } target.copy { from(inputFiles) into(jarExtractWorkdir) @@ -466,7 +461,7 @@ open class GreasePlugin : Plugin { destinationDirectory.set(greaseShadowDir) from(greaseProcessTask.get().outputs) - val packagesToReplace = mutableMapOf() + val addedPackagesNames = mutableSetOf() doFirst { target.delete(greaseShadowDir) @@ -487,52 +482,35 @@ open class GreasePlugin : Plugin { .asSequence() .flatMap { inputFile -> inputFile.packageNames } .distinct() - .forEach { packageName -> - val newPackageName = "${relocationPrefix}.$packageName" + .map { packageName -> packageName to "${relocationPrefix}.$packageName" } + .distinct() + .filterNot { (packageName, _) -> addedPackagesNames.any(packageName::contains) } + .forEach { (packageName, newPackageName) -> log.d { "Relocate package from $packageName to $newPackageName" } relocate(packageName, newPackageName) - packagesToReplace[packageName] = newPackageName + addedPackagesNames += packageName } } - greaseExtension.relocators.get().forEach { relocator -> - relocate(relocator) - if (relocator is SimpleRelocator) { - packagesToReplace[relocator.pattern] = relocator.shadedPattern - } - } - + greaseExtension.relocators.get().forEach(::relocate) greaseExtension.transformers.get().forEach(::transform) + transform(KotlinModuleShadowTransformer(logger.child("kotlin_module"))) } doLast { val shadowJar = greaseShadowDir.file(jarFileName).asFile - val shadowManifest = greaseShadowDir.file("AndroidManifest.xml").asFile log.d { "Copy shaded inputJar=${shadowJar} outputDir=$aarExtractWorkdir..." } target.copy { from(shadowJar) into(aarExtractWorkdir) } - val manifestWriter = shadowManifest.bufferedWriter() - val manifestReader = aarExtractWorkdir.file("AndroidManifest.xml").asFile.bufferedReader() - - manifestReader.useLines { strings -> - strings - .map { string -> - packagesToReplace.entries.fold(string) { acc, (from, to) -> - acc.replace(from, to) - } - }.forEach { - manifestWriter.write(it) - manifestWriter.newLine() - } - } - manifestWriter.close() - target.copy { - from(shadowManifest) - into(aarExtractWorkdir) - } + replacePackagesInFile( + aarExtractWorkdir.file("AndroidManifest.xml").asFile, + greaseShadowDir.file("AndroidManifest.xml").asFile, + relocators, + target, + ) val oldArchive = bundleAar.archiveFile.get().asFile val archiveParent = oldArchive.parentFile @@ -543,14 +521,59 @@ open class GreasePlugin : Plugin { } bundleLibraryTask?.configure { - finalizedBy(greaseExpandTask) + outputs.upToDateWhen { false } + finalizedBy(greaseShadowTask) } greaseExpandTask.configure { + mustRunAfter(bundleLibraryTask) finalizedBy(greaseProcessTask) } greaseProcessTask.configure { + mustRunAfter(bundleLibraryTask) finalizedBy(greaseShadowTask) } + greaseShadowTask.configure { + mustRunAfter(bundleLibraryTask) + } + target.plugins.withId("org.gradle.maven-publish") { + target.tasks.withType(PublishToMavenRepository::class.java) { + val publication = publication + if (publication is DefaultMavenPublication) { + if (creationConfig.name == publication.component.get().name) { + dependsOn(greaseShadowTask) + } + } + } + } + } + + private fun replacePackagesInFile( + input: File, + output: File, + relocators: List, + target: Project, + ) { + val reader = input.bufferedReader() + val writer = output.bufferedWriter() + reader.useLines { strings -> + strings + .map { string -> + relocators + .filterNot { it is RClassRelocator } + .fold(string) { acc, relocator -> + relocator.applyToSourceContent(acc) + } + }.forEach { + writer.write(it) + writer.newLine() + } + } + writer.close() + + target.copy { + from(output) + into(input.parentFile) + } } /** diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/KotlinModuleShadowTransformer.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/KotlinModuleShadowTransformer.kt new file mode 100644 index 0000000..dccc3bf --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/KotlinModuleShadowTransformer.kt @@ -0,0 +1,65 @@ +package io.deepmedia.tools.grease + +import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer +import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import org.gradle.api.file.FileTreeElement +import kotlinx.metadata.jvm.KotlinModuleMetadata +import kotlinx.metadata.jvm.UnstableMetadataApi +import org.apache.tools.zip.ZipEntry +import org.apache.tools.zip.ZipOutputStream + +// from kotlin sources + +@CacheableTransformer +@OptIn(UnstableMetadataApi::class) +internal class KotlinModuleShadowTransformer(private val logger: Logger) : Transformer { + @Suppress("ArrayInDataClass") + private data class Entry(val path: String, val bytes: ByteArray) + + private val data = mutableListOf() + + override fun getName() = "KotlinModuleShadowTransformer" + + override fun canTransformResource(element: FileTreeElement): Boolean = + element.path.substringAfterLast(".") == KOTLIN_MODULE + + override fun transform(context: TransformerContext) { + fun relocate(content: String): String = + context.relocators + .filterNot { it is RClassRelocator } + .fold(content) { acc, relocator -> relocator.applyToSourceContent(acc) } + + logger.i { "Transforming kotlin_module ${context.path}" } + val metadata = KotlinModuleMetadata.read(context.`is`.readBytes()) + val module = metadata.kmModule + + val packageParts = module.packageParts.toMap() + module.packageParts.clear() + packageParts.map { (fqName, parts) -> + require(parts.multiFileClassParts.isEmpty()) { parts.multiFileClassParts } // There are no multi-file class parts in core + + val fileFacades = parts.fileFacades.toList() + parts.fileFacades.clear() + fileFacades.mapTo(parts.fileFacades) { relocate(it) } + + relocate(fqName) to parts + }.toMap(module.packageParts) + + data += Entry(context.path, metadata.write()) + } + + override fun hasTransformedResource(): Boolean = data.isNotEmpty() + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + for ((path, bytes) in data) { + os.putNextEntry(ZipEntry(path)) + os.write(bytes) + } + data.clear() + } + + companion object { + const val KOTLIN_MODULE = "kotlin_module" + } +} \ No newline at end of file diff --git a/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt b/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt index dd8276f..e5cd6e7 100644 --- a/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt +++ b/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt @@ -2,4 +2,8 @@ package io.deepmedia.tools.grease.sample.dependency.pure object PureDependencyClass { fun foo() = "bar" +} + +fun PureDependencyFunction() { + println("foo") } \ No newline at end of file