Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cdsScenarios.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

a_testFunctional_noCds {
title = "testFunctional - No CDS"
tasks = [":dokka-gradle-plugin:testFunctional"]
gradle-args = ["-PenableDokkaCds=false"]
warm-ups = 2
}

b_testFunctional_withCds {
title = "testFunctional - With CDS"
tasks = [":dokka-gradle-plugin:testFunctional"]
gradle-args = ["-PenableDokkaCds=true"]
warm-ups = 2
}
7 changes: 4 additions & 3 deletions dokka-integration-tests/gradle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,10 @@ fun registerTestProjectSuite(
.inputFile("templateSettingsGradleKts", templateSettingsGradleKts)
.withPathSensitivity(NAME_ONLY)

if (jvm != null) {
javaLauncher = javaToolchains.launcherFor { languageVersion = jvm }
}
// if (jvm != null) {
// javaLauncher = javaToolchains.launcherFor { languageVersion = jvm }
// }
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }
}
}
configure()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class DatetimeGradleIntegrationTest : AbstractGradleIntegrationTest(), TestOutpu
@ParameterizedTest(name = "{0}")
@ArgumentsSource(DatetimeBuildVersionsArgumentsProvider::class)
fun execute(buildVersions: BuildVersions) {
println("TESTING PROJECT DIR " + projectDir.absoluteFile.invariantSeparatorsPath)
val result = createGradleRunner(buildVersions, ":kotlinx-datetime:dokkaGenerate").buildRelaxed()

assertEquals(TaskOutcome.SUCCESS, assertNotNull(result.task(":kotlinx-datetime:dokkaGenerate")).outcome)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package org.jetbrains.dokka.it

import org.jsoup.Jsoup
import org.junit.jupiter.api.io.CleanupMode
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.net.URL
Expand All @@ -15,7 +16,7 @@ import kotlin.test.assertTrue

abstract class AbstractIntegrationTest {

@field:TempDir
@field:TempDir(cleanup = CleanupMode.NEVER)
lateinit var tempFolder: File

/** Working directory of the test. Contains the project that should be tested. */
Expand Down
7 changes: 7 additions & 0 deletions dokka-runners/dokka-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ testing.suites {
systemProperty("kotest.framework.config.fqn", "org.jetbrains.dokka.gradle.utils.KotestProjectConfig")
// FIXME remove autoscan when Kotest >= 6.0
systemProperty("kotest.framework.classpath.scanning.autoscan.disable", "true")

// cds requires java >= 17
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }

outputs.upToDateWhen {
!providers.gradleProperty("enableDokkaCds").isPresent
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package org.jetbrains.dokka.gradle.internal

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.logging.Logging
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.process.ExecOperations
import org.jetbrains.kotlin.konan.file.use
import java.io.File
import java.io.OutputStream
import java.io.RandomAccessFile
import java.math.BigInteger
import java.nio.channels.FileChannel
import java.nio.channels.FileLock
import java.nio.channels.OverlappingFileLockException
import java.nio.file.Files
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import java.util.jar.JarFile
import javax.inject.Inject
import kotlin.concurrent.withLock
import kotlin.random.Random


internal abstract class CdsSource
@Inject
internal constructor(
private val execOps: ExecOperations
) : ValueSource<File, CdsSource.Parameters> {

interface Parameters : ValueSourceParameters {
val classpath: ConfigurableFileCollection
}

private val classpathChecksum: String by lazy {
checksum(parameters.classpath)
}

private val cdsFile: File by lazy {
cdsCacheDir.resolve("$classpathChecksum.jsa")
}
private val lockFile: File by lazy {
cdsCacheDir.resolve("$classpathChecksum.lock")
}

override fun obtain(): File? {
if (currentJavaVersion < 17) {
logger.warn("CDS generation is only supported for Java 17 and above. Current version $currentJavaVersion.")
return null
}
if (cdsFile.exists()) {
cdsFile.setLastModified(System.currentTimeMillis())
return cdsFile
}

lock.withLock {
RandomAccessFile(lockFile, "rw").use {
it.channel.lockLenient().use {
if (cdsFile.exists()) {
return cdsFile
} else {
generateStaticCds()
logger.warn("Using CDS ${cdsFile.absoluteFile.invariantSeparatorsPath}")
return cdsFile
}
}
}
}
}

private fun generateStaticCds() {

val classListFile = Files.createTempFile("CdsSource", "classlist").toFile()
classListFile.deleteOnExit()

parameters.classpath.files
.flatMap { file -> getClassNamesFromJarFile(file) }
.distinct()
.sorted()
.joinToString("\n")
.let {
classListFile.writeText(it)
}
logger.warn("Generating CDS from class list: ${classListFile.absoluteFile.invariantSeparatorsPath}")

execOps.exec {
executable("java")
args(
"-Xshare:dump",
"-XX:SharedArchiveFile=${cdsFile.absoluteFile.invariantSeparatorsPath}",
"-XX:SharedClassListFile=${classListFile.absoluteFile.invariantSeparatorsPath}",
"-cp",
parameters.classpath.asPath,
// "${parameters.classpath.asPath}${File.pathSeparator}/Users/dev/projects/jetbrains/dokka/dokka-runners/runner-cli/build/libs/runner-cli-2.0.20-SNAPSHOT.jar",
// parameters.classpath.asPath,
// "org.jetbrains.dokka.MainKt"
)
//logger.warn("Generating CDS args: $args")
}
}

companion object {
private val lock: Lock = ReentrantLock()

private val logger = Logging.getLogger(CdsSource::class.java)

private val cdsCacheDir: File by lazy {
val cdsFromEnv = System.getenv("DOKKA_CDS_CACHE_DIR")

if (cdsFromEnv != null) {
File(cdsFromEnv).apply {
mkdirs()
}
} else {
val osName = System.getProperty("os.name").lowercase()
val homeDir = System.getProperty("user.home")
val appDataDir = System.getenv("APP_DATA") ?: homeDir

val userCacheDir = when {
"win" in osName -> "$appDataDir/Caches/"
"mac" in osName -> "$homeDir/Library/Caches/"
"nix" in osName -> "$homeDir/.cache/"
else -> "$homeDir/.cache/"
}

File(userCacheDir).resolve("dokka-cds").apply {
mkdirs()
}
}
}
}
}


private fun checksum(
files: ConfigurableFileCollection
): String {
val md = MessageDigest.getInstance("md5")
DigestOutputStream(nullOutputStream(), md).use { os ->
os.write(files.asPath.encodeToByteArray())

files.forEach { file ->
file.inputStream().use {
it.copyTo(os)
}
}
}
return BigInteger(1, md.digest()).toString(16)
.padStart(md.digestLength * 2, '0')
}


private fun nullOutputStream(): OutputStream =
object : OutputStream() {
override fun write(b: Int) {}
}


private fun getClassNamesFromJarFile(source: File): Set<String> {
JarFile(source).use { jarFile ->
return jarFile.entries().asSequence()
.filter { it.name.endsWith(".class") }
.map { entry ->
entry.name
.replace("/", ".")
.removeSuffix(".class")
}
.toSet()
}
}

private val currentJavaVersion: Int =
System.getProperty("java.version")
.removePrefix("1.")
.substringBefore(".")
.toInt()


/**
* Leniently obtain a [FileLock] for the channel.
*
* @throws [InterruptedException] if the current thread is interrupted before the lock can be acquired.
*/
private tailrec fun FileChannel.lockLenient(): FileLock {
if (Thread.interrupted()) {
throw InterruptedException("Interrupted while waiting for lock on FileChannel@${[email protected]()}")
}

val lock = try {
tryLock()
} catch (_: OverlappingFileLockException) {
// ignore exception - it means the lock is already held by this process.
null
}

if (lock != null) {
return lock
}

try {
Thread.sleep(Random.nextLong(25, 125))
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
throw e
}

return lockLenient()
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.*
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.of
import org.gradle.kotlin.dsl.submit
import org.gradle.workers.WorkerExecutor
import org.jetbrains.dokka.DokkaConfiguration
import org.jetbrains.dokka.DokkaConfigurationImpl
import org.jetbrains.dokka.gradle.DokkaBasePlugin.Companion.jsonMapper
import org.jetbrains.dokka.gradle.engine.parameters.DokkaGeneratorParametersSpec
import org.jetbrains.dokka.gradle.engine.parameters.builders.DokkaParametersBuilder
import org.jetbrains.dokka.gradle.internal.CdsSource
import org.jetbrains.dokka.gradle.internal.DokkaPluginParametersContainer
import org.jetbrains.dokka.gradle.internal.InternalDokkaGradlePluginApi
import org.jetbrains.dokka.gradle.workers.ClassLoaderIsolation
Expand Down Expand Up @@ -50,6 +53,10 @@ constructor(
pluginsConfiguration: DokkaPluginParametersContainer,
) : DokkaBaseTask() {

@InternalDokkaGradlePluginApi
@get:Inject
protected open val providers: ProviderFactory get() = error("injected")

private val dokkaParametersBuilder = DokkaParametersBuilder(archives)

/**
Expand Down Expand Up @@ -122,6 +129,12 @@ constructor(
Publication,
}

init {
outputs.upToDateWhen {
!providers.gradleProperty("enableDokkaCds").isPresent
}
}

@InternalDokkaGradlePluginApi
protected fun generateDocumentation(
generationType: GeneratorMode,
Expand Down Expand Up @@ -160,6 +173,19 @@ constructor(
isolation.minHeapSize.orNull?.let(this::setMinHeapSize)
isolation.jvmArgs.orNull?.filter { it.isNotBlank() }?.let(this::setJvmArgs)
isolation.systemProperties.orNull?.let(this::systemProperties)

if (providers.gradleProperty("enableDokkaCds").orNull.toBoolean()) {
val cds = providers.of(CdsSource::class) {
parameters {
classpath.from(runtimeClasspath)
}
}
cds.orNull?.let {
jvmArgs(
"-XX:SharedArchiveFile=${it.absoluteFile.invariantSeparatorsPath}"
)
}
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions dokka-runners/runner-cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ tasks.shadowJar {
attributes("Main-Class" to "org.jetbrains.dokka.MainKt")
}
}

tasks.jar {
manifest {
attributes("Main-Class" to "org.jetbrains.dokka.MainKt")
}
}
Loading