aboutsummaryrefslogtreecommitdiff
path: root/dokka-runners/dokkatoo/buildSrc/src
diff options
context:
space:
mode:
Diffstat (limited to 'dokka-runners/dokkatoo/buildSrc/src')
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts78
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts155
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts68
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts27
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts160
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts44
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts19
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts37
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts93
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts137
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt13
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt96
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt62
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt19
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt68
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt73
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt49
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt118
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt45
-rw-r--r--dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt26
20 files changed, 1387 insertions, 0 deletions
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts
new file mode 100644
index 00000000..ed22d799
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/android-setup.gradle.kts
@@ -0,0 +1,78 @@
+package buildsrc.conventions
+
+import org.jetbrains.kotlin.util.suffixIfNot
+
+
+/**
+ * Utilities for preparing Android projects
+ */
+
+plugins {
+ base
+ id("buildsrc.conventions.base")
+}
+
+
+val androidSdkDirPath: Provider<String> = providers
+ // first try getting the SDK installed on via GitHub step setup-android
+ .environmentVariable("ANDROID_SDK_ROOT").map(::File)
+ // else get the project-local SDK
+ .orElse(layout.projectDirectory.file("projects/ANDROID_SDK").asFile)
+ .map { it.invariantSeparatorsPath }
+
+
+val createAndroidLocalPropertiesFile by tasks.registering {
+
+ val localPropertiesFile = temporaryDir.resolve("local.properties")
+ outputs.file(localPropertiesFile).withPropertyName("localPropertiesFile")
+
+ val androidSdkDirPath = androidSdkDirPath
+ inputs.property("androidSdkDirPath", androidSdkDirPath)
+
+ doLast {
+ localPropertiesFile.apply {
+ parentFile.mkdirs()
+ createNewFile()
+ writeText(
+ """
+ |# DO NOT EDIT - Generated by $path
+ |
+ |sdk.dir=${androidSdkDirPath.get()}
+ |
+ """.trimMargin()
+ )
+ }
+ }
+}
+
+
+val updateAndroidLocalProperties by tasks.registering {
+
+ // find all local.properties files
+ val localPropertiesFiles = layout.projectDirectory.dir("projects")
+ .asFileTree
+ .matching { include("**/local.properties") }
+ .files
+
+ outputs.files(localPropertiesFiles).withPropertyName("localPropertiesFiles")
+
+ val androidSdkDirPath = androidSdkDirPath
+ inputs.property("androidSdkDirPath", androidSdkDirPath)
+
+ doLast {
+ localPropertiesFiles
+ .filter { it.exists() }
+ .forEach { file ->
+ file.writeText(
+ file.useLines { lines ->
+ lines.joinToString("\n") { line ->
+ when {
+ line.startsWith("sdk.dir=") -> "sdk.dir=${androidSdkDirPath.get()}"
+ else -> line
+ }
+ }.suffixIfNot("\n")
+ }
+ )
+ }
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts
new file mode 100644
index 00000000..60bfa2fe
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/base.gradle.kts
@@ -0,0 +1,155 @@
+package buildsrc.conventions
+
+import java.time.Duration
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+
+/**
+ * A convention plugin that sets up common config and sensible defaults for all subprojects.
+ */
+
+plugins {
+ base
+}
+
+if (project != rootProject) {
+ project.version = rootProject.version
+ project.group = rootProject.group
+}
+
+tasks.withType<AbstractArchiveTask>().configureEach {
+ // https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives
+ isPreserveFileTimestamps = false
+ isReproducibleFileOrder = true
+}
+
+tasks.withType<AbstractTestTask>().configureEach {
+ timeout.set(Duration.ofMinutes(60))
+
+ testLogging {
+ showCauses = true
+ showExceptions = true
+ showStackTraces = true
+ showStandardStreams = true
+ events(
+ TestLogEvent.PASSED,
+ TestLogEvent.FAILED,
+ TestLogEvent.SKIPPED,
+ TestLogEvent.STARTED,
+ TestLogEvent.STANDARD_ERROR,
+ TestLogEvent.STANDARD_OUT,
+ )
+ }
+}
+
+tasks.withType<AbstractCopyTask>().configureEach {
+ includeEmptyDirs = false
+}
+
+val updateTestReportCss by tasks.registering {
+ description = "Hack so the Gradle test reports have dark mode"
+ // the CSS is based on https://github.com/gradle/gradle/pull/12177
+
+ mustRunAfter(tasks.withType<Test>())
+ mustRunAfter(tasks.withType<TestReport>())
+
+ val cssFiles = layout.buildDirectory.asFileTree.matching {
+ include("reports/**/css/base-style.css")
+ include("reports/**/css/style.css")
+ }
+
+ outputs.files(cssFiles.files)
+
+ doLast {
+ cssFiles.forEach { cssFile ->
+ val fileContent = cssFile.readText()
+
+ if ("/* Dark mode */" in fileContent) {
+ return@forEach
+ } else {
+ when (cssFile.name) {
+ "base-style.css" -> cssFile.writeText(
+ fileContent + """
+
+ /* Dark mode */
+ @media (prefers-color-scheme: dark) {
+ html {
+ background: black;
+ }
+ body, a, a:visited {
+ color: #E7E7E7FF;
+ }
+ #footer, #footer a {
+ color: #cacaca;
+ }
+ ul.tabLinks li {
+ border: solid 1px #cacaca;
+ background-color: #151515;
+ }
+ ul.tabLinks li:hover {
+ background-color: #383838;
+ }
+ ul.tabLinks li.selected {
+ background-color: #002d32;
+ border-color: #007987;
+ }
+ div.tab th, div.tab table {
+ border-bottom: solid #d0d0d0 1px;
+ }
+ span.code pre {
+ background-color: #0a0a0a;
+ border: solid 1px #5f5f5f;
+ }
+ }
+ """.trimIndent()
+ )
+
+ "style.css" -> cssFile.writeText(
+ fileContent + """
+
+ /* Dark mode */
+ @media (prefers-color-scheme: dark) {
+ .breadcrumbs, .breadcrumbs a {
+ color: #9b9b9b;
+ }
+ #successRate, .summaryGroup {
+ border: solid 2px #d0d0d0;
+ }
+ .success, .success a {
+ color: #7fff7f;
+ }
+ div.success, #successRate.success {
+ background-color: #001c00;
+ border-color: #7fff7f;
+ }
+ .failures, .failures a {
+ color: #a30000;
+ }
+ .skipped, .skipped a {
+ color: #a26d13;
+ }
+ div.failures, #successRate.failures {
+ background-color: #170000;
+ border-color: #a30000;
+ }
+ }
+ """.trimIndent()
+ )
+ }
+ }
+ }
+ }
+}
+
+tasks.withType<Test>().configureEach {
+ finalizedBy(updateTestReportCss)
+}
+
+tasks.withType<TestReport>().configureEach {
+ finalizedBy(updateTestReportCss)
+}
+
+tasks.matching { it.name == "validatePlugins" }.configureEach {
+ // prevent warning
+ // Task ':validatePlugins' uses this output of task ':updateTestReportCss' without declaring an explicit or implicit dependency.
+ mustRunAfter(updateTestReportCss)
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts
new file mode 100644
index 00000000..69e384e1
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokka-source-downloader.gradle.kts
@@ -0,0 +1,68 @@
+package buildsrc.conventions
+
+import buildsrc.settings.DokkaSourceDownloaderSettings
+import buildsrc.utils.asConsumer
+import buildsrc.utils.asProvider
+import buildsrc.utils.dropDirectories
+import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE
+import org.gradle.kotlin.dsl.support.serviceOf
+
+plugins {
+ id("buildsrc.conventions.base")
+}
+
+val dsdExt: DokkaSourceDownloaderSettings = extensions.create<DokkaSourceDownloaderSettings>(
+ DokkaSourceDownloaderSettings.EXTENSION_NAME
+)
+
+val kotlinDokkaSource by configurations.creating<Configuration> {
+ asConsumer()
+ attributes {
+ attribute(USAGE_ATTRIBUTE, objects.named("externals-dokka-src"))
+ }
+}
+
+val kotlinDokkaSourceElements by configurations.registering {
+ asProvider()
+ attributes {
+ attribute(USAGE_ATTRIBUTE, objects.named("externals-dokka-src"))
+ }
+}
+
+dependencies {
+ kotlinDokkaSource(dsdExt.dokkaVersion.map { "kotlin:dokka:$it@zip" })
+}
+
+val prepareDokkaSource by tasks.registering(Sync::class) {
+ group = "dokka setup"
+ description = "Download & unpack Kotlin Dokka source code"
+
+ inputs.property("dokkaVersion", dsdExt.dokkaVersion).optional(false)
+
+ val archives = serviceOf<ArchiveOperations>()
+
+ from(
+ kotlinDokkaSource.incoming
+ .artifacts
+ .resolvedArtifacts
+ .map { artifacts ->
+ artifacts.map { archives.zipTree(it.file) }
+ }
+ ) {
+ // drop the first dir (dokka-$version)
+ eachFile {
+ relativePath = relativePath.dropDirectories(1)
+ }
+ }
+
+ into(temporaryDir)
+
+ exclude(
+ "*.github",
+ "*.gradle",
+ "**/gradlew",
+ "**/gradlew.bat",
+ "**/gradle/wrapper/gradle-wrapper.jar",
+ "**/gradle/wrapper/gradle-wrapper.properties",
+ )
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts
new file mode 100644
index 00000000..5c2c45fa
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects-base.gradle.kts
@@ -0,0 +1,27 @@
+package buildsrc.conventions
+
+import buildsrc.utils.asConsumer
+import buildsrc.utils.asProvider
+
+plugins {
+ id("buildsrc.conventions.base")
+}
+
+
+val exampleProjectsAttribute: Attribute<String> =
+ Attribute.of("example-projects", String::class.java)
+
+dependencies.attributesSchema {
+ attribute(exampleProjectsAttribute)
+}
+
+
+val exampleProjects by configurations.registering {
+ asConsumer()
+ attributes { attribute(exampleProjectsAttribute, "dokka") }
+}
+
+val exampleProjectsElements by configurations.registering {
+ asProvider()
+ attributes { attribute(exampleProjectsAttribute, "dokka") }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts
new file mode 100644
index 00000000..c6994a83
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/dokkatoo-example-projects.gradle.kts
@@ -0,0 +1,160 @@
+package buildsrc.conventions
+
+import buildsrc.settings.*
+import buildsrc.tasks.*
+import buildsrc.utils.*
+
+plugins {
+ id("buildsrc.conventions.base")
+ id("buildsrc.conventions.dokka-source-downloader")
+ id("buildsrc.conventions.maven-publish-test")
+ id("buildsrc.conventions.dokkatoo-example-projects-base")
+}
+
+val mavenPublishTestExtension = extensions.getByType<MavenPublishTestSettings>()
+val dokkaTemplateProjectSettings =
+ extensions.create<DokkaTemplateProjectSettings>(
+ DokkaTemplateProjectSettings.EXTENSION_NAME,
+ { project.copySpec() }
+ ).apply {
+ this.destinationBaseDir.convention(layout.projectDirectory)
+ }
+
+val prepareDokkaSource by tasks.existing(Sync::class)
+
+dokkaTemplateProjectSettings.dokkaSourceDir.convention(
+ prepareDokkaSource.flatMap {
+ layout.dir(providers.provider {
+ it.destinationDir
+ })
+ }
+)
+
+tasks.withType<SetupDokkaProjects>().configureEach {
+ dependsOn(prepareDokkaSource)
+
+ dokkaSourceDir.convention(dokkaTemplateProjectSettings.dokkaSourceDir)
+ destinationBaseDir.convention(dokkaTemplateProjectSettings.destinationBaseDir)
+
+ templateProjects.addAllLater(provider {
+ dokkaTemplateProjectSettings.templateProjects
+ })
+}
+
+val setupDokkaTemplateProjects by tasks.registering(SetupDokkaProjects::class)
+
+fun createDokkatooExampleProjectsSettings(
+ projectDir: Directory = project.layout.projectDirectory
+): DokkatooExampleProjectsSettings {
+ return extensions.create<DokkatooExampleProjectsSettings>(
+ DokkatooExampleProjectsSettings.EXTENSION_NAME
+ ).apply {
+
+ // find all Gradle settings files
+ val settingsFiles = projectDir.asFileTree
+ .matching {
+ include(
+ "**/*dokkatoo*/**/settings.gradle.kts",
+ "**/*dokkatoo*/**/settings.gradle",
+ )
+ }.files
+
+ // for each settings file, create a DokkatooExampleProjectSpec
+ settingsFiles.forEach {
+ val destinationDir = it.parentFile
+ val name = destinationDir.toRelativeString(projectDir.asFile).toAlphaNumericCamelCase()
+ exampleProjects.register(name) {
+ this.exampleProjectDir.set(destinationDir)
+ }
+ }
+
+ exampleProjects.configureEach {
+ gradlePropertiesContent.add(
+ mavenPublishTestExtension.testMavenRepoPath.map { testMavenRepoPath ->
+ "testMavenRepo=$testMavenRepoPath"
+ }
+ )
+ }
+ }
+}
+
+val dokkatooExampleProjectsSettings = createDokkatooExampleProjectsSettings()
+
+val updateDokkatooExamplesGradleProperties by tasks.registering(
+ UpdateDokkatooExampleProjects::class
+) {
+ group = DokkatooExampleProjectsSettings.TASK_GROUP
+
+ mustRunAfter(tasks.withType<SetupDokkaProjects>())
+
+ exampleProjects.addAllLater(providers.provider {
+ dokkatooExampleProjectsSettings.exampleProjects
+ })
+}
+
+val dokkatooVersion = provider { project.version.toString() }
+
+val updateDokkatooExamplesBuildFiles by tasks.registering {
+ group = DokkatooExampleProjectsSettings.TASK_GROUP
+ description = "Update the Gradle build files in the Dokkatoo examples"
+
+ outputs.upToDateWhen { false }
+
+ mustRunAfter(tasks.withType<SetupDokkaProjects>())
+ shouldRunAfter(updateDokkatooExamplesGradleProperties)
+
+ val dokkatooVersion = dokkatooVersion
+
+ val dokkatooDependencyVersionMatcher = """
+ \"dev\.adamko\.dokkatoo\:dokkatoo\-plugin\:([^"]+?)\"
+ """.trimIndent().toRegex()
+
+ val dokkatooPluginVersionMatcher = """
+ id[^"]+?"dev\.adamko\.dokkatoo".+?version "([^"]+?)"
+ """.trimIndent().toRegex()
+
+ val gradleBuildFiles =
+ layout.projectDirectory.asFileTree
+ .matching {
+ include(
+ "**/*dokkatoo*/**/build.gradle.kts",
+ "**/*dokkatoo*/**/build.gradle",
+ )
+ }.elements
+ outputs.files(gradleBuildFiles)
+
+ doLast {
+ gradleBuildFiles.get().forEach { fileLocation ->
+ val file = fileLocation.asFile
+ if (file.exists()) {
+ file.writeText(
+ file.readText()
+ .replace(dokkatooPluginVersionMatcher) {
+ val oldVersion = it.groupValues[1]
+ it.value.replace(oldVersion, dokkatooVersion.get())
+ }
+ .replace(dokkatooDependencyVersionMatcher) {
+ val oldVersion = it.groupValues[1]
+ it.value.replace(oldVersion, dokkatooVersion.get())
+ }
+ )
+ }
+ }
+ }
+}
+
+
+val updateDokkatooExamples by tasks.registering {
+ group = DokkatooExampleProjectsSettings.TASK_GROUP
+ description = "lifecycle task for all '${DokkatooExampleProjectsSettings.TASK_GROUP}' tasks"
+ dependsOn(
+ setupDokkaTemplateProjects,
+ updateDokkatooExamplesGradleProperties,
+ updateDokkatooExamplesBuildFiles,
+ )
+}
+
+tasks.assemble {
+ dependsOn(updateDokkatooExamples)
+ dependsOn(setupDokkaTemplateProjects)
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts
new file mode 100644
index 00000000..1d9fc43b
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/gradle-plugin-variants.gradle.kts
@@ -0,0 +1,44 @@
+package buildsrc.conventions
+
+import org.gradle.api.attributes.plugin.GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE
+
+plugins {
+ id("buildsrc.conventions.base")
+ `java-gradle-plugin`
+}
+
+fun registerGradleVariant(name: String, gradleVersion: String) {
+ val variantSources = sourceSets.create(name)
+
+ java {
+ registerFeature(variantSources.name) {
+ usingSourceSet(variantSources)
+ capability("${project.group}", "${project.name}", "${project.version}")
+
+ withJavadocJar()
+ withSourcesJar()
+ }
+ }
+
+ configurations
+ .matching { it.isCanBeConsumed && it.name.startsWith(variantSources.name) }
+ .configureEach {
+ attributes {
+ attribute(GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, objects.named(gradleVersion))
+ }
+ }
+
+ tasks.named<Copy>(variantSources.processResourcesTaskName) {
+ val copyPluginDescriptors = rootSpec.addChild()
+ copyPluginDescriptors.into("META-INF/gradle-plugins")
+// copyPluginDescriptors.into(tasks.pluginDescriptors.flatMap { it.outputDirectory })
+ copyPluginDescriptors.from(tasks.pluginDescriptors)
+ }
+
+ dependencies {
+ add(variantSources.compileOnlyConfigurationName, gradleApi())
+ }
+}
+
+registerGradleVariant("gradle7", "7.6")
+registerGradleVariant("gradle8", "8.0")
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts
new file mode 100644
index 00000000..203b80f2
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/java-base.gradle.kts
@@ -0,0 +1,19 @@
+package buildsrc.conventions
+
+import org.gradle.api.JavaVersion
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.`java-base`
+
+plugins {
+ id("buildsrc.conventions.base")
+ `java`
+}
+
+extensions.getByType<JavaPluginExtension>().apply {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(11))
+ }
+ withSourcesJar()
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts
new file mode 100644
index 00000000..4174088a
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/kotlin-gradle-plugin.gradle.kts
@@ -0,0 +1,37 @@
+package buildsrc.conventions
+
+plugins {
+ id("buildsrc.conventions.base")
+ id("buildsrc.conventions.java-base")
+ id("org.gradle.kotlin.kotlin-dsl")
+ id("com.gradle.plugin-publish")
+}
+
+tasks.validatePlugins {
+ enableStricterValidation.set(true)
+}
+
+val createJavadocJarReadme by tasks.registering(Sync::class) {
+ description = "generate a readme.txt for the Javadoc JAR"
+ from(
+ resources.text.fromString(
+ """
+ This Javadoc JAR is intentionally empty.
+
+ For documentation, see the sources JAR or https://github.com/adamko-dev/dokkatoo/
+
+ """.trimIndent()
+ )
+ ) {
+ rename { "readme.txt" }
+ }
+ into(temporaryDir)
+}
+
+
+// The Gradle Publish Plugin enables the Javadoc JAR in afterEvaluate, so find it lazily
+tasks.withType<Jar>()
+ .matching { it.name == "javadocJar" }
+ .configureEach {
+ from(createJavadocJarReadme)
+ }
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts
new file mode 100644
index 00000000..38678b5b
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publish-test.gradle.kts
@@ -0,0 +1,93 @@
+package buildsrc.conventions
+
+import buildsrc.settings.MavenPublishTestSettings
+import buildsrc.utils.*
+
+
+/** Utility for publishing a project to a local Maven directory for use in integration tests. */
+
+plugins {
+ base
+}
+
+val Gradle.rootGradle: Gradle get() = generateSequence(gradle) { it.parent }.last()
+
+val mavenPublishTestExtension = extensions.create<MavenPublishTestSettings>(
+ "mavenPublishTest",
+ gradle.rootGradle.rootProject.layout.buildDirectory.dir("test-maven-repo"),
+)
+
+
+val publishToTestMavenRepo by tasks.registering {
+ group = PublishingPlugin.PUBLISH_TASK_GROUP
+ description = "Publishes all Maven publications to the test Maven repository."
+}
+
+
+plugins.withType<MavenPublishPlugin>().all {
+ extensions
+ .getByType<PublishingExtension>()
+ .publications
+ .withType<MavenPublication>().all publication@{
+ val publicationName = this@publication.name
+ val installTaskName = "publish${publicationName.uppercaseFirstChar()}PublicationToTestMavenRepo"
+
+ // Register a publication task for each publication.
+ // Use PublishToMavenLocal, because the PublishToMavenRepository task will *always* create
+ // a new jar, even if nothing has changed, and append a timestamp, which results in a large
+ // directory and tasks are never up-to-date.
+ // PublishToMavenLocal does not append a timestamp, so the target directory is smaller, and
+ // up-to-date checks work.
+ val installTask = tasks.register<PublishToMavenLocal>(installTaskName) {
+ description = "Publishes Maven publication '$publicationName' to the test Maven repository."
+ group = PublishingPlugin.PUBLISH_TASK_GROUP
+ outputs.cacheIf { true }
+ publication = this@publication
+ val destinationDir = mavenPublishTestExtension.testMavenRepo.get().asFile
+ inputs.property("testMavenRepoTempDir", destinationDir.invariantSeparatorsPath)
+ doFirst {
+ /**
+ * `maven.repo.local` will set the destination directory for this [PublishToMavenLocal] task.
+ *
+ * @see org.gradle.api.internal.artifacts.mvnsettings.DefaultLocalMavenRepositoryLocator.getLocalMavenRepository
+ */
+ System.setProperty("maven.repo.local", destinationDir.absolutePath)
+ }
+ }
+
+ publishToTestMavenRepo.configure {
+ dependsOn(installTask)
+ }
+
+ tasks.check {
+ mustRunAfter(installTask)
+ }
+ }
+}
+
+
+val testMavenPublication by configurations.registering {
+ asConsumer()
+ attributes {
+ attribute(MavenPublishTestSettings.attribute, "testMavenRepo")
+ }
+}
+
+val testMavenPublicationElements by configurations.registering {
+ asProvider()
+ extendsFrom(testMavenPublication.get())
+ attributes {
+ attribute(MavenPublishTestSettings.attribute, "testMavenRepo")
+ }
+ outgoing {
+ artifact(mavenPublishTestExtension.testMavenRepo) {
+ builtBy(publishToTestMavenRepo)
+ }
+ }
+}
+
+dependencies {
+ attributesSchema {
+ attribute(MavenPublishTestSettings.attribute)
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts
new file mode 100644
index 00000000..7af7b69f
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/conventions/maven-publishing.gradle.kts
@@ -0,0 +1,137 @@
+package buildsrc.conventions
+
+import buildsrc.settings.MavenPublishingSettings
+
+plugins {
+ `maven-publish`
+ signing
+}
+
+val mavenPublishing =
+ extensions.create<MavenPublishingSettings>(MavenPublishingSettings.EXTENSION_NAME, project)
+
+
+//region POM convention
+publishing {
+ publications.withType<MavenPublication>().configureEach {
+ pom {
+ name.convention("Dokkatoo")
+ description.convention("Dokkatoo is a Gradle plugin that generates documentation for your Kotlin projects")
+ url.convention("https://github.com/adamko-dev/dokkatoo")
+
+ scm {
+ connection.convention("scm:git:https://github.com/adamko-dev/dokkatoo")
+ developerConnection.convention("scm:git:https://github.com/adamko-dev/dokkatoo")
+ url.convention("https://github.com/adamko-dev/dokkatoo")
+ }
+
+ licenses {
+ license {
+ name.convention("Apache-2.0")
+ url.convention("https://www.apache.org/licenses/LICENSE-2.0.txt")
+ }
+ }
+
+ developers {
+ developer {
+ email.set("adam@adamko.dev")
+ }
+ }
+ }
+ }
+}
+//endregion
+
+
+//region GitHub branch publishing
+publishing {
+ repositories {
+ maven(mavenPublishing.githubPublishDir) {
+ name = "GitHubPublish"
+ }
+ }
+}
+//endregion
+
+
+//region Maven Central publishing/signing
+publishing {
+ repositories {
+ val mavenCentralUsername = mavenPublishing.mavenCentralUsername.orNull
+ val mavenCentralPassword = mavenPublishing.mavenCentralPassword.orNull
+ if (!mavenCentralUsername.isNullOrBlank() && !mavenCentralPassword.isNullOrBlank()) {
+ maven(mavenPublishing.sonatypeReleaseUrl) {
+ name = "SonatypeRelease"
+ credentials {
+ username = mavenCentralUsername
+ password = mavenCentralPassword
+ }
+ }
+ }
+ }
+
+ // com.gradle.plugin-publish automatically adds a Javadoc jar
+}
+
+signing {
+ logger.info("maven-publishing.gradle.kts enabled signing for ${project.path}")
+
+ val keyId = mavenPublishing.signingKeyId.orNull
+ val key = mavenPublishing.signingKey.orNull
+ val password = mavenPublishing.signingPassword.orNull
+
+ if (!keyId.isNullOrBlank() && !key.isNullOrBlank() && !password.isNullOrBlank()) {
+ useInMemoryPgpKeys(keyId, key, password)
+ }
+
+ setRequired({
+ gradle.taskGraph.allTasks.filterIsInstance<PublishToMavenRepository>().any {
+ it.repository.name == "SonatypeRelease"
+ }
+ })
+}
+
+//afterEvaluate {
+// com.gradle.plugin-publish automatically signs tasks in a weird way, that stops this from working:
+// signing {
+// sign(publishing.publications)
+// }
+//}
+//endregion
+
+
+//region Fix Gradle warning about signing tasks using publishing task outputs without explicit dependencies
+// https://youtrack.jetbrains.com/issue/KT-46466 https://github.com/gradle/gradle/issues/26091
+tasks.withType<AbstractPublishToMaven>().configureEach {
+ val signingTasks = tasks.withType<Sign>()
+ mustRunAfter(signingTasks)
+}
+//endregion
+
+
+//region publishing logging
+tasks.withType<AbstractPublishToMaven>().configureEach {
+ val publicationGAV = provider { publication?.run { "$group:$artifactId:$version" } }
+ doLast("log publication GAV") {
+ if (publicationGAV.isPresent) {
+ logger.lifecycle("[task: ${path}] ${publicationGAV.get()}")
+ }
+ }
+}
+//endregion
+
+
+//region IJ workarounds
+// manually define the Kotlin DSL accessors because IntelliJ _still_ doesn't load them properly
+fun Project.publishing(configure: PublishingExtension.() -> Unit): Unit =
+ extensions.configure(configure)
+
+val Project.publishing: PublishingExtension
+ get() = extensions.getByType()
+
+fun Project.signing(configure: SigningExtension.() -> Unit): Unit =
+ extensions.configure(configure)
+
+val Project.signing: SigningExtension
+ get() = extensions.getByType()
+//endregion
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt
new file mode 100644
index 00000000..c3f9906c
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaSourceDownloaderSettings.kt
@@ -0,0 +1,13 @@
+package buildsrc.settings
+
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.api.provider.Property
+
+abstract class DokkaSourceDownloaderSettings : ExtensionAware {
+
+ abstract val dokkaVersion: Property<String>
+
+ companion object {
+ const val EXTENSION_NAME = "dokkaSourceDownload"
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt
new file mode 100644
index 00000000..7bacafb9
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkaTemplateProjectSettings.kt
@@ -0,0 +1,96 @@
+package buildsrc.settings
+
+import buildsrc.utils.adding
+import buildsrc.utils.domainObjectContainer
+import buildsrc.utils.toAlphaNumericCamelCase
+import javax.inject.Inject
+import org.gradle.api.Named
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.CopySpec
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.SetProperty
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.kotlin.dsl.*
+
+private typealias TemplateProjectsContainer = NamedDomainObjectContainer<DokkaTemplateProjectSettings.DokkaTemplateProjectSpec>
+
+abstract class DokkaTemplateProjectSettings @Inject constructor(
+ private val objects: ObjectFactory,
+ private val copySpecs: () -> CopySpec
+) : ExtensionAware {
+
+ /** Directory that will contain the projects downloaded from the Dokka source code. */
+ abstract val dokkaSourceDir: DirectoryProperty
+
+ abstract val destinationBaseDir: DirectoryProperty
+
+ internal val templateProjects: TemplateProjectsContainer =
+ // create an extension so Gradle will generate DSL accessors
+ extensions.adding("templateProjects", objects.domainObjectContainer { name ->
+ objects.newInstance<DokkaTemplateProjectSpec>(name, copySpecs())
+ })
+
+ /**
+ * Copy a directory from the Dokka source project into a local directory.
+ *
+ * @param[source] Source dir, relative to [templateProjectsDir]
+ * @param[destination] Destination dir, relative to [destinationBaseDir]
+ */
+ fun register(
+ source: String,
+ destination: String,
+ configure: DokkaTemplateProjectSpec.() -> Unit = {},
+ ) {
+ val name = source.toAlphaNumericCamelCase()
+ templateProjects.register(name) {
+ this.sourcePath.set(source)
+ this.destinationPath.set(destination)
+ configure()
+ }
+ }
+
+ fun configureEach(configure: DokkaTemplateProjectSpec.() -> Unit) {
+ templateProjects.configureEach(configure)
+ }
+
+ /**
+ * Details for how to copy a Dokka template project from the Dokka project to a local directory.
+ */
+ abstract class DokkaTemplateProjectSpec @Inject constructor(
+ private val named: String,
+ @get:Internal
+ internal val copySpec: CopySpec,
+ ) : Named {
+
+ @get:Input
+ abstract val sourcePath: Property<String>
+
+ @get:Input
+ @get:Optional
+ abstract val destinationPath: Property<String>
+
+ @get:Input
+ abstract val additionalPaths: SetProperty<String>
+
+ @get:InputFiles
+ abstract val additionalFiles: ConfigurableFileCollection
+
+ fun configureCopy(configure: CopySpec.() -> Unit) {
+ copySpec.configure()
+ }
+
+ @Input
+ override fun getName(): String = named
+ }
+
+ companion object {
+ const val EXTENSION_NAME = "dokkaTemplateProjects"
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt
new file mode 100644
index 00000000..a3124904
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/DokkatooExampleProjectsSettings.kt
@@ -0,0 +1,62 @@
+package buildsrc.settings
+
+import buildsrc.utils.adding
+import buildsrc.utils.domainObjectContainer
+import javax.inject.Inject
+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.ListProperty
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+
+/**
+ * Settings for the [buildsrc.conventions.Dokkatoo_example_projects_gradle] convention plugin
+ */
+abstract class DokkatooExampleProjectsSettings @Inject constructor(
+ objects: ObjectFactory,
+) : ExtensionAware {
+
+ val exampleProjects: NamedDomainObjectContainer<DokkatooExampleProjectSpec> =
+ // create an extension so Gradle will generate DSL accessors
+ extensions.adding("exampleProjects", objects.domainObjectContainer())
+
+ abstract class DokkatooExampleProjectSpec(
+ private val name: String
+ ): Named {
+
+ /** The `gradle.properties` file of the example project */
+ @get:OutputFile
+ val gradlePropertiesFile: Provider<RegularFile>
+ get() = exampleProjectDir.file("gradle.properties")
+
+ /** The directory that contains the example project */
+ @get:Internal
+ abstract val exampleProjectDir: DirectoryProperty
+
+ /**
+ * Content to add to the `gradle.properties` file.
+ *
+ * Elements may span multiple lines.
+ *
+ * Elements will be sorted before appending to the file (to improve caching & reproducibility).
+ */
+ @get:Input
+ @get:Optional
+ abstract val gradlePropertiesContent: ListProperty<String>
+
+ @Input
+ override fun getName(): String = name
+ }
+
+ companion object {
+ const val TASK_GROUP = "dokkatoo examples"
+ const val EXTENSION_NAME = "dokkatooExampleProjects"
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt
new file mode 100644
index 00000000..0a701986
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishTestSettings.kt
@@ -0,0 +1,19 @@
+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 {
+ val testMavenRepoPath: Provider<String> = testMavenRepo.map { it.asFile.invariantSeparatorsPath }
+
+ companion object {
+ val attribute = Attribute.of("maven-publish-test", String::class.java)
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt
new file mode 100644
index 00000000..9ec28faa
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/settings/MavenPublishingSettings.kt
@@ -0,0 +1,68 @@
+package buildsrc.settings
+
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.Project
+import org.gradle.api.provider.Provider
+import org.gradle.api.provider.ProviderFactory
+import org.gradle.kotlin.dsl.*
+
+
+/**
+ * Settings for the [buildsrc.conventions.Maven_publish_test_gradle] convention plugin.
+ */
+abstract class MavenPublishingSettings @Inject constructor(
+ private val project: Project,
+ private val providers: ProviderFactory,
+) {
+
+ private val isReleaseVersion: Provider<Boolean> =
+ providers.provider { !project.version.toString().endsWith("-SNAPSHOT") }
+
+ val sonatypeReleaseUrl: Provider<String> =
+ isReleaseVersion.map { isRelease ->
+ if (isRelease) {
+ "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
+ } else {
+ "https://s01.oss.sonatype.org/content/repositories/snapshots/"
+ }
+ }
+
+ val mavenCentralUsername: Provider<String> =
+ d2Prop("mavenCentralUsername")
+ .orElse(providers.environmentVariable("MAVEN_SONATYPE_USERNAME"))
+ val mavenCentralPassword: Provider<String> =
+ d2Prop("mavenCentralPassword")
+ .orElse(providers.environmentVariable("MAVEN_SONATYPE_PASSWORD"))
+
+ val signingKeyId: Provider<String> =
+ d2Prop("signing.keyId")
+ .orElse(providers.environmentVariable("MAVEN_SONATYPE_SIGNING_KEY_ID"))
+ val signingKey: Provider<String> =
+ d2Prop("signing.key")
+ .orElse(providers.environmentVariable("MAVEN_SONATYPE_SIGNING_KEY"))
+ val signingPassword: Provider<String> =
+ d2Prop("signing.password")
+ .orElse(providers.environmentVariable("MAVEN_SONATYPE_SIGNING_PASSWORD"))
+
+ val githubPublishDir: Provider<File> =
+ providers.environmentVariable("GITHUB_PUBLISH_DIR").map { File(it) }
+
+ private fun d2Prop(name: String): Provider<String> =
+ providers.gradleProperty("org.jetbrains.dokka.dokkatoo.$name")
+
+ private fun <T : Any> d2Prop(name: String, convert: (String) -> T): Provider<T> =
+ d2Prop(name).map(convert)
+
+ companion object {
+ const val EXTENSION_NAME = "mavenPublishing"
+
+ /** Retrieve the [KayrayBuildProperties] extension. */
+ internal val Project.mavenPublishing: MavenPublishingSettings
+ get() = extensions.getByType()
+
+ /** Configure the [KayrayBuildProperties] extension. */
+ internal fun Project.mavenPublishing(configure: MavenPublishingSettings.() -> Unit) =
+ extensions.configure(configure)
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt
new file mode 100644
index 00000000..d473d287
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/SetupDokkaProjects.kt
@@ -0,0 +1,73 @@
+package buildsrc.tasks
+
+import buildsrc.settings.DokkaTemplateProjectSettings.DokkaTemplateProjectSpec
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.FileCollection
+import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.file.ProjectLayout
+import org.gradle.api.provider.ProviderFactory
+import org.gradle.api.tasks.*
+
+abstract class SetupDokkaProjects @Inject constructor(
+ private val fs: FileSystemOperations,
+ private val layout: ProjectLayout,
+ private val providers: ProviderFactory,
+) : DefaultTask() {
+
+ @get:OutputDirectories
+ val destinationDirs: FileCollection
+ get() = layout.files(
+ destinationBaseDir.map { base ->
+ templateProjects.map { spec -> base.dir(spec.destinationPath) }
+ }
+ )
+
+ @get:Internal // tracked by destinationDirs
+ abstract val destinationBaseDir: DirectoryProperty
+
+ @get:Nested
+ abstract val templateProjects: NamedDomainObjectContainer<DokkaTemplateProjectSpec>
+
+ @get:InputDirectory
+ abstract val dokkaSourceDir: DirectoryProperty
+
+ @get:InputFiles
+ val additionalFiles: FileCollection
+ get() = layout.files(
+ providers.provider {
+ templateProjects.map { it.additionalFiles }
+ }
+ )
+
+ init {
+ group = "dokka examples"
+ }
+
+ @TaskAction
+ internal fun action() {
+ val dokkaSourceDir = dokkaSourceDir.get()
+ val destinationBaseDir = destinationBaseDir.get()
+ val templateProjects = templateProjects.filter { it.destinationPath.isPresent }
+
+ templateProjects.forEach { spec ->
+ fs.sync {
+ with(spec.copySpec)
+
+ from(dokkaSourceDir.dir(spec.sourcePath))
+
+ from(
+ spec.additionalPaths.get().map { additionalPath ->
+ dokkaSourceDir.asFile.resolve(additionalPath)
+ }
+ )
+
+ from(spec.additionalFiles)
+
+ into(destinationBaseDir.dir(spec.destinationPath))
+ }
+ }
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt
new file mode 100644
index 00000000..7737e098
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/tasks/UpdateDokkatooExampleProjects.kt
@@ -0,0 +1,49 @@
+package buildsrc.tasks
+
+import buildsrc.settings.DokkatooExampleProjectsSettings.DokkatooExampleProjectSpec
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Nested
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Utility for updating the `gradle.properties` of projects used in automated tests.
+ */
+@CacheableTask
+abstract class UpdateDokkatooExampleProjects @Inject constructor(
+ @get:Internal
+ val objects: ObjectFactory
+) : DefaultTask() {
+
+ @get:Nested
+ abstract val exampleProjects: NamedDomainObjectContainer<DokkatooExampleProjectSpec>
+
+ private val taskPath: String = path // renamed for clarity
+
+ @TaskAction
+ fun update() {
+ exampleProjects.forEach { exampleProject ->
+ updateGradleProperties(exampleProject)
+ }
+ }
+
+ private fun updateGradleProperties(exampleProject: DokkatooExampleProjectSpec) {
+
+ val gradlePropertiesContent = exampleProject.gradlePropertiesContent.orNull?.sorted() ?: return
+
+ val content = buildString {
+ appendLine("# DO NOT EDIT - Generated by $taskPath")
+ appendLine()
+
+ gradlePropertiesContent.forEach {
+ appendLine(it)
+ }
+ }
+
+ exampleProject.gradlePropertiesFile.get().asFile.writeText(content)
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt
new file mode 100644
index 00000000..0af662d4
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/gradle.kt
@@ -0,0 +1,118 @@
+package buildsrc.utils
+
+import java.io.File
+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.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.
+ *
+ * ```
+ * isCanBeResolved = false
+ * isCanBeConsumed = true
+ * ```
+ */
+fun Configuration.asProvider(
+ visible: Boolean = true
+) {
+ isVisible = visible
+ isCanBeResolved = false
+ isCanBeConsumed = true
+}
+
+/**
+ * Mark this [Configuration] as one that will consume artifacts from other subprojects (also known as 'resolving')
+ *
+ * ```
+ * isCanBeResolved = true
+ * isCanBeConsumed = false
+ * ```
+ * */
+fun Configuration.asConsumer(
+ visible: Boolean = false
+) {
+ isVisible = visible
+ isCanBeResolved = true
+ isCanBeConsumed = false
+}
+
+
+/** Drop the first [count] directories from the path */
+fun RelativePath.dropDirectories(count: Int): RelativePath =
+ RelativePath(true, *segments.drop(count).toTypedArray())
+
+
+/** Drop the first directory from the path */
+fun RelativePath.dropDirectory(): RelativePath =
+ dropDirectories(1)
+
+
+/** Drop the first directory from the path */
+fun RelativePath.dropDirectoriesWhile(
+ segmentPrediate: (segment: String) -> Boolean
+): RelativePath =
+ RelativePath(
+ true,
+ *segments.dropWhile(segmentPrediate).toTypedArray(),
+ )
+
+
+/**
+ * Don't publish test fixtures (which causes warnings when publishing)
+ *
+ * https://docs.gradle.org/current/userguide/java_testing.html#publishing_test_fixtures
+ */
+fun Project.skipTestFixturesPublications() {
+ val javaComponent = components["java"] as AdhocComponentWithVariants
+ 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)
+ }
+
+/** workaround for the overly verbose replacement for the deprecated [Project.getBuildDir] property */
+val Project.buildDir_: File get() = layout.buildDirectory.get().asFile
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt
new file mode 100644
index 00000000..f93e7683
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/intellij.kt
@@ -0,0 +1,45 @@
+package buildsrc.utils
+
+import org.gradle.api.Project
+import org.gradle.api.file.ProjectLayout
+import org.gradle.plugins.ide.idea.model.IdeaModule
+
+
+/** exclude generated Gradle code, so it doesn't clog up search results */
+fun IdeaModule.excludeGeneratedGradleDsl(layout: ProjectLayout) {
+
+ val generatedSrcDirs = listOf(
+ "kotlin-dsl-accessors",
+ "kotlin-dsl-external-plugin-spec-builders",
+ "kotlin-dsl-plugins",
+ )
+
+ excludeDirs.addAll(
+ layout.projectDirectory.asFile.walk()
+ .filter { it.isDirectory && it.parentFile.name in generatedSrcDirs }
+ .flatMap { file ->
+ file.walk().maxDepth(1).filter { it.isDirectory }.toList()
+ }
+ )
+}
+
+
+/** Sets a logo for project IDEs */
+fun Project.initIdeProjectLogo(
+ svgLogoPath: String
+) {
+ val logoSvg = rootProject.layout.projectDirectory.file(svgLogoPath)
+ val ideaDir = rootProject.layout.projectDirectory.dir(".idea")
+
+ if (
+ logoSvg.asFile.exists()
+ && ideaDir.asFile.exists()
+ && !ideaDir.file("icon.png").asFile.exists()
+ && !ideaDir.file("icon.svg").asFile.exists()
+ ) {
+ copy {
+ from(logoSvg) { rename { "icon.svg" } }
+ into(ideaDir)
+ }
+ }
+}
diff --git a/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt
new file mode 100644
index 00000000..6a0749ce
--- /dev/null
+++ b/dokka-runners/dokkatoo/buildSrc/src/main/kotlin/buildsrc/utils/strings.kt
@@ -0,0 +1,26 @@
+package buildsrc.utils
+
+
+/** Title case the first char of a string */
+internal fun String.uppercaseFirstChar(): String = mapFirstChar(Character::toTitleCase)
+
+
+/** Lowercase the first char of a string */
+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
+
+
+/**
+ * Exclude all non-alphanumeric characters and converts the result into a camelCase string.
+ */
+internal fun String.toAlphaNumericCamelCase(): String =
+ map { if (it.isLetterOrDigit()) it else ' ' }
+ .joinToString("")
+ .split(" ")
+ .filter { it.isNotBlank() }
+ .joinToString("") { it.uppercaseFirstChar() }
+ .lowercaseFirstChar()