blob: 74a2af46b823c63f20e1fe1593bc97d70006e022 [file] [log] [blame]
package org.jetbrains.dokka.gradle
import groovy.lang.Closure
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.compile.AbstractCompile
import org.jetbrains.dokka.*
import org.jetbrains.dokka.ReflectDsl.isNotInstance
import org.jetbrains.dokka.gradle.ClassloaderContainer.fatJarClassLoader
import org.jetbrains.dokka.gradle.DokkaVersion.version
import ru.yole.jkid.JsonExclude
import ru.yole.jkid.serialization.serialize
import java.io.File
import java.io.InputStream
import java.io.Serializable
import java.net.URLClassLoader
import java.util.*
import java.util.concurrent.Callable
import java.util.function.BiConsumer
open class DokkaPlugin : Plugin<Project> {
override fun apply(project: Project) {
DokkaVersion.loadFrom(javaClass.getResourceAsStream("/META-INF/gradle-plugins/org.jetbrains.dokka.properties"))
project.tasks.create("dokka", DokkaTask::class.java).apply {
moduleName = project.name
outputDirectory = File(project.buildDir, "dokka").absolutePath
}
}
}
object DokkaVersion {
var version: String? = null
fun loadFrom(stream: InputStream) {
version = Properties().apply {
load(stream)
}.getProperty("dokka-version")
}
}
object ClassloaderContainer {
@JvmField
var fatJarClassLoader: ClassLoader? = null
}
const val `deprecationMessage reportNotDocumented` = "Will be removed in 0.9.17, see dokka#243"
open class DokkaTask : DefaultTask() {
fun defaultKotlinTasks() = with(ReflectDsl) {
val abstractKotlinCompileClz = try {
project.buildscript.classLoader.loadClass(ABSTRACT_KOTLIN_COMPILE)
} catch (cnfe: ClassNotFoundException) {
logger.warn("$ABSTRACT_KOTLIN_COMPILE class not found, default kotlin tasks ignored")
return@with emptyList<Task>()
}
return@with project.tasks.filter { it isInstance abstractKotlinCompileClz }.filter { "Test" !in it.name }
}
init {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates dokka documentation for Kotlin"
@Suppress("LeakingThis")
dependsOn(Callable { kotlinTasks.map { it.taskDependencies } })
}
@Input
var moduleName: String = ""
@Input
var outputFormat: String = "html"
var outputDirectory: String = ""
@Deprecated("Going to be removed in 0.9.16, use classpath + sourceDirs instead if kotlinTasks is not suitable for you")
@Input var processConfigurations: List<Any?> = emptyList()
@InputFiles var classpath: Iterable<File> = arrayListOf()
@Input
var includes: List<Any?> = arrayListOf()
@Input
var linkMappings: ArrayList<LinkMapping> = arrayListOf()
@Input
var samples: List<Any?> = arrayListOf()
@Input
var jdkVersion: Int = 6
@Input
var generateClassIndexPage = true
@Input
var generatePackageIndexPage = true
@Input
var sourceDirs: Iterable<File> = emptyList()
@Input
var sourceRoots: MutableList<SourceRoot> = arrayListOf()
@Input
var dokkaFatJar: Any = "org.jetbrains.dokka:dokka-fatjar:$version"
@Input var includeNonPublic = false
@Input var skipDeprecated = false
@Input var skipEmptyPackages = true
@Input var outlineRoot: String = ""
@Input var dacRoot: String = ""
@Deprecated(`deprecationMessage reportNotDocumented`, replaceWith = ReplaceWith("reportUndocumented"))
var reportNotDocumented
get() = reportUndocumented
set(value) {
logger.warn("Dokka: reportNotDocumented is deprecated and " + `deprecationMessage reportNotDocumented`.decapitalize())
reportUndocumented = value
}
@Input var reportUndocumented = true
@Input var perPackageOptions: MutableList<PackageOptions> = arrayListOf()
@Input var impliedPlatforms: MutableList<String> = arrayListOf()
@Input var externalDocumentationLinks = mutableListOf<DokkaConfiguration.ExternalDocumentationLink>()
@Input var noStdlibLink: Boolean = false
@Input
var noJdkLink: Boolean = false
@Optional @Input
var cacheRoot: String? = null
@Optional @Input
var languageVersion: String? = null
@Optional @Input
var apiVersion: String? = null
@Input
var collectInheritedExtensionsFromLibraries: Boolean = false
@get:Internal
internal val kotlinCompileBasedClasspathAndSourceRoots: ClasspathAndSourceRoots by lazy { extractClasspathAndSourceRootsFromKotlinTasks() }
private var kotlinTasksConfigurator: () -> List<Any?>? = { defaultKotlinTasks() }
private val kotlinTasks: List<Task> by lazy { extractKotlinCompileTasks() }
fun kotlinTasks(closure: Closure<Any?>) {
kotlinTasksConfigurator = { closure.call() as? List<Any?> }
}
fun linkMapping(closure: Closure<Unit>) {
val mapping = LinkMapping()
closure.delegate = mapping
closure.call()
if (mapping.path.isEmpty()) {
throw IllegalArgumentException("Link mapping should have dir")
}
if (mapping.url.isEmpty()) {
throw IllegalArgumentException("Link mapping should have url")
}
linkMappings.add(mapping)
}
fun sourceRoot(closure: Closure<Unit>) {
val sourceRoot = SourceRoot()
closure.delegate = sourceRoot
closure.call()
sourceRoots.add(sourceRoot)
}
fun packageOptions(closure: Closure<Unit>) {
val packageOptions = PackageOptions()
closure.delegate = packageOptions
closure.call()
perPackageOptions.add(packageOptions)
}
fun externalDocumentationLink(closure: Closure<Unit>) {
val builder = DokkaConfiguration.ExternalDocumentationLink.Builder()
closure.delegate = builder
closure.call()
externalDocumentationLinks.add(builder.build())
}
fun tryResolveFatJar(project: Project): File {
return try {
val dependency = project.buildscript.dependencies.create(dokkaFatJar)
val configuration = project.buildscript.configurations.detachedConfiguration(dependency)
configuration.description = "Dokka main jar"
configuration.resolve().first()
} catch (e: Exception) {
project.parent?.let { tryResolveFatJar(it) } ?: throw e
}
}
fun loadFatJar() {
if (fatJarClassLoader == null) {
val fatjar = if (dokkaFatJar is File)
dokkaFatJar as File
else
tryResolveFatJar(project)
fatJarClassLoader = URLClassLoader(arrayOf(fatjar.toURI().toURL()), ClassLoader.getSystemClassLoader().parent)
}
}
internal data class ClasspathAndSourceRoots(val classpathFileCollection: FileCollection, val sourceRoots: List<File>) : Serializable
private fun extractKotlinCompileTasks(): List<Task> {
val inputList = (kotlinTasksConfigurator.invoke() ?: emptyList()).filterNotNull()
val (paths, other) = inputList.partition { it is String }
val taskContainer = project.tasks
val tasksByPath = paths.map { taskContainer.findByPath(it as String) ?: throw IllegalArgumentException("Task with path '$it' not found") }
other
.filter { it !is Task || it isNotInstance getAbstractKotlinCompileFor(it) }
.forEach { throw IllegalArgumentException("Illegal entry in kotlinTasks, must be subtype of $ABSTRACT_KOTLIN_COMPILE or String, but was $it") }
tasksByPath
.filter { it == null || it isNotInstance getAbstractKotlinCompileFor(it) }
.forEach { throw IllegalArgumentException("Illegal task path in kotlinTasks, must be subtype of $ABSTRACT_KOTLIN_COMPILE, but was $it") }
return (tasksByPath + other) as List<Task>
}
private fun extractClasspathAndSourceRootsFromKotlinTasks(): ClasspathAndSourceRoots {
val allTasks = kotlinTasks
val allClasspath = mutableSetOf<File>()
var allClasspathFileCollection: FileCollection = project.files()
val allSourceRoots = mutableSetOf<File>()
allTasks.forEach {
logger.debug("Dokka found AbstractKotlinCompile task: $it")
with(ReflectDsl) {
val taskSourceRoots: List<File> = it["sourceRootsContainer"]["sourceRoots"].v()
val abstractKotlinCompileClz = getAbstractKotlinCompileFor(it)!!
val taskClasspath: Iterable<File> =
(it["getClasspath", AbstractCompile::class].takeIfIsFunc()?.invoke()
?: it["compileClasspath", abstractKotlinCompileClz].takeIfIsProp()?.v()
?: it["getClasspath", abstractKotlinCompileClz]())
if (taskClasspath is FileCollection) {
allClasspathFileCollection += taskClasspath
} else {
allClasspath += taskClasspath
}
allSourceRoots += taskSourceRoots.filter { it.exists() }
}
}
return ClasspathAndSourceRoots(allClasspathFileCollection + project.files(allClasspath), allSourceRoots.toList())
}
private fun Iterable<File>.toSourceRoots(): List<SourceRoot> = this.filter { it.exists() }.map { SourceRoot().apply { path = it.path } }
protected open fun collectSuppressedFiles(sourceRoots: List<SourceRoot>): List<String> = emptyList()
@TaskAction
fun generate() {
val kotlinColorsEnabledBefore = System.getProperty(COLORS_ENABLED_PROPERTY) ?: "false"
System.setProperty(COLORS_ENABLED_PROPERTY, "false")
try {
loadFatJar()
val (tasksClasspath, tasksSourceRoots) = kotlinCompileBasedClasspathAndSourceRoots
val project = project
val sourceRoots = collectSourceRoots() + tasksSourceRoots.toSourceRoots()
if (sourceRoots.isEmpty()) {
logger.warn("No source directories found: skipping dokka generation")
return
}
val fullClasspath = collectClasspathFromOldSources() + tasksClasspath + classpath
val bootstrapClass = fatJarClassLoader!!.loadClass("org.jetbrains.dokka.DokkaBootstrapImpl")
val bootstrapInstance = bootstrapClass.constructors.first().newInstance()
val bootstrapProxy: DokkaBootstrap = automagicTypedProxy(javaClass.classLoader, bootstrapInstance)
val configuration = SerializeOnlyDokkaConfiguration(
moduleName,
fullClasspath.map { it.absolutePath },
sourceRoots,
samples.filterNotNull().map { project.file(it).absolutePath },
includes.filterNotNull().map { project.file(it).absolutePath },
outputDirectory,
outputFormat,
includeNonPublic,
false,
reportUndocumented,
skipEmptyPackages,
skipDeprecated,
jdkVersion,
generateClassIndexPage,
generatePackageIndexPage,
linkMappings,
impliedPlatforms,
perPackageOptions,
externalDocumentationLinks,
noStdlibLink,
noJdkLink,
cacheRoot,
collectSuppressedFiles(sourceRoots),
languageVersion,
apiVersion,
collectInheritedExtensionsFromLibraries,
outlineRoot,
dacRoot)
bootstrapProxy.configure(
BiConsumer { level, message ->
when (level) {
"info" -> logger.info(message)
"warn" -> logger.warn(message)
"error" -> logger.error(message)
}
},
serialize(configuration)
)
bootstrapProxy.generate()
} finally {
System.setProperty(COLORS_ENABLED_PROPERTY, kotlinColorsEnabledBefore)
}
}
private fun collectClasspathFromOldSources(): List<File> {
val allConfigurations = project.configurations
val fromConfigurations =
processConfigurations.flatMap { allConfigurations.getByName(it.toString()) }
return fromConfigurations
}
private fun collectSourceRoots(): List<SourceRoot> {
val sourceDirs = if (sourceDirs.any()) {
logger.info("Dokka: Taking source directories provided by the user")
sourceDirs.toSet()
} else if (kotlinTasks.isEmpty()) {
project.convention.findPlugin(JavaPluginConvention::class.java)?.let { javaPluginConvention ->
logger.info("Dokka: Taking source directories from default java plugin")
val sourceSets = javaPluginConvention.sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME)
sourceSets?.allSource?.srcDirs
}
} else {
emptySet()
}
return sourceRoots + (sourceDirs?.toSourceRoots() ?: emptyList())
}
@Classpath
fun getInputClasspath(): FileCollection {
val (classpathFileCollection) = extractClasspathAndSourceRootsFromKotlinTasks()
return project.files(collectClasspathFromOldSources() + classpath) + classpathFileCollection
}
@InputFiles
fun getInputFiles(): FileCollection {
val (_, tasksSourceRoots) = extractClasspathAndSourceRootsFromKotlinTasks()
return project.files(tasksSourceRoots.map { project.fileTree(it) }) +
project.files(collectSourceRoots().map { project.fileTree(File(it.path)) }) +
project.files(includes) +
project.files(samples.filterNotNull().map { project.fileTree(it) })
}
@OutputDirectory
fun getOutputDirectoryAsFile(): File = project.file(outputDirectory)
companion object {
const val COLORS_ENABLED_PROPERTY = "kotlin.colors.enabled"
const val ABSTRACT_KOTLIN_COMPILE = "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile"
private fun getAbstractKotlinCompileFor(task: Task) = try {
task.project.buildscript.classLoader.loadClass(ABSTRACT_KOTLIN_COMPILE)
} catch (e: ClassNotFoundException) {
null
}
}
}
class SourceRoot : DokkaConfiguration.SourceRoot, Serializable {
override var path: String = ""
set(value) {
field = File(value).absolutePath
}
override var platforms: List<String> = arrayListOf()
override fun toString(): String {
return "${platforms.joinToString()}::$path"
}
}
open class LinkMapping : Serializable, DokkaConfiguration.SourceLinkDefinition {
@JsonExclude
var dir: String
get() = path
set(value) {
path = value
}
override var path: String = ""
override var url: String = ""
@JsonExclude
var suffix: String?
get() = lineSuffix
set(value) {
lineSuffix = value
}
override var lineSuffix: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as LinkMapping
if (path != other.path) return false
if (url != other.url) return false
if (lineSuffix != other.lineSuffix) return false
return true
}
override fun hashCode(): Int {
var result = path.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + (lineSuffix?.hashCode() ?: 0)
return result
}
companion object {
const val serialVersionUID: Long = -8133501684312445981L
}
}
class PackageOptions : Serializable, DokkaConfiguration.PackageOptions {
override var prefix: String = ""
override var includeNonPublic: Boolean = false
override var reportUndocumented: Boolean = true
override var skipDeprecated: Boolean = false
override var suppress: Boolean = false
}