[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'
This commit is contained in:
sebastian.sellmair
2022-08-03 14:04:17 +02:00
committed by Space
parent 9634e64322
commit 3b19cf5831
3 changed files with 91 additions and 1 deletions
@@ -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 <T : Named> NamedDomainObjectContainer<T>.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)
}
}
}
}
@@ -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<Class<*>>(UnknownDomainObjectException::class.java, causes.last().javaClass)
}
}
@@ -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()