Created
January 3, 2022 16:48
-
-
Save ZakTaccardi/a4e1b62b31a7e1069c309a2a169b1058 to your computer and use it in GitHub Desktop.
`local.properties` and project level gradle properties support for Configuration Cache
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.gradle.api.Project | |
import org.gradle.api.file.ProjectLayout | |
import org.gradle.api.provider.Property | |
import org.gradle.api.provider.Provider | |
import org.gradle.api.provider.ProviderFactory | |
import org.gradle.api.provider.ValueSource | |
import org.gradle.api.provider.ValueSourceParameters | |
import org.gradle.kotlin.dsl.of | |
import java.io.StringReader | |
import java.util.Properties | |
/** | |
* Like [ProviderFactory.gradleProperty] - but respects `local.properties` and project level `gradle.properties` | |
* | |
* Workaround for the lack of support here: | |
* * https://github.com/gradle/gradle/issues/12283 | |
* * https://github.com/gradle/gradle/issues/13302 | |
* | |
* Properties are loaded in the following order: | |
* | |
* 1. `-PcommandLineProperty=..` | |
* 2. `subproject/local.properties` | |
* 3. `local.properties` (root) | |
* 4. `subproject/gradle.properties` | |
* 5. `gradle.properties` (root) | |
* | |
* Note - configuration cache support is currently blocked by: | |
* * https://github.com/gradle/gradle/issues/19474 | |
*/ | |
fun ProviderFactory.gradleProperty( | |
project: Project, | |
propertyName: String | |
): Provider<String> = of( | |
LocalPropertySupportValueSource::class, | |
) { | |
parameters { | |
this.rootGradleProperty.set( | |
rootGradleProperty(propertyName).unwrap() | |
) | |
this.rootLocalProperty.set( | |
rootLocalProperty(project, propertyName).unwrap() | |
) | |
this.subprojectGradleProperty.set( | |
subprojectGradleProperty(project, propertyName).unwrap() | |
) | |
this.projectLevelLocalProperty.set( | |
projectLevelLocalProperty(project, propertyName).unwrap() | |
) | |
// note: this property does not yet support the configuration cache due to | |
// https://github.com/gradle/gradle/issues/19474 | |
this.startParameterGradleProperty.set( | |
startParameterGradleProperty(project, propertyName).unwrap() | |
) | |
} | |
} | |
private val localPropertiesFileName = "local.properties" | |
private fun ProviderFactory.rootGradleProperty( | |
propertyName: String | |
): Provider<PropertyOrNull> = gradleProperty(propertyName) | |
.map<PropertyOrNull> { PropertyOrNull.NonNull(it) } | |
.orElse(PropertyOrNull.Null) | |
// value from root `local.properties` | |
private fun ProviderFactory.rootLocalProperty( | |
project: Project, | |
propertyName: String | |
): Provider<PropertyOrNull> = loadFromPropertiesFile( | |
layout = project.rootProject.layout, | |
propertyName = propertyName, | |
propertyFileName = localPropertiesFileName | |
) | |
// value from project level `local.properties` | |
private fun ProviderFactory.projectLevelLocalProperty( | |
project: Project, | |
propertyName: String | |
): Provider<PropertyOrNull> = loadFromPropertiesFile( | |
layout = project.layout, | |
propertyName = propertyName, | |
propertyFileName = localPropertiesFileName | |
) | |
// value from project level `gradle.properties` | |
private fun ProviderFactory.subprojectGradleProperty( | |
project: Project, | |
propertyName: String | |
): Provider<PropertyOrNull> = loadFromPropertiesFile( | |
layout = project.layout, | |
propertyName = propertyName, | |
propertyFileName = "gradle.properties" | |
) | |
// value from gradle start parameter | |
private fun ProviderFactory.startParameterGradleProperty( | |
project: Project, | |
propertyName: String | |
): Provider<PropertyOrNull> { | |
return provider<PropertyOrNull?> { | |
val startParameter = project.gradle.startParameter | |
val value = startParameter.projectProperties | |
.get(propertyName) | |
PropertyOrNull.create(value) | |
} | |
} | |
private fun ProviderFactory.loadFromPropertiesFile( | |
layout: ProjectLayout, | |
propertyName: String, | |
propertyFileName: String | |
): Provider<PropertyOrNull> { | |
val providers = this | |
return providers.fileContents( | |
layout.projectDirectory.file(propertyFileName) | |
) | |
.asText | |
.map { stringContents -> | |
Properties().apply { | |
load(StringReader(stringContents)) | |
} | |
} | |
.map<PropertyOrNull> { props -> | |
PropertyOrNull.create(props.getProperty(propertyName)) | |
} | |
.orElse(PropertyOrNull.Null) | |
} | |
private val Provider<PropertyOrNull>.extractOrNull: String? | |
get() = map<String> { | |
it.getOrNull.sneakyNull() | |
} | |
.orNull | |
private fun Provider<PropertyOrNull>.unwrap(): Provider<String> = map { | |
it.getOrNull.sneakyNull() | |
} | |
private sealed class PropertyOrNull { | |
abstract val getOrNull: String? | |
data class NonNull(val value: String) : PropertyOrNull() { | |
override val getOrNull = value | |
} | |
object Null : PropertyOrNull() { | |
override val getOrNull: String? = null | |
} | |
companion object { | |
fun create(value: String?): PropertyOrNull = if (value != null) { | |
NonNull(value) | |
} else { | |
Null | |
} | |
} | |
} | |
/** | |
* Workaround for https://github.com/gradle/gradle/issues/12388#issuecomment-643427098 | |
*/ | |
@SuppressWarnings("UNCHECKED_CAST") | |
private fun <T> T?.sneakyNull() = this as T | |
/** | |
* Suggested here: | |
* https://gradle-community.slack.com/archives/CAHSN3LDN/p1640939568404100?thread_ts=1640913506.403000&cid=CAHSN3LDN | |
*/ | |
internal interface LocalPropertySupportValueSourceParameters : ValueSourceParameters { | |
val startParameterGradleProperty: Property<String> | |
val projectLevelLocalProperty: Property<String> | |
val rootLocalProperty: Property<String> | |
val subprojectGradleProperty: Property<String> | |
val rootGradleProperty: Property<String> | |
} | |
/** | |
* Suggested here: | |
* https://gradle-community.slack.com/archives/CAHSN3LDN/p1640939568404100?thread_ts=1640913506.403000&cid=CAHSN3LDN | |
*/ | |
internal abstract class LocalPropertySupportValueSource : ValueSource<String, LocalPropertySupportValueSourceParameters> { | |
override fun obtain(): String? { | |
val params = parameters | |
val rootGradleProperty = params.rootGradleProperty.orNull | |
val rootLocalProperty = params.rootLocalProperty.orNull | |
val subprojectGradleProperty = params.subprojectGradleProperty.orNull | |
val projectLevelLocalProperty = params.projectLevelLocalProperty.orNull | |
val startParameterGradleProperty = params.startParameterGradleProperty.orNull | |
return listOfNotNull( | |
startParameterGradleProperty, | |
projectLevelLocalProperty, | |
rootLocalProperty, | |
subprojectGradleProperty, | |
rootGradleProperty | |
) | |
.firstOrNull() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.assertj.core.api.Assertions.assertThat | |
import org.gradle.testkit.runner.BuildResult | |
import org.gradle.testkit.runner.GradleRunner | |
import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated | |
import org.junit.jupiter.api.Test | |
import org.junit.jupiter.api.io.TempDir | |
import java.io.File | |
/** | |
* Tests [gradleProperty] | |
*/ | |
class LocalPropertySupportKtTest { | |
@TempDir | |
@JvmField | |
var testProjectDir: File? = null | |
@Test | |
fun `1 - startParameter has highest precedence`() = runTest( | |
rootGradle = rootGradle, | |
subprojectGradle = subprojectGradle, | |
rootLocal = rootLocal, | |
subprojectLocal = subprojectLocal, | |
startParameter = startParameter, | |
expected = startParameter, | |
// note: `startParameter` gradle property does not yet support the configuration cache due to | |
// https://github.com/gradle/gradle/issues/19474 | |
testConfigCacheSupport = true // this will fail - set to `false` to pass | |
) | |
@Test | |
fun `2 - subproject local has 2nd highest precedence`() = runTest( | |
rootGradle = rootGradle, | |
subprojectGradle = subprojectGradle, | |
rootLocal = rootLocal, | |
subprojectLocal = subprojectLocal, | |
expected = subprojectLocal | |
) | |
@Test | |
fun `3 - root project local has 3rd highest precedence`() = runTest( | |
rootGradle = rootGradle, | |
subprojectGradle = subprojectGradle, | |
rootLocal = rootLocal, | |
expected = rootLocal | |
) | |
@Test | |
fun `4 - subproject gradle properties has 4th highest precedence`() = runTest( | |
rootGradle = rootGradle, | |
subprojectGradle = subprojectGradle, | |
expected = subprojectGradle | |
) | |
@Test | |
fun `5 - root gradle properties has 5th highest precedence`() = runTest( | |
rootGradle = rootGradle, | |
expected = rootGradle | |
) | |
private fun runTest( | |
rootGradle: String? = notProvided, | |
rootLocal: String? = notProvided, | |
subprojectGradle: String? = notProvided, | |
subprojectLocal: String? = notProvided, | |
startParameter: String? = notProvided, | |
/** | |
* Currently blocked due to these outstanding questions: | |
* * https://gradle-community.slack.com/archives/CAHSN3LDN/p1640913506403000 | |
* * https://gradle-community.slack.com/archives/C013WEPGQF9/p1640915521055300 | |
*/ | |
testConfigCacheSupport: Boolean = true, | |
expected: String | |
) { | |
val propertyName = "testProperty" | |
val subprojectName = "subproject" | |
val testProjectDir = checkNotNull(testProjectDir) { | |
"`testProjectDir` was `null`" | |
} | |
val settingsFile = File(testProjectDir, "settings.gradle.kts") | |
val rootBuildFile = File(testProjectDir, "build.gradle.kts") | |
val subprojectBuildDir = File(testProjectDir, subprojectName) | |
.apply { mkdirs() } | |
val subprojectBuildFile = File(subprojectBuildDir, "build.gradle.kts") | |
settingsFile.writeText( | |
""" | |
rootProject.name = "local-property-test" | |
include(":subproject") | |
""".trimIndent() | |
) | |
rootBuildFile.writeText( | |
""" | |
// empty root build file | |
tasks.register<Exec>("listFiles") { | |
commandLine("tree") | |
} | |
""".trimIndent() | |
) | |
val rootGradleProperties = if (rootGradle != null) { | |
File(testProjectDir, "gradle.properties") | |
} else { | |
null | |
} | |
val rootLocalGradleProperties = if (rootLocal != null) { | |
File(testProjectDir, "local.properties") | |
} else { | |
null | |
} | |
val subprojectGradleProperties = if (subprojectGradle != null) { | |
File(testProjectDir, "$subprojectName/gradle.properties") | |
.apply { | |
this.parentFile.mkdirs() | |
} | |
} else { | |
null | |
} | |
val subprojectLocalProperties = if (subprojectLocal != null) { | |
File(testProjectDir, "$subprojectName/local.properties") | |
.apply { | |
this.parentFile.mkdirs() | |
} | |
} else { | |
null | |
} | |
@Suppress("NAME_SHADOWING") | |
var startParameter = startParameter?.formatAsGradleProperty(propertyName) | |
rootGradleProperties?.writePropertiesFile(propertyName, rootGradle) | |
rootLocalGradleProperties?.writePropertiesFile(propertyName, rootLocal) | |
subprojectGradleProperties?.writePropertiesFile(propertyName, subprojectGradle) | |
subprojectLocalProperties?.writePropertiesFile(propertyName, subprojectLocal) | |
// see https://youtrack.jetbrains.com/issue/KT-2425 | |
val escapedTestProperty = "\$providerValue" | |
subprojectBuildFile.writeText( | |
""" | |
import isdk.gradleProperty | |
import org.gradle.api.DefaultTask | |
import org.gradle.api.model.ObjectFactory | |
import org.gradle.api.tasks.Input | |
import org.gradle.api.tasks.TaskAction | |
import org.gradle.kotlin.dsl.property | |
import javax.inject.Inject | |
plugins { | |
id("isdk-config") apply(false) | |
} | |
val testProperty = providers.gradleProperty(project, "$propertyName") | |
tasks.register<PrintProviderTask>("printProperty") { | |
providerToPrint.set(testProperty) | |
} | |
abstract class PrintProviderTask @Inject constructor( | |
private val objects: ObjectFactory | |
): DefaultTask() { | |
@Input | |
val providerToPrint = objects.property<String>() | |
@TaskAction | |
fun doWork() { | |
val providerValue = providerToPrint.get() | |
logger.lifecycle("TEST_PROPERTY_VALUE<$escapedTestProperty>") | |
} | |
} | |
""".trimIndent() | |
) | |
fun GradleRunner.withUpdatedArguments(): GradleRunner { | |
val configCacheFlag = if (testConfigCacheSupport) { | |
"--configuration-cache" | |
} else { | |
"--no-configuration-cache" | |
} | |
return withArgumentsNotNull( | |
// uncomment this to list files - only works if you have `tree` command installed | |
// "listFiles", | |
":$subprojectName:printProperty", | |
startParameter, // this needs to be re-read for every build bc it may change | |
configCacheFlag, | |
"--stacktrace" | |
) | |
} | |
fun gradleRunner() = GradleRunner.create() | |
.withProjectDir(testProjectDir) | |
.withPluginClasspath() | |
gradleRunner() | |
.withUpdatedArguments() | |
.build { | |
assertExpectedValue(expected) | |
} | |
// run again to ensure config cache works as expected | |
gradleRunner() | |
.withUpdatedArguments() | |
.build { | |
if (testConfigCacheSupport) assertConfigCacheWasReused() | |
} | |
// change the expected value and ensure config cache is still re-used | |
val expectedProperty = Property.from(expected) | |
println("expected property is $expectedProperty") | |
val modifiedValue = "modified" | |
when (expectedProperty) { | |
Property.StartParameter -> startParameter = modifiedValue.formatAsGradleProperty(propertyName) | |
Property.SubprojectLocal -> subprojectLocalProperties!!.writePropertiesFile(propertyName, modifiedValue) | |
Property.RootLocal -> rootLocalGradleProperties!!.writePropertiesFile(propertyName, modifiedValue) | |
Property.SubprojectGradle -> subprojectGradleProperties!!.writePropertiesFile( | |
propertyName, | |
modifiedValue | |
) | |
Property.RootGradle -> rootGradleProperties!!.writePropertiesFile(propertyName, modifiedValue) | |
} | |
println("startParameter=$startParameter") | |
gradleRunner() | |
.withUpdatedArguments() | |
.build { | |
if (testConfigCacheSupport) assertConfigCacheWasReused() | |
// expected value should be modified | |
assertExpectedValue(modifiedValue) | |
} | |
} | |
private fun File.copyProjectDir(expected: String) { | |
val testProjectDir = this | |
val copyLocation = File("build/properties/$expected") | |
.apply { | |
println(absolutePath) | |
} | |
testProjectDir.ensureParentDirsCreated() | |
testProjectDir.copyRecursively(copyLocation, true) | |
} | |
private fun File.writePropertiesFile(propertyName: String, propertyValue: String?) { | |
val textToWrite = if (propertyValue != null) { | |
""" | |
$propertyName=$propertyValue | |
""".trimIndent() | |
} else { | |
" " | |
} | |
writeText(textToWrite) | |
println("$absolutePath contents") | |
println(readText()) | |
} | |
} | |
private fun GradleRunner.withArgumentsNotNull(vararg arguments: String?): GradleRunner = withArguments( | |
*arguments.filterNotNull() | |
.toTypedArray() | |
) | |
private fun String.formatAsGradleProperty( | |
propertyName: String | |
): String = "-P$propertyName=$this" | |
private fun BuildResult.assertConfigCacheWasReused(expected: String? = null) = apply { | |
output.contains("Reusing configuration cache.") | |
if (expected != null) { | |
assertExpectedValue(expected) | |
} | |
} | |
private fun BuildResult.assertExpectedValue(expected: String): BuildResult = apply { | |
val result = this | |
val regex = Regex("TEST_PROPERTY_VALUE<(.*)>") | |
val output = result.output | |
println(result.output) | |
val actual: String = regex.find(output)!! | |
.groupValues.last() // should be value between the `<..> | |
assertThat(actual) | |
.isEqualTo(expected) | |
output.contains("Reusing configuration cache.") | |
} | |
private fun GradleRunner.build(block: BuildResult.() -> Unit) { | |
val result = build() | |
val output = result.output | |
println(result.output) | |
block.invoke(result) | |
} | |
private const val rootGradle: String = "rootGradle" | |
private const val rootLocal: String = "rootLocal" | |
private const val subprojectGradle: String = "subprojectGradle" | |
private const val subprojectLocal: String = "subprojectLocal" | |
private const val startParameter: String = "startParameter" | |
private val notProvided: String? = null | |
private sealed class Property { | |
abstract val propertyValue: String | |
object RootGradle : Property() { | |
override val propertyValue: String = rootGradle | |
} | |
object RootLocal : Property() { | |
override val propertyValue: String = rootLocal | |
} | |
object SubprojectGradle : Property() { | |
override val propertyValue: String = subprojectGradle | |
} | |
object SubprojectLocal : Property() { | |
override val propertyValue: String = subprojectLocal | |
} | |
object StartParameter : Property() { | |
override val propertyValue: String = startParameter | |
} | |
companion object { | |
private val all = listOf( | |
RootGradle, | |
RootLocal, | |
SubprojectGradle, | |
SubprojectLocal, | |
StartParameter | |
) | |
fun from(stringDef: String): Property { | |
all.forEach { | |
if (it.propertyValue == stringDef) { | |
return@from it | |
} | |
} | |
error("did not find value for `$stringDef`") | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment