diff --git a/build-logic/src/main/kotlin/Deployment.kt b/build-logic/src/main/kotlin/Deployment.kt index 3a5f9c41..7d7ead8d 100644 --- a/build-logic/src/main/kotlin/Deployment.kt +++ b/build-logic/src/main/kotlin/Deployment.kt @@ -2,7 +2,6 @@ import groovy.lang.Closure import groovy.util.Node -import org.gradle.api.NamedDomainObjectCollection import org.gradle.api.Project import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.ProjectDependency @@ -10,12 +9,14 @@ import org.gradle.api.plugins.ExtensionAware import org.gradle.api.plugins.ExtraPropertiesExtension import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.internal.PublicationInternal +import org.gradle.api.publish.internal.metadata.ModuleMetadataSpec import org.gradle.api.publish.maven.MavenArtifact import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.internal.publication.DefaultMavenPublication import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven +import org.gradle.api.publish.tasks.GenerateModuleMetadata import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer +import org.gradle.internal.Try import org.gradle.jvm.tasks.Jar import org.gradle.kotlin.dsl.closureOf import org.gradle.kotlin.dsl.maybeCreate @@ -25,7 +26,8 @@ import org.gradle.kotlin.dsl.withGroovyBuilder import org.gradle.kotlin.dsl.withType import org.gradle.plugins.signing.Sign import org.gradle.plugins.signing.SigningExtension -import java.io.File +import java.lang.reflect.Field +import java.lang.reflect.Method /** * Configure deployment tasks and properties for a project using the provided [deployConfig]. @@ -146,9 +148,25 @@ private fun Project.configureAndroidDeployment( } // Disable main publication - tasks.withType().configureEach { + tasks.withType { isEnabled = "Main" !in name } + + // Hook into Gradle module metadata generation + // and replace project references (e.g. "core") with the correct + // Maven dependency coordinates ("android-test-core") + tasks.withType { + val junit = SupportedJUnit.values() + .firstOrNull { name.contains(it.variant, ignoreCase = true) } + + if (junit != null) { + val modifier = ReflectiveModuleMetadataModifier(this, junit) + + doFirst { + modifier.run() + } + } + } } private fun Project.configurePluginDeployment( @@ -465,3 +483,68 @@ private fun Project.centralPublishing( } } } + +/** + * Gradle module metadata modifier, replacing references to instrumentation libraries + * with the correct artifact ID in each module's JSON file (build/publications/.../module.json). + */ +private class ReflectiveModuleMetadataModifier( + private val task: GenerateModuleMetadata, + private val junit: SupportedJUnit +) { + @Suppress("UNCHECKED_CAST") + private companion object { + private val reflectiveMethodCache = mutableMapOf, MutableMap>() + private val reflectiveFieldCache = mutableMapOf, MutableMap>() + + fun method(cls: Class<*>, named: String, receiver: Any, vararg args: Any): R { + val methods = reflectiveMethodCache.getOrPut(cls, ::mutableMapOf) + val method = methods.getOrPut(named) { + cls.getDeclaredMethod(named).also { it.isAccessible = true } + } + return method.invoke(receiver, *args) as R + } + + fun field(cls: Class<*>, named: String): Field { + val fields = reflectiveFieldCache.getOrPut(cls, ::mutableMapOf) + val field = fields.getOrPut(named) { + cls.getDeclaredField(named).also { it.isAccessible = true } + } + return field + } + + fun Any.field(named: String): R = field(this.javaClass, named).get(this) as R + } + + fun run() { + // Access the inputs of the task, then crawl through to the dependencies + // of each variant inside its module metadata. Rewrite the coordinates of each + // instrumentation lib found inside there. + val inputState = method(GenerateModuleMetadata::class.java, "inputState", task) + val metadataSpecTry = inputState.field>("moduleMetadataSpec") + val metadataSpec = metadataSpecTry.get() + val variants = metadataSpec.field>("variants") + + for (variant in variants) { + val variantDependencies = + runCatching { variant.field>("dependencies") } + .getOrNull() + ?: continue + + for (variantDependency in variantDependencies) { + val depCoordinates = variantDependency.field("coordinates") + val depGroup = depCoordinates.field("group") + val depName = depCoordinates.field("name") + + if (depGroup == Artifacts.Instrumentation.groupId) { + Artifacts.from(depName)?.let { replacement -> + field(depCoordinates.javaClass, "name").set( + depCoordinates, + suffixedArtifactId(replacement.artifactId, junit) + ) + } + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/Environment.kt b/build-logic/src/main/kotlin/Environment.kt index 00026a9d..0580374c 100644 --- a/build-logic/src/main/kotlin/Environment.kt +++ b/build-logic/src/main/kotlin/Environment.kt @@ -88,13 +88,14 @@ object Artifacts { const val LICENSE = "Apache-2.0" /** - * Retrieve the artifact configuration based on a Gradle project reference. - * Return null if none can be found + * Retrieve the artifact configuration based on a string name, or null if none can be found */ - fun from(project: Project) = - when (project.name) { + fun from(name: String): Deployed? = + when (name) { "core" -> Instrumentation.Core + "extensions" -> Instrumentation.Extensions "runner" -> Instrumentation.Runner + "compose" -> Instrumentation.Compose "android-junit5" -> Plugin else -> null }