From 3b19cf583131fb7da8fc00a4998c5dd4592b2842 Mon Sep 17 00:00:00 2001 From: "sebastian.sellmair" Date: Wed, 3 Aug 2022 14:04:17 +0200 Subject: [PATCH] [Gradle][KT-53342] Introduce 'invokeWhenCreated' API to configure objects when created This aims to make it possible and sane to configure Android SourceSets that get created in AGP's afterEvaluate phase. SourceSets that contain flavor and buildTypes will not be available right away. Using regular Gradle APIs like ``` sourceSets.matching { it.name == "..." }.configure {} ``` are not sufficient, since misconfigurations here will stay unnoticed, whilst this 'invokeWhenCreated' API ensures that the object is available at least before the buildscripts 'afterEvluate' --- .../gradle/dsl/KotlinProjectExtension.kt | 16 ++++- .../kotlin/gradle/InvokeWhenCreatedTest.kt | 70 +++++++++++++++++++ .../jetbrains/kotlin/gradle/buildProject.kt | 6 ++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt index c40a68537d6..3f08ce9d061 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt @@ -115,12 +115,26 @@ abstract class KotlinTopLevelExtension(internal val project: Project) : KotlinTo explicitApi = ExplicitApiMode.Warning } + /** + * Can be used to configure objects that are not yet created, or will be created in + * 'afterEvaluate' (e.g. typically Android source sets containing flavors and buildTypes) + * + * Will fail project evaluation if the domain object is not created before 'afterEvaluate' listeners in the buildscript. + * + * @param configure: Called inline, if the value is already present. Called once the domain object is created. + */ @ExperimentalKotlinGradlePluginApi fun NamedDomainObjectContainer.invokeWhenCreated(name: String, configure: T.() -> Unit) { val invoked = AtomicBoolean(false) matching { it.name == name }.all { value -> if (!invoked.getAndSet(true)) value.configure() } project.whenEvaluated { - if (!invoked.getAndSet(true)) named(name).configure(configure) + if (!invoked.getAndSet(true)) { + /* + Expected to fail, since the listener was not called. + Letting Gradle fail here will result in a more natural error message + */ + named(name).configure(configure) + } } } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt new file mode 100644 index 00000000000..0e8162d1f47 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/InvokeWhenCreatedTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2022 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:Suppress("FunctionName") + +package org.jetbrains.kotlin.gradle + +import org.gradle.api.Named +import org.gradle.api.UnknownDomainObjectException +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.tooling.core.withLinearClosure +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFails +import kotlin.test.fail + +class InvokeWhenCreatedTest { + + data class Value(private val name: String) : Named { + override fun getName(): String = name + override fun toString(): String = name + } + + private val project = buildProjectWithJvm() + private val container = project.objects.domainObjectContainer(Value::class.java) { name -> Value(name) } + + @Test + fun `test - values is already present - is called inline`() { + var invocations = 0 + + container.create("x") + project.kotlinExtension.apply { + container.invokeWhenCreated("x") { invocations++; assertEquals("x", name) } + assertEquals(1, invocations, "Expected 'invokeWhenCreated' to be called inline, once") + } + + project.evaluate() + assertEquals(1, invocations, "Expected 'invokeWhenCreated' to be called only once") + } + + @Test + fun `test - value will be created later`() { + var invocations = 0 + + project.kotlinExtension.apply { + container.invokeWhenCreated("x") { invocations++; assertEquals("x", name) } + assertEquals(0, invocations, "Expected 'invokeWhenCreated' to be not called") + } + + container.create("y") + assertEquals(0, invocations, "Expected 'invokeWhenCreated' to be not called") + + container.create("x") + assertEquals(1, invocations, "Expected 'invokeWhenCreated' to be called once") + } + + @Test + fun `test - value is missing - evaluate will fail`() { + project.kotlinExtension.apply { + container.invokeWhenCreated("x") { + fail("Called 'invokeWhenCreated' unexpectedly") + } + } + + val causes = assertFails { project.evaluate() }.withLinearClosure { it.cause } + assertEquals>(UnknownDomainObjectException::class.java, causes.last().javaClass) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt index b889b631c1c..e13fff5229c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/buildProject.kt @@ -11,6 +11,7 @@ import org.gradle.api.internal.project.ProjectInternal import org.gradle.testfixtures.ProjectBuilder import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformJvmPlugin import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20ProjectExtension fun buildProject( @@ -35,6 +36,11 @@ fun buildProjectWithKPM(code: Project.() -> Unit= {}) = buildProject { code() } +fun buildProjectWithJvm(code: Project.() -> Unit = {}) = buildProject { + project.plugins.apply(KotlinPlatformJvmPlugin::class.java) + code() +} + fun Project.kotlin(code: KotlinMultiplatformExtension.() -> Unit) { val kotlin = project.kotlinExtension as KotlinMultiplatformExtension kotlin.code()