Skip to content

Commit

Permalink
refactor DokkatooExampleProjects convention plugin so that the `gradl…
Browse files Browse the repository at this point in the history
…e.properties` files can be configured using a DSL
  • Loading branch information
aSemy committed May 21, 2023
1 parent 4cb056b commit 0d6605b
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package buildsrc.conventions

import buildsrc.conventions.Maven_publish_test_gradle.MavenPublishTest
import buildsrc.utils.asConsumer
import buildsrc.utils.asProvider
import buildsrc.tasks.SetupDokkaProjects

plugins {
id("buildsrc.conventions.base")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package buildsrc.conventions

import buildsrc.conventions.Maven_publish_test_gradle.MavenPublishTest
import buildsrc.tasks.SetupDokkaProjects
import buildsrc.settings.MavenPublishTestSettings
import buildsrc.settings.DokkatooExampleProjectsSettings
import buildsrc.tasks.*
import buildsrc.utils.*
import org.gradle.kotlin.dsl.support.serviceOf

plugins {
Expand Down Expand Up @@ -34,48 +36,55 @@ val setupDokkaTemplateProjects by tasks.registering(SetupDokkaProjects::class) {
destinationToSources.convention(emptyMap())
}

val mavenPublishTestExtension = extensions.getByType<MavenPublishTest>()
val mavenPublishTestExtension = extensions.getByType<MavenPublishTestSettings>()


val updateDokkatooExamplesGradleProperties by tasks.registering {
group = TASK_GROUP

mustRunAfter(tasks.withType<SetupDokkaProjects>())
fun createDokkatooExampleProjectsSettings(
projectDir: Directory = project.layout.projectDirectory
): DokkatooExampleProjectsSettings {
return extensions.create<DokkatooExampleProjectsSettings>(
DokkatooExampleProjectsSettings.EXTENSION_NAME
).apply {

val gradlePropertiesFiles =
layout.projectDirectory.asFileTree
// find all Gradle settings files
val settingsFiles = projectDir.asFileTree
.matching {
include(
"**/*dokkatoo*/settings.gradle.kts",
"**/*dokkatoo*/settings.gradle",
)
}.elements.map { settingsFiles ->
settingsFiles.map {
it.asFile.resolveSibling("gradle.properties")
}
}.files

// for each settings file, create a GradlePropertiesSpec
settingsFiles.forEach {
val destinationDir = it.parentFile
val name =
destinationDir.toRelativeString(projectDir.asFile).toAlphaNumericCamelCase()
gradleProperties.register(name) {
this.destinationDir.set(destinationDir)
}
}

outputs.files(gradlePropertiesFiles)

val testMavenRepoPath = mavenPublishTestExtension.testMavenRepo.map {
it.asFile.invariantSeparatorsPath
}
inputs.property("testMavenRepoPath", testMavenRepoPath)

doLast task@{
gradlePropertiesFiles.get().forEach {
it.writeText(
"""
|# DO NOT EDIT - Generated by ${this@task.path}
|
|testMavenRepo=${testMavenRepoPath.get()}
|
""".trimMargin()
)
gradleProperties.configureEach {
enableTestMavenRepo.convention(true)
}
}
}

val dokkatooExampleProjectsSettings = createDokkatooExampleProjectsSettings()

val updateDokkatooExamplesGradleProperties by tasks.registering(
UpdateDokkatooExampleGradleProperties::class
) {
group = TASK_GROUP

mustRunAfter(tasks.withType<SetupDokkaProjects>())

gradleProperties.addAllLater(providers.provider { dokkatooExampleProjectsSettings.gradleProperties })

testMavenRepo.set(mavenPublishTestExtension.testMavenRepo)
}

val dokkatooVersion = provider { project.version.toString() }

val updateDokkatooExamplesBuildFiles by tasks.registering {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package buildsrc.conventions

import buildsrc.settings.MavenPublishTestSettings
import buildsrc.utils.asConsumer
import buildsrc.utils.asProvider

Expand All @@ -8,25 +9,15 @@ import buildsrc.utils.asProvider
Utility for publishing a project to a local Maven directory for use in integration tests.
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

plugins {
base
}

abstract class MavenPublishTest(
val testMavenRepo: Provider<Directory>
) {
companion object {
val attribute = Attribute.of("maven-publish-test", String::class.java)
}
}

val Gradle.rootGradle: Gradle get() = generateSequence(gradle) { it.parent }.last()

val mavenPublishTestExtension = extensions.create<MavenPublishTest>(
val mavenPublishTestExtension = extensions.create<MavenPublishTestSettings>(
"mavenPublishTest",
gradle.rootGradle.rootProject.layout.buildDirectory.dir("test-maven-repo"),
)
Expand Down Expand Up @@ -84,7 +75,7 @@ val testMavenPublication by configurations.registering {
asConsumer()
isVisible = false
attributes {
attribute(MavenPublishTest.attribute, "testMavenRepo")
attribute(MavenPublishTestSettings.attribute, "testMavenRepo")
}
}

Expand All @@ -93,7 +84,7 @@ val testMavenPublicationElements by configurations.registering {
isVisible = true
extendsFrom(testMavenPublication.get())
attributes {
attribute(MavenPublishTest.attribute, "testMavenRepo")
attribute(MavenPublishTestSettings.attribute, "testMavenRepo")
}
outgoing {
artifact(mavenPublishTestExtension.testMavenRepo) {
Expand All @@ -104,6 +95,6 @@ val testMavenPublicationElements by configurations.registering {

dependencies {
attributesSchema {
attribute(MavenPublishTest.attribute)
attribute(MavenPublishTestSettings.attribute)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package buildsrc.settings

import buildsrc.tasks.UpdateDokkatooExampleGradleProperties.GradlePropertiesSpec
import buildsrc.utils.adding
import buildsrc.utils.domainObjectContainer
import javax.inject.Inject
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionAware

/**
* Settings for the [buildsrc.conventions.Dokkatoo_example_projects_gradle] convention plugin
*/
abstract class DokkatooExampleProjectsSettings @Inject constructor(
private val objects: ObjectFactory,
) : ExtensionAware {

val gradleProperties: NamedDomainObjectContainer<GradlePropertiesSpec> =
// create an extension so Gradle will generate DSL accessors
extensions.adding("gradleProperties", objects.domainObjectContainer())

companion object {
const val EXTENSION_NAME = "dokkatooExampleProjects"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package buildsrc.settings

import org.gradle.api.attributes.Attribute
import org.gradle.api.file.Directory
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Provider

/**
* Settings for the [buildsrc.conventions.Maven_publish_test_gradle] convention plugin.
*/
abstract class MavenPublishTestSettings(
val testMavenRepo: Provider<Directory>
) : ExtensionAware {
companion object {
val attribute = Attribute.of("maven-publish-test", String::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package buildsrc.tasks

import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*

/**
* Utility for updating the `gradle.properties` of projects used in automated tests.
*/
@CacheableTask
abstract class UpdateDokkatooExampleGradleProperties @Inject constructor(
@get:Internal
val objects: ObjectFactory
) : DefaultTask(), ExtensionAware {

@get:Nested
abstract val gradleProperties: NamedDomainObjectContainer<GradlePropertiesSpec>

@get:Internal // tracked by testMavenRepoPath (don't care about the directory contents, only the path)
abstract val testMavenRepo: DirectoryProperty

@get:Input
protected val testMavenRepoPath: Provider<String> =
testMavenRepo.asFile.map { it.invariantSeparatorsPath }

@TaskAction
fun update() {
gradleProperties.forEach { spec ->

val content = buildString {
appendLine("# DO NOT EDIT - Generated by ${this@UpdateDokkatooExampleGradleProperties.path}")
appendLine()
if (spec.enableTestMavenRepo.get()) {
appendLine("testMavenRepo=${testMavenRepoPath.get()}")
appendLine()
}
spec.content.orNull?.takeIf { it.isNotBlank() }?.let { content ->
appendLine(content)
appendLine()
}
}

spec.gradlePropertiesFile.get().asFile.writeText(content)
}
}

/**
* Represents a `gradle.properties` file that should be automatically generated,
* into the file [gradleProperties].
*/
abstract class GradlePropertiesSpec(
private val name: String
) : Named {

/** The generated `gradle.properties` file */
@get:OutputFile
val gradlePropertiesFile: Provider<RegularFile> = destinationDir.file("gradle.properties")

/** The directory that the `gradle.properties` will be generated into */
@get:Internal
abstract val destinationDir: DirectoryProperty

/** Optional additional content to append to the `gradle.properties` file */
@get:Input
@get:Optional
abstract val content: Property<String>

/** Whether the project-local Maven repo (that contains Dokkatoo snapshots) should be added */
@get:Input
abstract val enableTestMavenRepo: Property<Boolean>

@Input
override fun getName(): String = name
}
}
45 changes: 44 additions & 1 deletion buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package buildsrc.utils

import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.NamedDomainObjectFactory
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.component.AdhocComponentWithVariants
import org.gradle.api.file.RelativePath
import org.gradle.kotlin.dsl.get
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.kotlin.dsl.*

/**
* Mark this [Configuration] as one that will be consumed by other subprojects.
Expand Down Expand Up @@ -63,3 +67,42 @@ fun Project.skipTestFixturesPublications() {
javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() }
javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() }
}


/**
* Add an extension to the [ExtensionContainer], and return the value.
*
* Adding an extension is especially useful for improving the DSL in build scripts when [T] is a
* [NamedDomainObjectContainer].
* Using an extension will allow Gradle to generate
* [type-safe model accessors](https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:accessor_applicability)
* for added types.
*
* ([name] should match the property name. This has to be done manually. I tried using a
* delegated-property provider but then Gradle can't introspect the types properly, so it fails to
* create accessors).
*/
internal inline fun <reified T : Any> ExtensionContainer.adding(
name: String,
value: T,
): T {
add<T>(name, value)
return value
}

/**
* Create a new [NamedDomainObjectContainer], using
* [org.gradle.kotlin.dsl.domainObjectContainer]
* (but [T] is `reified`).
*
* @param[factory] an optional factory for creating elements
* @see org.gradle.kotlin.dsl.domainObjectContainer
*/
internal inline fun <reified T : Any> ObjectFactory.domainObjectContainer(
factory: NamedDomainObjectFactory<T>? = null
): NamedDomainObjectContainer<T> =
if (factory == null) {
domainObjectContainer(T::class)
} else {
domainObjectContainer(T::class, factory)
}
30 changes: 30 additions & 0 deletions buildSrc/src/main/kotlin/buildsrc/utils/strings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package buildsrc.utils


/**
* Title case the first char of a string.
*
* (Custom implementation because [toUpperCase]/[uppercase] is deprecated.
* Can be removed when Gradle is updated)
*/
internal fun String.uppercaseFirstChar(): String = mapFirstChar(Character::toTitleCase)


internal fun String.lowercaseFirstChar(): String = mapFirstChar(Character::toLowerCase)


private inline fun String.mapFirstChar(
transform: (Char) -> Char
): String = if (isNotEmpty()) transform(this[0]) + substring(1) else this


/**
* Filters all non-alphanumeric characters and converts the result into camelCase.
*/
internal fun String.toAlphaNumericCamelCase(): String =
map { if (it.isLetterOrDigit()) it else ' '}
.joinToString("")
.split(" ")
.filter { it.isNotBlank() }
.joinToString("") { it.uppercaseFirstChar() }
.lowercaseFirstChar()

0 comments on commit 0d6605b

Please sign in to comment.