Compare commits

..
Author SHA1 Message Date
renovate[bot] 7fc57f6858 Update dependency com.android.tools.build:gradle to v9.5.0 2026-10-28 16:32:41 +00:00
58 changed files with 48 additions and 3123 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
*.kt text eol=lf
kotlin text eol=lf
kotlin.bat text eol=crlf
-60
View File
@@ -1,60 +0,0 @@
name: Build and test
on:
push:
pull_request:
permissions:
contents: read
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
KOTLIN_CLI_NO_WELCOME_BANNER: "1"
jobs:
validation:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: "17"
cache: gradle
- uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
with:
packages: platform-tools
- name: Install the declared Android SDK
run: sdkmanager "platforms;android-37.0" "build-tools;37.0.0"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/JetBrains/Kotlin
key: kotlin-toolchain-${{ runner.os }}-${{ hashFiles('kotlin', 'kslog/module.yaml', 'project.yaml', 'gradle/libs.versions.toml', 'plugins/**/*.yaml', 'plugins/**/*.kt') }}
restore-keys: |
kotlin-toolchain-${{ runner.os }}-
- name: Check release/version consistency
run: |
python3 verification/check_release_consistency.py
bash verification/test_changelog_parser.sh
python3 verification/check_wrapper_line_endings.py
- name: Load Kotlin Toolchain model
run: ./kotlin show modules
- name: Validate Gradle Dokka output overrides
run: |
./gradlew --no-daemon help --task dokkaHtml -PDOKKA_PATH="$RUNNER_TEMP/kslog-dokka-property"
DOKKA_PATH="$RUNNER_TEMP/kslog-dokka-environment" ./gradlew --no-daemon help --task dokkaHtml
- name: Build Kotlin Toolchain targets
run: ./kotlin build
- name: Run Kotlin Toolchain checks
run: ./kotlin check
- name: Build the complete retained Gradle fallback
run: ./gradlew --no-daemon clean build
- name: Verify isolated Gradle publication and consumers
run: bash verification/verify_gradle_fallback.sh
+8 -60
View File
@@ -1,73 +1,21 @@
name: Publish KDocs name: Publish KDocs
on: on:
push: push:
branches: branches:
- master - master
permissions:
contents: read
concurrency:
group: kdocs-${{ github.ref }}
cancel-in-progress: false
env:
KOTLIN_CLI_NO_WELCOME_BANNER: "1"
jobs: jobs:
generate: publishing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps: steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: actions/checkout@v2
- uses: actions/setup-java@v1
with: with:
persist-credentials: false java-version: 17
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4 - name: Build
with: run: ./gradlew dokkaHtml
distribution: temurin
java-version: "17"
- uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
with:
packages: platform-tools
- name: Install the declared Android SDK
run: sdkmanager "platforms;android-37.0" "build-tools;37.0.0"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.cache/JetBrains/Kotlin
key: kotlin-toolchain-${{ runner.os }}-${{ hashFiles('kotlin', 'kslog/module.yaml', 'project.yaml', 'gradle/libs.versions.toml', 'plugins/dokka/**/*.yaml', 'plugins/dokka/**/*.kt') }}
restore-keys: |
kotlin-toolchain-${{ runner.os }}-
- name: Materialize platform analysis classpaths
run: ./kotlin build -m kslog -p js -p wasmJs
- name: Generate KDocs
run: ./kotlin do dokkaHtml
- name: Upload generated KDocs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: kdocs-html
path: build/dokka/html
if-no-files-found: error
retention-days: 1
deploy:
needs: generate
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download generated KDocs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: kdocs-html
path: kdocs-html
- name: Publish KDocs - name: Publish KDocs
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 uses: peaceiris/actions-gh-pages@v3
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./kdocs-html publish_dir: ./build/dokka/html
publish_branch: kdocs publish_branch: kdocs
+29
View File
@@ -0,0 +1,29 @@
name: Publish package to GitHub Packages
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
- name: Build
run: ./gradlew build
- name: Publish to Gitea
continue-on-error: true
run: ./gradlew publishAllPublicationsToGiteaRepository
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Publish
continue-on-error: true
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
-148
View File
@@ -1,148 +0,0 @@
# Developing KSLog
## Prerequisites
- JDK 17
- Android SDK platform 37 and build tools 37.0.0
(`sdkmanager` package IDs: `platforms;android-37.0` and `build-tools;37.0.0`;
the library's `compileSdk` remains 37)
- Git
- Bash and Python 3 for the committed compatibility verifiers
The checked-in `kotlin` and `kotlin.bat` wrappers download the pinned Kotlin
Toolchain distribution into the normal user cache. No global Toolchain install
is required.
## Build and test
```shell
./kotlin show modules
./kotlin clean
./kotlin build
./kotlin check
```
Build one supported platform with `./kotlin build -p <platform>`, where the
declared platforms are `android`, `js`, `jvm`, `linuxArm64`, `linuxX64`,
`mingwX64`, and `wasmJs`. Run the real JVM tests directly with:
```shell
./kotlin test -p jvm
```
Kotlin Toolchain 0.13.0 does not expose `linuxArm32Hfp`. Until that target and
the existing `kslog-wasm-js` artifact name can be represented, the Gradle build
remains the full-contract fallback:
```shell
./gradlew --no-daemon build
./gradlew --no-daemon compileKotlinLinuxArm32Hfp
```
Do not remove the Gradle path merely because the supported Toolchain targets
compile. See `MIGRATION_REPORT.md` for the exact capability gaps and validation
matrix.
Run the release/version preflight independently with:
```shell
python3 verification/check_release_consistency.py
bash verification/test_changelog_parser.sh
```
CI and the explicit GitHub release command run the same consistency check.
## Documentation
```shell
./kotlin build -m kslog -p js -p wasmJs
./kotlin do dokkaHtml
./kotlin do dokkaGfm
```
The outputs are `build/dokka/html` and `build/dokka/gfm`. CI only deploys HTML
documentation from `master`; pull requests and other branch pushes never deploy
documentation. Toolchain 0.13.0 exposes only the JVM module classpath to local
plugins, so the platform build first materializes the transformed Common plus
matching JS and WasmJS standard-library KLIBs in the Toolchain cache. The Dokka
plugin discovers those KLIBs from the resolved JVM stdlib version and fails with
the prebuild command above if any one is missing.
The legacy Gradle-only `DOKKA_PATH` property/environment override is not
implemented by the Toolchain plugin. Toolchain documentation is deliberately
restricted to generated directories under `build/dokka` so cleanup cannot
target an arbitrary external path. Use the retained Gradle path when that
legacy override is required.
## Local publication and compatibility testing
Publication is enabled for the KSLog module, while the two local build-plugin
modules are intentionally not publishable. Select the library module explicitly:
```shell
isolated_repo="$(mktemp -d)"
KOTLIN_CLI_JAVA_OPTIONS="-Dmaven.repo.local=$isolated_repo" \
KOTLIN_TOOLCHAIN_SIGNING_KEY="$(gpg --armor --export-secret-keys YOUR_KEY_ID)" \
./kotlin publish -m kslog mavenLocal
```
Use a throwaway key for local validation. Never use the command above without
the isolated Maven path for version 2.1.0, because it would overwrite the normal
local copy of that released version.
The retained Gradle publication can be checked without credentials or signing
keys against an archive of the exact migration base:
```shell
bash verification/verify_gradle_fallback.sh
```
The verifier creates isolated Gradle user homes and Maven repositories, checks
the baseline/current 46-file publication inventories, source JARs, required POM
metadata, source bytes, JVM/Android public method and field signatures, module
identities, and compiles the committed consumer probe. It never writes to the
normal Maven local repository.
## Remote publication and release
The validation workflow used for pushes and pull requests never publishes
packages or creates tags/releases. The separate `master`-only KDocs workflow
intentionally deploys generated HTML to the `kdocs` branch after a read-only
generation job succeeds. Toolchain remote package publication is disabled in
the resolved model: the module exposes only `mavenLocal`, has no Inmo Nexus
repository/credentials, and does not enable Maven Central. `mavenLocal` is not
inherently isolated; without the `-Dmaven.repo.local=...` JVM override shown
above it writes to the user's normal Maven Local repository. There is no
configured Toolchain destination that uploads packages remotely.
Do not re-enable Toolchain Nexus or Central publication until both the missing
Linux ARM32 target and the `kslog-wasm-js` coordinate have compatible
implementations and the complete publication diff has been reviewed again.
The retained Gradle build is the only full-target remote publication path. Its
existing release tasks remain explicit maintainer operations and are not run by
CI:
```shell
INMONEXUS_USER=... INMONEXUS_PASSWORD=... \
./gradlew --no-daemon publishAllPublicationsToInmoNexusRepository
SONATYPE_USER=... SONATYPE_PASSWORD=... \
./gradlew --no-daemon publishAllPublicationsToSonatypeRepository
```
Those commands require the existing Gradle/GPG release configuration and cause
real uploads. They are documented only to preserve the fallback release
contract; do not run them as migration validation.
Preview the GitHub release payload without network mutation:
```shell
KSLOG_GITHUB_RELEASE_DRY_RUN=true ./kotlin do githubRelease
```
A real release requires `GITHUB_RELEASE_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN`
and an explicit `./kotlin do githubRelease` invocation. The command first runs
the release/version consistency preflight, then delegates to the standard
`gh release create` CLI. It may create the remote tag when that tag is absent;
it does not alter local git state. Validation CI never invokes it.
-159
View File
@@ -1,159 +0,0 @@
# KSLog Gradle to Kotlin Toolchain migration plan
This inventory was written before adding any Kotlin Toolchain YAML. It records the
Gradle contract at commit `0b9fb30862c7319bb228039ee33eef9a0ddef95c` and is the
checklist for the migration.
## Pinned inputs
- Kotlin Toolchain release: `v0.13.0`, source commit
`abc7a5f7579e9ef267107782048c76777f0bf932`.
- Official wrapper from that tag: CLI distribution `0.13.0-dev-4444`, SHA-256
`13c2448146ac6dba0fa3a4e5d367dab73d8cfd7400bab348bebe905c165adecf`.
- Kotlin: `2.4.20` (set explicitly even though the release defaults to it).
- Dokka: `2.2.0`, source commit
`656ca46fbbd676d872b0bd383042fc12ae7adcdd`.
- Migration guidance: the four official Kotlin agent skills installed from
`Kotlin/kotlin-agent-skills@087f5689c1934135ed58718cdf524525e452b7e9`.
The pinned Toolchain documentation and source confirm that nested module
templates are supported in 0.13.0. This resolves the contradictory statement in
the core agent skill, although this one-module project does not need a template
hierarchy.
## Existing project contract
### Source and API
- One Kotlin Multiplatform library with 20 production Kotlin files and no tests.
- Source sets: `commonMain`, `jvmMain`, `androidMain`, `jsMain`, `wasmJsMain`,
and `nativeMain`.
- Production source bytes must remain unchanged. Moving source roots to the
stable `kslog` Toolchain module's KMP layout is allowed; legacy Gradle
source-set paths will be mapped to those directories while the compatibility
fallback remains.
- Public package/API/behavior must not change.
### Targets and execution environments
| Existing declaration | Required Toolchain mapping | Status in Toolchain 0.13.0 |
|---|---|---|
| JVM, bytecode 8 | `jvm`, target-specific `-jvm-target=1.8` | Supported; verified class major 52 |
| Android, bytecode 17 | `android`, target-specific `-jvm-target=17` | Supported; verified class major 61 |
| Android min/compile/build tools 21/37/37.0.0 | `settings.android` | Supported |
| JS IR, browser + Node.js | `js` | Library compilation/publication supported; runtime selection is not configurable |
| WasmJS, browser + Node.js + d8 | `wasmJs` | Library compilation/publication and browser tests supported; Node.js/d8 runners are not exposed |
| linuxX64 | `linuxX64` | Supported |
| linuxArm64 | `linuxArm64` | Supported (cross-compilation on this host) |
| linuxArm32Hfp | none | **Blocked:** not present in the 0.13.0 platform schema |
| mingwX64 | `mingwX64` | Supported (cross-compilation on this host) |
| Shared native implementation | `src@native` | Supported |
The pinned `Platform` enum omits `linuxArm32Hfp`; a minimal model-load probe
reports an empty effective platform list at that value. The probe remains
outside the active project model and its result is recorded in
`MIGRATION_REPORT.md`.
Toolchain 0.13.0 does not allow `settings.jvm.release` to vary by platform, so
the common value is unset and the JVM/Android compiler targets are supplied as
platform-specific Kotlin arguments. Inspected output proves the required
bytecode levels.
### Build plugins and custom logic
| Gradle plugin/feature | Decision | Replacement/justification |
|---|---|---|
| Kotlin Multiplatform | Native | `product.type: kmp/lib` and explicit platforms |
| Android KMP library plugin | Native | Android platform in the KMP product |
| Maven Publish | Native for isolated local validation | `settings.publishing` plus the sole publish repository `mavenLocal`; Toolchain remotes stay disabled while parity is blocked |
| NMCP aggregation/Central Portal | Retained in Gradle only | Toolchain `mavenCentral` remains at its disabled default; the complete Gradle fallback preserves the existing remote path |
| Signing plugin/GPG command | Native | `signArtifacts` and Toolchain signing environment variables |
| Dokka Gradle plugin | Local plugin | Vendor/extend the official Toolchain Dokka build plugin; add KMP source sets, HTML and GFM commands |
| GitHub release Gradle plugin | Local plugin using standard `gh` CLI | Explicit `githubRelease` command, dry-run support, no automatic release |
| Ben Manes versions plugin | Drop | Dependency maintenance only; Renovate remains |
| Dexcount | Drop from Toolchain path | No library/API/publication output; the Toolchain has no native equivalent |
| `javadocsJar` | Partial native publication behavior | Toolchain 0.13.0 publishes placeholder Javadoc JARs for platform artifacts but omits the root `kslog-2.1.0-javadoc.jar` |
| `changelog_parser.sh` | Preserve | Used by the GitHub release command |
The official Toolchain Dokka plugin was reviewed first. It explicitly says it
does not support KMP and only exposes HTML. The local adaptation keeps its
Apache-2.0 headers and provenance, models the actual KSLog source-set graph, and
adds the official Dokka GFM plugin. No existing Toolchain/GitHub-release plugin
was found; `gh` is already installed and is the standard implementation used by
the thin release action.
### Publication contract
- Group/version/base artifact: `dev.inmo:kslog:2.1.0`.
- Existing variant artifact IDs:
- `kslog-android`
- `kslog-js`
- `kslog-jvm`
- `kslog-linuxarm32hfp`
- `kslog-linuxarm64`
- `kslog-linuxx64`
- `kslog-mingwx64`
- `kslog-wasm-js`
- Publish KMP root metadata, per-platform artifacts, sources JARs, and empty
javadoc JARs.
- Preserve POM name/description/homepage, both developers, MIT license, and SCM
URL/developer connection.
- Preserve complete Inmo Nexus/Maven Central/signing capability through the
retained Gradle fallback and preserve the explicit GitHub release command.
Toolchain remote repositories stay absent/disabled until parity is restored.
Never run a remote publication/release as migration validation.
- Validate to an isolated Maven local directory via
`-Dmaven.repo.local=<temporary path>`; never overwrite normal `~/.m2` artifacts.
Known 0.13.0 publication mismatch: built-in Toolchain publication derives the
Wasm artifact suffix from `wasmJs` as `-wasmjs`, while Kotlin Gradle publishes
`-wasm-js`. Together with the missing Linux ARM32 target, this prevents a
publication-compatible cutover. The legacy Gradle files therefore remain until
these two blockers are resolved; the Toolchain implementation is a reversible,
working supported-target migration rather than a claim of full parity.
The stable `kslog` module directory plus narrow compiler-identity overrides
preserve the existing JVM Kotlin module and KLIB unique/short names. Remaining
built-in POM, Javadoc-placeholder, AAR-layout, and JS manifest-shape differences
are itemized in `MIGRATION_REPORT.md`.
### CI and documentation contract
- Replace the stale push workflow that names nonexistent Gitea and
GitHub-Packages Gradle tasks with build/check validation only.
- Run the complete retained Gradle build plus an isolated baseline/current
local-publication, public-signature, and consumer verifier in validation CI.
- Push and pull-request validation must not publish artifacts, docs, tags, or
releases.
- Keep documentation deployment master-only and change generation to the
Toolchain `dokkaHtml` command with output `build/dokka/html`.
- Generate docs in a read-only job without persisted checkout credentials, then
deploy the generated artifact in a separate write-enabled job.
- Keep `dokkaGfm` with output `build/dokka/gfm` for local/consumer workflows.
- Remote publication and GitHub release remain explicit manual commands and are
documented, not automatically broadened.
## Implementation checklist
- [x] Add the checked-in 0.13.0 wrappers.
- [x] Add `project.yaml`, stable `kslog/module.yaml`, and extend the existing version catalog.
- [x] Move source roots byte-for-byte to Toolchain KMP layout.
- [x] Map legacy Gradle source sets to the moved roots while blockers remain.
- [x] Add focused JVM behavior tests so validation is not `NO-SOURCE`.
- [x] Vendor and adapt the official Dokka plugin with source/commit/license.
- [x] Add the narrow GitHub release plugin with a non-network dry-run.
- [x] Load the project model and enumerate modules, platforms, commands, and checks.
- [x] Clean-build every declared Toolchain-supported target.
- [x] Run real tests/checks and distinguish host-unrunnable targets.
- [x] Generate HTML and GFM docs at the preserved paths.
- [x] Publish Toolchain artifacts with an ephemeral signing key to an isolated
Maven repository before disabling all Toolchain remote destinations.
- [x] Inspect artifacts, signatures, POMs, and Gradle module metadata.
- [x] Add a committed verifier that republishes the untouched base and the
post-move Gradle fallback with isolated Gradle homes and no signing secrets.
- [x] Run Gradle KMP and Android consumer smoke tests against the isolated repository.
- [x] Compare production source hashes and JVM/Android public bytecode/API with Gradle.
- [x] Validate CI YAML, shell scripts, and `git diff --check`.
- [x] Rebuild `ast-index` after source moves and edits.
- [x] Record exact commands, results, logs, blockers, and remote-only checks in
`MIGRATION_REPORT.md`.
-278
View File
@@ -1,278 +0,0 @@
# KSLog Kotlin Toolchain migration report
Date: 2026-11-02
Base commit: `0b9fb30862c7319bb228039ee33eef9a0ddef95c`
Kotlin Toolchain: `v0.13.0` source commit
`abc7a5f7579e9ef267107782048c76777f0bf932`
## Result
KSLog now has a checked-in, release-pinned Kotlin Toolchain project that loads,
builds every platform supported by Toolchain 0.13.0, runs real JVM tests,
generates both documentation formats, can publish signed KMP artifacts through
the configured `mavenLocal` destination, and is consumable by Gradle KMP and
Android projects. Migration validation redirects Maven Local to a temporary
repository; the destination itself is not inherently isolated. Toolchain Nexus
credentials/repository configuration is absent and resolved Maven Central
publishing is disabled.
This is **not yet a publication-compatible full cutover**. Toolchain 0.13.0
cannot represent `linuxArm32Hfp` and hard-codes the WasmJS artifact suffix as
`-wasmjs` instead of KSLog's released `-wasm-js`. The Gradle build is therefore
retained, mapped to the moved sources, and validated as the complete target and
publication fallback. No remote artifact, documentation, tag, or release was
published during this migration.
## Implemented layout
- `kotlin` and `kotlin.bat` preserve the official 0.13.0 wrapper content. Git
attributes enforce LF for `kotlin`/Kotlin sources and CRLF for `kotlin.bat`.
The wrappers select CLI `0.13.0-dev-4444` with the official pinned
distribution SHA-256.
- `project.yaml` registers the stable `kslog` library module and two local build
plugin modules. The stable directory name prevents compiler module identity
from depending on the checkout/worktree directory name.
- `kslog/module.yaml` declares Android, JS, JVM, WasmJS, Linux x64/ARM64, and
MinGW x64, plus Android/JDK/Kotlin settings and full publication metadata.
- The 20 production files moved byte-for-byte into `kslog/src*` Toolchain
source roots. `build.gradle` maps the retained Gradle source sets to those
roots, including Linux ARM32.
- Compiler module identities are pinned to the existing published identities:
JVM/Android `META-INF/dev.inmo_kslog.kotlin_module`, leaf KLIB
`unique_name=dev.inmo:kslog`, common metadata `kslog_commonMain`, and native
metadata `dev.inmo:kslog_nativeMain`/`kslog_nativeMain`.
- `kslog/test@jvm/CallbackKSLogTest.kt` adds two focused tests covering exact
callback forwarding and rejected lazy-message evaluation.
- `plugins/dokka` adapts the official Apache-2.0 Toolchain Dokka plugin for
KSLog's KMP graph and Dokka 2.2.0 HTML/GFM output. Each adapted Apache source
carries a prominent KSLog modification/provenance notice, and cleanup rejects
symbolic-link ancestors and requires real-path containment. Platform analysis
receives JVM/common, JS, and WasmJS standard-library classpaths. Because the
plugin API only exposes the JVM module classpath, the documented JS/WasmJS
prebuild materializes the transformed Common and matching platform KLIBs for
deterministic discovery. Generation
fails before deployment if unresolved-type placeholders remain.
- `plugins/github-release` provides an explicit `gh release create` command and
a non-network dry-run mode. It runs the committed version consistency
preflight first; no workflow invokes it.
- `DEVELOPMENT.md` documents Toolchain use, the Gradle fallback, isolated local
publication, disabled Toolchain remotes, the retained Gradle release path,
and release gating.
- `verification/` contains portable release/parser/wrapper checks plus the
baseline/current Gradle publication, source-identity, public-signature, and
consumer verifier used by CI.
## Target and test matrix
| Target | Toolchain result | Runtime/test result | Contract notes |
|---|---|---|---|
| JVM | PASS | 2 tests found, 2 started, 2 passed | Bytecode major 52; 25-class list and public JVM method/field signatures from `javap -public -s -constants` match Gradle |
| Android | PASS | No device tests in the project; Android consumer compilation PASS | Bytecode major 61; 24-class list and public JVM method/field signatures match Gradle; minSdk 21, compileSdk/build tools 37 |
| JS | PASS | No test sources; Toolchain 0.13.0 exposes no browser/Node runner selection for a library | Gradle fallback keeps browser + Node declarations |
| WasmJS | PASS | No test sources; Toolchain 0.13.0 exposes no Node/d8 selection and only has browser test support | Gradle fallback keeps browser + Node + d8 declarations |
| linuxX64 | PASS | No test sources | Host-compilable KLIB |
| linuxArm64 | PASS | Cross-compiled; not runnable on this x64 host | KLIB publication produced |
| mingwX64 | PASS | Cross-compiled; not runnable on this Linux host | KLIB publication produced |
| linuxArm32Hfp | **UNSUPPORTED** | Gradle clean build compiles it; no tests | Absent from Toolchain 0.13.0 `Platform`; minimal model probe fails |
`./kotlin check` did not mistake empty platform suites for passing tests: the
report only claims the two JVM tests. Gradle reports JS, WasmJS, Android, and
native tests as `NO-SOURCE` or skipped, while still compiling all declared
targets, including Linux ARM32.
## Verification evidence
The commands below are reproducible from the repository root. Temporary build
logs and repositories are intentionally not part of the published report.
### Model, build, checks, and compatibility
| Command | Result |
|---|---|
| `./kotlin show modules` | PASS; modules `dokka`, `github-release`, `kslog` |
| `./kotlin show settings -m kslog` plus `./kotlin show tasks` | PASS; `mavenCentral.enabled: false`, only local publish repository configured, no Nexus/Central task references |
| `./kotlin show commands` / `show checks` | PASS; Dokka/release commands and builtin tests enumerated |
| `./kotlin clean && ./kotlin build` | PASS; all seven Toolchain platforms plus both plugin modules |
| `./kotlin check` | PASS; KSLog JVM tests 2/2 plus Dokka guard/classpath tests 4/4 |
| `./kotlin test -m dokka -p jvm` | PASS 4/4; transformed/platform classpath mapping, unresolved-output rejection, symlink containment, and scoped cleanup |
| `./gradlew --no-daemon clean build` | PASS; complete legacy build, including Linux ARM32 |
| `bash verification/verify_gradle_fallback.sh` | PASS; isolated baseline/current Gradle publication and committed consumer probe |
| `python3 verification/check_release_consistency.py` and parser/wrapper checks | PASS; four version copies, literal changelog heading, LF/CRLF attributes |
| Linux ARM32 minimal model probe | Expected failure: `Product platforms list should not be empty` at `linuxArm32Hfp` |
| GitHub YAML, actionlint 1.7.7, shell syntax, `git diff --check` | PASS |
| `ast-index rebuild` | PASS after final edits |
The Toolchain emits the existing insignificant inline warning. It also warns
that `-module-name`/`-Xshort-module-name` are passed more than once because the
built-in checkout-derived names are intentionally overridden; inspection of
the output JAR/KLIB manifests proves the last values match the released Gradle
module identities.
### Source and ABI preservation
- All 20 production source files match the base commit byte-for-byte after
their moves.
- The JVM 25-class and Android 24-class lists match; public JVM method/field
signature dumps from `javap -public -s -constants` have zero differences.
- Gradle/Toolchain bytecode majors are JVM 52/52 and Android 61/61.
- Published JVM, common/native metadata, and supported leaf KLIB module
identities match Gradle.
These checks are deliberately narrow. Source identity, consumer compilation,
JVM class/signature comparison, and inspected module names are not a complete
Kotlin metadata or KLIB ABI proof. Full cutover remains blocked and requires a
dedicated metadata/KLIB compatibility review in addition to resolving the two
upstream capability gaps.
### Documentation and release command
| Command | Result |
|---|---|
| `./kotlin build -m kslog -p js -p wasmJs` | PASS; materializes the Common/JS/WasmJS analysis KLIBs required by the pinned plugin API |
| `./kotlin do dokkaHtml` | PASS; 156 files at `build/dokka/html`, zero unresolved-type placeholders |
| `./kotlin do dokkaGfm` | PASS; 101 files at `build/dokka/gfm`, zero unresolved-type placeholders |
| Output/link/platform inspection | PASS; canonical source links and Common/JVM/Android/JS/WasmJS/Native tabs |
| `KSLOG_GITHUB_RELEASE_DRY_RUN=true ./kotlin do githubRelease` | PASS; consistency preflight and `v2.1.0` payload, no network mutation |
The legacy `DOKKA_PATH` override is intentionally not ported. It remains
available only through Gradle because Toolchain cleanup is restricted to direct
children of `build/dokka`.
### Isolated publication and consumers
Toolchain publication used a temporary Maven repository selected with the JVM
`maven.repo.local` override, `-m kslog`, and a generated throwaway signing key.
The configured destination is named `mavenLocal` and would use the normal Maven
Local repository without that override. The temporary private key was deleted
after verification, and the normal Maven Local repository was not written.
| Check | Result |
|---|---|
| `./kotlin publish -m kslog mavenLocal` | PASS in an isolated repository |
| Toolchain artifact/signature inventory | 40 primary files and 40 detached signatures; 40/40 signatures verified with the throwaway public key |
| Toolchain required POM metadata comparison | PASS for root and every supported platform artifact |
| Gradle KMP consumer compile | PASS for JVM, JS, WasmJS, linuxX64, linuxArm64, mingwX64 |
| Gradle Android KMP consumer compile | PASS |
| `bash verification/verify_gradle_fallback.sh` | PASS; untouched-base and post-move Gradle outputs each contain the same 46 primary files, nine source JARs, and nine required-metadata POMs; no signing task/key/config is used |
Produced IDs are `kslog`, `kslog-android`, `kslog-js`, `kslog-jvm`,
`kslog-linuxarm64`, `kslog-linuxx64`, `kslog-mingwx64`, and `kslog-wasmjs`.
Root Gradle module metadata, Kotlin Tooling metadata, and sources are present.
Platform Toolchain publications contain placeholder Javadoc JARs, while the
root Toolchain Javadoc JAR is absent. The committed Gradle verifier uses a fresh
Gradle user home with only optional `caches`/`wrapper` links, an empty GnuPG
home, sanitized credential/signing environment variables, and separate Maven
repositories; it neither reads user Gradle signing configuration nor writes to
the normal Maven local repository.
## Exact parity blockers and differences
1. **Linux ARM32:** `linuxArm32Hfp` is absent from the pinned 0.13.0
`Platform` enum and schema. Adding it to `product.platforms` is rejected at
model load, before compilation or publication can be customized.
2. **Wasm artifact coordinate:** built-in KMP publication maps `wasmJs` to
`kslog-wasmjs`; there is no per-platform artifact-suffix override. KSLog's
released coordinate is `kslog-wasm-js`.
3. **Runtime declarations:** JS browser/Node and WasmJS browser/Node/d8 runner
selection are not exposed for this Toolchain library model. Compilation and
publication work; those execution contracts stay in Gradle.
4. **POM shape:** required name/description/URL/developers/license/SCM URL and
developer connection are preserved. Toolchain additionally generates the
SCM `connection`. Its root KMP POM uses `<packaging>pom</packaging>` and a
`kslog-jvm` compile dependency, while Gradle's root POM carries a Kotlin
stdlib runtime dependency. The Toolchain Android POM additionally declares
`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8`; its JS POM omits Gradle's
`kotlin-dom-api-compat`; and its Linux ARM64, Linux x64, and MinGW POMs move
the Kotlin stdlib dependency from compile to runtime scope. Gradle module
metadata consumer selection and the committed consumers passed, but direct
POM consumers can observe these dependency/scope changes, so this is not
publication-shape parity and requires review before full cutover.
5. **Root Javadoc artifact:** the 46-file Gradle publication has
`kslog-2.1.0-javadoc.jar`; the 40-file Toolchain publication omits it. This is
the sixth missing primary file in addition to the five Linux ARM32 files.
Toolchain platform Javadoc JARs contain a manifest plus a 78-byte `README.md`
placeholder, while Gradle's Javadoc JARs contain only the manifest.
6. **Android manifest and AAR layout:** Gradle's manifest declares only
`minSdkVersion=21`; Toolchain additionally emits `targetSdkVersion=37`.
Gradle uses `classes.jar`; Toolchain uses
`libs/kslog-jvm.jar`. Both are valid AAR layouts, the bytecode level and
compared public JVM method/field signatures match, and an Android KMP
consumer compiles successfully.
7. **JS manifest detail:** public KLIB identity matches
`unique_name=dev.inmo:kslog`, but Toolchain's JS/Wasm KLIB does not include
Gradle's `jsOutputName=kslog` manifest entry.
8. **Android resource exclusions:** `resourcePackaging` is not accepted for a
Toolchain `kmp/lib`. The produced AAR contains only the manifest and library
JAR, so none of the three legacy serialization metadata exclusions are
currently present. Gradle keeps the exclusions for the fallback.
9. **Per-platform JVM release:** Toolchain 0.13.0 does not allow `jvm.release`
to differ by platform. The module uses target-specific `-jvm-target=1.8`
and `-jvm-target=17`; inspected bytecode proves majors 52 and 61.
10. **Documentation output override:** Gradle honors `DOKKA_PATH`; Toolchain
intentionally does not because arbitrary external cleanup would weaken the
generated-output containment guarantee.
Because items 1 and 2 change released target/coordinate contracts, remote
Toolchain publication must remain blocked until Toolchain gains the required
capabilities or an explicitly reviewed compatibility implementation is added.
## CI behavior
- `.github/workflows/build.yml` replaces the stale workflow that attempted
nonexistent Gitea/GitHub-Packages Gradle tasks on every push. Pushes and pull
requests now run release/parser/wrapper preflights, Toolchain build/check, the
complete retained Gradle build, and the isolated publication/API/consumer
verifier. Checkout fetches full history so the verifier's pinned migration
base commit is available. It has read-only repository permissions and no
secrets or remote publication commands.
- `.github/workflows/kdocs.yml` remains `master`-only. Its read-only generation
job checks out with `persist-credentials: false`, materializes the platform
analysis KLIBs, runs Toolchain/Dokka, and
uploads the generated artifact. A separate write-enabled job downloads only
that artifact and deploys it to `kdocs`; project/plugin code never runs with a
write credential.
- All workflow actions are pinned to resolved commit SHAs.
- Hosted CI status is reported on [draft PR #136](https://github.com/InsanusMokrassar/KSLog/pull/136); the local evidence above does not imply a hosted pass. SDK setup explicitly requests `platforms;android-37.0` (API 37) and excludes the removed legacy `tools` package.
## Remote-only checks not performed
- Inmo Nexus upload.
- Maven Central bundle upload/validation/release.
- GitHub release creation.
- Documentation deployment to the `kdocs` branch.
- Android device tests and browser/Node/d8 tests (there are currently no such
test sources); Linux ARM64 and MinGW runtime execution on matching hosts.
## Recommended pull request
Suggested title:
> build: add Kotlin Toolchain migration with compatibility fallback
Suggested body:
> ## Summary
> - add the pinned Kotlin Toolchain 0.13.0 wrappers and a stable `kslog` KMP module
> - migrate source roots byte-for-byte and retain a validated Gradle fallback
> - add real JVM tests, KMP-capable Dokka HTML/GFM tasks, and an explicit GitHub release command
> - replace stale package-publishing CI with non-publishing Toolchain and full Gradle validation
> - document isolated publication, release gating, and full verification evidence
>
> ## Validation
> - all Toolchain-supported targets build
> - JVM tests: 2/2 pass
> - retained Gradle clean build passes, including linuxArm32Hfp
> - JVM/Android class lists and public JVM method/field signature diffs are empty; bytecode remains 8/17
> - signed isolated KMP publication verified and consumed by Gradle KMP + Android projects
> - Dokka HTML/GFM and GitHub release dry-run pass
>
> ## Known blockers
> Toolchain 0.13.0 has no linuxArm32Hfp platform and publishes `-wasmjs` instead
> of the released `-wasm-js` coordinate. Therefore Gradle remains the publication
> fallback and remote Toolchain publication is not enabled. See
> `MIGRATION_REPORT.md` for the complete matrix and artifact differences.
This migration is ready only for a **draft PR**. Full Gradle removal is not
review-ready and remains blocked by Toolchain 0.13.0's missing Linux ARM32
platform and incompatible Wasm artifact ID; the narrow checks above do not
waive those requirements.
-23
View File
@@ -91,26 +91,3 @@ implementation "dev.inmo:kslog:$kslog_version"
<version>${kslog_version}</version> <version>${kslog_version}</version>
</dependency> </dependency>
``` ```
## Building from source
The checked-in `kotlin`/`kotlin.bat` wrappers pin JetBrains Kotlin Toolchain
0.13.0. With JDK 17 and Android SDK 37 installed, the main validation commands
are:
```shell
./kotlin build
./kotlin check
./kotlin do dokkaHtml
```
Kotlin Toolchain 0.13.0 cannot model the existing `linuxArm32Hfp` target and
derives `kslog-wasmjs` instead of the released `kslog-wasm-js` artifact ID.
The Gradle build is retained as a compatibility path until those publication
contracts can be represented without dropping a target or changing consumer
coordinates.
See [DEVELOPMENT.md](DEVELOPMENT.md) for target-specific commands, documentation
generation, and explicitly gated publishing/release procedures. The migration
inventory and verification evidence are in [MIGRATION_PLAN.md](MIGRATION_PLAN.md)
and [MIGRATION_REPORT.md](MIGRATION_REPORT.md).
-18
View File
@@ -82,39 +82,21 @@ kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
kotlin.srcDir("kslog/src")
dependencies { dependencies {
implementation libs.kotlin implementation libs.kotlin
} }
} }
commonTest { commonTest {
kotlin.srcDir("kslog/test")
dependencies { dependencies {
implementation libs.kotlin.test.common implementation libs.kotlin.test.common
implementation libs.kotlin.test.annotations.common implementation libs.kotlin.test.annotations.common
} }
} }
androidMain {
kotlin.srcDir("kslog/src@android")
}
jsMain {
kotlin.srcDir("kslog/src@js")
}
jvmMain {
kotlin.srcDir("kslog/src@jvm")
}
jvmTest { jvmTest {
kotlin.srcDir("kslog/test@jvm")
dependencies { dependencies {
implementation libs.kotlin.test.junit implementation libs.kotlin.test.junit
} }
} }
nativeMain {
kotlin.srcDir("kslog/src@native")
}
wasmJsMain {
kotlin.srcDir("kslog/src@wasmJs")
}
} }
} }
+8 -19
View File
@@ -1,32 +1,21 @@
#!/bin/bash #!/bin/bash
function parse() { function parse() {
local version="$1" version="$1"
local line
local found=""
while IFS= read -r line; do while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+ $version"`" ]
if [[ "$line" == "## $version" ]]; then do
found="yes" : # do nothing
break
fi
done done
if [[ -z "$found" ]]; then while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+"`" ]
printf 'No literal changelog heading found for %s\n' "$version" >&2 do
return 1 echo "$line"
fi
while IFS= read -r line; do
if [[ "$line" == \#* ]]; then
break
fi
printf '%s\n' "$line"
done done
} }
version="$1" version="$1"
file="${2:-}" file="$2"
if [ -n "$file" ]; then if [ -n "$file" ]; then
parse "$version" < "$file" parse "$version" < "$file"
+2 -2
View File
@@ -1,10 +1,10 @@
Object callback = { Object callback = {
switch (true) { switch (true) {
case project.hasProperty("DOKKA_PATH"): case project.hasProperty("DOKKA_PATH"):
outputDirectory.set(file(project.property("DOKKA_PATH").toString())) outputDirectory = project.property("DOKKA_PATH").toString()
break break
case System.getenv("DOKKA_PATH") != null: case System.getenv("DOKKA_PATH") != null:
outputDirectory.set(file(System.getenv("DOKKA_PATH"))) outputDirectory = System.getenv("DOKKA_PATH")
break break
} }
+1 -9
View File
@@ -1,7 +1,7 @@
[versions] [versions]
kotlin = "2.4.20" kotlin = "2.4.20"
android-gradle-plugin = "9.1.1" android-gradle-plugin = "9.5.0"
nmcp = "1.6.2" nmcp = "1.6.2"
versions = "0.64.0" versions = "0.64.0"
@@ -37,14 +37,6 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
kotlin-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } kotlin-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
github-release = { module = "com.github.breadmoirai:github-release", version.ref = "github-release" } github-release = { module = "com.github.breadmoirai:github-release", version.ref = "github-release" }
# Kotlin Toolchain local Dokka plugin
dokka-cli = { module = "org.jetbrains.dokka:dokka-cli", version.ref = "dokka" }
dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka" }
dokka-analysis-kotlin-descriptors = { module = "org.jetbrains.dokka:analysis-kotlin-descriptors", version.ref = "dokka" }
dokka-gfm = { module = "org.jetbrains.dokka:gfm-plugin", version.ref = "dokka" }
kotlinx-html-jvm = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0"
freemarker = "org.freemarker:freemarker:2.3.31"
[plugins] [plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
-286
View File
@@ -1,286 +0,0 @@
#!/bin/sh
#
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
#
# Possible environment variables:
# KOTLIN_CLI_DOWNLOAD_ROOT Maven repository to download the Kotlin CLI dist from.
# default: https://packages.jetbrains.team/maven/p/amper/amper
# KOTLIN_CLI_JRE_DOWNLOAD_ROOT Url prefix to download the JRE to run the Kotlin CLI
# default: https:/
# KOTLIN_CLI_BOOTSTRAP_CACHE_DIR Cache directory to store the extracted JRE and Kotlin CLI distribution
# KOTLIN_CLI_JAVA_HOME JRE to run the Kotlin CLI itself (optional, does not affect compilation)
# KOTLIN_CLI_JAVA_OPTIONS JVM options to pass to the JVM running the Kotlin CLI (does not affect the user's application)
# KOTLIN_CLI_NO_WELCOME_BANNER Disables the first-run welcome message if set to a non-empty value
set -e -u
# The version of the Kotlin Toolchain (and CLI) distribution to provision and use
kotlin_cli_version=0.13.0-dev-4444
# Establish chain of trust from here by specifying exact checksum of Kotlin Toolchain (and CLI) distribution to be run
kotlin_cli_sha256=13c2448146ac6dba0fa3a4e5d367dab73d8cfd7400bab348bebe905c165adecf
KOTLIN_CLI_DOWNLOAD_ROOT="${KOTLIN_CLI_DOWNLOAD_ROOT:-https://packages.jetbrains.team/maven/p/amper/amper}"
die() {
echo >&2
echo "$@" >&2
echo >&2
exit 1
}
# usage: check_sha SOURCE_MONIKER FILE SHA_CHECKSUM SHA_SIZE
# $1 SOURCE_MONIKER (e.g. url)
# $2 FILE
# $3 SHA hex string
# $4 SHA size in bits (256, 512, ...)
check_sha() {
sha_size=$4
if command -v shasum >/dev/null 2>&1; then
echo "$3 *$2" | shasum -a "$sha_size" --status -c || {
die "ERROR: Checksum mismatch for $2 (downloaded from $1): expected checksum $3 but got $(shasum --binary -a "$sha_size" "$2" | awk '{print $1}')"
}
return 0
fi
shaNsumCommand="sha${sha_size}sum"
if command -v "$shaNsumCommand" >/dev/null 2>&1; then
# discard the output as sha*sum may print redundant warnings in some versions
echo "$3 *$2" | $shaNsumCommand -w -c >/dev/null 2>&1 || {
die "ERROR: Checksum mismatch for $2 (downloaded from $1): expected checksum $3 but got $($shaNsumCommand "$2" | awk '{print $1}')"
}
return 0
fi
echo "Both 'shasum' and 'sha${sha_size}sum' utilities are missing. Please install one of them"
return 1
}
download_and_extract() {
moniker="$1"
file_url="$2"
file_sha="$3"
sha_size="$4"
cache_dir="$5"
extract_dir="$6"
show_banner_on_cache_miss="$7"
if [ -e "$extract_dir/.flag" ] && [ "$(cat "$extract_dir/.flag")" = "${file_sha}" ]; then
# Everything is up-to-date in $extract_dir, do nothing
return 0;
fi
mkdir -p "$cache_dir"
# Take a lock for the download of this file
short_sha=$(echo "$file_sha" | cut -c1-32) # cannot use the ${short_sha:0:32} syntax in regular /bin/sh
download_lock_file="$cache_dir/download-${short_sha}.lock"
process_lock_file="$cache_dir/download-${short_sha}.$$.lock"
echo $$ >"$process_lock_file"
while ! ln "$process_lock_file" "$download_lock_file" 2>/dev/null; do
lock_owner=$(cat "$download_lock_file" 2>/dev/null || true)
# We use `kill -0` instead of `ps -p` as the first one is more portable
if [ -n "$lock_owner" ] && kill -0 "$lock_owner" >/dev/null; then
echo "Another Kotlin CLI instance (pid $lock_owner) is downloading $moniker. Awaiting the result..."
sleep 1
elif [ -n "$lock_owner" ] && [ "$(cat "$download_lock_file" 2>/dev/null)" = "$lock_owner" ]; then
rm -f "$download_lock_file"
# We don't want to simply loop again here, because multiple concurrent processes may face this at the same time,
# which means the 'rm' command above from another script could delete our new valid lock file. Instead, we just
# ask the user to try again. This doesn't 100% eliminate the race, but the probability of issues is drastically
# reduced because it would involve 4 processes with perfect timing. We can revisit this later.
die "Another Kotlin CLI instance (pid $lock_owner) locked the download of $moniker, but is no longer running. The lock file is now removed, please try again."
fi
done
# shellcheck disable=SC2064
trap "rm -f \"$download_lock_file\"" EXIT
rm -f "$process_lock_file"
unlock_and_cleanup() {
rm -f "$download_lock_file"
trap - EXIT
return 0
}
if [ -e "$extract_dir/.flag" ] && [ "$(cat "$extract_dir/.flag")" = "${file_sha}" ]; then
# Everything is up-to-date in $extract_dir, just release the lock
unlock_and_cleanup
return 0;
fi
if [ "$show_banner_on_cache_miss" = "true" ] && [ -z "${KOTLIN_CLI_NO_WELCOME_BANNER:-}" ]; then
echo
cat <<EOF
Welcome to
@@@ @@@@ @@@ @@@
@@@ @@@@ @@@ @@@ @@@
@@@ #@@@" ,@@@ @@@
@@@ ,@@@% ,@@@@@@@, @@@@@@@@@@ @@@ @@@ @@@ ,@@@@@,
@@@ @@@@ @@@@@%"%@@@@@ @@@@@@@@@@ @@@ @@@ @@@@@@%%@@@@@
@@@@@@% @@@% %@@@ @@@ @@@ @@@ @@@@ %@@%
@@@ @@@@= #@@@ @@@# @@@ @@@ @@@ @@@ @@@
@@@ @@@@ #@@@ @@@# @@@ @@@ @@@ @@@ @@@
@@@ *@@@% @@@@ @@@@ @@@ @@@ @@@ @@@ @@@
@@@ %@@@= %@@@@*,*@@@@% @@@@### @@@ @@@ @@@ @@@
@@@ @@@@ "@@@@@@@" %@@@@@ @@@ @@@ @@@ @@@
EOF
echo
echo "This is the first run of the Kotlin CLI v$kotlin_cli_version, so we need to download the Kotlin Toolchain."
echo "Please give us a few seconds, subsequent runs will be faster."
echo
fi
echo "Downloading $moniker..."
temp_file="$cache_dir/download-file-$$.bin"
rm -f "$temp_file"
if command -v curl >/dev/null 2>&1; then
if [ -t 1 ]; then CURL_PROGRESS="--progress-bar"; else CURL_PROGRESS="--silent --show-error"; fi
# shellcheck disable=SC2086
curl $CURL_PROGRESS -L --fail --retry 5 --connect-timeout 30 --output "${temp_file}" "$file_url"
elif command -v wget >/dev/null 2>&1; then
if [ -t 1 ]; then WGET_PROGRESS=""; else WGET_PROGRESS="-nv"; fi
wget $WGET_PROGRESS --tries=5 --connect-timeout=30 --read-timeout=120 -O "${temp_file}" "$file_url"
else
die "ERROR: Please install 'wget' or 'curl', as one of them is required to download $moniker"
fi
check_sha "$file_url" "$temp_file" "$file_sha" "$sha_size"
rm -rf "$extract_dir"
mkdir -p "$extract_dir"
case "$file_url" in
*".zip")
if command -v unzip >/dev/null 2>&1; then
unzip -q "$temp_file" -d "$extract_dir"
else
die "ERROR: Please install 'unzip', which is required to extract $moniker"
fi ;;
*)
if command -v tar >/dev/null 2>&1; then
tar -x -f "$temp_file" -C "$extract_dir"
else
die "ERROR: Please install 'tar', which is required to extract $moniker"
fi ;;
esac
rm -f "$temp_file"
echo "$file_sha" >"$extract_dir/.flag"
# Unlock and cleanup the lock file
unlock_and_cleanup
echo "Download complete."
echo
}
# ********** Project-local version detection **********
# 1. Search upwards for an executable `amper` file and/or `project.yaml`
# Sets wrapper_script to the found wrapper path, or empty string if not found.
find_project_context() {
wrapper_script=""
this_script="$(realpath "$0")"
project_dir=$(pwd)
while [ "$project_dir" != "/" ] && [ -n "$project_dir" ]; do
wrapper_candidate="$project_dir/kotlin"
if [ "$this_script" = "$wrapper_candidate" ]; then
# Found itself (local wrapper case), no need to update any version or search further.
return 1
fi
if [ -f "$wrapper_candidate" ] && [ -x "$wrapper_candidate" ]; then
# Found the wrapper — check that a project context exists alongside it
if [ -f "$project_dir/project.yaml" ] || [ -f "$project_dir/module.yaml" ]; then
wrapper_script="$wrapper_candidate"
return 0
else
echo "WARNING: Found wrapper script '$wrapper_candidate', but no project.yaml or module.yaml near it. Skipping." >&2
# Continue the search
fi
elif [ -f "$project_dir/project.yaml" ]; then
# Found project.yaml but no executable wrapper alongside it
echo "WARNING: Found a project.yaml in '$project_dir', but the wrapper script is missing; using Kotlin Toolchain v$kotlin_cli_version." >&2
return 1
fi
project_dir=$(dirname "$project_dir")
done
# Do not check root '/' - it's an unlikely candidate for a project
return 1
}
parse_project_context() {
# Parse kotlin_cli_version and kotlin_cli_sha256 from "$wrapper_script" without executing it.
parsed_kotlin_cli_version=$(
sed -n 's/^kotlin_cli_version=\([A-Za-z0-9._+-]\{1,\}\)[[:space:]]*$/\1/p' "$wrapper_script" \
| head -n 1
)
parsed_kotlin_cli_sha256=$(
sed -n 's/^kotlin_cli_sha256=\([0-9a-fA-F]\{64\}\)[[:space:]]*$/\1/p' "$wrapper_script" \
| head -n 1
)
if [ -z "$parsed_kotlin_cli_version" ]; then
echo "ERROR: Suspicious local wrapper script: failed to detect the distribution version in '$wrapper_script'" >&2
return 1
fi
if [ -z "$parsed_kotlin_cli_sha256" ]; then
echo "ERROR: Suspicious local wrapper script: failed to detect the distribution checksum in '$wrapper_script'" >&2
return 1
fi
# overwrite builtin values and proceed
kotlin_cli_version=$parsed_kotlin_cli_version
kotlin_cli_sha256=$parsed_kotlin_cli_sha256
return 0
}
if [ -z "${KOTLIN_CLI_WRAPPER_ALWAYS_USE_INTRINSIC_VERSION:-}" ]; then
find_project_context && parse_project_context
fi
# ********** System detection **********
kernelName=$(uname -s)
case "$kernelName" in
Darwin* )
default_kotlin_cli_cache_dir="$HOME/Library/Caches/JetBrains/Kotlin/cli"
;;
Linux* )
default_kotlin_cli_cache_dir="$HOME/.cache/JetBrains/Kotlin/cli"
;;
CYGWIN* | MSYS* | MINGW* )
if command -v cygpath >/dev/null 2>&1; then
default_kotlin_cli_cache_dir=$(cygpath -u "$LOCALAPPDATA\JetBrains\Kotlin\cli")
else
die "The 'cypath' command is not available, but the Kotlin CLI needs it. Use kotlin.bat instead, or try a Cygwin or MSYS environment."
fi
;;
*)
die "Unsupported platform $kernelName"
;;
esac
kotlin_cli_cache_dir="${KOTLIN_CLI_BOOTSTRAP_CACHE_DIR:-$default_kotlin_cli_cache_dir}"
# ********** Provision the Kotlin Toolchain distribution **********
kotlin_cli_url="$KOTLIN_CLI_DOWNLOAD_ROOT/org/jetbrains/kotlin/kotlin-cli/$kotlin_cli_version/kotlin-cli-$kotlin_cli_version-dist.tgz"
kotlin_cli_target_dir="$kotlin_cli_cache_dir/kotlin-cli-$kotlin_cli_version"
download_and_extract "Kotlin Toolchain distribution v$kotlin_cli_version" "$kotlin_cli_url" "$kotlin_cli_sha256" 256 "$kotlin_cli_cache_dir" "$kotlin_cli_target_dir" "true"
# ********** Launch the Kotlin CLI **********
launcher_script="$kotlin_cli_target_dir/bin/launcher.sh"
KOTLIN_CLI_WRAPPER_PATH="$(realpath "$0")" \
exec /bin/sh "$launcher_script" "$@"
-265
View File
@@ -1,265 +0,0 @@
@echo off
@rem
@rem Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@rem
@rem Possible environment variables:
@rem KOTLIN_CLI_DOWNLOAD_ROOT Maven repository to download the Kotlin CLI dist from
@rem default: https://packages.jetbrains.team/maven/p/amper/amper
@rem KOTLIN_CLI_JRE_DOWNLOAD_ROOT Url prefix to download the Kotlin CLI JRE from.
@rem default: https:/
@rem KOTLIN_CLI_BOOTSTRAP_CACHE_DIR Cache directory to store the extracted JRE and Kotlin CLI distribution
@rem KOTLIN_CLI_JAVA_HOME JRE to run the Kotlin CLI itself (optional, does not affect compilation)
@rem KOTLIN_CLI_JAVA_OPTIONS JVM options to pass to the JVM running the Kotlin CLI (does not affect the user's application)
@rem KOTLIN_CLI_NO_WELCOME_BANNER Disables the first-run welcome message if set to a non-empty value
setlocal
@rem The version of the Kotlin Toolchain distribution to provision and use
set kotlin_cli_version=0.13.0-dev-4444
@rem Establish chain of trust from here by specifying the exact checksum of the Kotlin Toolchain distribution to be run
set kotlin_cli_sha256=13c2448146ac6dba0fa3a4e5d367dab73d8cfd7400bab348bebe905c165adecf
if not defined KOTLIN_CLI_DOWNLOAD_ROOT set KOTLIN_CLI_DOWNLOAD_ROOT=https://packages.jetbrains.team/maven/p/amper/amper
if not defined KOTLIN_CLI_BOOTSTRAP_CACHE_DIR set KOTLIN_CLI_BOOTSTRAP_CACHE_DIR=%LOCALAPPDATA%\JetBrains\Kotlin\cli
@rem remove trailing \ if present
if [%KOTLIN_CLI_BOOTSTRAP_CACHE_DIR:~-1%] EQU [\] set KOTLIN_CLI_BOOTSTRAP_CACHE_DIR=%KOTLIN_CLI_BOOTSTRAP_CACHE_DIR:~0,-1%
goto :after_function_declarations
REM ********** Download and extract any zip or .tar.gz archive **********
:download_and_extract
setlocal
set moniker=%~1
set url=%~2
set target_dir=%~3
set sha=%~4
set sha_size=%~5
set show_banner_on_cache_miss=%~6
setlocal enableDelayedExpansion
set flag_file=%target_dir%\.flag
if exist "%flag_file%" (
set /p current_flag=<"%flag_file%"
if "!current_flag!" == "%sha%" exit /b
)
set NL=^
@rem two empty lines required above for the NL character
@rem We have to build the welcome banner here as an env var because we
@rem can't pass a multiline string through the single line powershell
set welcome_banner=!NL! ^
Welcome to !NL! ^
!NL! ^
@@@ @@@@ @@@ @@@ !NL! ^
@@@ @@@@ @@@ @@@ @@@ !NL! ^
@@@ #@@@^" ,@@@ @@@ !NL! ^
@@@ ,@@@%% ,@@@@@@@, @@@@@@@@@@ @@@ @@@ @@@ ,@@@@@, !NL! ^
@@@ @@@@ @@@@@%%^"%%@@@@@ @@@@@@@@@@ @@@ @@@ @@@@@@%%%%@@@@@ !NL! ^
@@@@@@%% @@@%% %%@@@ @@@ @@@ @@@ @@@@ %%@@%% !NL! ^
@@@ @@@@= #@@@ @@@# @@@ @@@ @@@ @@@ @@@ !NL! ^
@@@ @@@@ #@@@ @@@# @@@ @@@ @@@ @@@ @@@ !NL! ^
@@@ *@@@%% @@@@ @@@@ @@@ @@@ @@@ @@@ @@@ !NL! ^
@@@ %%@@@= %%@@@@*,*@@@@%% @@@@### @@@ @@@ @@@ @@@ !NL! ^
@@@ @@@@ ^"@@@@@@@^" %%@@@@@ @@@ @@@ @@@ @@@ !NL!
@rem This multiline string is actually passed as a single line to powershell, meaning #-comments are not possible.
@rem So here are a few comments about the code below:
@rem - we need to support both .zip and .tar.gz archives (for the Kotlin Toolchain distribution and the JRE)
@rem - tar should be present in all Windows machines since 2018 (and usable from both cmd and powershell)
@rem - tar requires the destination dir to exist
@rem - We use (New-Object Net.WebClient).DownloadFile instead of Invoke-WebRequest for performance. See the issue
@rem https://github.com/PowerShell/PowerShell/issues/16914, which is still not fixed in Windows PowerShell 5.1
@rem - DownloadFile requires the directories in the destination file's path to exist
set download_and_extract_ps1= ^
Set-StrictMode -Version 3.0; ^
$ErrorActionPreference = 'Stop'; ^
^
$createdNew = $false; ^
$lock = New-Object System.Threading.Mutex($true, ('Global\kotlin-cli-bootstrap.' + '%target_dir%'.GetHashCode().ToString()), [ref]$createdNew); ^
if (-not $createdNew) { ^
Write-Host 'Another Kotlin CLI instance is bootstrapping. Waiting for our turn...'; ^
[void]$lock.WaitOne(); ^
} ^
^
try { ^
if ((Get-Content '%flag_file%' -ErrorAction Ignore) -ne '%sha%') { ^
if (('%show_banner_on_cache_miss%' -eq 'true') -and [string]::IsNullOrEmpty('%KOTLIN_CLI_NO_WELCOME_BANNER%')) { ^
Write-Host \"$env:welcome_banner\"; ^
Write-Host ''; ^
Write-Host 'This is the first run of the Kotlin CLI v%kotlin_cli_version%, so we need to download the Kotlin Toolchain.'; ^
Write-Host 'Please give us a few seconds now, subsequent runs will be faster.'; ^
Write-Host ''; ^
} ^
$temp_file = '%KOTLIN_CLI_BOOTSTRAP_CACHE_DIR%\' + [System.IO.Path]::GetRandomFileName(); ^
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ^
Write-Host 'Downloading %moniker%...'; ^
[void](New-Item '%KOTLIN_CLI_BOOTSTRAP_CACHE_DIR%' -ItemType Directory -Force); ^
if (Get-Command curl.exe -errorAction SilentlyContinue) { ^
curl.exe -L --silent --show-error --fail --output $temp_file '%url%'; ^
} else { ^
(New-Object Net.WebClient).DownloadFile('%url%', $temp_file); ^
} ^
^
$actualSha = (Get-FileHash -Algorithm SHA%sha_size% -Path $temp_file).Hash.ToString(); ^
if ($actualSha -ne '%sha%') { ^
$writeErr = if ($Host.Name -eq 'ConsoleHost') { [Console]::Error.WriteLine } else { $host.ui.WriteErrorLine } ^
$writeErr.Invoke(\"ERROR: Checksum mismatch for $temp_file (downloaded from %url%): expected checksum %sha% but got $actualSha\"); ^
exit 1; ^
} ^
^
if (Test-Path '%target_dir%') { ^
Remove-Item '%target_dir%' -Recurse; ^
} ^
if ($temp_file -like '*.zip') { ^
Add-Type -A 'System.IO.Compression.FileSystem'; ^
[IO.Compression.ZipFile]::ExtractToDirectory($temp_file, '%target_dir%'); ^
} else { ^
[void](New-Item '%target_dir%' -ItemType Directory -Force); ^
tar -xzf $temp_file -C '%target_dir%'; ^
} ^
Remove-Item $temp_file; ^
^
Set-Content '%flag_file%' -Value '%sha%'; ^
Write-Host 'Download complete.'; ^
Write-Host ''; ^
} ^
} ^
finally { ^
$lock.ReleaseMutex(); ^
}
rem We reset the PSModulePath in case this batch script was called from PowerShell Core
rem See https://github.com/PowerShell/PowerShell/issues/18108#issuecomment-2269703022
set PSModulePath=
set powershell=%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe
"%powershell%" -NonInteractive -NoProfile -NoLogo -Command %download_and_extract_ps1%
if errorlevel 1 exit /b 1
exit /b 0
:find_project_context
@rem Search upwards for a kotlin.bat wrapper file and/or project.yaml
@rem Sets wrapper_script to the found wrapper path, or empty string if not found.
@rem Returns errorlevel 0 if a valid wrapper (that is not this script itself) was found, 1 otherwise.
set wrapper_script=
set this_script=%~f0
set project_dir=%CD%
:find_loop
set wrapper_candidate=%project_dir%\kotlin.bat
if "%this_script%"=="%wrapper_candidate%" (
@rem Found itself (local wrapper case), no need to update any version or search further.
exit /b 1
)
if exist "%wrapper_candidate%" (
@rem Found a wrapper — check that a project context exists alongside it
if exist "%project_dir%\project.yaml" (
set wrapper_script=%wrapper_candidate%
exit /b 0
)
if exist "%project_dir%\module.yaml" (
set wrapper_script=%wrapper_candidate%
exit /b 0
)
echo WARNING: Found wrapper script '%wrapper_candidate%', but no project.yaml or module.yaml near it. Skipping. >&2
@rem Continue the search
goto :find_next_parent
)
if exist "%project_dir%\project.yaml" (
@rem Found project.yaml but no wrapper alongside it
echo WARNING: Found a project.yaml in '%project_dir%', but the wrapper script is missing; using Kotlin Toolchain v%kotlin_cli_version%. >&2
exit /b 1
)
:find_next_parent
@rem Move to parent directory
for %%P in ("%project_dir%\..") do set parent_dir=%%~fP
if "%parent_dir%"=="%project_dir%" (
@rem Reached the root, stop searching
exit /b 1
)
set project_dir=%parent_dir%
goto :find_loop
:parse_project_context
@rem Parse kotlin_cli_version and kotlin_cli_sha256 from the found wrapper_script without executing it.
set parsed_kotlin_cli_version=
set parsed_kotlin_cli_sha256=
for /f "tokens=2 delims==" %%A in ('findstr /r /c:"^set kotlin_cli_version=[A-Za-z0-9._+-]*$" "%wrapper_script%"') do (
if not defined parsed_kotlin_cli_version set parsed_kotlin_cli_version=%%A
)
for /f "tokens=2 delims==" %%A in ('findstr /r /c:"^set kotlin_cli_sha256=[0-9a-fA-F]*$" "%wrapper_script%"') do (
if not defined parsed_kotlin_cli_sha256 set parsed_kotlin_cli_sha256=%%A
)
if not defined parsed_kotlin_cli_version (
echo ERROR: Suspicious local wrapper script: failed to detect the distribution version in '%wrapper_script%' >&2
exit /b 1
)
if not defined parsed_kotlin_cli_sha256 (
echo ERROR: Suspicious local wrapper script: failed to detect the distribution checksum in '%wrapper_script%' >&2
exit /b 1
)
@rem Overwrite builtin values and proceed
set kotlin_cli_version=%parsed_kotlin_cli_version%
set kotlin_cli_sha256=%parsed_kotlin_cli_sha256%
exit /b 0
:fail
echo ERROR: Kotlin CLI bootstrap failed, see errors above
exit /b 1
:after_function_declarations
REM ********** Project-local version detection **********
if defined KOTLIN_CLI_WRAPPER_ALWAYS_USE_INTRINSIC_VERSION goto :after_local_version_detection
call :find_project_context
if errorlevel 1 goto :after_local_version_detection
call :parse_project_context
if errorlevel 1 goto fail
:after_local_version_detection
REM ********** Provision the Kotlin Toolchain distribution **********
set kotlin_cli_url=%KOTLIN_CLI_DOWNLOAD_ROOT%/org/jetbrains/kotlin/kotlin-cli/%kotlin_cli_version%/kotlin-cli-%kotlin_cli_version%-dist.tgz
set kotlin_cli_target_dir=%KOTLIN_CLI_BOOTSTRAP_CACHE_DIR%\kotlin-cli-%kotlin_cli_version%
call :download_and_extract "Kotlin Toolchain distribution v%kotlin_cli_version%" "%kotlin_cli_url%" "%kotlin_cli_target_dir%" "%kotlin_cli_sha256%" "256" "true"
if errorlevel 1 goto fail
REM ********** Launch the Kotlin CLI **********
rem Determine the correct busybox binary based on architecture
if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
set busybox_exe=%kotlin_cli_target_dir%\bin\busybox64a.exe
) else if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
set busybox_exe=%kotlin_cli_target_dir%\bin\busybox64u.exe
) else (
echo Unsupported architecture %PROCESSOR_ARCHITECTURE% >&2
goto fail
)
rem We use busybox here because it doesn't reinterpret the user-passed command-line arguments (that we pass via %*).
rem Also this way we can use the unified launcher script (.sh)
set KOTLIN_CLI_WRAPPER_PATH=%~f0
rem The '& call' after the app run is to avoid the "Terminate batch job (Y/N)?" prompt
"%busybox_exe%" sh "%kotlin_cli_target_dir%\bin\launcher.sh" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem We use "%COMSPEC%" /d /c exit so that and/or operators work properly when calling kotlin.bat directly from a script
@rem without the `call` command. With a plain `exit /B %ERRORLEVEL%`, using `kotlin.bat && echo success` would print
@rem 'success' even in case of failure. Consumers would have to use `call kotlin.bat && echo success` for it to work.`
"%COMSPEC%" /d /c exit %ERRORLEVEL%
-119
View File
@@ -1,119 +0,0 @@
product:
type: kmp/lib
platforms:
- android
- js
- jvm
- linuxArm64
- linuxX64
- mingwX64
- wasmJs
description: Simple logging library for kotlin
repositories:
- id: mavenLocal
url: mavenLocal
resolve: false
publish: true
plugins:
dokka:
enabled: true
moduleName: KSLog
moduleVersion: 2.1.0
sourceLinkBaseUrl: https://github.com/InsanusMokrassar/KSLog/blob/master/kslog
skipDeprecated: true
github-release:
enabled: true
owner: InsanusMokrassar
repository: KSLog
version: 2.1.0
tagPrefix: v
targetCommitish: 2.1.0
changelogFile: CHANGELOG.md
settings:
android:
buildToolsVersion: 37.0.0
compileSdk: 37
minSdk: 21
namespace: dev.inmo.kslog
junit: junit-4
jvm:
jdk:
version: 17
release: null
kotlin:
version: 2.4.20
publishing:
enabled: true
group: dev.inmo
version: 2.1.0
artifactId: kslog
signArtifacts: true
publishSources: true
pom:
name: kslog
description: Simple logging library for kotlin
url: https://github.com/InsanusMokrassar/KSLog
scm:
url: https://github.com/InsanusMokrassar/KSLog.git
developerConnection: scm:git:[fetch=]https://github.com/InsanusMokrassar/KSLog.git[push=]https://github.com/InsanusMokrassar/KSLog.git
developers:
- id: Akkihi
name: Akkihi
email: akkihiguy@gmail.com
- id: InsanusMokrassar
name: InsanusMokrassar
email: ovsyannikov.alexey95@gmail.com
licenses:
- name: MIT License
url: https://opensource.org/licenses/MIT
settings@android:
kotlin:
freeCompilerArgs:
- -module-name=dev.inmo_kslog
- -jvm-target=17
settings@js:
kotlin:
freeCompilerArgs:
- -Xir-module-name=dev.inmo:kslog
settings@jvm:
kotlin:
freeCompilerArgs:
- -module-name=dev.inmo_kslog
- -jvm-target=1.8
settings@linuxArm64:
kotlin:
freeCompilerArgs:
- -module-name=dev.inmo:kslog
- -Xshort-module-name=kslog
settings@linuxX64:
kotlin:
freeCompilerArgs:
- -module-name=dev.inmo:kslog
- -Xshort-module-name=kslog
settings@mingwX64:
kotlin:
freeCompilerArgs:
- -module-name=dev.inmo:kslog
- -Xshort-module-name=kslog
settings@native:
kotlin:
freeCompilerArgs:
- -module-name=dev.inmo:kslog_nativeMain
- -Xshort-module-name=kslog_nativeMain
- -Xexport-kdoc
settings@wasmJs:
kotlin:
freeCompilerArgs:
- -Xir-module-name=dev.inmo:kslog
-57
View File
@@ -1,57 +0,0 @@
package dev.inmo.kslog.common
import dev.inmo.kslog.common.filter.filtered
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
class CallbackKSLogTest {
@Test
fun forwardsExactArguments() {
val expectedThrowable = IllegalStateException("boom")
var observedLevel: LogLevel? = null
var observedTag: String? = null
var observedMessage: Any? = null
var observedThrowable: Throwable? = null
val logger = KSLog { level, tag, message, throwable ->
observedLevel = level
observedTag = tag
observedMessage = message
observedThrowable = throwable
}
logger.performLog(LogLevel.WARNING, "network", "retrying", expectedThrowable)
assertEquals(LogLevel.WARNING, observedLevel)
assertEquals("network", observedTag)
assertEquals("retrying", observedMessage)
assertSame(expectedThrowable, observedThrowable)
}
@Test
fun filteredLoggerDoesNotBuildRejectedLazyMessage() {
var callbackInvoked = false
var messageBuilt = false
val logger = KSLog { _, _, _, _ -> callbackInvoked = true }
.filtered { level, _, _ -> level >= LogLevel.INFO }
logger.performLog(LogLevel.DEBUG, "test", null) {
messageBuilt = true
"expensive"
}
assertFalse(callbackInvoked)
assertFalse(messageBuilt)
logger.performLog(LogLevel.ERROR, "test", null) {
messageBuilt = true
"needed"
}
assertTrue(callbackInvoked)
assertTrue(messageBuilt)
}
}
-201
View File
@@ -1,201 +0,0 @@
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.
-27
View File
@@ -1,27 +0,0 @@
# 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.
-20
View File
@@ -1,20 +0,0 @@
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
-37
View File
@@ -1,37 +0,0 @@
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
-65
View File
@@ -1,65 +0,0 @@
/*
* 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,
)
-22
View File
@@ -1,22 +0,0 @@
/*
* 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
}
-340
View File
@@ -1,340 +0,0 @@
/*
* 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 = listOf("android-37.0", "android-37")
.map { Path.of(sdkRoot).resolve("platforms/$it/android.jar") }
.firstOrNull { Files.isRegularFile(it) }
if (androidJar != null) 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"
}
}
}
@@ -1,110 +0,0 @@
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(): &lt;Error class: unknown class&gt;")
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()
}
}
}
-17
View File
@@ -1,17 +0,0 @@
# 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.
-15
View File
@@ -1,15 +0,0 @@
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
-9
View File
@@ -1,9 +0,0 @@
tasks:
release:
action: !dev.inmo.kslog.build.release.githubRelease
projectRootDir: ${project.rootDir}
settings: ${pluginSettings}
commands:
- name: githubRelease
performedBy: release
@@ -1,86 +0,0 @@
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
}
@@ -1,13 +0,0 @@
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"
}
-8
View File
@@ -1,8 +0,0 @@
modules:
- kslog
- plugins/dokka
- plugins/github-release
plugins:
- //plugins/dokka
- //plugins/github-release
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""Fail when KSLog's duplicated release version settings diverge."""
from __future__ import annotations
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[1]
def fail(message: str) -> None:
raise SystemExit(f"release consistency check failed: {message}")
def gradle_property(name: str) -> str:
for raw_line in (ROOT / "gradle.properties").read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key.strip() == name:
return value.strip()
fail(f"missing {name!r} in gradle.properties")
def yaml_scalar(path: tuple[str, ...]) -> str:
stack: list[tuple[int, str]] = []
for raw_line in (ROOT / "kslog/module.yaml").read_text(encoding="utf-8").splitlines():
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip(" "))
content = raw_line.strip()
if ":" not in content or content.startswith("-"):
continue
key, value = content.split(":", 1)
while stack and stack[-1][0] >= indent:
stack.pop()
candidate = tuple(item[1] for item in stack) + (key,)
value = value.strip().strip('"\'')
if candidate == path:
if not value:
fail(f"empty YAML scalar at {'.'.join(path)}")
return value
if not value:
stack.append((indent, key))
fail(f"missing YAML scalar at {'.'.join(path)}")
def main() -> None:
version = gradle_property("version")
copies = {
"plugins.dokka.moduleVersion": yaml_scalar(("plugins", "dokka", "moduleVersion")),
"plugins.github-release.version": yaml_scalar(("plugins", "github-release", "version")),
"plugins.github-release.targetCommitish": yaml_scalar(
("plugins", "github-release", "targetCommitish")
),
"settings.publishing.version": yaml_scalar(("settings", "publishing", "version")),
}
mismatches = {name: value for name, value in copies.items() if value != version}
if mismatches:
rendered = ", ".join(f"{name}={value!r}" for name, value in mismatches.items())
fail(f"gradle.properties version is {version!r}, but {rendered}")
expected_heading = f"## {version}"
headings = [
(index, line)
for index, line in enumerate(
(ROOT / "CHANGELOG.md").read_text(encoding="utf-8").splitlines(), start=1
)
if line == expected_heading
]
if len(headings) != 1:
fail(f"expected exactly one literal {expected_heading!r} heading, found {len(headings)}")
print(
f"release consistency: PASS version={version} "
f"copies={len(copies)} changelogLine={headings[0][0]}"
)
if __name__ == "__main__":
try:
main()
except (OSError, UnicodeError) as error:
fail(str(error))
@@ -1,42 +0,0 @@
#!/usr/bin/env python3
"""Verify wrapper checkout attributes and on-disk line endings."""
from pathlib import Path
import subprocess
ROOT = Path(__file__).resolve().parents[1]
def fail(message: str) -> None:
raise SystemExit(f"wrapper line-ending check failed: {message}")
shell_bytes = (ROOT / "kotlin").read_bytes()
batch_bytes = (ROOT / "kotlin.bat").read_bytes()
if b"\r" in shell_bytes:
fail("kotlin must contain LF line endings only")
if b"\r\n" not in batch_bytes or b"\n" in batch_bytes.replace(b"\r\n", b""):
fail("kotlin.bat must contain CRLF line endings only")
completed = subprocess.run(
["git", "-C", str(ROOT), "check-attr", "text", "eol", "--", "kotlin", "kotlin.bat"],
check=True,
text=True,
stdout=subprocess.PIPE,
)
attributes = {}
for line in completed.stdout.splitlines():
path, attribute, value = line.split(": ", 2)
attributes[(path, attribute)] = value
expected = {
("kotlin", "text"): "set",
("kotlin", "eol"): "lf",
("kotlin.bat", "text"): "set",
("kotlin.bat", "eol"): "crlf",
}
if attributes != expected:
fail(f"unexpected attributes: {attributes}")
print("wrapper line endings: PASS kotlin=LF kotlin.bat=CRLF checkoutAttributes=PASS")
-39
View File
@@ -1,39 +0,0 @@
plugins {
id "org.jetbrains.kotlin.multiplatform" version "2.4.20"
id "com.android.kotlin.multiplatform.library" version "9.1.1"
}
def externalBuildDir = providers.environmentVariable("KSLOG_CONSUMER_BUILD_DIR").orNull
if (externalBuildDir != null) {
layout.buildDirectory.set(file(externalBuildDir))
}
kotlin {
jvm()
js {
nodejs()
}
wasmJs {
nodejs()
}
linuxX64()
linuxArm64()
mingwX64()
android {
namespace = "dev.inmo.kslog.verification"
compileSdk {
version = release(37)
}
minSdk {
version = release(21)
}
}
sourceSets {
commonMain {
dependencies {
implementation "dev.inmo:kslog:2.1.0"
}
}
}
}
-22
View File
@@ -1,22 +0,0 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
def testRepository = providers.environmentVariable("KSLOG_TEST_REPOSITORY").orNull
if (testRepository == null) {
throw new GradleException("KSLOG_TEST_REPOSITORY must point to an isolated Maven repository")
}
maven { url = uri(testRepository) }
google()
mavenCentral()
}
}
rootProject.name = "kslog-publication-consumer"
@@ -1,14 +0,0 @@
package dev.inmo.kslog.verification
import dev.inmo.kslog.common.CallbackKSLog
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
fun exercisePublishedApi(): String {
var captured = ""
val logger: KSLog = CallbackKSLog { level, tag, message, throwable ->
captured = listOf(level, tag, message, throwable).joinToString("|")
}
logger.performLog(LogLevel.INFO, "consumer", "message", null)
return captured
}
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
fixture_dir=$(mktemp -d /tmp/kslog-changelog-parser.XXXXXX)
trap 'rm -rf -- "$fixture_dir"' EXIT
cat > "$fixture_dir/CHANGELOG.md" <<'EOF'
# Changelog
## 2x1x0
regex lookalike must not match
## 2.1.0
literal section
## 2.0.0
older section
EOF
actual=$(bash "$root_dir/changelog_parser.sh" 2.1.0 "$fixture_dir/CHANGELOG.md")
if [[ "$actual" != $'\nliteral section' ]]; then
printf 'unexpected parser output:\n%s\n' "$actual" >&2
exit 1
fi
stdin_actual=$(bash "$root_dir/changelog_parser.sh" 2.1.0 < "$fixture_dir/CHANGELOG.md")
if [[ "$stdin_actual" != "$actual" ]]; then
printf 'stdin/file parser behavior differs\n' >&2
exit 1
fi
if bash "$root_dir/changelog_parser.sh" 9.9.9 "$fixture_dir/CHANGELOG.md" >/dev/null 2>&1; then
printf 'missing literal heading unexpectedly succeeded\n' >&2
exit 1
fi
printf 'changelog parser: PASS literal heading selected; regex lookalike rejected\n'
-110
View File
@@ -1,110 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
root_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
base_commit=${KSLOG_MIGRATION_BASE_COMMIT:-0b9fb30862c7319bb228039ee33eef9a0ddef95c}
work_dir=$(mktemp -d /tmp/kslog-gradle-verifier.XXXXXX)
cleanup() {
if [[ "${KSLOG_KEEP_VERIFICATION_TEMP:-false}" == "true" ]]; then
printf 'verification workspace retained at %s\n' "$work_dir"
else
rm -rf -- "$work_dir"
fi
}
trap cleanup EXIT
for command_name in git tar python3; do
command -v "$command_name" >/dev/null || {
printf 'required command is unavailable: %s\n' "$command_name" >&2
exit 1
}
done
git -C "$root_dir" cat-file -e "$base_commit^{commit}"
baseline_dir="$work_dir/baseline"
baseline_repo="$work_dir/baseline-m2"
current_repo="$work_dir/current-m2"
gradle_home="$work_dir/gradle-home"
gnupg_home="$work_dir/gnupg"
archive="$work_dir/baseline.tar"
mkdir -p "$baseline_dir" "$baseline_repo" "$current_repo" "$gradle_home" "$gnupg_home"
chmod 700 "$gnupg_home"
git -C "$root_dir" archive --format=tar --output="$archive" "$base_commit"
tar -xf "$archive" -C "$baseline_dir"
cache_source=${KSLOG_GRADLE_CACHE_SOURCE:-${GRADLE_USER_HOME:-${HOME}/.gradle}}
for cache_entry in caches wrapper; do
if [[ -d "$cache_source/$cache_entry" ]]; then
ln -s "$cache_source/$cache_entry" "$gradle_home/$cache_entry"
fi
done
if [[ -e "$gradle_home/gradle.properties" || -e "$gradle_home/init.d" ]]; then
printf 'isolated Gradle home unexpectedly contains user configuration\n' >&2
exit 1
fi
run_publication() {
local project_dir=$1
local repository_dir=$2
local log_file=$3
(
cd "$project_dir"
unset \
GITHUB_RELEASE_TOKEN GH_TOKEN GITHUB_TOKEN \
INMONEXUS_USER INMONEXUS_PASSWORD \
SONATYPE_USER SONATYPE_PASSWORD PUBLISHING_TYPE \
KOTLIN_TOOLCHAIN_SIGNING_KEY KOTLIN_TOOLCHAIN_SIGNING_KEY_PASSPHRASE \
GRADLE_OPTS JAVA_TOOL_OPTIONS _JAVA_OPTIONS
export GRADLE_USER_HOME="$gradle_home"
export GNUPGHOME="$gnupg_home"
./gradlew --no-daemon \
-Dmaven.repo.local="$repository_dir" \
clean publishToMavenLocal
) | tee "$log_file"
if find "$repository_dir" -type f -name '*.asc' -print -quit | grep -q .; then
printf 'isolated publication unexpectedly produced signatures: %s\n' "$repository_dir" >&2
exit 1
fi
if grep -Eq '^> Task :sign[A-Z]' "$log_file"; then
printf 'isolated publication unexpectedly executed a signing task\n' >&2
exit 1
fi
}
printf 'Publishing untouched baseline %s to an isolated repository\n' "$base_commit"
run_publication "$baseline_dir" "$baseline_repo" "$work_dir/baseline-publish.log"
printf 'Publishing post-move Gradle fallback to an isolated repository\n'
run_publication "$root_dir" "$current_repo" "$work_dir/current-publish.log"
python3 "$root_dir/verification/verify_gradle_publication.py" \
--project-root "$root_dir" \
--base-commit "$base_commit" \
--baseline-repository "$baseline_repo" \
--current-repository "$current_repo"
printf 'Compiling the committed consumer probe against the post-move publication\n'
(
unset \
GITHUB_RELEASE_TOKEN GH_TOKEN GITHUB_TOKEN \
INMONEXUS_USER INMONEXUS_PASSWORD \
SONATYPE_USER SONATYPE_PASSWORD PUBLISHING_TYPE \
KOTLIN_TOOLCHAIN_SIGNING_KEY KOTLIN_TOOLCHAIN_SIGNING_KEY_PASSPHRASE \
GRADLE_OPTS JAVA_TOOL_OPTIONS _JAVA_OPTIONS
export GRADLE_USER_HOME="$gradle_home"
export GNUPGHOME="$gnupg_home"
export KSLOG_TEST_REPOSITORY="$current_repo"
export KSLOG_CONSUMER_BUILD_DIR="$work_dir/consumer-build"
"$root_dir/gradlew" --no-daemon \
--project-dir "$root_dir/verification/consumer" \
--project-cache-dir "$work_dir/consumer-project-cache" \
assemble
) | tee "$work_dir/consumer.log"
printf 'Gradle fallback verification: PASS baseline=%s primaryFiles=46 consumer=PASS\n' "$base_commit"
-292
View File
@@ -1,292 +0,0 @@
#!/usr/bin/env python3
"""Compare post-move Gradle publication/API output with the untouched base."""
from __future__ import annotations
import argparse
import hashlib
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as ET
import zipfile
EXPECTED_ARTIFACT_IDS = {
"kslog",
"kslog-android",
"kslog-js",
"kslog-jvm",
"kslog-linuxarm32hfp",
"kslog-linuxarm64",
"kslog-linuxx64",
"kslog-mingwx64",
"kslog-wasm-js",
}
EXPECTED_VERSION = "2.1.0"
SOURCE_ROOTS = {
"kslog/src": "src/commonMain/kotlin",
"kslog/src@android": "src/androidMain/kotlin",
"kslog/src@js": "src/jsMain/kotlin",
"kslog/src@jvm": "src/jvmMain/kotlin",
"kslog/src@native": "src/nativeMain/kotlin",
"kslog/src@wasmJs": "src/wasmJsMain/kotlin",
}
def fail(message: str) -> None:
raise SystemExit(f"Gradle fallback verification failed: {message}")
def primary_files(repository: Path) -> dict[str, Path]:
group_root = repository / "dev/inmo"
if not group_root.is_dir():
fail(f"missing publication group directory: {group_root}")
ignored_names = {"maven-metadata-local.xml", "_remote.repositories"}
ignored_suffixes = (".asc", ".md5", ".sha1", ".sha256", ".sha512", ".lastUpdated")
result = {}
for path in sorted(group_root.rglob("*")):
if not path.is_file() or path.name in ignored_names or path.name.endswith(ignored_suffixes):
continue
result[path.relative_to(group_root).as_posix()] = path
return result
def zip_payload(path: Path) -> dict[str, str]:
with zipfile.ZipFile(path) as archive:
return {
info.filename: hashlib.sha256(archive.read(info)).hexdigest()
for info in archive.infolist()
if not info.is_dir() and info.filename != "META-INF/MANIFEST.MF"
}
def xml_text(element: ET.Element, child: str) -> str:
found = element.find(f"{{*}}{child}")
return "" if found is None or found.text is None else found.text.strip()
def pom_model(path: Path) -> dict[str, object]:
root = ET.parse(path).getroot()
def records(container: str, record: str, fields: tuple[str, ...]) -> tuple[tuple[str, ...], ...]:
parent = root.find(f"{{*}}{container}")
if parent is None:
return ()
return tuple(
sorted(tuple(xml_text(item, field) for field in fields) for item in parent.findall(f"{{*}}{record}"))
)
scm = root.find("{*}scm")
return {
"groupId": xml_text(root, "groupId"),
"artifactId": xml_text(root, "artifactId"),
"version": xml_text(root, "version"),
"packaging": xml_text(root, "packaging"),
"name": xml_text(root, "name"),
"description": xml_text(root, "description"),
"url": xml_text(root, "url"),
"licenses": records("licenses", "license", ("name", "url")),
"developers": records("developers", "developer", ("id", "name", "email")),
"scm": ()
if scm is None
else (
xml_text(scm, "url"),
xml_text(scm, "connection"),
xml_text(scm, "developerConnection"),
),
"dependencies": records(
"dependencies",
"dependency",
("groupId", "artifactId", "version", "scope", "type", "classifier", "optional"),
),
}
def validate_required_pom(path: Path, model: dict[str, object]) -> None:
expected_artifact_id = path.parent.parent.name
required = {
"groupId": "dev.inmo",
"artifactId": expected_artifact_id,
"version": EXPECTED_VERSION,
"name": "kslog",
"description": "Simple logging library for kotlin",
"url": "https://github.com/InsanusMokrassar/KSLog",
"licenses": (("MIT License", "https://opensource.org/licenses/MIT"),),
"developers": (
("Akkihi", "Akkihi", "akkihiguy@gmail.com"),
("InsanusMokrassar", "InsanusMokrassar", "ovsyannikov.alexey95@gmail.com"),
),
"scm": (
"https://github.com/InsanusMokrassar/KSLog.git",
"",
"scm:git:[fetch=]https://github.com/InsanusMokrassar/KSLog.git[push=]https://github.com/InsanusMokrassar/KSLog.git",
),
}
for field, expected in required.items():
if model[field] != expected:
fail(f"required POM metadata mismatch in {path}: {field}={model[field]!r}")
def compare_publications(baseline_repository: Path, current_repository: Path) -> None:
baseline = primary_files(baseline_repository)
current = primary_files(current_repository)
if len(baseline) != 46 or len(current) != 46:
fail(f"expected 46 primary files, got baseline={len(baseline)} current={len(current)}")
if baseline.keys() != current.keys():
missing = sorted(baseline.keys() - current.keys())
added = sorted(current.keys() - baseline.keys())
fail(f"publication inventory differs; missing={missing}, added={added}")
artifact_ids = {relative.split("/", 1)[0] for relative in baseline}
if artifact_ids != EXPECTED_ARTIFACT_IDS:
fail(f"published artifact IDs differ: {sorted(artifact_ids)}")
source_jars = [relative for relative in baseline if relative.endswith("-sources.jar")]
if len(source_jars) != 9:
fail(f"expected 9 source JARs, found {len(source_jars)}")
for relative in source_jars:
if zip_payload(baseline[relative]) != zip_payload(current[relative]):
fail(f"source JAR payload differs: {relative}")
pom_paths = [relative for relative in baseline if relative.endswith(".pom")]
if len(pom_paths) != 9:
fail(f"expected 9 POMs, found {len(pom_paths)}")
for relative in pom_paths:
baseline_model = pom_model(baseline[relative])
current_model = pom_model(current[relative])
validate_required_pom(baseline[relative], baseline_model)
validate_required_pom(current[relative], current_model)
if baseline_model != current_model:
fail(f"POM model differs: {relative}")
print("publication inventory: PASS primaryFiles=46 coordinates=9 sourceJars=9 poms=9")
def verify_source_bytes(project_root: Path, base_commit: str) -> None:
checked = 0
for current_root, previous_root in SOURCE_ROOTS.items():
for current_path in sorted((project_root / current_root).rglob("*.kt")):
relative = current_path.relative_to(project_root / current_root)
previous_path = f"{previous_root}/{relative.as_posix()}"
completed = subprocess.run(
["git", "-C", str(project_root), "show", f"{base_commit}:{previous_path}"],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
fail(f"base source is missing: {previous_path}")
if completed.stdout != current_path.read_bytes():
fail(f"production source bytes changed: {current_path.relative_to(project_root)}")
checked += 1
if checked != 20:
fail(f"expected 20 production sources, checked {checked}")
print("production source identity: PASS files=20")
def javap_path() -> str:
java_home = Path(os.environ.get("JAVA_HOME", ""))
candidate = java_home / "bin/javap"
if candidate.is_file():
return str(candidate)
found = shutil.which("javap")
if found is None:
fail("javap is unavailable; set JAVA_HOME to a JDK")
return found
def public_api(archive_path: Path) -> tuple[tuple[str, ...], str, bytes]:
with zipfile.ZipFile(archive_path) as archive:
class_names = tuple(
sorted(
info.filename[:-6].replace("/", ".")
for info in archive.infolist()
if info.filename.endswith(".class") and not info.filename.startswith("META-INF/")
)
)
module_entry = "META-INF/dev.inmo_kslog.kotlin_module"
if module_entry not in archive.namelist():
fail(f"missing Kotlin module identity in {archive_path}")
module_bytes = archive.read(module_entry)
output = []
for class_name in class_names:
completed = subprocess.run(
[javap_path(), "-public", "-s", "-constants", "-classpath", str(archive_path), class_name],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
fail(f"javap failed for {class_name}: {completed.stderr.strip()}")
output.append(completed.stdout)
return class_names, "\n".join(output), module_bytes
def nested_classes_jar(aar: Path, destination: Path) -> Path:
with zipfile.ZipFile(aar) as archive:
try:
classes = archive.read("classes.jar")
except KeyError as error:
fail(f"Gradle AAR lacks classes.jar: {aar}")
destination.write_bytes(classes)
return destination
def compare_public_jvm_signatures(baseline_repository: Path, current_repository: Path) -> None:
group = Path("dev/inmo")
jvm_relative = group / "kslog-jvm/2.1.0/kslog-jvm-2.1.0.jar"
android_relative = group / "kslog-android/2.1.0/kslog-android-2.1.0.aar"
baseline_jvm = public_api(baseline_repository / jvm_relative)
current_jvm = public_api(current_repository / jvm_relative)
if baseline_jvm != current_jvm:
fail("JVM class list, public method/field signatures, or Kotlin module bytes differ")
if len(current_jvm[0]) != 25:
fail(f"expected 25 JVM classes, found {len(current_jvm[0])}")
with tempfile.TemporaryDirectory(prefix="kslog-android-api-") as temporary:
temporary_path = Path(temporary)
baseline_android = public_api(
nested_classes_jar(baseline_repository / android_relative, temporary_path / "baseline.jar")
)
current_android = public_api(
nested_classes_jar(current_repository / android_relative, temporary_path / "current.jar")
)
if baseline_android != current_android:
fail("Android class list, public method/field signatures, or Kotlin module bytes differ")
if len(current_android[0]) != 24:
fail(f"expected 24 Android classes, found {len(current_android[0])}")
with zipfile.ZipFile(baseline_repository / android_relative) as baseline_aar:
baseline_manifest = baseline_aar.read("AndroidManifest.xml")
with zipfile.ZipFile(current_repository / android_relative) as current_aar:
current_manifest = current_aar.read("AndroidManifest.xml")
if baseline_manifest != current_manifest:
fail("post-move Gradle AndroidManifest.xml differs from the untouched baseline")
print("public JVM signatures: PASS jvmClasses=25 androidClasses=24 module=dev.inmo_kslog")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--project-root", type=Path, required=True)
parser.add_argument("--base-commit", required=True)
parser.add_argument("--baseline-repository", type=Path, required=True)
parser.add_argument("--current-repository", type=Path, required=True)
return parser.parse_args()
def main() -> None:
args = parse_args()
compare_publications(args.baseline_repository, args.current_repository)
verify_source_bytes(args.project_root, args.base_commit)
compare_public_jvm_signatures(args.baseline_repository, args.current_repository)
if __name__ == "__main__":
main()