Skip to content

Commit

Permalink
KT-64377 Maven dev publish (#3433)
Browse files Browse the repository at this point in the history
Use project-local Maven dev repos for integration testing.

### Impact

Using a Maven Repo is useful for integration tests, because it verifies that the artifacts are published correctly with the expected coordinates. It is also useful for Gradle Plugins, because we can verify that the [plugin marker](https://docs.gradle.org/8.6/userguide/plugins.html#sec:plugin_markers) was correctly published.

### Summary

* Local convention plugin `dokkabuild.dev-maven-publish.gradle.kts` publishes subprojects to a project-local directory.

  Benefits:
  * compatible with: config cache, build cache, isolated projects
  * doesn't use Maven Local, so there's no chance of cross-contamination with outdated artifacts or other projects

* Dynamically inject the local Maven dir:
  * Gradle projects: find/replace `/* %{PROJECT_LOCAL_MAVEN_DIR}% */` with an exclusive Maven repo
  * Maven projects: provide a `settings.xml` with the local plugin repo.

--- 

* Maven publish artifacts to project-local directory. This helps with Gradle Build & Config cache.
* merge cleanup
* change plugin `test-suite-base` -> `jvm-test-suite`
* move registerTestProjectSuite() to top-level function

* move gradle-specific test utils to gradle integration test

* move unused systemVariableProviders.kt back

* move `multimodule-inter-module-links` to test-suite

* register DOKKA_TEST_OUTPUT_PATH as a test task input, and the dir as an output

* rm old import

* make testOutputPath optional

* re-add integrationTest lifecycle task

* fix `it-multimodule-inter-module-links` dir, add property name to templateProjectDir for better error messages

* comment to explain test-suites are independent

* add property names to coroutines/serialization project dirs

* use jvm11 for kotlinx.serialization

* update maven-dev-plugin usage

* revert MaxMetaspaceSize in Coroutines test, and add comment to explain

* environment workaround for gradle/gradle#11534

* update CI build properties

* remove `allWarningsAsErrors = false`

* remove 'check' task dependency

* tidy

* remove `integrationTestPreparation` (it was less useful than I thought), plus misc tidying

* update git patches, and dev-maven injection

* tidying

* fix dokka version in IT projects

* remove unnecessary `.toString()`

* revert change to Dokkatoo util

* move dev maven repo task input registration to util function

* replace `System.getenv("dokka_it_dokka_version")` with `providers.gradleProperty("dokka_it_dokka_version").get()` in all test projects

* update dev-repos after git patches

* update coroutines and serialization patches

* fix coroutines git patch

* fomatting

* bump dev-publish-plugin version

* restore deleted it-multimodule-inter-module-links/gradle.properties

* replace `dev.adamko.dev-publish` with custom implementation that uses `maven.repo.local`

* remove old kotlinx-serialization git submodule

* fix CC compatibility

* fixin' build caching

- only register deterministic repo files as a task input
- cache the output of Gradle integration test tasks

* use `tasks.check {}` dsl accessor

* update IB comment

* update comment regarding Maven Metadata exclusions
  • Loading branch information
adam-enko committed Mar 27, 2024
1 parent 1ed0186 commit ad9b822
Show file tree
Hide file tree
Showing 29 changed files with 768 additions and 426 deletions.
6 changes: 0 additions & 6 deletions build-logic/src/main/kotlin/dokkabuild.base.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,3 @@ tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}

val integrationTestPreparation by tasks.registering {
description =
"lifecycle task for preparing the project for integration tests (for example, publishing to the test Maven repo)"
group = VERIFICATION_GROUP
}
122 changes: 122 additions & 0 deletions build-logic/src/main/kotlin/dokkabuild.dev-maven-publish.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
import dokkabuild.DevMavenPublishExtension
import dokkabuild.DevMavenPublishExtension.Companion.DEV_MAVEN_PUBLISH_EXTENSION_NAME
import dokkabuild.internal.consumable
import dokkabuild.internal.declarable
import dokkabuild.internal.resolvable
import org.gradle.kotlin.dsl.support.uppercaseFirstChar

/**
* Utility for publishing a project to a local Maven directory for use in integration tests.
*
* Using a local directory is beneficial because Maven Local
* [has some flaws](https://docs.gradle.org/8.6/userguide/declaring_repositories.html#sec:case-for-maven-local),
* and we can more tightly control what artifacts are published and are persisted.
*
* It's possible to publish to a local directory using a regular [PublishToMavenRepository] task,
* but when the project has a SNAPSHOT version the output will be timestamped, so Gradle will
* _always_ publish a new artifact. This causes two issues:
*
* - The publication tasks, and any test tasks, will _always_ be out-of-date, even if no code changed.
* - The local directory will endlessly grow in size
* (which can be remedied by running `./gradlew clean`, but this is not ideal)
*
* To overcome this we manually set the system property `maven.repo.local` to a local directory.
* Gradle will respect this property, and publish artifacts to the local directory only when
* they have changed, improving performance.
*/
plugins {
base
}

/**
* Directory for the output of the current subproject's 'publishToMavenLocal'
*/
val currentProjectDevMavenRepo = gradle.rootProject.layout.buildDirectory.dir("dev-maven-repo")

val devMavenPublishAttribute = Attribute.of("dev-maven-publish", String::class.java)

dependencies {
attributesSchema {
attribute(devMavenPublishAttribute)
}
}

val publishToDevMavenRepo by tasks.registering {
description = "Publishes all Maven publications to the dev Maven repository."
group = PublishingPlugin.PUBLISH_TASK_GROUP
}


plugins.withType<MavenPublishPlugin>().all {
extensions
.getByType<PublishingExtension>()
.publications
.withType<MavenPublication>().all publication@{
val publicationName = this@publication.name
val installTaskName = "publish${publicationName.uppercaseFirstChar()}PublicationToDevMavenRepo"

// Register a new publication task for each publication.
val installTask = tasks.register<PublishToMavenLocal>(installTaskName) {
description = "Publishes Maven publication '$publicationName' to the test Maven repository."
group = PublishingPlugin.PUBLISH_TASK_GROUP
publication = this@publication

val destinationDir = currentProjectDevMavenRepo.get().asFile
inputs.property("currentProjectDevMavenRepoPath", 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)
}
}

publishToDevMavenRepo.configure {
dependsOn(installTask)
}

tasks.check {
mustRunAfter(installTask)
}
}
}


