diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesBuildService.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesBuildService.kt new file mode 100644 index 00000000000..a68099ecffd --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/PropertiesBuildService.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2023 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 + +import org.gradle.api.Project +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.jetbrains.kotlin.gradle.plugin.internal.ConfigurationTimePropertiesAccessor +import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor +import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime +import org.jetbrains.kotlin.gradle.utils.localProperties +import org.jetbrains.kotlin.gradle.utils.registerClassLoaderScopedBuildService +import java.util.concurrent.Callable +import java.util.concurrent.ConcurrentHashMap + +/** + * [BuildService] that looks up properties in the following precedence order: + * 1. Project's extra properties ([org.gradle.api.plugins.ExtensionContainer.getExtraProperties]) + * 2. Project's Gradle properties ([org.gradle.api.provider.ProviderFactory.gradleProperty]) + * 3. Root project's `local.properties` file ([Project.localProperties]) + * + * Note that extra and Gradle properties may differ across projects, whereas `local.properties` is shared across all projects. + */ +internal abstract class PropertiesBuildService : BuildService { + + interface Params : BuildServiceParameters { + val localProperties: MapProperty + } + + private val propertiesManager = ConcurrentHashMap() + + /** Returns a [Provider] of the value of the property with the given [propertyName] in the given [project]. */ + fun property(propertyName: String, project: Project): Provider { + return propertiesManager.computeIfAbsent(project) { PropertiesManager(project, parameters.localProperties.get()) } + .property(propertyName) + } + + /** Returns the value of the property with the given [propertyName] in the given [project]. */ + fun get(propertyName: String, project: Project): String? { + return property(propertyName, project).orNull + } + + companion object { + + fun registerIfAbsent(project: Project): Provider = + project.gradle.registerClassLoaderScopedBuildService(PropertiesBuildService::class) { + it.parameters.localProperties.set(project.localProperties) + } + } +} + +private class PropertiesManager(private val project: Project, private val localProperties: Map) { + + private val configurationTimePropertiesAccessor: ConfigurationTimePropertiesAccessor by lazy { + project.configurationTimePropertiesAccessor + } + + private val properties = ConcurrentHashMap>() + + fun property(propertyName: String): Provider { + // Note: The same property may be read many times (KT-62496). Therefore, + // - Use a map to create only one Provider per property. + // - Use MemoizedCallable to resolve the Provider only once + return properties.computeIfAbsent(propertyName) { + project.provider { computeValue(propertyName) } + } + } + + private fun computeValue(propertyName: String): String? { + return project.extraProperties.getOrNull(propertyName) as String? // We don't memoize extraProperties as they can still change + ?: MemoizedCallable { + project.providers.gradleProperty(propertyName).usedAtConfigurationTime(configurationTimePropertiesAccessor).orNull + ?: localProperties[propertyName] + }.call() + } +} + +private class MemoizedCallable(valueResolver: Callable) : Callable { + private val value by lazy { valueResolver.call() } + override fun call(): T = value +} 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 a6c050b5a01..902beaccaf8 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,10 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLI 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_PUBLISH_JVM_ENVIRONMENT_ATTRIBUTE 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_USE_XCODE_MESSAGE_STYLE +import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_PUBLISH_JVM_ENVIRONMENT_ATTRIBUTE import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_RUN_COMPILER_VIA_BUILD_TOOLS_API import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_STDLIB_DEFAULT_DEPENDENCY import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_STDLIB_JDK_VARIANTS_VERSION_ALIGNMENT @@ -55,7 +55,6 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinIrJsGeneratedTSValidation import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrOutputGranularity import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader -import org.jetbrains.kotlin.gradle.utils.loadProperty import org.jetbrains.kotlin.gradle.utils.localProperties import org.jetbrains.kotlin.konan.target.KonanTarget import org.jetbrains.kotlin.konan.target.presetName @@ -529,6 +528,12 @@ internal class PropertiesProvider private constructor(private val project: Proje val suppressBuildToolsApiVersionConsistencyChecks: Boolean get() = booleanProperty(PropertyNames.KOTLIN_SUPPRESS_BUILD_TOOLS_API_VERSION_CONSISTENCY_CHECKS) ?: false + private val propertiesBuildService = PropertiesBuildService.registerIfAbsent(project).get() + + internal fun property(propertyName: String): Provider = propertiesBuildService.property(propertyName, project) + + internal fun get(propertyName: String): String? = propertiesBuildService.get(propertyName, project) + /** * Retrieves a comma-separated list of browsers to use when running karma tests for [target] * @see KOTLIN_JS_KARMA_BROWSERS @@ -538,23 +543,22 @@ internal class PropertiesProvider private constructor(private val project: Proje ?: property(KOTLIN_JS_KARMA_BROWSERS).orNull private fun propertyWithDeprecatedVariant(propName: String, deprecatedPropName: String): String? { - val deprecatedProperty = property(deprecatedPropName).orNull + val deprecatedProperty = get(deprecatedPropName) if (deprecatedProperty != null) { project.reportDiagnosticOncePerBuild(KotlinToolingDiagnostics.DeprecatedPropertyWithReplacement(deprecatedPropName, propName)) } - return property(propName).orNull ?: deprecatedProperty + return get(propName) ?: deprecatedProperty } private fun booleanProperty(propName: String): Boolean? = - property(propName).orNull?.toBoolean() + get(propName)?.toBoolean() private inline fun > enumProperty( propName: String, defaultValue: T, - ): T = this.property(propName).orNull?.let { enumValueOf(it.toUpperCaseAsciiOnly()) } ?: defaultValue + ): T = get(propName)?.let { enumValueOf(it.toUpperCaseAsciiOnly()) } ?: defaultValue - private val localProperties = project.localProperties - internal fun property(propName: String): Provider = project.loadProperty(propName, localProperties) + private val localProperties: Map by lazy { project.localProperties.get() } private fun propertiesWithPrefix(prefix: String): Map { val result: MutableMap = mutableMapOf() @@ -563,7 +567,7 @@ internal class PropertiesProvider private constructor(private val project: Proje result[name] = value } } - localProperties.orNull?.forEach { (name, value) -> + localProperties.forEach { (name, value) -> if (name.startsWith(prefix)) { // Project properties have higher priority. result.putIfAbsent(name, value) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/gradleUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/gradleUtils.kt index 06b9af87961..0173848757a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/gradleUtils.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/gradleUtils.kt @@ -31,8 +31,8 @@ internal inline fun Any.getExtension(name: String): T? = internal inline fun Any.findExtension(name: String): T? = (this as ExtensionAware).extensions.findByName(name)?.let { it as T? } -inline val Any.extraProperties: ExtraPropertiesExtension - get() = (this as ExtensionAware).extensions.extraProperties +inline val ExtensionAware.extraProperties: ExtraPropertiesExtension + get() = extensions.extraProperties @JvmName("getOrNullTyped") internal inline fun ExtraPropertiesExtension.getOrNull(name: String): T? { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/internal/CustomPropertiesFileValueSource.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/internal/CustomPropertiesFileValueSource.kt index b3235fec032..8050be5de2e 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/internal/CustomPropertiesFileValueSource.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/internal/CustomPropertiesFileValueSource.kt @@ -18,7 +18,7 @@ import java.util.* * Returned type is `Map` due to the following [bug in Gradle](https://github.com/gradle/gradle/pull/24846) which * prevents proper serialization of [Properties] type. * - * If the file does not exist - returned provider will be empty. + * If the file does not exist - returned map will be empty. * * Usage: * ``` @@ -38,7 +38,7 @@ internal abstract class CustomPropertiesFileValueSource : ValueSource? { + override fun obtain(): Map { val customFile = parameters.propertiesFile.get().asFile return if (customFile.exists()) { customFile.bufferedReader().use { @@ -46,7 +46,7 @@ internal abstract class CustomPropertiesFileValueSource : ValueSource } } else { - null + emptyMap() } } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatsService.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatsService.kt index a99f0bef971..a0c93270aa1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatsService.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatsService.kt @@ -14,12 +14,11 @@ import org.gradle.invocation.DefaultGradle import org.gradle.tooling.events.OperationCompletionListener import org.gradle.tooling.events.task.TaskFinishEvent import org.jetbrains.kotlin.gradle.plugin.BuildEventsListenerRegistryHolder +import org.jetbrains.kotlin.gradle.plugin.PropertiesBuildService import org.jetbrains.kotlin.gradle.plugin.internal.ConfigurationTimePropertiesAccessor import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatHandler.Companion.runSafe -import org.jetbrains.kotlin.gradle.utils.loadProperty -import org.jetbrains.kotlin.gradle.utils.localProperties import org.jetbrains.kotlin.statistics.BuildSessionLogger import org.jetbrains.kotlin.statistics.BuildSessionLogger.Companion.STATISTICS_FOLDER_NAME import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics @@ -246,16 +245,11 @@ internal abstract class AbstractKotlinBuildStatsService( private val customSessionLoggerRootPath: String? init { - val localProperties = project.localProperties - forcePropertiesValidation = project - .loadProperty(FORCE_VALUES_VALIDATION, localProperties) - .orNull?.toBoolean() ?: false - customSessionLoggerRootPath = project - .loadProperty(CUSTOM_LOGGER_ROOT_PATH, localProperties) - .orNull - ?.also { - logger.warn("$CUSTOM_LOGGER_ROOT_PATH property for test purpose only") - } + val propertiesBuildService = PropertiesBuildService.registerIfAbsent(project).get() + forcePropertiesValidation = propertiesBuildService.get(FORCE_VALUES_VALIDATION, project)?.toBoolean() ?: false + customSessionLoggerRootPath = propertiesBuildService.get(CUSTOM_LOGGER_ROOT_PATH, project)?.also { + logger.warn("$CUSTOM_LOGGER_ROOT_PATH property for test purpose only") + } } private val sessionLoggerRootPath = diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt index aed46814b8a..59fd17a0aac 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/fileUtils.kt @@ -9,8 +9,6 @@ import org.gradle.api.Project import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile import org.gradle.api.provider.Provider -import org.jetbrains.kotlin.gradle.plugin.extraProperties -import org.jetbrains.kotlin.gradle.plugin.getOrNull import org.jetbrains.kotlin.gradle.plugin.internal.CustomPropertiesFileValueSource import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime @@ -133,31 +131,6 @@ internal fun File.existsCompat(): Boolean = exists() } -/** - * Looks up the property in the following sources with decreasing priority: - * 1. Current project extra properties - * 2. Project Gradle properties (-P, gradle.properties, etc...) - * 3. `local.properties` file located in the rootDir - * - * If multiple properties are loaded from a same caller, it is better to cache `localProperties` into local variable. - */ -internal fun Project.loadProperty( - propName: String, - localPropertiesProvider: Provider> = localProperties -): Provider = providers - .provider { - extraProperties.getOrNull(propName) as? String - } - .orElse( - project.providers - .gradleProperty(propName) - .usedAtConfigurationTime(configurationTimePropertiesAccessor) - ) - .orElse( - @Suppress("TYPE_MISMATCH") - localPropertiesProvider.map { it[propName] } - ) - /** * Loads 'local.properties' file content as [Properties]. * diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiUtilsTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiUtilsTest.kt index aa4d8ae4c1a..c4bb55b525d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiUtilsTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/ExternalKotlinTargetApiUtilsTest.kt @@ -31,7 +31,6 @@ class ExternalKotlinTargetApiUtilsTest { @Test fun `test - publishJvmEnvironmentAttribute`() { val project = buildProjectWithMPP() - project.externalKotlinTargetApiUtils.publishJvmEnvironmentAttribute(true) assertTrue(project.kotlinPropertiesProvider.publishJvmEnvironmentAttribute) diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/diagnosticsTests/DiagnosticsReportingFunctionalTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/diagnosticsTests/DiagnosticsReportingFunctionalTest.kt index a15f65ddc3a..8fd2832ce80 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/diagnosticsTests/DiagnosticsReportingFunctionalTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/diagnosticsTests/DiagnosticsReportingFunctionalTest.kt @@ -9,14 +9,13 @@ import org.gradle.api.Project import org.gradle.api.internal.project.ProjectInternal import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider import org.jetbrains.kotlin.gradle.plugin.diagnostics.* -import org.jetbrains.kotlin.gradle.util.applyKotlinJvmPlugin -import org.jetbrains.kotlin.gradle.util.buildProject -import org.jetbrains.kotlin.gradle.util.checkDiagnostics -import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic.Severity import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic.Severity.ERROR import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic.Severity.WARNING import org.jetbrains.kotlin.gradle.plugin.extraProperties +import org.jetbrains.kotlin.gradle.util.applyKotlinJvmPlugin +import org.jetbrains.kotlin.gradle.util.buildProject +import org.jetbrains.kotlin.gradle.util.checkDiagnostics import org.jetbrains.kotlin.gradle.util.set import org.junit.Test @@ -120,6 +119,7 @@ class DiagnosticsReportingFunctionalTest { fun testSuppressedWarnings() { buildProject().run { applyKotlinJvmPlugin() + extraProperties.set(PropertiesProvider.PropertyNames.KOTLIN_SUPPRESS_GRADLE_PLUGIN_WARNINGS, "TEST_DIAGNOSTIC") reportTestDiagnostic() evaluate() diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/sources/android/KotlinAndroidSourceSetLayoutExtensionTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/sources/android/KotlinAndroidSourceSetLayoutExtensionTest.kt index 9ff28a9fddd..c4e4db60a16 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/sources/android/KotlinAndroidSourceSetLayoutExtensionTest.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/unitTests/sources/android/KotlinAndroidSourceSetLayoutExtensionTest.kt @@ -25,6 +25,7 @@ class KotlinAndroidSourceSetLayoutExtensionTest { fun `single platform plugin`() { val project = ProjectBuilder.builder().build() project.plugins.apply(KotlinAndroidPluginWrapper::class.java) + assertEquals(singleTargetAndroidSourceSetLayout, project.kotlinAndroidSourceSetLayout) project.setMultiplatformAndroidSourceSetLayoutVersion(1) @@ -36,8 +37,7 @@ class KotlinAndroidSourceSetLayoutExtensionTest { @Test fun `test multiplatform plugin`() { - val project = buildProjectWithMPP { } - + val project = buildProjectWithMPP() assertEquals( multiplatformAndroidSourceSetLayoutV2, project.kotlinAndroidSourceSetLayout, "Expected v2 being set as default"