diff --git a/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api b/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api index 4d066064ce3..e68ecf82c00 100644 --- a/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api +++ b/libraries/tools/kotlin-gradle-plugin/api/kotlin-gradle-plugin.api @@ -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; diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt index 771dc5bc0ef..b7944062f51 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt @@ -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 { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt index d98aeca84f9..0e264f8046c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt @@ -55,7 +55,9 @@ internal fun Project.categoryByName(categoryName: String): Category = internal inline fun 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.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, diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt index 9403d40f36f..7e40d50ff38 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt @@ -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 + get() = project.providers + .provider { + 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") diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt index 00cae3debf0..fde8b71ef30 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/KotlinToolingDiagnostics.kt @@ -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( """ diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/AbstractKotlinTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/AbstractKotlinTarget.kt index f8e1dd5b24e..874b9a4e94f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/AbstractKotlinTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/AbstractKotlinTarget.kt @@ -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") diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/InternalKotlinTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/InternalKotlinTarget.kt index 4200935968a..0dacaaf39c8 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/InternalKotlinTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/InternalKotlinTarget.kt @@ -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 = diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt index 459a19729c4..dc2998b7a92 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinUsages.kt @@ -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 { + override fun execute(details: CompatibilityCheckDetails) = 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 { override fun execute(details: MultipleCandidatesDetails) = 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) + } } } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/Publishing.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/Publishing.kt index bf9572d4db5..0156bc404e2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/Publishing.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/Publishing.kt @@ -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) +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/KotlinCompilationConfigurationsContainer.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/KotlinCompilationConfigurationsContainer.kt index 5e8257f5ebf..76bded61073 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/KotlinCompilationConfigurationsContainer.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/KotlinCompilationConfigurationsContainer.kt @@ -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( diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/factory/KotlinCompilationDependencyConfigurationsFactories.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/factory/KotlinCompilationDependencyConfigurationsFactories.kt index 0c4f7b479af..2a26b27f723 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/factory/KotlinCompilationDependencyConfigurationsFactories.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/compilationImpl/factory/KotlinCompilationDependencyConfigurationsFactories.kt @@ -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, ) } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotlinTargetImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotlinTargetImpl.kt index cc513b9c681..0247bd8d148 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotlinTargetImpl.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/ExternalKotlinTargetImpl.kt @@ -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 = setOf(kotlinTargetComponent) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/createExternalKotlinTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/createExternalKotlinTarget.kt index 34cd9c4b9df..fd6ef08305d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/createExternalKotlinTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/external/createExternalKotlinTarget.kt @@ -59,6 +59,10 @@ fun 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 KotlinMultiplatformExtension.createExter apiElementsPublishedConfiguration = apiElementsPublishedConfiguration, runtimeElementsPublishedConfiguration = runtimeElementsPublishedConfiguration, sourcesElementsPublishedConfiguration = sourcesElementsPublishedConfiguration, + resourcesElementsPublishedConfiguration = resourcesElementsPublishedConfiguration, kotlinTargetComponent = kotlinTargetComponent, artifactsTaskLocator = artifactsTaskLocator ) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt index 8dda3083ac7..a2bc578f077 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/KotlinTargetResourcesPublicationImpl.kt @@ -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" } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/publication/KotlinTargetVariantResourcesPublication.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/publication/KotlinTargetVariantResourcesPublication.kt new file mode 100644 index 00000000000..45d5f12219e --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/publication/KotlinTargetVariantResourcesPublication.kt @@ -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( + "${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 +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/resolutionStrategies.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/resolutionStrategies.kt new file mode 100644 index 00000000000..ee90dbf4aeb --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resources/resolve/resolutionStrategies.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/CreateTargetConfigurationsSideEffect.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/CreateTargetConfigurationsSideEffect.kt index 3299d1ccbab..52ef42cabcd 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/CreateTargetConfigurationsSideEffect.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/CreateTargetConfigurationsSideEffect.kt @@ -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) } } -} \ No newline at end of file +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt index 6dbc9c96e22..d4d63f8ab99 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt @@ -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) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTarget.kt index e98452cfc4f..47bccf66890 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/KotlinNativeTarget.kt @@ -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) diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetVariantResourcesPublicationTests.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetVariantResourcesPublicationTests.kt new file mode 100644 index 00000000000..da46022e6ea --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetVariantResourcesPublicationTests.kt @@ -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 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 { + 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") }, + ) + } + +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetVariantResourcesResolutionTests.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetVariantResourcesResolutionTests.kt new file mode 100644 index 00000000000..86b0b4aedb7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/KotlinTargetVariantResourcesResolutionTests.kt @@ -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 { + 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( + { 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( + { 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 { + 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 = 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) -> Set = { it }, + expectedResult: (consumer: Project, producer: Project) -> Set, + ) = 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) -> Set = { it }, + dependencyScope: DependencyScopeProvider = { ::implementation }, + middlePublishesResources: Boolean = false, + consumerPublishesResources: Boolean = false, + expectedResult: (consumer: Project, middle: Project, producer: Project) -> Set, + ) { + 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?) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/util/buildProject.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/util/buildProject.kt index 8a44d709eb4..2f398a56506 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/util/buildProject.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/util/buildProject.kt @@ -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()) }