mirror of
https://github.com/InsanusMokrassar/KSLog.git
synced 2026-11-02 01:32:54 +00:00
Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b287bb318 | ||
|
|
9fc0fed48e | ||
|
|
4d0f039787 | ||
|
|
0b9fb30862 | ||
|
|
9edc998ccc | ||
|
|
e1b2aea2ea | ||
|
|
f5e26c0308 | ||
|
|
3fb8731f0a | ||
|
|
bd34d84095 | ||
|
|
42b56fdf99 | ||
|
|
cd63889a34 | ||
|
|
892ccd442b | ||
|
|
dac23a366b | ||
|
|
f8d6591c86 | ||
|
|
ddb0202069 | ||
|
|
fa16f7f5d3 | ||
|
|
d37630c1d4 | ||
|
|
e0e97292aa | ||
|
|
22b22c48f4 | ||
|
|
52f18b234d | ||
|
|
4d9708b53f | ||
|
|
91f3516c8f | ||
|
|
7fbea3c8b6 | ||
|
|
0417cd117a | ||
|
|
2015dc5083 | ||
|
|
7f92a3e614 | ||
|
|
3c4e59ddc2 | ||
|
|
461bc92966 | ||
|
|
8c0968ba86 | ||
|
|
526de46a98 | ||
|
|
0af8c78e37 | ||
|
|
6840598e04 | ||
|
|
7a2d070ee3 | ||
|
|
92651d3ba6 | ||
|
|
b8c72f1131 | ||
|
|
9b92fa29f4 | ||
|
|
42ced6edd2 | ||
|
|
dd3e4c4c2a | ||
|
|
f528909915 | ||
|
|
b3919c8439 | ||
|
|
d1dd159e36 | ||
|
|
3db55982e9 | ||
|
|
8e477b1056 | ||
|
|
480d2c8132 | ||
|
|
a2a73dfe83 | ||
|
|
4528918347 | ||
|
|
9ef93724ad | ||
|
|
dccbd13788 | ||
|
|
8bb34d6842 | ||
|
|
cff3f71de7 | ||
|
|
aab0ac064f | ||
|
|
d2909ed5f0 | ||
|
|
81eba88ce9 | ||
|
|
a73f0d132c | ||
|
|
bd87184f81 | ||
|
|
7228e81344 | ||
|
|
ac828b1af7 | ||
|
|
a8c85b9614 | ||
|
|
98fbf43d4d | ||
|
|
ef8a3e1500 | ||
|
|
3edbb78d56 | ||
|
|
663395ccbe | ||
|
|
ecd2b08c69 | ||
|
|
f5e50d00a2 | ||
|
|
a1784dc6af | ||
|
|
4d6410c870 | ||
|
|
3a4035f1a6 | ||
|
|
86bf9468ba | ||
|
|
24adeef67b | ||
|
|
4823df5c6f | ||
|
|
2d397e9b12 | ||
|
|
37a0fceb56 | ||
|
|
0860d6cacc | ||
|
|
d9863ded5a | ||
|
|
48a265e3d5 | ||
|
|
30cdbea2c9 | ||
|
|
b19639e8a1 | ||
|
|
30d82ebda6 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
*.kt text eol=lf
|
||||
kotlin text eol=lf
|
||||
kotlin.bat text eol=crlf
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Build and test
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
KOTLIN_CLI_NO_WELCOME_BANNER: "1"
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
cache: gradle
|
||||
- uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
|
||||
with:
|
||||
packages: platform-tools
|
||||
- name: Install the declared Android SDK
|
||||
run: sdkmanager "platforms;android-37.0" "build-tools;37.0.0"
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.cache/JetBrains/Kotlin
|
||||
key: kotlin-toolchain-${{ runner.os }}-${{ hashFiles('kotlin', 'kslog/module.yaml', 'project.yaml', 'gradle/libs.versions.toml', 'plugins/**/*.yaml', 'plugins/**/*.kt') }}
|
||||
restore-keys: |
|
||||
kotlin-toolchain-${{ runner.os }}-
|
||||
- name: Check release/version consistency
|
||||
run: |
|
||||
python3 verification/check_release_consistency.py
|
||||
bash verification/test_changelog_parser.sh
|
||||
python3 verification/check_wrapper_line_endings.py
|
||||
- name: Load Kotlin Toolchain model
|
||||
run: ./kotlin show modules
|
||||
- name: Validate Gradle Dokka output overrides
|
||||
run: |
|
||||
./gradlew --no-daemon help --task dokkaHtml -PDOKKA_PATH="$RUNNER_TEMP/kslog-dokka-property"
|
||||
DOKKA_PATH="$RUNNER_TEMP/kslog-dokka-environment" ./gradlew --no-daemon help --task dokkaHtml
|
||||
- name: Build Kotlin Toolchain targets
|
||||
run: ./kotlin build
|
||||
- name: Run Kotlin Toolchain checks
|
||||
run: ./kotlin check
|
||||
- name: Build the complete retained Gradle fallback
|
||||
run: ./gradlew --no-daemon clean build
|
||||
- name: Verify isolated Gradle publication and consumers
|
||||
run: bash verification/verify_gradle_fallback.sh
|
||||
@@ -1,21 +1,73 @@
|
||||
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
|
||||
with:
|
||||
packages: platform-tools
|
||||
- name: Install the declared Android SDK
|
||||
run: sdkmanager "platforms;android-37.0" "build-tools;37.0.0"
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.cache/JetBrains/Kotlin
|
||||
key: kotlin-toolchain-${{ runner.os }}-${{ hashFiles('kotlin', 'kslog/module.yaml', 'project.yaml', 'gradle/libs.versions.toml', 'plugins/dokka/**/*.yaml', 'plugins/dokka/**/*.kt') }}
|
||||
restore-keys: |
|
||||
kotlin-toolchain-${{ runner.os }}-
|
||||
- name: Materialize platform analysis classpaths
|
||||
run: ./kotlin build -m kslog -p js -p wasmJs
|
||||
- name: Generate KDocs
|
||||
run: ./kotlin do dokkaHtml
|
||||
- name: Upload generated KDocs
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: kdocs-html
|
||||
path: build/dokka/html
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
deploy:
|
||||
needs: generate
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download generated KDocs
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: kdocs-html
|
||||
path: kdocs-html
|
||||
- name: Publish KDocs
|
||||
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
|
||||
@@ -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 }}
|
||||
@@ -1,4 +1,6 @@
|
||||
.idea
|
||||
.kotlin
|
||||
kotlin-js-store/
|
||||
out/*
|
||||
*.iml
|
||||
target
|
||||
@@ -12,5 +14,6 @@ out/
|
||||
secret.gradle
|
||||
local.*
|
||||
local/
|
||||
**/*.local.*
|
||||
|
||||
publishing.sh
|
||||
@@ -1,5 +1,80 @@
|
||||
# Changelog
|
||||
|
||||
## 2.1.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `2.4.10` -> `2.4.20`
|
||||
* `com.gradleup.nmcp.aggregation`: `1.6.1` -> `1.6.2`
|
||||
* `io.github.ben-manes.versions`: `0.61.0` -> `0.64.0`
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* `Versions`:
|
||||
* `Kotlin`: `2.3.21` -> `2.4.10`
|
||||
|
||||
## 1.7.0
|
||||
|
||||
* `Kotlin`: `2.3.21`
|
||||
|
||||
## 1.6.1
|
||||
|
||||
* `Kotlin`: `2.3.20`
|
||||
|
||||
## 1.6.0
|
||||
|
||||
* `Kotlin`: `2.3.0`
|
||||
|
||||
## 1.5.2
|
||||
|
||||
* `Kotlin`: `2.2.21`
|
||||
|
||||
## 1.5.1
|
||||
|
||||
* `Kotlin`: `2.2.20`
|
||||
|
||||
## 1.5.0
|
||||
|
||||
* `Kotlin`: `2.2.0`
|
||||
|
||||
## 1.4.2
|
||||
|
||||
* `Kotlin`: `2.1.21`
|
||||
* Android SDK Level: `36`
|
||||
|
||||
## 1.4.1
|
||||
|
||||
* `Kotlin`: `2.1.10`
|
||||
|
||||
## 1.4.0
|
||||
|
||||
* `Kotlin`: `2.1.0`
|
||||
|
||||
## 1.3.6
|
||||
|
||||
* `Kotlin`: `2.0.20`
|
||||
|
||||
## 1.3.5
|
||||
|
||||
* `Kotlin`: `2.0.10`
|
||||
* Android SDK Level: `35`
|
||||
|
||||
## 1.3.4
|
||||
|
||||
* `Kotlin`: `1.9.24`
|
||||
* Native realization of `KSLoggerDefaultPlatformLoggerLambda` now is common for all native targets
|
||||
|
||||
## 1.3.3
|
||||
|
||||
* `Kotlin`: `1.9.23`
|
||||
|
||||
## 1.3.2
|
||||
|
||||
* `Kotlin`: `1.9.22`
|
||||
|
||||
## 1.3.1
|
||||
|
||||
* `Kotlin`: `1.9.21`
|
||||
|
||||
## 1.3.0
|
||||
|
||||
**THIS UPDATE CONTAINS CHANGES OF LOG LEVELS ACCORDANCES. LOOK AT THE LEVELS ACCORDANCE IN THE [DOCUMENTATION](https://docs.inmo.dev/kslog/logging.html#logging)
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# Developing KSLog
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- JDK 17
|
||||
- Android SDK platform 37 and build tools 37.0.0
|
||||
(`sdkmanager` package IDs: `platforms;android-37.0` and `build-tools;37.0.0`;
|
||||
the library's `compileSdk` remains 37)
|
||||
- Git
|
||||
- Bash and Python 3 for the committed compatibility verifiers
|
||||
|
||||
The checked-in `kotlin` and `kotlin.bat` wrappers download the pinned Kotlin
|
||||
Toolchain distribution into the normal user cache. No global Toolchain install
|
||||
is required.
|
||||
|
||||
## Build and test
|
||||
|
||||
```shell
|
||||
./kotlin show modules
|
||||
./kotlin clean
|
||||
./kotlin build
|
||||
./kotlin check
|
||||
```
|
||||
|
||||
Build one supported platform with `./kotlin build -p <platform>`, where the
|
||||
declared platforms are `android`, `js`, `jvm`, `linuxArm64`, `linuxX64`,
|
||||
`mingwX64`, and `wasmJs`. Run the real JVM tests directly with:
|
||||
|
||||
```shell
|
||||
./kotlin test -p jvm
|
||||
```
|
||||
|
||||
Kotlin Toolchain 0.13.0 does not expose `linuxArm32Hfp`. Until that target and
|
||||
the existing `kslog-wasm-js` artifact name can be represented, the Gradle build
|
||||
remains the full-contract fallback:
|
||||
|
||||
```shell
|
||||
./gradlew --no-daemon build
|
||||
./gradlew --no-daemon compileKotlinLinuxArm32Hfp
|
||||
```
|
||||
|
||||
Do not remove the Gradle path merely because the supported Toolchain targets
|
||||
compile. See `MIGRATION_REPORT.md` for the exact capability gaps and validation
|
||||
matrix.
|
||||
|
||||
Run the release/version preflight independently with:
|
||||
|
||||
```shell
|
||||
python3 verification/check_release_consistency.py
|
||||
bash verification/test_changelog_parser.sh
|
||||
```
|
||||
|
||||
CI and the explicit GitHub release command run the same consistency check.
|
||||
|
||||
## Documentation
|
||||
|
||||
```shell
|
||||
./kotlin build -m kslog -p js -p wasmJs
|
||||
./kotlin do dokkaHtml
|
||||
./kotlin do dokkaGfm
|
||||
```
|
||||
|
||||
The outputs are `build/dokka/html` and `build/dokka/gfm`. CI only deploys HTML
|
||||
documentation from `master`; pull requests and other branch pushes never deploy
|
||||
documentation. Toolchain 0.13.0 exposes only the JVM module classpath to local
|
||||
plugins, so the platform build first materializes the transformed Common plus
|
||||
matching JS and WasmJS standard-library KLIBs in the Toolchain cache. The Dokka
|
||||
plugin discovers those KLIBs from the resolved JVM stdlib version and fails with
|
||||
the prebuild command above if any one is missing.
|
||||
|
||||
The legacy Gradle-only `DOKKA_PATH` property/environment override is not
|
||||
implemented by the Toolchain plugin. Toolchain documentation is deliberately
|
||||
restricted to generated directories under `build/dokka` so cleanup cannot
|
||||
target an arbitrary external path. Use the retained Gradle path when that
|
||||
legacy override is required.
|
||||
|
||||
## Local publication and compatibility testing
|
||||
|
||||
Publication is enabled for the KSLog module, while the two local build-plugin
|
||||
modules are intentionally not publishable. Select the library module explicitly:
|
||||
|
||||
```shell
|
||||
isolated_repo="$(mktemp -d)"
|
||||
KOTLIN_CLI_JAVA_OPTIONS="-Dmaven.repo.local=$isolated_repo" \
|
||||
KOTLIN_TOOLCHAIN_SIGNING_KEY="$(gpg --armor --export-secret-keys YOUR_KEY_ID)" \
|
||||
./kotlin publish -m kslog mavenLocal
|
||||
```
|
||||
|
||||
Use a throwaway key for local validation. Never use the command above without
|
||||
the isolated Maven path for version 2.1.0, because it would overwrite the normal
|
||||
local copy of that released version.
|
||||
|
||||
The retained Gradle publication can be checked without credentials or signing
|
||||
keys against an archive of the exact migration base:
|
||||
|
||||
```shell
|
||||
bash verification/verify_gradle_fallback.sh
|
||||
```
|
||||
|
||||
The verifier creates isolated Gradle user homes and Maven repositories, checks
|
||||
the baseline/current 46-file publication inventories, source JARs, required POM
|
||||
metadata, source bytes, JVM/Android public method and field signatures, module
|
||||
identities, and compiles the committed consumer probe. It never writes to the
|
||||
normal Maven local repository.
|
||||
|
||||
## Remote publication and release
|
||||
|
||||
The validation workflow used for pushes and pull requests never publishes
|
||||
packages or creates tags/releases. The separate `master`-only KDocs workflow
|
||||
intentionally deploys generated HTML to the `kdocs` branch after a read-only
|
||||
generation job succeeds. Toolchain remote package publication is disabled in
|
||||
the resolved model: the module exposes only `mavenLocal`, has no Inmo Nexus
|
||||
repository/credentials, and does not enable Maven Central. `mavenLocal` is not
|
||||
inherently isolated; without the `-Dmaven.repo.local=...` JVM override shown
|
||||
above it writes to the user's normal Maven Local repository. There is no
|
||||
configured Toolchain destination that uploads packages remotely.
|
||||
|
||||
Do not re-enable Toolchain Nexus or Central publication until both the missing
|
||||
Linux ARM32 target and the `kslog-wasm-js` coordinate have compatible
|
||||
implementations and the complete publication diff has been reviewed again.
|
||||
|
||||
The retained Gradle build is the only full-target remote publication path. Its
|
||||
existing release tasks remain explicit maintainer operations and are not run by
|
||||
CI:
|
||||
|
||||
```shell
|
||||
INMONEXUS_USER=... INMONEXUS_PASSWORD=... \
|
||||
./gradlew --no-daemon publishAllPublicationsToInmoNexusRepository
|
||||
|
||||
SONATYPE_USER=... SONATYPE_PASSWORD=... \
|
||||
./gradlew --no-daemon publishAllPublicationsToSonatypeRepository
|
||||
```
|
||||
|
||||
Those commands require the existing Gradle/GPG release configuration and cause
|
||||
real uploads. They are documented only to preserve the fallback release
|
||||
contract; do not run them as migration validation.
|
||||
|
||||
Preview the GitHub release payload without network mutation:
|
||||
|
||||
```shell
|
||||
KSLOG_GITHUB_RELEASE_DRY_RUN=true ./kotlin do githubRelease
|
||||
```
|
||||
|
||||
A real release requires `GITHUB_RELEASE_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN`
|
||||
and an explicit `./kotlin do githubRelease` invocation. The command first runs
|
||||
the release/version consistency preflight, then delegates to the standard
|
||||
`gh release create` CLI. It may create the remote tag when that tag is absent;
|
||||
it does not alter local git state. Validation CI never invokes it.
|
||||
@@ -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=<temporary path>`; never overwrite normal `~/.m2` artifacts.
|
||||
|
||||
Known 0.13.0 publication mismatch: built-in Toolchain publication derives the
|
||||
Wasm artifact suffix from `wasmJs` as `-wasmjs`, while Kotlin Gradle publishes
|
||||
`-wasm-js`. Together with the missing Linux ARM32 target, this prevents a
|
||||
publication-compatible cutover. The legacy Gradle files therefore remain until
|
||||
these two blockers are resolved; the Toolchain implementation is a reversible,
|
||||
working supported-target migration rather than a claim of full parity.
|
||||
|
||||
The stable `kslog` module directory plus narrow compiler-identity overrides
|
||||
preserve the existing JVM Kotlin module and KLIB unique/short names. Remaining
|
||||
built-in POM, Javadoc-placeholder, AAR-layout, and JS manifest-shape differences
|
||||
are itemized in `MIGRATION_REPORT.md`.
|
||||
|
||||
### CI and documentation contract
|
||||
|
||||
- Replace the stale push workflow that names nonexistent Gitea and
|
||||
GitHub-Packages Gradle tasks with build/check validation only.
|
||||
- Run the complete retained Gradle build plus an isolated baseline/current
|
||||
local-publication, public-signature, and consumer verifier in validation CI.
|
||||
- Push and pull-request validation must not publish artifacts, docs, tags, or
|
||||
releases.
|
||||
- Keep documentation deployment master-only and change generation to the
|
||||
Toolchain `dokkaHtml` command with output `build/dokka/html`.
|
||||
- Generate docs in a read-only job without persisted checkout credentials, then
|
||||
deploy the generated artifact in a separate write-enabled job.
|
||||
- Keep `dokkaGfm` with output `build/dokka/gfm` for local/consumer workflows.
|
||||
- Remote publication and GitHub release remain explicit manual commands and are
|
||||
documented, not automatically broadened.
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
- [x] Add the checked-in 0.13.0 wrappers.
|
||||
- [x] Add `project.yaml`, stable `kslog/module.yaml`, and extend the existing version catalog.
|
||||
- [x] Move source roots byte-for-byte to Toolchain KMP layout.
|
||||
- [x] Map legacy Gradle source sets to the moved roots while blockers remain.
|
||||
- [x] Add focused JVM behavior tests so validation is not `NO-SOURCE`.
|
||||
- [x] Vendor and adapt the official Dokka plugin with source/commit/license.
|
||||
- [x] Add the narrow GitHub release plugin with a non-network dry-run.
|
||||
- [x] Load the project model and enumerate modules, platforms, commands, and checks.
|
||||
- [x] Clean-build every declared Toolchain-supported target.
|
||||
- [x] Run real tests/checks and distinguish host-unrunnable targets.
|
||||
- [x] Generate HTML and GFM docs at the preserved paths.
|
||||
- [x] Publish Toolchain artifacts with an ephemeral signing key to an isolated
|
||||
Maven repository before disabling all Toolchain remote destinations.
|
||||
- [x] Inspect artifacts, signatures, POMs, and Gradle module metadata.
|
||||
- [x] Add a committed verifier that republishes the untouched base and the
|
||||
post-move Gradle fallback with isolated Gradle homes and no signing secrets.
|
||||
- [x] Run Gradle KMP and Android consumer smoke tests against the isolated repository.
|
||||
- [x] Compare production source hashes and JVM/Android public bytecode/API with Gradle.
|
||||
- [x] Validate CI YAML, shell scripts, and `git diff --check`.
|
||||
- [x] Rebuild `ast-index` after source moves and edits.
|
||||
- [x] Record exact commands, results, logs, blockers, and remote-only checks in
|
||||
`MIGRATION_REPORT.md`.
|
||||
@@ -0,0 +1,278 @@
|
||||
# KSLog Kotlin Toolchain migration report
|
||||
|
||||
Date: 2026-11-02
|
||||
Base commit: `0b9fb30862c7319bb228039ee33eef9a0ddef95c`
|
||||
Kotlin Toolchain: `v0.13.0` source commit
|
||||
`abc7a5f7579e9ef267107782048c76777f0bf932`
|
||||
|
||||
## Result
|
||||
|
||||
KSLog now has a checked-in, release-pinned Kotlin Toolchain project that loads,
|
||||
builds every platform supported by Toolchain 0.13.0, runs real JVM tests,
|
||||
generates both documentation formats, can publish signed KMP artifacts through
|
||||
the configured `mavenLocal` destination, and is consumable by Gradle KMP and
|
||||
Android projects. Migration validation redirects Maven Local to a temporary
|
||||
repository; the destination itself is not inherently isolated. Toolchain Nexus
|
||||
credentials/repository configuration is absent and resolved Maven Central
|
||||
publishing is disabled.
|
||||
|
||||
This is **not yet a publication-compatible full cutover**. Toolchain 0.13.0
|
||||
cannot represent `linuxArm32Hfp` and hard-codes the WasmJS artifact suffix as
|
||||
`-wasmjs` instead of KSLog's released `-wasm-js`. The Gradle build is therefore
|
||||
retained, mapped to the moved sources, and validated as the complete target and
|
||||
publication fallback. No remote artifact, documentation, tag, or release was
|
||||
published during this migration.
|
||||
|
||||
## Implemented layout
|
||||
|
||||
- `kotlin` and `kotlin.bat` preserve the official 0.13.0 wrapper content. Git
|
||||
attributes enforce LF for `kotlin`/Kotlin sources and CRLF for `kotlin.bat`.
|
||||
The wrappers select CLI `0.13.0-dev-4444` with the official pinned
|
||||
distribution SHA-256.
|
||||
- `project.yaml` registers the stable `kslog` library module and two local build
|
||||
plugin modules. The stable directory name prevents compiler module identity
|
||||
from depending on the checkout/worktree directory name.
|
||||
- `kslog/module.yaml` declares Android, JS, JVM, WasmJS, Linux x64/ARM64, and
|
||||
MinGW x64, plus Android/JDK/Kotlin settings and full publication metadata.
|
||||
- The 20 production files moved byte-for-byte into `kslog/src*` Toolchain
|
||||
source roots. `build.gradle` maps the retained Gradle source sets to those
|
||||
roots, including Linux ARM32.
|
||||
- Compiler module identities are pinned to the existing published identities:
|
||||
JVM/Android `META-INF/dev.inmo_kslog.kotlin_module`, leaf KLIB
|
||||
`unique_name=dev.inmo:kslog`, common metadata `kslog_commonMain`, and native
|
||||
metadata `dev.inmo:kslog_nativeMain`/`kslog_nativeMain`.
|
||||
- `kslog/test@jvm/CallbackKSLogTest.kt` adds two focused tests covering exact
|
||||
callback forwarding and rejected lazy-message evaluation.
|
||||
- `plugins/dokka` adapts the official Apache-2.0 Toolchain Dokka plugin for
|
||||
KSLog's KMP graph and Dokka 2.2.0 HTML/GFM output. Each adapted Apache source
|
||||
carries a prominent KSLog modification/provenance notice, and cleanup rejects
|
||||
symbolic-link ancestors and requires real-path containment. Platform analysis
|
||||
receives JVM/common, JS, and WasmJS standard-library classpaths. Because the
|
||||
plugin API only exposes the JVM module classpath, the documented JS/WasmJS
|
||||
prebuild materializes the transformed Common and matching platform KLIBs for
|
||||
deterministic discovery. Generation
|
||||
fails before deployment if unresolved-type placeholders remain.
|
||||
- `plugins/github-release` provides an explicit `gh release create` command and
|
||||
a non-network dry-run mode. It runs the committed version consistency
|
||||
preflight first; no workflow invokes it.
|
||||
- `DEVELOPMENT.md` documents Toolchain use, the Gradle fallback, isolated local
|
||||
publication, disabled Toolchain remotes, the retained Gradle release path,
|
||||
and release gating.
|
||||
- `verification/` contains portable release/parser/wrapper checks plus the
|
||||
baseline/current Gradle publication, source-identity, public-signature, and
|
||||
consumer verifier used by CI.
|
||||
|
||||
## Target and test matrix
|
||||
|
||||
| Target | Toolchain result | Runtime/test result | Contract notes |
|
||||
|---|---|---|---|
|
||||
| JVM | PASS | 2 tests found, 2 started, 2 passed | Bytecode major 52; 25-class list and public JVM method/field signatures from `javap -public -s -constants` match Gradle |
|
||||
| Android | PASS | No device tests in the project; Android consumer compilation PASS | Bytecode major 61; 24-class list and public JVM method/field signatures match Gradle; minSdk 21, compileSdk/build tools 37 |
|
||||
| JS | PASS | No test sources; Toolchain 0.13.0 exposes no browser/Node runner selection for a library | Gradle fallback keeps browser + Node declarations |
|
||||
| WasmJS | PASS | No test sources; Toolchain 0.13.0 exposes no Node/d8 selection and only has browser test support | Gradle fallback keeps browser + Node + d8 declarations |
|
||||
| linuxX64 | PASS | No test sources | Host-compilable KLIB |
|
||||
| linuxArm64 | PASS | Cross-compiled; not runnable on this x64 host | KLIB publication produced |
|
||||
| mingwX64 | PASS | Cross-compiled; not runnable on this Linux host | KLIB publication produced |
|
||||
| linuxArm32Hfp | **UNSUPPORTED** | Gradle clean build compiles it; no tests | Absent from Toolchain 0.13.0 `Platform`; minimal model probe fails |
|
||||
|
||||
`./kotlin check` did not mistake empty platform suites for passing tests: the
|
||||
report only claims the two JVM tests. Gradle reports JS, WasmJS, Android, and
|
||||
native tests as `NO-SOURCE` or skipped, while still compiling all declared
|
||||
targets, including Linux ARM32.
|
||||
|
||||
## Verification evidence
|
||||
|
||||
The commands below are reproducible from the repository root. Temporary build
|
||||
logs and repositories are intentionally not part of the published report.
|
||||
|
||||
### Model, build, checks, and compatibility
|
||||
|
||||
| Command | Result |
|
||||
|---|---|
|
||||
| `./kotlin show modules` | PASS; modules `dokka`, `github-release`, `kslog` |
|
||||
| `./kotlin show settings -m kslog` plus `./kotlin show tasks` | PASS; `mavenCentral.enabled: false`, only local publish repository configured, no Nexus/Central task references |
|
||||
| `./kotlin show commands` / `show checks` | PASS; Dokka/release commands and builtin tests enumerated |
|
||||
| `./kotlin clean && ./kotlin build` | PASS; all seven Toolchain platforms plus both plugin modules |
|
||||
| `./kotlin check` | PASS; KSLog JVM tests 2/2 plus Dokka guard/classpath tests 4/4 |
|
||||
| `./kotlin test -m dokka -p jvm` | PASS 4/4; transformed/platform classpath mapping, unresolved-output rejection, symlink containment, and scoped cleanup |
|
||||
| `./gradlew --no-daemon clean build` | PASS; complete legacy build, including Linux ARM32 |
|
||||
| `bash verification/verify_gradle_fallback.sh` | PASS; isolated baseline/current Gradle publication and committed consumer probe |
|
||||
| `python3 verification/check_release_consistency.py` and parser/wrapper checks | PASS; four version copies, literal changelog heading, LF/CRLF attributes |
|
||||
| Linux ARM32 minimal model probe | Expected failure: `Product platforms list should not be empty` at `linuxArm32Hfp` |
|
||||
| GitHub YAML, actionlint 1.7.7, shell syntax, `git diff --check` | PASS |
|
||||
| `ast-index rebuild` | PASS after final edits |
|
||||
|
||||
The Toolchain emits the existing insignificant inline warning. It also warns
|
||||
that `-module-name`/`-Xshort-module-name` are passed more than once because the
|
||||
built-in checkout-derived names are intentionally overridden; inspection of
|
||||
the output JAR/KLIB manifests proves the last values match the released Gradle
|
||||
module identities.
|
||||
|
||||
### Source and ABI preservation
|
||||
|
||||
- All 20 production source files match the base commit byte-for-byte after
|
||||
their moves.
|
||||
- The JVM 25-class and Android 24-class lists match; public JVM method/field
|
||||
signature dumps from `javap -public -s -constants` have zero differences.
|
||||
- Gradle/Toolchain bytecode majors are JVM 52/52 and Android 61/61.
|
||||
- Published JVM, common/native metadata, and supported leaf KLIB module
|
||||
identities match Gradle.
|
||||
|
||||
These checks are deliberately narrow. Source identity, consumer compilation,
|
||||
JVM class/signature comparison, and inspected module names are not a complete
|
||||
Kotlin metadata or KLIB ABI proof. Full cutover remains blocked and requires a
|
||||
dedicated metadata/KLIB compatibility review in addition to resolving the two
|
||||
upstream capability gaps.
|
||||
|
||||
### Documentation and release command
|
||||
|
||||
| Command | Result |
|
||||
|---|---|
|
||||
| `./kotlin build -m kslog -p js -p wasmJs` | PASS; materializes the Common/JS/WasmJS analysis KLIBs required by the pinned plugin API |
|
||||
| `./kotlin do dokkaHtml` | PASS; 156 files at `build/dokka/html`, zero unresolved-type placeholders |
|
||||
| `./kotlin do dokkaGfm` | PASS; 101 files at `build/dokka/gfm`, zero unresolved-type placeholders |
|
||||
| Output/link/platform inspection | PASS; canonical source links and Common/JVM/Android/JS/WasmJS/Native tabs |
|
||||
| `KSLOG_GITHUB_RELEASE_DRY_RUN=true ./kotlin do githubRelease` | PASS; consistency preflight and `v2.1.0` payload, no network mutation |
|
||||
|
||||
The legacy `DOKKA_PATH` override is intentionally not ported. It remains
|
||||
available only through Gradle because Toolchain cleanup is restricted to direct
|
||||
children of `build/dokka`.
|
||||
|
||||
### Isolated publication and consumers
|
||||
|
||||
Toolchain publication used a temporary Maven repository selected with the JVM
|
||||
`maven.repo.local` override, `-m kslog`, and a generated throwaway signing key.
|
||||
The configured destination is named `mavenLocal` and would use the normal Maven
|
||||
Local repository without that override. The temporary private key was deleted
|
||||
after verification, and the normal Maven Local repository was not written.
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `./kotlin publish -m kslog mavenLocal` | PASS in an isolated repository |
|
||||
| Toolchain artifact/signature inventory | 40 primary files and 40 detached signatures; 40/40 signatures verified with the throwaway public key |
|
||||
| Toolchain required POM metadata comparison | PASS for root and every supported platform artifact |
|
||||
| Gradle KMP consumer compile | PASS for JVM, JS, WasmJS, linuxX64, linuxArm64, mingwX64 |
|
||||
| Gradle Android KMP consumer compile | PASS |
|
||||
| `bash verification/verify_gradle_fallback.sh` | PASS; untouched-base and post-move Gradle outputs each contain the same 46 primary files, nine source JARs, and nine required-metadata POMs; no signing task/key/config is used |
|
||||
|
||||
Produced IDs are `kslog`, `kslog-android`, `kslog-js`, `kslog-jvm`,
|
||||
`kslog-linuxarm64`, `kslog-linuxx64`, `kslog-mingwx64`, and `kslog-wasmjs`.
|
||||
Root Gradle module metadata, Kotlin Tooling metadata, and sources are present.
|
||||
Platform Toolchain publications contain placeholder Javadoc JARs, while the
|
||||
root Toolchain Javadoc JAR is absent. The committed Gradle verifier uses a fresh
|
||||
Gradle user home with only optional `caches`/`wrapper` links, an empty GnuPG
|
||||
home, sanitized credential/signing environment variables, and separate Maven
|
||||
repositories; it neither reads user Gradle signing configuration nor writes to
|
||||
the normal Maven local repository.
|
||||
|
||||
## Exact parity blockers and differences
|
||||
|
||||
1. **Linux ARM32:** `linuxArm32Hfp` is absent from the pinned 0.13.0
|
||||
`Platform` enum and schema. Adding it to `product.platforms` is rejected at
|
||||
model load, before compilation or publication can be customized.
|
||||
2. **Wasm artifact coordinate:** built-in KMP publication maps `wasmJs` to
|
||||
`kslog-wasmjs`; there is no per-platform artifact-suffix override. KSLog's
|
||||
released coordinate is `kslog-wasm-js`.
|
||||
3. **Runtime declarations:** JS browser/Node and WasmJS browser/Node/d8 runner
|
||||
selection are not exposed for this Toolchain library model. Compilation and
|
||||
publication work; those execution contracts stay in Gradle.
|
||||
4. **POM shape:** required name/description/URL/developers/license/SCM URL and
|
||||
developer connection are preserved. Toolchain additionally generates the
|
||||
SCM `connection`. Its root KMP POM uses `<packaging>pom</packaging>` and a
|
||||
`kslog-jvm` compile dependency, while Gradle's root POM carries a Kotlin
|
||||
stdlib runtime dependency. The Toolchain Android POM additionally declares
|
||||
`kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8`; its JS POM omits Gradle's
|
||||
`kotlin-dom-api-compat`; and its Linux ARM64, Linux x64, and MinGW POMs move
|
||||
the Kotlin stdlib dependency from compile to runtime scope. Gradle module
|
||||
metadata consumer selection and the committed consumers passed, but direct
|
||||
POM consumers can observe these dependency/scope changes, so this is not
|
||||
publication-shape parity and requires review before full cutover.
|
||||
5. **Root Javadoc artifact:** the 46-file Gradle publication has
|
||||
`kslog-2.1.0-javadoc.jar`; the 40-file Toolchain publication omits it. This is
|
||||
the sixth missing primary file in addition to the five Linux ARM32 files.
|
||||
Toolchain platform Javadoc JARs contain a manifest plus a 78-byte `README.md`
|
||||
placeholder, while Gradle's Javadoc JARs contain only the manifest.
|
||||
6. **Android manifest and AAR layout:** Gradle's manifest declares only
|
||||
`minSdkVersion=21`; Toolchain additionally emits `targetSdkVersion=37`.
|
||||
Gradle uses `classes.jar`; Toolchain uses
|
||||
`libs/kslog-jvm.jar`. Both are valid AAR layouts, the bytecode level and
|
||||
compared public JVM method/field signatures match, and an Android KMP
|
||||
consumer compiles successfully.
|
||||
7. **JS manifest detail:** public KLIB identity matches
|
||||
`unique_name=dev.inmo:kslog`, but Toolchain's JS/Wasm KLIB does not include
|
||||
Gradle's `jsOutputName=kslog` manifest entry.
|
||||
8. **Android resource exclusions:** `resourcePackaging` is not accepted for a
|
||||
Toolchain `kmp/lib`. The produced AAR contains only the manifest and library
|
||||
JAR, so none of the three legacy serialization metadata exclusions are
|
||||
currently present. Gradle keeps the exclusions for the fallback.
|
||||
9. **Per-platform JVM release:** Toolchain 0.13.0 does not allow `jvm.release`
|
||||
to differ by platform. The module uses target-specific `-jvm-target=1.8`
|
||||
and `-jvm-target=17`; inspected bytecode proves majors 52 and 61.
|
||||
10. **Documentation output override:** Gradle honors `DOKKA_PATH`; Toolchain
|
||||
intentionally does not because arbitrary external cleanup would weaken the
|
||||
generated-output containment guarantee.
|
||||
|
||||
Because items 1 and 2 change released target/coordinate contracts, remote
|
||||
Toolchain publication must remain blocked until Toolchain gains the required
|
||||
capabilities or an explicitly reviewed compatibility implementation is added.
|
||||
|
||||
## CI behavior
|
||||
|
||||
- `.github/workflows/build.yml` replaces the stale workflow that attempted
|
||||
nonexistent Gitea/GitHub-Packages Gradle tasks on every push. Pushes and pull
|
||||
requests now run release/parser/wrapper preflights, Toolchain build/check, the
|
||||
complete retained Gradle build, and the isolated publication/API/consumer
|
||||
verifier. Checkout fetches full history so the verifier's pinned migration
|
||||
base commit is available. It has read-only repository permissions and no
|
||||
secrets or remote publication commands.
|
||||
- `.github/workflows/kdocs.yml` remains `master`-only. Its read-only generation
|
||||
job checks out with `persist-credentials: false`, materializes the platform
|
||||
analysis KLIBs, runs Toolchain/Dokka, and
|
||||
uploads the generated artifact. A separate write-enabled job downloads only
|
||||
that artifact and deploys it to `kdocs`; project/plugin code never runs with a
|
||||
write credential.
|
||||
- All workflow actions are pinned to resolved commit SHAs.
|
||||
- Hosted CI status is reported on [draft PR #136](https://github.com/InsanusMokrassar/KSLog/pull/136); the local evidence above does not imply a hosted pass. SDK setup explicitly requests `platforms;android-37.0` (API 37) and excludes the removed legacy `tools` package.
|
||||
|
||||
## Remote-only checks not performed
|
||||
|
||||
- Inmo Nexus upload.
|
||||
- Maven Central bundle upload/validation/release.
|
||||
- GitHub release creation.
|
||||
- Documentation deployment to the `kdocs` branch.
|
||||
- Android device tests and browser/Node/d8 tests (there are currently no such
|
||||
test sources); Linux ARM64 and MinGW runtime execution on matching hosts.
|
||||
|
||||
## Recommended pull request
|
||||
|
||||
Suggested title:
|
||||
|
||||
> build: add Kotlin Toolchain migration with compatibility fallback
|
||||
|
||||
Suggested body:
|
||||
|
||||
> ## Summary
|
||||
> - add the pinned Kotlin Toolchain 0.13.0 wrappers and a stable `kslog` KMP module
|
||||
> - migrate source roots byte-for-byte and retain a validated Gradle fallback
|
||||
> - add real JVM tests, KMP-capable Dokka HTML/GFM tasks, and an explicit GitHub release command
|
||||
> - replace stale package-publishing CI with non-publishing Toolchain and full Gradle validation
|
||||
> - document isolated publication, release gating, and full verification evidence
|
||||
>
|
||||
> ## Validation
|
||||
> - all Toolchain-supported targets build
|
||||
> - JVM tests: 2/2 pass
|
||||
> - retained Gradle clean build passes, including linuxArm32Hfp
|
||||
> - JVM/Android class lists and public JVM method/field signature diffs are empty; bytecode remains 8/17
|
||||
> - signed isolated KMP publication verified and consumed by Gradle KMP + Android projects
|
||||
> - Dokka HTML/GFM and GitHub release dry-run pass
|
||||
>
|
||||
> ## Known blockers
|
||||
> Toolchain 0.13.0 has no linuxArm32Hfp platform and publishes `-wasmjs` instead
|
||||
> of the released `-wasm-js` coordinate. Therefore Gradle remains the publication
|
||||
> fallback and remote Toolchain publication is not enabled. See
|
||||
> `MIGRATION_REPORT.md` for the complete matrix and artifact differences.
|
||||
|
||||
This migration is ready only for a **draft PR**. Full Gradle removal is not
|
||||
review-ready and remains blocked by Toolchain 0.13.0's missing Linux ARM32
|
||||
platform and incompatible Wasm artifact ID; the narrow checks above do not
|
||||
waive those requirements.
|
||||
@@ -91,3 +91,26 @@ implementation "dev.inmo:kslog:$kslog_version"
|
||||
<version>${kslog_version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## Building from source
|
||||
|
||||
The checked-in `kotlin`/`kotlin.bat` wrappers pin JetBrains Kotlin Toolchain
|
||||
0.13.0. With JDK 17 and Android SDK 37 installed, the main validation commands
|
||||
are:
|
||||
|
||||
```shell
|
||||
./kotlin build
|
||||
./kotlin check
|
||||
./kotlin do dokkaHtml
|
||||
```
|
||||
|
||||
Kotlin Toolchain 0.13.0 cannot model the existing `linuxArm32Hfp` target and
|
||||
derives `kslog-wasmjs` instead of the released `kslog-wasm-js` artifact ID.
|
||||
The Gradle build is retained as a compatibility path until those publication
|
||||
contracts can be represented without dropping a target or changing consumer
|
||||
coordinates.
|
||||
|
||||
See [DEVELOPMENT.md](DEVELOPMENT.md) for target-specific commands, documentation
|
||||
generation, and explicitly gated publishing/release procedures. The migration
|
||||
inventory and verification evidence are in [MIGRATION_PLAN.md](MIGRATION_PLAN.md)
|
||||
and [MIGRATION_REPORT.md](MIGRATION_REPORT.md).
|
||||
+47
-14
@@ -20,9 +20,29 @@ plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.dokka)
|
||||
alias(libs.plugins.versions)
|
||||
alias(libs.plugins.nmcp.aggregation)
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "com.android.kotlin.multiplatform.library"
|
||||
apply from: "defaultAndroidSettings.gradle"
|
||||
|
||||
apply from: "github_release.gradle"
|
||||
apply from: "publish.gradle"
|
||||
apply from: "dokka.gradle"
|
||||
|
||||
|
||||
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
||||
nmcpAggregation {
|
||||
centralPortal {
|
||||
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||
validationTimeout = Duration.ofHours(4)
|
||||
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
|
||||
}
|
||||
|
||||
publishAllProjectsProbablyBreakingProjectIsolation()
|
||||
}
|
||||
}
|
||||
|
||||
project.version = "$version"
|
||||
project.group = "$group"
|
||||
@@ -45,13 +65,10 @@ kotlin {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
androidTarget {
|
||||
publishAllLibraryVariants()
|
||||
compilations.all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
android {
|
||||
project.ext.configureAndroidTarget(delegate)
|
||||
withHostTest {}
|
||||
withDeviceTest {}
|
||||
}
|
||||
linuxArm64()
|
||||
linuxArm32Hfp()
|
||||
@@ -65,17 +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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +122,3 @@ java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
apply from: "defaultAndroidSettings.gradle"
|
||||
|
||||
apply from: "github_release.gradle"
|
||||
apply from: "publish.gradle"
|
||||
apply from: "dokka.gradle"
|
||||
+19
-8
@@ -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"
|
||||
|
||||
@@ -1,63 +1,18 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
android {
|
||||
ext {
|
||||
jvmKotlinFolderFile = {
|
||||
String sep = File.separator
|
||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
if (jvmKotlinFolder.exists()) {
|
||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||
android.sourceSets.main.java.srcDirs = []
|
||||
for (oldDir in oldDirs) {
|
||||
if (oldDir != jvmKotlinFolder.path) {
|
||||
android.sourceSets.main.java.srcDirs += oldDir
|
||||
}
|
||||
}
|
||||
ext.configureAndroidTarget = { target ->
|
||||
target.compileSdk = libs.versions.android.compileSdk.get().toInteger()
|
||||
target.buildToolsVersion = libs.versions.android.buildTools.get()
|
||||
target.minSdk = libs.versions.android.minSdk.get().toInteger()
|
||||
target.namespace = "${group}.${project.name}"
|
||||
target.packaging {
|
||||
resources {
|
||||
excludes.add 'META-INF/kotlinx-serialization-runtime.kotlin_module'
|
||||
excludes.add 'META-INF/kotlinx-serialization-cbor.kotlin_module'
|
||||
excludes.add 'META-INF/kotlinx-serialization-properties.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
compileSdk libs.versions.android.compileSdk.get().toInteger()
|
||||
buildToolsVersion = libs.versions.android.buildTools.get()
|
||||
namespace "${group}.${project.name}"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdkVersion libs.versions.android.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
debug {
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
|
||||
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sep = File.separator
|
||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||
}
|
||||
target.compilerOptions.jvmTarget.set(
|
||||
target.compilerOptions.jvmTarget.get().class.fromTarget("17")
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+25
-11
@@ -1,9 +1,15 @@
|
||||
interface InjectedExecOps {
|
||||
@Inject //@javax.inject.Inject
|
||||
ExecOperations getExecOps()
|
||||
}
|
||||
private String getCurrentVersionChangelog() {
|
||||
OutputStream changelogDataOS = new ByteArrayOutputStream()
|
||||
exec {
|
||||
|
||||
def injected = project.objects.newInstance(InjectedExecOps)
|
||||
injected.execOps.exec {
|
||||
commandLine 'chmod', "+x", './changelog_parser.sh'
|
||||
}
|
||||
exec {
|
||||
injected.execOps.exec {
|
||||
standardOutput = changelogDataOS
|
||||
commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md'
|
||||
}
|
||||
@@ -11,20 +17,28 @@ private String getCurrentVersionChangelog() {
|
||||
return changelogDataOS.toString().trim()
|
||||
}
|
||||
|
||||
if (new File(projectDir, "secret.gradle").exists()) {
|
||||
apply from: './secret.gradle'
|
||||
|
||||
def githubTokenVariableName = "GITHUB_RELEASE_TOKEN"
|
||||
def githubTokenVariableFromEnv = System.getenv(githubTokenVariableName)
|
||||
|
||||
def secretFile = new File(projectDir, "secret.gradle")
|
||||
if (secretFile.exists() || project.hasProperty(githubTokenVariableName) || (githubTokenVariableFromEnv != "" && githubTokenVariableFromEnv != null)) {
|
||||
if (secretFile.exists()) {
|
||||
apply from: './secret.gradle'
|
||||
}
|
||||
apply plugin: "com.github.breadmoirai.github-release"
|
||||
def githubReleaseToken = project.hasProperty(githubTokenVariableName) ? project.property(githubTokenVariableName).toString() : githubTokenVariableFromEnv
|
||||
|
||||
githubRelease {
|
||||
token "${project.property('GITHUB_RELEASE_TOKEN')}"
|
||||
token githubReleaseToken
|
||||
|
||||
owner "InsanusMokrassar"
|
||||
repo "KSLog"
|
||||
owner = "InsanusMokrassar"
|
||||
repo = "KSLog"
|
||||
|
||||
tagName "v${project.version}"
|
||||
releaseName "${project.version}"
|
||||
targetCommitish "${project.version}"
|
||||
tagName = "v${project.version}"
|
||||
releaseName = "${project.version}"
|
||||
targetCommitish = "${project.version}"
|
||||
|
||||
body getCurrentVersionChangelog()
|
||||
body = getCurrentVersionChangelog()
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -1,14 +1,12 @@
|
||||
kotlin.code.style=official
|
||||
org.gradle.parallel=true
|
||||
kotlin.js.generate.externals=true
|
||||
kotlin.incremental=true
|
||||
kotlin.incremental.js=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx512m
|
||||
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=1.3.0
|
||||
android_code_version=27
|
||||
version=2.1.0
|
||||
android_code_version=44
|
||||
+23
-12
@@ -1,22 +1,23 @@
|
||||
[versions]
|
||||
|
||||
kotlin = "1.9.20"
|
||||
kotlin-gradle-plugin = "8.1.4"
|
||||
kotlin = "2.4.20"
|
||||
android-gradle-plugin = "9.1.1"
|
||||
|
||||
versions = "0.50.0"
|
||||
nmcp = "1.6.2"
|
||||
versions = "0.64.0"
|
||||
|
||||
dokka = "1.9.10"
|
||||
dokka = "2.2.0"
|
||||
|
||||
dexcount = "4.0.0"
|
||||
junit_version = "4.12"
|
||||
test_ext_junit_version = "1.1.5"
|
||||
espresso_core = "3.5.1"
|
||||
junit_version = "4.13.2"
|
||||
test_ext_junit_version = "1.3.0"
|
||||
espresso_core = "3.7.0"
|
||||
|
||||
android-minSdk = "21"
|
||||
android-compileSdk = "34"
|
||||
android-buildTools = "34.0.0"
|
||||
android-compileSdk = "37"
|
||||
android-buildTools = "37.0.0"
|
||||
|
||||
github-release = "2.4.1"
|
||||
github-release = "2.5.2"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -30,15 +31,25 @@ android-test-espresso-core = { module = "androidx.test.espresso:espresso-core",
|
||||
|
||||
# buildscript classpaths
|
||||
|
||||
android-tools-build = { module = "com.android.tools.build:gradle", version.ref = "kotlin-gradle-plugin" }
|
||||
android-tools-build = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
|
||||
android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
|
||||
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
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" }
|
||||
kotlin-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
|
||||
|
||||
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
|
||||
versions = { id = "io.github.ben-manes.versions", version.ref = "versions" }
|
||||
|
||||
nmcp-aggregation = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -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 <<EOF
|
||||
Welcome to
|
||||
|
||||
@@@ @@@@ @@@ @@@
|
||||
@@@ @@@@ @@@ @@@ @@@
|
||||
@@@ #@@@" ,@@@ @@@
|
||||
@@@ ,@@@% ,@@@@@@@, @@@@@@@@@@ @@@ @@@ @@@ ,@@@@@,
|
||||
@@@ @@@@ @@@@@%"%@@@@@ @@@@@@@@@@ @@@ @@@ @@@@@@%%@@@@@
|
||||
@@@@@@% @@@% %@@@ @@@ @@@ @@@ @@@@ %@@%
|
||||
@@@ @@@@= #@@@ @@@# @@@ @@@ @@@ @@@ @@@
|
||||
@@@ @@@@ #@@@ @@@# @@@ @@@ @@@ @@@ @@@
|
||||
@@@ *@@@% @@@@ @@@@ @@@ @@@ @@@ @@@ @@@
|
||||
@@@ %@@@= %@@@@*,*@@@@% @@@@### @@@ @@@ @@@ @@@
|
||||
@@@ @@@@ "@@@@@@@" %@@@@@ @@@ @@@ @@@ @@@
|
||||
EOF
|
||||
echo
|
||||
echo "This is the first run of the Kotlin CLI v$kotlin_cli_version, so we need to download the Kotlin Toolchain."
|
||||
echo "Please give us a few seconds, subsequent runs will be faster."
|
||||
echo
|
||||
fi
|
||||
|
||||
echo "Downloading $moniker..."
|
||||
|
||||
temp_file="$cache_dir/download-file-$$.bin"
|
||||
rm -f "$temp_file"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -t 1 ]; then CURL_PROGRESS="--progress-bar"; else CURL_PROGRESS="--silent --show-error"; fi
|
||||
# shellcheck disable=SC2086
|
||||
curl $CURL_PROGRESS -L --fail --retry 5 --connect-timeout 30 --output "${temp_file}" "$file_url"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
if [ -t 1 ]; then WGET_PROGRESS=""; else WGET_PROGRESS="-nv"; fi
|
||||
wget $WGET_PROGRESS --tries=5 --connect-timeout=30 --read-timeout=120 -O "${temp_file}" "$file_url"
|
||||
else
|
||||
die "ERROR: Please install 'wget' or 'curl', as one of them is required to download $moniker"
|
||||
fi
|
||||
|
||||
check_sha "$file_url" "$temp_file" "$file_sha" "$sha_size"
|
||||
|
||||
rm -rf "$extract_dir"
|
||||
mkdir -p "$extract_dir"
|
||||
|
||||
case "$file_url" in
|
||||
*".zip")
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -q "$temp_file" -d "$extract_dir"
|
||||
else
|
||||
die "ERROR: Please install 'unzip', which is required to extract $moniker"
|
||||
fi ;;
|
||||
*)
|
||||
if command -v tar >/dev/null 2>&1; then
|
||||
tar -x -f "$temp_file" -C "$extract_dir"
|
||||
else
|
||||
die "ERROR: Please install 'tar', which is required to extract $moniker"
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
rm -f "$temp_file"
|
||||
|
||||
echo "$file_sha" >"$extract_dir/.flag"
|
||||
|
||||
# Unlock and cleanup the lock file
|
||||
unlock_and_cleanup
|
||||
|
||||
echo "Download complete."
|
||||
echo
|
||||
}
|
||||
|
||||
|
||||
# ********** Project-local version detection **********
|
||||
|
||||
# 1. Search upwards for an executable `amper` file and/or `project.yaml`
|
||||
# Sets wrapper_script to the found wrapper path, or empty string if not found.
|
||||
find_project_context() {
|
||||
wrapper_script=""
|
||||
this_script="$(realpath "$0")"
|
||||
project_dir=$(pwd)
|
||||
while [ "$project_dir" != "/" ] && [ -n "$project_dir" ]; do
|
||||
wrapper_candidate="$project_dir/kotlin"
|
||||
if [ "$this_script" = "$wrapper_candidate" ]; then
|
||||
# Found itself (local wrapper case), no need to update any version or search further.
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -f "$wrapper_candidate" ] && [ -x "$wrapper_candidate" ]; then
|
||||
# Found the wrapper — check that a project context exists alongside it
|
||||
if [ -f "$project_dir/project.yaml" ] || [ -f "$project_dir/module.yaml" ]; then
|
||||
wrapper_script="$wrapper_candidate"
|
||||
return 0
|
||||
else
|
||||
echo "WARNING: Found wrapper script '$wrapper_candidate', but no project.yaml or module.yaml near it. Skipping." >&2
|
||||
# Continue the search
|
||||
fi
|
||||
elif [ -f "$project_dir/project.yaml" ]; then
|
||||
# Found project.yaml but no executable wrapper alongside it
|
||||
echo "WARNING: Found a project.yaml in '$project_dir', but the wrapper script is missing; using Kotlin Toolchain v$kotlin_cli_version." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
project_dir=$(dirname "$project_dir")
|
||||
done
|
||||
# Do not check root '/' - it's an unlikely candidate for a project
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
parse_project_context() {
|
||||
# Parse kotlin_cli_version and kotlin_cli_sha256 from "$wrapper_script" without executing it.
|
||||
parsed_kotlin_cli_version=$(
|
||||
sed -n 's/^kotlin_cli_version=\([A-Za-z0-9._+-]\{1,\}\)[[:space:]]*$/\1/p' "$wrapper_script" \
|
||||
| head -n 1
|
||||
)
|
||||
parsed_kotlin_cli_sha256=$(
|
||||
sed -n 's/^kotlin_cli_sha256=\([0-9a-fA-F]\{64\}\)[[:space:]]*$/\1/p' "$wrapper_script" \
|
||||
| head -n 1
|
||||
)
|
||||
|
||||
if [ -z "$parsed_kotlin_cli_version" ]; then
|
||||
echo "ERROR: Suspicious local wrapper script: failed to detect the distribution version in '$wrapper_script'" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$parsed_kotlin_cli_sha256" ]; then
|
||||
echo "ERROR: Suspicious local wrapper script: failed to detect the distribution checksum in '$wrapper_script'" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# overwrite builtin values and proceed
|
||||
kotlin_cli_version=$parsed_kotlin_cli_version
|
||||
kotlin_cli_sha256=$parsed_kotlin_cli_sha256
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -z "${KOTLIN_CLI_WRAPPER_ALWAYS_USE_INTRINSIC_VERSION:-}" ]; then
|
||||
find_project_context && parse_project_context
|
||||
fi
|
||||
|
||||
# ********** System detection **********
|
||||
|
||||
kernelName=$(uname -s)
|
||||
case "$kernelName" in
|
||||
Darwin* )
|
||||
default_kotlin_cli_cache_dir="$HOME/Library/Caches/JetBrains/Kotlin/cli"
|
||||
;;
|
||||
Linux* )
|
||||
default_kotlin_cli_cache_dir="$HOME/.cache/JetBrains/Kotlin/cli"
|
||||
;;
|
||||
CYGWIN* | MSYS* | MINGW* )
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
default_kotlin_cli_cache_dir=$(cygpath -u "$LOCALAPPDATA\JetBrains\Kotlin\cli")
|
||||
else
|
||||
die "The 'cypath' command is not available, but the Kotlin CLI needs it. Use kotlin.bat instead, or try a Cygwin or MSYS environment."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
die "Unsupported platform $kernelName"
|
||||
;;
|
||||
esac
|
||||
|
||||
kotlin_cli_cache_dir="${KOTLIN_CLI_BOOTSTRAP_CACHE_DIR:-$default_kotlin_cli_cache_dir}"
|
||||
|
||||
# ********** Provision the Kotlin Toolchain distribution **********
|
||||
|
||||
kotlin_cli_url="$KOTLIN_CLI_DOWNLOAD_ROOT/org/jetbrains/kotlin/kotlin-cli/$kotlin_cli_version/kotlin-cli-$kotlin_cli_version-dist.tgz"
|
||||
kotlin_cli_target_dir="$kotlin_cli_cache_dir/kotlin-cli-$kotlin_cli_version"
|
||||
download_and_extract "Kotlin Toolchain distribution v$kotlin_cli_version" "$kotlin_cli_url" "$kotlin_cli_sha256" 256 "$kotlin_cli_cache_dir" "$kotlin_cli_target_dir" "true"
|
||||
|
||||
# ********** Launch the Kotlin CLI **********
|
||||
|
||||
launcher_script="$kotlin_cli_target_dir/bin/launcher.sh"
|
||||
|
||||
KOTLIN_CLI_WRAPPER_PATH="$(realpath "$0")" \
|
||||
exec /bin/sh "$launcher_script" "$@"
|
||||
File diff suppressed because it is too large.
Load diff
+265
@@ -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%
|
||||
@@ -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
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,6 +2,18 @@
|
||||
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* Extension functions for convenient logging with [KSLog]
|
||||
*
|
||||
* This file provides:
|
||||
* - Generic log functions for all log levels
|
||||
* - Specific functions for each log level (trace, debug, verbose, info, warning, error, assert)
|
||||
* - Short aliases (t, d, v, i, w, e, wtf) for quick logging
|
||||
* - Suspendable variants (with 'S' suffix) for async contexts
|
||||
* - Support for lazy message evaluation via lambda builders
|
||||
* - Both tagged and untagged variants
|
||||
*/
|
||||
|
||||
inline fun KSLog.log(level: LogLevel, tag: String?, e: Throwable? = null, noinline messageBuilder: () -> Any) = performLog(level, tag, e, messageBuilder)
|
||||
inline fun KSLog.log(level: LogLevel, tag: String, noinline messageBuilder: () -> Any) = performLog(level, tag, null, messageBuilder)
|
||||
suspend inline fun KSLog.logS(level: LogLevel, tag: String?, e: Throwable? = null, noinline messageBuilder: suspend () -> Any) = performLogS(level, tag, e, messageBuilder)
|
||||
@@ -0,0 +1,300 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
import dev.inmo.kslog.common.filter.filtered
|
||||
import dev.inmo.kslog.common.utils.plus
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration of all available log levels ordered from least to most severe
|
||||
*
|
||||
* Used to categorize and filter log messages by importance
|
||||
*/
|
||||
enum class LogLevel {
|
||||
/** Trace level - most detailed logging, typically for tracing code execution */
|
||||
TRACE,
|
||||
/** Debug level - detailed information useful for debugging */
|
||||
DEBUG,
|
||||
/** Verbose level - detailed informational messages */
|
||||
VERBOSE,
|
||||
/** Info level - general informational messages about application state */
|
||||
INFO,
|
||||
/** Warning level - potentially harmful situations */
|
||||
WARNING,
|
||||
/** Error level - error events that might still allow the application to continue */
|
||||
ERROR,
|
||||
/** Assert level - severe errors that should never happen */
|
||||
ASSERT,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for any logger in KSLog framework
|
||||
*
|
||||
* This is the core logging interface that all logger implementations must implement.
|
||||
* It provides multiple overloads for logging messages with various combinations of parameters.
|
||||
*
|
||||
* The interface supports:
|
||||
* - Synchronous and suspendable logging
|
||||
* - Lazy message evaluation via lambda builders
|
||||
* - Optional tags for categorizing logs
|
||||
* - Exception/throwable logging
|
||||
* - All standard log levels (TRACE, DEBUG, VERBOSE, INFO, WARNING, ERROR, ASSERT)
|
||||
*
|
||||
* @see default Default logger instance
|
||||
* @see DefaultKSLog Default implementation
|
||||
* @see FilterKSLog Logger with filtering capabilities
|
||||
* @see TypedKSLog Logger with type-based routing
|
||||
*/
|
||||
interface KSLog {
|
||||
/**
|
||||
* The core logging method that must be implemented by all logger implementations
|
||||
*
|
||||
* All other [performLog] overloads will eventually call this method by default.
|
||||
*
|
||||
* @param level The severity level of the log message
|
||||
* @param tag Optional tag to categorize or identify the source of the log
|
||||
* @param message The message to be logged (can be any object, will be converted to string)
|
||||
* @param throwable Optional exception or throwable to be logged alongside the message
|
||||
*/
|
||||
fun performLog(level: LogLevel, tag: String?, message: Any, throwable: Throwable?)
|
||||
|
||||
/**
|
||||
* Logs a message without a tag
|
||||
*
|
||||
* Convenience method that calls the main [performLog] with `tag` set to `null`
|
||||
*
|
||||
* @param level The severity level of the log message
|
||||
* @param message The message to be logged
|
||||
* @param throwable Optional exception or throwable to be logged
|
||||
*/
|
||||
fun performLog(level: LogLevel, message: Any, throwable: Throwable?) = performLog(level, null, message, throwable)
|
||||
/**
|
||||
* Logs a message with lazy evaluation of the message content
|
||||
*
|
||||
* The [messageBuilder] lambda will only be executed if the message is actually logged.
|
||||
* This is useful for expensive message construction that should be avoided when the
|
||||
* message would be filtered out.
|
||||
*
|
||||
* This method should be overridden in logger implementations that support lazy evaluation
|
||||
* (like [dev.inmo.kslog.common.filter.FilterKSLog]).
|
||||
*
|
||||
* @param level The severity level of the log message
|
||||
* @param tag Optional tag to categorize the log
|
||||
* @param throwable Optional exception or throwable to be logged
|
||||
* @param messageBuilder Lambda that builds the message only when needed
|
||||
*/
|
||||
fun performLog(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: () -> Any
|
||||
) = performLog(level, tag, messageBuilder(), throwable)
|
||||
|
||||
/**
|
||||
* Suspendable variant of [performLog] with lazy message evaluation
|
||||
*
|
||||
* Similar to the non-suspending [performLog] with [messageBuilder], but allows the
|
||||
* message builder to be a suspending function. This enables logging with messages
|
||||
* that require async operations to construct.
|
||||
*
|
||||
* By default, executes the suspending [messageBuilder] and calls the regular [performLog].
|
||||
*
|
||||
* @param level The severity level of the log message
|
||||
* @param tag Optional tag to categorize the log
|
||||
* @param throwable Optional exception or throwable to be logged
|
||||
* @param messageBuilder Suspending lambda that builds the message only when needed
|
||||
*/
|
||||
suspend fun performLogS(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: suspend () -> Any
|
||||
) = performLog(level, tag, messageBuilder(), throwable)
|
||||
|
||||
|
||||
companion object : KSLog {
|
||||
private var defaultLogger: KSLog? = null
|
||||
|
||||
/**
|
||||
* Default logger instance used when calling logging extensions without an explicit receiver
|
||||
*
|
||||
* If not explicitly set, a default logger with tag "app" will be created automatically.
|
||||
* You can customize this by setting your own logger implementation.
|
||||
*
|
||||
* @see setDefaultKSLog
|
||||
* @see addDefaultKSLog
|
||||
*/
|
||||
var default: KSLog
|
||||
get() {
|
||||
return defaultLogger ?: KSLog("app").also {
|
||||
defaultLogger = it
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
defaultLogger = value
|
||||
}
|
||||
override fun performLog(level: LogLevel, tag: String?, message: Any, throwable: Throwable?) = default.performLog(level, tag, message, throwable)
|
||||
override fun performLog(level: LogLevel, message: Any, throwable: Throwable?) = default.performLog(level, message, throwable)
|
||||
override fun performLog(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: () -> Any
|
||||
) = default.performLog(level, tag, throwable, messageBuilder)
|
||||
override suspend fun performLogS(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: suspend () -> Any
|
||||
) = default.performLogS(level, tag, throwable, messageBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a [CallbackKSLog] by invoking a [KSLog] instance with a callback function
|
||||
*
|
||||
* This operator allows convenient logger creation syntax:
|
||||
* ```kotlin
|
||||
* val logger = KSLog { level, tag, message, throwable ->
|
||||
* // Custom logging logic
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param performLogCallback The callback function to handle log messages
|
||||
* @return A new [CallbackKSLog] instance
|
||||
*/
|
||||
operator fun KSLog.invoke(performLogCallback: (level: LogLevel, tag: String?, message: Any, throwable: Throwable?) -> Unit) = CallbackKSLog(performLogCallback)
|
||||
|
||||
/**
|
||||
* Simple logging implementation that uses `println` to output formatted log messages
|
||||
*
|
||||
* Uses [defaultMessageFormatter] to format messages before outputting to standard output.
|
||||
* This is used as the default logging backend for Native and WASM platforms.
|
||||
*
|
||||
* @see defaultMessageFormatter
|
||||
*/
|
||||
internal val printlnLogging: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, t, m, e ->
|
||||
println(defaultMessageFormatter(l, t, m, e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [DefaultKSLog] logger with the specified default tag
|
||||
*
|
||||
* @param defaultTag The default tag to use for all log messages from this logger
|
||||
* @return A new [DefaultKSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
): KSLog = DefaultKSLog(defaultTag)
|
||||
|
||||
/**
|
||||
* Creates a [DefaultKSLog] logger with a custom message formatter
|
||||
*
|
||||
* @param defaultTag The default tag to use for all log messages
|
||||
* @param messageFormatter Custom formatter for converting log parameters to strings
|
||||
* @return A new [DefaultKSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
messageFormatter: MessageFormatter
|
||||
): KSLog = DefaultKSLog(
|
||||
defaultTag,
|
||||
messageFormatter
|
||||
)
|
||||
|
||||
@Deprecated("Filtering should be replaced with FilterKSLog")
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
filter: MessageFilter,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog = KSLog (
|
||||
defaultTag,
|
||||
messageFormatter
|
||||
).filtered(filter)
|
||||
|
||||
/**
|
||||
* Creates a filtered [KSLog] that only logs messages at specific levels
|
||||
*
|
||||
* Builds a logger that will only process log messages whose level is included in the [levels] collection.
|
||||
* All other log levels will be filtered out.
|
||||
*
|
||||
* @param defaultTag The default tag to use for all log messages
|
||||
* @param levels Collection of log levels that should be logged
|
||||
* @param messageFormatter Custom formatter for converting log parameters to strings (defaults to [defaultMessageFormatter])
|
||||
* @return A new filtered [KSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
levels: Iterable<LogLevel>,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog {
|
||||
val levelsSet = levels.toSet()
|
||||
return KSLog (defaultTag, messageFormatter).filtered { l, _, _ ->
|
||||
l in levelsSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a filtered [KSLog] that only logs messages at the specified levels
|
||||
*
|
||||
* Convenience overload that accepts individual log levels as parameters.
|
||||
*
|
||||
* @param defaultTag The default tag to use for all log messages
|
||||
* @param firstLevel First log level to include
|
||||
* @param secondLevel Second log level to include
|
||||
* @param otherLevels Additional log levels to include
|
||||
* @param messageFormatter Custom formatter for converting log parameters to strings (defaults to [defaultMessageFormatter])
|
||||
* @return A new filtered [KSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
firstLevel: LogLevel,
|
||||
secondLevel: LogLevel,
|
||||
vararg otherLevels: LogLevel,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter,
|
||||
): KSLog = KSLog(defaultTag, setOf(firstLevel, secondLevel, *otherLevels), messageFormatter)
|
||||
|
||||
/**
|
||||
* Creates a filtered [KSLog] that only logs messages at or above a minimum severity level
|
||||
*
|
||||
* Uses the ordinal values of [LogLevel] enum to determine which messages to log.
|
||||
* For example, if [minLoggingLevel] is INFO, then INFO, WARNING, ERROR, and ASSERT will be logged,
|
||||
* while TRACE, DEBUG, and VERBOSE will be filtered out.
|
||||
*
|
||||
* @param defaultTag The default tag to use for all log messages
|
||||
* @param minLoggingLevel Minimum log level to include (inclusive)
|
||||
* @param messageFormatter Custom formatter for converting log parameters to strings (defaults to [defaultMessageFormatter])
|
||||
* @return A new filtered [KSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
minLoggingLevel: LogLevel,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter,
|
||||
): KSLog = KSLog (
|
||||
defaultTag,
|
||||
messageFormatter
|
||||
).filtered { l, _, _ ->
|
||||
minLoggingLevel.ordinal <= l.ordinal
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the [KSLog.default] logger to a new instance
|
||||
*
|
||||
* This replaces the current default logger completely.
|
||||
*
|
||||
* @param newDefault The new default logger to use
|
||||
*/
|
||||
fun setDefaultKSLog(newDefault: KSLog) { KSLog.default = newDefault }
|
||||
|
||||
/**
|
||||
* Adds an additional logger to the [KSLog.default] logger chain
|
||||
*
|
||||
* Uses the [plus] operator to combine the current default logger with [newDefault],
|
||||
* so that log messages will be sent to both loggers.
|
||||
*
|
||||
* @param newDefault Additional logger to add to the default logger chain
|
||||
* @see dev.inmo.kslog.common.utils.plus
|
||||
*/
|
||||
fun addDefaultKSLog(newDefault: KSLog) {
|
||||
KSLog.default += newDefault
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* Function type for formatting log messages
|
||||
*
|
||||
* @param l The log level
|
||||
* @param t Optional tag for the log message
|
||||
* @param m The message object to be logged
|
||||
* @param Throwable Optional exception/throwable to be logged
|
||||
* @return Formatted string representation of the log message
|
||||
*/
|
||||
typealias MessageFormatter = (l: LogLevel, t: String?, m: Any, Throwable?) -> String
|
||||
|
||||
/**
|
||||
* Helper function to create a [MessageFormatter] from a lambda
|
||||
*/
|
||||
inline fun MessageFormatter(noinline formatter: MessageFormatter) = formatter
|
||||
|
||||
/**
|
||||
* Function type for filtering log messages
|
||||
*
|
||||
* @param l The log level
|
||||
* @param t Optional tag for the log message
|
||||
* @param Throwable Optional exception/throwable associated with the log
|
||||
* @return `true` if the message should be logged, `false` to filter it out
|
||||
*/
|
||||
typealias MessageFilter = (l: LogLevel, t: String?, Throwable?) -> Boolean
|
||||
|
||||
/**
|
||||
* Default message formatter that formats logs as: `[LEVEL] tag - message`
|
||||
*
|
||||
* Example output: `[LogLevel.INFO] MyTag - Hello world`
|
||||
*/
|
||||
val defaultMessageFormatter: MessageFormatter = { l, t, m, _ -> "[$l] ${t ?.let { "$it " } ?: ""}- $m" }
|
||||
|
||||
/**
|
||||
* Message formatter that includes stack trace of exceptions in the output
|
||||
*
|
||||
* Formats logs as: `[LEVEL] tag - message: stacktrace`
|
||||
*
|
||||
* Example output: `[LogLevel.ERROR] MyTag - Error occurred: java.lang.Exception: ...`
|
||||
*/
|
||||
val defaultMessageFormatterWithErrorPrint: MessageFormatter = { l, t, m, e -> "[$l] ${t ?.let { "$it " } ?: ""}- $m${e ?.let { ": ${it.stackTraceToString()}" } ?: ""}" }
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* Platform-specific default logging implementation
|
||||
*
|
||||
* This is an `expect` declaration that provides platform-specific logging behavior:
|
||||
* - **Android**: Uses `android.util.Log` (Log.d, Log.i, Log.w, Log.e, etc.)
|
||||
* - **JVM**: Uses `java.util.logging.Logger`
|
||||
* - **JS**: Uses browser `console` (console.debug, console.info, console.warn, console.error)
|
||||
* - **Native/WASM**: Uses `println` for output
|
||||
*
|
||||
* This lambda is used by [DefaultKSLog] as the default logging backend when no custom
|
||||
* logging function is provided.
|
||||
*
|
||||
* **Warning**: This is a global mutable variable. Changing it will affect all loggers
|
||||
* that use the default platform logger. It's primarily used by [KSLog.default].
|
||||
*
|
||||
* @see DefaultKSLog
|
||||
* @see KSLog.default
|
||||
*/
|
||||
expect var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit
|
||||
File renamed without changes.
@@ -3,8 +3,21 @@ package dev.inmo.kslog.common.filter
|
||||
import dev.inmo.kslog.common.*
|
||||
|
||||
/**
|
||||
* In its [performLog]/[performLogS] methods do logging only in cases when [messageFilter] returns true for incoming
|
||||
* parameters
|
||||
* A logger that filters messages before passing them to a fallback logger
|
||||
*
|
||||
* This logger implementation applies a [messageFilter] predicate to all incoming log requests.
|
||||
* Only when the filter returns `true` will the message be passed to the [fallbackLogger].
|
||||
*
|
||||
* This enables:
|
||||
* - Log level filtering (e.g., only log ERROR and above)
|
||||
* - Tag-based filtering (e.g., only log from specific components)
|
||||
* - Conditional logging based on exception presence
|
||||
* - Lazy message evaluation - messages are only constructed if they pass the filter
|
||||
*
|
||||
* @param fallbackLogger The underlying logger that will receive filtered messages
|
||||
* @param messageFilter Predicate function that determines which messages to log
|
||||
*
|
||||
* @see filtered Extension function for convenient filter creation
|
||||
*/
|
||||
class FilterKSLog(
|
||||
private val fallbackLogger: KSLog,
|
||||
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.kslog.common.filter
|
||||
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.MessageFilter
|
||||
|
||||
/**
|
||||
* Creates a [FilterKSLog] that wraps this logger with a message filter
|
||||
*
|
||||
* This extension function provides a convenient way to add filtering to any existing logger.
|
||||
* The resulting logger will only pass messages to this logger when the [filter] returns `true`.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val logger = KSLog("app").filtered { level, tag, throwable ->
|
||||
* level.ordinal >= LogLevel.INFO.ordinal // Only log INFO and above
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param filter Predicate function that receives (level, tag, throwable) and returns whether to log
|
||||
* @return A new [FilterKSLog] instance wrapping this logger
|
||||
* @see FilterKSLog
|
||||
*/
|
||||
fun KSLog.filtered(
|
||||
filter: MessageFilter
|
||||
) = FilterKSLog(this, filter)
|
||||
@@ -4,9 +4,25 @@ import dev.inmo.kslog.common.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Uses [typedLoggers] [Map] to determine, where incoming __message__s should be sent. If there is no [KClass] key for
|
||||
* incoming message in [typedLoggers], logger will use logger by `null` key if exists. If there is no default logger
|
||||
* (by `null` key), logging will be skipped
|
||||
* A logger that routes messages to different loggers based on the message type
|
||||
*
|
||||
* This logger uses the Kotlin class ([KClass]) of the log message to determine which logger to use.
|
||||
* This enables type-based routing of log messages, allowing different message types to be
|
||||
* handled by different loggers.
|
||||
*
|
||||
* The routing works as follows:
|
||||
* 1. Extract the [KClass] from the incoming message object
|
||||
* 2. Look up the class in [typedLoggers] map
|
||||
* 3. If found, use that logger
|
||||
* 4. If not found, fall back to the logger with `null` key (if it exists)
|
||||
* 5. If no default logger exists, skip logging
|
||||
*
|
||||
* Example use case: Route structured log events to different destinations based on their type.
|
||||
*
|
||||
* @param typedLoggers Map from message class to logger. Use `null` key for the default/fallback logger
|
||||
*
|
||||
* @see TypedKSLogBuilder Builder for creating typed loggers
|
||||
* @see buildTypedLogger DSL function for convenient creation
|
||||
*/
|
||||
class TypedKSLog(
|
||||
private val typedLoggers: Map<KClass<*>?, KSLog>
|
||||
@@ -0,0 +1,101 @@
|
||||
package dev.inmo.kslog.common.typed
|
||||
|
||||
import dev.inmo.kslog.common.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Builder for creating [TypedKSLog] instances with a fluent API
|
||||
*
|
||||
* Provides convenient methods for configuring type-based routing of log messages.
|
||||
* Supports both explicit [KClass] specifications and reified type parameters.
|
||||
*
|
||||
* @param preset Optional initial map of type-to-logger associations to start with
|
||||
*
|
||||
* @see buildTypedLogger DSL function for convenient usage
|
||||
*/
|
||||
class TypedKSLogBuilder(
|
||||
private val preset: Map<KClass<*>?, KSLog> = emptyMap()
|
||||
) {
|
||||
private val loggers = mutableMapOf<KClass<*>?, KSLog>().apply {
|
||||
putAll(preset)
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a specific class with a logger
|
||||
*
|
||||
* @param kClass The class to route, or `null` for the default/fallback logger
|
||||
* @param with The logger to use for this class
|
||||
*/
|
||||
fun on(kClass: KClass<*>?, with: KSLog) = loggers.put(kClass, with)
|
||||
|
||||
/**
|
||||
* Associates a specific class with a callback-based logger
|
||||
*
|
||||
* @param kClass The class to route, or `null` for the default/fallback logger
|
||||
* @param with The callback function to handle logs for this class
|
||||
*/
|
||||
fun on(kClass: KClass<*>?, with: SimpleKSLogCallback) = loggers.put(kClass, CallbackKSLog(with))
|
||||
|
||||
/**
|
||||
* Sets the default/fallback logger for messages of unregistered types
|
||||
*
|
||||
* @param with The logger to use as default
|
||||
*/
|
||||
fun default(with: KSLog) = on(null, with)
|
||||
|
||||
/**
|
||||
* Sets the default/fallback logger using a callback function
|
||||
*
|
||||
* @param with The callback function to handle logs from unregistered types
|
||||
*/
|
||||
fun default(with: SimpleKSLogCallback) = on(null, with)
|
||||
|
||||
/**
|
||||
* Associates a specific type with a logger using reified type parameter
|
||||
*
|
||||
* Example: `on<MyEvent>(customLogger)`
|
||||
*
|
||||
* @param T The type to route
|
||||
* @param with The logger to use for this type
|
||||
*/
|
||||
inline fun <reified T: Any> on(with: KSLog) = on(T::class, with)
|
||||
|
||||
/**
|
||||
* Associates a specific type with a callback-based logger using reified type parameter
|
||||
*
|
||||
* Example: `on<MyEvent> { level, tag, message, error -> ... }`
|
||||
*
|
||||
* @param T The type to route
|
||||
* @param with The callback function to handle logs for this type
|
||||
*/
|
||||
inline fun <reified T: Any> on(noinline with: SimpleKSLogCallback) = on(T::class, with)
|
||||
|
||||
/**
|
||||
* Builds the [TypedKSLog] instance with the configured type-to-logger associations
|
||||
*/
|
||||
fun build() = TypedKSLog(loggers.toMap())
|
||||
}
|
||||
|
||||
/**
|
||||
* DSL function for building a [TypedKSLog] with a fluent configuration block
|
||||
*
|
||||
* Provides a convenient way to create typed loggers with type-to-logger associations.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val logger = buildTypedLogger {
|
||||
* on<ErrorEvent>(errorLogger)
|
||||
* on<WarningEvent>(warningLogger)
|
||||
* default(defaultLogger)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param preset Optional initial map of type-to-logger associations
|
||||
* @param block Configuration block for specifying type-to-logger associations
|
||||
* @return A new [TypedKSLog] instance
|
||||
* @see TypedKSLogBuilder
|
||||
*/
|
||||
inline fun buildTypedLogger(
|
||||
preset: Map<KClass<*>?, KSLog> = emptyMap(),
|
||||
block: TypedKSLogBuilder.() -> Unit
|
||||
) = TypedKSLogBuilder(preset).apply(block).build()
|
||||
@@ -0,0 +1,36 @@
|
||||
package dev.inmo.kslog.common.utils
|
||||
|
||||
import dev.inmo.kslog.common.CallbackKSLog
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
|
||||
/**
|
||||
* Combines two loggers into one that sends messages to both
|
||||
*
|
||||
* Creates a new logger that will invoke [performLog] on both this logger and the [other] logger
|
||||
* for every log message. Both loggers will be called even if one fails.
|
||||
*
|
||||
* Error handling:
|
||||
* - If this logger throws an exception, it will be rethrown after attempting to log to [other]
|
||||
* - If [other] throws an exception, it will also be rethrown
|
||||
* - If both throw exceptions, both will be rethrown (first this logger's, then other's)
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val combinedLogger = consoleLogger + fileLogger
|
||||
* combinedLogger.info("Message") // Logs to both console and file
|
||||
* ```
|
||||
*
|
||||
* @param other The second logger to combine with this one
|
||||
* @return A new [CallbackKSLog] that logs to both loggers
|
||||
* @see addDefaultKSLog Uses this operator to add loggers to the default logger
|
||||
*/
|
||||
infix operator fun KSLog.plus(other: KSLog) = CallbackKSLog { l, t, m, e ->
|
||||
val resultOfFirst = runCatching {
|
||||
this@plus.performLog(l, t, m, e)
|
||||
}
|
||||
val resultOfSecond = runCatching {
|
||||
other.performLog(l, t, m, e)
|
||||
}
|
||||
resultOfFirst.onFailure { throw it }
|
||||
resultOfSecond.onFailure { throw it }
|
||||
}
|
||||
+15
@@ -2,6 +2,21 @@ package dev.inmo.kslog.common
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Android platform implementation of the default logger
|
||||
*
|
||||
* Uses Android's `[android.util.Log]` as the logging backend, mapping KSLog levels
|
||||
* to Android log levels:
|
||||
* - [LogLevel.TRACE] → [Log.d] (debug)
|
||||
* - [LogLevel.DEBUG] → [Log.d] (debug)
|
||||
* - [LogLevel.VERBOSE] → [Log.v] (verbose)
|
||||
* - [LogLevel.INFO] → [Log.i] (info)
|
||||
* - [LogLevel.WARNING] → [Log.w] (warning)
|
||||
* - [LogLevel.ERROR] → [Log.e] (error)
|
||||
* - [LogLevel.ASSERT] → [Log.wtf] (what a terrible failure)
|
||||
*
|
||||
* Logs can be viewed using `adb logcat` or Android Studio's Logcat window.
|
||||
*/
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, t, m, e ->
|
||||
val messageString = m.toString()
|
||||
when(l) {
|
||||
@@ -0,0 +1,63 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
import kotlin.js.Console
|
||||
|
||||
/**
|
||||
* Extended Console interface that includes additional console methods
|
||||
* not present in Kotlin's standard Console interface
|
||||
*
|
||||
* Provides access to browser/Node.js console methods like `debug`, `trace`, and `assert`.
|
||||
*/
|
||||
external interface ExtendedConsole : Console {
|
||||
/**
|
||||
* Outputs a stack trace to the console
|
||||
*/
|
||||
fun trace()
|
||||
|
||||
/**
|
||||
* Outputs a debug message to the console
|
||||
*
|
||||
* @param o Objects to output
|
||||
*/
|
||||
fun debug(vararg o: Any?)
|
||||
|
||||
/**
|
||||
* Writes an error message to the console if the assertion is false
|
||||
*
|
||||
* @param assertion Boolean condition to test
|
||||
* @param o Objects to output if assertion is false
|
||||
*/
|
||||
fun assert(assertion: Boolean, vararg o: Any?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to output debug messages to the console
|
||||
*
|
||||
* Maps to the browser's `console.debug()` method.
|
||||
*
|
||||
* [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/console/debug_static)
|
||||
*
|
||||
* @param args Objects to output
|
||||
*/
|
||||
fun Console.debug(vararg args: Any?) = unsafeCast<ExtendedConsole>().debug(*args)
|
||||
|
||||
/**
|
||||
* Extension function to write an error message if assertion fails
|
||||
*
|
||||
* Maps to the browser's `console.assert()` method.
|
||||
*
|
||||
* [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/console/assert_static)
|
||||
*
|
||||
* @param assertion Boolean condition to test
|
||||
* @param args Objects to output if assertion is false
|
||||
*/
|
||||
fun Console.assert(assertion: Boolean, vararg args: Any?) = unsafeCast<ExtendedConsole>().assert(assertion, *args)
|
||||
|
||||
/**
|
||||
* Extension function to output a stack trace to the console
|
||||
*
|
||||
* Maps to the browser's `console.trace()` method.
|
||||
*
|
||||
* [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/console/trace_static)
|
||||
*/
|
||||
fun Console.trace() = unsafeCast<ExtendedConsole>().trace()
|
||||
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* JavaScript platform implementation of the default logger
|
||||
*
|
||||
* Uses browser/Node.js `console` API as the logging backend, mapping KSLog levels
|
||||
* to console methods:
|
||||
* - [LogLevel.TRACE] → console.trace() + console.debug()
|
||||
* - [LogLevel.DEBUG] → console.debug()
|
||||
* - [LogLevel.VERBOSE] → console.info()
|
||||
* - [LogLevel.INFO] → console.info()
|
||||
* - [LogLevel.WARNING] → console.warn()
|
||||
* - [LogLevel.ERROR] → console.error()
|
||||
* - [LogLevel.ASSERT] → console.error()
|
||||
*
|
||||
* Logs can be viewed in the browser's Developer Tools console or Node.js console output.
|
||||
*
|
||||
* **Note**: The tag parameter is ignored in JS implementation as console methods
|
||||
* don't support tagging natively.
|
||||
*/
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, _, m, e ->
|
||||
val args = e ?.let {
|
||||
arrayOf(m, e)
|
||||
} ?: arrayOf(m)
|
||||
when (l) {
|
||||
LogLevel.TRACE -> {
|
||||
console.trace()
|
||||
console.debug(*args)
|
||||
}
|
||||
LogLevel.DEBUG -> console.debug(*args)
|
||||
LogLevel.VERBOSE,
|
||||
LogLevel.INFO -> console.info(*args)
|
||||
LogLevel.WARNING -> console.warn(*args)
|
||||
LogLevel.ERROR -> console.error(*args)
|
||||
LogLevel.ASSERT -> console.error(*args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
import dev.inmo.kslog.common.filter.filtered
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Default Java Logger instance used by KSLog on JVM platform
|
||||
*
|
||||
* Initialized lazily with the name "KSLog"
|
||||
*/
|
||||
internal val defaultKSLogLogger by lazy {
|
||||
Logger.getLogger("KSLog")
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to log with KSLog levels using Java Logger
|
||||
*
|
||||
* Maps KSLog [LogLevel] to Java [Level]:
|
||||
* - [LogLevel.TRACE] → [Level.FINEST]
|
||||
* - [LogLevel.DEBUG] → [Level.FINER]
|
||||
* - [LogLevel.VERBOSE] → [Level.FINE]
|
||||
* - [LogLevel.INFO] → [Level.INFO]
|
||||
* - [LogLevel.WARNING] → [Level.WARNING]
|
||||
* - [LogLevel.ERROR] → [Level.SEVERE]
|
||||
* - [LogLevel.ASSERT] → [Level.SEVERE]
|
||||
*
|
||||
* @param l KSLog log level
|
||||
* @param m Message string
|
||||
* @param e Optional throwable
|
||||
*/
|
||||
internal fun Logger.doLog(
|
||||
l: LogLevel, m: String, e: Throwable?
|
||||
) = log(
|
||||
when(l) {
|
||||
LogLevel.TRACE -> Level.FINEST
|
||||
LogLevel.DEBUG -> Level.FINER
|
||||
LogLevel.VERBOSE -> Level.FINE
|
||||
LogLevel.INFO -> Level.INFO
|
||||
LogLevel.WARNING -> Level.WARNING
|
||||
LogLevel.ERROR -> Level.SEVERE
|
||||
LogLevel.ASSERT -> Level.SEVERE
|
||||
},
|
||||
m,
|
||||
e
|
||||
)
|
||||
|
||||
@Deprecated("Filtering should be replaced with FilterKSLog")
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
filter: MessageFilter,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
) = KSLog { l, t, m, e ->
|
||||
if (!filter(l, t, e)) return@KSLog
|
||||
val text = messageFormatter(l,t ?: defaultTag,m.toString(),e)
|
||||
logger.doLog(l, text, e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KSLog that uses a specific Java [Logger] instance
|
||||
*
|
||||
* @param defaultTag Default tag to use when tag is not specified
|
||||
* @param logger The Java Logger instance to use for logging
|
||||
* @param messageFormatter Formatter for log messages (defaults to [defaultMessageFormatter])
|
||||
* @return A new [CallbackKSLog] instance wrapping the Java logger
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
) = KSLog { l, t, m, e ->
|
||||
val text = messageFormatter(l,t ?: defaultTag,m.toString(),e)
|
||||
logger.doLog(l, text, e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a filtered KSLog using a Java [Logger] that only logs specific levels
|
||||
*
|
||||
* @param defaultTag Default tag to use when tag is not specified
|
||||
* @param logger The Java Logger instance to use for logging
|
||||
* @param levels Collection of log levels to include
|
||||
* @param messageFormatter Formatter for log messages (defaults to [defaultMessageFormatter])
|
||||
* @return A new filtered [KSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
levels: Iterable<LogLevel>,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog {
|
||||
val levelsSet = levels.toSet()
|
||||
return KSLog (defaultTag, logger, messageFormatter).filtered { l, _, _ -> l in levelsSet }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a filtered KSLog using a Java [Logger] with specific log levels
|
||||
*
|
||||
* @param defaultTag Default tag to use when tag is not specified
|
||||
* @param logger The Java Logger instance to use for logging
|
||||
* @param firstLevel First log level to include
|
||||
* @param secondLevel Second log level to include
|
||||
* @param otherLevels Additional log levels to include
|
||||
* @param messageFormatter Formatter for log messages (defaults to [defaultMessageFormatter])
|
||||
* @return A new filtered [KSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
firstLevel: LogLevel,
|
||||
secondLevel: LogLevel,
|
||||
vararg otherLevels: LogLevel,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter,
|
||||
): KSLog = KSLog (defaultTag, logger, setOf(firstLevel, secondLevel, *otherLevels), messageFormatter)
|
||||
|
||||
/**
|
||||
* Creates a filtered KSLog using a Java [Logger] with a minimum log level
|
||||
*
|
||||
* Only logs messages at or above the specified minimum level.
|
||||
*
|
||||
* @param defaultTag Default tag to use when tag is not specified
|
||||
* @param logger The Java Logger instance to use for logging
|
||||
* @param minLoggingLevel Minimum log level to include (defaults to TRACE - the first level)
|
||||
* @param messageFormatter Formatter for log messages (defaults to [defaultMessageFormatter])
|
||||
* @return A new filtered [KSLog] instance
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
minLoggingLevel: LogLevel = LogLevel.values().first(),
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog = KSLog (defaultTag, logger, messageFormatter).filtered { l, _, _ -> minLoggingLevel.ordinal <= l.ordinal }
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* JVM platform implementation of the default logger
|
||||
*
|
||||
* Uses `[java.util.logging.Logger]` as the logging backend, mapping KSLog levels
|
||||
* to Java logging levels.
|
||||
*
|
||||
* @see defaultKSLogLogger
|
||||
*/
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, t, m, e ->
|
||||
defaultKSLogLogger.doLog(l, m.toString(), e)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* Native platform implementation of the default logger
|
||||
*
|
||||
* Uses [printlnLogging] which formats and outputs logs using `[println]`.
|
||||
* Logs are written to standard output.
|
||||
*
|
||||
* @see printlnLogging
|
||||
*/
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* WebAssembly JavaScript platform implementation of the default logger
|
||||
*
|
||||
* Uses [printlnLogging] which formats and outputs logs using `[println]`.
|
||||
*
|
||||
* @see printlnLogging
|
||||
*/
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020-2021 JetBrains s.r.o. and and respective authors and developers.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,27 @@
|
||||
# KSLog Dokka Toolchain plugin
|
||||
|
||||
This local plugin is adapted from JetBrains Kotlin Toolchain's
|
||||
`build-sources/dokka` plugin at release `v0.13.0`, commit
|
||||
`abc7a5f7579e9ef267107782048c76777f0bf932`.
|
||||
|
||||
Upstream source: <https://github.com/JetBrains/kotlin-toolchain/tree/abc7a5f7579e9ef267107782048c76777f0bf932/build-sources/dokka>
|
||||
|
||||
Changes made for KSLog:
|
||||
|
||||
- models the KMP common/JVM/Android/JS/WasmJS/native source-set graph;
|
||||
- uses Dokka 2.2.0 to match the pre-migration build;
|
||||
- provides separate `dokkaHtml` and `dokkaGfm` commands;
|
||||
- writes to the preserved `build/dokka/html` and `build/dokka/gfm` paths;
|
||||
- preserves deprecated-member filtering and GitHub source links;
|
||||
- supplies the resolved JVM classpath plus transformed Common and matching
|
||||
JS/WasmJS standard-library KLIBs materialized by the documented prebuild;
|
||||
- fails generation if Dokka emits an unresolved-type placeholder, preventing
|
||||
the deployment workflow from uploading incomplete API signatures.
|
||||
|
||||
The legacy Gradle `DOKKA_PATH` override remains Gradle-only. The Toolchain
|
||||
plugin intentionally accepts only output directories below `build/dokka`,
|
||||
rejects symbolic-link ancestors, and verifies real-path containment before
|
||||
cleaning generated output.
|
||||
|
||||
The adapted Kotlin files retain the upstream Apache-2.0 header. See
|
||||
`LICENSE.txt` in this directory.
|
||||
@@ -0,0 +1,20 @@
|
||||
product: jvm/amper-plugin
|
||||
|
||||
description: KMP-capable Dokka HTML and GFM tasks adapted from the Kotlin Toolchain Dokka plugin.
|
||||
|
||||
pluginInfo:
|
||||
id: dokka
|
||||
settingsClass: dev.inmo.kslog.build.dokka.DokkaSettings
|
||||
|
||||
test-dependencies:
|
||||
- $libs.kotlin.test.junit
|
||||
|
||||
settings:
|
||||
junit: junit-4
|
||||
jvm:
|
||||
jdk:
|
||||
version: 17
|
||||
release: 17
|
||||
kotlin:
|
||||
version: 2.4.20
|
||||
serialization: json
|
||||
@@ -0,0 +1,37 @@
|
||||
tasks:
|
||||
html:
|
||||
action: !dev.inmo.kslog.build.dokka.dokkaHtml
|
||||
projectRootDir: ${project.rootDir}
|
||||
moduleRootDir: ${module.rootDir}
|
||||
moduleCompileClasspath: ${module.compileClasspath}
|
||||
runtimeClasspath:
|
||||
- $libs.dokka.cli
|
||||
pluginsClasspath:
|
||||
- $libs.dokka.base
|
||||
- $libs.dokka.analysis.kotlin.descriptors
|
||||
- $libs.kotlinx.html.jvm
|
||||
- $libs.freemarker
|
||||
outputDir: ${project.rootDir}/build/dokka/html
|
||||
settings: ${pluginSettings}
|
||||
|
||||
gfm:
|
||||
action: !dev.inmo.kslog.build.dokka.dokkaGfm
|
||||
projectRootDir: ${project.rootDir}
|
||||
moduleRootDir: ${module.rootDir}
|
||||
moduleCompileClasspath: ${module.compileClasspath}
|
||||
runtimeClasspath:
|
||||
- $libs.dokka.cli
|
||||
pluginsClasspath:
|
||||
- $libs.dokka.base
|
||||
- $libs.dokka.analysis.kotlin.descriptors
|
||||
- $libs.dokka.gfm
|
||||
- $libs.kotlinx.html.jvm
|
||||
- $libs.freemarker
|
||||
outputDir: ${project.rootDir}/build/dokka/gfm
|
||||
settings: ${pluginSettings}
|
||||
|
||||
commands:
|
||||
- name: dokkaHtml
|
||||
performedBy: html
|
||||
- name: dokkaGfm
|
||||
performedBy: gfm
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
/*
|
||||
* KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0
|
||||
* build-sources/dokka. KSLog extends the configuration model for its KMP
|
||||
* source-set graph and HTML/GFM plugin configuration. See ../README.md.
|
||||
*/
|
||||
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class DokkaConfiguration(
|
||||
val moduleName: String,
|
||||
val moduleVersion: String,
|
||||
val outputDir: String,
|
||||
val sourceSets: List<DokkaSourceSet>,
|
||||
val pluginsClasspath: List<String>,
|
||||
val pluginsConfiguration: List<PluginConfiguration>,
|
||||
val failOnWarning: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SourceSetId(
|
||||
val scopeId: String,
|
||||
val sourceSetName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class DokkaSourceSet(
|
||||
val sourceSetID: SourceSetId,
|
||||
val displayName: String,
|
||||
val dependentSourceSets: List<SourceSetId>,
|
||||
val classpath: List<String>,
|
||||
val sourceRoots: List<String>,
|
||||
val documentedVisibilities: List<String> = listOf("PUBLIC"),
|
||||
val skipDeprecated: Boolean,
|
||||
val jdkVersion: Int,
|
||||
val languageVersion: String = "2.4",
|
||||
val apiVersion: String = "2.4",
|
||||
val analysisPlatform: String,
|
||||
val sourceLinks: List<SourceLink>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SourceLink(
|
||||
val localDirectory: String,
|
||||
val remoteUrl: String,
|
||||
val remoteLineSuffix: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PluginConfiguration(
|
||||
val fqPluginName: String,
|
||||
val serializationFormat: String,
|
||||
val values: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class DokkaBase(
|
||||
val footerMessage: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
/*
|
||||
* KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0
|
||||
* build-sources/dokka. KSLog adds versioned KMP documentation settings and
|
||||
* project-specific source-link defaults. See ../README.md for provenance.
|
||||
*/
|
||||
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import org.jetbrains.amper.plugins.Configurable
|
||||
|
||||
@Configurable
|
||||
interface DokkaSettings {
|
||||
val moduleName: String get() = "KSLog"
|
||||
val moduleVersion: String
|
||||
val sourceLinkBaseUrl: String get() = "https://github.com/InsanusMokrassar/KSLog/blob/master/"
|
||||
val sourceLinkLineSuffix: String get() = "#L"
|
||||
val skipDeprecated: Boolean get() = true
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
/*
|
||||
* KSLog modification notice: adapted from JetBrains Kotlin Toolchain v0.13.0
|
||||
* build-sources/dokka. KSLog adds KMP source sets, HTML/GFM entry points, and
|
||||
* project-confined, symlink-safe generated-output cleanup. See ../README.md.
|
||||
*/
|
||||
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.amper.plugins.Classpath
|
||||
import org.jetbrains.amper.plugins.ExecutionAvoidance
|
||||
import org.jetbrains.amper.plugins.Input
|
||||
import org.jetbrains.amper.plugins.Output
|
||||
import org.jetbrains.amper.plugins.TaskAction
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
@TaskAction(ExecutionAvoidance.Disabled)
|
||||
fun dokkaHtml(
|
||||
projectRootDir: String,
|
||||
moduleRootDir: String,
|
||||
@Input moduleCompileClasspath: Classpath,
|
||||
@Input runtimeClasspath: Classpath,
|
||||
@Input pluginsClasspath: Classpath,
|
||||
@Output outputDir: Path,
|
||||
settings: DokkaSettings,
|
||||
) = runDokka(
|
||||
projectRootDir,
|
||||
moduleRootDir,
|
||||
moduleCompileClasspath,
|
||||
runtimeClasspath,
|
||||
pluginsClasspath,
|
||||
outputDir,
|
||||
settings,
|
||||
)
|
||||
|
||||
@TaskAction(ExecutionAvoidance.Disabled)
|
||||
fun dokkaGfm(
|
||||
projectRootDir: String,
|
||||
moduleRootDir: String,
|
||||
@Input moduleCompileClasspath: Classpath,
|
||||
@Input runtimeClasspath: Classpath,
|
||||
@Input pluginsClasspath: Classpath,
|
||||
@Output outputDir: Path,
|
||||
settings: DokkaSettings,
|
||||
) = runDokka(
|
||||
projectRootDir,
|
||||
moduleRootDir,
|
||||
moduleCompileClasspath,
|
||||
runtimeClasspath,
|
||||
pluginsClasspath,
|
||||
outputDir,
|
||||
settings,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
private fun runDokka(
|
||||
projectRootDir: String,
|
||||
moduleRootDir: String,
|
||||
moduleCompileClasspath: Classpath,
|
||||
runtimeClasspath: Classpath,
|
||||
pluginsClasspath: Classpath,
|
||||
outputDir: Path,
|
||||
settings: DokkaSettings,
|
||||
) {
|
||||
val normalizedRoot = Path.of(moduleRootDir).toRealPath()
|
||||
val normalizedOutput = prepareDokkaOutput(projectRootDir, outputDir)
|
||||
|
||||
val commonId = SourceSetId(settings.moduleName, "commonMain")
|
||||
val sourceLink = SourceLink(
|
||||
localDirectory = normalizedRoot.pathString,
|
||||
remoteUrl = settings.sourceLinkBaseUrl,
|
||||
remoteLineSuffix = settings.sourceLinkLineSuffix,
|
||||
)
|
||||
val analysisClasspaths = resolveDokkaAnalysisClasspaths(moduleCompileClasspath.resolvedFiles)
|
||||
|
||||
fun sourceSet(
|
||||
directoryName: String,
|
||||
sourceSetName: String,
|
||||
displayName: String,
|
||||
analysisPlatform: String,
|
||||
dependentOnCommon: Boolean,
|
||||
classpath: List<String> = emptyList(),
|
||||
): DokkaSourceSet? {
|
||||
val sourceRoot = normalizedRoot.resolve(directoryName)
|
||||
if (!sourceRoot.exists()) return null
|
||||
return DokkaSourceSet(
|
||||
sourceSetID = SourceSetId(settings.moduleName, sourceSetName),
|
||||
displayName = displayName,
|
||||
dependentSourceSets = if (dependentOnCommon) listOf(commonId) else emptyList(),
|
||||
classpath = classpath,
|
||||
sourceRoots = listOf(sourceRoot.pathString),
|
||||
skipDeprecated = settings.skipDeprecated,
|
||||
jdkVersion = if (displayName == "JVM") 8 else 17,
|
||||
analysisPlatform = analysisPlatform,
|
||||
sourceLinks = listOf(sourceLink),
|
||||
)
|
||||
}
|
||||
|
||||
val androidClasspath = buildList {
|
||||
addAll(analysisClasspaths.jvm)
|
||||
val sdkRoot = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT")
|
||||
if (sdkRoot != null) {
|
||||
val androidJar = listOf("android-37.0", "android-37")
|
||||
.map { Path.of(sdkRoot).resolve("platforms/$it/android.jar") }
|
||||
.firstOrNull { Files.isRegularFile(it) }
|
||||
if (androidJar != null) add(androidJar.pathString)
|
||||
}
|
||||
}
|
||||
|
||||
val sourceSets = listOfNotNull(
|
||||
sourceSet(
|
||||
"src",
|
||||
"commonMain",
|
||||
"Common",
|
||||
"common",
|
||||
dependentOnCommon = false,
|
||||
classpath = analysisClasspaths.commonNative,
|
||||
),
|
||||
sourceSet(
|
||||
"src@jvm",
|
||||
"jvmMain",
|
||||
"JVM",
|
||||
"jvm",
|
||||
dependentOnCommon = true,
|
||||
classpath = analysisClasspaths.jvm,
|
||||
),
|
||||
sourceSet("src@android", "androidMain", "Android", "jvm", dependentOnCommon = true, classpath = androidClasspath),
|
||||
sourceSet("src@js", "jsMain", "JS", "js", dependentOnCommon = true, classpath = analysisClasspaths.js),
|
||||
sourceSet(
|
||||
"src@wasmJs",
|
||||
"wasmJsMain",
|
||||
"WasmJS",
|
||||
"js",
|
||||
dependentOnCommon = true,
|
||||
classpath = analysisClasspaths.wasmJs,
|
||||
),
|
||||
sourceSet(
|
||||
"src@native",
|
||||
"nativeMain",
|
||||
"Native",
|
||||
"native",
|
||||
dependentOnCommon = true,
|
||||
classpath = analysisClasspaths.commonNative,
|
||||
),
|
||||
)
|
||||
|
||||
val configuration = DokkaConfiguration(
|
||||
moduleName = settings.moduleName,
|
||||
moduleVersion = settings.moduleVersion,
|
||||
outputDir = normalizedOutput.pathString,
|
||||
sourceSets = sourceSets,
|
||||
pluginsClasspath = pluginsClasspath.resolvedFiles.map { it.pathString },
|
||||
pluginsConfiguration = listOf(
|
||||
PluginConfiguration(
|
||||
fqPluginName = "org.jetbrains.dokka.base.DokkaBase",
|
||||
serializationFormat = "JSON",
|
||||
values = Json.encodeToString(DokkaBase(footerMessage = "KSLog ${settings.moduleVersion}")),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
val configurationFile = Files.createTempFile("kslog-dokka-", ".json")
|
||||
try {
|
||||
configurationFile.writeText(Json.encodeToString(configuration))
|
||||
val cliJar = runtimeClasspath.resolvedFiles.single()
|
||||
val commandLine = listOf(
|
||||
ProcessHandle.current().info().command().orElse("java"),
|
||||
"-jar",
|
||||
cliJar.pathString,
|
||||
configurationFile.pathString,
|
||||
)
|
||||
val process = ProcessBuilder(commandLine)
|
||||
.directory(normalizedRoot.toFile())
|
||||
.start()
|
||||
val capture = listOf(
|
||||
thread { process.inputStream.copyTo(System.out) },
|
||||
thread { process.errorStream.copyTo(System.err) },
|
||||
)
|
||||
val exitCode = process.waitFor()
|
||||
capture.forEach { it.join() }
|
||||
check(exitCode == 0) { "Dokka terminated with exit code $exitCode" }
|
||||
requireNoUnresolvedTypes(normalizedOutput)
|
||||
} finally {
|
||||
configurationFile.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
internal data class DokkaAnalysisClasspaths(
|
||||
val commonNative: List<String>,
|
||||
val jvm: List<String>,
|
||||
val js: List<String>,
|
||||
val wasmJs: List<String>,
|
||||
)
|
||||
|
||||
internal fun resolveDokkaAnalysisClasspaths(moduleClasspath: List<Path>): DokkaAnalysisClasspaths {
|
||||
val kotlinStdlib = checkNotNull(
|
||||
moduleClasspath.firstOrNull { path ->
|
||||
val version = path.parent?.fileName?.toString()
|
||||
version != null &&
|
||||
path.parent?.parent?.fileName?.toString() == "kotlin-stdlib" &&
|
||||
path.fileName?.toString() == "kotlin-stdlib-$version.jar"
|
||||
}
|
||||
) {
|
||||
"Cannot locate the Kotlin JVM stdlib in the module compile classpath; " +
|
||||
"it is required to discover the matching JS and WasmJS analysis KLIBs"
|
||||
}
|
||||
val versionDirectory = checkNotNull(kotlinStdlib.parent)
|
||||
val version = versionDirectory.fileName.toString()
|
||||
val kotlinGroupDirectory = checkNotNull(versionDirectory.parent?.parent)
|
||||
val mavenCacheDirectory = checkNotNull(kotlinGroupDirectory.parent?.parent?.parent)
|
||||
check(mavenCacheDirectory.fileName.toString() == ".m2.cache") {
|
||||
"Unexpected Kotlin Toolchain Maven cache layout for $kotlinStdlib"
|
||||
}
|
||||
val transformedStdlibDirectory = checkNotNull(mavenCacheDirectory.parent)
|
||||
.resolve("kotlin/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin/kotlin-stdlib/$version")
|
||||
val commonStdlib = if (Files.isDirectory(transformedStdlibDirectory)) {
|
||||
Files.walk(transformedStdlibDirectory, 2).use { paths ->
|
||||
paths
|
||||
.filter { Files.isRegularFile(it) }
|
||||
.filter { it.fileName.toString() == "kotlin-stdlib-commonMain-$version.klib" }
|
||||
.sorted()
|
||||
.findFirst()
|
||||
.orElse(null)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun platformStdlib(artifactId: String): Path = kotlinGroupDirectory
|
||||
.resolve(artifactId)
|
||||
.resolve(version)
|
||||
.resolve("$artifactId-$version.klib")
|
||||
|
||||
val jsStdlib = platformStdlib("kotlin-stdlib-js")
|
||||
val wasmJsStdlib = platformStdlib("kotlin-stdlib-wasm-js")
|
||||
val missing = listOfNotNull(jsStdlib, wasmJsStdlib).filterNot(Files::isRegularFile)
|
||||
check(commonStdlib != null && missing.isEmpty()) {
|
||||
"Missing Kotlin analysis KLIB(s) for Dokka below $transformedStdlibDirectory or in " +
|
||||
"${missing.joinToString()}. " +
|
||||
"Run `./kotlin build -m kslog -p js -p wasmJs` before documentation generation."
|
||||
}
|
||||
|
||||
return DokkaAnalysisClasspaths(
|
||||
commonNative = listOf(commonStdlib.pathString),
|
||||
jvm = moduleClasspath.map { it.pathString },
|
||||
js = listOf(jsStdlib.pathString),
|
||||
wasmJs = listOf(wasmJsStdlib.pathString),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun requireNoUnresolvedTypes(outputDir: Path) {
|
||||
val unresolvedFiles = Files.walk(outputDir).use { paths ->
|
||||
paths
|
||||
.filter { Files.isRegularFile(it) }
|
||||
.filter { path ->
|
||||
path.fileName.toString().substringAfterLast('.', "") in setOf("html", "json", "md")
|
||||
}
|
||||
.filter { Files.readString(it).contains("Error class: unknown class") }
|
||||
.map { outputDir.relativize(it).pathString }
|
||||
.sorted()
|
||||
.toList()
|
||||
}
|
||||
check(unresolvedFiles.isEmpty()) {
|
||||
"Dokka generated unresolved type placeholders in ${unresolvedFiles.size} file(s): " +
|
||||
unresolvedFiles.joinToString(limit = 10)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
internal fun prepareDokkaOutput(projectRootDir: String, outputDir: Path): Path {
|
||||
val configuredProjectRoot = Path.of(projectRootDir).toAbsolutePath().normalize()
|
||||
require(Files.isDirectory(configuredProjectRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||
"Project root must be a real directory, not a symbolic link: $configuredProjectRoot"
|
||||
}
|
||||
|
||||
val configuredGeneratedRoot = configuredProjectRoot.resolve("build/dokka").normalize()
|
||||
val configuredOutput = outputDir.toAbsolutePath().normalize()
|
||||
require(configuredOutput.parent == configuredGeneratedRoot) {
|
||||
"Refusing to clean Dokka output outside a direct child of $configuredGeneratedRoot: $configuredOutput"
|
||||
}
|
||||
|
||||
val realProjectRoot = configuredProjectRoot.toRealPath()
|
||||
val relativeOutput = configuredProjectRoot.relativize(configuredOutput)
|
||||
val generatedRoot = realProjectRoot.resolve("build/dokka").normalize()
|
||||
val output = realProjectRoot.resolve(relativeOutput).normalize()
|
||||
require(output.parent == generatedRoot) {
|
||||
"Resolved Dokka output escaped $generatedRoot: $output"
|
||||
}
|
||||
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
Files.createDirectories(generatedRoot)
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
|
||||
val realGeneratedRoot = generatedRoot.toRealPath()
|
||||
require(realGeneratedRoot.startsWith(realProjectRoot)) {
|
||||
"Resolved Dokka root escaped the project: $realGeneratedRoot"
|
||||
}
|
||||
|
||||
if (Files.exists(output, LinkOption.NOFOLLOW_LINKS)) {
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
val realOutput = output.toRealPath()
|
||||
require(realOutput.parent == realGeneratedRoot) {
|
||||
"Resolved Dokka output escaped $realGeneratedRoot: $realOutput"
|
||||
}
|
||||
output.deleteRecursively()
|
||||
}
|
||||
|
||||
output.createDirectories()
|
||||
requireNoSymbolicLinkAncestors(realProjectRoot, output)
|
||||
val createdOutput = output.toRealPath()
|
||||
require(createdOutput.parent == realGeneratedRoot) {
|
||||
"Created Dokka output escaped $realGeneratedRoot: $createdOutput"
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
private fun requireNoSymbolicLinkAncestors(root: Path, target: Path) {
|
||||
require(target.startsWith(root)) { "Path is outside the project root: $target" }
|
||||
var current = root
|
||||
for (component in root.relativize(target)) {
|
||||
current = current.resolve(component)
|
||||
require(!Files.isSymbolicLink(current)) {
|
||||
"Refusing Dokka cleanup through symbolic link: $current"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package dev.inmo.kslog.build.dokka
|
||||
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.ExperimentalPathApi
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteRecursively
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.io.path.writeText
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalPathApi::class)
|
||||
class RunDokkaOutputGuardTest {
|
||||
@Test
|
||||
fun rejectsSymlinkedDokkaAncestorWithoutTouchingOutsideFiles() {
|
||||
val projectRoot = Files.createTempDirectory("kslog-dokka-project-")
|
||||
val outside = Files.createTempDirectory("kslog-dokka-outside-")
|
||||
val sentinel = outside.resolve("sentinel.txt")
|
||||
sentinel.writeText("preserve me")
|
||||
val build = projectRoot.resolve("build").createDirectories()
|
||||
val dokkaLink = build.resolve("dokka")
|
||||
Files.createSymbolicLink(dokkaLink, outside)
|
||||
|
||||
try {
|
||||
val failure = assertFailsWith<IllegalArgumentException> {
|
||||
prepareDokkaOutput(projectRoot.toString(), dokkaLink.resolve("html"))
|
||||
}
|
||||
assertTrue(failure.message.orEmpty().contains("symbolic link"))
|
||||
assertEquals("preserve me", sentinel.readText())
|
||||
} finally {
|
||||
Files.deleteIfExists(dokkaLink)
|
||||
projectRoot.deleteRecursively()
|
||||
outside.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleansOnlyTheSelectedGeneratedOutputDirectory() {
|
||||
val projectRoot = Files.createTempDirectory("kslog-dokka-project-")
|
||||
val html = projectRoot.resolve("build/dokka/html").createDirectories()
|
||||
val stale = html.resolve("stale.txt")
|
||||
stale.writeText("old")
|
||||
val gfmSentinel = projectRoot.resolve("build/dokka/gfm/sentinel.txt")
|
||||
gfmSentinel.parent.createDirectories()
|
||||
gfmSentinel.writeText("keep")
|
||||
|
||||
try {
|
||||
val prepared = prepareDokkaOutput(projectRoot.toString(), html)
|
||||
assertEquals(html.toRealPath(), prepared.toRealPath())
|
||||
assertTrue(prepared.exists())
|
||||
assertFalse(stale.exists())
|
||||
assertEquals("keep", gfmSentinel.readText())
|
||||
} finally {
|
||||
projectRoot.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsGeneratedDocumentationWithUnresolvedTypePlaceholders() {
|
||||
val output = Files.createTempDirectory("kslog-dokka-output-")
|
||||
val generatedPage = output.resolve("broken.md")
|
||||
generatedPage.writeText("fun example(): <Error class: unknown class>")
|
||||
|
||||
try {
|
||||
val failure = assertFailsWith<IllegalStateException> {
|
||||
requireNoUnresolvedTypes(output)
|
||||
}
|
||||
assertTrue(failure.message.orEmpty().contains("broken.md"))
|
||||
} finally {
|
||||
output.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolvesMatchingCommonJsAndWasmAnalysisKlibsFromTheToolchainCache() {
|
||||
val cache = Files.createTempDirectory("kslog-dokka-cache-")
|
||||
val kotlinGroup = cache.resolve(".m2.cache/org/jetbrains/kotlin")
|
||||
val version = "2.4.20"
|
||||
val kotlinStdlib = kotlinGroup
|
||||
.resolve("kotlin-stdlib/$version/kotlin-stdlib-$version.jar")
|
||||
val commonStdlib = cache.resolve(
|
||||
"kotlin/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin/" +
|
||||
"kotlin-stdlib/$version/test-hash/kotlin-stdlib-commonMain-$version.klib"
|
||||
)
|
||||
val jsStdlib = kotlinGroup
|
||||
.resolve("kotlin-stdlib-js/$version/kotlin-stdlib-js-$version.klib")
|
||||
val wasmJsStdlib = kotlinGroup
|
||||
.resolve("kotlin-stdlib-wasm-js/$version/kotlin-stdlib-wasm-js-$version.klib")
|
||||
val dependency = cache.resolve("dependency.jar")
|
||||
listOf(kotlinStdlib, commonStdlib, jsStdlib, wasmJsStdlib).forEach {
|
||||
it.parent.createDirectories()
|
||||
it.writeText("fixture")
|
||||
}
|
||||
dependency.writeText("fixture")
|
||||
|
||||
try {
|
||||
val classpaths = resolveDokkaAnalysisClasspaths(listOf(kotlinStdlib, dependency))
|
||||
assertEquals(listOf(commonStdlib.toString()), classpaths.commonNative)
|
||||
assertEquals(listOf(kotlinStdlib.toString(), dependency.toString()), classpaths.jvm)
|
||||
assertEquals(listOf(jsStdlib.toString()), classpaths.js)
|
||||
assertEquals(listOf(wasmJsStdlib.toString()), classpaths.wasmJs)
|
||||
} finally {
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# KSLog GitHub release Toolchain plugin
|
||||
|
||||
Kotlin Toolchain 0.13.0 has no built-in GitHub release task. This local plugin
|
||||
keeps the existing explicit release capability as a narrow wrapper around the
|
||||
standard `gh release create` CLI.
|
||||
|
||||
It extracts the matching version section from `CHANGELOG.md`, preserves the
|
||||
legacy tag/title/target conventions, accepts the existing
|
||||
`GITHUB_RELEASE_TOKEN` variable plus standard `GH_TOKEN`/`GITHUB_TOKEN`, and
|
||||
supports a non-network `KSLOG_GITHUB_RELEASE_DRY_RUN=true` validation mode. It
|
||||
runs the committed release/version consistency preflight before rendering or
|
||||
creating a release. No CI workflow invokes it.
|
||||
|
||||
`gh release create` does not modify local git state, but GitHub may create the
|
||||
remote tag at `--target` when the requested tag does not already exist. A real
|
||||
invocation therefore mutates both the remote release and potentially the remote
|
||||
tag; dry-run mode performs neither action.
|
||||
@@ -0,0 +1,15 @@
|
||||
product: jvm/amper-plugin
|
||||
|
||||
description: Explicit GitHub release command backed by the standard GitHub CLI.
|
||||
|
||||
pluginInfo:
|
||||
id: github-release
|
||||
settingsClass: dev.inmo.kslog.build.release.GitHubReleaseSettings
|
||||
|
||||
settings:
|
||||
jvm:
|
||||
jdk:
|
||||
version: 17
|
||||
release: 17
|
||||
kotlin:
|
||||
version: 2.4.20
|
||||
@@ -0,0 +1,9 @@
|
||||
tasks:
|
||||
release:
|
||||
action: !dev.inmo.kslog.build.release.githubRelease
|
||||
projectRootDir: ${project.rootDir}
|
||||
settings: ${pluginSettings}
|
||||
|
||||
commands:
|
||||
- name: githubRelease
|
||||
performedBy: release
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.inmo.kslog.build.release
|
||||
|
||||
import org.jetbrains.amper.plugins.ExecutionAvoidance
|
||||
import org.jetbrains.amper.plugins.TaskAction
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.isRegularFile
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.writeText
|
||||
|
||||
@TaskAction(ExecutionAvoidance.Disabled)
|
||||
fun githubRelease(
|
||||
projectRootDir: String,
|
||||
settings: GitHubReleaseSettings,
|
||||
) {
|
||||
val root = Path.of(projectRootDir).toAbsolutePath().normalize()
|
||||
val changelog = root.resolve(settings.changelogFile).normalize()
|
||||
require(changelog.startsWith(root) && changelog.isRegularFile()) {
|
||||
"Changelog must be an existing file inside the module: $changelog"
|
||||
}
|
||||
|
||||
val releasePreflight = root.resolve("verification/check_release_consistency.py")
|
||||
require(releasePreflight.isRegularFile()) { "Missing release consistency preflight: $releasePreflight" }
|
||||
runCommand(root, listOf("python3", releasePreflight.pathString))
|
||||
|
||||
val parser = root.resolve("changelog_parser.sh")
|
||||
require(parser.isRegularFile()) { "Missing changelog parser: $parser" }
|
||||
val notes = runCommand(root, listOf("bash", parser.pathString, settings.version, changelog.pathString))
|
||||
.trim()
|
||||
require(notes.isNotEmpty()) { "No CHANGELOG.md section found for ${settings.version}" }
|
||||
|
||||
val tag = "${settings.tagPrefix}${settings.version}"
|
||||
val targetCommitish = settings.targetCommitish.ifBlank { settings.version }
|
||||
val repository = "${settings.owner}/${settings.repository}"
|
||||
if (System.getenv("KSLOG_GITHUB_RELEASE_DRY_RUN").toBoolean()) {
|
||||
println("Dry run: GitHub release $repository tag=$tag target=$targetCommitish title=${settings.version}")
|
||||
println(notes)
|
||||
return
|
||||
}
|
||||
|
||||
val token = System.getenv("GITHUB_RELEASE_TOKEN")
|
||||
?: System.getenv("GH_TOKEN")
|
||||
?: System.getenv("GITHUB_TOKEN")
|
||||
?: error("Set GITHUB_RELEASE_TOKEN, GH_TOKEN, or GITHUB_TOKEN before creating a release")
|
||||
|
||||
val notesFile = Files.createTempFile("kslog-release-notes-", ".md")
|
||||
try {
|
||||
notesFile.writeText(notes)
|
||||
val process = ProcessBuilder(
|
||||
"gh",
|
||||
"release",
|
||||
"create",
|
||||
tag,
|
||||
"--repo",
|
||||
repository,
|
||||
"--title",
|
||||
settings.version,
|
||||
"--target",
|
||||
targetCommitish,
|
||||
"--notes-file",
|
||||
notesFile.pathString,
|
||||
)
|
||||
.directory(root.toFile())
|
||||
.redirectErrorStream(true)
|
||||
process.environment()["GH_TOKEN"] = token
|
||||
val started = process.start()
|
||||
val output = started.inputStream.bufferedReader().use { it.readText() }
|
||||
val exitCode = started.waitFor()
|
||||
check(exitCode == 0) { "gh release create failed with exit code $exitCode:\n$output" }
|
||||
print(output)
|
||||
} finally {
|
||||
notesFile.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommand(workingDirectory: Path, command: List<String>): String {
|
||||
val process = ProcessBuilder(command)
|
||||
.directory(workingDirectory.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }
|
||||
val exitCode = process.waitFor()
|
||||
check(exitCode == 0) { "Command failed with exit code $exitCode: ${command.first()}\n$output" }
|
||||
return output
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.kslog.build.release
|
||||
|
||||
import org.jetbrains.amper.plugins.Configurable
|
||||
|
||||
@Configurable
|
||||
interface GitHubReleaseSettings {
|
||||
val owner: String
|
||||
val repository: String
|
||||
val version: String
|
||||
val tagPrefix: String get() = "v"
|
||||
val targetCommitish: String get() = ""
|
||||
val changelogFile: String get() = "CHANGELOG.md"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
modules:
|
||||
- kslog
|
||||
- plugins/dokka
|
||||
- plugins/github-release
|
||||
|
||||
plugins:
|
||||
- //plugins/dokka
|
||||
- //plugins/github-release
|
||||
+54
-59
@@ -1,3 +1,4 @@
|
||||
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
@@ -19,70 +20,47 @@ publishing {
|
||||
}
|
||||
|
||||
developers {
|
||||
|
||||
developer {
|
||||
id = "Akkihi"
|
||||
name = "Akkihi"
|
||||
email = "akkihiguy@gmail.com"
|
||||
}
|
||||
|
||||
developer {
|
||||
id = "Akkihi"
|
||||
name = "Akkihi"
|
||||
email = "akkihiguy@gmail.com"
|
||||
}
|
||||
|
||||
developer {
|
||||
id = "InsanusMokrassar"
|
||||
name = "InsanusMokrassar"
|
||||
email = "ovsyannikov.alexey95@gmail.com"
|
||||
}
|
||||
|
||||
developer {
|
||||
id = "InsanusMokrassar"
|
||||
name = "InsanusMokrassar"
|
||||
email = "ovsyannikov.alexey95@gmail.com"
|
||||
}
|
||||
}
|
||||
|
||||
licenses {
|
||||
|
||||
license {
|
||||
name = "MIT License"
|
||||
url = "https://opensource.org/licenses/MIT"
|
||||
}
|
||||
|
||||
license {
|
||||
name = "MIT License"
|
||||
url = "https://opensource.org/licenses/MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "GithubPackages"
|
||||
url = uri("https://maven.pkg.github.com/InsanusMokrassar/KSLog")
|
||||
|
||||
credentials {
|
||||
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
|
||||
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
|
||||
}
|
||||
|
||||
}
|
||||
repositories {
|
||||
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "InmoNexus"
|
||||
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
|
||||
|
||||
credentials {
|
||||
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
|
||||
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
|
||||
}
|
||||
}
|
||||
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
|
||||
maven {
|
||||
name = "Gitea"
|
||||
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
|
||||
|
||||
credentials(HttpHeaderCredentials) {
|
||||
name = "Authorization"
|
||||
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
|
||||
}
|
||||
|
||||
authentication {
|
||||
header(HttpHeaderAuthentication)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "sonatype"
|
||||
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
|
||||
|
||||
credentials {
|
||||
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||
}
|
||||
|
||||
}
|
||||
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
|
||||
maven {
|
||||
name = "sonatype"
|
||||
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
|
||||
|
||||
credentials {
|
||||
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
|
||||
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,13 +69,13 @@ publishing {
|
||||
|
||||
if (project.hasProperty("signing.gnupg.keyName")) {
|
||||
apply plugin: 'signing'
|
||||
|
||||
|
||||
signing {
|
||||
useGpgCmd()
|
||||
|
||||
|
||||
sign publishing.publications
|
||||
}
|
||||
|
||||
|
||||
task signAll {
|
||||
tasks.withType(Sign).forEach {
|
||||
dependsOn(it)
|
||||
@@ -109,4 +87,21 @@ if (project.hasProperty("signing.gnupg.keyName")) {
|
||||
def signingTasks = project.getTasks().withType(Sign.class)
|
||||
mustRunAfter(signingTasks)
|
||||
}
|
||||
// Workaround to make test tasks use sign
|
||||
project.getTasks().withType(Sign.class).configureEach { signTask ->
|
||||
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
|
||||
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
|
||||
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
|
||||
|
||||
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
|
||||
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
|
||||
if (debugTestTask != null) {
|
||||
signTask.mustRunAfter(debugTestTask)
|
||||
}
|
||||
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
|
||||
def testTask = tasks.findByName("compileTestKotlin$pubName")
|
||||
if (testTask != null) {
|
||||
signTask.mustRunAfter(testTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"licenses":[{"id":"MIT","title":"MIT License","url":"https://opensource.org/licenses/MIT"}],"mavenConfig":{"name":"${project.name}","description":"Simple logging library for kotlin","url":"https://github.com/InsanusMokrassar/KSLog","vcsUrl":"https://github.com/InsanusMokrassar/KSLog.git","developers":[{"id":"Akkihi","name":"Akkihi","eMail":"akkihiguy@gmail.com"},{"id":"InsanusMokrassar","name":"InsanusMokrassar","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/KSLog"},{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
|
||||
{"licenses":[{"id":"MIT","title":"MIT License","url":"https://opensource.org/licenses/MIT"}],"mavenConfig":{"name":"${project.name}","description":"Simple logging library for kotlin","url":"https://github.com/InsanusMokrassar/KSLog","vcsUrl":"https://github.com/InsanusMokrassar/KSLog.git","developers":[{"id":"Akkihi","name":"Akkihi","eMail":"akkihiguy@gmail.com"},{"id":"InsanusMokrassar","name":"InsanusMokrassar","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}
|
||||
@@ -1,175 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
import dev.inmo.kslog.common.filter.filtered
|
||||
import dev.inmo.kslog.common.utils.plus
|
||||
|
||||
|
||||
enum class LogLevel {
|
||||
TRACE,
|
||||
DEBUG,
|
||||
VERBOSE,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
ASSERT,
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for any logger
|
||||
*
|
||||
* @see default
|
||||
*
|
||||
*/
|
||||
interface KSLog {
|
||||
/**
|
||||
* The only one function required to realization in any inheritor. All other [performLog] functions
|
||||
* will call this one by default
|
||||
*/
|
||||
fun performLog(level: LogLevel, tag: String?, message: Any, throwable: Throwable?)
|
||||
|
||||
/**
|
||||
* Calls default [performLog] with `tag` == `null`
|
||||
*/
|
||||
fun performLog(level: LogLevel, message: Any, throwable: Throwable?) = performLog(level, null, message, throwable)
|
||||
/**
|
||||
* Calls default [performLog] with `message` built using [messageBuilder]. This method supposed to be overriden in
|
||||
* case when logger supports lazy-loaded messages (like [dev.inmo.kslog.common.filter.FilterKSLog])
|
||||
*/
|
||||
fun performLog(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: () -> Any
|
||||
) = performLog(level, tag, messageBuilder(), throwable)
|
||||
|
||||
/**
|
||||
* Suspendable variant of [performLog] with [messageBuilder]. Uses default [performLog] with `message` built using
|
||||
* [messageBuilder] by default
|
||||
*/
|
||||
suspend fun performLogS(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: suspend () -> Any
|
||||
) = performLog(level, tag, messageBuilder(), throwable)
|
||||
|
||||
|
||||
companion object : KSLog {
|
||||
private var defaultLogger: KSLog? = null
|
||||
|
||||
/**
|
||||
* Default logger used in case you are trying to use [KSLog] as a receiver for extensions like [info]
|
||||
*/
|
||||
var default: KSLog
|
||||
get() {
|
||||
return defaultLogger ?: KSLog("app").also {
|
||||
defaultLogger = it
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
defaultLogger = value
|
||||
}
|
||||
override fun performLog(level: LogLevel, tag: String?, message: Any, throwable: Throwable?) = default.performLog(level, tag, message, throwable)
|
||||
override fun performLog(level: LogLevel, message: Any, throwable: Throwable?) = default.performLog(level, message, throwable)
|
||||
override fun performLog(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: () -> Any
|
||||
) = default.performLog(level, tag, throwable, messageBuilder)
|
||||
override suspend fun performLogS(
|
||||
level: LogLevel,
|
||||
tag: String?,
|
||||
throwable: Throwable?,
|
||||
messageBuilder: suspend () -> Any
|
||||
) = default.performLogS(level, tag, throwable, messageBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
operator fun KSLog.invoke(performLogCallback: (level: LogLevel, tag: String?, message: Any, throwable: Throwable?) -> Unit) = CallbackKSLog(performLogCallback)
|
||||
|
||||
internal val printlnLogging: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, t, m, e ->
|
||||
println(defaultMessageFormatter(l, t, m, e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple builder for [DefaultKSLog] logger based on [defaultTag]
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
): KSLog = DefaultKSLog(defaultTag)
|
||||
|
||||
/**
|
||||
* Simple builder for [DefaultKSLog] logger based on [defaultTag] and [messageFormatter]
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
messageFormatter: MessageFormatter
|
||||
): KSLog = DefaultKSLog(
|
||||
defaultTag,
|
||||
messageFormatter
|
||||
)
|
||||
|
||||
@Deprecated("Filtering should be replaced with FilterKSLog")
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
filter: MessageFilter,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog = KSLog (
|
||||
defaultTag,
|
||||
messageFormatter
|
||||
).filtered(filter)
|
||||
|
||||
/**
|
||||
* Building logger using [KSLog] builder based on [defaultTag] and [messageFormatter]. This logger will also filter
|
||||
* incoming levels: only levels passed in [levels] param will be logged
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
levels: Iterable<LogLevel>,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog {
|
||||
val levelsSet = levels.toSet()
|
||||
return KSLog (defaultTag, messageFormatter).filtered { l, _, _ ->
|
||||
l in levelsSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Building logger using [KSLog] builder based on [defaultTag] and [messageFormatter]. This logger will also filter
|
||||
* incoming levels: only levels passed in [firstLevel], [secondLevel] and [otherLevels] param will be logged
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
firstLevel: LogLevel,
|
||||
secondLevel: LogLevel,
|
||||
vararg otherLevels: LogLevel,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter,
|
||||
): KSLog = KSLog(defaultTag, setOf(firstLevel, secondLevel, *otherLevels), messageFormatter)
|
||||
|
||||
/**
|
||||
* Building logger using [KSLog] builder based on [defaultTag] and [messageFormatter]. This logger will also filter
|
||||
* incoming levels: only levels above [minLoggingLevel] will be logged
|
||||
*/
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
minLoggingLevel: LogLevel,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter,
|
||||
): KSLog = KSLog (
|
||||
defaultTag,
|
||||
messageFormatter
|
||||
).filtered { l, _, _ ->
|
||||
minLoggingLevel.ordinal <= l.ordinal
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting [KSLog.default] logger to [newDefault]
|
||||
*/
|
||||
fun setDefaultKSLog(newDefault: KSLog) { KSLog.default = newDefault }
|
||||
/**
|
||||
* Setting [KSLog.default] logger to new [CallbackKSLog] using [plus] operation
|
||||
*/
|
||||
fun addDefaultKSLog(newDefault: KSLog) {
|
||||
KSLog.default += newDefault
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
typealias MessageFormatter = (l: LogLevel, t: String?, m: Any, Throwable?) -> String
|
||||
inline fun MessageFormatter(noinline formatter: MessageFormatter) = formatter
|
||||
typealias MessageFilter = (l: LogLevel, t: String?, Throwable?) -> Boolean
|
||||
val defaultMessageFormatter: MessageFormatter = { l, t, m, _ -> "[$l] ${t ?.let { "$it " } ?: ""}- $m" }
|
||||
val defaultMessageFormatterWithErrorPrint: MessageFormatter = { l, t, m, e -> "[$l] ${t ?.let { "$it " } ?: ""}- $m${e ?.let { ": ${it.stackTraceToString()}" } ?: ""}" }
|
||||
@@ -1,7 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
/**
|
||||
* Default logger for current platform. You may change it, but you should remember that it is global logger
|
||||
* used in [KSLog.default] by default
|
||||
*/
|
||||
expect var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit
|
||||
@@ -1,11 +0,0 @@
|
||||
package dev.inmo.kslog.common.filter
|
||||
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.MessageFilter
|
||||
|
||||
/**
|
||||
* Creates [FilterKSLog] with applying of [filter] to it
|
||||
*/
|
||||
fun KSLog.filtered(
|
||||
filter: MessageFilter
|
||||
) = FilterKSLog(this, filter)
|
||||
@@ -1,34 +0,0 @@
|
||||
package dev.inmo.kslog.common.typed
|
||||
|
||||
import dev.inmo.kslog.common.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Special builder for [TypedKSLog]
|
||||
*/
|
||||
class TypedKSLogBuilder(
|
||||
private val preset: Map<KClass<*>?, KSLog> = emptyMap()
|
||||
) {
|
||||
private val loggers = mutableMapOf<KClass<*>?, KSLog>().apply {
|
||||
putAll(preset)
|
||||
}
|
||||
|
||||
fun on(kClass: KClass<*>?, with: KSLog) = loggers.put(kClass, with)
|
||||
fun on(kClass: KClass<*>?, with: SimpleKSLogCallback) = loggers.put(kClass, CallbackKSLog(with))
|
||||
|
||||
fun default(with: KSLog) = on(null, with)
|
||||
fun default(with: SimpleKSLogCallback) = on(null, with)
|
||||
|
||||
inline fun <reified T: Any> on(with: KSLog) = on(T::class, with)
|
||||
inline fun <reified T: Any> on(noinline with: SimpleKSLogCallback) = on(T::class, with)
|
||||
|
||||
fun build() = TypedKSLog(loggers.toMap())
|
||||
}
|
||||
|
||||
/**
|
||||
* DSL for [TypedKSLogBuilder]
|
||||
*/
|
||||
inline fun buildTypedLogger(
|
||||
preset: Map<KClass<*>?, KSLog> = emptyMap(),
|
||||
block: TypedKSLogBuilder.() -> Unit
|
||||
) = TypedKSLogBuilder(preset).apply(block).build()
|
||||
@@ -1,19 +0,0 @@
|
||||
package dev.inmo.kslog.common.utils
|
||||
|
||||
import dev.inmo.kslog.common.CallbackKSLog
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
|
||||
/**
|
||||
* Will send [KSLog.performLog] of both [this] and [other] [KSLog] instances. In case when [this] will throw exception
|
||||
* result logger will rethrow it. After it, if [other] will throw exception - will also rethrow it
|
||||
*/
|
||||
infix operator fun KSLog.plus(other: KSLog) = CallbackKSLog { l, t, m, e ->
|
||||
val resultOfFirst = runCatching {
|
||||
this@plus.performLog(l, t, m, e)
|
||||
}
|
||||
val resultOfSecond = runCatching {
|
||||
other.performLog(l, t, m, e)
|
||||
}
|
||||
resultOfFirst.onFailure { throw it }
|
||||
resultOfSecond.onFailure { throw it }
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
import kotlin.js.Console
|
||||
|
||||
external interface ExtendedConsole : Console {
|
||||
fun trace()
|
||||
fun debug(vararg o: Any?)
|
||||
fun assert(assertion: Boolean, vararg o: Any?)
|
||||
}
|
||||
|
||||
/**
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/API/console/debug_static](https://developer.mozilla.org/en-US/docs/Web/API/console/debug_static)
|
||||
*/
|
||||
fun Console.debug(vararg args: Any?) = unsafeCast<ExtendedConsole>().debug(*args)
|
||||
|
||||
/**
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/APtraceI/console/assert_static](https://developer.mozilla.org/en-US/docs/Web/API/console/assert_static)
|
||||
*/
|
||||
fun Console.assert(assertion: Boolean, vararg args: Any?) = unsafeCast<ExtendedConsole>().assert(assertion, *args)
|
||||
|
||||
/**
|
||||
* [https://developer.mozilla.org/en-US/docs/Web/API/console/trace_static](https://developer.mozilla.org/en-US/docs/Web/API/console/trace_static)
|
||||
*/
|
||||
fun Console.trace() = unsafeCast<ExtendedConsole>().trace()
|
||||
@@ -1,19 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, _, m, e ->
|
||||
val args = e ?.let {
|
||||
arrayOf(m, e)
|
||||
} ?: arrayOf(m)
|
||||
when (l) {
|
||||
LogLevel.TRACE -> {
|
||||
console.trace()
|
||||
console.debug(*args)
|
||||
}
|
||||
LogLevel.DEBUG -> console.debug(*args)
|
||||
LogLevel.VERBOSE,
|
||||
LogLevel.INFO -> console.info(*args)
|
||||
LogLevel.WARNING -> console.warn(*args)
|
||||
LogLevel.ERROR -> console.error(*args)
|
||||
LogLevel.ASSERT -> console.error(*args)
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
import dev.inmo.kslog.common.filter.filtered
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
internal val defaultKSLogLogger by lazy {
|
||||
Logger.getLogger("KSLog")
|
||||
}
|
||||
internal fun Logger.doLog(
|
||||
l: LogLevel, t: String, m: String, e: Throwable?
|
||||
) = log(
|
||||
when(l) {
|
||||
LogLevel.TRACE -> Level.FINEST
|
||||
LogLevel.DEBUG -> Level.FINER
|
||||
LogLevel.VERBOSE -> Level.FINE
|
||||
LogLevel.INFO -> Level.INFO
|
||||
LogLevel.WARNING -> Level.WARNING
|
||||
LogLevel.ERROR -> Level.SEVERE
|
||||
LogLevel.ASSERT -> Level.SEVERE
|
||||
},
|
||||
m,
|
||||
e
|
||||
)
|
||||
|
||||
@Deprecated("Filtering should be replaced with FilterKSLog")
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
filter: MessageFilter,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
) = KSLog { l, t, m, e ->
|
||||
if (!filter(l, t, e)) return@KSLog
|
||||
val text = messageFormatter(l,t ?: defaultTag,m.toString(),e)
|
||||
logger.doLog(l, t ?: defaultTag, text, e)
|
||||
}
|
||||
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
) = KSLog { l, t, m, e ->
|
||||
val text = messageFormatter(l,t ?: defaultTag,m.toString(),e)
|
||||
logger.doLog(l, t ?: defaultTag, text, e)
|
||||
}
|
||||
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
levels: Iterable<LogLevel>,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog {
|
||||
val levelsSet = levels.toSet()
|
||||
return KSLog (defaultTag, logger, messageFormatter).filtered { l, _, _ -> l in levelsSet }
|
||||
}
|
||||
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
firstLevel: LogLevel,
|
||||
secondLevel: LogLevel,
|
||||
vararg otherLevels: LogLevel,
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter,
|
||||
): KSLog = KSLog (defaultTag, logger, setOf(firstLevel, secondLevel, *otherLevels), messageFormatter)
|
||||
|
||||
fun KSLog(
|
||||
defaultTag: String,
|
||||
logger: Logger,
|
||||
minLoggingLevel: LogLevel = LogLevel.values().first(),
|
||||
messageFormatter: MessageFormatter = defaultMessageFormatter
|
||||
): KSLog = KSLog (defaultTag, logger, messageFormatter).filtered { l, _, _ -> minLoggingLevel.ordinal <= l.ordinal }
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit = { l, t, m, e ->
|
||||
defaultKSLogLogger.doLog(l, t, m.toString(), e)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -1,4 +0,0 @@
|
||||
package dev.inmo.kslog.common
|
||||
|
||||
actual var KSLoggerDefaultPlatformLoggerLambda: (level: LogLevel, tag: String, message: Any, throwable: Throwable?) -> Unit =
|
||||
printlnLogging
|
||||
@@ -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))
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user