diff --git a/MODULE.bazel b/MODULE.bazel index 16d4bf7..fb98648 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -6,6 +6,7 @@ module( bazel_dep(name = "rules_java", version = "9.3.0") bazel_dep(name = "rules_jvm_external", version = "7.1") +bazel_dep(name = "rules_kotlin", version = "2.4.0") bazel_dep(name = "bazel_skylib", version = "1.9.0", dev_dependency = True) bazel_dep(name = "buildifier_prebuilt", version = "8.2.1.2", dev_dependency = True) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 79b1b23..c0f9419 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -40,13 +40,15 @@ "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", + "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", - "https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751", + "https://bcr.bazel.build/modules/bazel_lib/3.1.0/MODULE.bazel": "6809765c14e3c766a9b9286c7b0ec56ed87a73326e48fe01749f0c0fdcfe3287", + "https://bcr.bazel.build/modules/bazel_lib/3.1.0/source.json": "aaf7c2dc816219f4cb356c9d65f2555fb7f9543e537199f74a921f7877d23dfb", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -202,7 +204,8 @@ "https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025", "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/MODULE.bazel": "00d39c5e0fa78cd86193946265bb849e7878c24e44260f9525108428852b315c", - "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/source.json": "7a32c2259c79ae0c9a036121f120de825e3ba5f0f3a209ffbbdccf4dc62489b9", + "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/MODULE.bazel": "38dac18bb76c0a47ff60dfcd95c666985cbc46374f28ea4eeb868bdbc58c5bec", + "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/source.json": "07b6a307448817c071c4ba90dcb03f801e008959f8dfd8b152a241cc0ee01a23", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", @@ -267,6 +270,64 @@ }, "selectedYankedVersions": {}, "moduleExtensions": { + "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": { + "general": { + "bzlTransitiveDigest": "NRXra7941UfmNUyIxnLt82V5hULluVGL2nBsijTl4j4=", + "usagesDigest": "D1r3lfzMuUBFxgG8V6o0bQTLMk3GkaGOaPzw53wrwyw=", + "recordedInputs": [ + "REPO_MAPPING:pybind11_bazel+,bazel_tools bazel_tools", + "FILE:@@pybind11_bazel+//MODULE.bazel e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34" + ], + "generatedRepoSpecs": { + "pybind11": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@pybind11_bazel+//:pybind11-BUILD.bazel", + "strip_prefix": "pybind11-2.12.0", + "urls": [ + "https://github.com/pybind/pybind11/archive/v2.12.0.zip" + ] + } + } + } + } + }, + "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": { + "general": { + "bzlTransitiveDigest": "qEcqhUuyYlPmvAtyYRqS+4Y7cRHA2Vho3JIBJZJJX+s=", + "usagesDigest": "zr/niBQ/s2fHozWAsg4vI70wAxcuFjG+QtM15qGkq9o=", + "recordedInputs": [ + "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "apksig": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://android.googlesource.com/platform/tools/apksig/+archive/24e3075e68ebe17c0b529bb24bfda819db5e2f3b.tar.gz", + "build_file": "@@rules_android+//bzlmod_extensions:apksig.BUILD" + } + } + } + } + }, + "@@rules_android+//bzlmod_extensions:com_android_dex.bzl%com_android_dex_extension": { + "general": { + "bzlTransitiveDigest": "gTuOqknGgqPw9YQSCY+qXxgxVwiLRGdBCsCe4i4q/xw=", + "usagesDigest": "c1Y/KGGjUYCyd8zNIVTUh1bynVXRFz6xGKaSCBpQANM=", + "recordedInputs": [ + "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "com_android_dex": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://android.googlesource.com/platform/dalvik/+archive/5a81c499a569731e2395f7c8d13c0e0d4e17a2b6.tar.gz", + "build_file": "@@rules_android+//bzlmod_extensions:com_android_dex.BUILD" + } + } + } + } + }, "@@rules_android+//rules/android_sdk_repository:rule.bzl%android_sdk_repository_extension": { "general": { "bzlTransitiveDigest": "+rMrzIrv7sImYmkbXJYv+gFpTJQ79X3MpwwMLI2A+oA=", diff --git a/dokka/private/BUILD.bazel b/dokka/private/BUILD.bazel index eda5140..a9483c4 100644 --- a/dokka/private/BUILD.bazel +++ b/dokka/private/BUILD.bazel @@ -1,15 +1,15 @@ -load("@rules_java//java:defs.bzl", "java_binary") +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_binary") package(default_visibility = ["//dokka:__subpackages__"]) exports_files( - ["DokkaGeneratorRunner.java"], + ["DokkaGeneratorRunner.kt"], visibility = ["//visibility:public"], ) -java_binary( +kt_jvm_binary( name = "dokka_generator", - srcs = ["DokkaGeneratorRunner.java"], + srcs = ["DokkaGeneratorRunner.kt"], main_class = "rules.dokka.DokkaGeneratorRunner", visibility = ["//dokka:__pkg__"], deps = [ diff --git a/dokka/private/DokkaGeneratorRunner.java b/dokka/private/DokkaGeneratorRunner.java deleted file mode 100644 index 476be40..0000000 --- a/dokka/private/DokkaGeneratorRunner.java +++ /dev/null @@ -1,224 +0,0 @@ -package rules.dokka; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import org.jetbrains.dokka.ConfigurationJsonUtilsKt; -import org.jetbrains.dokka.ConfigurationKt; -import org.jetbrains.dokka.DokkaConfiguration; -import org.jetbrains.dokka.DokkaConfigurationImpl; -import org.jetbrains.dokka.DokkaGenerator; -import org.jetbrains.dokka.DokkaModuleDescriptionImpl; -import org.jetbrains.dokka.LinkMapperKt; -import org.jetbrains.dokka.PluginConfigurationImpl; -import org.jetbrains.dokka.utilities.DokkaConsoleLogger; - -/** - * Runs Dokka in-process with paths resolved inside the Bazel action's execution root. - * - *

Normal configurations use Dokka's documented JSON parser. Multi-module manifests are - * converted to typed module descriptions because Dokka does not document the aggregate {@code - * modules} JSON representation. - */ -public final class DokkaGeneratorRunner { - private static final String MULTI_MODULE_MODE = "rules_dokka_multi_module"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - public static void main(String[] args) throws Exception { - if (args.length != 1) { - throw new IllegalArgumentException("usage: DokkaGeneratorRunner CONFIGURATION"); - } - - ObjectNode root = (ObjectNode) OBJECT_MAPPER.readTree(Paths.get(args[0]).toFile()); - DokkaConfiguration configuration = - MULTI_MODULE_MODE.equals(root.path("runnerMode").asText()) - ? multiModuleConfiguration(root) - : standardConfiguration(root); - new DokkaGenerator(configuration, new DokkaConsoleLogger()).generate(); - } - - private static DokkaConfiguration standardConfiguration(ObjectNode root) throws Exception { - absolutizeField(root, "outputDir"); - absolutizeOptionalField(root, "cacheRoot"); - absolutizeArray(root, "includes"); - absolutizeArray(root, "pluginsClasspath"); - - JsonNode sourceSets = root.path("sourceSets"); - if (sourceSets.isArray()) { - for (JsonNode sourceSetNode : (ArrayNode) sourceSets) { - ObjectNode sourceSet = (ObjectNode) sourceSetNode; - for (String field : - List.of( - "classpath", - "includes", - "samples", - "sourceRoots", - "suppressedFiles")) { - absolutizeArray(sourceSet, field); - } - JsonNode sourceLinks = sourceSet.path("sourceLinks"); - if (sourceLinks.isArray()) { - for (JsonNode sourceLinkNode : (ArrayNode) sourceLinks) { - absolutizeOptionalField((ObjectNode) sourceLinkNode, "localDirectory"); - } - } - } - } - - String json = OBJECT_MAPPER.writeValueAsString(root); - DokkaConfiguration configuration = - ConfigurationKt.apply( - ConfigurationJsonUtilsKt.DokkaConfigurationImpl(json), - ConfigurationJsonUtilsKt.GlobalDokkaConfiguration(json)); - for (DokkaConfiguration.DokkaSourceSet sourceSet : configuration.getSourceSets()) { - sourceSet.getExternalDocumentationLinks().addAll(LinkMapperKt.defaultLinks(sourceSet)); - } - return configuration; - } - - private static DokkaConfiguration multiModuleConfiguration(ObjectNode root) { - Path outputDirectory = absolutePath(requiredText(root, "outputDir")); - List modules = new ArrayList<>(); - JsonNode moduleNodes = root.path("modules"); - if (!moduleNodes.isArray() || moduleNodes.isEmpty()) { - throw new IllegalArgumentException("Multi-module Dokka manifest must contain modules"); - } - - for (JsonNode moduleNode : (ArrayNode) moduleNodes) { - ObjectNode module = (ObjectNode) moduleNode; - String portableRelativeOutput = requiredText(module, "relativePathToOutputDirectory"); - Path relativeOutput = Paths.get(portableRelativeOutput); - if (relativeOutput.isAbsolute() - || !outputDirectory.resolve(relativeOutput).normalize().startsWith(outputDirectory)) { - throw new IllegalArgumentException( - "Module output escapes the aggregate documentation directory: " + relativeOutput); - } - modules.add( - new DokkaModuleDescriptionImpl( - requiredText(module, "name"), - new PortableModulePath(relativeOutput, portableRelativeOutput), - files(module.path("includes")), - absolutePath(requiredText(module, "sourceOutputDirectory")).toFile())); - } - - List pluginConfigurations = new ArrayList<>(); - JsonNode pluginNodes = root.path("pluginsConfiguration"); - if (pluginNodes.isArray()) { - for (JsonNode pluginNode : (ArrayNode) pluginNodes) { - pluginConfigurations.add( - new PluginConfigurationImpl( - requiredText(pluginNode, "fqPluginName"), - DokkaConfiguration.SerializationFormat.valueOf( - requiredText(pluginNode, "serializationFormat")), - requiredText(pluginNode, "values"))); - } - } - - // AllModulesPageGeneration ignores the single-module warning and suppression settings. - return new DokkaConfigurationImpl( - requiredText(root, "moduleName"), - optionalText(root, "moduleVersion"), - outputDirectory.toFile(), - optionalFile(root, "cacheRoot"), - root.path("offlineMode").asBoolean(), - List.of(), - new ArrayList<>(files(root.path("pluginsClasspath"))), - pluginConfigurations, - modules, - false, - false, - true, - files(root.path("includes")), - false, - root.path("finalizeCoroutines").asBoolean(true)); - } - - private static Set files(JsonNode nodes) { - Set files = new LinkedHashSet<>(); - if (nodes.isArray()) { - for (JsonNode node : (ArrayNode) nodes) { - files.add(absolutePath(node.asText()).toFile()); - } - } - return files; - } - - private static void absolutizeArray(ObjectNode node, String field) { - JsonNode values = node.path(field); - if (!values.isArray()) { - return; - } - ArrayNode absoluteValues = OBJECT_MAPPER.createArrayNode(); - for (JsonNode value : (ArrayNode) values) { - absoluteValues.add(absolutePath(value.asText()).toString()); - } - node.set(field, absoluteValues); - } - - private static void absolutizeField(ObjectNode node, String field) { - node.put(field, absolutePath(requiredText(node, field)).toString()); - } - - private static void absolutizeOptionalField(ObjectNode node, String field) { - String value = optionalText(node, field); - if (value != null && !value.isEmpty()) { - node.put(field, absolutePath(value).toString()); - } - } - - private static File optionalFile(ObjectNode node, String field) { - String value = optionalText(node, field); - return value == null || value.isEmpty() ? null : absolutePath(value).toFile(); - } - - private static String requiredText(JsonNode node, String field) { - String value = optionalText(node, field); - if (value == null || value.isEmpty()) { - throw new IllegalArgumentException("Missing required Dokka manifest field: " + field); - } - return value; - } - - private static String optionalText(JsonNode node, String field) { - JsonNode value = node.get(field); - return value == null || value.isNull() ? null : value.asText(); - } - - private static Path absolutePath(String path) { - return Paths.get(path).toAbsolutePath().normalize(); - } - - /** - * Preserves a URL-compatible module path when Dokka renders aggregate links. - * - *

Dokka 2.2's all-modules-page plugin appends {@code relativePathToOutputDirectory} directly - * to a string when it creates module URLs. A regular {@link File} renders that path with - * backslashes on Windows. File operations still use the native path stored by {@link File}; - * only its string representation remains portable. - */ - private static final class PortableModulePath extends File { - private static final long serialVersionUID = 1L; - - private final String portablePath; - - PortableModulePath(Path nativePath, String portablePath) { - super(nativePath.toString()); - this.portablePath = portablePath; - } - - @Override - public String toString() { - return portablePath; - } - } - - private DokkaGeneratorRunner() {} -} diff --git a/dokka/private/DokkaGeneratorRunner.kt b/dokka/private/DokkaGeneratorRunner.kt new file mode 100644 index 0000000..9d9eb7d --- /dev/null +++ b/dokka/private/DokkaGeneratorRunner.kt @@ -0,0 +1,225 @@ +package rules.dokka + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.ObjectNode +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.DokkaGenerator +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import org.jetbrains.dokka.GlobalDokkaConfiguration +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.apply +import org.jetbrains.dokka.defaultLinks +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Runs Dokka in-process with paths resolved inside the Bazel action's execution root. + * + * Normal configurations use Dokka's documented JSON parser. Multi-module manifests are converted + * to typed module descriptions because Dokka does not document the aggregate `modules` JSON + * representation. + */ +object DokkaGeneratorRunner { + private const val MULTI_MODULE_MODE = "rules_dokka_multi_module" + private val objectMapper = ObjectMapper() + + @JvmStatic + fun main(args: Array) { + require(args.size == 1) { "usage: DokkaGeneratorRunner CONFIGURATION" } + + val root = objectMapper.readTree(Paths.get(args[0]).toFile()) as ObjectNode + val configuration = + if (root.path("runnerMode").asText() == MULTI_MODULE_MODE) { + multiModuleConfiguration(root) + } else { + standardConfiguration(root) + } + DokkaGenerator(configuration, DokkaConsoleLogger()).generate() + } + + private fun standardConfiguration(root: ObjectNode): DokkaConfiguration { + absolutizeField(root, "outputDir") + absolutizeOptionalField(root, "cacheRoot") + absolutizeArray(root, "includes") + absolutizeArray(root, "pluginsClasspath") + + val sourceSets = root.path("sourceSets") + if (sourceSets.isArray) { + sourceSets.forEach { sourceSetNode -> + val sourceSet = sourceSetNode as ObjectNode + listOf( + "classpath", + "includes", + "samples", + "sourceRoots", + "suppressedFiles", + ).forEach { field -> absolutizeArray(sourceSet, field) } + + val sourceLinks = sourceSet.path("sourceLinks") + if (sourceLinks.isArray) { + sourceLinks.forEach { sourceLinkNode -> + absolutizeOptionalField(sourceLinkNode as ObjectNode, "localDirectory") + } + } + } + } + + val json = objectMapper.writeValueAsString(root) + val configuration = + DokkaConfigurationImpl(json).apply(GlobalDokkaConfiguration(json)) + configuration.sourceSets.forEach { sourceSet -> + @Suppress("UNCHECKED_CAST") + val externalDocumentationLinks = + sourceSet.externalDocumentationLinks + as MutableSet + externalDocumentationLinks.addAll(defaultLinks(sourceSet)) + } + return configuration + } + + private fun multiModuleConfiguration(root: ObjectNode): DokkaConfiguration { + val outputDirectory = absolutePath(requiredText(root, "outputDir")) + val moduleNodes = root.path("modules") + require(moduleNodes.isArray && !moduleNodes.isEmpty) { + "Multi-module Dokka manifest must contain modules" + } + + val modules = + moduleNodes.map { moduleNode -> + val module = moduleNode as ObjectNode + val portableRelativeOutput = + requiredText(module, "relativePathToOutputDirectory") + val relativeOutput = Paths.get(portableRelativeOutput) + require( + !relativeOutput.isAbsolute && + outputDirectory.resolve(relativeOutput).normalize().startsWith(outputDirectory), + ) { + "Module output escapes the aggregate documentation directory: $relativeOutput" + } + DokkaModuleDescriptionImpl( + requiredText(module, "name"), + PortableModulePath(relativeOutput, portableRelativeOutput), + files(module.path("includes")), + absolutePath(requiredText(module, "sourceOutputDirectory")).toFile(), + ) + } + + val pluginNodes = root.path("pluginsConfiguration") + val pluginConfigurations = + if (pluginNodes.isArray) { + pluginNodes.map { pluginNode -> + PluginConfigurationImpl( + requiredText(pluginNode, "fqPluginName"), + DokkaConfiguration.SerializationFormat.valueOf( + requiredText(pluginNode, "serializationFormat"), + ), + requiredText(pluginNode, "values"), + ) + } + } else { + emptyList() + } + + // AllModulesPageGeneration ignores the single-module warning and suppression settings. + return DokkaConfigurationImpl( + requiredText(root, "moduleName"), + optionalText(root, "moduleVersion"), + outputDirectory.toFile(), + optionalFile(root, "cacheRoot"), + root.path("offlineMode").asBoolean(), + emptyList(), + files(root.path("pluginsClasspath")).toList(), + pluginConfigurations, + modules, + false, + false, + true, + files(root.path("includes")), + false, + root.path("finalizeCoroutines").asBoolean(true), + ) + } + + private fun files(nodes: JsonNode): Set = + if (nodes.isArray) { + nodes.mapTo(linkedSetOf()) { node -> absolutePath(node.asText()).toFile() } + } else { + emptySet() + } + + private fun absolutizeArray( + node: ObjectNode, + field: String, + ) { + val values = node.path(field) + if (!values.isArray) { + return + } + val absoluteValues = objectMapper.createArrayNode() + values.forEach { value -> absoluteValues.add(absolutePath(value.asText()).toString()) } + node.set(field, absoluteValues) + } + + private fun absolutizeField( + node: ObjectNode, + field: String, + ) { + node.put(field, absolutePath(requiredText(node, field)).toString()) + } + + private fun absolutizeOptionalField( + node: ObjectNode, + field: String, + ) { + optionalText(node, field) + ?.takeIf(String::isNotEmpty) + ?.let { value -> node.put(field, absolutePath(value).toString()) } + } + + private fun optionalFile( + node: ObjectNode, + field: String, + ): File? = + optionalText(node, field) + ?.takeIf(String::isNotEmpty) + ?.let { value -> absolutePath(value).toFile() } + + private fun requiredText( + node: JsonNode, + field: String, + ): String = + requireNotNull(optionalText(node, field)?.takeIf(String::isNotEmpty)) { + "Missing required Dokka manifest field: $field" + } + + private fun optionalText( + node: JsonNode, + field: String, + ): String? = node.get(field)?.takeUnless(JsonNode::isNull)?.asText() + + private fun absolutePath(path: String): Path = Paths.get(path).toAbsolutePath().normalize() + + /** + * Preserves a URL-compatible module path when Dokka renders aggregate links. + * + * Dokka 2.2's all-modules-page plugin appends `relativePathToOutputDirectory` directly to a + * string when it creates module URLs. A regular [File] renders that path with backslashes on + * Windows. File operations still use the native path stored by [File]; only its string + * representation remains portable. + */ + private class PortableModulePath( + nativePath: Path, + private val portablePath: String, + ) : File(nativePath.toString()) { + override fun toString(): String = portablePath + + private companion object { + private const val serialVersionUID = 1L + } + } +} diff --git a/dokka/private/toolchain.bzl b/dokka/private/toolchain.bzl index d1cdd48..8be02f0 100644 --- a/dokka/private/toolchain.bzl +++ b/dokka/private/toolchain.bzl @@ -1,10 +1,10 @@ """Dokka toolchain implementation.""" -load("@rules_java//java:defs.bzl", "java_binary") load("@rules_java//java/common:java_info.bzl", "JavaInfo") +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_binary") load(":providers.bzl", "DokkaConfigInfo", "DokkaToolchainInfo") -_GENERATOR_RUNNER_SOURCE = Label("//dokka/private:DokkaGeneratorRunner.java") +_GENERATOR_RUNNER_SOURCE = Label("//dokka/private:DokkaGeneratorRunner.kt") def _runtime_jars(targets): return depset( @@ -74,7 +74,7 @@ def dokka_generator(name, dokka_cli, jackson_databind, visibility = None): jackson_databind: Java target for Jackson databind. visibility: Visibility of the generator target. """ - java_binary( + kt_jvm_binary( name = name, srcs = [_GENERATOR_RUNNER_SOURCE], main_class = "rules.dokka.DokkaGeneratorRunner", diff --git a/tests/rules/BUILD.bazel b/tests/rules/BUILD.bazel index 1207e79..ec63aac 100644 --- a/tests/rules/BUILD.bazel +++ b/tests/rules/BUILD.bazel @@ -1,16 +1,16 @@ -load("@rules_java//java:defs.bzl", "java_library") +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") load("//dokka:defs.bzl", "dokka", "dokka_config", "dokka_multi_module") load(":dokka_analysis_test.bzl", "dokka_analysis_test_suite") -java_library( +kt_jvm_library( name = "dependency", - srcs = ["Dependency.java"], + srcs = ["Dependency.kt"], neverlink = True, ) -java_library( +kt_jvm_library( name = "custom_plugin", - srcs = ["CustomPlugin.java"], + srcs = ["CustomPlugin.kt"], ) dokka_config( diff --git a/tests/rules/CustomPlugin.java b/tests/rules/CustomPlugin.kt similarity index 54% rename from tests/rules/CustomPlugin.java rename to tests/rules/CustomPlugin.kt index 1a3cd61..4f32077 100644 --- a/tests/rules/CustomPlugin.java +++ b/tests/rules/CustomPlugin.kt @@ -1,4 +1,4 @@ -package com.example; +package com.example /** A stand-in used to verify custom plugin classpath wiring. */ -public final class CustomPlugin {} +class CustomPlugin diff --git a/tests/rules/Dependency.java b/tests/rules/Dependency.java deleted file mode 100644 index 524a32c..0000000 --- a/tests/rules/Dependency.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example; - -/** A dependency used by the analysis test. */ -public final class Dependency {} diff --git a/tests/rules/Dependency.kt b/tests/rules/Dependency.kt new file mode 100644 index 0000000..62559e1 --- /dev/null +++ b/tests/rules/Dependency.kt @@ -0,0 +1,4 @@ +package com.example + +/** A dependency used by the analysis test. */ +class Dependency diff --git a/tests/rules/dokka_analysis_test.bzl b/tests/rules/dokka_analysis_test.bzl index 05e4513..983d056 100644 --- a/tests/rules/dokka_analysis_test.bzl +++ b/tests/rules/dokka_analysis_test.bzl @@ -68,7 +68,7 @@ def _dokka_configuration_test_impl(ctx): ) asserts.true( env, - _contains_fragment(configuration["pluginsClasspath"], "libcustom_plugin.jar"), + _contains_fragment(configuration["pluginsClasspath"], "custom_plugin.jar"), ) asserts.false( env, @@ -100,7 +100,7 @@ def _dokka_configuration_test_impl(ctx): asserts.true(env, _contains_fragment(source_set["includes"], "module.md")) asserts.true(env, _contains_fragment(source_set["samples"], "Sample.kt")) asserts.true(env, _contains_fragment(source_set["suppressedFiles"], "Suppressed.kt")) - asserts.true(env, _contains_fragment(source_set["classpath"], "libdependency")) + asserts.true(env, _contains_fragment(source_set["classpath"], "dependency.abi.jar")) dokka_actions = _actions_with_mnemonic(env, "Dokka") asserts.equals(env, 1, len(dokka_actions)) @@ -117,11 +117,11 @@ def _dokka_configuration_test_impl(ctx): "Sample.kt", "Suppressed.kt", "analysis_fixture.dokka.json", - "libcustom_plugin.jar", + "custom_plugin.jar", "module.md", ]: asserts.true(env, expected_input in input_basenames) - asserts.true(env, _contains_fragment(input_basenames, "libdependency")) + asserts.true(env, "dependency.abi.jar" in input_basenames) return analysistest.end(env) @@ -255,7 +255,7 @@ def _dokka_multi_module_test_impl(ctx): ) asserts.true( env, - _contains_fragment(configuration["pluginsClasspath"], "libcustom_plugin.jar"), + _contains_fragment(configuration["pluginsClasspath"], "custom_plugin.jar"), ) modules = configuration["modules"] @@ -331,7 +331,7 @@ def _dokka_reusable_config_test_impl(ctx): ) asserts.true( env, - _contains_fragment(configuration["pluginsClasspath"], "libcustom_plugin.jar"), + _contains_fragment(configuration["pluginsClasspath"], "custom_plugin.jar"), ) asserts.false( env, @@ -364,7 +364,7 @@ def _dokka_reusable_config_test_impl(ctx): asserts.equals(env, 1, len(dokka_actions)) if dokka_actions: input_basenames = [file.basename for file in dokka_actions[0].inputs.to_list()] - asserts.true(env, "libcustom_plugin.jar" in input_basenames) + asserts.true(env, "custom_plugin.jar" in input_basenames) asserts.equals(env, "javadoc", target[DokkaInfo].format) return analysistest.end(env)