diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/AndroidDependencyResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/AndroidDependencyResolver.kt new file mode 100644 index 00000000000..3454c8c25b4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/AndroidDependencyResolver.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.dsl + +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.api.AndroidSourceSet +import com.android.build.gradle.internal.LoggerWrapper +import com.android.build.gradle.internal.publishing.AndroidArtifacts +import com.android.sdklib.IAndroidTarget +import com.android.sdklib.repository.AndroidSdkHandler +import com.android.sdklib.repository.LoggerProgressIndicatorWrapper +import com.intellij.openapi.util.Version +import org.gradle.api.Project +import org.gradle.api.artifacts.* +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ModuleComponentSelector +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.forEachVariant +import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName +import java.io.File +import kotlin.reflect.full.findParameterByName + +data class AndroidDependency( + val name: String? = null, + val jar: File? = null, + val source: File? = null, + val group: String? = null, + val version: String? = null, + val collection: Set? = null +) + +object AndroidDependencyResolver { + private fun getAndroidSdkJar(project: Project): AndroidDependency? { + val androidExtension = project.extensions.findByName("android") as BaseExtension? ?: return null + val sdkLocation = getClassOrNull("com.android.build.gradle.internal.SdkLocationSourceSet")?.let { sdkLocationSourceSetClass -> + val sdkLocationSourceSet = sdkLocationSourceSetClass.kotlin.constructors.firstOrNull()?.let { callable -> + callable.findParameterByName("projectRoot")?.let { param -> + callable.callBy(mapOf(param to project.rootDir)) + } + } + + val sdkLocatorClass = getClassOrNull("com.android.build.gradle.internal.SdkLocator") + val sdkLocation = + sdkLocatorClass?.getMethodOrNull("getSdkLocation", sdkLocationSourceSetClass)?.invoke(sdkLocatorClass, sdkLocationSourceSet) + sdkLocation?.javaClass?.getMethodOrNull("getDirectory")?.invoke(sdkLocation) as? File + } ?: return null + val sdkHandler = AndroidSdkHandler.getInstance(sdkLocation) + val logger = LoggerProgressIndicatorWrapper(LoggerWrapper(project.logger)) + val androidTarget = sdkHandler.getAndroidTargetManager(logger).getTargetFromHashString(androidExtension.compileSdkVersion, logger) + return AndroidDependency( + androidTarget.fullName, + File(androidTarget.getPath(IAndroidTarget.ANDROID_JAR)), + File(androidTarget.getPath(IAndroidTarget.SOURCES)) + ) + } + + private fun getClassOrNull(s: String) = + try { + Class.forName(s) + } catch (e: Exception) { + null + } + + private fun Class<*>.getMethodOrNull(name: String, vararg parameterTypes: Class<*>) = + try { + getMethod(name, *parameterTypes) + } catch (e: Exception) { + null + } + + private fun isAndroidPluginCompatible(): Boolean { + val version = getClassOrNull("com.android.Version")?.let { + try { + it.getField("ANDROID_GRADLE_PLUGIN_VERSION").get(null) as String + } catch (e: Exception) { + return false + } + } ?: return false + return Version.parseVersion(version)?.isOrGreaterThan(3, 6) ?: false + } + + fun getAndroidSourceSetDependencies(project: Project): Map?> { + if (!isAndroidPluginCompatible()) return emptyMap() + + data class SourceSetConfigs(val implConfig: Configuration, val compileConfig: Configuration) + + val androidSdkJar = getAndroidSdkJar(project) ?: return emptyMap() + val sourceSet2Impl = HashMap() + val allImplConfigs = HashSet() + + project.forEachVariant { variant -> + val compileConfig = variant.compileConfiguration + variant.sourceSets.filterIsInstance(AndroidSourceSet::class.java).map { + val implConfig = project.configurations.getByName(it.implementationConfigurationName) + allImplConfigs.add(implConfig) + sourceSet2Impl[lowerCamelCaseName("android", it.name)] = SourceSetConfigs(implConfig, compileConfig) + } + } + + return sourceSet2Impl.mapValues { entry -> + val dependencies = findDependencies(allImplConfigs, entry.value.implConfig) + + val selfResolved = dependencies.filterIsInstance().map { AndroidDependency(collection = it.resolve()) } + val resolvedExternal = dependencies.filterIsInstance() + .flatMap { collectDependencies(it.module, entry.value.compileConfig) } + + val result = (selfResolved + resolvedExternal + androidSdkJar).toMutableList() + + if (entry.key == "androidMain") { + // this is a terrible hack, but looks like the only way, other than proper support via light-classes + val task = project.tasks.findByName("processDebugResources") + getClassOrNull("com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask")?.let { linkAppClass -> + @Suppress("UNCHECKED_CAST") + val rClassOutputJar = + linkAppClass.getMethodOrNull("getRClassOutputJar")?.invoke(task) as Provider? + rClassOutputJar?.orNull?.asFile?.let { result += AndroidDependency("R.jar", it) } + } + } + + result + }.toMap() + } + + @Suppress("UnstableApiUsage") + private fun collectDependencies( + module: ModuleIdentifier, + compileClasspathConf: Configuration + ): List { + val viewConfig: (ArtifactView.ViewConfiguration) -> Unit = { config -> + config.attributes { it.attribute(AndroidArtifacts.ARTIFACT_TYPE, AndroidArtifacts.ArtifactType.JAR.type) } + config.isLenient = true + } + + val resolvedArtifacts = compileClasspathConf.incoming.artifactView(viewConfig).artifacts.mapNotNull { + val componentIdentifier = it.id.componentIdentifier as? ModuleComponentIdentifier ?: return@mapNotNull null + val id = componentIdentifier.moduleIdentifier ?: return@mapNotNull null + id to AndroidDependency(id.name, it.file, null, id.group, componentIdentifier.version) + }.toMap() + + val resolutionResults = compileClasspathConf.incoming.resolutionResult.allComponents.mapNotNull { + val id = it.moduleVersion?.module ?: return@mapNotNull null + id to it + }.toMap() + + val deps = HashSet().also { doCollectDependencies(listOf(module), resolutionResults, it) } + return deps.mapNotNull { resolvedArtifacts[it] } + } + + @Suppress("UnstableApiUsage") + private tailrec fun doCollectDependencies( + modules: List, + resolutionResults: Map, + result: MutableSet + ) { + if (modules.isEmpty()) return + val newModules = modules.filter { result.add(it) }.mapNotNull { resolutionResults[it] }.flatMap { it.dependencies } + .mapNotNull { (it.requested as? ModuleComponentSelector)?.moduleIdentifier } + doCollectDependencies(newModules, resolutionResults, result) + } + + private fun findDependencies(implConfigs: Set, conf: Configuration): Set { + return HashSet().also { doFindDependencies(implConfigs, listOf(conf), it) } + } + + private tailrec fun doFindDependencies( + implConfigs: Set, configs: List, result: MutableSet, + visited: MutableSet = HashSet() + ) { + if (configs.isEmpty()) return + result.addAll(configs.flatMap { it.dependencies }) + doFindDependencies(implConfigs, configs.flatMap { it.extendsFrom }.filter { it !in implConfigs && visited.add(it) }, result) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt index f1dde142656..33f24aefb2d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt @@ -5,37 +5,21 @@ package org.jetbrains.kotlin.gradle.dsl -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.api.AndroidSourceSet -import com.android.build.gradle.internal.LoggerWrapper -import com.android.build.gradle.internal.publishing.AndroidArtifacts -import com.android.build.gradle.internal.publishing.AndroidArtifacts.ARTIFACT_TYPE -import com.android.sdklib.IAndroidTarget -import com.android.sdklib.repository.AndroidSdkHandler -import com.android.sdklib.repository.LoggerProgressIndicatorWrapper -import com.intellij.openapi.util.Version import groovy.lang.Closure import org.gradle.api.InvalidUserCodeException import org.gradle.api.NamedDomainObjectCollection import org.gradle.api.Project -import org.gradle.api.artifacts.* -import org.gradle.api.artifacts.component.ModuleComponentIdentifier -import org.gradle.api.artifacts.component.ModuleComponentSelector -import org.gradle.api.artifacts.result.ResolvedComponentResult -import org.gradle.api.file.FileSystemLocation -import org.gradle.api.provider.Provider import org.gradle.util.ConfigureUtil -import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetsContainerWithPresets import org.jetbrains.kotlin.gradle.plugin.mpp.* -import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName -import java.io.File -import kotlin.reflect.full.findParameterByName open class KotlinMultiplatformExtension : KotlinProjectExtension(), KotlinTargetContainerWithPresetFunctions, - KotlinTargetContainerWithNativeShortcuts -{ + KotlinTargetContainerWithNativeShortcuts { override lateinit var presets: NamedDomainObjectCollection> internal set @@ -72,155 +56,9 @@ open class KotlinMultiplatformExtension : KotlinSoftwareComponentWithCoordinatesAndPublication("kotlin", targets) } - data class AndroidDependency( - val name: String? = null, - val jar: File? = null, - val source: File? = null, - val group: String? = null, - val version: String? = null, - val collection: Set? = null - ) - - private fun getAndroidSdkJar(project: Project): AndroidDependency? { - val androidExtension = project.extensions.findByName("android") as BaseExtension? ?: return null - val sdkLocation = getClassOrNull("com.android.build.gradle.internal.SdkLocationSourceSet")?.let { sdkLocationSourceSetClass -> - val sdkLocationSourceSet = sdkLocationSourceSetClass.kotlin.constructors.firstOrNull()?.let { callable -> - callable.findParameterByName("projectRoot")?.let { param -> - callable.callBy(mapOf(param to project.rootDir)) - } - } - - val sdkLocatorClass = getClassOrNull("com.android.build.gradle.internal.SdkLocator") - val sdkLocation = - sdkLocatorClass?.getMethodOrNull("getSdkLocation", sdkLocationSourceSetClass)?.invoke(sdkLocatorClass, sdkLocationSourceSet) - sdkLocation?.javaClass?.getMethodOrNull("getDirectory")?.invoke(sdkLocation) as? File - } ?: return null - val sdkHandler = AndroidSdkHandler.getInstance(sdkLocation) - val logger = LoggerProgressIndicatorWrapper(LoggerWrapper(project.logger)) - val androidTarget = sdkHandler.getAndroidTargetManager(logger).getTargetFromHashString(androidExtension.compileSdkVersion, logger) - return AndroidDependency( - androidTarget.fullName, - File(androidTarget.getPath(IAndroidTarget.ANDROID_JAR)), - File(androidTarget.getPath(IAndroidTarget.SOURCES)) - ) - } - - private fun getClassOrNull(s: String) = - try { - Class.forName(s) - } catch (e: Exception) { - null - } - - private fun Class<*>.getMethodOrNull(name: String, vararg parameterTypes: Class<*>) = - try { - getMethod(name, *parameterTypes) - } catch (e: Exception) { - null - } - - private fun isAndroidPluginCompatible(): Boolean { - val version = getClassOrNull("com.android.Version")?.let { - try { - it.getField("ANDROID_GRADLE_PLUGIN_VERSION").get(null) as String - } catch (e: Exception) { - return false - } - } ?: return false - return Version.parseVersion(version)?.isOrGreaterThan(3, 6) ?: false - } - - fun getAndroidSourceSetDependencies(project: Project): Map?> { - if (!isAndroidPluginCompatible()) return emptyMap() - - data class SourceSetConfigs(val implConfig: Configuration, val compileConfig: Configuration) - - val androidSdkJar = getAndroidSdkJar(project) ?: return emptyMap() - val sourceSet2Impl = HashMap() - val allImplConfigs = HashSet() - - project.forEachVariant { variant -> - val compileConfig = variant.compileConfiguration - variant.sourceSets.filterIsInstance(AndroidSourceSet::class.java).map { - val implConfig = project.configurations.getByName(it.implementationConfigurationName) - allImplConfigs.add(implConfig) - sourceSet2Impl[lowerCamelCaseName("android", it.name)] = SourceSetConfigs(implConfig, compileConfig) - } - } - - return sourceSet2Impl.mapValues { entry -> - val dependencies = findDependencies(allImplConfigs, entry.value.implConfig) - - val selfResolved = dependencies.filterIsInstance().map { AndroidDependency(collection = it.resolve()) } - val resolvedExternal = dependencies.filterIsInstance() - .flatMap { collectDependencies(it.module, entry.value.compileConfig) } - - val result = (selfResolved + resolvedExternal + androidSdkJar).toMutableList() - - if (entry.key == "androidMain") { - // this is a terrible hack, but looks like the only way, other than proper support via light-classes - val task = project.tasks.findByName("processDebugResources") - getClassOrNull("com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask")?.let { linkAppClass -> - @Suppress("UNCHECKED_CAST") - val rClassOutputJar = - linkAppClass.getMethodOrNull("getRClassOutputJar")?.invoke(task) as Provider? - rClassOutputJar?.orNull?.asFile?.let { result += AndroidDependency("R.jar", it) } - } - } - - result - }.toMap() - } - - @Suppress("UnstableApiUsage") - private fun collectDependencies( - module: ModuleIdentifier, - compileClasspathConf: Configuration - ): List { - val viewConfig: (ArtifactView.ViewConfiguration) -> Unit = { config -> - config.attributes { it.attribute(ARTIFACT_TYPE, AndroidArtifacts.ArtifactType.JAR.type) } - config.isLenient = true - } - - val resolvedArtifacts = compileClasspathConf.incoming.artifactView(viewConfig).artifacts.mapNotNull { - val componentIdentifier = it.id.componentIdentifier as? ModuleComponentIdentifier ?: return@mapNotNull null - val id = componentIdentifier.moduleIdentifier ?: return@mapNotNull null - id to AndroidDependency(id.name, it.file, null, id.group, componentIdentifier.version) - }.toMap() - - val resolutionResults = compileClasspathConf.incoming.resolutionResult.allComponents.mapNotNull { - val id = it.moduleVersion?.module ?: return@mapNotNull null - id to it - }.toMap() - - val deps = HashSet().also { doCollectDependencies(listOf(module), resolutionResults, it) } - return deps.mapNotNull { resolvedArtifacts[it] } - } - - @Suppress("UnstableApiUsage") - private tailrec fun doCollectDependencies( - modules: List, - resolutionResults: Map, - result: MutableSet - ) { - if (modules.isEmpty()) return - val newModules = modules.filter { result.add(it) }.mapNotNull { resolutionResults[it] }.flatMap { it.dependencies } - .mapNotNull { (it.requested as? ModuleComponentSelector)?.moduleIdentifier } - doCollectDependencies(newModules, resolutionResults, result) - } - - private fun findDependencies(implConfigs: Set, conf: Configuration): Set { - return HashSet().also { doFindDependencies(implConfigs, listOf(conf), it) } - } - - private tailrec fun doFindDependencies( - implConfigs: Set, configs: List, result: MutableSet, - visited: MutableSet = HashSet() - ) { - if (configs.isEmpty()) return - result.addAll(configs.flatMap { it.dependencies }) - doFindDependencies(implConfigs, configs.flatMap { it.extendsFrom }.filter { it !in implConfigs && visited.add(it) }, result) - } + @Suppress("unused") + fun getAndroidSourceSetDependencies(project: Project) = + AndroidDependencyResolver.getAndroidSourceSetDependencies(project) } internal fun KotlinTarget.isProducedFromPreset(kotlinTargetPreset: KotlinTargetPreset<*>): Boolean =