Skip to content

Instantly share code, notes, and snippets.

@abdulowork
Last active April 28, 2025 14:00
Show Gist options
  • Save abdulowork/b6d29701dc57568459905c682218739d to your computer and use it in GitHub Desktop.
Save abdulowork/b6d29701dc57568459905c682218739d to your computer and use it in GitHub Desktop.
KGP transform graph explosion
import org.gradle.api.attributes.Usage.*
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages.KOTLIN_API
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages.KOTLIN_METADATA
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages.KOTLIN_RUNTIME
plugins {
kotlin("multiplatform") version "2.1.20"
id("com.android.library") version "8.7.0"
}
group = "org.example"
version = "1.0"
kotlin {
iosArm64()
iosX64()
iosSimulatorArm64()
watchosArm32()
watchosArm64()
watchosSimulatorArm64()
tvosArm64()
tvosX64()
tvosSimulatorArm64()
tvosSimulatorArm64()
linuxArm64()
linuxX64()
macosX64()
macosArm64()
js()
wasmJs()
wasmWasi()
jvm()
sourceSets.commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
}
android {
compileSdk = 34
defaultConfig {
minSdk = 31
}
namespace = "org.jetbrains.kotlin.sample"
}
@CacheableTransform
internal abstract class UnzipUklibTransform @Inject constructor(
private val fileOperations: FileSystemOperations,
private val archiveOperations: ArchiveOperations,
) : TransformAction<TransformParameters.None> {
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override fun transform(outputs: TransformOutputs) {
outputs.file(inputArtifact.get().asFile)
}
}
@CacheableTransform
internal abstract class RemoveMetadataJarsTransform : TransformAction<TransformParameters.None> {
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override fun transform(outputs: TransformOutputs) {
outputs.file(inputArtifact.get().asFile)
}
}
@CacheableTransform
internal abstract class UnzippedUklibToPlatformCompilationTransform :
TransformAction<UnzippedUklibToPlatformCompilationTransform.Parameters> {
interface Parameters : TransformParameters {
@get:Input
val targetFragmentAttribute: Property<String>
}
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
override fun transform(outputs: TransformOutputs) {
outputs.dir(inputArtifact.get().asFile)
}
}
internal val uklibStateAttribute = Attribute.of("org.jetbrains.kotlin.uklibState", String::class.java)
internal val uklibStateCompressed = "compressed"
internal val uklibStateDecompressed = "decompressed"
internal val uklibViewAttribute = Attribute.of("org.jetbrains.kotlin.uklibView", String::class.java)
internal val uklibViewAttributeWholeUklib = "whole_uklib"
internal val isUklib = Attribute.of("org.jetbrains.kotlin.uklib", String::class.java)
internal val isUklibTrue = "true"
private val isMetadataJar = Attribute.of("org.jetbrains.kotlin.isMetadataJar", String::class.java)
private val isMetadataJarUnknown = "unknown"
private val notMetadataJar = "not-a-metadata-jar"
setupUklibConsumption()
private fun Project.setupUklibConsumption() {
val sourceSets = kotlin.sourceSets
val targets = kotlin.targets
registerCompressedUklibArtifact()
allowUklibsToDecompress()
allowMetadataConfigurationsToResolveUnzippedUklib(sourceSets)
allowPSMBasedKMPToResolveLenientlyAndSelectBestMatchingVariant()
allowPlatformCompilationsToResolvePlatformCompilationArtifactFromUklib(targets)
}
private fun Project.allowPlatformCompilationsToResolvePlatformCompilationArtifactFromUklib(
targets: NamedDomainObjectCollection<KotlinTarget>
) {
targets.configureEach {
val target = this
dependencies.registerTransform(UnzippedUklibToPlatformCompilationTransform::class.java) {
with(from) {
attribute(uklibStateAttribute, uklibStateDecompressed)
attribute(uklibViewAttribute, uklibViewAttributeWholeUklib)
}
with(to) {
attribute(uklibStateAttribute, uklibStateDecompressed)
attribute(uklibViewAttribute, target.name)
}
parameters.targetFragmentAttribute.set(target.name)
}
target.compilations.configureEach {
val compilation = this
listOfNotNull<Pair<Configuration, Usage>>(
configurations.getByName(compilation.compileDependencyConfigurationName) to usageByName(UklibUsages.KOTLIN_UKLIB_API),
compilation.runtimeDependencyConfigurationName?.let { configurations.getByName(it) to usageByName(UklibUsages.KOTLIN_UKLIB_RUNTIME)},
).forEach {
it.first.applyUklibAttributes(it.second, target.name)
}
}
}
}
private fun Configuration.applyUklibAttributes(
usage: Usage,
uklibFragmentPlatformAttribute: String,
) {
with(attributes) {
attribute(USAGE_ATTRIBUTE, usage)
attribute(uklibStateAttribute, uklibStateDecompressed)
attribute(uklibViewAttribute, uklibFragmentPlatformAttribute)
attribute(
isMetadataJar,
notMetadataJar
)
attribute(
isUklib,
isUklibTrue
)
}
}
private fun Project.registerCompressedUklibArtifact() {
with(dependencies.artifactTypes.create("uklib").attributes) {
attribute(uklibStateAttribute, uklibStateCompressed)
attribute(uklibViewAttribute, uklibViewAttributeWholeUklib)
}
}
private fun Project.allowUklibsToDecompress() {
dependencies.registerTransform(UnzipUklibTransform::class.java) {
from.attribute(uklibStateAttribute, uklibStateCompressed)
to.attribute(uklibStateAttribute, uklibStateDecompressed)
}
}
private fun Project.allowMetadataConfigurationsToResolveUnzippedUklib(
sourceSets: NamedDomainObjectContainer<KotlinSourceSet>,
) {
sourceSets.all {
val ss = this
afterEvaluate {
afterEvaluate {
configurations.getByName("${ss.name}ResolvableDependenciesMetadata").attributes {
attribute(USAGE_ATTRIBUTE, usageByName(UklibUsages.KOTLIN_UKLIB_METADATA))
attribute(uklibStateAttribute, uklibStateDecompressed)
attribute(uklibViewAttribute, uklibViewAttributeWholeUklib)
attribute(
isUklib,
isUklibTrue
)
}
}
}
}
}
private fun Project.allowPSMBasedKMPToResolveLenientlyAndSelectBestMatchingVariant() {
dependencies.attributesSchema.attribute(USAGE_ATTRIBUTE) {
val strategy = this
strategy.compatibilityRules.add(AllowPlatformConfigurationsToFallBackToMetadataForLenientKmpResolutionUsage::class.java)
strategy.disambiguationRules.add(SelectBestMatchingVariantForKmpResolutionUsage::class.java)
}
dependencies.attributesSchema.attribute(KotlinPlatformType.attribute) {
val strategy = this
strategy.compatibilityRules.add(AllowPlatformConfigurationsToFallBackToMetadataForLenientKmpResolution::class.java)
}
with(dependencies.artifactTypes.getByName("jar").attributes) {
attribute(
isMetadataJar,
isMetadataJarUnknown
)
}
dependencies.registerTransform(RemoveMetadataJarsTransform::class.java) {
from.attribute(
isMetadataJar,
isMetadataJarUnknown
)
to.attribute(
isMetadataJar,
notMetadataJar
)
}
}
private class AllowPlatformConfigurationsToFallBackToMetadataForLenientKmpResolution : AttributeCompatibilityRule<KotlinPlatformType> {
override fun execute(details: CompatibilityCheckDetails<KotlinPlatformType>) = with(details) {
consumerValue?.name ?: return@with
val producer = producerValue?.name ?: return@with
if (producer == KotlinPlatformType.common.name) compatible()
}
}
internal class AllowPlatformConfigurationsToFallBackToMetadataForLenientKmpResolutionUsage : AttributeCompatibilityRule<Usage> {
override fun execute(details: CompatibilityCheckDetails<Usage>) = with(details) {
val consumerUsage = consumerValue?.name ?: return@with
val producerUsage = producerValue?.name ?: return@with
if (
mapOf(
UklibUsages.KOTLIN_UKLIB_API to setOf(
KOTLIN_API,
JAVA_API,
KOTLIN_METADATA
),
UklibUsages.KOTLIN_UKLIB_RUNTIME to setOf(
KOTLIN_RUNTIME,
JAVA_RUNTIME,
JAVA_API,
),
UklibUsages.KOTLIN_UKLIB_METADATA to setOf(
UklibUsages.KOTLIN_UKLIB_API,
KOTLIN_METADATA,
KOTLIN_API,
JAVA_API,
),
)[consumerUsage]?.contains(producerUsage) == true
) compatible()
}
}
internal class SelectBestMatchingVariantForKmpResolutionUsage : AttributeDisambiguationRule<Usage> {
override fun execute(details: MultipleCandidatesDetails<Usage>) = details.run {
val consumerUsage = consumerValue?.name ?: return@run
details.candidateValues
mapOf(
UklibUsages.KOTLIN_UKLIB_API to listOf(
UklibUsages.KOTLIN_UKLIB_API,
KOTLIN_API,
JAVA_API,
KOTLIN_METADATA,
),
UklibUsages.KOTLIN_UKLIB_RUNTIME to listOf(
UklibUsages.KOTLIN_UKLIB_RUNTIME,
KOTLIN_RUNTIME,
JAVA_RUNTIME,
JAVA_API,
KOTLIN_METADATA
),
UklibUsages.KOTLIN_UKLIB_METADATA to listOf(
UklibUsages.KOTLIN_UKLIB_API,
KOTLIN_METADATA,
),
)[consumerUsage]?.let {
closestMatchToFirstAppropriateCandidate(it)
}
return@run
}
private fun MultipleCandidatesDetails<Usage>.closestMatchToFirstAppropriateCandidate(acceptedProducerValues: List<String>) {
val candidatesMap = candidateValues.associateBy { it.name }
acceptedProducerValues.firstOrNull { it in candidatesMap }?.let { closestMatch(candidatesMap.getValue(it)) }
}
}
private fun usageByName(name: String): Usage {
return objects.named<Usage>(name)
}
object UklibUsages {
const val KOTLIN_UKLIB_API = "KOTLIN_UKLIB_API"
const val KOTLIN_UKLIB_RUNTIME = "KOTLIN_UKLIB_RUNTIME"
const val KOTLIN_UKLIB_METADATA = "KOTLIN_UKLIB_METADATA"
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
pluginManagement {
repositories {
mavenCentral()
google()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment