blob: 19c8f6aed8e9f3a720beb430b53d8330c6f8aa38 [file] [log] [blame] [edit]
/*
* 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
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.file.FileCollection
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.KotlinMPPGradleModel.Companion.NO_KOTLIN_NATIVE_HOME
import org.jetbrains.kotlin.gradle.KotlinSourceSet.Companion.COMMON_MAIN_SOURCE_SET_NAME
import org.jetbrains.kotlin.gradle.KotlinSourceSet.Companion.COMMON_TEST_SOURCE_SET_NAME
import org.jetbrains.plugins.gradle.DefaultExternalDependencyId
import org.jetbrains.plugins.gradle.model.*
import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder
import org.jetbrains.plugins.gradle.tooling.ModelBuilderService
import org.jetbrains.plugins.gradle.tooling.util.DependencyResolver
import org.jetbrains.plugins.gradle.tooling.util.SourceSetCachedFinder
import org.jetbrains.plugins.gradle.tooling.util.resolve.DependencyResolverImpl
import java.io.File
import java.lang.reflect.Method
class KotlinMPPGradleModelBuilder : ModelBuilderService {
// This flag enables import of source sets which do not belong to any compilation
private val DEFAULT_IMPORT_ORPHAN_SOURCE_SETS = true
private val DEFAULT_BUILD_METADATA_DEPENDENCIES_FOR_ACTUALISED_SOURCE_SETS = true
override fun getErrorMessageBuilder(project: Project, e: Exception): ErrorMessageBuilder {
return ErrorMessageBuilder
.create(project, e, "Gradle import errors")
.withDescription("Unable to build Kotlin project configuration")
}
override fun canBuild(modelName: String?): Boolean {
return modelName == KotlinMPPGradleModel::class.java.name
}
override fun buildAll(modelName: String, project: Project): Any? {
val projectTargets = project.getTargets() ?: return null
val dependencyResolver = DependencyResolverImpl(
project,
false,
false,
true,
SourceSetCachedFinder(project)
)
val dependencyMapper = KotlinDependencyMapper()
val sourceSets = buildSourceSets(dependencyResolver, project, dependencyMapper) ?: return null
val sourceSetMap = sourceSets.map { it.name to it }.toMap()
val targets = buildTargets(projectTargets, sourceSetMap, dependencyResolver, project, dependencyMapper) ?: return null
computeSourceSetsDeferredInfo(sourceSetMap, targets, isHMPPEnabled(project), shouldCoerceRootSourceSetToCommon(project))
val coroutinesState = getCoroutinesState(project)
reportUnresolvedDependencies(targets)
val kotlinNativeHome = KotlinNativeHomeEvaluator.getKotlinNativeHome(project) ?: NO_KOTLIN_NATIVE_HOME
return KotlinMPPGradleModelImpl(
filterOrphanSourceSets(sourceSetMap, targets, project),
targets,
ExtraFeaturesImpl(coroutinesState, isHMPPEnabled(project), isNativeDependencyPropagationEnabled(project)),
kotlinNativeHome,
dependencyMapper.toDependencyMap()
)
}
private fun filterOrphanSourceSets(
sourceSets: Map<String, KotlinSourceSetImpl>,
targets: Collection<KotlinTarget>,
project: Project
): Map<String, KotlinSourceSetImpl> {
if (try {
project.properties["import_orphan_source_sets"]
} catch (e: Exception) {
null
}?.toString()?.toBoolean() ?: DEFAULT_IMPORT_ORPHAN_SOURCE_SETS
) return sourceSets
val compiledSourceSets: Collection<String> = targets.flatMap { it.compilations }.flatMap { it.sourceSets }.flatMap { it.dependsOnSourceSets.union(listOf(it.name)) }.distinct()
sourceSets.filter { !compiledSourceSets.contains(it.key) }.forEach {
logger.warn("[sync warning] Source set \"${it.key}\" is not compiled with any compilation. This source set is not imported in the IDE.")
}
return sourceSets.filter { compiledSourceSets.contains(it.key) }
}
private fun isHMPPEnabled(project: Project): Boolean {
//TODO(auskov): replace with Project.isKotlinGranularMetadataEnabled after merging with gradle branch
return (project.findProperty("kotlin.mpp.enableGranularSourceSetsMetadata") as? String)?.toBoolean() ?: false
}
private fun shouldCoerceRootSourceSetToCommon(project: Project): Boolean {
return (project.findProperty("kotlin.mpp.coerceRootSourceSetsToCommon") as? String)?.toBoolean() ?: true
}
private fun isNativeDependencyPropagationEnabled(project: Project): Boolean {
return (project.findProperty("kotlin.native.enableDependencyPropagation") as? String)?.toBoolean() ?: true
}
private fun reportUnresolvedDependencies(targets: Collection<KotlinTarget>) {
targets.asSequence()
.flatMap { it.compilations.asSequence() }
.flatMap { it.dependencies.asSequence() }
.mapNotNull { (it as? UnresolvedExternalDependency)?.failureMessage }
.toSet()
.forEach { logger.warn(it) }
}
private fun getCoroutinesState(project: Project): String? {
val kotlinExt = project.extensions.findByName("kotlin") ?: return null
val getExperimental = kotlinExt.javaClass.getMethodOrNull("getExperimental") ?: return null
val experimentalExt = getExperimental(kotlinExt) ?: return null
val getCoroutines = experimentalExt.javaClass.getMethodOrNull("getCoroutines") ?: return null
return getCoroutines(experimentalExt) as? String
}
private fun calculateDependsOnClosure(
sourceSet: KotlinSourceSetImpl?,
sourceSetsMap: Map<String, KotlinSourceSetImpl>,
cache: MutableMap<String, Set<String>>
): Set<String> {
return if (sourceSet == null) {
emptySet()
} else {
cache[sourceSet.name] ?: sourceSet.dependsOnSourceSets.flatMap { name ->
calculateDependsOnClosure(
sourceSetsMap[name],
sourceSetsMap,
cache
).union(setOf(name))
}.toSet().also { cache[sourceSet.name] = it }
}
}
private fun buildSourceSets(
dependencyResolver: DependencyResolver,
project: Project,
dependencyMapper: KotlinDependencyMapper
): Collection<KotlinSourceSetImpl>? {
val kotlinExt = project.extensions.findByName("kotlin") ?: return null
val getSourceSets = kotlinExt.javaClass.getMethodOrNull("getSourceSets") ?: return null
@Suppress("UNCHECKED_CAST")
val sourceSets =
(getSourceSets(kotlinExt) as? NamedDomainObjectContainer<Named>)?.asMap?.values ?: emptyList<Named>()
val androidDeps = buildAndroidDeps(kotlinExt.javaClass.classLoader, project)
// Some performance optimisation: do not build metadata dependencies if source set is not common
val doBuildMetadataDependencies = try {
project.properties["build_metadata_dependencies_for_actualised_source_sets"]?.toString()?.toBoolean()
} catch (_: Exception) {
null
} ?: DEFAULT_BUILD_METADATA_DEPENDENCIES_FOR_ACTUALISED_SOURCE_SETS
val allSourceSetsProtos = sourceSets.mapNotNull { buildSourceSet(it, dependencyResolver, project, dependencyMapper, androidDeps) }
val allSourceSets = if (doBuildMetadataDependencies) {
allSourceSetsProtos.map { proto -> proto.buildKotlinSourceSetImpl(true) }
} else {
val unactualizedSourceSets = allSourceSetsProtos.flatMap { it.dependsOnSourceSets }.distinct()
allSourceSetsProtos.map { proto -> proto.buildKotlinSourceSetImpl(unactualizedSourceSets.contains(proto.name)) }
}
val map = allSourceSets.map { it.name to it }.toMap()
val dependsOnCache = HashMap<String, Set<String>>()
return allSourceSets.map { sourceSet ->
KotlinSourceSetImpl(
sourceSet.name,
sourceSet.languageSettings,
sourceSet.sourceDirs,
sourceSet.resourceDirs,
sourceSet.dependencies,
calculateDependsOnClosure(sourceSet, map, dependsOnCache),
sourceSet.actualPlatforms as KotlinPlatformContainerImpl,
sourceSet.isTestModule
)
}
}
private fun buildAndroidDeps(classLoader: ClassLoader, project: Project): Map<String, List<Any>>? {
val includeAndroidDeps = try {
project.properties["kotlin.include.android.dependencies"]?.toString()?.toBoolean() == true
} catch (_: Exception) {
false
}
if (includeAndroidDeps) {
try {
val resolverClass = classLoader.loadClass("org.jetbrains.kotlin.gradle.targets.android.internal.AndroidDependencyResolver")
val getAndroidSourceSetDependencies = resolverClass.getMethodOrNull("getAndroidSourceSetDependencies", Project::class.java)
val resolver = resolverClass.getField("INSTANCE").get(null)
@Suppress("UNCHECKED_CAST")
return getAndroidSourceSetDependencies?.let { it(resolver, project) } as Map<String, List<Any>>?
} catch (e: Exception) {
logger.info("Unexpected exception", e)
}
}
return null
}
private fun buildSourceSet(
gradleSourceSet: Named,
dependencyResolver: DependencyResolver,
project: Project,
dependencyMapper: KotlinDependencyMapper,
androidDeps: Map<String, List<Any>>?
): KotlinSourceSetProto? {
val sourceSetClass = gradleSourceSet.javaClass
val getLanguageSettings = sourceSetClass.getMethodOrNull("getLanguageSettings") ?: return null
val getSourceDirSet = sourceSetClass.getMethodOrNull("getKotlin") ?: return null
val getResourceDirSet = sourceSetClass.getMethodOrNull("getResources") ?: return null
val getDependsOn = sourceSetClass.getMethodOrNull("getDependsOn") ?: return null
val languageSettings = getLanguageSettings(gradleSourceSet)?.let { buildLanguageSettings(it) } ?: return null
val sourceDirs = (getSourceDirSet(gradleSourceSet) as? SourceDirectorySet)?.srcDirs ?: emptySet()
val resourceDirs = (getResourceDirSet(gradleSourceSet) as? SourceDirectorySet)?.srcDirs ?: emptySet()
@Suppress("UNCHECKED_CAST")
val dependsOnSourceSets = (getDependsOn(gradleSourceSet) as? Set<Named>)?.mapTo(LinkedHashSet()) { it.name } ?: emptySet<String>()
val sourceSetDependenciesBuilder: () -> Array<KotlinDependencyId> = {
buildSourceSetDependencies(gradleSourceSet, dependencyResolver, project, androidDeps).map { dependencyMapper.getId(it) }.distinct()
.toTypedArray()
}
return KotlinSourceSetProto(
gradleSourceSet.name,
languageSettings,
sourceDirs,
resourceDirs,
sourceSetDependenciesBuilder,
dependsOnSourceSets
)
}
private fun buildLanguageSettings(gradleLanguageSettings: Any): KotlinLanguageSettings? {
val languageSettingsClass = gradleLanguageSettings.javaClass
val getLanguageVersion = languageSettingsClass.getMethodOrNull("getLanguageVersion") ?: return null
val getApiVersion = languageSettingsClass.getMethodOrNull("getApiVersion") ?: return null
val getProgressiveMode = languageSettingsClass.getMethodOrNull("getProgressiveMode") ?: return null
val getEnabledLanguageFeatures = languageSettingsClass.getMethodOrNull("getEnabledLanguageFeatures") ?: return null
val getExperimentalAnnotationsInUse = languageSettingsClass.getMethodOrNull("getExperimentalAnnotationsInUse")
val getCompilerPluginArguments = languageSettingsClass.getMethodOrNull("getCompilerPluginArguments")
val getCompilerPluginClasspath = languageSettingsClass.getMethodOrNull("getCompilerPluginClasspath")
val getFreeCompilerArgs = languageSettingsClass.getMethodOrNull("getFreeCompilerArgs")
@Suppress("UNCHECKED_CAST")
return KotlinLanguageSettingsImpl(
getLanguageVersion(gradleLanguageSettings) as? String,
getApiVersion(gradleLanguageSettings) as? String,
getProgressiveMode(gradleLanguageSettings) as? Boolean ?: false,
getEnabledLanguageFeatures(gradleLanguageSettings) as? Set<String> ?: emptySet(),
getExperimentalAnnotationsInUse?.invoke(gradleLanguageSettings) as? Set<String> ?: emptySet(),
(getCompilerPluginArguments?.invoke(gradleLanguageSettings) as? List<String> ?: emptyList()).toTypedArray(),
(getCompilerPluginClasspath?.invoke(gradleLanguageSettings) as? FileCollection)?.files ?: emptySet(),
(getFreeCompilerArgs?.invoke(gradleLanguageSettings) as? List<String>).orEmpty().toTypedArray()
)
}
private fun buildDependencies(
dependencyHolder: Any,
dependencyResolver: DependencyResolver,
configurationNameAccessor: String,
scope: String,
project: Project,
metadataDependencyTransformationBuilder: MetadataDependencyTransformationBuilder
): Collection<KotlinDependency> {
val dependencyHolderClass = dependencyHolder.javaClass
val getConfigurationName = dependencyHolderClass.getMethodOrNull(configurationNameAccessor) ?: return emptyList()
val configurationName = getConfigurationName(dependencyHolder) as? String ?: return emptyList()
val configuration = project.configurations.findByName(configurationName) ?: return emptyList()
if (!configuration.isCanBeResolved) return emptyList()
val dependencyAdjuster =
DependencyAdjuster(configuration, scope, project, metadataDependencyTransformationBuilder.getTransformations(configurationName))
val resolvedDependencies = dependencyResolver
.resolveDependencies(configuration)
.apply {
forEach<ExternalDependency?> { (it as? AbstractExternalDependency)?.scope = scope }
forEach<ExternalDependency?> {
if (it is DefaultExternalProjectDependency && it.projectDependencyArtifacts !is ArrayList) {
it.projectDependencyArtifacts = ArrayList(it.projectDependencyArtifacts)
}
}
}
.flatMap { dependencyAdjuster.adjustDependency(it) }
val singleDependencyFiles = resolvedDependencies.mapNotNullTo(LinkedHashSet<File>()) {
(it as? FileCollectionDependency)?.files?.singleOrNull()
}
// Workaround for duplicated dependencies specified as a file collection (KT-26675)
// Drop this code when the issue is fixed in the platform
return resolvedDependencies.filter { dependency ->
if (dependency !is FileCollectionDependency) return@filter true
val files = dependency.files
if (files.size <= 1) return@filter true
(files.any { it !in singleDependencyFiles })
}
}
private fun buildTargets(
projectTargets: Collection<Named>,
sourceSetMap: Map<String, KotlinSourceSet>,
dependencyResolver: DependencyResolver,
project: Project,
dependencyMapper: KotlinDependencyMapper
): Collection<KotlinTarget>? {
val isHMPPEnabled = isHMPPEnabled(project)
return projectTargets.mapNotNull { buildTarget(it, sourceSetMap, dependencyResolver, project, dependencyMapper, isHMPPEnabled) }
}
private operator fun Any?.get(methodName: String, vararg params: Any): Any? {
return this[methodName, params.map { it.javaClass }, params.toList()]
}
private operator fun Any?.get(methodName: String, paramTypes: List<Class<*>>, params: List<Any?>): Any? {
if (this == null) return null
return this::class.java.getMethodOrNull(methodName, *paramTypes.toTypedArray())?.invoke(this, *params.toTypedArray())
}
private fun buildArtifact(
executableName: String,
linkTask: Task,
runConfiguration: KonanRunConfigurationModel
): KonanArtifactModel? {
val outputKind = linkTask["getOutputKind"]["name"] as? String ?: return null
val konanTargetName = linkTask["getTarget"] as? String ?: error("No arch target found")
val outputFile = (linkTask["getOutputFile"] as? Provider<*>)?.orNull as? File ?: return null
val compilationTarget = linkTask["getCompilation"]["getTarget"]
val compilationTargetName = compilationTarget["getName"] as? String ?: return null
val isTests = linkTask["getProcessTests"] as? Boolean ?: return null
return KonanArtifactModelImpl(
compilationTargetName,
executableName,
outputKind,
konanTargetName,
outputFile,
linkTask.path,
runConfiguration,
isTests
)
}
private fun konanArtifacts(target: Named): List<KonanArtifactModel> {
val result = ArrayList<KonanArtifactModel>()
val binaries = target["getBinaries"] as? Collection<*> ?: return result
binaries.forEach { binary ->
val executableName = binary["getBaseName"] as? String ?: ""
val linkTask = binary["getLinkTask"] as? Task ?: return@forEach
val runConfiguration = KonanRunConfigurationModelImpl(binary["getRunTask"] as? Exec)
buildArtifact(executableName, linkTask, runConfiguration)?.let { result.add(it) }
}
return result
}
private fun buildTarget(
gradleTarget: Named,
sourceSetMap: Map<String, KotlinSourceSet>,
dependencyResolver: DependencyResolver,
project: Project,
dependencyMapper: KotlinDependencyMapper,
isHMPPEnabled: Boolean
): KotlinTarget? {
val targetClass = gradleTarget.javaClass
val getPlatformType = targetClass.getMethodOrNull("getPlatformType") ?: return null
val getDisambiguationClassifier = targetClass.getMethodOrNull("getDisambiguationClassifier") ?: return null
val platformId = (getPlatformType.invoke(gradleTarget) as? Named)?.name ?: return null
val platform = KotlinPlatform.byId(platformId) ?: return null
val useDisambiguationClassifier =
targetClass.getMethodOrNull("getUseDisambiguationClassifierAsSourceSetNamePrefix")?.invoke(gradleTarget) as? Boolean ?: true
val disambiguationClassifier = if (useDisambiguationClassifier)
getDisambiguationClassifier(gradleTarget) as? String
else {
targetClass.getMethodOrNull("getOverrideDisambiguationClassifierOnIdeImport")?.invoke(gradleTarget) as? String
}
val getPreset = targetClass.getMethodOrNull("getPreset")
val targetPresetName: String?
targetPresetName = try {
val targetPreset = getPreset?.invoke(gradleTarget)
val getPresetName = targetPreset?.javaClass?.getMethodOrNull("getName")
getPresetName?.invoke(targetPreset) as? String
} catch (e: Throwable) {
"${e::class.java.name}:${e.message}"
}
val gradleCompilations = getCompilations(gradleTarget) ?: return null
val compilations = gradleCompilations.mapNotNull {
val compilation = buildCompilation(it, disambiguationClassifier, sourceSetMap, dependencyResolver, project, dependencyMapper)
if (compilation == null || platform != KotlinPlatform.ANDROID) {
compilation
} else {
compilation.addDependsOnSourceSetsToCompilation(sourceSetMap, isHMPPEnabled)
}
}
val jar = buildTargetJar(gradleTarget, project)
val testRunTasks = buildTestRunTasks(project, gradleTarget)
val nativeMainRunTasks =
if (platform == KotlinPlatform.NATIVE) buildNativeMainRunTasks(gradleTarget)
else emptyList()
val artifacts = konanArtifacts(gradleTarget)
val target = KotlinTargetImpl(
gradleTarget.name,
targetPresetName,
disambiguationClassifier,
platform,
compilations,
testRunTasks,
nativeMainRunTasks,
jar,
artifacts
)
compilations.forEach {
it.disambiguationClassifier = target.disambiguationClassifier
it.platform = target.platform
}
return target
}
private fun KotlinCompilationImpl.addDependsOnSourceSetsToCompilation(
sourceSetMap: Map<String, KotlinSourceSet>,
isHMPPEnabled: Boolean
): KotlinCompilationImpl {
val dependsOnSourceSets = this.sourceSets.flatMap { it.dependsOnSourceSets }.mapNotNull { sourceSetMap[it] }
if (!isHMPPEnabled) {
// intermediate source sets should be common if HMPP is disabled
dependsOnSourceSets.subtract(this.sourceSets).forEach {
it.actualPlatforms.addSimplePlatforms(listOf(KotlinPlatform.COMMON))
}
}
return KotlinCompilationImpl(
this.name,
this.sourceSets.union(dependsOnSourceSets),
this.dependencies,
this.output,
this.arguments,
this.dependencyClasspath,
this.kotlinTaskProperties,
this.nativeExtensions
)
}
private fun buildNativeMainRunTasks(gradleTarget: Named): Collection<KotlinNativeMainRunTask> {
val executableBinaries = (gradleTarget::class.java.getMethodOrNull("getBinaries")?.invoke(gradleTarget) as? Collection<Any>)
?.filter { it.javaClass.name == "org.jetbrains.kotlin.gradle.plugin.mpp.Executable" } ?: return emptyList()
return executableBinaries.mapNotNull { binary ->
val runTaskName = binary::class.java.getMethod("getRunTaskName").invoke(binary) as String? ?: return@mapNotNull null
val entryPoint = binary::class.java.getMethod("getEntryPoint").invoke(binary) as String? ?: return@mapNotNull null
val debuggable = binary::class.java.getMethod("getDebuggable").invoke(binary) as Boolean
val compilationName = binary.javaClass.getMethodOrNull("getCompilation")?.invoke(binary)?.let {
it.javaClass.getMethodOrNull("getCompilationName")?.invoke(it)?.toString()
} ?: KotlinCompilation.MAIN_COMPILATION_NAME
KotlinNativeMainRunTaskImpl(
runTaskName,
compilationName,
entryPoint,
debuggable
)
}
}
private fun buildTestRunTasks(project: Project, gradleTarget: Named): Collection<KotlinTestRunTask> {
val getTestRunsMethod = gradleTarget.javaClass.getMethodOrNull("getTestRuns")
if (getTestRunsMethod != null) {
val testRuns = getTestRunsMethod?.invoke(gradleTarget) as? Iterable<Any>
if (testRuns != null) {
val testReports =
testRuns.mapNotNull { (it.javaClass.getMethodOrNull("getExecutionTask")?.invoke(it) as? TaskProvider<Task>)?.get() }
val testTasks = testReports.flatMap {
((it.javaClass.getMethodOrNull("getTestTasks")?.invoke(it) as? Collection<Any>)?.mapNotNull {
when {
//TODO(auskov): getTestTasks should return collection of TaskProviders without mixing with Tasks
it is Provider<*> -> it.get() as? Task
it is Task -> it
else -> null
}
}) ?: listOf(it)
}
return testTasks.filter { it.enabled }.map {
val name = it.name
val compilation = it.javaClass.getMethodOrNull("getCompilation")?.invoke(it)
val compilationName = compilation?.javaClass?.getMethodOrNull("getCompilationName")?.invoke(compilation)?.toString()
?: KotlinCompilation.TEST_COMPILATION_NAME
KotlinTestRunTaskImpl(name, compilationName)
}.toList()
}
return emptyList()
}
// Otherwise, find the Kotlin test task with names matching the target name. This is a workaround that makes assumptions about
// the tasks naming logic and is therefore an unstable and temporary solution until test runs API is implemented:
@Suppress("UNCHECKED_CAST")
val kotlinTestTaskClass = try {
gradleTarget.javaClass.classLoader.loadClass("org.jetbrains.kotlin.gradle.tasks.KotlinTest") as Class<out Task>
} catch (_: ClassNotFoundException) {
return emptyList()
}
val targetDisambiguationClassifier = run {
val getDisambiguationClassifier = gradleTarget.javaClass.getMethodOrNull("getDisambiguationClassifier")
?: return emptyList()
getDisambiguationClassifier(gradleTarget) as String?
}
// The 'targetName' of a test task matches the target disambiguation classifier, potentially with suffix, e.g. jsBrowser
val getTargetName = kotlinTestTaskClass.getDeclaredMethodOrNull("getTargetName") ?: return emptyList()
val jvmTestTaskClass = try {
gradleTarget.javaClass.classLoader.loadClass("org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest") as Class<out Task>
} catch (_: ClassNotFoundException) {
return emptyList()
}
val getJvmTargetName = jvmTestTaskClass.getDeclaredMethodOrNull("getTargetName") ?: return emptyList()
return project.tasks.filter { kotlinTestTaskClass.isInstance(it) || jvmTestTaskClass.isInstance(it) }.mapNotNull { task ->
val testTaskDisambiguationClassifier =
(if (kotlinTestTaskClass.isInstance(task)) getTargetName(task) else getJvmTargetName(task)) as String?
task.name.takeIf {
targetDisambiguationClassifier.isNullOrEmpty() ||
testTaskDisambiguationClassifier != null &&
testTaskDisambiguationClassifier.startsWith(targetDisambiguationClassifier.orEmpty())
}
}.map { KotlinTestRunTaskImpl(it, KotlinCompilation.TEST_COMPILATION_NAME) }
}
private fun buildTargetJar(gradleTarget: Named, project: Project): KotlinTargetJar? {
val targetClass = gradleTarget.javaClass
val getArtifactsTaskName = targetClass.getMethodOrNull("getArtifactsTaskName") ?: return null
val artifactsTaskName = getArtifactsTaskName(gradleTarget) as? String ?: return null
val jarTask = project.tasks.findByName(artifactsTaskName) ?: return null
val jarTaskClass = jarTask.javaClass
val getArchivePath = jarTaskClass.getMethodOrNull("getArchivePath")
val archiveFile = getArchivePath?.invoke(jarTask) as? File?
return KotlinTargetJarImpl(archiveFile)
}
private fun buildCompilation(
gradleCompilation: Named,
classifier: String?,
sourceSetMap: Map<String, KotlinSourceSet>,
dependencyResolver: DependencyResolver,
project: Project,
dependencyMapper: KotlinDependencyMapper
): KotlinCompilationImpl? {
val compilationClass = gradleCompilation.javaClass
val getKotlinSourceSets = compilationClass.getMethodOrNull("getKotlinSourceSets") ?: return null
@Suppress("UNCHECKED_CAST")
val kotlinGradleSourceSets = (getKotlinSourceSets(gradleCompilation) as? Collection<Named>) ?: return null
val kotlinSourceSets = kotlinGradleSourceSets.mapNotNull { sourceSetMap[it.name] }
val compileKotlinTask = getCompileKotlinTaskName(project, gradleCompilation) ?: return null
val output = buildCompilationOutput(gradleCompilation, compileKotlinTask) ?: return null
val arguments = buildCompilationArguments(compileKotlinTask) ?: return null
val dependencyClasspath = buildDependencyClasspath(compileKotlinTask)
val dependencies =
buildCompilationDependencies(gradleCompilation, classifier, sourceSetMap, dependencyResolver, project, dependencyMapper)
val kotlinTaskProperties = getKotlinTaskProperties(compileKotlinTask)
// Get konanTarget (for native compilations only).
val konanTarget = compilationClass.getMethodOrNull("getKonanTarget")?.let { getKonanTarget ->
val konanTarget = getKonanTarget.invoke(gradleCompilation)
konanTarget.javaClass.getMethodOrNull("getName")?.let {
it.invoke(konanTarget) as? String
}
}
val nativeExtensions = konanTarget?.let(::KotlinNativeCompilationExtensionsImpl)
return KotlinCompilationImpl(
gradleCompilation.name,
kotlinSourceSets,
dependencies.map { dependencyMapper.getId(it) }.distinct().toTypedArray(),
output,
arguments,
dependencyClasspath.toTypedArray(),
kotlinTaskProperties,
nativeExtensions
)
}
/**
* Returns only those dependencies with RUNTIME scope which are not present with compile scope
*/
private fun Collection<KotlinDependency>.onlyNewDependencies(compileDependencies: Collection<KotlinDependency>): List<KotlinDependency> {
val compileDependencyArtefacts = compileDependencies.flatMap { (it as? ExternalProjectDependency)?.projectDependencyArtifacts ?: emptyList() }
return this.filter {
if (it is ExternalProjectDependency)
!(compileDependencyArtefacts.containsAll(it.projectDependencyArtifacts))
else
true
}
}
private fun buildCompilationDependencies(
gradleCompilation: Named,
classifier: String?,
sourceSetMap: Map<String, KotlinSourceSet>,
dependencyResolver: DependencyResolver,
project: Project,
dependencyMapper: KotlinDependencyMapper
): Set<KotlinDependency> {
return LinkedHashSet<KotlinDependency>().apply {
val transformationBuilder = MetadataDependencyTransformationBuilder(gradleCompilation)
this += buildDependencies(
gradleCompilation, dependencyResolver, "getCompileDependencyConfigurationName", "COMPILE", project, transformationBuilder
)
this += buildDependencies(
gradleCompilation, dependencyResolver, "getRuntimeDependencyConfigurationName", "RUNTIME", project, transformationBuilder
).onlyNewDependencies(this)
this += sourceSetMap[compilationFullName(
gradleCompilation.name,
classifier
)]?.dependencies?.map { dependencyMapper.getDependency(it) }?.filterNotNull() ?: emptySet()
}
}
private class MetadataDependencyTransformationBuilder(val sourceSet: Any) {
val transformationsMethod = sourceSet.javaClass.getMethodOrNull("getDependenciesTransformation", String::class.java)
class KotlinMetadataDependencyTransformation(
val groupId: String?,
val moduleName: String,
val projectPath: String?,
val allVisibleSourceSets: Set<String>,
val useFilesForSourceSets: Map<String, Iterable<File>>
) {
constructor(
transformation: Any,
group: Method,
module: Method,
projectPath: Method,
visibleSourceSets: Method,
useFilesForSourceSets: Method
) : this(
group(transformation) as String?,
module(transformation) as String,
projectPath(transformation) as String?,
visibleSourceSets(transformation) as Set<String>,
useFilesForSourceSets(transformation) as Map<String, Iterable<File>>
)
}
fun getTransformations(configurationName: String): Collection<KotlinMetadataDependencyTransformation> {
val transformations = transformationsMethod?.invoke(sourceSet, configurationName) as? Iterable<Any> ?: return emptyList()
val transformationClass = transformations.firstOrNull()?.javaClass
?: return emptyList()
val getGroupId = transformationClass.getMethodOrNull("getGroupId") ?: return emptyList()
val getModuleName = transformationClass.getMethodOrNull("getModuleName") ?: return emptyList()
val getProjectPath = transformationClass.getMethodOrNull("getProjectPath") ?: return emptyList()
val getAllVisibleSourceSets = transformationClass.getMethodOrNull("getAllVisibleSourceSets") ?: return emptyList()
val getUseFilesForSourceSets = transformationClass.getMethodOrNull("getUseFilesForSourceSets") ?: return emptyList()
return transformations.map { transformation ->
KotlinMetadataDependencyTransformation(
transformation,
getGroupId,
getModuleName,
getProjectPath,
getAllVisibleSourceSets,
getUseFilesForSourceSets
)
}.filter { it.allVisibleSourceSets.isNotEmpty() }
}
}
private fun buildSourceSetDependencies(
gradleSourceSet: Named,
dependencyResolver: DependencyResolver,
project: Project,
androidDeps: Map<String, List<Any>>?
): List<KotlinDependency> {
return ArrayList<KotlinDependency>().apply {
val transformationBuilder = MetadataDependencyTransformationBuilder(gradleSourceSet)
this += buildDependencies(
gradleSourceSet, dependencyResolver, "getApiMetadataConfigurationName", "COMPILE", project, transformationBuilder
)
this += buildDependencies(
gradleSourceSet, dependencyResolver, "getImplementationMetadataConfigurationName", "COMPILE", project, transformationBuilder
)
this += buildDependencies(
gradleSourceSet, dependencyResolver, "getCompileOnlyMetadataConfigurationName", "COMPILE", project, transformationBuilder
)
this += buildDependencies(
gradleSourceSet, dependencyResolver, "getRuntimeOnlyMetadataConfigurationName", "RUNTIME", project, transformationBuilder
).onlyNewDependencies(this)
this += buildAndroidSourceSetDependencies(androidDeps, gradleSourceSet)
}
}
private fun buildAndroidSourceSetDependencies(
androidDeps: Map<String, List<Any>>?,
gradleSourceSet: Named
): Collection<KotlinDependency> {
return androidDeps?.get(gradleSourceSet.name)?.map { it ->
@Suppress("UNCHECKED_CAST")
val collection = it["getCollection"] as Set<File>?
if (collection == null) {
DefaultExternalLibraryDependency().apply {
(id as? DefaultExternalDependencyId)?.apply {
name = it["getName"] as String?
group = it["getGroup"] as String?
version = it["getVersion"] as String?
}
file = it["getJar"] as File
source = it["getSource"] as File?
}
} else {
DefaultFileCollectionDependency(collection)
}
} ?: emptyList()
}
@Suppress("UNCHECKED_CAST")
private fun safelyGetArguments(compileKotlinTask: Task, accessor: Method?) = try {
accessor?.invoke(compileKotlinTask) as? List<String>
} catch (e: Exception) {
logger.info(e.message ?: "Unexpected exception: $e", e)
null
} ?: emptyList()
private fun buildCompilationArguments(compileKotlinTask: Task): KotlinCompilationArguments? {
val compileTaskClass = compileKotlinTask.javaClass
val getCurrentArguments = compileTaskClass.getMethodOrNull("getSerializedCompilerArguments")
val getDefaultArguments = compileTaskClass.getMethodOrNull("getDefaultSerializedCompilerArguments")
val currentArguments = safelyGetArguments(compileKotlinTask, getCurrentArguments)
val defaultArguments = safelyGetArguments(compileKotlinTask, getDefaultArguments)
return KotlinCompilationArgumentsImpl(defaultArguments.toTypedArray(), currentArguments.toTypedArray())
}
private fun buildDependencyClasspath(compileKotlinTask: Task): List<String> {
val abstractKotlinCompileClass =
compileKotlinTask.javaClass.classLoader.loadClass(AbstractKotlinGradleModelBuilder.ABSTRACT_KOTLIN_COMPILE_CLASS)
val getCompileClasspath =
abstractKotlinCompileClass.getDeclaredMethodOrNull("getCompileClasspath") ?: return emptyList()
@Suppress("UNCHECKED_CAST")
return (getCompileClasspath(compileKotlinTask) as? Collection<File>)?.map { it.path } ?: emptyList()
}
private fun buildCompilationOutput(
gradleCompilation: Named,
compileKotlinTask: Task
): KotlinCompilationOutput? {
val compilationClass = gradleCompilation.javaClass
val getOutput = compilationClass.getMethodOrNull("getOutput") ?: return null
val gradleOutput = getOutput(gradleCompilation) ?: return null
val gradleOutputClass = gradleOutput.javaClass
val getClassesDirs = gradleOutputClass.getMethodOrNull("getClassesDirs") ?: return null
val getResourcesDir = gradleOutputClass.getMethodOrNull("getResourcesDir") ?: return null
val compileKotlinTaskClass = compileKotlinTask.javaClass
val getDestinationDir = compileKotlinTaskClass.getMethodOrNull("getDestinationDir")
val getOutputFile = compileKotlinTaskClass.getMethodOrNull("getOutputFile")
val classesDirs = getClassesDirs(gradleOutput) as? FileCollection ?: return null
val resourcesDir = getResourcesDir(gradleOutput) as? File ?: return null
@Suppress("UNCHECKED_CAST") val destinationDir =
getDestinationDir?.invoke(compileKotlinTask) as? File
//TODO: Hack for KotlinNativeCompile
?: (getOutputFile?.invoke(compileKotlinTask) as? Property<File>)?.orNull?.parentFile
?: return null
return KotlinCompilationOutputImpl(classesDirs.files, destinationDir, resourcesDir)
}
private fun computeSourceSetsDeferredInfo(
sourceSets: Map<String, KotlinSourceSetImpl>,
targets: Collection<KotlinTarget>,
isHMPPEnabled: Boolean,
coerceRootSourceSetsToCommon: Boolean
) {
// includes only compilations where source set is listed
val compiledSourceSetToCompilations = LinkedHashMap<KotlinSourceSet, MutableSet<KotlinCompilation>>()
// includes compilations where source set is included via dependsOn
val allSourceSetToCompilations = LinkedHashMap<KotlinSourceSet, MutableSet<KotlinCompilation>>()
for (target in targets) {
for (compilation in target.compilations) {
for (sourceSet in compilation.sourceSets) {
compiledSourceSetToCompilations.getOrPut(sourceSet) { LinkedHashSet() } += compilation
allSourceSetToCompilations.getOrPut(sourceSet) { LinkedHashSet() } += compilation
sourceSet.dependsOnSourceSets.mapNotNull { sourceSets[it] }.forEach {
allSourceSetToCompilations.getOrPut(it) { LinkedHashSet() } += compilation
}
}
}
}
for (sourceSet in sourceSets.values) {
if (!isHMPPEnabled) {
val name = sourceSet.name
if (name == KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) {
sourceSet.isTestModule = false
continue
}
if (name == KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) {
sourceSet.isTestModule = true
continue
}
}
(allSourceSetToCompilations[sourceSet]?.all { it.isTestModule }
?: if (!isHMPPEnabled && sourceSet.name == KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) true else null)?.let { isTest ->
sourceSet.isTestModule = isTest
}
(if (isHMPPEnabled) allSourceSetToCompilations[sourceSet] else compiledSourceSetToCompilations[sourceSet])?.let { compilations ->
val platforms = compilations.map { it.platform }
sourceSet.actualPlatforms.addSimplePlatforms(platforms)
}
if (sourceSet.shouldCoerceToCommon(isHMPPEnabled, coerceRootSourceSetsToCommon)) {
sourceSet.actualPlatforms.addSimplePlatforms(listOf(KotlinPlatform.COMMON))
}
}
}
private fun KotlinSourceSetImpl.shouldCoerceToCommon(isHMPPEnabled: Boolean, coerceRootSourceSetsToCommon: Boolean): Boolean {
val isRoot = name == COMMON_MAIN_SOURCE_SET_NAME || name == COMMON_TEST_SOURCE_SET_NAME
// never makes sense to coerce single-targeted source-sets
if (actualPlatforms.platforms.size == 1) return false
return when {
// pre-HMPP has only single-targeted source sets and COMMON
!isHMPPEnabled -> true
// in HMPP, we might want to coerce source sets to common, but only root ones, and only
// when the corresponding setting is turned on
isHMPPEnabled && isRoot && coerceRootSourceSetsToCommon -> true
// in all other cases, in HMPP we shouldn't coerce anything
else -> false
}
}
private class DependencyAdjuster(
private val configuration: Configuration,
private val scope: String,
private val project: Project,
transformations: Collection<MetadataDependencyTransformationBuilder.KotlinMetadataDependencyTransformation>
) {
private val adjustmentMap = HashMap<ExternalDependency, List<ExternalDependency>>()
private val EXTRA_DEFAULT_CONFIGURATION_NAMES = listOf("metadataApiElements")
private val projectDependencyTransformation =
transformations.filter { it.projectPath != null }.associateBy { it.projectPath }
val dependenciesByProjectPath by lazy {
configuration
.resolvedConfiguration
.lenientConfiguration
.allModuleDependencies
.mapNotNull { dependency ->
val artifact = dependency.moduleArtifacts.firstOrNull {
it.id.componentIdentifier is ProjectComponentIdentifier
} ?: return@mapNotNull null
dependency to artifact
}
.groupBy { (it.second.id.componentIdentifier as ProjectComponentIdentifier).projectPath }
}
private fun wrapDependency(dependency: ExternalProjectDependency, newConfigurationName: String): ExternalProjectDependency {
return DefaultExternalProjectDependency(dependency).apply {
this.configurationName = newConfigurationName
val nestedDependencies = this.dependencies.flatMap { adjustDependency(it) }
this.dependencies.clear()
this.dependencies.addAll(nestedDependencies)
}
}
private val libraryDependencyTransformation =
transformations.filter { it.projectPath == null }.associateBy { it.groupId to it.moduleName }
private fun adjustLibraryDependency(dependency: ExternalDependency, parentScope: String? = null): List<ExternalDependency> =
when (dependency) {
is ExternalLibraryDependency -> {
val replaceFiles = libraryDependencyTransformation[dependency.id.group to dependency.id.name]?.useFilesForSourceSets
when {
replaceFiles != null -> replaceFiles.flatMap { (sourceSetName, replaceFiles) ->
replaceFiles.map { replaceFile ->
DefaultExternalLibraryDependency(dependency).apply {
// Transitive dependencies don't have their scope set properly; TODO investigate may be IJ bug?
scope = dependency.scope ?: parentScope
classifier = sourceSetName
file = replaceFile
val adjustedDependencies =
dependency.dependencies.flatMap { adjustDependency(it, dependency.scope ?: parentScope) }
dependencies.clear()
dependencies.addAll(adjustedDependencies)
}
}
}
else ->
listOf(
// Do nothing but set the correct scope for this dependency if needed and adjust recursively:
DefaultExternalLibraryDependency(dependency).apply {
scope = dependency.scope ?: parentScope
val adjustedDependencies =
dependency.dependencies.flatMap { adjustDependency(it, dependency.scope ?: parentScope) }
dependencies.clear()
dependencies.addAll(adjustedDependencies)
}
)
}
}
else -> listOf(dependency)
}
fun adjustDependency(dependency: ExternalDependency, parentScope: String? = null): List<ExternalDependency> {
return adjustmentMap.getOrPut(dependency) {
if (dependency !is ExternalProjectDependency)
return@getOrPut adjustLibraryDependency(dependency, parentScope)
if (dependency.configurationName != Dependency.DEFAULT_CONFIGURATION &&
!EXTRA_DEFAULT_CONFIGURATION_NAMES.contains(dependency.configurationName))
return@getOrPut listOf(dependency)
val artifacts = dependenciesByProjectPath[dependency.projectPath] ?: return@getOrPut listOf(dependency)
val artifactConfiguration = artifacts.mapTo(LinkedHashSet()) {
it.first.configuration
}.singleOrNull() ?: return@getOrPut listOf(dependency)
val taskGetterName = when (scope) {
"COMPILE" -> "getApiElementsConfigurationName"
"RUNTIME" -> "getRuntimeElementsConfigurationName"
else -> return@getOrPut listOf(dependency)
}
val dependencyProject =
if (project.rootProject.path == dependency.projectPath)
project.rootProject
else
project.rootProject.getChildProjectByPath(dependency.projectPath)
val targets = dependencyProject?.getTargets() ?: return@getOrPut listOf(dependency)
val gradleTarget = targets.firstOrNull {
val getter = it.javaClass.getMethodOrNull(taskGetterName) ?: return@firstOrNull false
getter(it) == artifactConfiguration
} ?: return@getOrPut listOf(dependency)
val classifier = gradleTarget.javaClass.getMethodOrNull("getDisambiguationClassifier")?.invoke(gradleTarget) as? String
?: return@getOrPut listOf(dependency)
val platformDependency = if (classifier != KotlinTarget.METADATA_TARGET_NAME) {
wrapDependency(dependency, compilationFullName(KotlinCompilation.MAIN_COMPILATION_NAME, classifier))
} else null
val commonDependencies = if (dependencyProject.path in projectDependencyTransformation) {
val visibleSourceSets = projectDependencyTransformation.getValue(dependencyProject.path).allVisibleSourceSets
visibleSourceSets.map { sourceSetName -> wrapDependency(dependency, sourceSetName) }
} else {
listOf(wrapDependency(dependency, KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME))
}
return if (platformDependency != null) listOf(platformDependency) + commonDependencies else commonDependencies
}
}
}
companion object {
private val logger = Logging.getLogger(KotlinMPPGradleModelBuilder::class.java)
fun Project.getTargets(): Collection<Named>? {
val kotlinExt = project.extensions.findByName("kotlin") ?: return null
val getTargets = kotlinExt.javaClass.getMethodOrNull("getTargets") ?: return null
@Suppress("UNCHECKED_CAST")
return (getTargets.invoke(kotlinExt) as? NamedDomainObjectContainer<Named>)?.asMap?.values ?: emptyList()
}
fun getCompilations(target: Named): Collection<Named>? {
val getCompilationsMethod = target.javaClass.getMethodOrNull("getCompilations") ?: return null
@Suppress("UNCHECKED_CAST")
return (getCompilationsMethod.invoke(target) as? NamedDomainObjectContainer<Named>)?.asMap?.values ?: emptyList()
}
fun getCompileKotlinTaskName(project: Project, compilation: Named): Task? {
val compilationClass = compilation.javaClass
val getCompileKotlinTaskName = compilationClass.getMethodOrNull("getCompileKotlinTaskName") ?: return null
@Suppress("UNCHECKED_CAST")
val compileKotlinTaskName = (getCompileKotlinTaskName(compilation) as? String) ?: return null
return project.tasks.findByName(compileKotlinTaskName) ?: return null
}
}
}
private fun Project.getChildProjectByPath(path: String): Project? {
var project = this
for (name in path.split(":").asSequence().drop(1)) {
project = project.childProjects[name] ?: return null
}
return project
}