mirror of
https://github.com/InsanusMokrassar/KSLog.git
synced 2026-11-03 10:03:18 +00:00
build: add Kotlin Toolchain migration with Gradle fallback
This commit is contained in:
1 parent
0b9fb30862
commit
4d0f039787
58 files changed
+3115
-47
No files matched your search
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020-2021 JetBrains s.r.o. and and respective authors and developers.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,27 @@
|
||||
# KSLog Dokka Toolchain plugin
|
||||
|
||||
This local plugin is adapted from JetBrains Kotlin Toolchain's
|
||||
`build-sources/dokka` plugin at release `v0.13.0`, commit
|
||||
`abc7a5f7579e9ef267107782048c76777f0bf932`.
|
||||
|
||||
Upstream source: <https://github.com/JetBrains/kotlin-toolchain/tree/abc7a5f7579e9ef267107782048c76777f0bf932/build-sources/dokka>
|
||||
|
||||
Changes made for KSLog:
|
||||
|
||||
- models the KMP common/JVM/Android/JS/WasmJS/native source-set graph;
|
||||
- uses Dokka 2.2.0 to match the pre-migration build;
|
||||
- provides separate `dokkaHtml` and `dokkaGfm` commands;
|
||||
- writes to the preserved `build/dokka/html` and `build/dokka/gfm` paths;
|
||||
- preserves deprecated-member filtering and GitHub source links;
|
||||
- supplies the resolved JVM classpath plus transformed Common and matching
|
||||
JS/WasmJS standard-library KLIBs materialized by the documented prebuild;
|
||||
- fails generation if Dokka emits an unresolved-type placeholder, preventing
|
||||
the deployment workflow from uploading incomplete API signatures.
|
||||
|
||||
The legacy Gradle `DOKKA_PATH` override remains Gradle-only. The Toolchain
|
||||
plugin intentionally accepts only output directories below `build/dokka`,
|
||||
rejects symbolic-link ancestors, and verifies real-path containment before
|
||||
cleaning generated output.
|
||||
|
||||
The adapted Kotlin files retain the upstream Apache-2.0 header. See
|
||||
`LICENSE.txt` in this directory.
|
||||
@@ -0,0 +1,20 @@
|
||||
product: jvm/amper-plugin
|
||||
|
||||
description: KMP-capable Dokka HTML and GFM tasks adapted from the Kotlin Toolchain Dokka plugin.
|
||||
|
||||
pluginInfo:
|
||||
id: dokka
|
||||
settingsClass: dev.inmo.kslog.build.dokka.DokkaSettings
|
||||
|
||||
test-dependencies:
|
||||
- $libs.kotlin.test.junit
|
||||
|
||||
settings:
|
||||
junit: junit-4
|
||||
jvm:
|
||||
jdk:
|
||||
version: 17
|
||||
release: 17
|
||||
kotlin:
|
||||
version: 2.4.20
|
||||
serialization: json
|
||||
@@ -0,0 +1,37 @@
|
||||
tasks:
|
||||
html:
|
||||
action: !dev.inmo.kslog.build.dokka.dokkaHtml
|
||||
projectRootDir: ${project.rootDir}
|
||||
moduleRootDir: ${module.rootDir}
|
||||
moduleCompileClasspath: ${module.compileClasspath}
|
||||
runtimeClasspath:
|
||||
- $libs.dokka.cli
|
||||
pluginsClasspath:
|
||||
- $libs.dokka.base
|
||||
- $libs.dokka.analysis.kotlin.descriptors
|
||||
- $libs.kotlinx.html.jvm
|
||||
- $libs.freemarker
|
||||
outputDir: ${project.rootDir}/build/dokka/html
|
||||
settings: ${pluginSettings}
|
||||
|
||||
gfm:
|
||||
action: !dev.inmo.kslog.build.dokka.dokkaGfm
|
||||
projectRootDir: ${project.rootDir}
|
||||
moduleRootDir: ${module.rootDir}
|
||||
moduleCompileClasspath: ${module.compileClasspath}
|
||||
runtimeClasspath:
|
||||
- $libs.dokka.cli
|
||||
pluginsClasspath:
|
||||
- $libs.dokka.base
|
||||
- $libs.dokka.analysis.kotlin.descriptors
|
||||
- $libs.dokka.gfm
|
||||
- $libs.kotlinx.html.jvm
|
||||
- $libs.freemarker
|
||||
outputDir: ${project.rootDir}/build/dokka/gfm
|
||||
settings: ${pluginSettings}
|
||||
|
||||
commands:
|
||||
- name: dokkaHtml
|
||||
performedBy: html
|
||||
- name: dokkaGfm
|
||||
performedBy: gfm
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
/*
|
||||
* KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0
|
||||
* build-sources/dokka. KSLog extends the configuration model for its KMP
|
||||
* source-set graph and HTML/GFM plugin configuration. See ../README.md.
|
||||
*/
|
||||
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class DokkaConfiguration(
|
||||
val moduleName: String,
|
||||
val moduleVersion: String,
|
||||
val outputDir: String,
|
||||
val sourceSets: List<DokkaSourceSet>,
|
||||
val pluginsClasspath: List<String>,
|
||||
val pluginsConfiguration: List<PluginConfiguration>,
|
||||
val failOnWarning: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SourceSetId(
|
||||
val scopeId: String,
|
||||
val sourceSetName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class DokkaSourceSet(
|
||||
val sourceSetID: SourceSetId,
|
||||
val displayName: String,
|
||||
val dependentSourceSets: List<SourceSetId>,
|
||||
val classpath: List<String>,
|
||||
val sourceRoots: List<String>,
|
||||
val documentedVisibilities: List<String> = listOf("PUBLIC"),
|
||||
val skipDeprecated: Boolean,
|
||||
val jdkVersion: Int,
|
||||
val languageVersion: String = "2.4",
|
||||
val apiVersion: String = "2.4",
|
||||
val analysisPlatform: String,
|
||||
val sourceLinks: List<SourceLink>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SourceLink(
|
||||
val localDirectory: String,
|
||||
val remoteUrl: String,
|
||||
val remoteLineSuffix: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PluginConfiguration(
|
||||
val fqPluginName: String,
|
||||
val serializationFormat: String,
|
||||
val values: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class DokkaBase(
|
||||
val footerMessage: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
/*
|
||||
* KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0
|
||||
* build-sources/dokka. KSLog adds versioned KMP documentation settings and
|
||||
* project-specific source-link defaults. See ../README.md for provenance.
|
||||
*/
|
||||
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import org.jetbrains.amper.plugins.Configurable
|
||||
|
||||
@Configurable
|
||||
interface DokkaSettings {
|
||||
val moduleName: String get() = "KSLog"
|
||||
val moduleVersion: String
|
||||
val sourceLinkBaseUrl: String get() = "https://github.com/InsanusMokrassar/KSLog/blob/master/"
|
||||
val sourceLinkLineSuffix: String get() = "#L"
|
||||
val skipDeprecated: Boolean get() = true
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
/*
|
||||
* KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0
|
||||
* build-sources/dokka. KSLog adds KMP source sets, HTML/GFM entry points, and
|
||||
* project-confined, symlink-safe generated-output cleanup. See ../README.md.
|
||||
*/
|
||||
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.amper.plugins.Classpath
|
||||
import org.jetbrains.amper.plugins.ExecutionAvoidance
|
||||
import org.jetbrains.amper.plugins.Input
|
||||
import org.jetbrains.amper.plugins.Output
|
||||
import org.jetbrains.amper.plugins.TaskAction
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
@TaskAction(ExecutionAvoidance.Disabled)
|
||||
fun dokkaHtml(
|
||||
projectRootDir: String,
|
||||
moduleRootDir: String,
|
||||
@Input moduleCompileClasspath: Classpath,
|
||||
@Input runtimeClasspath: Classpath,
|
||||
@Input pluginsClasspath: Classpath,
|
||||
@Output outputDir: Path,
|
||||
settings: DokkaSettings,
|
||||
) = runDokka(
|
||||
projectRootDir,
|
||||
moduleRootDir,
|
||||
moduleCompileClasspath,
|
||||
runtimeClasspath,
|
||||
pluginsClasspath,
|
||||
outputDir,
|
||||
settings,
|
||||
)
|
||||
|
||||
@TaskAction(ExecutionAvoidance.Disabled)
|
||||
fun dokkaGfm(
|
||||
projectRootDir: String,
|
||||
moduleRootDir: String,
|
||||
@Input moduleCompileClasspath: Classpath,
|
||||
@Input runtimeClasspath: Classpath,
|
||||
@Input pluginsClasspath: Classpath,
|
||||
@Output outputDir: Path,
|
||||
settings: DokkaSettings,
|
||||
) = runDokka(
|
||||
projectRootDir,
|
||||
moduleRootDir,
|
||||
moduleCompileClasspath,
|
||||
runtimeClasspath,
|
||||
pluginsClasspath,
|
||||
outputDir,
|
||||
settings,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private fun runDokka(
|
||||
projectRootDir: String,
|
||||
moduleRootDir: String,
|
||||
moduleCompileClasspath: Classpath,
|
||||
runtimeClasspath: Classpath,
|
||||
pluginsClasspath: Classpath,
|
||||
outputDir: Path,
|
||||
settings: DokkaSettings,
|
||||
) {
|
||||
val normalizedRoot = Path.of(moduleRootDir).toRealPath()
|
||||
val normalizedOutput = prepareDokkaOutput(projectRootDir, outputDir)
|
||||
|
||||
val commonId = SourceSetId(settings.moduleName, "commonMain")
|
||||
val sourceLink = SourceLink(
|
||||
localDirectory = normalizedRoot.pathString,
|
||||
remoteUrl = settings.sourceLinkBaseUrl,
|
||||
remoteLineSuffix = settings.sourceLinkLineSuffix,
|
||||
)
|
||||
val analysisClasspaths = resolveDokkaAnalysisClasspaths(moduleCompileClasspath.resolvedFiles)
|
||||
|
||||
fun sourceSet(
|
||||
directoryName: String,
|
||||
sourceSetName: String,
|
||||
displayName: String,
|
||||
analysisPlatform: String,
|
||||
dependentOnCommon: Boolean,
|
||||
classpath: List<String> = emptyList(),
|
||||
): DokkaSourceSet? {
|
||||
val sourceRoot = normalizedRoot.resolve(directoryName)
|
||||
if (!sourceRoot.exists()) return null
|
||||
return DokkaSourceSet(
|
||||
sourceSetID = SourceSetId(settings.moduleName, sourceSetName),
|
||||
displayName = displayName,
|
||||
dependentSourceSets = if (dependentOnCommon) listOf(commonId) else emptyList(),
|
||||
classpath = classpath,
|
||||
sourceRoots = listOf(sourceRoot.pathString),
|
||||
skipDeprecated = settings.skipDeprecated,
|
||||
jdkVersion = if (displayName == "JVM") 8 else 17,
|
||||
analysisPlatform = analysisPlatform,
|
||||
sourceLinks = listOf(sourceLink),
|
||||
)
|
||||
}
|
||||
|
||||
val androidClasspath = buildList {
|
||||
addAll(analysisClasspaths.jvm)
|
||||
val sdkRoot = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
|
||||
if (sdkRoot != null) {
|
||||
val androidJar = Path.of(sdkRoot).resolve("platforms/android-37/android.jar")
|
||||
if (Files.isRegularFile(androidJar)) add(androidJar.pathString)
|
||||
}
|
||||
}
|
||||
|
||||
val sourceSets = listOfNotNull(
|
||||
sourceSet(
|
||||
"src",
|
||||
"commonMain",
|
||||
"Common",
|
||||
"common",
|
||||
dependentOnCommon = false,
|
||||
classpath = analysisClasspaths.commonNative,
|
||||
),
|
||||
sourceSet(
|
||||
"src@jvm",
|
||||
"jvmMain",
|
||||
"JVM",
|
||||
"jvm",
|
||||
dependentOnCommon = true,
|
||||
classpath = analysisClasspaths.jvm,
|
||||
),
|
||||
sourceSet("src@android", "androidMain", "Android", "jvm", dependentOnCommon = true, classpath = androidClasspath),
|
||||
sourceSet("src@js", "jsMain", "JS", "js", dependentOnCommon = true, classpath = analysisClasspaths.js),
|
||||
sourceSet(
|
||||
"src@wasmJs",
|
||||
"wasmJsMain",
|
||||
"WasmJS",
|
||||
"js",
|
||||
dependentOnCommon = true,
|
||||
classpath = analysisClasspaths.wasmJs,
|
||||
),
|
||||
sourceSet(
|
||||
"src@native",
|
||||
"nativeMain",
|
||||
"Native",
|
||||
"native",
|
||||
dependentOnCommon = true,
|
||||
classpath = analysisClasspaths.commonNative,
|
||||
),
|
||||
)
|
||||
|
||||
val configuration = DokkaConfiguration(
|
||||
moduleName = settings.moduleName,
|
||||
moduleVersion = settings.moduleVersion,
|
||||
outputDir = normalizedOutput.pathString,
|
||||
sourceSets = sourceSets,
|
||||
pluginsClasspath = pluginsClasspath.resolvedFiles.map { it.pathString },
|
||||
pluginsConfiguration = listOf(
|
||||
PluginConfiguration(
|
||||
fqPluginName = "org.jetbrains.dokka.base.DokkaBase",
|
||||
serializationFormat = "JSON",
|
||||
values = Json.encodeToString(DokkaBase(footerMessage = "KSLog ${settings.moduleVersion}")),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
val configurationFile = Files.createTempFile("kslog-dokka-", ".json")
|
||||
try {
|
||||
configurationFile.writeText(Json.encodeToString(configuration))
|
||||
val cliJar = runtimeClasspath.resolvedFiles.single()
|
||||
val commandLine = listOf(
|
||||
ProcessHandle.current().info().command().orElse("java"),
|
||||
"-jar",
|
||||
cliJar.pathString,
|
||||
configurationFile.pathString,
|
||||
)
|
||||
val process = ProcessBuilder(commandLine)
|
||||
.directory(normalizedRoot.toFile())
|
||||
.start()
|
||||
val capture = listOf(
|
||||
thread { process.inputStream.copyTo(System.out) },
|
||||
thread { process.errorStream.copyTo(System.err) },
|
||||
)
|
||||
val exitCode = process.waitFor()
|
||||
capture.forEach { it.join() }
|
||||
check(exitCode == 0) { "Dokka terminated with exit code $exitCode" }
|
||||
requireNoUnresolvedTypes(normalizedOutput)
|
||||
} finally {
|
||||
configurationFile.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal data class DokkaAnalysisClasspaths(
|
||||
val commonNative: List<String>,
|
||||
val jvm: List<String>,
|
||||
val js: List<String>,
|
||||
val wasmJs: List<String>,
|
||||
)
|
||||
|
||||
internal fun resolveDokkaAnalysisClasspaths(moduleClasspath: List<Path>): DokkaAnalysisClasspaths {
|
||||
val kotlinStdlib = checkNotNull(
|
||||
moduleClasspath.firstOrNull { path ->
|
||||
val version = path.parent?.fileName?.toString()
|
||||
version != null &&
|
||||
path.parent?.parent?.fileName?.toString() == "kotlin-stdlib" &&
|
||||
path.fileName?.toString() == "kotlin-stdlib-$version.jar"
|
||||
}
|
||||
) {
|
||||
"Cannot locate the Kotlin JVM stdlib in the module compile classpath; " +
|
||||
"it is required to discover the matching JS and WasmJS analysis KLIBs"
|
||||
}
|
||||
val versionDirectory = checkNotNull(kotlinStdlib.parent)
|
||||
val version = versionDirectory.fileName.toString()
|
||||
val kotlinGroupDirectory = checkNotNull(versionDirectory.parent?.parent)
|
||||
val mavenCacheDirectory = checkNotNull(kotlinGroupDirectory.parent?.parent?.parent)
|
||||
check(mavenCacheDirectory.fileName.toString() == ".m2.cache") {
|
||||
"Unexpected Kotlin Toolchain Maven cache layout for $kotlinStdlib"
|
||||
}
|
||||
val transformedStdlibDirectory = checkNotNull(mavenCacheDirectory.parent)
|
||||
.resolve("kotlin/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin/kotlin-stdlib/$version")
|
||||
val commonStdlib = if (Files.isDirectory(transformedStdlibDirectory)) {
|
||||
Files.walk(transformedStdlibDirectory, 2).use { paths ->
|
||||
paths
|
||||
.filter { Files.isRegularFile(it) }
|
||||
.filter { it.fileName.toString() == "kotlin-stdlib-commonMain-$version.klib" }
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElse(null)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun platformStdlib(artifactId: String): Path = kotlinGroupDirectory
|
||||
.resolve(artifactId)
|
||||
.resolve(version)
|
||||
.resolve("$artifactId-$version.klib")
|
||||
|
||||
val jsStdlib = platformStdlib("kotlin-stdlib-js")
|
||||
val wasmJsStdlib = platformStdlib("kotlin-stdlib-wasm-js")
|
||||
val missing = listOfNotNull(jsStdlib, wasmJsStdlib).filterNot(Files::isRegularFile)
|
||||
check(commonStdlib != null && missing.isEmpty()) {
|
||||
"Missing Kotlin analysis KLIB(s) for Dokka below $transformedStdlibDirectory or in " +
|
||||
"${missing.joinToString()}. " +
|
||||
"Run `./kotlin build -m kslog -p js -p wasmJs` before documentation generation."
|
||||
}
|
||||
|
||||
return DokkaAnalysisClasspaths(
|
||||
commonNative = listOf(commonStdlib.pathString),
|
||||
jvm = moduleClasspath.map { it.pathString },
|
||||
js = listOf(jsStdlib.pathString),
|
||||
wasmJs = listOf(wasmJsStdlib.pathString),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun requireNoUnresolvedTypes(outputDir: Path) {
|
||||
val unresolvedFiles = Files.walk(outputDir).use { paths ->
|
||||
paths
|
||||
.filter { Files.isRegularFile(it) }
|
||||
.filter { path ->
|
||||
path.fileName.toString().substringAfterLast('.', "") in setOf("html", "json", "md")
|
||||
}
|
||||
.filter { Files.readString(it).contains("Error class: unknown class") }
|
||||
.map { outputDir.relativize(it).pathString }
|
||||
.sorted()
|
||||
.toList()
|
||||
}
|
||||
check(unresolvedFiles.isEmpty()) {
|
||||
"Dokka generated unresolved type placeholders in ${unresolvedFiles.size} file(s): " +
|
||||
unresolvedFiles.joinToString(limit = 10)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
internal fun prepareDokkaOutput(projectRootDir: String, outputDir: Path): Path {
|
||||
val configuredProjectRoot = Path.of(projectRootDir).toAbsolutePath().normalize()
|
||||
require(Files.isDirectory(configuredProjectRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||
"Project root must be a real directory, not a symbolic link: $configuredProjectRoot"
|
||||
}
|
||||
|
||||
val configuredGeneratedRoot = configuredProjectRoot.resolve("build/dokka").normalize()
|
||||
val configuredOutput = outputDir.toAbsolutePath().normalize()
|
||||
require(configuredOutput.parent == configuredGeneratedRoot) {
|
||||
"Refusing to clean Dokka output outside a direct child of $configuredGeneratedRoot: $configuredOutput"
|
||||
}
|
||||
|
||||
val realProjectRoot = configuredProjectRoot.toRealPath()
|
||||
val relativeOutput = configuredProjectRoot.relativize(configuredOutput)
|
||||
val generatedRoot = realProjectRoot.resolve("build/dokka").normalize()
|
||||
val output = realProjectRoot.resolve(relativeOutput).normalize()
|
||||
require(output.parent == generatedRoot) {
|
||||
"Resolved Dokka output escaped $generatedRoot: $output"
|
||||
}
|
||||
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
Files.createDirectories(generatedRoot)
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
|
||||
val realGeneratedRoot = generatedRoot.toRealPath()
|
||||
require(realGeneratedRoot.startsWith(realProjectRoot)) {
|
||||
"Resolved Dokka root escaped the project: $realGeneratedRoot"
|
||||
}
|
||||
|
||||
if (Files.exists(output, LinkOption.NOFOLLOW_LINKS)) {
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
val realOutput = output.toRealPath()
|
||||
require(realOutput.parent == realGeneratedRoot) {
|
||||
"Resolved Dokka output escaped $realGeneratedRoot: $realOutput"
|
||||
}
|
||||
output.deleteRecursively()
|
||||
}
|
||||
|
||||
output.createDirectories()
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
val createdOutput = output.toRealPath()
|
||||
require(createdOutput.parent == realGeneratedRoot) {
|
||||
"Created Dokka output escaped $realGeneratedRoot: $createdOutput"
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
private fun requireNoSymbolicLinkAncestors(root: Path, target: Path) {
|
||||
require(target.startsWith(root)) { "Path is outside the project root: $target" }
|
||||
var current = root
|
||||
for (component in root.relativize(target)) {
|
||||
current = current.resolve(component)
|
||||
require(!Files.isSymbolicLink(current)) {
|
||||
"Refusing Dokka cleanup through symbolic link: $current"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
class RunDokkaOutputGuardTest {
|
||||
@Test
|
||||
fun rejectsSymlinkedDokkaAncestorWithoutTouchingOutsideFiles() {
|
||||
val projectRoot = Files.createTempDirectory("kslog-dokka-project-")
|
||||
val outside = Files.createTempDirectory("kslog-dokka-outside-")
|
||||
val sentinel = outside.resolve("sentinel.txt")
|
||||
sentinel.writeText("preserve me")
|
||||
val build = projectRoot.resolve("build").createDirectories()
|
||||
val dokkaLink = build.resolve("dokka")
|
||||
Files.createSymbolicLink(dokkaLink, outside)
|
||||
|
||||
try {
|
||||
val failure = assertFailsWith<IllegalArgumentException> {
|
||||
prepareDokkaOutput(projectRoot.toString(), dokkaLink.resolve("html"))
|
||||
}
|
||||
assertTrue(failure.message.orEmpty().contains("symbolic link"))
|
||||
assertEquals("preserve me", sentinel.readText())
|
||||
} finally {
|
||||
Files.deleteIfExists(dokkaLink)
|
||||
projectRoot.deleteRecursively()
|
||||
outside.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleansOnlyTheSelectedGeneratedOutputDirectory() {
|
||||
val projectRoot = Files.createTempDirectory("kslog-dokka-project-")
|
||||
val html = projectRoot.resolve("build/dokka/html").createDirectories()
|
||||
val stale = html.resolve("stale.txt")
|
||||
stale.writeText("old")
|
||||
val gfmSentinel = projectRoot.resolve("build/dokka/gfm/sentinel.txt")
|
||||
gfmSentinel.parent.createDirectories()
|
||||
gfmSentinel.writeText("keep")
|
||||
|
||||
try {
|
||||
val prepared = prepareDokkaOutput(projectRoot.toString(), html)
|
||||
assertEquals(html.toRealPath(), prepared.toRealPath())
|
||||
assertTrue(prepared.exists())
|
||||
assertFalse(stale.exists())
|
||||
assertEquals("keep", gfmSentinel.readText())
|
||||
} finally {
|
||||
projectRoot.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsGeneratedDocumentationWithUnresolvedTypePlaceholders() {
|
||||
val output = Files.createTempDirectory("kslog-dokka-output-")
|
||||
val generatedPage = output.resolve("broken.md")
|
||||
generatedPage.writeText("fun example(): <Error class: unknown class>")
|
||||
|
||||
try {
|
||||
val failure = assertFailsWith<IllegalStateException> {
|
||||
requireNoUnresolvedTypes(output)
|
||||
}
|
||||
assertTrue(failure.message.orEmpty().contains("broken.md"))
|
||||
} finally {
|
||||
output.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolvesMatchingCommonJsAndWasmAnalysisKlibsFromTheToolchainCache() {
|
||||
val cache = Files.createTempDirectory("kslog-dokka-cache-")
|
||||
val kotlinGroup = cache.resolve(".m2.cache/org/jetbrains/kotlin")
|
||||
val version = "2.4.20"
|
||||
val kotlinStdlib = kotlinGroup
|
||||
.resolve("kotlin-stdlib/$version/kotlin-stdlib-$version.jar")
|
||||
val commonStdlib = cache.resolve(
|
||||
"kotlin/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin/" +
|
||||
"kotlin-stdlib/$version/test-hash/kotlin-stdlib-commonMain-$version.klib"
|
||||
)
|
||||
val jsStdlib = kotlinGroup
|
||||
.resolve("kotlin-stdlib-js/$version/kotlin-stdlib-js-$version.klib")
|
||||
val wasmJsStdlib = kotlinGroup
|
||||
.resolve("kotlin-stdlib-wasm-js/$version/kotlin-stdlib-wasm-js-$version.klib")
|
||||
val dependency = cache.resolve("dependency.jar")
|
||||
listOf(kotlinStdlib, commonStdlib, jsStdlib, wasmJsStdlib).forEach {
|
||||
it.parent.createDirectories()
|
||||
it.writeText("fixture")
|
||||
}
|
||||
dependency.writeText("fixture")
|
||||
|
||||
try {
|
||||
val classpaths = resolveDokkaAnalysisClasspaths(listOf(kotlinStdlib, dependency))
|
||||
assertEquals(listOf(commonStdlib.toString()), classpaths.commonNative)
|
||||
assertEquals(listOf(kotlinStdlib.toString(), dependency.toString()), classpaths.jvm)
|
||||
assertEquals(listOf(jsStdlib.toString()), classpaths.js)
|
||||
assertEquals(listOf(wasmJsStdlib.toString()), classpaths.wasmJs)
|
||||
} finally {
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# KSLog GitHub release Toolchain plugin
|
||||
|
||||
Kotlin Toolchain 0.13.0 has no built-in GitHub release task. This local plugin
|
||||
keeps the existing explicit release capability as a narrow wrapper around the
|
||||
standard `gh release create` CLI.
|
||||
|
||||
It extracts the matching version section from `CHANGELOG.md`, preserves the
|
||||
legacy tag/title/target conventions, accepts the existing
|
||||
`GITHUB_RELEASE_TOKEN` variable plus standard `GH_TOKEN`/`GITHUB_TOKEN`, and
|
||||
supports a non-network `KSLOG_GITHUB_RELEASE_DRY_RUN=true` validation mode. It
|
||||
runs the committed release/version consistency preflight before rendering or
|
||||
creating a release. No CI workflow invokes it.
|
||||
|
||||
`gh release create` does not modify local git state, but GitHub may create the
|
||||
remote tag at `--target` when the requested tag does not already exist. A real
|
||||
invocation therefore mutates both the remote release and potentially the remote
|
||||
tag; dry-run mode performs neither action.
|
||||
@@ -0,0 +1,15 @@
|
||||
product: jvm/amper-plugin
|
||||
|
||||
description: Explicit GitHub release command backed by the standard GitHub CLI.
|
||||
|
||||
pluginInfo:
|
||||
id: github-release
|
||||
settingsClass: dev.inmo.kslog.build.release.GitHubReleaseSettings
|
||||
|
||||
settings:
|
||||
jvm:
|
||||
jdk:
|
||||
version: 17
|
||||
release: 17
|
||||
kotlin:
|
||||
version: 2.4.20
|
||||
@@ -0,0 +1,9 @@
|
||||
tasks:
|
||||
release:
|
||||
action: !dev.inmo.kslog.build.release.githubRelease
|
||||
projectRootDir: ${project.rootDir}
|
||||
settings: ${pluginSettings}
|
||||
|
||||
commands:
|
||||
- name: githubRelease
|
||||
performedBy: release
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.inmo.kslog.build.release
|
||||
|
||||
import org.jetbrains.amper.plugins.ExecutionAvoidance
|
||||
import org.jetbrains.amper.plugins.TaskAction
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
@TaskAction(ExecutionAvoidance.Disabled)
|
||||
fun githubRelease(
|
||||
projectRootDir: String,
|
||||
settings: GitHubReleaseSettings,
|
||||
) {
|
||||
val root = Path.of(projectRootDir).toAbsolutePath().normalize()
|
||||
val changelog = root.resolve(settings.changelogFile).normalize()
|
||||
require(changelog.startsWith(root) && changelog.isRegularFile()) {
|
||||
"Changelog must be an existing file inside the module: $changelog"
|
||||
}
|
||||
|
||||
val releasePreflight = root.resolve("verification/check_release_consistency.py")
|
||||
require(releasePreflight.isRegularFile()) { "Missing release consistency preflight: $releasePreflight" }
|
||||
runCommand(root, listOf("python3", releasePreflight.pathString))
|
||||
|
||||
val parser = root.resolve("changelog_parser.sh")
|
||||
require(parser.isRegularFile()) { "Missing changelog parser: $parser" }
|
||||
val notes = runCommand(root, listOf("bash", parser.pathString, settings.version, changelog.pathString))
|
||||
.trim()
|
||||
require(notes.isNotEmpty()) { "No CHANGELOG.md section found for ${settings.version}" }
|
||||
|
||||
val tag = "${settings.tagPrefix}${settings.version}"
|
||||
val targetCommitish = settings.targetCommitish.ifBlank { settings.version }
|
||||
val repository = "${settings.owner}/${settings.repository}"
|
||||
if (System.getenv("KSLOG_GITHUB_RELEASE_DRY_RUN").toBoolean()) {
|
||||
println("Dry run: GitHub release $repository tag=$tag target=$targetCommitish title=${settings.version}")
|
||||
println(notes)
|
||||
return
|
||||
}
|
||||
|
||||
val token = System.getenv("GITHUB_RELEASE_TOKEN")
|
||||
?: System.getenv("GH_TOKEN")
|
||||
?: System.getenv("GITHUB_TOKEN")
|
||||
?: error("Set GITHUB_RELEASE_TOKEN, GH_TOKEN, or GITHUB_TOKEN before creating a release")
|
||||
|
||||
val notesFile = Files.createTempFile("kslog-release-notes-", ".md")
|
||||
try {
|
||||
notesFile.writeText(notes)
|
||||
val process = ProcessBuilder(
|
||||
"gh",
|
||||
"release",
|
||||
"create",
|
||||
tag,
|
||||
"--repo",
|
||||
repository,
|
||||
"--title",
|
||||
settings.version,
|
||||
"--target",
|
||||
targetCommitish,
|
||||
"--notes-file",
|
||||
notesFile.pathString,
|
||||
)
|
||||
.directory(root.toFile())
|
||||
.redirectErrorStream(true)
|
||||
process.environment()["GH_TOKEN"] = token
|
||||
val started = process.start()
|
||||
val output = started.inputStream.bufferedReader().use { it.readText() }
|
||||
val exitCode = started.waitFor()
|
||||
check(exitCode == 0) { "gh release create failed with exit code $exitCode:\n$output" }
|
||||
print(output)
|
||||
} finally {
|
||||
notesFile.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommand(workingDirectory: Path, command: List<String>): String {
|
||||
val process = ProcessBuilder(command)
|
||||
.directory(workingDirectory.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }
|
||||
val exitCode = process.waitFor()
|
||||
check(exitCode == 0) { "Command failed with exit code $exitCode: ${command.first()}\n$output" }
|
||||
return output
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.kslog.build.release
|
||||
|
||||
import org.jetbrains.amper.plugins.Configurable
|
||||
|
||||
@Configurable
|
||||
interface GitHubReleaseSettings {
|
||||
val owner: String
|
||||
val repository: String
|
||||
val version: String
|
||||
val tagPrefix: String get() = "v"
|
||||
val targetCommitish: String get() = ""
|
||||
val changelogFile: String get() = "CHANGELOG.md"
|
||||
}
|
||||
Reference in new issue
Block a user