From 4d0f039787b209b05861b9821898e6ffb8cf5dba Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 2 Nov 2026 03:01:39 +0600 Subject: [PATCH] build: add Kotlin Toolchain migration with Gradle fallback --- .gitattributes | 3 + .github/workflows/build.yml | 58 +++ .github/workflows/kdocs.yml | 66 +++- .github/workflows/publish.yml | 29 -- DEVELOPMENT.md | 146 ++++++++ MIGRATION_PLAN.md | 159 ++++++++ MIGRATION_REPORT.md | 279 +++++++++++++++ README.md | 23 ++ build.gradle | 18 + changelog_parser.sh | 27 +- dokka.gradle | 4 +- gradle/libs.versions.toml | 8 + kotlin | 286 +++++++++++++++ kotlin.bat | 265 ++++++++++++++ kslog/module.yaml | 119 ++++++ .../kotlin => kslog/src}/AutoLoggers.kt | 0 .../kotlin => kslog/src}/CallbackKSLog.kt | 0 .../kotlin => kslog/src}/DefaultKSLog.kt | 0 .../kotlin => kslog/src}/Extensions.kt | 0 {src/commonMain/kotlin => kslog/src}/KSLog.kt | 0 .../kotlin => kslog/src}/KSLogTypealiases.kt | 0 ...LoggerDefaultPlatformLoggerLambdaExpect.kt | 0 .../kotlin => kslog/src}/TagLogger.kt | 0 .../src}/filter/FilterKSLog.kt | 0 .../src}/filter/FilteredExtension.kt | 0 .../kotlin => kslog/src}/typed/TypedKSLog.kt | 0 .../src}/typed/TypedKSLogBuilder.kt | 0 .../kotlin => kslog/src}/utils/Combination.kt | 0 .../KSLoggerDefaultPlatformLoggerLambda.kt | 0 .../src@js}/ExtendedConsole.kt | 0 .../KSLoggerDefaultPlatformLoggerLambda.kt | 0 .../kotlin => kslog/src@jvm}/ActualLogger.kt | 0 .../KSLoggerDefaultPlatformLoggerLambda.kt | 0 .../KSLoggerDefaultPlatformLoggerLambda.kt | 0 .../KSLoggerDefaultPlatformLoggerLambda.kt | 0 kslog/test@jvm/CallbackKSLogTest.kt | 57 +++ plugins/dokka/LICENSE.txt | 201 +++++++++++ plugins/dokka/README.md | 27 ++ plugins/dokka/module.yaml | 20 ++ plugins/dokka/plugin.yaml | 37 ++ plugins/dokka/src/DokkaConfiguration.kt | 65 ++++ plugins/dokka/src/DokkaSettings.kt | 22 ++ plugins/dokka/src/RunDokka.kt | 338 ++++++++++++++++++ plugins/dokka/test/RunDokkaOutputGuardTest.kt | 110 ++++++ plugins/github-release/README.md | 17 + plugins/github-release/module.yaml | 15 + plugins/github-release/plugin.yaml | 9 + plugins/github-release/src/GitHubRelease.kt | 86 +++++ .../src/GitHubReleaseSettings.kt | 13 + project.yaml | 8 + verification/check_release_consistency.py | 87 +++++ verification/check_wrapper_line_endings.py | 42 +++ verification/consumer/build.gradle | 39 ++ verification/consumer/settings.gradle | 22 ++ .../src/commonMain/kotlin/ConsumerSmoke.kt | 14 + verification/test_changelog_parser.sh | 41 +++ verification/verify_gradle_fallback.sh | 110 ++++++ verification/verify_gradle_publication.py | 292 +++++++++++++++ 58 files changed, 3115 insertions(+), 47 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 DEVELOPMENT.md create mode 100644 MIGRATION_PLAN.md create mode 100644 MIGRATION_REPORT.md create mode 100755 kotlin create mode 100644 kotlin.bat create mode 100644 kslog/module.yaml rename {src/commonMain/kotlin => kslog/src}/AutoLoggers.kt (100%) rename {src/commonMain/kotlin => kslog/src}/CallbackKSLog.kt (100%) rename {src/commonMain/kotlin => kslog/src}/DefaultKSLog.kt (100%) rename {src/commonMain/kotlin => kslog/src}/Extensions.kt (100%) rename {src/commonMain/kotlin => kslog/src}/KSLog.kt (100%) rename {src/commonMain/kotlin => kslog/src}/KSLogTypealiases.kt (100%) rename {src/commonMain/kotlin => kslog/src}/KSLoggerDefaultPlatformLoggerLambdaExpect.kt (100%) rename {src/commonMain/kotlin => kslog/src}/TagLogger.kt (100%) rename {src/commonMain/kotlin => kslog/src}/filter/FilterKSLog.kt (100%) rename {src/commonMain/kotlin => kslog/src}/filter/FilteredExtension.kt (100%) rename {src/commonMain/kotlin => kslog/src}/typed/TypedKSLog.kt (100%) rename {src/commonMain/kotlin => kslog/src}/typed/TypedKSLogBuilder.kt (100%) rename {src/commonMain/kotlin => kslog/src}/utils/Combination.kt (100%) rename {src/androidMain/kotlin => kslog/src@android}/KSLoggerDefaultPlatformLoggerLambda.kt (100%) rename {src/jsMain/kotlin => kslog/src@js}/ExtendedConsole.kt (100%) rename {src/jsMain/kotlin => kslog/src@js}/KSLoggerDefaultPlatformLoggerLambda.kt (100%) rename {src/jvmMain/kotlin => kslog/src@jvm}/ActualLogger.kt (100%) rename {src/jvmMain/kotlin => kslog/src@jvm}/KSLoggerDefaultPlatformLoggerLambda.kt (100%) rename {src/nativeMain/kotlin => kslog/src@native}/KSLoggerDefaultPlatformLoggerLambda.kt (100%) rename {src/wasmJsMain/kotlin => kslog/src@wasmJs}/KSLoggerDefaultPlatformLoggerLambda.kt (100%) create mode 100644 kslog/test@jvm/CallbackKSLogTest.kt create mode 100644 plugins/dokka/LICENSE.txt create mode 100644 plugins/dokka/README.md create mode 100644 plugins/dokka/module.yaml create mode 100644 plugins/dokka/plugin.yaml create mode 100644 plugins/dokka/src/DokkaConfiguration.kt create mode 100644 plugins/dokka/src/DokkaSettings.kt create mode 100644 plugins/dokka/src/RunDokka.kt create mode 100644 plugins/dokka/test/RunDokkaOutputGuardTest.kt create mode 100644 plugins/github-release/README.md create mode 100644 plugins/github-release/module.yaml create mode 100644 plugins/github-release/plugin.yaml create mode 100644 plugins/github-release/src/GitHubRelease.kt create mode 100644 plugins/github-release/src/GitHubReleaseSettings.kt create mode 100644 project.yaml create mode 100644 verification/check_release_consistency.py create mode 100644 verification/check_wrapper_line_endings.py create mode 100644 verification/consumer/build.gradle create mode 100644 verification/consumer/settings.gradle create mode 100644 verification/consumer/src/commonMain/kotlin/ConsumerSmoke.kt create mode 100644 verification/test_changelog_parser.sh create mode 100644 verification/verify_gradle_fallback.sh create mode 100644 verification/verify_gradle_publication.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..505a267 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.kt text eol=lf +kotlin text eol=lf +kotlin.bat text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..daedfc6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,58 @@ +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 + - name: Install the declared Android SDK + run: sdkmanager "platforms;android-37" "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 diff --git a/.github/workflows/kdocs.yml b/.github/workflows/kdocs.yml index fbcb0e8..ee81237 100644 --- a/.github/workflows/kdocs.yml +++ b/.github/workflows/kdocs.yml @@ -1,21 +1,71 @@ name: Publish KDocs + on: push: branches: - master + +permissions: + contents: read + +concurrency: + group: kdocs-${{ github.ref }} + cancel-in-progress: false + +env: + KOTLIN_CLI_NO_WELCOME_BANNER: "1" + jobs: - publishing: + generate: runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - java-version: 17 - - name: Build - run: ./gradlew dokkaHtml + persist-credentials: false + - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4 + with: + distribution: temurin + java-version: "17" + - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3 + - name: Install the declared Android SDK + run: sdkmanager "platforms;android-37" "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 - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/dokka/html + publish_dir: ./kdocs-html publish_branch: kdocs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index c3ea479..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,29 +0,0 @@ -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 }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..c6dc50c --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,146 @@ +# Developing KSLog + +## Prerequisites + +- JDK 17 +- Android SDK platform 37 and build tools 37.0.0 +- 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 `, 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. diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md new file mode 100644 index 0000000..cf70d0a --- /dev/null +++ b/MIGRATION_PLAN.md @@ -0,0 +1,159 @@ +# 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=`; 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`. diff --git a/MIGRATION_REPORT.md b/MIGRATION_REPORT.md new file mode 100644 index 0000000..6a6d2c3 --- /dev/null +++ b/MIGRATION_REPORT.md @@ -0,0 +1,279 @@ +# 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 `pom` 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. +- Remote CI execution is intentionally unverified until the branch is pushed. + +## Remote-only checks not performed + +- GitHub Actions on an actual hosted runner. +- 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. diff --git a/README.md b/README.md index 3f9d251..24b8152 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,26 @@ implementation "dev.inmo:kslog:$kslog_version" ${kslog_version} ``` + +## 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). diff --git a/build.gradle b/build.gradle index fb766e6..b403e2a 100644 --- a/build.gradle +++ b/build.gradle @@ -82,21 +82,39 @@ kotlin { sourceSets { commonMain { + kotlin.srcDir("kslog/src") dependencies { implementation libs.kotlin } } commonTest { + kotlin.srcDir("kslog/test") dependencies { implementation libs.kotlin.test.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 { + kotlin.srcDir("kslog/test@jvm") dependencies { implementation libs.kotlin.test.junit } } + nativeMain { + kotlin.srcDir("kslog/src@native") + } + wasmJsMain { + kotlin.srcDir("kslog/src@wasmJs") + } } } diff --git a/changelog_parser.sh b/changelog_parser.sh index 9dd39e6..5a6c106 100755 --- a/changelog_parser.sh +++ b/changelog_parser.sh @@ -1,21 +1,32 @@ #!/bin/bash function parse() { - version="$1" + local version="$1" + local line + local found="" - while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+ $version"`" ] - do - : # do nothing + while IFS= read -r line; do + if [[ "$line" == "## $version" ]]; then + found="yes" + break + fi done - while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+"`" ] - do - echo "$line" + if [[ -z "$found" ]]; then + printf 'No literal changelog heading found for %s\n' "$version" >&2 + return 1 + fi + + while IFS= read -r line; do + if [[ "$line" == \#* ]]; then + break + fi + printf '%s\n' "$line" done } version="$1" -file="$2" +file="${2:-}" if [ -n "$file" ]; then parse "$version" < "$file" diff --git a/dokka.gradle b/dokka.gradle index f533b7c..28d3325 100644 --- a/dokka.gradle +++ b/dokka.gradle @@ -1,10 +1,10 @@ Object callback = { switch (true) { case project.hasProperty("DOKKA_PATH"): - outputDirectory = project.property("DOKKA_PATH").toString() + outputDirectory.set(file(project.property("DOKKA_PATH").toString())) break case System.getenv("DOKKA_PATH") != null: - outputDirectory = System.getenv("DOKKA_PATH") + outputDirectory.set(file(System.getenv("DOKKA_PATH"))) break } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 604481c..b856841 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,14 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v 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" } +# 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] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/kotlin b/kotlin new file mode 100755 index 0000000..7d8b8ac --- /dev/null +++ b/kotlin @@ -0,0 +1,286 @@ +#!/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 </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" "$@" diff --git a/kotlin.bat b/kotlin.bat new file mode 100644 index 0000000..c08d3b7 --- /dev/null +++ b/kotlin.bat @@ -0,0 +1,265 @@ +@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% diff --git a/kslog/module.yaml b/kslog/module.yaml new file mode 100644 index 0000000..80e4d12 --- /dev/null +++ b/kslog/module.yaml @@ -0,0 +1,119 @@ +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 diff --git a/src/commonMain/kotlin/AutoLoggers.kt b/kslog/src/AutoLoggers.kt similarity index 100% rename from src/commonMain/kotlin/AutoLoggers.kt rename to kslog/src/AutoLoggers.kt diff --git a/src/commonMain/kotlin/CallbackKSLog.kt b/kslog/src/CallbackKSLog.kt similarity index 100% rename from src/commonMain/kotlin/CallbackKSLog.kt rename to kslog/src/CallbackKSLog.kt diff --git a/src/commonMain/kotlin/DefaultKSLog.kt b/kslog/src/DefaultKSLog.kt similarity index 100% rename from src/commonMain/kotlin/DefaultKSLog.kt rename to kslog/src/DefaultKSLog.kt diff --git a/src/commonMain/kotlin/Extensions.kt b/kslog/src/Extensions.kt similarity index 100% rename from src/commonMain/kotlin/Extensions.kt rename to kslog/src/Extensions.kt diff --git a/src/commonMain/kotlin/KSLog.kt b/kslog/src/KSLog.kt similarity index 100% rename from src/commonMain/kotlin/KSLog.kt rename to kslog/src/KSLog.kt diff --git a/src/commonMain/kotlin/KSLogTypealiases.kt b/kslog/src/KSLogTypealiases.kt similarity index 100% rename from src/commonMain/kotlin/KSLogTypealiases.kt rename to kslog/src/KSLogTypealiases.kt diff --git a/src/commonMain/kotlin/KSLoggerDefaultPlatformLoggerLambdaExpect.kt b/kslog/src/KSLoggerDefaultPlatformLoggerLambdaExpect.kt similarity index 100% rename from src/commonMain/kotlin/KSLoggerDefaultPlatformLoggerLambdaExpect.kt rename to kslog/src/KSLoggerDefaultPlatformLoggerLambdaExpect.kt diff --git a/src/commonMain/kotlin/TagLogger.kt b/kslog/src/TagLogger.kt similarity index 100% rename from src/commonMain/kotlin/TagLogger.kt rename to kslog/src/TagLogger.kt diff --git a/src/commonMain/kotlin/filter/FilterKSLog.kt b/kslog/src/filter/FilterKSLog.kt similarity index 100% rename from src/commonMain/kotlin/filter/FilterKSLog.kt rename to kslog/src/filter/FilterKSLog.kt diff --git a/src/commonMain/kotlin/filter/FilteredExtension.kt b/kslog/src/filter/FilteredExtension.kt similarity index 100% rename from src/commonMain/kotlin/filter/FilteredExtension.kt rename to kslog/src/filter/FilteredExtension.kt diff --git a/src/commonMain/kotlin/typed/TypedKSLog.kt b/kslog/src/typed/TypedKSLog.kt similarity index 100% rename from src/commonMain/kotlin/typed/TypedKSLog.kt rename to kslog/src/typed/TypedKSLog.kt diff --git a/src/commonMain/kotlin/typed/TypedKSLogBuilder.kt b/kslog/src/typed/TypedKSLogBuilder.kt similarity index 100% rename from src/commonMain/kotlin/typed/TypedKSLogBuilder.kt rename to kslog/src/typed/TypedKSLogBuilder.kt diff --git a/src/commonMain/kotlin/utils/Combination.kt b/kslog/src/utils/Combination.kt similarity index 100% rename from src/commonMain/kotlin/utils/Combination.kt rename to kslog/src/utils/Combination.kt diff --git a/src/androidMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt b/kslog/src@android/KSLoggerDefaultPlatformLoggerLambda.kt similarity index 100% rename from src/androidMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt rename to kslog/src@android/KSLoggerDefaultPlatformLoggerLambda.kt diff --git a/src/jsMain/kotlin/ExtendedConsole.kt b/kslog/src@js/ExtendedConsole.kt similarity index 100% rename from src/jsMain/kotlin/ExtendedConsole.kt rename to kslog/src@js/ExtendedConsole.kt diff --git a/src/jsMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt b/kslog/src@js/KSLoggerDefaultPlatformLoggerLambda.kt similarity index 100% rename from src/jsMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt rename to kslog/src@js/KSLoggerDefaultPlatformLoggerLambda.kt diff --git a/src/jvmMain/kotlin/ActualLogger.kt b/kslog/src@jvm/ActualLogger.kt similarity index 100% rename from src/jvmMain/kotlin/ActualLogger.kt rename to kslog/src@jvm/ActualLogger.kt diff --git a/src/jvmMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt b/kslog/src@jvm/KSLoggerDefaultPlatformLoggerLambda.kt similarity index 100% rename from src/jvmMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt rename to kslog/src@jvm/KSLoggerDefaultPlatformLoggerLambda.kt diff --git a/src/nativeMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt b/kslog/src@native/KSLoggerDefaultPlatformLoggerLambda.kt similarity index 100% rename from src/nativeMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt rename to kslog/src@native/KSLoggerDefaultPlatformLoggerLambda.kt diff --git a/src/wasmJsMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt b/kslog/src@wasmJs/KSLoggerDefaultPlatformLoggerLambda.kt similarity index 100% rename from src/wasmJsMain/kotlin/KSLoggerDefaultPlatformLoggerLambda.kt rename to kslog/src@wasmJs/KSLoggerDefaultPlatformLoggerLambda.kt diff --git a/kslog/test@jvm/CallbackKSLogTest.kt b/kslog/test@jvm/CallbackKSLogTest.kt new file mode 100644 index 0000000..c837194 --- /dev/null +++ b/kslog/test@jvm/CallbackKSLogTest.kt @@ -0,0 +1,57 @@ +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) + } +} diff --git a/plugins/dokka/LICENSE.txt b/plugins/dokka/LICENSE.txt new file mode 100644 index 0000000..244380c --- /dev/null +++ b/plugins/dokka/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020-2021 JetBrains s.r.o. and and respective authors and developers. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/dokka/README.md b/plugins/dokka/README.md new file mode 100644 index 0000000..82c4a0e --- /dev/null +++ b/plugins/dokka/README.md @@ -0,0 +1,27 @@ +# KSLog Dokka Toolchain plugin + +This local plugin is adapted from JetBrains Kotlin Toolchain's +`build-sources/dokka` plugin at release `v0.13.0`, commit +`abc7a5f7579e9ef267107782048c76777f0bf932`. + +Upstream source: + +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. diff --git a/plugins/dokka/module.yaml b/plugins/dokka/module.yaml new file mode 100644 index 0000000..65da0d7 --- /dev/null +++ b/plugins/dokka/module.yaml @@ -0,0 +1,20 @@ +product: jvm/amper-plugin + +description: KMP-capable Dokka HTML and GFM tasks adapted from the Kotlin Toolchain Dokka plugin. + +pluginInfo: + id: dokka + settingsClass: dev.inmo.kslog.build.dokka.DokkaSettings + +test-dependencies: + - $libs.kotlin.test.junit + +settings: + junit: junit-4 + jvm: + jdk: + version: 17 + release: 17 + kotlin: + version: 2.4.20 + serialization: json diff --git a/plugins/dokka/plugin.yaml b/plugins/dokka/plugin.yaml new file mode 100644 index 0000000..4c11383 --- /dev/null +++ b/plugins/dokka/plugin.yaml @@ -0,0 +1,37 @@ +tasks: + html: + action: !dev.inmo.kslog.build.dokka.dokkaHtml + projectRootDir: ${project.rootDir} + moduleRootDir: ${module.rootDir} + moduleCompileClasspath: ${module.compileClasspath} + runtimeClasspath: + - $libs.dokka.cli + pluginsClasspath: + - $libs.dokka.base + - $libs.dokka.analysis.kotlin.descriptors + - $libs.kotlinx.html.jvm + - $libs.freemarker + outputDir: ${project.rootDir}/build/dokka/html + settings: ${pluginSettings} + + gfm: + action: !dev.inmo.kslog.build.dokka.dokkaGfm + projectRootDir: ${project.rootDir} + moduleRootDir: ${module.rootDir} + moduleCompileClasspath: ${module.compileClasspath} + runtimeClasspath: + - $libs.dokka.cli + pluginsClasspath: + - $libs.dokka.base + - $libs.dokka.analysis.kotlin.descriptors + - $libs.dokka.gfm + - $libs.kotlinx.html.jvm + - $libs.freemarker + outputDir: ${project.rootDir}/build/dokka/gfm + settings: ${pluginSettings} + +commands: + - name: dokkaHtml + performedBy: html + - name: dokkaGfm + performedBy: gfm diff --git a/plugins/dokka/src/DokkaConfiguration.kt b/plugins/dokka/src/DokkaConfiguration.kt new file mode 100644 index 0000000..021bf10 --- /dev/null +++ b/plugins/dokka/src/DokkaConfiguration.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +/* + * KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0 + * build-sources/dokka. KSLog extends the configuration model for its KMP + * source-set graph and HTML/GFM plugin configuration. See ../README.md. + */ + +package dev.inmo.kslog.build.dokka + +import kotlinx.serialization.Serializable + +@Serializable +internal data class DokkaConfiguration( + val moduleName: String, + val moduleVersion: String, + val outputDir: String, + val sourceSets: List, + val pluginsClasspath: List, + val pluginsConfiguration: List, + 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, + val classpath: List, + val sourceRoots: List, + val documentedVisibilities: List = listOf("PUBLIC"), + val skipDeprecated: Boolean, + val jdkVersion: Int, + val languageVersion: String = "2.4", + val apiVersion: String = "2.4", + val analysisPlatform: String, + val sourceLinks: List, +) + +@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, +) diff --git a/plugins/dokka/src/DokkaSettings.kt b/plugins/dokka/src/DokkaSettings.kt new file mode 100644 index 0000000..d9d047f --- /dev/null +++ b/plugins/dokka/src/DokkaSettings.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +/* + * KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0 + * build-sources/dokka. KSLog adds versioned KMP documentation settings and + * project-specific source-link defaults. See ../README.md for provenance. + */ + +package dev.inmo.kslog.build.dokka + +import org.jetbrains.amper.plugins.Configurable + +@Configurable +interface DokkaSettings { + val moduleName: String get() = "KSLog" + val moduleVersion: String + val sourceLinkBaseUrl: String get() = "https://github.com/InsanusMokrassar/KSLog/blob/master/" + val sourceLinkLineSuffix: String get() = "#L" + val skipDeprecated: Boolean get() = true +} diff --git a/plugins/dokka/src/RunDokka.kt b/plugins/dokka/src/RunDokka.kt new file mode 100644 index 0000000..b7955ec --- /dev/null +++ b/plugins/dokka/src/RunDokka.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +/* + * KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0 + * build-sources/dokka. KSLog adds KMP source sets, HTML/GFM entry points, and + * project-confined, symlink-safe generated-output cleanup. See ../README.md. + */ + +package dev.inmo.kslog.build.dokka + +import kotlinx.serialization.json.Json +import org.jetbrains.amper.plugins.Classpath +import org.jetbrains.amper.plugins.ExecutionAvoidance +import org.jetbrains.amper.plugins.Input +import org.jetbrains.amper.plugins.Output +import org.jetbrains.amper.plugins.TaskAction +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path +import kotlin.concurrent.thread +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteIfExists +import kotlin.io.path.deleteRecursively +import kotlin.io.path.exists +import kotlin.io.path.pathString +import kotlin.io.path.writeText + +@TaskAction(ExecutionAvoidance.Disabled) +fun dokkaHtml( + projectRootDir: String, + moduleRootDir: String, + @Input moduleCompileClasspath: Classpath, + @Input runtimeClasspath: Classpath, + @Input pluginsClasspath: Classpath, + @Output outputDir: Path, + settings: DokkaSettings, +) = runDokka( + projectRootDir, + moduleRootDir, + moduleCompileClasspath, + runtimeClasspath, + pluginsClasspath, + outputDir, + settings, +) + +@TaskAction(ExecutionAvoidance.Disabled) +fun dokkaGfm( + projectRootDir: String, + moduleRootDir: String, + @Input moduleCompileClasspath: Classpath, + @Input runtimeClasspath: Classpath, + @Input pluginsClasspath: Classpath, + @Output outputDir: Path, + settings: DokkaSettings, +) = runDokka( + projectRootDir, + moduleRootDir, + moduleCompileClasspath, + runtimeClasspath, + pluginsClasspath, + outputDir, + settings, +) + +@OptIn(ExperimentalPathApi::class) +private fun runDokka( + projectRootDir: String, + moduleRootDir: String, + moduleCompileClasspath: Classpath, + runtimeClasspath: Classpath, + pluginsClasspath: Classpath, + outputDir: Path, + settings: DokkaSettings, +) { + val normalizedRoot = Path.of(moduleRootDir).toRealPath() + val normalizedOutput = prepareDokkaOutput(projectRootDir, outputDir) + + val commonId = SourceSetId(settings.moduleName, "commonMain") + val sourceLink = SourceLink( + localDirectory = normalizedRoot.pathString, + remoteUrl = settings.sourceLinkBaseUrl, + remoteLineSuffix = settings.sourceLinkLineSuffix, + ) + val analysisClasspaths = resolveDokkaAnalysisClasspaths(moduleCompileClasspath.resolvedFiles) + + fun sourceSet( + directoryName: String, + sourceSetName: String, + displayName: String, + analysisPlatform: String, + dependentOnCommon: Boolean, + classpath: List = emptyList(), + ): DokkaSourceSet? { + val sourceRoot = normalizedRoot.resolve(directoryName) + if (!sourceRoot.exists()) return null + return DokkaSourceSet( + sourceSetID = SourceSetId(settings.moduleName, sourceSetName), + displayName = displayName, + dependentSourceSets = if (dependentOnCommon) listOf(commonId) else emptyList(), + classpath = classpath, + sourceRoots = listOf(sourceRoot.pathString), + skipDeprecated = settings.skipDeprecated, + jdkVersion = if (displayName == "JVM") 8 else 17, + analysisPlatform = analysisPlatform, + sourceLinks = listOf(sourceLink), + ) + } + + val androidClasspath = buildList { + addAll(analysisClasspaths.jvm) + val sdkRoot = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") + if (sdkRoot != null) { + val androidJar = Path.of(sdkRoot).resolve("platforms/android-37/android.jar") + if (Files.isRegularFile(androidJar)) add(androidJar.pathString) + } + } + + val sourceSets = listOfNotNull( + sourceSet( + "src", + "commonMain", + "Common", + "common", + dependentOnCommon = false, + classpath = analysisClasspaths.commonNative, + ), + sourceSet( + "src@jvm", + "jvmMain", + "JVM", + "jvm", + dependentOnCommon = true, + classpath = analysisClasspaths.jvm, + ), + sourceSet("src@android", "androidMain", "Android", "jvm", dependentOnCommon = true, classpath = androidClasspath), + sourceSet("src@js", "jsMain", "JS", "js", dependentOnCommon = true, classpath = analysisClasspaths.js), + sourceSet( + "src@wasmJs", + "wasmJsMain", + "WasmJS", + "js", + dependentOnCommon = true, + classpath = analysisClasspaths.wasmJs, + ), + sourceSet( + "src@native", + "nativeMain", + "Native", + "native", + dependentOnCommon = true, + classpath = analysisClasspaths.commonNative, + ), + ) + + val configuration = DokkaConfiguration( + moduleName = settings.moduleName, + moduleVersion = settings.moduleVersion, + outputDir = normalizedOutput.pathString, + sourceSets = sourceSets, + pluginsClasspath = pluginsClasspath.resolvedFiles.map { it.pathString }, + pluginsConfiguration = listOf( + PluginConfiguration( + fqPluginName = "org.jetbrains.dokka.base.DokkaBase", + serializationFormat = "JSON", + values = Json.encodeToString(DokkaBase(footerMessage = "KSLog ${settings.moduleVersion}")), + ) + ), + ) + + val configurationFile = Files.createTempFile("kslog-dokka-", ".json") + try { + configurationFile.writeText(Json.encodeToString(configuration)) + val cliJar = runtimeClasspath.resolvedFiles.single() + val commandLine = listOf( + ProcessHandle.current().info().command().orElse("java"), + "-jar", + cliJar.pathString, + configurationFile.pathString, + ) + val process = ProcessBuilder(commandLine) + .directory(normalizedRoot.toFile()) + .start() + val capture = listOf( + thread { process.inputStream.copyTo(System.out) }, + thread { process.errorStream.copyTo(System.err) }, + ) + val exitCode = process.waitFor() + capture.forEach { it.join() } + check(exitCode == 0) { "Dokka terminated with exit code $exitCode" } + requireNoUnresolvedTypes(normalizedOutput) + } finally { + configurationFile.deleteIfExists() + } +} + +internal data class DokkaAnalysisClasspaths( + val commonNative: List, + val jvm: List, + val js: List, + val wasmJs: List, +) + +internal fun resolveDokkaAnalysisClasspaths(moduleClasspath: List): 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" + } + } +} diff --git a/plugins/dokka/test/RunDokkaOutputGuardTest.kt b/plugins/dokka/test/RunDokkaOutputGuardTest.kt new file mode 100644 index 0000000..b4a5f74 --- /dev/null +++ b/plugins/dokka/test/RunDokkaOutputGuardTest.kt @@ -0,0 +1,110 @@ +package dev.inmo.kslog.build.dokka + +import java.nio.file.Files +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteRecursively +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalPathApi::class) +class RunDokkaOutputGuardTest { + @Test + fun rejectsSymlinkedDokkaAncestorWithoutTouchingOutsideFiles() { + val projectRoot = Files.createTempDirectory("kslog-dokka-project-") + val outside = Files.createTempDirectory("kslog-dokka-outside-") + val sentinel = outside.resolve("sentinel.txt") + sentinel.writeText("preserve me") + val build = projectRoot.resolve("build").createDirectories() + val dokkaLink = build.resolve("dokka") + Files.createSymbolicLink(dokkaLink, outside) + + try { + val failure = assertFailsWith { + prepareDokkaOutput(projectRoot.toString(), dokkaLink.resolve("html")) + } + assertTrue(failure.message.orEmpty().contains("symbolic link")) + assertEquals("preserve me", sentinel.readText()) + } finally { + Files.deleteIfExists(dokkaLink) + projectRoot.deleteRecursively() + outside.deleteRecursively() + } + } + + @Test + fun cleansOnlyTheSelectedGeneratedOutputDirectory() { + val projectRoot = Files.createTempDirectory("kslog-dokka-project-") + val html = projectRoot.resolve("build/dokka/html").createDirectories() + val stale = html.resolve("stale.txt") + stale.writeText("old") + val gfmSentinel = projectRoot.resolve("build/dokka/gfm/sentinel.txt") + gfmSentinel.parent.createDirectories() + gfmSentinel.writeText("keep") + + try { + val prepared = prepareDokkaOutput(projectRoot.toString(), html) + assertEquals(html.toRealPath(), prepared.toRealPath()) + assertTrue(prepared.exists()) + assertFalse(stale.exists()) + assertEquals("keep", gfmSentinel.readText()) + } finally { + projectRoot.deleteRecursively() + } + } + + @Test + fun rejectsGeneratedDocumentationWithUnresolvedTypePlaceholders() { + val output = Files.createTempDirectory("kslog-dokka-output-") + val generatedPage = output.resolve("broken.md") + generatedPage.writeText("fun example(): <Error class: unknown class>") + + try { + val failure = assertFailsWith { + 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() + } + } +} diff --git a/plugins/github-release/README.md b/plugins/github-release/README.md new file mode 100644 index 0000000..cdb7cba --- /dev/null +++ b/plugins/github-release/README.md @@ -0,0 +1,17 @@ +# KSLog GitHub release Toolchain plugin + +Kotlin Toolchain 0.13.0 has no built-in GitHub release task. This local plugin +keeps the existing explicit release capability as a narrow wrapper around the +standard `gh release create` CLI. + +It extracts the matching version section from `CHANGELOG.md`, preserves the +legacy tag/title/target conventions, accepts the existing +`GITHUB_RELEASE_TOKEN` variable plus standard `GH_TOKEN`/`GITHUB_TOKEN`, and +supports a non-network `KSLOG_GITHUB_RELEASE_DRY_RUN=true` validation mode. It +runs the committed release/version consistency preflight before rendering or +creating a release. No CI workflow invokes it. + +`gh release create` does not modify local git state, but GitHub may create the +remote tag at `--target` when the requested tag does not already exist. A real +invocation therefore mutates both the remote release and potentially the remote +tag; dry-run mode performs neither action. diff --git a/plugins/github-release/module.yaml b/plugins/github-release/module.yaml new file mode 100644 index 0000000..33485e2 --- /dev/null +++ b/plugins/github-release/module.yaml @@ -0,0 +1,15 @@ +product: jvm/amper-plugin + +description: Explicit GitHub release command backed by the standard GitHub CLI. + +pluginInfo: + id: github-release + settingsClass: dev.inmo.kslog.build.release.GitHubReleaseSettings + +settings: + jvm: + jdk: + version: 17 + release: 17 + kotlin: + version: 2.4.20 diff --git a/plugins/github-release/plugin.yaml b/plugins/github-release/plugin.yaml new file mode 100644 index 0000000..a3dce20 --- /dev/null +++ b/plugins/github-release/plugin.yaml @@ -0,0 +1,9 @@ +tasks: + release: + action: !dev.inmo.kslog.build.release.githubRelease + projectRootDir: ${project.rootDir} + settings: ${pluginSettings} + +commands: + - name: githubRelease + performedBy: release diff --git a/plugins/github-release/src/GitHubRelease.kt b/plugins/github-release/src/GitHubRelease.kt new file mode 100644 index 0000000..ff6788e --- /dev/null +++ b/plugins/github-release/src/GitHubRelease.kt @@ -0,0 +1,86 @@ +package dev.inmo.kslog.build.release + +import org.jetbrains.amper.plugins.ExecutionAvoidance +import org.jetbrains.amper.plugins.TaskAction +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.deleteIfExists +import kotlin.io.path.isRegularFile +import kotlin.io.path.pathString +import kotlin.io.path.writeText + +@TaskAction(ExecutionAvoidance.Disabled) +fun githubRelease( + projectRootDir: String, + settings: GitHubReleaseSettings, +) { + val root = Path.of(projectRootDir).toAbsolutePath().normalize() + val changelog = root.resolve(settings.changelogFile).normalize() + require(changelog.startsWith(root) && changelog.isRegularFile()) { + "Changelog must be an existing file inside the module: $changelog" + } + + val releasePreflight = root.resolve("verification/check_release_consistency.py") + require(releasePreflight.isRegularFile()) { "Missing release consistency preflight: $releasePreflight" } + runCommand(root, listOf("python3", releasePreflight.pathString)) + + val parser = root.resolve("changelog_parser.sh") + require(parser.isRegularFile()) { "Missing changelog parser: $parser" } + val notes = runCommand(root, listOf("bash", parser.pathString, settings.version, changelog.pathString)) + .trim() + require(notes.isNotEmpty()) { "No CHANGELOG.md section found for ${settings.version}" } + + val tag = "${settings.tagPrefix}${settings.version}" + val targetCommitish = settings.targetCommitish.ifBlank { settings.version } + val repository = "${settings.owner}/${settings.repository}" + if (System.getenv("KSLOG_GITHUB_RELEASE_DRY_RUN").toBoolean()) { + println("Dry run: GitHub release $repository tag=$tag target=$targetCommitish title=${settings.version}") + println(notes) + return + } + + val token = System.getenv("GITHUB_RELEASE_TOKEN") + ?: System.getenv("GH_TOKEN") + ?: System.getenv("GITHUB_TOKEN") + ?: error("Set GITHUB_RELEASE_TOKEN, GH_TOKEN, or GITHUB_TOKEN before creating a release") + + val notesFile = Files.createTempFile("kslog-release-notes-", ".md") + try { + notesFile.writeText(notes) + val process = ProcessBuilder( + "gh", + "release", + "create", + tag, + "--repo", + repository, + "--title", + settings.version, + "--target", + targetCommitish, + "--notes-file", + notesFile.pathString, + ) + .directory(root.toFile()) + .redirectErrorStream(true) + process.environment()["GH_TOKEN"] = token + val started = process.start() + val output = started.inputStream.bufferedReader().use { it.readText() } + val exitCode = started.waitFor() + check(exitCode == 0) { "gh release create failed with exit code $exitCode:\n$output" } + print(output) + } finally { + notesFile.deleteIfExists() + } +} + +private fun runCommand(workingDirectory: Path, command: List): String { + 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 +} diff --git a/plugins/github-release/src/GitHubReleaseSettings.kt b/plugins/github-release/src/GitHubReleaseSettings.kt new file mode 100644 index 0000000..e616c8e --- /dev/null +++ b/plugins/github-release/src/GitHubReleaseSettings.kt @@ -0,0 +1,13 @@ +package dev.inmo.kslog.build.release + +import org.jetbrains.amper.plugins.Configurable + +@Configurable +interface GitHubReleaseSettings { + val owner: String + val repository: String + val version: String + val tagPrefix: String get() = "v" + val targetCommitish: String get() = "" + val changelogFile: String get() = "CHANGELOG.md" +} diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..f17d558 --- /dev/null +++ b/project.yaml @@ -0,0 +1,8 @@ +modules: + - kslog + - plugins/dokka + - plugins/github-release + +plugins: + - //plugins/dokka + - //plugins/github-release diff --git a/verification/check_release_consistency.py b/verification/check_release_consistency.py new file mode 100644 index 0000000..a15a99f --- /dev/null +++ b/verification/check_release_consistency.py @@ -0,0 +1,87 @@ +#!/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)) diff --git a/verification/check_wrapper_line_endings.py b/verification/check_wrapper_line_endings.py new file mode 100644 index 0000000..a84ef5f --- /dev/null +++ b/verification/check_wrapper_line_endings.py @@ -0,0 +1,42 @@ +#!/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") diff --git a/verification/consumer/build.gradle b/verification/consumer/build.gradle new file mode 100644 index 0000000..d1b2464 --- /dev/null +++ b/verification/consumer/build.gradle @@ -0,0 +1,39 @@ +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" + } + } + } +} diff --git a/verification/consumer/settings.gradle b/verification/consumer/settings.gradle new file mode 100644 index 0000000..2395ab8 --- /dev/null +++ b/verification/consumer/settings.gradle @@ -0,0 +1,22 @@ +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" diff --git a/verification/consumer/src/commonMain/kotlin/ConsumerSmoke.kt b/verification/consumer/src/commonMain/kotlin/ConsumerSmoke.kt new file mode 100644 index 0000000..14cdaef --- /dev/null +++ b/verification/consumer/src/commonMain/kotlin/ConsumerSmoke.kt @@ -0,0 +1,14 @@ +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 +} diff --git a/verification/test_changelog_parser.sh b/verification/test_changelog_parser.sh new file mode 100644 index 0000000..5137d2f --- /dev/null +++ b/verification/test_changelog_parser.sh @@ -0,0 +1,41 @@ +#!/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' diff --git a/verification/verify_gradle_fallback.sh b/verification/verify_gradle_fallback.sh new file mode 100644 index 0000000..cec199e --- /dev/null +++ b/verification/verify_gradle_fallback.sh @@ -0,0 +1,110 @@ +#!/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" diff --git a/verification/verify_gradle_publication.py b/verification/verify_gradle_publication.py new file mode 100644 index 0000000..b61528d --- /dev/null +++ b/verification/verify_gradle_publication.py @@ -0,0 +1,292 @@ +#!/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()