From 950d5a991ccffbe9d7b831af413b30c97aa5ccf0 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov <654232+AlexeyTsvetkov@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:33:11 +0200 Subject: [PATCH] Manage Kotlin native cache kind automatically based on Kotlin version (#3477) Set Kotlin/Native cache kind based on Kotlin version Resolves #2046 Resolves #2386 --- .../ComposeCompilerKotlinSupportPlugin.kt | 20 +-- .../ComposeMultiplatformBuildService.kt | 78 ++++++++---- .../org/jetbrains/compose/ComposePlugin.kt | 6 +- .../internal/ComposeProjectProperties.kt | 22 ++-- .../configureNativeCompilerCaching.kt | 114 ++++++++++++++++++ .../internal/utils/IosGradleProperties.kt | 4 +- .../BuildEventsListenerRegistryProvider.kt | 18 +++ .../internal/utils/KGPPropertyProvider.kt | 35 ++++++ .../compose/internal/utils/fileUtils.kt | 13 +- .../compose/internal/utils/gradleUtils.kt | 11 ++ .../compose/internal/utils/providerUtils.kt | 2 +- .../tests/integration/GradlePluginTest.kt | 55 +++++++++ .../compose/test/utils/TestKotlinVersions.kt | 2 + .../compose/test/utils/TestProjects.kt | 2 + .../compose/test/utils/assertUtils.kt | 22 ++++ .../misc/nativeCacheKind/build.gradle | 27 +++++ .../misc/nativeCacheKind/gradle.properties | 1 + .../misc/nativeCacheKind/settings.gradle | 12 ++ .../src/commonMain/kotlin/App.kt | 10 ++ .../misc/nativeCacheKindWarning/build.gradle | 20 +++ .../nativeCacheKindWarning/gradle.properties | 1 + .../nativeCacheKindWarning/settings.gradle | 12 ++ .../src/commonMain/kotlin/App.kt | 10 ++ 23 files changed, 439 insertions(+), 58 deletions(-) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/configureNativeCompilerCaching.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/BuildEventsListenerRegistryProvider.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/KGPPropertyProvider.kt create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/build.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/gradle.properties create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/settings.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/src/commonMain/kotlin/App.kt create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/build.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/gradle.properties create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/settings.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/src/commonMain/kotlin/App.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt index caeac692ef0..3d9c326490f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerKotlinSupportPlugin.kt @@ -13,7 +13,6 @@ import org.jetbrains.compose.internal.mppExtOrNull import org.jetbrains.compose.internal.webExt import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget -import java.lang.ClassCastException import javax.inject.Inject @Suppress("UnstableApiUsage") @@ -48,24 +47,7 @@ class ComposeCompilerKotlinSupportPlugin @Inject constructor( return !groupId.startsWith("org.jetbrains.compose.compiler") } - val service = ComposeMultiplatformBuildService.provider(target) - buildEventsListenerRegistry.onTaskCompletion(service) - - val providedService = try { - service.get() - } catch (e: ClassCastException) { - // Compose Gradle plugin was probably loaded more than once - // See https://github.com/JetBrains/compose-multiplatform/issues/3459 - - throw IllegalStateException( - "Failed to get ComposeMultiplatformBuildService instance." + - " Compose Gradle plugin was probably loaded more than once." + - " Consider declaring it in the root build.gradle.kts", - e - ) - } - - providedService.parameters.unsupportedCompilerPlugins.add( + ComposeMultiplatformBuildService.getInstance(target).unsupportedCompilerPlugins.add( target.provider { composeCompilerArtifactProvider.compilerArtifact.takeIf { target.hasNonJvmTargets() && it.isNonJBComposeCompiler() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt index 5e117da92ee..e8da1a73c4a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeMultiplatformBuildService.kt @@ -6,27 +6,32 @@ import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters -import org.gradle.api.services.BuildServiceRegistry import org.gradle.tooling.events.FinishEvent import org.gradle.tooling.events.OperationCompletionListener +import org.jetbrains.compose.internal.utils.BuildEventsListenerRegistryProvider import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact // The service implements OperationCompletionListener just so Gradle would use the service // even if the service is not used by any task or transformation -abstract class ComposeMultiplatformBuildService : BuildService, +abstract class ComposeMultiplatformBuildService : BuildService, OperationCompletionListener, AutoCloseable { - interface Parameters : BuildServiceParameters { - val unsupportedCompilerPlugins: SetProperty> - } private val log = Logging.getLogger(this.javaClass) + internal abstract val unsupportedCompilerPlugins: SetProperty> + internal abstract val delayedWarnings: SetProperty + + fun warnOnceAfterBuild(message: String) { + delayedWarnings.add(message) + } + override fun close() { notifyAboutUnsupportedCompilerPlugin() + logDelayedWarnings() } private fun notifyAboutUnsupportedCompilerPlugin() { - val unsupportedCompilerPlugin = parameters.unsupportedCompilerPlugins.orNull + val unsupportedCompilerPlugin = unsupportedCompilerPlugins.orNull ?.firstOrNull() ?.orNull @@ -35,28 +40,55 @@ abstract class ComposeMultiplatformBuildService : BuildService Unit): Provider = - project.gradle.sharedServices.registerOrConfigure { - fn() + private val COMPOSE_SERVICE_FQ_NAME = ComposeMultiplatformBuildService::class.java.canonicalName + + private fun findExistingComposeService(project: Project): ComposeMultiplatformBuildService? { + val registration = project.gradle.sharedServices.registrations.findByName(COMPOSE_SERVICE_FQ_NAME) + val service = registration?.service?.orNull + if (service != null) { + if (service !is ComposeMultiplatformBuildService) { + // Compose Gradle plugin was probably loaded more than once + // See https://github.com/JetBrains/compose-multiplatform/issues/3459 + if (service.javaClass.canonicalName == ComposeMultiplatformBuildService::class.java.canonicalName) { + val rootScript = project.rootProject.buildFile + error(""" + Compose Multiplatform Gradle plugin has been loaded in multiple classloaders. + To avoid classloading issues, declare Compose Gradle Plugin in root build file $rootScript. + """.trimIndent()) + } else { + error("Shared build service '$COMPOSE_SERVICE_FQ_NAME' has unexpected type: ${service.javaClass.canonicalName}") + } + } + return service } - fun provider(project: Project): Provider = configure(project) {} + return null + } + + @Suppress("UnstableApiUsage") + fun init(project: Project) { + val existingService = findExistingComposeService(project) + if (existingService != null) { + return + } + + val newService = project.gradle.sharedServices.registerIfAbsent(COMPOSE_SERVICE_FQ_NAME, ComposeMultiplatformBuildService::class.java) { + } + // workaround to instanciate a service even if it not binded to a task + BuildEventsListenerRegistryProvider.getInstance(project).onTaskCompletion(newService) + } + + fun getInstance(project: Project): ComposeMultiplatformBuildService = + findExistingComposeService(project) ?: error("ComposeMultiplatformBuildService was not initialized!") } } -inline fun > BuildServiceRegistry.registerOrConfigure( - crossinline fn: P.() -> Unit -): Provider { - val serviceClass = S::class.java - val serviceFqName = serviceClass.canonicalName - val existingService = registrations.findByName(serviceFqName) - ?.apply { (parameters as? P)?.fn() } - ?.service - return (existingService as? Provider) - ?: registerIfAbsent(serviceFqName, serviceClass) { - it.parameters.fn() - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index 041a68bb4a6..d7e473e28ef 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt @@ -21,6 +21,7 @@ import org.jetbrains.compose.desktop.preview.internal.initializePreview import org.jetbrains.compose.experimental.dsl.ExperimentalExtension import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck import org.jetbrains.compose.experimental.internal.configureExperimental +import org.jetbrains.compose.experimental.internal.configureNativeCompilerCaching import org.jetbrains.compose.experimental.uikit.internal.resources.configureSyncTask import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID import org.jetbrains.compose.internal.mppExt @@ -35,8 +36,10 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType internal val composeVersion get() = ComposeBuildConfig.composeVersion -class ComposePlugin : Plugin { +abstract class ComposePlugin : Plugin { override fun apply(project: Project) { + ComposeMultiplatformBuildService.init(project) + val composeExtension = project.extensions.create("compose", ComposeExtension::class.java, project) val desktopExtension = composeExtension.extensions.create("desktop", DesktopExtension::class.java) val androidExtension = composeExtension.extensions.create("android", AndroidExtension::class.java) @@ -52,6 +55,7 @@ class ComposePlugin : Plugin { composeExtension.extensions.create("web", WebExtension::class.java) project.plugins.apply(ComposeCompilerKotlinSupportPlugin::class.java) + project.configureNativeCompilerCaching() project.afterEvaluate { configureDesktop(project, desktopExtension) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt index ab1011bc808..08123a41254 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt @@ -7,7 +7,7 @@ package org.jetbrains.compose.desktop.application.internal import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory -import org.jetbrains.compose.internal.utils.findProperty +import org.jetbrains.compose.internal.utils.valueOrNull import org.jetbrains.compose.internal.utils.toBooleanProvider internal object ComposeProperties { @@ -23,32 +23,32 @@ internal object ComposeProperties { internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor" fun isVerbose(providers: ProviderFactory): Provider = - providers.findProperty(VERBOSE).toBooleanProvider(false) + providers.valueOrNull(VERBOSE).toBooleanProvider(false) fun preserveWorkingDir(providers: ProviderFactory): Provider = - providers.findProperty(PRESERVE_WD).toBooleanProvider(false) + providers.valueOrNull(PRESERVE_WD).toBooleanProvider(false) fun macSign(providers: ProviderFactory): Provider = - providers.findProperty(MAC_SIGN).toBooleanProvider(false) + providers.valueOrNull(MAC_SIGN).toBooleanProvider(false) fun macSignIdentity(providers: ProviderFactory): Provider = - providers.findProperty(MAC_SIGN_ID) + providers.valueOrNull(MAC_SIGN_ID) fun macSignKeychain(providers: ProviderFactory): Provider = - providers.findProperty(MAC_SIGN_KEYCHAIN) + providers.valueOrNull(MAC_SIGN_KEYCHAIN) fun macSignPrefix(providers: ProviderFactory): Provider = - providers.findProperty(MAC_SIGN_PREFIX) + providers.valueOrNull(MAC_SIGN_PREFIX) fun macNotarizationAppleID(providers: ProviderFactory): Provider = - providers.findProperty(MAC_NOTARIZATION_APPLE_ID) + providers.valueOrNull(MAC_NOTARIZATION_APPLE_ID) fun macNotarizationPassword(providers: ProviderFactory): Provider = - providers.findProperty(MAC_NOTARIZATION_PASSWORD) + providers.valueOrNull(MAC_NOTARIZATION_PASSWORD) fun macNotarizationAscProvider(providers: ProviderFactory): Provider = - providers.findProperty(MAC_NOTARIZATION_ASC_PROVIDER) + providers.valueOrNull(MAC_NOTARIZATION_ASC_PROVIDER) fun checkJdkVendor(providers: ProviderFactory): Provider = - providers.findProperty(CHECK_JDK_VENDOR).toBooleanProvider(true) + providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/configureNativeCompilerCaching.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/configureNativeCompilerCaching.kt new file mode 100644 index 00000000000..55e3a440a09 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/internal/configureNativeCompilerCaching.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.experimental.internal + +import org.gradle.api.Project +import org.jetbrains.compose.ComposeMultiplatformBuildService +import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID +import org.jetbrains.compose.internal.mppExt +import org.jetbrains.compose.internal.utils.KGPPropertyProvider +import org.jetbrains.compose.internal.utils.configureEachWithType +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.presetName + +private const val PROJECT_CACHE_KIND_PROPERTY_NAME = "kotlin.native.cacheKind" +private const val COMPOSE_NATIVE_MANAGE_CACHE_KIND = "compose.kotlin.native.manageCacheKind" +private const val NONE_VALUE = "none" + +internal fun Project.configureNativeCompilerCaching() { + if (findProperty(COMPOSE_NATIVE_MANAGE_CACHE_KIND) == "false") return + + plugins.withId(KOTLIN_MPP_PLUGIN_ID) { + val kotlinVersion = kotlinVersionNumbers(this) + mppExt.targets.configureEachWithType { + checkCacheKindUserValueIsNotNone() + configureTargetCompilerCache(kotlinVersion) + } + } +} + +private fun KotlinNativeTarget.checkCacheKindUserValueIsNotNone() { + // To determine cache kind KGP checks kotlin.native.cacheKind. first, then kotlin.native.cacheKind + // For each property it tries to read Project.property, then checks local.properties + // See https://github.com/JetBrains/kotlin/blob/d4d30dcfcf1afb083f09279c6f1ba05031efeabb/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesProvider.kt#L416 + val cacheKindProperties = listOf(targetCacheKindPropertyName, PROJECT_CACHE_KIND_PROPERTY_NAME) + val propertyProviders = listOf( + KGPPropertyProvider.GradleProperties(project), + KGPPropertyProvider.LocalProperties(project) + ) + + for (cacheKindProperty in cacheKindProperties) { + for (provider in propertyProviders) { + val value = provider.valueOrNull(cacheKindProperty) + if (value != null) { + if (value.equals(NONE_VALUE, ignoreCase = true)) { + ComposeMultiplatformBuildService + .getInstance(project) + .warnOnceAfterBuild(cacheKindPropertyWarningMessage(cacheKindProperty, provider)) + } + return + } + } + } +} + +private fun cacheKindPropertyWarningMessage( + cacheKindProperty: String, + provider: KGPPropertyProvider +) = """ + |Warning: '$cacheKindProperty' is explicitly set to `none`. + |This option significantly slows the Kotlin/Native compiler. + |Compose Multiplatform Gradle plugin can set this property automatically, + |when it is necessary. + | * Recommended action: remove explicit '$cacheKindProperty=$NONE_VALUE' from ${provider.location}. + | * Alternative action: if you are sure you need '$cacheKindProperty=$NONE_VALUE', disable + |this warning by adding '$COMPOSE_NATIVE_MANAGE_CACHE_KIND=false' to your 'gradle.properties'. +""".trimMargin() + +private fun KotlinNativeTarget.configureTargetCompilerCache(kotlinVersion: KotlinVersion) { + // See comments in https://youtrack.jetbrains.com/issue/KT-57329 + when { + // Kotlin < 1.9.0 => disable cache + kotlinVersion < KotlinVersion(1, 9, 0) -> { + disableKotlinNativeCache() + } + // 1.9.0 <= Kotlin < 1.9.20 => add -Xlazy-ir-for-caches=disable + kotlinVersion < KotlinVersion(1, 9, 20) -> { + disableLazyIrForCaches() + } + // Kotlin >= 1.9.20 => do nothing + else -> {} + } +} + +private val KotlinNativeTarget.targetCacheKindPropertyName: String + get() = "$PROJECT_CACHE_KIND_PROPERTY_NAME.${konanTarget.presetName}" + +private fun KotlinNativeTarget.disableKotlinNativeCache() { + if (project.hasProperty(targetCacheKindPropertyName)) { + project.setProperty(targetCacheKindPropertyName, NONE_VALUE) + } else { + project.extensions.extraProperties.set(targetCacheKindPropertyName, NONE_VALUE) + } +} + +private fun KotlinNativeTarget.disableLazyIrForCaches() { + compilations.configureEach { compilation -> + compilation.kotlinOptions.freeCompilerArgs += listOf("-Xlazy-ir-for-caches=disable") + } +} + +private fun kotlinVersionNumbers(project: Project): KotlinVersion { + val version = project.getKotlinPluginVersion() + val m = Regex("(\\d+)\\.(\\d+)\\.(\\d+)").find(version) ?: error("Kotlin version has unexpected format: '$version'") + val (_, majorPart, minorPart, patchPart) = m.groupValues + return KotlinVersion( + major = majorPart.toIntOrNull() ?: error("Could not parse major part '$majorPart' of Kotlin plugin version: '$version'"), + minor = minorPart.toIntOrNull() ?: error("Could not parse minor part '$minorPart' of Kotlin plugin version: '$version'"), + patch = patchPart.toIntOrNull() ?: error("Could not parse patch part '$patchPart' of Kotlin plugin version: '$version'"), + ) +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt index bfacedbf39e..fa5829b21d4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/uikit/internal/utils/IosGradleProperties.kt @@ -7,12 +7,12 @@ package org.jetbrains.compose.experimental.uikit.internal.utils import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory -import org.jetbrains.compose.internal.utils.findProperty +import org.jetbrains.compose.internal.utils.valueOrNull import org.jetbrains.compose.internal.utils.toBooleanProvider internal object IosGradleProperties { const val SYNC_RESOURCES_PROPERTY = "org.jetbrains.compose.ios.resources.sync" fun syncResources(providers: ProviderFactory): Provider = - providers.findProperty(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true) + providers.valueOrNull(SYNC_RESOURCES_PROPERTY).toBooleanProvider(true) } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/BuildEventsListenerRegistryProvider.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/BuildEventsListenerRegistryProvider.kt new file mode 100644 index 00000000000..5aeab21c98a --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/BuildEventsListenerRegistryProvider.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.internal.utils + +import org.gradle.api.Project +import org.gradle.build.event.BuildEventsListenerRegistry +import javax.inject.Inject + +@Suppress("UnstableApiUsage") +internal abstract class BuildEventsListenerRegistryProvider @Inject constructor(val registry: BuildEventsListenerRegistry) { + companion object { + fun getInstance(project: Project): BuildEventsListenerRegistry = + project.objects.newInstance(BuildEventsListenerRegistryProvider::class.java).registry + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/KGPPropertyProvider.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/KGPPropertyProvider.kt new file mode 100644 index 00000000000..bef38b4a257 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/KGPPropertyProvider.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.internal.utils + +import org.gradle.api.Project +import java.util.* + +/** + * Reads Kotlin Gradle plugin properties. + * + * Kotlin Gradle plugin supports reading property from two sources: + * 1. Gradle properties. Normally located in gradle.properties file, + * but can also be provided via command-line, /gradle.properties + * or can be set via Gradle API. + * 2. local.properties file. local.properties file is not supported by Gradle out-of-the-box. + * Nevertheless, it became a widespread convention. + */ +internal abstract class KGPPropertyProvider { + abstract fun valueOrNull(propertyName: String): String? + abstract val location: String + + class GradleProperties(private val project: Project) : KGPPropertyProvider() { + override fun valueOrNull(propertyName: String): String? = project.findProperty(propertyName)?.toString() + override val location: String = "gradle.properties" + } + + class LocalProperties(project: Project) : KGPPropertyProvider() { + private val localProperties: Properties by lazyLoadProperties(project.localPropertiesFile) + override fun valueOrNull(propertyName: String): String? = localProperties.getProperty(propertyName) + override val location: String = "local.properties" + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/fileUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/fileUtils.kt index ed4dcec5ed2..cb1cc91f4b7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/fileUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/fileUtils.kt @@ -12,6 +12,7 @@ import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.RegularFile import org.gradle.api.provider.Provider import java.io.File +import java.util.* internal fun Provider.toDir(project: Project): Provider = project.layout.dir(map { File(it) }) @@ -55,4 +56,14 @@ internal fun FileSystemOperations.clearDirs(vararg dirs: Provider>.ioFiles(): Array = - let { providers -> Array(size) { i -> providers[i].ioFile } } \ No newline at end of file + let { providers -> Array(size) { i -> providers[i].ioFile } } + +internal fun lazyLoadProperties(propertiesFile: File): Lazy = lazy { + Properties().apply { + if (propertiesFile.isFile) { + propertiesFile.inputStream().use { + load(it) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt index 570262ba67b..4b418218910 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt @@ -5,6 +5,7 @@ package org.jetbrains.compose.internal.utils +import org.gradle.api.DomainObjectCollection import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.logging.Logger @@ -61,3 +62,13 @@ internal fun Project.detachedDependency( internal fun Configuration.excludeTransitiveDependencies(): Configuration = apply { isTransitive = false } + +internal inline fun DomainObjectCollection<*>.configureEachWithType( + crossinline fn: SubT.() -> Unit +) { + configureEach { + if (it is SubT) { + it.fn() + } + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt index 955b49dd140..07bd94dd3bc 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/providerUtils.kt @@ -30,7 +30,7 @@ internal inline fun Provider.toProperty(objects: ObjectFactory): internal inline fun Task.provider(noinline fn: () -> T): Provider = project.provider(fn) -internal fun ProviderFactory.findProperty(prop: String): Provider = +internal fun ProviderFactory.valueOrNull(prop: String): Provider = provider { gradleProperty(prop).forUseAtConfigurationTimeSafe().orNull } diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt index f32fde4f746..f2cd0db71a2 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/GradlePluginTest.kt @@ -108,6 +108,61 @@ class GradlePluginTest : GradlePluginTestBase() { } } + @Test + fun nativeCacheKind() { + Assumptions.assumeTrue(currentOS == OS.MacOS) + fun nativeCacheKindProject(kotlinVersion: String) = testProject( + TestProjects.nativeCacheKind, + defaultTestEnvironment.copy(kotlinVersion = kotlinVersion, useGradleConfigurationCache = false) + ) + + val task = ":linkDebugFrameworkIosX64" + with(nativeCacheKindProject(kotlinVersion = TestKotlinVersions.v1_8_20)) { + gradle(task, "--info").checks { + check.taskSuccessful(task) + check.logDoesntContain("-Xauto-cache-from=") + } + } + testWorkDir.deleteRecursively() + testWorkDir.mkdirs() + with(nativeCacheKindProject(kotlinVersion = TestKotlinVersions.v1_9_0) ) { + gradle(task, "--info").checks { + check.taskSuccessful(task) + check.logContains("-Xauto-cache-from=") + check.logContains("-Xlazy-ir-for-caches=disable") + } + } + } + + @Test + fun nativeCacheKindWarning() { + Assumptions.assumeTrue(currentOS == OS.MacOS) + fun nativeCacheKindWarningProject(kotlinVersion: String) = testProject( + TestProjects.nativeCacheKindWarning, + defaultTestEnvironment.copy(kotlinVersion = kotlinVersion) + ) + + val cacheKindWarning = "Warning: 'kotlin.native.cacheKind' is explicitly set to `none`" + + val args = arrayOf("build", "--dry-run", "-Pkotlin.native.cacheKind=none") + with(nativeCacheKindWarningProject(kotlinVersion = TestKotlinVersions.v1_8_20)) { + gradle(*args).checks { + check.logContainsOnce(cacheKindWarning) + } + // check that the warning is shown even when the configuration is loaded from cache + gradle(*args).checks { + check.logContainsOnce(cacheKindWarning) + } + } + testWorkDir.deleteRecursively() + testWorkDir.mkdirs() + with(nativeCacheKindWarningProject(kotlinVersion = TestKotlinVersions.v1_9_0) ) { + gradle(*args).checks { + check.logContainsOnce(cacheKindWarning) + } + } + } + @Test fun skikoWasm() = with( testProject( diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestKotlinVersions.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestKotlinVersions.kt index bfa4c103363..5dc8a2c5558 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestKotlinVersions.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestKotlinVersions.kt @@ -7,4 +7,6 @@ package org.jetbrains.compose.test.utils object TestKotlinVersions { val Default = TestProperties.composeCompilerCompatibleKotlinVersion + val v1_8_20 = "1.8.20" + val v1_9_0 = "1.9.0" } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt index 8ca826726a4..b2a84f49ba7 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/TestProjects.kt @@ -26,4 +26,6 @@ object TestProjects { const val jvmPreview = "misc/jvmPreview" const val iosResources = "misc/iosResources" const val iosMokoResources = "misc/iosMokoResources" + const val nativeCacheKind = "misc/nativeCacheKind" + const val nativeCacheKindWarning = "misc/nativeCacheKindWarning" } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt index 955616674cc..6be9978bb01 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt @@ -29,6 +29,14 @@ internal class BuildResultChecks(private val result: BuildResult) { val log: String get() = result.output + fun logContainsOnce(substring: String) { + val actualCount = log.countOccurrencesOf(substring) + if (actualCount != 1) throw AssertionError( + "Test output must contain substring '$substring' exactly once. " + + "Actual number of occurrences: $actualCount" + ) + } + fun logContains(substring: String) { if (!result.output.contains(substring)) { throw AssertionError("Test output does not contain the expected string: '$substring'") @@ -96,3 +104,17 @@ internal fun assertNotEqualTextFiles(actual: File, expected: File) { private fun File.normalizedText() = readLines().joinToString("\n") { it.trim() } + +private fun String.countOccurrencesOf(substring: String): Int { + var count = 0 + var i = 0 + while (i >= 0 && i < length) { + i = indexOf(substring, startIndex = i) + + if (i == -1) break + + i++ + count++ + } + return count +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/build.gradle new file mode 100644 index 00000000000..1e43a147b8e --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/build.gradle @@ -0,0 +1,27 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.compose" +} + +kotlin { + iosX64 { + binaries.framework { + isStatic = true + baseName = "shared" + } + } + iosArm64 { + binaries.framework { + isStatic = true + baseName = "shared" + } + } + + sourceSets { + commonMain { + dependencies { + implementation(compose.runtime) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/gradle.properties b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/gradle.properties new file mode 100644 index 00000000000..689880ee3f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.uikit.enabled=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/settings.gradle b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/settings.gradle new file mode 100644 index 00000000000..a270b9409b9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } + } +} +rootProject.name = "nativeCacheKind" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/src/commonMain/kotlin/App.kt b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/src/commonMain/kotlin/App.kt new file mode 100644 index 00000000000..2fe6d0719f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKind/src/commonMain/kotlin/App.kt @@ -0,0 +1,10 @@ +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun App() { + var text by remember { mutableStateOf("Hello, World!") } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/build.gradle new file mode 100644 index 00000000000..40170b68cb3 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/build.gradle @@ -0,0 +1,20 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.compose" +} + +kotlin { + iosX64() + iosArm64() + iosSimulatorArm64() + macosX64() + macosArm64() + + sourceSets { + commonMain { + dependencies { + implementation(compose.runtime) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/gradle.properties b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/gradle.properties new file mode 100644 index 00000000000..689880ee3f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.uikit.enabled=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/settings.gradle b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/settings.gradle new file mode 100644 index 00000000000..a270b9409b9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + maven { url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" } + } +} +rootProject.name = "nativeCacheKind" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/src/commonMain/kotlin/App.kt b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/src/commonMain/kotlin/App.kt new file mode 100644 index 00000000000..2fe6d0719f4 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/nativeCacheKindWarning/src/commonMain/kotlin/App.kt @@ -0,0 +1,10 @@ +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun App() { + var text by remember { mutableStateOf("Hello, World!") } +}