val devPublication: Configuration by configurations.creating {
description = "Depend on project-local Dev Maven repositories"
declarable()
}

val devPublicationResolver: Configuration by configurations.creating {
description = "Resolve project-local Dev Maven repositories"
resolvable()
extendsFrom(devPublication)
attributes {
attribute(devMavenPublishAttribute, "devMavenRepo")
}
}

val devPublicationConsumable: Configuration by configurations.creating {
description = "Provide project-local Dev Maven repositories dependencies"
consumable()
attributes {
attribute(devMavenPublishAttribute, "devMavenRepo")
}
outgoing {
artifact(currentProjectDevMavenRepo) {
builtBy(publishToDevMavenRepo)
}
}
}

val devMavenPublishExtension = extensions.create<DevMavenPublishExtension>(
DEV_MAVEN_PUBLISH_EXTENSION_NAME,
// fetch Dev Maven Repos from the dependencies
devPublicationResolver.incoming.files,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
*/

plugins {
id("dokkabuild.base")
`maven-publish`
signing
id("dokkabuild.dev-maven-publish")
}

publishing {
Expand Down Expand Up @@ -33,13 +35,6 @@ publishing {
password = System.getenv("DOKKA_SPACE_PACKAGES_SECRET")
}
}
// Publish to a project-local Maven directory, for verification. To test, run:
// ./gradlew publishAllPublicationsToProjectLocalRepository
// and check $rootDir/build/maven-project-local
maven {
name = "projectLocal"
url = uri(rootProject.layout.buildDirectory.dir("maven-project-local"))
}
}

publications.withType<MavenPublication>().configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ val integrationTest by tasks.registering(NonCacheableIntegrationTest::class) {
}

environment("isExhaustive", dokkaBuild.integrationTestExhaustive.get())

