Publish multiplatform resources as variants for wasm, js and native targets

^KT-65540
This commit is contained in:
Timofey Solonin
2024-02-27 12:36:37 +01:00
committed by Space Team
parent 5bbec23123
commit b145796e14
22 changed files with 1093 additions and 12 deletions
@@ -765,6 +765,7 @@ public abstract class org/jetbrains/kotlin/gradle/plugin/mpp/external/DecoratedE
public fun getPreset ()Lorg/jetbrains/kotlin/gradle/plugin/KotlinTargetPreset;
public fun getProject ()Lorg/gradle/api/Project;
public fun getPublishable ()Z
public fun getResourcesElementsConfigurationName ()Ljava/lang/String;
public final fun getRuntimeElementsConfiguration ()Lorg/gradle/api/artifacts/Configuration;
public fun getRuntimeElementsConfigurationName ()Ljava/lang/String;
public final fun getRuntimeElementsPublishedConfiguration ()Lorg/gradle/api/artifacts/Configuration;
@@ -31,9 +31,11 @@ import org.jetbrains.kotlin.gradle.internal.KOTLIN_BUILD_TOOLS_API_IMPL
import org.jetbrains.kotlin.gradle.internal.KOTLIN_COMPILER_EMBEDDABLE
import org.jetbrains.kotlin.gradle.internal.KOTLIN_MODULE_GROUP
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.internal.*
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.swiftexport.initSwiftExportClasspathConfigurations
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolutionStrategy
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSetFactory
import org.jetbrains.kotlin.gradle.plugin.statistics.BuildFusService
import org.jetbrains.kotlin.gradle.report.BuildMetricsService
@@ -210,7 +212,11 @@ abstract class DefaultKotlinBasePlugin : KotlinBasePlugin {
isKotlinGranularMetadata: Boolean = project.isKotlinGranularMetadataEnabled,
) = with(project.dependencies.attributesSchema) {
KotlinPlatformType.setupAttributesMatchingStrategy(this)
KotlinUsages.setupAttributesMatchingStrategy(this, isKotlinGranularMetadata)
KotlinUsages.setupAttributesMatchingStrategy(
this,
isKotlinGranularMetadata,
project.kotlinPropertiesProvider.mppResourcesResolutionStrategy == KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration
)
ProjectLocalConfigurations.setupAttributesMatchingStrategy(this)
project.whenJsOrMppEnabled {
@@ -55,7 +55,9 @@ internal fun Project.categoryByName(categoryName: String): Category =
internal inline fun <reified T : Named> Project.attributeValueByName(attributeValueName: String): T =
objects.named(T::class.java, attributeValueName)
fun Configuration.usesPlatformOf(target: KotlinTarget): Configuration {
fun Configuration.usesPlatformOf(target: KotlinTarget) = setUsesPlatformOf(target)
internal fun <T : HasAttributes> T.setUsesPlatformOf(target: KotlinTarget): T {
attributes.setAttribute(KotlinPlatformType.attribute, target.platformType)
when (target.platformType) {
@@ -94,7 +96,7 @@ fun Configuration.usesPlatformOf(target: KotlinTarget): Configuration {
return this
}
private fun Configuration.setJavaTargetEnvironmentAttributeIfSupported(project: Project, value: String) {
private fun HasAttributes.setJavaTargetEnvironmentAttributeIfSupported(project: Project, value: String) {
if (GradleVersion.current() >= GradleVersion.version("7.0")) {
attributes.setAttribute(
TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE,
@@ -39,10 +39,12 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLI
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_OPTIMISTIC_NUMBER_COMMONIZATION
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_PLATFORM_INTEGER_COMMONIZATION
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_RESOURCES_PUBLICATION
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_FILTER_RESOURCES_BY_EXTENSION
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_HIERARCHICAL_STRUCTURE_BY_DEFAULT
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_HIERARCHICAL_STRUCTURE_SUPPORT
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_IMPORT_ENABLE_KGP_DEPENDENCY_RESOLUTION
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_IMPORT_ENABLE_SLOW_SOURCES_JAR_RESOLVER
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_RESOURCES_RESOLUTION_STRATEGY
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_NATIVE_IGNORE_DISABLED_TARGETS
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_NATIVE_SUPPRESS_EXPERIMENTAL_ARTIFACTS_DSL_WARNING
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_NATIVE_TOOLCHAIN_ENABLED
@@ -53,7 +55,9 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLI
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_STDLIB_JDK_VARIANTS_VERSION_ALIGNMENT
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.MPP_13X_FLAGS_SET_BY_PLUGIN
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics
import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnostic
import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnosticOncePerBuild
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolutionStrategy
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinIrJsGeneratedTSValidationStrategy
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrOutputGranularity
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
@@ -203,6 +207,20 @@ internal class PropertiesProvider private constructor(private val project: Proje
val mppResourcesPublication: Boolean
get() = this.booleanProperty(KOTLIN_MPP_ENABLE_RESOURCES_PUBLICATION) ?: true
val mppResourcesResolutionStrategy: KotlinTargetResourcesResolutionStrategy
get() = this.get(KOTLIN_MPP_RESOURCES_RESOLUTION_STRATEGY)?.let {
val strategy = KotlinTargetResourcesResolutionStrategy.fromProperty(it)
if (strategy == null) { project.reportDiagnostic(KotlinToolingDiagnostics.UnknownValueProvidedForResourcesStrategy(it)) }
return@let strategy
} ?: KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration
val mppFilterResourcesByExtension: Provider<Boolean>
get() = project.providers
.provider<Boolean> {
booleanProperty(KOTLIN_MPP_FILTER_RESOURCES_BY_EXTENSION)
}
.orElse(false)
val wasmStabilityNoWarn: Boolean
get() = booleanProperty("kotlin.wasm.stability.nowarn") ?: false
@@ -635,6 +653,8 @@ internal class PropertiesProvider private constructor(private val project: Proje
val KOTLIN_MPP_ENABLE_INTRANSITIVE_METADATA_CONFIGURATION = property("kotlin.mpp.enableIntransitiveMetadataConfiguration")
val KOTLIN_MPP_APPLY_DEFAULT_HIERARCHY_TEMPLATE = property("kotlin.mpp.applyDefaultHierarchyTemplate")
val KOTLIN_MPP_ENABLE_RESOURCES_PUBLICATION = property("kotlin.mpp.enableResourcesPublication")
val KOTLIN_MPP_RESOURCES_RESOLUTION_STRATEGY = property("kotlin.mpp.resourcesResolutionStrategy")
val KOTLIN_MPP_FILTER_RESOURCES_BY_EXTENSION = property("kotlin.mpp.filterResourcesByExtension")
val KOTLIN_NATIVE_DEPENDENCY_PROPAGATION = property("kotlin.native.enableDependencyPropagation")
val KOTLIN_NATIVE_CACHE_ORCHESTRATION = property("kotlin.native.cacheOrchestration")
val KOTLIN_NATIVE_PARALLEL_THREADS = property("kotlin.native.parallelThreads")
@@ -796,6 +796,44 @@ object KotlinToolingDiagnostics {
)
}
object ResourceMayNotBeResolvedForTarget : ToolingDiagnosticFactory(ERROR) {
operator fun invoke(targetName: String) = build(
"""
Resources resolution for target $targetName is not supported.
$resourcesBugReportRequest
""".trimIndent()
)
}
object UnknownValueProvidedForResourcesStrategy : ToolingDiagnosticFactory(ERROR) {
operator fun invoke(value: String) = build(
"""
Unknown value $value provided for ${PropertiesProvider.PropertyNames.KOTLIN_MPP_RESOURCES_RESOLUTION_STRATEGY}
""".trimIndent()
)
}
object MissingConfigurationForWasmTarget : ToolingDiagnosticFactory(ERROR) {
operator fun invoke(targetName: String) = build(
"""
Resources will not be resolved for $targetName as it is missing runtimeDependencyConfiguration.
$resourcesBugReportRequest
""".trimIndent()
)
}
object MissingResourcesConfigurationForTarget : ToolingDiagnosticFactory(ERROR) {
operator fun invoke(targetName: String) = build(
"""
Resources will not be resolved for $targetName as it is missing resourcesConfiguration.
$resourcesBugReportRequest
""".trimIndent()
)
}
object DependencyDoesNotPhysicallyExist : ToolingDiagnosticFactory(WARNING) {
operator fun invoke(dependency: File) = build(
"""
@@ -54,6 +54,10 @@ abstract class AbstractKotlinTarget(
override val sourcesElementsConfigurationName: String
get() = disambiguateName("sourcesElements")
@InternalKotlinGradlePluginApi
override val resourcesElementsConfigurationName: String
get() = disambiguateName("resourcesElements")
override val artifactsTaskName: String
get() = disambiguateName("jar")
@@ -39,6 +39,8 @@ internal interface InternalKotlinTarget : KotlinTarget, HasMutableExtras {
@Deprecated(TOOLCHAIN_DSL_WRONG_USAGE_ERROR, level = DeprecationLevel.ERROR)
fun jvmToolchain(jdkVersion: Int): Unit = error(TOOLCHAIN_DSL_WRONG_USAGE_ERROR)
val resourcesElementsConfigurationName: String
}
private const val TOOLCHAIN_DSL_WRONG_USAGE_ERROR =
@@ -37,6 +37,15 @@ object KotlinUsages {
const val KOTLIN_COMMONIZED_CINTEROP = "kotlin-commonized-cinterop"
const val KOTLIN_SOURCES = "kotlin-sources"
/**
* Multiplatform resources usage:
* Resources variants for native, wasmJs and wasmWasi targets publish with these usages. To resolve a resources configuration with
* transitive dependencies that might not have a resource variant, we must use a compatibility rule to take dependencies from
* the klib variant. For native that is a "kotlin-api" variant and for wasmJs and wasmWasi that is a "kotlin-runtime" variant.
*/
const val KOTLIN_RESOURCES = "kotlin-multiplatformresources"
const val KOTLIN_RESOURCES_JS = "kotlin-multiplatformresourcesjs"
// Following two constants were removed in Gradle 8.0 from 'Usages' class
private const val JAVA_RUNTIME_CLASSES = "java-runtime-classes"
private const val JAVA_RUNTIME_RESOURCES = "java-runtime-resources"
@@ -125,6 +134,27 @@ object KotlinUsages {
}
}
private class KotlinResourcesCompatibility : AttributeCompatibilityRule<Usage> {
override fun execute(details: CompatibilityCheckDetails<Usage>) = with(details) {
/**
* When resolving resources using KotlinTarget.resourcesConfiguration, if a dependency doesn't have resources variant, we must
* take dependencies from the klib variant because they might contain transitive resources variants.
* */
val consumerValueName = consumerValue?.name
val producerValueName = producerValue?.name
if (consumerValueName == null || producerValueName == null) return
if (
mapOf(
KOTLIN_RESOURCES to KOTLIN_API,
KOTLIN_RESOURCES_JS to KOTLIN_RUNTIME,
)[consumerValueName] == producerValueName
) {
compatible()
}
}
}
private class KotlinCinteropDisambiguation : AttributeDisambiguationRule<Usage> {
override fun execute(details: MultipleCandidatesDetails<Usage?>) = details.run {
if (consumerValue?.name == KOTLIN_CINTEROP) {
@@ -186,7 +216,11 @@ object KotlinUsages {
closestMatch(candidateValues.single { it?.name == name }!!)
}
internal fun setupAttributesMatchingStrategy(attributesSchema: AttributesSchema, isKotlinGranularMetadata: Boolean) {
internal fun setupAttributesMatchingStrategy(
attributesSchema: AttributesSchema,
isKotlinGranularMetadata: Boolean,
isKotlinResourcesCompatibilityRuleEnabled: Boolean
) {
attributesSchema.attribute(USAGE_ATTRIBUTE) { strategy ->
strategy.compatibilityRules.add(KotlinJavaRuntimeJarsCompatibility::class.java)
strategy.disambiguationRules.add(KotlinUsagesDisambiguation::class.java)
@@ -198,6 +232,11 @@ object KotlinUsages {
strategy.compatibilityRules.add(KotlinMetadataCompatibility::class.java)
strategy.disambiguationRules.add(KotlinMetadataDisambiguation::class.java)
}
// Only enable resources compatibility rule when resources configuration is used, so that for variant reselection klibs aren't selected
if (isKotlinResourcesCompatibilityRuleEnabled) {
strategy.compatibilityRules.add(KotlinResourcesCompatibility::class.java)
}
}
}
}
@@ -10,6 +10,8 @@ import org.gradle.api.artifacts.Configuration
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.DocsType
import org.gradle.api.attributes.HasAttributes
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.provider.Provider
import org.gradle.api.publish.PublicationContainer
@@ -24,6 +26,7 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope
import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
import org.jetbrains.kotlin.gradle.targets.metadata.isKotlinGranularMetadataEnabled
import org.jetbrains.kotlin.gradle.tooling.buildKotlinToolingMetadataTask
import org.jetbrains.kotlin.gradle.utils.*
@@ -177,3 +180,26 @@ internal fun Configuration.configureSourcesPublicationAttributes(target: KotlinT
attributes.setAttribute(Bundling.BUNDLING_ATTRIBUTE, project.attributeValueByName(Bundling.EXTERNAL))
usesPlatformOf(target)
}
internal fun HasAttributes.configureResourcesPublicationAttributes(target: KotlinTarget) {
val project = target.project
val usage = if (target is KotlinJsIrTarget) {
KotlinUsages.KOTLIN_RESOURCES_JS
} else {
KotlinUsages.KOTLIN_RESOURCES
}
attributes.setAttribute(
Usage.USAGE_ATTRIBUTE,
project.usageByName(usage)
)
attributes.setAttribute(
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
project.objects.named(usage)
)
attributes.setAttribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category.LIBRARY))
attributes.setAttribute(Bundling.BUNDLING_ATTRIBUTE, project.attributeValueByName(Bundling.EXTERNAL))
setUsesPlatformOf(target)
}
@@ -23,6 +23,7 @@ interface KotlinCompilationConfigurationsContainer {
val runtimeDependencyConfiguration: Configuration?
val hostSpecificMetadataConfiguration: Configuration?
val pluginConfiguration: Configuration
val resourcesConfiguration: Configuration?
}
internal class DefaultKotlinCompilationConfigurationsContainer(
@@ -35,7 +36,8 @@ internal class DefaultKotlinCompilationConfigurationsContainer(
override val compileDependencyConfiguration: Configuration,
override val runtimeDependencyConfiguration: Configuration?,
override val hostSpecificMetadataConfiguration: Configuration?,
override val pluginConfiguration: Configuration
override val pluginConfiguration: Configuration,
override val resourcesConfiguration: Configuration?,
) : KotlinCompilationConfigurationsContainer
internal fun HasKotlinDependencies(
@@ -5,15 +5,19 @@
package org.jetbrains.kotlin.gradle.plugin.mpp.compilationImpl.factory
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.compilationImpl.DefaultKotlinCompilationConfigurationsContainer
import org.jetbrains.kotlin.gradle.plugin.mpp.compilationImpl.KotlinCompilationConfigurationsContainer
import org.jetbrains.kotlin.gradle.plugin.mpp.configureResourcesPublicationAttributes
import org.jetbrains.kotlin.gradle.plugin.mpp.javaSourceSets
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl.Companion.RESOURCES_PATH
import org.jetbrains.kotlin.gradle.plugin.sources.METADATA_CONFIGURATION_NAME_SUFFIX
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
import org.jetbrains.kotlin.gradle.utils.*
internal sealed class DefaultKotlinCompilationDependencyConfigurationsFactory :
@@ -43,6 +47,7 @@ internal object NativeKotlinCompilationDependencyConfigurationsFactory :
naming = naming,
withRuntime = false,
withHostSpecificMetadata = true,
withResources = true,
compileClasspathConfigurationName = naming.name("compileKlibraries")
)
}
@@ -82,7 +87,7 @@ private const val compileClasspath = "compileClasspath"
private const val runtimeClasspath = "runtimeClasspath"
private fun KotlinCompilationDependencyConfigurationsContainer(
target: KotlinTarget, compilationName: String, withRuntime: Boolean, withHostSpecificMetadata: Boolean = false,
target: KotlinTarget, compilationName: String, withRuntime: Boolean, withHostSpecificMetadata: Boolean = false, withResources: Boolean = false,
naming: ConfigurationNaming = ConfigurationNaming.Default(target, compilationName),
apiConfigurationName: String = naming.name(compilation, API),
implementationConfigurationName: String = naming.name(compilation, IMPLEMENTATION),
@@ -95,7 +100,8 @@ private fun KotlinCompilationDependencyConfigurationsContainer(
PLUGIN_CLASSPATH_CONFIGURATION_NAME,
target.disambiguationClassifier,
compilationName
)
),
resourcesPathConfigurationName: String = naming.name(RESOURCES_PATH),
): KotlinCompilationConfigurationsContainer {
val compilationCoordinates = "${target.disambiguationClassifier}/$compilationName"
@@ -198,6 +204,23 @@ private fun KotlinCompilationDependencyConfigurationsContainer(
description = "Kotlin compiler plugins for $compilation"
}
val resourcesConfiguration = if (withResources || target is KotlinJsIrTarget) {
target.project.configurations.maybeCreateResolvable(resourcesPathConfigurationName).apply {
// Inherit from compile dependency configuration, i.e. from the configuration that consumes apiElements
if (target is KotlinJsIrTarget) {
extendsFrom(runtimeDependencyConfiguration)
} else {
extendsFrom(compileDependencyConfiguration)
}
isVisible = false
configureResourcesPublicationAttributes(target)
description = "Kotlin resources for $compilation"
}
} else null
return DefaultKotlinCompilationConfigurationsContainer(
deprecatedCompileConfiguration = deprecatedCompileConfiguration,
deprecatedRuntimeConfiguration = deprecatedRuntimeConfiguration,
@@ -208,6 +231,7 @@ private fun KotlinCompilationDependencyConfigurationsContainer(
compileDependencyConfiguration = compileDependencyConfiguration,
runtimeDependencyConfiguration = runtimeDependencyConfiguration,
hostSpecificMetadataConfiguration = hostSpecificMetadataConfiguration,
pluginConfiguration = pluginConfiguration
pluginConfiguration = pluginConfiguration,
resourcesConfiguration = resourcesConfiguration,
)
}
@@ -36,6 +36,7 @@ internal class ExternalKotlinTargetImpl internal constructor(
val apiElementsPublishedConfiguration: Configuration,
val runtimeElementsPublishedConfiguration: Configuration,
val sourcesElementsPublishedConfiguration: Configuration,
val resourcesElementsPublishedConfiguration: Configuration,
val kotlinTargetComponent: ExternalKotlinTargetComponent,
private val artifactsTaskLocator: ArtifactsTaskLocator,
) : InternalKotlinTarget,
@@ -86,6 +87,9 @@ internal class ExternalKotlinTargetImpl internal constructor(
override val sourcesElementsConfigurationName: String
get() = sourcesElementsConfiguration.name
override val resourcesElementsConfigurationName: String
get() = resourcesElementsPublishedConfiguration.name
@InternalKotlinGradlePluginApi
override val kotlinComponents: Set<KotlinTargetComponent> = setOf(kotlinTargetComponent)
@@ -59,6 +59,10 @@ fun <T : DecoratedExternalKotlinTarget> KotlinMultiplatformExtension.createExter
.maybeCreateDependencyScope(lowerCamelCaseName(descriptor.targetName, "sourcesElements-published"))
.notVisible()
val resourcesElementsPublishedConfiguration = project.configurations
.maybeCreateDependencyScope(lowerCamelCaseName(descriptor.targetName, "resourcesElements-published"))
.notVisible()
val kotlinTargetComponent = ExternalKotlinTargetComponent(
ExternalKotlinTargetComponent.TargetProvider.byTargetName(this, descriptor.targetName)
)
@@ -88,6 +92,7 @@ fun <T : DecoratedExternalKotlinTarget> KotlinMultiplatformExtension.createExter
apiElementsPublishedConfiguration = apiElementsPublishedConfiguration,
runtimeElementsPublishedConfiguration = runtimeElementsPublishedConfiguration,
sourcesElementsPublishedConfiguration = sourcesElementsPublishedConfiguration,
resourcesElementsPublishedConfiguration = resourcesElementsPublishedConfiguration,
kotlinTargetComponent = kotlinTargetComponent,
artifactsTaskLocator = artifactsTaskLocator
)
@@ -36,6 +36,8 @@ internal abstract class KotlinTargetResourcesPublicationImpl @Inject constructor
)
private val targetsThatSupportPublication = listOf(
KotlinJsIrTarget::class,
KotlinNativeTarget::class,
KotlinJvmTarget::class,
KotlinAndroidTarget::class,
)
@@ -64,6 +66,9 @@ internal abstract class KotlinTargetResourcesPublicationImpl @Inject constructor
override fun canPublishResources(target: KotlinTarget): Boolean {
if (targetsThatSupportPublication.none { it.isInstance(target) }) return false
if (target is KotlinJsIrTarget) {
return target.platformType == KotlinPlatformType.wasm
}
if (target is KotlinAndroidTarget) {
return AndroidGradlePluginVersion.current >= KotlinAndroidTargetResourcesPublication.MIN_AGP_VERSION
}
@@ -114,6 +119,10 @@ internal abstract class KotlinTargetResourcesPublicationImpl @Inject constructor
internal companion object {
const val MULTIPLATFORM_RESOURCES_DIRECTORY = "kotlin-multiplatform-resources"
const val RESOURCES_CLASSIFIER = "kotlin_resources"
const val RESOURCES_ZIP_EXTENSION = "${RESOURCES_CLASSIFIER}.zip"
const val RESOURCES_PATH = "ResourcesPath"
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2010-2024 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.plugin.mpp.resources.publication
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.tasks.bundling.Zip
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.launch
import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultKotlinUsageContext
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl.Companion.RESOURCES_CLASSIFIER
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublicationImpl.Companion.RESOURCES_ZIP_EXTENSION
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.assembleHierarchicalResources
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resourcesPublicationExtension
import org.jetbrains.kotlin.gradle.tasks.registerTask
// Use KotlinMultiplatformExtension to make sure this usage context is only creatable in MPP
@Suppress("UnusedReceiverParameter")
internal fun KotlinMultiplatformExtension.setUpResourcesVariant(
target: AbstractKotlinTarget,
compilation: KotlinCompilation<*>,
): DefaultKotlinUsageContext = with(target) {
var targetRegistersResourcesForPublication = false
val resourcesVariant = DefaultKotlinUsageContext(
compilation = compilation,
dependencyConfigurationName = resourcesElementsConfigurationName,
includeIntoProjectStructureMetadata = false,
publishOnlyIf = {
targetRegistersResourcesForPublication
}
)
project.multiplatformExtension.resourcesPublicationExtension?.subscribeOnPublishResources(this) { resources ->
targetRegistersResourcesForPublication = true
val copyTask = compilation.assembleHierarchicalResources(
targetName,
resources,
)
val zippedResourcesDirectory = project.layout.buildDirectory.dir(
"${KotlinTargetResourcesPublicationImpl.MULTIPLATFORM_RESOURCES_DIRECTORY}/zip-for-publication/${targetName}"
)
val zipResourcesForPublication = project.registerTask<Zip>(
"${targetName}ZipMultiplatformResourcesForPublication"
) { copy ->
copy.destinationDirectory.set(zippedResourcesDirectory)
copy.duplicatesStrategy = DuplicatesStrategy.FAIL
copy.archiveExtension.set(RESOURCES_ZIP_EXTENSION)
}
zipResourcesForPublication.configure {
it.from(copyTask)
}
project.artifacts.add(
compilation.target.internal.resourcesElementsConfigurationName,
zipResourcesForPublication
) { artifact ->
artifact.extension = RESOURCES_ZIP_EXTENSION
artifact.classifier = RESOURCES_CLASSIFIER
artifact.type = "zip"
}
}
return resourcesVariant
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2024 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.plugin.mpp.resources.resolve
import org.gradle.api.file.FileCollection
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics
import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnostic
import org.jetbrains.kotlin.gradle.plugin.mpp.configureResourcesPublicationAttributes
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
import org.jetbrains.kotlin.gradle.utils.copyAttributesTo
enum class KotlinTargetResourcesResolutionStrategy {
VariantReselection,
ResourcesConfiguration;
fun resourceArchives(
compilation: KotlinCompilation<*>,
): FileCollection {
return when (this) {
VariantReselection -> {
val dependenciesConfiguration = if (compilation.target is KotlinJsIrTarget) {
compilation.internal.configurations.runtimeDependencyConfiguration
?: return compilation.project.files().also {
compilation.project.reportDiagnostic(
KotlinToolingDiagnostics.MissingConfigurationForWasmTarget(compilation.target.name)
)
}
} else {
compilation.internal.configurations.compileDependencyConfiguration
}
dependenciesConfiguration.incoming.artifactView {
it.withVariantReselection()
it.attributes { viewAttributes ->
viewAttributes.configureResourcesPublicationAttributes(compilation.target)
}
it.isLenient = true
}.files
}
ResourcesConfiguration -> compilation.internal.configurations.resourcesConfiguration
?: return compilation.project.files().also {
compilation.project.reportDiagnostic(
KotlinToolingDiagnostics.MissingResourcesConfigurationForTarget(compilation.target.name)
)
}
}
}
val propertyName: String
get() = when (this) {
VariantReselection -> "variantReselection"
ResourcesConfiguration -> "resourcesConfiguration"
}
companion object {
fun fromProperty(name: String): KotlinTargetResourcesResolutionStrategy? = when (name) {
VariantReselection.propertyName -> VariantReselection
ResourcesConfiguration.propertyName -> ResourcesConfiguration
else -> null
}
}
}
@@ -7,11 +7,13 @@ package org.jetbrains.kotlin.gradle.targets
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.Usage
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.configureSourcesPublicationAttributes
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.plugin.mpp.isSourcesPublishableFuture
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resourcesPublicationExtension
import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
import org.jetbrains.kotlin.gradle.utils.maybeCreateConsumable
import org.jetbrains.kotlin.gradle.utils.maybeCreateDependencyScope
@@ -33,12 +35,13 @@ internal val CreateTargetConfigurationsSideEffect = KotlinTargetSideEffect { tar
else -> null
}
val apiElementScope = configurations.maybeCreateDependencyScope(mainCompilation.apiConfigurationName)
configurations.maybeCreateConsumable(target.apiElementsConfigurationName).apply {
description = "API elements for main."
isVisible = false
attributes.setAttribute(Usage.USAGE_ATTRIBUTE, KotlinUsages.producerApiUsage(target))
attributes.setAttribute(Category.CATEGORY_ATTRIBUTE, project.categoryByName(Category.LIBRARY))
extendsFrom(configurations.maybeCreateDependencyScope(mainCompilation.apiConfigurationName))
extendsFrom(apiElementScope)
@Suppress("TYPEALIAS_EXPANSION_DEPRECATION")
if (mainCompilation is DeprecatedKotlinCompilationToRunnableFiles) {
val runtimeConfiguration = mainCompilation.internal.configurations.deprecatedRuntimeConfiguration
@@ -69,6 +72,20 @@ internal val CreateTargetConfigurationsSideEffect = KotlinTargetSideEffect { tar
project.launch { isCanBeConsumed = target.internal.isSourcesPublishableFuture.await() }
}
project.multiplatformExtensionOrNull?.resourcesPublicationExtension?.subscribeOnPublishResources(target) {
configurations.maybeCreateConsumable(target.internal.resourcesElementsConfigurationName).apply {
description = "Resource files of main compilation of ${target.name}."
isVisible = false
// Publish with dependencies of apiElements configuration, so that transitives are resolved correctly. Don't inherit from
// apiElementsConfiguration directly, because it contains klibs in project dependencies.
extendsFrom(apiElementScope)
// Resources from implementation scope should also be resolved transitively, hence extend from it.
extendsFrom(implementationConfiguration)
configureResourcesPublicationAttributes(target)
}
}
if (target !is KotlinMetadataTarget) {
val testCompilation = target.compilations.getByName(KotlinCompilation.TEST_COMPILATION_NAME)
val compileTestsConfiguration = testCompilation.internal.configurations.deprecatedCompileConfiguration
@@ -104,4 +121,4 @@ internal val CreateTargetConfigurationsSideEffect = KotlinTargetSideEffect { tar
attributes.setAttribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
}
}
}
}
@@ -20,7 +20,9 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetComponent
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication.setUpResourcesVariant
import org.jetbrains.kotlin.gradle.targets.js.JsAggregatingExecutionSource
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsReportAggregatingTestRun
import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTestRunFactory
@@ -90,6 +92,17 @@ constructor(
)
)
if (project.kotlinPropertiesProvider.mppResourcesPublication) {
project.multiplatformExtensionOrNull?.let {
usageContexts.add(
it.setUpResourcesVariant(
this,
mainCompilation
)
)
}
}
val result = createKotlinVariant(componentName, mainCompilation, usageContexts)
setOf(result)
@@ -12,6 +12,8 @@ import org.gradle.api.attributes.Attribute
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.publication.setUpResourcesVariant
import org.jetbrains.kotlin.gradle.plugin.sources.awaitPlatformCompilations
import org.jetbrains.kotlin.gradle.plugin.sources.internal
import org.jetbrains.kotlin.gradle.targets.metadata.*
@@ -81,6 +83,15 @@ abstract class KotlinNativeTarget @Inject constructor(
)
)
if (project.kotlinPropertiesProvider.mppResourcesPublication) {
mutableUsageContexts.add(
project.multiplatformExtension.setUpResourcesVariant(
this,
mainCompilation,
)
)
}
val result = createKotlinVariant(targetName, mainCompilation, mutableUsageContexts)
setOf(result)
@@ -0,0 +1,151 @@
/*
* Copyright 2010-2024 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.
*/
@file:OptIn(ExperimentalWasmDsl::class)
package org.jetbrains.kotlin.gradle.unitTests
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resourcesPublicationExtension
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
import org.jetbrains.kotlin.gradle.util.enableMppResourcesPublication
import org.jetbrains.kotlin.gradle.util.kotlin
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNull
class KotlinTargetVariantResourcesPublicationTests {
@Test
fun `test variant publication - doesn't happen - when resources publication is disabled`() {
val enableResourcePublication = false
testPublishedVariants(
enableResourcePublication = enableResourcePublication,
afterEvaluation = { target ->
publishFakeResources(target)
},
shouldPublishVariant = enableResourcePublication,
targetsToTest = listOf(
{ linuxX64() },
{ wasmWasi() },
{ wasmJs() },
)
)
}
@Test
fun `test variant publication - doesn't happen - when publishing api is not called`() {
testPublishedVariants(
enableResourcePublication = true,
afterEvaluation = { /* don't call publishing api */ },
shouldPublishVariant = false,
targetsToTest = listOf(
{ linuxX64() },
{ wasmWasi() },
{ wasmJs() },
)
)
}
@Test
fun `test variant publication - creates a variant - when publishing api is called`() {
testPublishedVariants(
enableResourcePublication = true,
afterEvaluation = { target ->
publishFakeResources(target)
},
shouldPublishVariant = true,
targetsToTest = listOf(
{ linuxX64() },
{ wasmWasi() },
{ wasmJs() },
)
)
}
@Test
fun `test resources configuration - outputs a single zip file - in outgoing artifacts`() {
val project = buildProjectWithMPP {
kotlin {
linuxX64()
}
enableMppResourcesPublication(true)
}.evaluate()
assertNull(
project.configurations.findByName(
project.multiplatformExtension.linuxX64().resourcesElementsConfigurationName
)
)
project.publishFakeResources(project.multiplatformExtension.linuxX64())
assertEquals(
listOf(
project.layout.buildDirectory.file(
"kotlin-multiplatform-resources/zip-for-publication/linuxX64/test.kotlin_resources.zip"
).get().asFile
),
project.configurations.findByName(
project.multiplatformExtension.linuxX64().resourcesElementsConfigurationName
)?.outgoing?.artifacts?.map { it.file },
)
}
private fun testPublishedVariants(
enableResourcePublication: Boolean,
afterEvaluation: Project.(KotlinTarget) -> Unit,
shouldPublishVariant: Boolean,
targetsToTest: List<KotlinMultiplatformExtension.() -> KotlinTarget>,
) {
val project = buildProjectWithMPP {
kotlin {
targetsToTest.forEach { createTarget ->
createTarget()
}
}
enableMppResourcesPublication(enableResourcePublication)
}.evaluate()
targetsToTest.forEach { targetToTest ->
val target = project.multiplatformExtension.targetToTest()
// Resources configuration should never be published before publishing API is called
assert(target.internal.resourcesElementsConfigurationName !in configurationNamesUsedInVariantPublications(target))
project.afterEvaluation(target)
assert(
shouldPublishVariant == target.internal.resourcesElementsConfigurationName in configurationNamesUsedInVariantPublications(target)
)
}
}
private fun configurationNamesUsedInVariantPublications(target: KotlinTarget): Set<String> {
return target.internal.kotlinComponents.flatMap { component ->
component.internal.usages.map { it.dependencyConfigurationName }
}.toSet()
}
private fun Project.publishFakeResources(target: KotlinTarget) {
project.multiplatformExtension.resourcesPublicationExtension?.publishResourcesAsKotlinComponent(
target,
resourcePathForSourceSet = {
KotlinTargetResourcesPublication.ResourceRoot(
project.provider { File(it.name) },
emptyList(),
emptyList(),
)
},
relativeResourcePlacement = project.provider { File("test") },
)
}
}
@@ -0,0 +1,564 @@
/*
* Copyright 2010-2024 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.
*/
@file:OptIn(ExperimentalWasmDsl::class)
package org.jetbrains.kotlin.gradle.unitTests
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ResolveException
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.Usage
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.kotlin.dsl.project
import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.dependencyResolutionTests.mavenCentralCacheRedirector
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.KotlinTargetResourcesPublication
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolutionStrategy
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resourcesPublicationExtension
import org.jetbrains.kotlin.gradle.plugin.usageByName
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.gradle.utils.buildOrNull
import org.jetbrains.kotlin.util.assertThrows
import org.junit.Test
import java.io.File
import kotlin.test.assertEquals
class KotlinTargetVariantResourcesResolutionTests {
@Test
fun `test direct dependency - is the same - for all resolution methods and supported scopes`() {
KotlinTargetResourcesResolutionStrategy.values().forEach { resolutionMethod ->
dependencyScopesWithResources().forEach { dependencyScope ->
testDirectDependencyOnResourcesProducer(
producerTarget = { linuxX64() },
consumerTarget = { linuxX64() },
dependencyScope = dependencyScope,
resolutionStrategy = resolutionMethod,
expectedResult = { _, producer ->
hashSetOf(producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"))
},
)
}
}
}
@Test
fun `test direct dependency - between different native targets - with artifact view`() {
testDirectDependencyOnResourcesProducer(
producerTarget = { linuxX64() },
consumerTarget = { linuxArm64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.VariantReselection,
expectedResult = { _, _ -> emptySet() },
)
}
@Test
fun `test direct dependency - between different native targets - with resources configuration`() {
directDependencyOnResourcesProducer(
producerTarget = { linuxX64() },
consumerTarget = { linuxArm64() },
strategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
assert = { consumer, _ ->
assertThrows<ResolveException> {
KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration.resourceArchives(
consumer.multiplatformExtension.linuxArm64().compilations.getByName("main")
).files
}
},
)
}
@Test
fun `test direct dependency - for wasmJs and wasmWasi targets - when using artifact view`() {
listOf<TargetProvider>(
{ wasmJs() },
{ wasmWasi() },
).forEach { target ->
testDirectDependencyOnResourcesProducer(
producerTarget = { target() },
consumerTarget = { target() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.VariantReselection,
expectedResult = { _, producer ->
hashSetOf(
producer.buildFile(
"kotlin-multiplatform-resources/zip-for-publication/${producer.multiplatformExtension.target().name}/producer.kotlin_resources.zip"
),
)
},
)
}
}
@Test
fun `test direct dependency - for wasmJs - when using resources configuration`() {
testDirectDependencyOnResourcesProducer(
producerTarget = { wasmJs() },
consumerTarget = { wasmJs() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
filterResolvedFiles = {
it.filterNot {
it.path.contains("kotlin-stdlib-wasm-js")
}.toSet()
},
expectedResult = { _, producer ->
hashSetOf(
producer.buildFile(
"kotlin-multiplatform-resources/zip-for-publication/wasmJs/producer.kotlin_resources.zip"
),
)
},
)
}
@Test
fun `test direct dependency - for wasmWasi - when using resources configuration`() {
testDirectDependencyOnResourcesProducer(
producerTarget = { wasmWasi() },
consumerTarget = { wasmWasi() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
filterResolvedFiles = {
it.filterNot {
it.path.contains("kotlin-stdlib-wasm-wasi")
}.toSet()
},
expectedResult = { _, producer ->
hashSetOf(
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/wasmWasi/producer.kotlin_resources.zip"),
)
},
)
}
@Test
fun `test transitive dependency - without resources in middle project - with configuration`() {
testTransitiveDependencyOnResourcesProducer(
targetProvider = { linuxX64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
expectedResult = { _, middle, producer ->
setOf(
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"),
middle.buildFile("classes/kotlin/linuxX64/main/klib/middle.klib"),
)
}
)
}
@Test
fun `test transitive dependency - without resources in middle project in wasm - with configuration`() {
dependencyScopesWithResources().forEach { dependencyScope ->
testTransitiveDependencyOnResourcesProducer(
targetProvider = { wasmJs() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
dependencyScope = dependencyScope,
filterResolvedFiles = {
it.filterNot {
it.path.contains("kotlin-stdlib-wasm-js")
}.toSet()
},
expectedResult = { _, middle, producer ->
setOf(
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/wasmJs/producer.kotlin_resources.zip"),
middle.buildFile("libs/middle-wasm-js.klib"),
)
}
)
}
}
@Test
fun `test transitive dependency - without resources in middle project in wasm - with artifact view`() {
dependencyScopesWithResources().forEach { dependencyScope ->
testTransitiveDependencyOnResourcesProducer(
targetProvider = { wasmJs() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.VariantReselection,
dependencyScope = dependencyScope,
filterResolvedFiles = {
it.filterNot {
it.path.contains("kotlin-stdlib-wasm-js")
}.toSet()
},
expectedResult = { _, _, producer ->
setOf(
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/wasmJs/producer.kotlin_resources.zip"),
)
}
)
}
}
@Test
fun `test transitive dependency - without resources in middle project - with artifact view`() {
testTransitiveDependencyOnResourcesProducer(
targetProvider = { linuxX64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.VariantReselection,
expectedResult = { _, _, producer ->
setOf(
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"),
// VariantReselection strategy disables compatibility rule that resolves klibs for resources configuration
// middle.buildFile("classes/kotlin/linuxX64/main/klib/middle.klib"),
)
}
)
}
@Test
fun `test transitive dependency - with resources in middle project - with artifact view`() {
dependencyScopesWithResources().forEach { dependencyScope ->
testTransitiveDependencyOnResourcesProducer(
targetProvider = { linuxX64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.VariantReselection,
dependencyScope = dependencyScope,
middlePublishesResources = true,
expectedResult = { _, middle, producer ->
setOf(
middle.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/middle.kotlin_resources.zip"),
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"),
)
}
)
}
testTransitiveDependencyOnResourcesProducer(
targetProvider = { linuxX64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.VariantReselection,
middlePublishesResources = true,
consumerPublishesResources = true,
expectedResult = { _, middle, producer ->
setOf(
middle.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/middle.kotlin_resources.zip"),
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"),
)
}
)
}
@Test
fun `test transitive dependency - with resources in middle project - with configuration`() {
dependencyScopesWithResources().forEach { dependencyScope ->
testTransitiveDependencyOnResourcesProducer(
targetProvider = { linuxX64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
dependencyScope = dependencyScope,
middlePublishesResources = true,
expectedResult = { _, middle, producer ->
setOf(
middle.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/middle.kotlin_resources.zip"),
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"),
)
}
)
}
testTransitiveDependencyOnResourcesProducer(
targetProvider = { linuxX64() },
resolutionStrategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
middlePublishesResources = true,
consumerPublishesResources = true,
expectedResult = { _, middle, producer ->
setOf(
middle.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/middle.kotlin_resources.zip"),
producer.buildFile("kotlin-multiplatform-resources/zip-for-publication/linuxX64/producer.kotlin_resources.zip"),
)
}
)
}
@Test
fun `test resources - don't leak into non-resources configurations`() {
resourcesConfigurationResolutionSanityCheck()
val targetsToTest = listOf<TargetProvider>(
{ wasmJs() },
{ wasmWasi() },
{ linuxX64() },
{ iosArm64() },
)
targetsToTest.indices.forEach { index ->
// Test when target is matching
testNonResourcesConfigurationDontResolveResourceVariants(
producerTarget = targetsToTest[index],
consumerTarget = targetsToTest[index],
strategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
)
// Test when target is not matching
testNonResourcesConfigurationDontResolveResourceVariants(
producerTarget = targetsToTest[index],
consumerTarget = targetsToTest[(index + 1) % targetsToTest.count()],
strategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
)
}
}
private fun dependencyScopesWithResources(): List<DependencyScopeProvider> {
return listOf(
{ this::implementation },
{ this::api },
// What are these supposed to mean for resources?
// { this::compileOnly }, ?
// { this::runtimeOnly }, ?
)
}
private fun resourcesConfigurationResolutionSanityCheck() {
directDependencyOnResourcesProducer(
producerTarget = { linuxX64() },
consumerTarget = { linuxX64() },
strategy = KotlinTargetResourcesResolutionStrategy.ResourcesConfiguration,
assert = { consumer, _ ->
val resourcesConfiguration = consumer.multiplatformExtension.linuxX64()
.compilations.getByName("main")
.internal.configurations.resourcesConfiguration ?: error("Missing resources configuration")
assertEquals(
mapOf(
// linuxX64ResourcesPath is the root resolvable configuration for the resolution
"test:consumer" to listOf(listOf("linuxX64ResourcesPath")),
// stdlib doesn't have resources, so apiElements is selected as per compatibility rule
"org.jetbrains.kotlin:kotlin-stdlib" to listOf(listOf("nativeApiElements")),
// producer provides the consumable configuration with resources for the consumer
"test:producer" to listOf(listOf("linuxX64ResourcesElements")),
),
resourcesConfiguration.incoming.resolutionResult.allComponents.groupBy(
keySelector = { "${it.moduleVersion?.group}:${it.moduleVersion?.name}" },
valueTransform = { it.variants.map { it.displayName } },
)
)
}
)
}
private fun testNonResourcesConfigurationDontResolveResourceVariants(
producerTarget: TargetProvider,
consumerTarget: TargetProvider,
strategy: KotlinTargetResourcesResolutionStrategy,
) {
directDependencyOnResourcesProducer(
producerTarget = producerTarget,
consumerTarget = consumerTarget,
strategy = strategy,
assert = { consumer, _ ->
val resourcesConfigurations = consumer.multiplatformExtension.targets.flatMap {
it.compilations.mapNotNull { it.internal.configurations.resourcesConfiguration }
}
val nonResourcesConfigurations: Set<Configuration> = consumer.configurations.filter {
it.isCanBeResolved
}.toHashSet() - resourcesConfigurations
assert(nonResourcesConfigurations.isNotEmpty())
nonResourcesConfigurations.forEach { resolvableConfiguration ->
val resolvedComponents = resolvableConfiguration.incoming.resolutionResult.allComponents
resolvedComponents.forEach { resolvedComponent ->
val resolvedVariants = resolvedComponent.variants
resolvedVariants.forEach { variant ->
val variantPath = { "$resolvableConfiguration -> $resolvedComponent -> $variant" }
val typedUsage = variant.attributes.getAttribute(Usage.USAGE_ATTRIBUTE)
val stringUsage = variant.attributes.getAttribute(Attribute.of(Usage.USAGE_ATTRIBUTE.name, String::class.java))
val isKotlinCompilerClasspath = variant.attributes.keySet().isEmpty()
assert(
typedUsage != null || stringUsage != null || isKotlinCompilerClasspath,
variantPath
)
assert(
typedUsage != consumer.project.usageByName(KotlinUsages.KOTLIN_RESOURCES),
variantPath,
)
assert(
stringUsage != KotlinUsages.KOTLIN_RESOURCES,
variantPath,
)
}
}
}
}
)
}
private fun testDirectDependencyOnResourcesProducer(
producerTarget: TargetProvider,
consumerTarget: TargetProvider,
dependencyScope: DependencyScopeProvider = { ::implementation },
resolutionStrategy: KotlinTargetResourcesResolutionStrategy,
filterResolvedFiles: (Set<File>) -> Set<File> = { it },
expectedResult: (consumer: Project, producer: Project) -> Set<File>,
) = directDependencyOnResourcesProducer(
producerTarget = producerTarget,
consumerTarget = consumerTarget,
dependencyScope = dependencyScope,
strategy = resolutionStrategy,
assert = { consumer: Project, producer: Project ->
consumer.setMppResourcesResolutionStrategy(resolutionStrategy)
assertEquals(
expectedResult(consumer, producer),
filterResolvedFiles(
resolutionStrategy.resourceArchives(
consumer.multiplatformExtension.consumerTarget().compilations.getByName("main"),
).files
)
)
}
)
private fun directDependencyOnResourcesProducer(
producerTarget: TargetProvider,
consumerTarget: TargetProvider,
strategy: KotlinTargetResourcesResolutionStrategy,
dependencyScope: DependencyScopeProvider = { ::implementation },
assert: (consumer: Project, producer: Project) -> Unit,
) {
val rootProject = buildProject()
val producer = rootProject.createSubproject("producer") {
kotlin { producerTarget() }
}
val consumer = rootProject.createSubproject(
"consumer",
preApplyCode = { setMppResourcesResolutionStrategy(strategy) }
) {
kotlin {
consumerTarget()
sourceSets.commonMain {
dependencies {
dependencyScope()(project(":${producer.name}"))
}
}
}
}
listOf(rootProject, producer, consumer).forEach { it.evaluate() }
producer.publishFakeResources(producer.multiplatformExtension.producerTarget())
assert(consumer, producer)
}
private fun testTransitiveDependencyOnResourcesProducer(
targetProvider: TargetProvider,
resolutionStrategy: KotlinTargetResourcesResolutionStrategy,
filterResolvedFiles: (Set<File>) -> Set<File> = { it },
dependencyScope: DependencyScopeProvider = { ::implementation },
middlePublishesResources: Boolean = false,
consumerPublishesResources: Boolean = false,
expectedResult: (consumer: Project, middle: Project, producer: Project) -> Set<File>,
) {
val rootProject = buildProject()
val producer = rootProject.createSubproject(
"producer",
preApplyCode = {
enableMppResourcesPublication(true)
setMppResourcesResolutionStrategy(resolutionStrategy)
}
) {
kotlin { targetProvider() }
}
val middle = rootProject.createSubproject(
"middle",
preApplyCode = {
enableMppResourcesPublication(middlePublishesResources)
setMppResourcesResolutionStrategy(resolutionStrategy)
}
) {
kotlin {
targetProvider()
sourceSets.commonMain {
dependencies {
dependencyScope()(dependencies.project(":${producer.name}"))
}
}
}
}
val consumer = rootProject.createSubproject(
"consumer",
preApplyCode = {
enableMppResourcesPublication(consumerPublishesResources)
setMppResourcesResolutionStrategy(resolutionStrategy)
}
) {
kotlin {
targetProvider()
sourceSets.commonMain {
dependencies {
dependencyScope()(dependencies.project(":${middle.name}"))
}
}
setMppResourcesResolutionStrategy(resolutionStrategy)
}
}
listOf(rootProject, producer, middle, consumer).forEach { it.evaluate() }
producer.publishFakeResources(producer.multiplatformExtension.targetProvider())
if (middlePublishesResources) middle.publishFakeResources(middle.multiplatformExtension.targetProvider())
if (consumerPublishesResources) consumer.publishFakeResources(consumer.multiplatformExtension.targetProvider())
assertEquals(
expectedResult(consumer, middle, producer),
filterResolvedFiles(
resolutionStrategy.resourceArchives(
consumer.multiplatformExtension.targetProvider().compilations.getByName("main"),
).files
),
)
}
private fun Project.buildFile(path: String) = layout.buildDirectory.file(path).get().asFile
private fun ProjectInternal.createSubproject(
name: String,
preApplyCode: Project.() -> Unit = {},
code: Project.() -> Unit = {},
) = buildProjectWithMPPAndStdlib(
projectBuilder = {
withParent(this@createSubproject)
withName(name)
},
preApplyCode = preApplyCode,
code = code,
)
private fun buildProjectWithMPPAndStdlib(
projectBuilder: ProjectBuilder.() -> Unit = { },
preApplyCode: Project.() -> Unit = {},
code: Project.() -> Unit = {},
) = buildProjectWithMPP(
projectBuilder = projectBuilder,
preApplyCode = preApplyCode,
) {
enableDependencyVerification(false)
enableDefaultStdlibDependency(true)
repositories.mavenLocal()
repositories.mavenCentralCacheRedirector()
code()
}
private fun Project.publishFakeResources(target: KotlinTarget) {
project.multiplatformExtension.resourcesPublicationExtension?.publishResourcesAsKotlinComponent(
target,
resourcePathForSourceSet = {
KotlinTargetResourcesPublication.ResourceRoot(
project.provider { File(it.name) },
emptyList(),
emptyList(),
)
},
relativeResourcePlacement = project.provider { File("test") },
)
}
}
private typealias TargetProvider = KotlinMultiplatformExtension.() -> (KotlinTarget)
private typealias DependencyScopeProvider = KotlinDependencyHandler.() -> ((Any) -> Dependency?)
@@ -19,6 +19,7 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_INTRANSITIVE_METADATA_CONFIGURATION
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.getExtension
import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.KotlinTargetResourcesResolutionStrategy
import org.jetbrains.kotlin.gradle.targets.native.tasks.artifact.KotlinArtifactsExtensionImpl
import org.jetbrains.kotlin.gradle.targets.native.tasks.artifact.kotlinArtifactsExtension
@@ -117,6 +118,10 @@ fun Project.enableMppResourcesPublication(enabled: Boolean = true) {
propertiesExtension.set(PropertiesProvider.PropertyNames.KOTLIN_MPP_ENABLE_RESOURCES_PUBLICATION, enabled.toString())
}
fun Project.setMppResourcesResolutionStrategy(strategy: KotlinTargetResourcesResolutionStrategy) {
propertiesExtension.set(PropertiesProvider.PropertyNames.KOTLIN_MPP_RESOURCES_RESOLUTION_STRATEGY, strategy.propertyName)
}
fun Project.enableIntransitiveMetadataConfiguration(enabled: Boolean = true) {
propertiesExtension.set(KOTLIN_MPP_ENABLE_INTRANSITIVE_METADATA_CONFIGURATION, enabled.toString())
}