testLogging {
exceptionFormat = TestExceptionFormat.FULL
events(TestLogEvent.SKIPPED, TestLogEvent.FAILED)
Expand Down
59 changes: 59 additions & 0 deletions build-logic/src/main/kotlin/dokkabuild/DevMavenPublishExtension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package dokkabuild

import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.PathSensitivity.RELATIVE
import org.gradle.process.JavaForkOptions

abstract class DevMavenPublishExtension(
/**
* Resolves Dev Maven repos from the current project's `devPublication` dependencies.
*
* Must only contain directories.
*/
private val devMavenRepositories: FileCollection,
) {

/**
* Files suitable for registering as a task input (as in, the files are reproducible-build compatible).
*/
private val devMavenRepositoriesInputFiles: FileTree = devMavenRepositories
.asFileTree
.matching {
// Exclude Maven Metadata files because they contain timestamps, meaning tasks that use
// devMavenRepositories as an input will never be up-to-date.
// The Gradle Module Metadata contains the same information (and more),
// so the Maven metadata is redundant.
exclude("**/maven-metadata*.xml")
}

/**
* Configures [task] to register [devMavenRepositories] as a task input,
* and (if possible) adds `devMavenRepository` as a [JavaForkOptions.systemProperty].
*/
fun configureTask(task: Task) {
task.inputs.files(devMavenRepositoriesInputFiles)
.withPropertyName("devMavenPublish.devMavenRepositoriesInputFiles")
.withPathSensitivity(RELATIVE)

task.dependsOn(devMavenRepositories)

if (task is JavaForkOptions) {
task.doFirst("devMavenRepositories systemProperty") {
// workaround https://github.com/gradle/gradle/issues/24267
task.systemProperty(
"devMavenRepositories",
devMavenRepositories.joinToString(",") { it.canonicalFile.invariantSeparatorsPath }
)
}
}
}

companion object {
const val DEV_MAVEN_PUBLISH_EXTENSION_NAME = "devMavenPublish"
}
}
61 changes: 61 additions & 0 deletions build-logic/src/main/kotlin/dokkabuild/internal/gradleUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package dokkabuild.internal

import org.gradle.api.artifacts.Configuration

/**
* Mark this [Configuration] as one that will be consumed by other subprojects.
*
* ```
* isCanBeResolved = false
* isCanBeConsumed = true
* isCanBeDeclared = false
* ```
*/
fun Configuration.consumable(
visible: Boolean = false
) {
isVisible = visible
isCanBeResolved = false
isCanBeConsumed = true
isCanBeDeclared = false
}

/**
* Mark this [Configuration] as one that will consume artifacts from other subprojects (also known as 'resolving')
*
* ```
* isCanBeResolved = true
* isCanBeConsumed = false
* isCanBeDeclared = false
* ```
*/
fun Configuration.resolvable(
visible: Boolean = false
) {
isVisible = visible
isCanBeResolved = true
isCanBeConsumed = false
isCanBeDeclared = false
}

/**
* Mark this [Configuration] as one that will be used to declare dependencies.
*
* ```
* isCanBeResolved = false
* isCanBeConsumed = false
* isCanBeDeclared = true
* ```
*/
fun Configuration.declarable(
visible: Boolean = false
) {
isVisible = visible
isCanBeResolved = false
isCanBeConsumed = false
isCanBeDeclared = true
}
45 changes: 35 additions & 10 deletions dokka-integration-tests/gradle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Disabled

plugins {
id("dokkabuild.kotlin-jvm")
id("dokkabuild.dev-maven-publish")
`jvm-test-suite`
`java-test-fixtures`
}
Expand All @@ -27,6 +28,30 @@ dependencies {
api(libs.junit.jupiterParams)

api(gradleTestKit())

val dokkaVersion = project.version.toString()
// We're using Gradle included-builds and dependency substitution, so we
// need to use the Gradle project name, *not* the published Maven artifact-id
devPublication("org.jetbrains.dokka:plugin-all-modules-page:$dokkaVersion")
devPublication("org.jetbrains.dokka:analysis-kotlin-api:$dokkaVersion")
devPublication("org.jetbrains.dokka:analysis-kotlin-descriptors:$dokkaVersion")
devPublication("org.jetbrains.dokka:analysis-kotlin-symbols:$dokkaVersion")
devPublication("org.jetbrains.dokka:analysis-markdown-jb:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-android-documentation:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-base:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-base-test-utils:$dokkaVersion")
devPublication("org.jetbrains.dokka:dokka-core:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-gfm:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-gfm-template-processing:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-javadoc:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-jekyll:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-jekyll-template-processing:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-kotlin-as-java:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-mathjax:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-templating:$dokkaVersion")
devPublication("org.jetbrains.dokka:plugin-versioning:$dokkaVersion")

devPublication("org.jetbrains.dokka:runner-gradle-plugin-classic:$dokkaVersion")
}

kotlin {
Expand All @@ -42,14 +67,7 @@ val aggregatingProject = gradle.includedBuild("dokka")
val templateSettingsGradleKts = layout.projectDirectory.file("projects/template.settings.gradle.kts")
val templateProjectsDir = layout.projectDirectory.dir("projects")

tasks.integrationTestPreparation {
// TODO remove this in https://github.com/Kotlin/dokka/pull/3433
dependsOn(aggregatingProject.task(":publishToMavenLocal"))
}

tasks.withType<Test>().configureEach {
dependsOn(tasks.integrationTestPreparation)

setForkEvery(1)
maxHeapSize = "2G"
dokkaBuild.integrationTestParallelism.orNull?.let { parallelism ->
Expand All @@ -69,7 +87,11 @@ tasks.withType<Test>().configureEach {
dokkaBuild.isCI.map { isCi -> if (isCi) "ALWAYS" else "ON_SUCCESS" }.get(),
)

environment("DOKKA_VERSION", project.version)
val dokkaVersion = provider { project.version.toString() }
inputs.property("dokkaVersion", dokkaVersion)
doFirst("set DOKKA_VERSION environment variable (workaround for https://github.com/gradle/gradle/issues/24267)") {
environment("DOKKA_VERSION", dokkaVersion.get())
}

// environment() isn't Provider API compatible yet https://github.com/gradle/gradle/issues/11534
dokkaBuild.integrationTestExhaustive.orNull?.let { exhaustive ->
Expand All @@ -87,8 +109,9 @@ tasks.withType<Test>().configureEach {
showStackTraces = true
}

// TODO remove this in https://github.com/Kotlin/dokka/pull/3433
doNotTrackState("uses artifacts from Maven Local")
// The tests produce report data and generated Dokka output.
// Always cache them so Gradle can skip running integration tests if nothing has changed.
outputs.cacheIf("always cache") { true }
}

testing {
Expand Down Expand Up @@ -200,6 +223,8 @@ fun TestingExtension.registerTestProjectSuite(
templateSettingsGradleKts.asFile.invariantSeparatorsPath,
)

devMavenPublish.configureTask(this)

if (jvm != null) {
javaLauncher = javaToolchains.launcherFor { languageVersion = jvm }
}
Expand Down

0 comments on commit ad9b822

Please sign in to comment.