From b739c7782516c3c1aa619e355be8217aa9edbb15 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 22 Dec 2025 19:26:29 +0000 Subject: [PATCH 1/4] Bump version -> `2.0.0-SNAPSHOT.205` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 4f3d2b7..3f3af8d 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -29,4 +29,4 @@ * * For dependencies on Spine modules please see [io.spine.dependency.local.Spine]. */ -val versionToPublish by extra("2.0.0-SNAPSHOT.200") +val versionToPublish by extra("2.0.0-SNAPSHOT.205") From 255f69ff1b3e993dd0956ec54d8adc8374813464 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 22 Dec 2025 19:27:02 +0000 Subject: [PATCH 2/4] Update `config` --- .agents/_TOC.md | 16 + .agents/advanced-safety-rules.md | 6 + .agents/coding-guidelines.md | 39 ++ .agents/common-tasks.md | 6 + .agents/documentation-guidelines.md | 14 + .agents/documentation-tasks.md | 20 + .agents/java-kotlin-conversion.md | 43 ++ .agents/project-overview.md | 7 + .agents/project-structure-expectations.md | 21 + .agents/quick-reference-card.md | 10 + .agents/refactoring-guidelines.md | 3 + .agents/running-builds.md | 18 + .agents/safety-rules.md | 7 + .agents/testing.md | 8 + .agents/version-policy.md | 30 ++ .github/workflows/detekt-analysis.yml | 103 ----- .github/workflows/publish.yml | 2 +- .gitignore | 48 +- .idea/codeStyles/Project.xml | 3 +- .idea/dictionaries/common.xml | 4 +- .idea/inspectionProfiles/Project_Default.xml | 14 +- .idea/kotlinc.xml | 7 +- .junie/guidelines.md | 21 + AGENTS.md | 5 + CLAUDE.md | 17 + build.gradle.kts | 87 ++-- buildSrc/build.gradle.kts | 44 +- buildSrc/quality/checkstyle-suppressions.xml | 42 ++ buildSrc/quality/checkstyle.xml | 86 ++++ buildSrc/quality/detekt-config.yml | 35 ++ buildSrc/quality/pmd.xml | 97 ++++ ...-kotlin.gradle.kts => settings.gradle.kts} | 21 +- buildSrc/src/main/kotlin/BuildExtensions.kt | 145 ++++-- buildSrc/src/main/kotlin/BuildSettings.kt | 1 + .../src/main/kotlin/DependencyResolution.kt | 27 +- .../src/main/kotlin/DocumentationSettings.kt | 94 ++++ buildSrc/src/main/kotlin/DokkaExts.kt | 207 +++++---- buildSrc/src/main/kotlin/LicenseSettings.kt | 43 ++ .../kotlin/detekt-code-analysis.gradle.kts | 4 +- ...java.gradle.kts => dokka-setup.gradle.kts} | 24 +- .../kotlin/io/spine/dependency/Dependency.kt | 138 ++++++ .../spine/dependency/boms/Boms.kt} | 55 ++- .../io/spine/dependency/boms/BomsPlugin.kt | 185 ++++++++ .../kotlin/io/spine/dependency/build/Dokka.kt | 4 +- .../io/spine/dependency/build/GradleDoctor.kt | 2 +- .../io/spine/dependency/build/JSpecify.kt | 40 ++ .../kotlin/io/spine/dependency/build/Ksp.kt | 32 +- .../spine/dependency/build/LicenseReport.kt | 2 +- .../dependency/build/PluginPublishPlugin.kt | 42 ++ .../kotlin/io/spine/dependency/build/Pmd.kt | 9 +- .../io/spine/dependency/kotlinx/AtomicFu.kt | 54 +++ .../kotlinx/Coroutines.kt} | 36 +- .../{local/Text.kt => kotlinx/DateTime.kt} | 18 +- .../kotlinx/KotlinX.kt} | 18 +- .../spine/dependency/kotlinx/Serialization.kt | 73 +++ .../kotlin/io/spine/dependency/lib/Aedile.kt | 4 +- .../io/spine/dependency/lib/Caffeine.kt | 6 +- .../io/spine/dependency/lib/Coroutines.kt | 27 +- .../kotlin/io/spine/dependency/lib/Grpc.kt | 65 ++- .../io/spine/dependency/lib/GrpcKotlin.kt | 3 +- .../kotlin/io/spine/dependency/lib/Gson.kt | 2 +- .../kotlin/io/spine/dependency/lib/Guava.kt | 2 +- .../kotlin/io/spine/dependency/lib/Jackson.kt | 114 ++++- .../kotlin/io/spine/dependency/lib/JavaX.kt | 2 +- .../kotlin/io/spine/dependency/lib/Kotlin.kt | 76 +++- .../io/spine/dependency/lib/KotlinPoet.kt | 2 +- .../io/spine/dependency/lib/KotlinSemver.kt | 2 +- .../kotlin/io/spine/dependency/lib/KotlinX.kt | 28 +- .../dependency/lib/PalantirJavaFormat.kt | 43 ++ .../io/spine/dependency/lib/Protobuf.kt | 6 +- .../kotlin/io/spine/dependency/lib/Roaster.kt | 2 +- .../spine/dependency/local/ArtifactVersion.kt | 137 ------ .../kotlin/io/spine/dependency/local/Base.kt | 18 +- .../io/spine/dependency/local/BaseTypes.kt | 2 +- .../io/spine/dependency/local/Change.kt | 2 +- .../io/spine/dependency/local/Compiler.kt | 193 ++++++++ .../local/{CoreJava.kt => CoreJvm.kt} | 20 +- .../spine/dependency/local/CoreJvmCompiler.kt | 97 ++++ .../io/spine/dependency/local/Logging.kt | 2 +- .../io/spine/dependency/local/McJava.kt | 4 +- .../io/spine/dependency/local/ProtoData.kt | 11 +- .../io/spine/dependency/local/ProtoTap.kt | 2 +- .../io/spine/dependency/local/Reflect.kt | 2 +- .../kotlin/io/spine/dependency/local/Spine.kt | 83 +--- .../io/spine/dependency/local/TestLib.kt | 2 +- .../kotlin/io/spine/dependency/local/Time.kt | 5 +- .../io/spine/dependency/local/ToolBase.kt | 33 +- .../io/spine/dependency/local/Validation.kt | 29 +- .../io/spine/dependency/test/AssertK.kt | 2 +- .../io/spine/dependency/test/Hamcrest.kt | 2 +- .../kotlin/io/spine/dependency/test/JUnit.kt | 81 +++- .../kotlin/io/spine/dependency/test/Jacoco.kt | 2 +- .../kotlin/io/spine/dependency/test/Kotest.kt | 9 +- .../dependency/test/KotlinCompileTesting.kt | 2 +- .../kotlin/io/spine/dependency/test/Kover.kt | 2 +- .../kotlin/io/spine/dependency/test/Truth.kt | 2 +- .../io/spine/gradle/{Runtime.kt => Cli.kt} | 25 +- .../kotlin/io/spine/gradle/ConfigTester.kt | 2 +- .../io/spine/gradle/ProjectExtensions.kt | 11 +- .../kotlin/io/spine/gradle/Repositories.kt | 370 ---------------- .../kotlin/io/spine/gradle/VersionWriter.kt | 149 ------- .../gradle/checkstyle/CheckStyleConfig.kt | 2 +- .../kotlin/io/spine/gradle/fs/LazyTempPath.kt | 4 +- .../kotlin/io/spine/gradle/git/Repository.kt | 107 +++-- .../github/pages/RepositoryExtensions.kt | 12 +- .../io/spine/gradle/github/pages/SshKey.kt | 32 +- .../io/spine/gradle/github/pages/TaskName.kt | 12 +- .../io/spine/gradle/github/pages/Update.kt | 57 ++- .../gradle/github/pages/UpdateGitHubPages.kt | 94 ++-- .../pages/UpdateGitHubPagesExtension.kt | 72 +-- .../kotlin/io/spine/gradle/java/Linters.kt | 61 +++ .../gradle/javadoc/ExcludeInternalDoclet.kt | 10 +- .../io/spine/gradle/javadoc/JavadocConfig.kt | 31 +- .../io/spine/gradle/kotlin/KotlinConfig.kt | 6 +- .../gradle/protobuf/ProtoTaskExtensions.kt | 418 ------------------ .../gradle/publish/CheckVersionIncrement.kt | 20 +- .../gradle/publish/CloudArtifactRegistry.kt | 10 +- .../io/spine/gradle/publish/CloudRepo.kt | 5 +- .../publish/CustomPublicationHandler.kt | 70 +++ .../io/spine/gradle/publish/GitHubPackages.kt | 14 +- .../kotlin/io/spine/gradle/publish/JarDsl.kt | 106 +---- .../io/spine/gradle/publish/ProtoExts.kt | 10 +- .../gradle/publish/PublicationHandler.kt | 259 +++++++++++ .../io/spine/gradle/publish/Publications.kt | 234 ---------- .../io/spine/gradle/publish/PublishingExts.kt | 111 +++-- .../spine/gradle/publish/PublishingRepos.kt | 2 +- .../io/spine/gradle/publish/ShadowJarExts.kt | 77 ++++ .../spine/gradle/publish/SpinePublishing.kt | 297 ++++++------- .../publish/StandardJavaPublicationHandler.kt | 133 ++++++ .../Credentials.kt} | 13 +- .../io/spine/gradle/{ => repo}/RepoSlug.kt | 9 +- .../io/spine/gradle/repo/Repositories.kt | 178 ++++++++ .../kotlin/io/spine/gradle/repo/Repository.kt | 138 ++++++ .../gradle/report/coverage/CodebaseFilter.kt | 1 - .../gradle/report/coverage/JacocoConfig.kt | 8 +- .../gradle/report/license/LicenseReporter.kt | 27 +- .../report/license/ModuleDataExtensions.kt | 1 - .../spine/gradle/report/license/Template.kt | 17 +- .../gradle/report/pom/DependencyWriter.kt | 7 +- .../spine/gradle/report/pom/InceptionYear.kt | 7 +- .../spine/gradle/report/pom/PomFormatting.kt | 2 +- .../spine/gradle/report/pom/PomGenerator.kt | 10 +- .../spine/gradle/report/pom/PomXmlWriter.kt | 16 +- .../kotlin/io/spine/gradle/testing/Tasks.kt | 7 +- .../src/main/kotlin/jacoco-kmm-jvm.gradle.kts | 72 +++ .../src/main/kotlin/jvm-module.gradle.kts | 77 +--- .../src/main/kotlin/kmp-module.gradle.kts | 187 ++++++++ .../src/main/kotlin/kmp-publish.gradle.kts | 75 ++++ .../src/main/kotlin/module-testing.gradle.kts | 120 +++++ buildSrc/src/main/kotlin/module.gradle.kts | 31 ++ .../src/main/kotlin/pmd-settings.gradle.kts | 2 +- .../src/main/kotlin/test-module.gradle.kts | 57 +++ .../main/kotlin/uber-jar-module.gradle.kts | 202 +++++++++ change/build.gradle.kts | 4 +- .../java/io/spine/change/package-info.java | 4 +- .../java/io/spine/change/ChangesTest.java | 2 +- config | 2 +- gradle.properties | 51 +-- gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 45633 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 11 +- gradlew.bat | 3 +- 162 files changed, 4722 insertions(+), 2709 deletions(-) create mode 100644 .agents/_TOC.md create mode 100644 .agents/advanced-safety-rules.md create mode 100644 .agents/coding-guidelines.md create mode 100644 .agents/common-tasks.md create mode 100644 .agents/documentation-guidelines.md create mode 100644 .agents/documentation-tasks.md create mode 100644 .agents/java-kotlin-conversion.md create mode 100644 .agents/project-overview.md create mode 100644 .agents/project-structure-expectations.md create mode 100644 .agents/quick-reference-card.md create mode 100644 .agents/refactoring-guidelines.md create mode 100644 .agents/running-builds.md create mode 100644 .agents/safety-rules.md create mode 100644 .agents/testing.md create mode 100644 .agents/version-policy.md delete mode 100644 .github/workflows/detekt-analysis.yml create mode 100644 .junie/guidelines.md create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 buildSrc/quality/checkstyle-suppressions.xml create mode 100644 buildSrc/quality/checkstyle.xml create mode 100644 buildSrc/quality/detekt-config.yml create mode 100644 buildSrc/quality/pmd.xml rename buildSrc/{src/main/kotlin/dokka-for-kotlin.gradle.kts => settings.gradle.kts} (78%) create mode 100644 buildSrc/src/main/kotlin/DocumentationSettings.kt create mode 100644 buildSrc/src/main/kotlin/LicenseSettings.kt rename buildSrc/src/main/kotlin/{dokka-for-java.gradle.kts => dokka-setup.gradle.kts} (73%) create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt rename buildSrc/src/main/kotlin/{compile-protobuf.gradle.kts => io/spine/dependency/boms/Boms.kt} (57%) create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/build/JSpecify.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/build/PluginPublishPlugin.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/kotlinx/AtomicFu.kt rename buildSrc/src/main/kotlin/io/spine/{gradle/dokka/DokkaExtensions.kt => dependency/kotlinx/Coroutines.kt} (60%) rename buildSrc/src/main/kotlin/io/spine/dependency/{local/Text.kt => kotlinx/DateTime.kt} (77%) rename buildSrc/src/main/kotlin/io/spine/{gradle/javadoc/TaskContainerExtensions.kt => dependency/kotlinx/KotlinX.kt} (76%) create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/kotlinx/Serialization.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/lib/PalantirJavaFormat.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/local/ArtifactVersion.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt rename buildSrc/src/main/kotlin/io/spine/dependency/local/{CoreJava.kt => CoreJvm.kt} (69%) create mode 100644 buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt rename buildSrc/src/main/kotlin/io/spine/gradle/{Runtime.kt => Cli.kt} (83%) delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/Repositories.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/VersionWriter.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt delete mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/Publications.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt rename buildSrc/src/main/kotlin/io/spine/gradle/{dokka/TaskContainerExtensions.kt => repo/Credentials.kt} (83%) rename buildSrc/src/main/kotlin/io/spine/gradle/{ => repo}/RepoSlug.kt (88%) create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt create mode 100644 buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt create mode 100644 buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts create mode 100644 buildSrc/src/main/kotlin/kmp-module.gradle.kts create mode 100644 buildSrc/src/main/kotlin/kmp-publish.gradle.kts create mode 100644 buildSrc/src/main/kotlin/module-testing.gradle.kts create mode 100644 buildSrc/src/main/kotlin/module.gradle.kts create mode 100644 buildSrc/src/main/kotlin/test-module.gradle.kts create mode 100644 buildSrc/src/main/kotlin/uber-jar-module.gradle.kts diff --git a/.agents/_TOC.md b/.agents/_TOC.md new file mode 100644 index 0000000..065df13 --- /dev/null +++ b/.agents/_TOC.md @@ -0,0 +1,16 @@ +# Table of Contents + +1. [Quick Reference Card](quick-reference-card.md) +2. [Project overview](project-overview.md) +3. [Coding guidelines](coding-guidelines.md) +4. [Documentation & comments](documentation-guidelines.md) +5. [Documentation tasks](documentation-tasks.md) +6. [Running builds](running-builds.md) +7. [Version policy](version-policy.md) +8. [Project structure expectations](project-structure-expectations.md) +9. [Testing](testing.md) +10. [Safety rules](safety-rules.md) +11. [Advanced safety rules](advanced-safety-rules.md) +12. [Refactoring guidelines](refactoring-guidelines.md) +13. [Common tasks](common-tasks.md) +14. [Java to Kotlin conversion](java-kotlin-conversion.md) diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md new file mode 100644 index 0000000..e410581 --- /dev/null +++ b/.agents/advanced-safety-rules.md @@ -0,0 +1,6 @@ +# ๐Ÿšจ Advanced safety rules + +- Do **not** auto-update external dependencies without explicit request. +- Do **not** inject analytics or telemetry code. +- Flag any usage of unsafe constructs (e.g., reflection, I/O on the main thread). +- Avoid generating blocking calls inside coroutines. diff --git a/.agents/coding-guidelines.md b/.agents/coding-guidelines.md new file mode 100644 index 0000000..3297d8a --- /dev/null +++ b/.agents/coding-guidelines.md @@ -0,0 +1,39 @@ +# ๐Ÿงพ Coding guidelines + +## Core principles + +- Adhere to [Spine Event Engine Documentation][spine-docs] for coding style. +- Generate code that compiles cleanly and passes static analysis. +- Respect existing architecture, naming conventions, and project structure. +- Write clear, incremental commits with descriptive messages. +- Include automated tests for any code change that alters functionality. + +## Kotlin best practices + +### โœ… Prefer +- **Kotlin idioms** over Java-style approaches: + - Extension functions + - `when` expressions + - Smart casts + - Data classes and sealed classes + - Immutable data structures +- **Simple nouns** over composite nouns (`user` > `userAccount`) +- **Generic parameters** over explicit variable types (`val list = mutableList()`) +- **Java interop annotations** only when needed (`@file:JvmName`, `@JvmStatic`) +- **Kotlin DSL** for Gradle files + +### โŒ Avoid +- Mutable data structures +- Java-style verbosity (builders with setters) +- Redundant null checks (`?.let` misuse) +- Using `!!` unless clearly justified +- Type names in variable names (`userObject`, `itemList`) +- String duplication (use constants in companion objects) +- Mixing Groovy and Kotlin DSLs in build logic +- Reflection unless specifically requested + +## Text formatting + - โœ… Remove double empty lines in the code. + - โœ… Remove trailing space characters in the code. + +[spine-docs]: https://github.com/SpineEventEngine/documentation/wiki diff --git a/.agents/common-tasks.md b/.agents/common-tasks.md new file mode 100644 index 0000000..5ee954d --- /dev/null +++ b/.agents/common-tasks.md @@ -0,0 +1,6 @@ +# ๐Ÿ“‹ Common tasks + +- **Adding a new dependency**: Update relevant files in `buildSrc` directory. +- **Creating a new module**: Follow existing module structure patterns. +- **Documentation**: Use KDoc style for public and internal APIs. +- **Testing**: Create comprehensive tests using Kotest assertions. diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md new file mode 100644 index 0000000..914dcc8 --- /dev/null +++ b/.agents/documentation-guidelines.md @@ -0,0 +1,14 @@ +# Documentation & comments + +## Commenting guidelines +- Avoid inline comments in production code unless necessary. +- Inline comments are helpful in tests. +- When using TODO comments, follow the format on the [dedicated page][todo-comments]. +- File and directory names should be formatted as code. + +## Avoid widows, runts, orphans, or rivers + +Agents should **AVOID** text flow patters illustrated +on [this diagram](widow-runt-orphan-river.jpg). + +[todo-comments]: https://github.com/SpineEventEngine/documentation/wiki/TODO-comments diff --git a/.agents/documentation-tasks.md b/.agents/documentation-tasks.md new file mode 100644 index 0000000..8ac4660 --- /dev/null +++ b/.agents/documentation-tasks.md @@ -0,0 +1,20 @@ +# ๐Ÿ“„ Documentation tasks + +1. Ensure all public and internal APIs have KDoc examples. +2. Add in-line code blocks for clarity in tests. +3. Convert inline API comments in Java to KDoc in Kotlin: + ```java + // Literal string to be inlined whenever a placeholder references a non-existent argument. + private final String missingArgumentMessage = "[MISSING ARGUMENT]"; + ``` + transforms to: + ```kotlin + /** + * Literal string to be inlined whenever a placeholder references a non-existent argument. + */ + private val missingArgumentMessage = "[MISSING ARGUMENT]" + ``` + +4. Javadoc -> KDoc conversion tasks: + - Remove `

` tags in the line with text: `"

This"` -> `"This"`. + - Replace `

` with empty line if the tag is the only text in the line. diff --git a/.agents/java-kotlin-conversion.md b/.agents/java-kotlin-conversion.md new file mode 100644 index 0000000..95cf929 --- /dev/null +++ b/.agents/java-kotlin-conversion.md @@ -0,0 +1,43 @@ +# ๐Ÿช„ Converting Java code to Kotlin + +* Java code API comments are Javadoc format. +* Kotlin code API comments are in KDoc format. + +## Javadoc to KDoc conversion + +* The wording of original Javadoc comments must be preserved. + +## Treating nullability + +* Use nullable Kotlin type only if the type in Java is annotated as `@Nullable`. + +## Efficient Conversion Workflow + +* First, analyze the entire Java file structure before beginning conversion to understand dependencies and class relationships. +* Convert Java code to Kotlin systematically: imports first, followed by class definitions, methods, and finally expressions. +* Preserve all existing functionality and behavior during conversion. +* Maintain original code structure and organization to ensure readability. + +## Common Java to Kotlin Patterns + +* Convert Java getters/setters to Kotlin properties with appropriate visibility modifiers. +* Transform Java static methods to companion object functions or top-level functions as appropriate. +* Replace Java anonymous classes with Kotlin lambda expressions when possible. +* Convert Java interfaces with default methods to Kotlin interfaces with implementations. +* Transform Java builders to Kotlin DSL patterns when appropriate. + +## Error Prevention + +* Pay special attention to Java's checked exceptions versus Kotlin's unchecked exceptions. +* Be cautious with Java wildcards (`? extends`, `? super`) conversion to Kotlin's `out` and `in` type parameters. +* Ensure proper handling of Java static initialization blocks in Kotlin companion objects. +* Verify that Java overloaded methods convert correctly with appropriate default parameter values in Kotlin. +* Remember that Kotlin has smart casts which can eliminate explicit type casting needed in Java. + +## Documentation Conversion + +* Convert `@param` to `@param` with the same description. +* Convert `@return` to `@return` with the same description. +* Convert `@throws` to `@throws` with the same description. +* Convert `{@link}` to `[name][fully.qualified.Name]` format. +* Convert `{@code}` to inline code with backticks (`). diff --git a/.agents/project-overview.md b/.agents/project-overview.md new file mode 100644 index 0000000..dfac73f --- /dev/null +++ b/.agents/project-overview.md @@ -0,0 +1,7 @@ +# ๐Ÿ› ๏ธ Project overview + +- **Languages**: Kotlin (primary), Java (secondary). +- **Build tool**: Gradle with Kotlin DSL. +- **Static analysis**: detekt, ErrorProne, Checkstyle, PMD. +- **Testing**: JUnit 5, Kotest Assertions, Codecov. +- **Tools used**: Gradle plugins, IntelliJ IDEA Platform, KSP, KotlinPoet, Dokka. diff --git a/.agents/project-structure-expectations.md b/.agents/project-structure-expectations.md new file mode 100644 index 0000000..81b8e1a --- /dev/null +++ b/.agents/project-structure-expectations.md @@ -0,0 +1,21 @@ +# ๐Ÿ“ Project structure expectations + +```yaml +.github +buildSrc/ + + src/ + โ”œโ”€โ”€ main/ + โ”‚ โ”œโ”€โ”€ kotlin/ # Kotlin source files + โ”‚ โ””โ”€โ”€ java/ # Legacy Java code + โ”œโ”€โ”€ test/ + โ”‚ โ””โ”€โ”€ kotlin/ # Unit and integration tests + build.gradle.kts # Kotlin-based build configuration + + +build.gradle.kts # Kotlin-based build configuration +settings.gradle.kts # Project structure and settings +README.md # Project overview +AGENTS.md # Entry point for LLM agent instructions +version.gradle.kts # Declares the project version. +``` diff --git a/.agents/quick-reference-card.md b/.agents/quick-reference-card.md new file mode 100644 index 0000000..6c25b9a --- /dev/null +++ b/.agents/quick-reference-card.md @@ -0,0 +1,10 @@ +# ๐Ÿ“ Quick Reference Card + +``` +๐Ÿ”‘ Key Information: +- Kotlin/Java project with CQRS architecture +- Use ChatGPT for documentation, Codex for code generation, GPT-4o for complex analysis +- Follow coding guidelines in Spine Event Engine docs +- Always include tests with code changes +- Version bump required for all PRs +``` diff --git a/.agents/refactoring-guidelines.md b/.agents/refactoring-guidelines.md new file mode 100644 index 0000000..191db49 --- /dev/null +++ b/.agents/refactoring-guidelines.md @@ -0,0 +1,3 @@ +# โš™๏ธ Refactoring guidelines + +- Do NOT replace Kotest assertions with standard Kotlin's built-in test assertions. diff --git a/.agents/running-builds.md b/.agents/running-builds.md new file mode 100644 index 0000000..db0338d --- /dev/null +++ b/.agents/running-builds.md @@ -0,0 +1,18 @@ +# Running builds + +1. When modifying code, run: + ```bash + ./gradlew build + ``` + +2. If Protobuf (`.proto`) files are modified run: + ```bash + ./gradlew clean build + ``` + +3. Documentation-only changes in Kotlin or Java sources run: + ```bash + ./gradlew dokka + ``` + +4. Documentation-only changes do not require running tests! diff --git a/.agents/safety-rules.md b/.agents/safety-rules.md new file mode 100644 index 0000000..08e9b33 --- /dev/null +++ b/.agents/safety-rules.md @@ -0,0 +1,7 @@ +# Safety rules + +- โœ… All code must compile and pass static analysis. +- โœ… Do not auto-update external dependencies. +- โŒ Never use reflection or unsafe code without an explicit approval. +- โŒ No analytics or telemetry code. +- โŒ No blocking calls inside coroutines. diff --git a/.agents/testing.md b/.agents/testing.md new file mode 100644 index 0000000..f81bdbf --- /dev/null +++ b/.agents/testing.md @@ -0,0 +1,8 @@ +# ๐Ÿงช Testing + +- Do not use mocks, use stubs. +- Prefer [Kotest assertions][kotest-assertions] over assertions from JUnit or Google Truth. +- Generate unit tests for APIs (handles edge cases/scenarios). +- Supply scaffolds for typical Kotlin patterns (`when`, sealed classes). + +[kotest-assertions]: https://kotest.io/docs/assertions/assertions.html diff --git a/.agents/version-policy.md b/.agents/version-policy.md new file mode 100644 index 0000000..65dc457 --- /dev/null +++ b/.agents/version-policy.md @@ -0,0 +1,30 @@ +# Version policy + +## We use semver +The version of the project is kept in the `version.gradle.kts` file in the root of the project. + +The version numbers in these files follow the conventions of +[Semantic Versioning 2.0.0](https://semver.org/). + +## Quick checklist for versioning +1. Increment the patch version in `version.gradle.kts`. + Retain zero-padding if applicable: + - Example: `"2.0.0-SNAPSHOT.009"` โ†’ `"2.0.0-SNAPSHOT.010"` +2. Commit the version bump separately with this comment: + ```text + Bump version โ†’ `$newVersion` + ``` +3. Rebuild using `./gradlew clean build`. +4. Update `pom.xml`, `dependencies.md` and commit changes with: `Update dependency reports` + +Remember: PRs without version bumps will fail CI (conflict resolution detailed above). + +## Resolving conflicts in `version.gradle.kts` +A branch conflict over the version number should be resolved as described below. + * If a merged branch has a number which is less than that of the current branch, the version of + the current branch stays. + * If the merged branch has the number which is greater or equal to that of the current branch, + the number should be increased by one. + +## When to bump the version? + - When a new branch is created. diff --git a/.github/workflows/detekt-analysis.yml b/.github/workflows/detekt-analysis.yml deleted file mode 100644 index 0523e79..0000000 --- a/.github/workflows/detekt-analysis.yml +++ /dev/null @@ -1,103 +0,0 @@ -# This workflow performs a static analysis of your Kotlin source code using -# Detekt. -# -# Scans are triggered: -# 1. On every push to default and protected branches -# 2. On every Pull Request targeting the default branch -# 3. On a weekly schedule -# 4. Manually, on demand, via the "workflow_dispatch" event -# -# The workflow should work with no modifications, but you might like to use a -# later version of the Detekt CLI by modifing the $DETEKT_RELEASE_TAG -# environment variable. -name: Scan with Detekt - -on: - # Triggers the workflow on push or pull request events but only for default and protected branches - push: - branches: [ master ] - pull_request: - branches: [ master ] - schedule: - - cron: '19 17 * * 4' - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -env: - # Release tag associated with version of Detekt to be installed - # SARIF support (required for this workflow) was introduced in Detekt v1.15.0 - DETEKT_RELEASE_TAG: v1.15.0 - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "scan" - scan: - name: Scan - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - # Gets the download URL associated with the $DETEKT_RELEASE_TAG - - name: Get Detekt download URL - id: detekt_info - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - DETEKT_DOWNLOAD_URL=$( gh api graphql --field tagName=$DETEKT_RELEASE_TAG --raw-field query=' - query getReleaseAssetDownloadUrl($tagName: String!) { - repository(name: "detekt", owner: "detekt") { - release(tagName: $tagName) { - releaseAssets(name: "detekt", first: 1) { - nodes { - downloadUrl - } - } - } - } - } - ' | \ - jq --raw-output '.data.repository.release.releaseAssets.nodes[0].downloadUrl' ) - echo "::set-output name=download_url::$DETEKT_DOWNLOAD_URL" - - # Sets up the detekt cli - - name: Setup Detekt - run: | - dest=$( mktemp -d ) - curl --request GET \ - --url ${{ steps.detekt_info.outputs.download_url }} \ - --silent \ - --location \ - --output $dest/detekt - chmod a+x $dest/detekt - echo $dest >> $GITHUB_PATH - - # Performs static analysis using Detekt - - name: Run Detekt - continue-on-error: true - run: | - detekt --input ${{ github.workspace }} --report sarif:${{ github.workspace }}/detekt.sarif.json - - # Modifies the SARIF output produced by Detekt so that absolute URIs are relative - # This is so we can easily map results onto their source files - # This can be removed once relative URI support lands in Detekt: https://git.io/JLBbA - - name: Make artifact location URIs relative - continue-on-error: true - run: | - echo "$( - jq \ - --arg github_workspace ${{ github.workspace }} \ - '. | ( .runs[].results[].locations[].physicalLocation.artifactLocation.uri |= if test($github_workspace) then .[($github_workspace | length | . + 1):] else . end )' \ - ${{ github.workspace }}/detekt.sarif.json - )" > ${{ github.workspace }}/detekt.sarif.json - - # Uploads results to GitHub repository using the upload-sarif action - - uses: github/codeql-action/upload-sarif@v2 - with: - # Path to SARIF file relative to the root of the repository - sarif_file: ${{ github.workspace }}/detekt.sarif.json - checkout_path: ${{ github.workspace }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8b5b4d5..7de0c51 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,6 +58,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FORMAL_GIT_HUB_PAGES_AUTHOR: developers@spine.io # https://docs.github.com/en/actions/reference/environment-variables - REPO_SLUG: $GITHUB_REPOSITORY # e.g. SpineEventEngine/core-java + REPO_SLUG: ${{ github.repository }} # e.g. SpineEventEngine/core-jvm GOOGLE_APPLICATION_CREDENTIALS: ./maven-publisher.json NPM_TOKEN: ${{ secrets.NPM_SECRET }} diff --git a/.gitignore b/.gitignore index 419020f..48de9f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ # -# Copyright 2022, TeamDev. All rights reserved. +# Copyright 2025, TeamDev. All rights reserved. +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 # # Redistribution and use in source and/or binary forms, with or without # modification, must retain the above copyright notice and the following @@ -25,6 +31,9 @@ # # Therefore, instructions below are superset of instructions required for all the projects. +# Temporary output of AI agents. +.output + # `jenv` local configuration. .java-version @@ -32,7 +41,7 @@ .fleet/ # Kotlin temp directories. -**/.kotlin/** +**/.kotlin/ # IntelliJ IDEA modules and interim config files. *.iml @@ -49,11 +58,27 @@ !.idea/codeStyles/ !.idea/copyright/ +# Ignore IDEA config files under `tests` +/tests/.idea/** + # Gradle interim configs **/.gradle/** +# Temp directory for Gradle TestKit runners +**/.gradle-test-kit/** + +# Integration test log files +/tests/_out/** + # Generated source code **/generated/** +**/*.pb.dart +**/*.pbenum.dart +**/*.pbserver.dart +**/*.pbjson.dart + +# Generated source code with custom path under `tests` +/tests/**/proto-gen/** # Gradle build files **/build/** @@ -77,9 +102,6 @@ gradle-app.setting # Spine internal directory for storing intermediate artifacts **/.spine/** -# Spine model compiler auto-generated resources -/tools/gradle-plugins/model-compiler/src/main/resources/spine-protoc.gradle - # Login details to Maven repository. # Each workstation should have developer's login defined in this file. credentials.tar @@ -104,17 +126,7 @@ hs_err_pid* .packages pubspec.lock +# Ignore the `tmp` directory used for building dependant repositories. +/tmp -# -# The gradle.properties file should contain settings specific to a developer's workstation. -# -# See sample file for a Mac OS X workstation below. -# ------- -# # Set Java home to point to JDK8. This is need to generate classes working with Java8 API. -# # Otherwise the following warning appears during the build: -# # warning: [options] bootstrap class path not set in conjunction with -source 1.8 -# # -# # suppress inspection "UnusedProperty" -# org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home/ -# ------- -gradle.properties +.gradle-test-kit/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 809943c..f60c273 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,6 @@ + - + \ No newline at end of file diff --git a/.idea/dictionaries/common.xml b/.idea/dictionaries/common.xml index e62952a..d1c3a7b 100644 --- a/.idea/dictionaries/common.xml +++ b/.idea/dictionaries/common.xml @@ -24,6 +24,8 @@ handshaker hohpe idempotency + jspecify + kotest lempira liskov melnik @@ -66,4 +68,4 @@ yevsyukov - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 229f1d3..0bd1d9d 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -255,18 +255,6 @@

{@link Runtime#exec(String[], String[], File) Executes} the given {@code String} array as - * a CLI command. If the execution is successful, returns the command output. Throws - * an {@link IllegalStateException} otherwise. + * [Executes][Runtime.exec] the given `String` array as a CLI command. + * + * If the execution is successful, returns the command output. + * Throws an [IllegalStateException] otherwise. * - * @param command the command to execute - * @return the command line output - * @throws IllegalStateException upon an execution error + * @param command the command to execute. + * @return the command line output. + * @throws IllegalStateException if the execution fails. */ fun execute(vararg command: String): String { val outWriter = StringWriter() @@ -89,7 +86,7 @@ class Cli(private val workingFolder: File) { * Asynchronously reads all lines from this [InputStream] and appends them * to the passed [StringWriter]. */ -fun InputStream.pourTo(dest: StringWriter) { +private fun InputStream.pourTo(dest: StringWriter) { Thread { val sc = Scanner(this) while (sc.hasNextLine()) { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt index 3de99c4..e5c3007 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/ConfigTester.kt @@ -195,7 +195,7 @@ class GitRepository( * to the specified [destinationFolder]. * * The source code is put to the sub-folder named after the repository. - * E.g. for `https://github.com/acme-org/foobar` the code is placed under + * E.g., for `https://github.com/acme-org/foobar` the code is placed under * the `destinationFolder/foobar` folder. * * If the supplied folder does not exist, it is created. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt index 3b43246..deda203 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/ProjectExtensions.kt @@ -40,6 +40,15 @@ import org.gradle.kotlin.dsl.getByType * This file contains extension methods and properties for the Gradle `Project`. */ +/** + * Logs the result of the function using the project logger at `INFO` level. + */ +fun Project.log(message: () -> String) { + if (logger.isInfoEnabled) { + logger.info(message.invoke()) + } +} + /** * Obtains the Java plugin extension of the project. */ @@ -68,7 +77,7 @@ fun Project.applyPlugin(cls: Class>) { * the generic parameter `T`. */ @Suppress("UNCHECKED_CAST") /* See the method docs. */ -fun Project.findTask(name: String): T { +fun Project.getTask(name: String): T { val task = this.tasks.findByName(name) ?: error("Unable to find a task named `$name` in the project `${this.name}`.") return task as T diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/Repositories.kt deleted file mode 100644 index 8ec449e..0000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/Repositories.kt +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -@file:Suppress("TooManyFunctions") // Deprecated functions will be kept for a while. - -package io.spine.gradle - -import io.spine.gradle.publish.CloudRepo -import io.spine.gradle.publish.PublishingRepos -import io.spine.gradle.publish.PublishingRepos.gitHub -import java.io.File -import java.net.URI -import java.util.* -import org.gradle.api.Project -import org.gradle.api.artifacts.dsl.RepositoryHandler -import org.gradle.api.artifacts.repositories.MavenArtifactRepository -import org.gradle.kotlin.dsl.ScriptHandlerScope -import org.gradle.kotlin.dsl.maven - -/** - * Applies [standard][doApplyStandard] repositories to this [ScriptHandlerScope] - * optionally adding [gitHub] repositories for Spine-only components, if - * names of such repositories are given. - * - * @param buildscript - * a [ScriptHandlerScope] to work with. Pass `this` under `buildscript { }`. - * @param rootProject - * a root project where the `buildscript` is declared. - * @param gitHubRepo - * a list of short repository names, or empty list if only - * [standard repositories][doApplyStandard] are required. - */ -@Suppress("unused") -@Deprecated( - message = "Please use `standardSpineSdkRepositories()`.", - replaceWith = ReplaceWith("standardSpineSdkRepositories()") -) -fun applyWithStandard( - buildscript: ScriptHandlerScope, - rootProject: Project, - vararg gitHubRepo: String -) { - val repositories = buildscript.repositories - gitHubRepo.iterator().forEachRemaining { repo -> - repositories.applyGitHubPackages(repo, rootProject) - } - repositories.standardToSpineSdk() -} - -/** - * Registers the selected GitHub Packages repos as Maven repositories. - * - * To be used in `buildscript` clauses when a fully-qualified call must be made. - * - * @param repositories - * the handler to accept registration of the GitHub Packages repository - * @param shortRepositoryName - * the short name of the GitHub repository (e.g. "core-java") - * @param project - * the project which is going to consume artifacts from the repository - * @see applyGitHubPackages - */ -@Suppress("unused") -@Deprecated( - message = "Please use `standardSpineSdkRepositories()`.", - replaceWith = ReplaceWith("standardSpineSdkRepositories()") -) -fun doApplyGitHubPackages( - repositories: RepositoryHandler, - shortRepositoryName: String, - project: Project -) = repositories.applyGitHubPackages(shortRepositoryName, project) - -/** - * Registers the standard set of Maven repositories. - * - * To be used in `buildscript` clauses when a fully-qualified call must be made. - */ -@Suppress("unused") -@Deprecated( - message = "Please use `standardSpineSdkRepositories()`.", - replaceWith = ReplaceWith("standardSpineSdkRepositories()") -) -fun doApplyStandard(repositories: RepositoryHandler) = repositories.standardToSpineSdk() - -/** - * Applies the repository hosted at GitHub Packages, to which Spine artifacts were published. - * - * This method should be used by those wishing to have Spine artifacts published - * to GitHub Packages as dependencies. - * - * @param shortRepositoryName - * short names of the GitHub repository (e.g. "base", "core-java", "model-tools") - * @param project - * the project which is going to consume artifacts from repositories - */ -fun RepositoryHandler.applyGitHubPackages(shortRepositoryName: String, project: Project) { - val repository = gitHub(shortRepositoryName) - val credentials = repository.credentials(project) - - credentials?.let { - spineMavenRepo(it, repository.releases) - spineMavenRepo(it, repository.snapshots) - } -} - -/** - * Applies the repositories hosted at GitHub Packages, to which Spine artifacts were published. - * - * This method should be used by those wishing to have Spine artifacts published - * to GitHub Packages as dependencies. - * - * @param shortRepositoryName - * the short name of the GitHub repository (e.g. "core-java") - * @param project - * the project which is going to consume or publish artifacts from - * the registered repository - */ -fun RepositoryHandler.applyGitHubPackages(project: Project, vararg shortRepositoryName: String) { - for (name in shortRepositoryName) { - applyGitHubPackages(name, project) - } -} - -/** - * Applies [standard][applyStandard] repositories to this [RepositoryHandler] - * optionally adding [applyGitHubPackages] repositories for Spine-only components, if - * names of such repositories are given. - * - * @param project - * a project to which we add dependencies - * @param gitHubRepo - * a list of short repository names, or empty list if only - * [standard repositories][applyStandard] are required. - */ -@Suppress("unused") -@Deprecated( - message = "Please use `standardToSpineSdk()`.", - replaceWith = ReplaceWith("standardToSpineSdk()") -) -fun RepositoryHandler.applyStandardWithGitHub(project: Project, vararg gitHubRepo: String) { - gitHubRepo.iterator().forEachRemaining { repo -> - applyGitHubPackages(repo, project) - } - standardToSpineSdk() -} - -/** - * A scrambled version of PAT generated with the only "read:packages" scope. - * - * The scrambling around PAT is necessary because GitHub analyzes commits for the presence - * of tokens and invalidates them. - * - * @see - * How to make GitHub packages to the public - */ -object Pat { - private const val shade = "_phg->8YlN->MFRA->gxIk->HVkm->eO6g->FqHJ->z8MS->H4zC->ZEPq" - private const val separator = "->" - private val chunks: Int = shade.split(separator).size - 1 - - fun credentials(): Credentials { - val pass = shade.replace(separator, "").splitAndReverse(chunks, "") - return Credentials("public", pass) - } - - /** - * Splits this string to the chunks, reverses each chunk, and joins them - * back to a string using the [separator]. - */ - private fun String.splitAndReverse(numChunks: Int, separator: String): String { - check(length / numChunks >= 2) { - "The number of chunks is too big. Must be <= ${length / 2}." - } - val chunks = chunked(length / numChunks) - val reversedChunks = chunks.map { chunk -> chunk.reversed() } - return reversedChunks.joinToString(separator) - } -} - -/** - * Adds a read-only view to all artifacts of the SpineEventEngine - * GitHub organization. - */ -fun RepositoryHandler.spineArtifacts(): MavenArtifactRepository = maven { - url = URI("https://maven.pkg.github.com/SpineEventEngine/*") - includeSpineOnly() - val pat = Pat.credentials() - credentials { - username = pat.username - password = pat.password - } -} - -val RepositoryHandler.intellijReleases: MavenArtifactRepository - get() = maven("https://www.jetbrains.com/intellij-repository/releases") - -val RepositoryHandler.jetBrainsCacheRedirector: MavenArtifactRepository - get() = maven("https://cache-redirector.jetbrains.com/intellij-dependencies") - -/** - * Applies repositories commonly used by Spine Event Engine projects. - */ -fun RepositoryHandler.standardToSpineSdk() { - spineArtifacts() - - val spineRepos = listOf( - Repos.spine, - Repos.spineSnapshots, - Repos.artifactRegistry, - Repos.artifactRegistrySnapshots - ) - - spineRepos - .map { URI(it) } - .forEach { - maven { - url = it - includeSpineOnly() - } - } - - intellijReleases - jetBrainsCacheRedirector - - maven { - url = URI(Repos.sonatypeSnapshots) - } - - mavenCentral() - gradlePluginPortal() - mavenLocal().includeSpineOnly() -} - -@Deprecated( - message = "Please use `standardToSpineSdk() instead.", - replaceWith = ReplaceWith("standardToSpineSdk()") -) -fun RepositoryHandler.applyStandard() = this.standardToSpineSdk() - -/** - * A Maven repository. - */ -data class Repository( - val releases: String, - val snapshots: String, - private val credentialsFile: String? = null, - private val credentialValues: ((Project) -> Credentials?)? = null, - val name: String = "Maven repository `$releases`" -) { - - /** - * Obtains the publishing password credentials to this repository. - * - * If the credentials are represented by a `.properties` file, reads the file and parses - * the credentials. The file must have properties `user.name` and `user.password`, which store - * the username and the password for the Maven repository auth. - */ - fun credentials(project: Project): Credentials? = when { - credentialValues != null -> credentialValues.invoke(project) - credentialsFile != null -> credsFromFile(credentialsFile, project) - else -> throw IllegalArgumentException( - "Credentials file or a supplier function should be passed." - ) - } - - private fun credsFromFile(fileName: String, project: Project): Credentials? { - val file = project.rootProject.file(fileName) - if (file.exists().not()) { - return null - } - - val log = project.logger - log.info("Using credentials from `$fileName`.") - val creds = file.parseCredentials() - log.info("Publishing build as `${creds.username}`.") - return creds - } - - private fun File.parseCredentials(): Credentials { - val properties = Properties().apply { load(inputStream()) } - val username = properties.getProperty("user.name") - val password = properties.getProperty("user.password") - return Credentials(username, password) - } - - override fun toString(): String { - return name - } -} - -/** - * Password credentials for a Maven repository. - */ -data class Credentials( - val username: String?, - val password: String? -) - -/** - * Defines names of additional repositories commonly used in the Spine SDK projects. - * - * @see [applyStandard] - */ -private object Repos { - val spine = CloudRepo.published.releases - val spineSnapshots = CloudRepo.published.snapshots - val artifactRegistry = PublishingRepos.cloudArtifactRegistry.releases - val artifactRegistrySnapshots = PublishingRepos.cloudArtifactRegistry.snapshots - - @Suppress("unused") - @Deprecated( - message = "Sonatype release repository redirects to the Maven Central", - replaceWith = ReplaceWith("sonatypeSnapshots"), - level = DeprecationLevel.ERROR - ) - const val sonatypeReleases = "https://oss.sonatype.org/content/repositories/snapshots" - const val sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots" -} - -/** - * Registers the Maven repository with the passed [repoCredentials] for authorization. - * - * Only includes the Spine-related artifact groups. - */ -private fun RepositoryHandler.spineMavenRepo( - repoCredentials: Credentials, - repoUrl: String -) { - maven { - url = URI(repoUrl) - includeSpineOnly() - credentials { - username = repoCredentials.username - password = repoCredentials.password - } - } -} - -/** - * Narrows down the search for this repository to Spine-related artifact groups. - */ -private fun MavenArtifactRepository.includeSpineOnly() { - content { - includeGroupByRegex("io\\.spine.*") - } -} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/VersionWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/VersionWriter.kt deleted file mode 100644 index d3d4323..0000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/VersionWriter.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.gradle - -import java.util.* -import org.gradle.api.DefaultTask -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.provider.MapProperty -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction - -/** - * A task that generates a dependency versions `.properties` file. - */ -abstract class WriteVersions : DefaultTask() { - - /** - * Versions to add to the file. - * - * The map key is a string in the format of `_`, and the value - * is the version corresponding to those group ID and artifact name. - * - * @see WriteVersions.version - */ - @get:Input - abstract val versions: MapProperty - - /** - * The directory that hosts the generated file. - */ - @get:OutputDirectory - abstract val versionsFileLocation: DirectoryProperty - - /** - * Adds a dependency version to write into the file. - * - * The given dependency notation is a Gradle artifact string of format: - * `"::"`. - * - * @see WriteVersions.versions - * @see WriteVersions.includeOwnVersion - */ - fun version(dependencyNotation: String) { - val parts = dependencyNotation.split(":") - check(parts.size == 3) { "Invalid dependency notation: `$dependencyNotation`." } - versions.put("${parts[0]}_${parts[1]}", parts[2]) - } - - /** - * Enables the versions file to include the version of the project that owns this task. - * - * @see WriteVersions.version - * @see WriteVersions.versions - */ - fun includeOwnVersion() { - val groupId = project.group.toString() - val artifactId = project.artifactId - val version = project.version.toString() - versions.put("${groupId}_${artifactId}", version) - } - - /** - * Creates a `.properties` file with versions, named after the value - * of [Project.artifactId] property. - * - * The name of the file would be: `versions-.properties`. - * - * By default, value of [Project.artifactId] property is a project's name with "spine-" prefix. - * For example, if a project's name is "tools", then the name of the file would be: - * `versions-spine-tools.properties`. - */ - @TaskAction - fun writeFile() { - versions.finalizeValue() - versionsFileLocation.finalizeValue() - - val values = versions.get() - val properties = Properties() - properties.putAll(values) - val outputDir = versionsFileLocation.get().asFile - outputDir.mkdirs() - val fileName = resourceFileName() - val file = outputDir.resolve(fileName) - file.createNewFile() - file.writer().use { - properties.store(it, "Dependency versions supplied by the `$path` task.") - } - } - - private fun resourceFileName(): String { - val artifactId = project.artifactId - return "versions-${artifactId}.properties" - } -} - -/** - * A plugin that enables storing dependency versions into a resource file. - * - * Dependency version may be used by Gradle plugins at runtime. - * - * The plugin adds one task โ€” `writeVersions`, which generates a `.properties` file with some - * dependency versions. - * - * The generated file will be available in classpath of the target project under the name: - * `versions-.properties`, where `` is the name of the target - * Gradle project. - */ -@Suppress("unused") -class VersionWriter : Plugin { - - override fun apply(target: Project): Unit = with (target.tasks) { - val task = register("writeVersions", WriteVersions::class.java) { - versionsFileLocation.convention(project.layout.buildDirectory.dir(name)) - includeOwnVersion() - project.sourceSets - .getByName("main") - .resources - .srcDir(versionsFileLocation) - } - getByName("processResources").dependsOn(task) - } -} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt index 122a604..bb2a181 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/checkstyle/CheckStyleConfig.kt @@ -56,7 +56,7 @@ object CheckStyleConfig { plugin(CheckstylePlugin::class.java) } - val configDir = project.rootDir.resolve("config/quality/") + val configDir = project.rootDir.resolve("buildSrc/quality/") with(project.the()) { toolVersion = CheckStyle.version diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt index f6e1777..0344819 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/fs/LazyTempPath.kt @@ -57,7 +57,7 @@ class LazyTempPath(private val prefix: String) : Path { vararg modifiers: WatchEvent.Modifier? ): WatchKey = delegate.register(watcher, events, *modifiers) - override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>?): WatchKey = + override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>): WatchKey = delegate.register(watcher, *events) override fun getFileSystem(): FileSystem = delegate.fileSystem @@ -101,7 +101,7 @@ class LazyTempPath(private val prefix: String) : Path { override fun toAbsolutePath(): Path = delegate.toAbsolutePath() - override fun toRealPath(vararg options: LinkOption?): Path = delegate.toRealPath(*options) + override fun toRealPath(vararg options: LinkOption): Path = delegate.toRealPath(*options) override fun toFile(): File = delegate.toFile() diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt index 55ce67f..e0ce827 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/git/Repository.kt @@ -26,8 +26,11 @@ package io.spine.gradle.git +import com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly import io.spine.gradle.Cli import io.spine.gradle.fs.LazyTempPath +import java.util.concurrent.TimeUnit.MILLISECONDS +import org.gradle.api.Project /** * Interacts with a real Git repository. @@ -43,26 +46,18 @@ import io.spine.gradle.fs.LazyTempPath * NOTE: This class creates a temporal folder, so it holds resources. For the proper * release of resources please use the provided functionality inside a `use` block or * call the `close` method manually. + * + * @property project The Gradle project in which context the repo operations are held. + * @property sshUrl The GitHub SSH URL to the underlying repository. + * @property user Current user configuration. + * This configuration determines what ends up in the `author` and `committer` fields of a commit. + * @property currentBranch The currently checked-out branch. */ class Repository private constructor( - - /** - * The GitHub SSH URL to the underlying repository. - */ + private val project: Project, private val sshUrl: String, - - /** - * Current user configuration. - * - * This configuration determines what ends up in author and committer fields of a commit. - */ private var user: UserInfo, - - /** - * Currently checked out branch. - */ - private var currentBranch: String - + private var currentBranch: String, ) : AutoCloseable { /** @@ -80,14 +75,21 @@ class Repository private constructor( /** * Executes a command in the [location]. */ - private fun repoExecute(vararg command: String): String = - Cli(location.toFile()).execute(*command) + private fun repoExecute(vararg command: String): String { + val cmd = command.toList().joinToString(" ") + val msg = "[Repo (${project.path})] Executing command: `$cmd`." + System.err.println(msg) + return Cli(location.toFile()).execute(*command) + } /** * Checks out the branch by its name. + * + * IMPORTANT. The branch must exist in the upstream repository. */ fun checkout(branch: String) { repoExecute("git", "checkout", branch) + repoExecute("git", "pull") currentBranch = branch } @@ -128,10 +130,15 @@ class Repository private constructor( } /** - * Pushes local repository to the remote. + * Pushes the current branch of the repository to the remote. + * + * Performs a pull with rebase before pushing to ensure the local branch is up-to-date. */ fun push() { - repoExecute("git", "push") + withRetries(description = "Pushing to $sshUrl, branch = '$currentBranch'") { + repoExecute("git", "pull", "--rebase") + repoExecute("git", "push") + } } override fun close() { @@ -139,18 +146,27 @@ class Repository private constructor( } companion object Factory { + /** * Clones the repository with the provided SSH URL in a temporal folder. - * Configures the username and the email of the Git user. See [configureUser] - * documentation for more information. Performs checkout of the branch in - * case it was passed. By default, [master][Branch.master] is checked out. + * + * Configures the username and the email of the Git user. + * See [configureUser] documentation for more information. + * + * Performs checkout of the branch in case it was passed. + * By default, [master][Branch.master] is checked out. * * @throws IllegalArgumentException if SSH URL is an empty string. */ - fun of(sshUrl: String, user: UserInfo, branch: String = Branch.master): Repository { + fun clone( + project: Project, + sshUrl: String, + user: UserInfo, + branch: String = Branch.master, + ): Repository { require(sshUrl.isNotBlank()) { "SSH URL cannot be an empty string." } - val repo = Repository(sshUrl, user, branch) + val repo = Repository(project, sshUrl, user, branch) repo.clone() repo.configureUser(user) @@ -162,3 +178,44 @@ class Repository private constructor( } } } + +/** + * Executes a given operation with retries using exponential backoff strategy. + * + * If the operation fails, it will be retried up to the specified number of times + * with increasing delays between attempts. + * The delay increases exponentially but is capped at the specified maximum value. + * + * If all retries fail, the exception from the final attempt will be thrown to the caller. + * + * @param T the type of value returned by the operation + * @param times the maximum number of attempts to execute the operation (default: 3) + * @param initialDelay the delay before the first retry in milliseconds (default: 100ms) + * @param maxDelay the maximum delay between retries in milliseconds (default: 2000ms) + * @param factor the multiplier used to increase delay after each failure (default: 2.0) + * @param description a description of the operation for error reporting (default: empty string) + * @param block the operation to execute + * @return the result of the successful operation execution + */ +@Suppress("TooGenericExceptionCaught", "LongParameterList") +private fun withRetries( + times: Int = 5, + initialDelay: Long = 2000, // ms + maxDelay: Long = 20000, // ms + factor: Double = 2.0, + description: String = "", + block: () -> T +): T { + var currentDelay = initialDelay + repeat(times - 1) { + try { + return block() + } catch (e: Exception) { + System.err.println("'$description' failed. " + + "Message: '${e.message}'. Retrying in $currentDelay ms.") + } + sleepUninterruptibly(currentDelay, MILLISECONDS) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + } + return block() +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt index 94ab6ac..de75295 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/RepositoryExtensions.kt @@ -26,10 +26,11 @@ package io.spine.gradle.github.pages -import io.spine.gradle.RepoSlug import io.spine.gradle.git.Branch import io.spine.gradle.git.Repository import io.spine.gradle.git.UserInfo +import io.spine.gradle.repo.RepoSlug +import org.gradle.api.Project /** * Clones the current project repository with the branch dedicated to publishing @@ -38,14 +39,15 @@ import io.spine.gradle.git.UserInfo * The repository's GitHub SSH URL is derived from the `REPO_SLUG` environment * variable. The [branch][Branch.documentation] dedicated to publishing documentation * is automatically checked out in this repository. Also, the username and the email - * of the git user are automatically configured. The username is set - * to "UpdateGitHubPages Plugin", and the email is derived from + * of the git user are automatically configured. + * + * The username is set to `"UpdateGitHubPages Plugin"`, and the email is derived from * the `FORMAL_GIT_HUB_PAGES_AUTHOR` environment variable. * * @throws org.gradle.api.GradleException if any of the environment variables described above * is not set. */ -internal fun Repository.Factory.forPublishingDocumentation(): Repository { +internal fun Repository.Factory.forPublishingDocumentation(project: Project): Repository { val host = RepoSlug.fromVar().gitHost() val username = "UpdateGitHubPages Plugin" @@ -54,5 +56,5 @@ internal fun Repository.Factory.forPublishingDocumentation(): Repository { val branch = Branch.documentation - return of(host, user, branch) + return clone(project, host, user, branch) } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt index 186c474..68be42a 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/SshKey.kt @@ -29,24 +29,43 @@ package io.spine.gradle.github.pages import io.spine.gradle.Cli import java.io.File import org.gradle.api.GradleException +import org.gradle.api.logging.Logger /** - * Registers SSH key for further operations with GitHub Pages. + * Registers the SSH key for further operations with GitHub Pages. + * + * @property rootProjectFolder The folder of the project for which we build the documentation. + * @property logger The logger for placing diagnostic messages of this class. */ -internal class SshKey(private val rootProjectFolder: File) { +internal class SshKey( + private val rootProjectFolder: File, + private val logger: Logger +) { + + private fun log(message: () -> String) { + if (logger.isInfoEnabled) { + logger.info("[SshKey] " + message()) + } + } + /** * Creates an SSH key with the credentials and registers it by invoking the * `register-ssh-key.sh` script. */ fun register() { + log { "Registering using ${rootProjectFolder.absolutePath}." } val gitHubAccessKey = gitHubKey() + log { "Obtained the key file at ${gitHubAccessKey.absolutePath}." } val sshConfigFile = sshConfigFile() + log { "Located the SSH key file at ${sshConfigFile.absolutePath}." } sshConfigFile.appendPublisher(gitHubAccessKey) + log { "SSH config file appended." } execute( "${rootProjectFolder.absolutePath}/config/scripts/register-ssh-key.sh", gitHubAccessKey.absolutePath ) + log { "The SSH key registered." } } /** @@ -59,7 +78,7 @@ internal class SshKey(private val rootProjectFolder: File) { * publishing. * * Thus, we configure the SSH agent to use the `deploy_rsa_key` only for specific - * references, namely in `github.com-publish`. + * references, namely in `github-publish`. * * @throws GradleException if `deploy_key_rsa` is not found. */ @@ -91,9 +110,10 @@ internal class SshKey(private val rootProjectFolder: File) { val nl = System.lineSeparator() this.appendText( nl + - "Host github.com-publish" + nl + - "User git" + nl + - "IdentityFile ${privateKey.absolutePath}" + nl + "Host github-publish" + nl + + " HostName github.com" + nl + + " User git" + nl + + " IdentityFile ${privateKey.absolutePath}" + nl ) } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt index f5e3bfc..72b8fd3 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/TaskName.kt @@ -34,14 +34,14 @@ object TaskName { const val updateGitHubPages = "updateGitHubPages" /** - * The name of the helper task to gather the generated Javadoc before updating - * GitHub Pages. + * The name of the helper task to gather the generated Javadoc format + * documentation generated by Dokka before updating GitHub Pages. */ - const val copyJavadoc = "copyJavadoc" + const val copyJavadocDocs = "copyJavadocDocs" /** - * The name of the helper task to gather Dokka-generated documentation before - * updating GitHub Pages. + * The name of the helper task to gather HTML documentation + * generated by Dokka before updating GitHub Pages. */ - const val copyDokka = "copyDokka" + const val copyHtmlDocs = "copyHtmlDocs" } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt index 0f9a0f5..785162e 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/Update.kt @@ -41,23 +41,19 @@ import org.gradle.api.logging.Logger fun Task.updateGhPages(project: Project) { val plugin = project.plugins.getPlugin(UpdateGitHubPages::class.java) - with(plugin) { - SshKey(rootFolder).register() - } + SshKey(plugin.rootFolder, logger).register() - val repository = Repository.forPublishingDocumentation() + val repository = Repository.forPublishingDocumentation(project) - val updateJavadoc = with(plugin) { - UpdateJavadoc(project, javadocOutputFolder, repository, logger) - } + val updateJavadocFormat = + UpdateJavadocFormat(project, plugin.javadocOutputFolder, repository, logger) - val updateDokka = with(plugin) { - UpdateDokka(project, dokkaOutputFolder, repository, logger) - } + val updateHtmlFormat = + UpdateHtmlFormat(project, plugin.htmlOutputFolder, repository, logger) repository.use { - updateJavadoc.run() - updateDokka.run() + updateJavadocFormat.run() + updateHtmlFormat.run() repository.push() } } @@ -80,17 +76,17 @@ private abstract class UpdateDocumentation( protected abstract val docsDestinationFolder: String /** - * The name of the tool used to generate the documentation to update. + * The name of the format of the documentation to update. * * This name will appear in logs as part of a message. */ - protected abstract val toolName: String + protected abstract val formatName: String private val mostRecentFolder by lazy { File("${repository.location}/${docsDestinationFolder}/${project.name}") } - private fun logDebug(message: () -> String) { + private fun log(message: () -> String) { if (logger.isDebugEnabled) { logger.debug(message()) } @@ -98,25 +94,24 @@ private abstract class UpdateDocumentation( fun run() { val module = project.name - logDebug {"Update of the $toolName documentation for module `$module` started." } + log { "Update of the `$formatName` documentation for the module `$module` started." } val documentation = replaceMostRecentDocs() copyIntoVersionDir(documentation) val version = project.version val updateMessage = - "Update `$toolName` documentation for module `$module` as for version $version" + "Update `$formatName` documentation for the module" + + " `$module` with the version `$version`." repository.commitAllChanges(updateMessage) - logDebug { "Update of the `$toolName` documentation for `$module` successfully finished." } + log { "Update of the `$formatName` documentation for `$module` successfully finished." } } private fun replaceMostRecentDocs(): ConfigurableFileCollection { val generatedDocs = project.files(docsSourceFolder) - logDebug { - "Replacing the most recent `$toolName` documentation in `${mostRecentFolder}`." - } + log { "Replacing the most recent `$formatName` documentation in `$mostRecentFolder`." } copyDocs(generatedDocs, mostRecentFolder) return generatedDocs @@ -133,14 +128,12 @@ private abstract class UpdateDocumentation( private fun copyIntoVersionDir(generatedDocs: ConfigurableFileCollection) { val versionedDocDir = File("$mostRecentFolder/v/${project.version}") - logDebug { - "Storing the new version of `$toolName` documentation in `${versionedDocDir}`." - } + log { "Storing the new version of `$formatName` documentation in `${versionedDocDir}`." } copyDocs(generatedDocs, versionedDocDir) } } -private class UpdateJavadoc( +private class UpdateJavadocFormat( project: Project, docsSourceFolder: Path, repository: Repository, @@ -148,12 +141,12 @@ private class UpdateJavadoc( ) : UpdateDocumentation(project, docsSourceFolder, repository, logger) { override val docsDestinationFolder: String - get() = "reference" - override val toolName: String - get() = "Javadoc" + get() = "javadoc" + override val formatName: String + get() = "javadoc" } -private class UpdateDokka( +private class UpdateHtmlFormat( project: Project, docsSourceFolder: Path, repository: Repository, @@ -161,7 +154,7 @@ private class UpdateDokka( ) : UpdateDocumentation(project, docsSourceFolder, repository, logger) { override val docsDestinationFolder: String - get() = "dokka-reference" - override val toolName: String - get() = "Dokka" + get() = "reference" + override val formatName: String + get() = "html" } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt index e46a565..cdfd2c4 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPages.kt @@ -27,13 +27,11 @@ package io.spine.gradle.github.pages import dokkaHtmlTask +import dokkaJavadocTask import io.spine.gradle.fs.LazyTempPath -import io.spine.gradle.github.pages.TaskName.copyDokka -import io.spine.gradle.github.pages.TaskName.copyJavadoc +import io.spine.gradle.github.pages.TaskName.copyHtmlDocs +import io.spine.gradle.github.pages.TaskName.copyJavadocDocs import io.spine.gradle.github.pages.TaskName.updateGitHubPages -import io.spine.gradle.isSnapshot -import io.spine.gradle.javadoc.ExcludeInternalDoclet -import io.spine.gradle.javadoc.javadocTask import java.io.File import org.gradle.api.Plugin import org.gradle.api.Project @@ -43,30 +41,12 @@ import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider /** - * Registers the `updateGitHubPages` task which performs the update of the GitHub - * Pages with the documentation generated by Javadoc and Dokka for a particular - * Gradle project. The generated documentation is appended to the `spine.io` site - * via GitHub pages by pushing commits to the `gh-pages` branch. - * - * Please note that the update is only performed for the projects which are - * NOT snapshots. + * Registers the `updateGitHubPages` task which performs the update of + * the GitHub Pages with the documentation generated in Javadoc and HTML format + * for a particular Gradle project. * - * Users may supply [allowInternalJavadoc][UpdateGitHubPagesExtension.allowInternalJavadoc] - * to configure documentation generated by Javadoc. The documentation for the code - * marked `@Internal` is included when the option is set to `true`. By default, this - * option is `false`. - * - * Usage: - * ``` - * updateGitHubPages { - * - * // Include `@Internal`-annotated code. - * allowInternalJavadoc.set(true) - * - * // Propagate the full path to the local folder of the repository root. - * rootFolder.set(rootDir.absolutePath) - * } - * ``` + * The generated documentation is appended to the `spine.io` site + * via GitHub pages by pushing commits to the `gh-pages` branch. * * In order to work, the script needs a `deploy_key_rsa` private RSA key file in the * repository root. It is recommended to encrypt it in the repository and then decrypt @@ -104,16 +84,16 @@ class UpdateGitHubPages : Plugin { private lateinit var includedInputs: Set /** - * Path to the temp folder used to gather the Javadoc output before submitting it - * to the GitHub Pages update. + * Path to the temp folder used to gather the Javadoc format output (generated by Dokka) + * before submitting it to the GitHub Pages update. */ internal val javadocOutputFolder = LazyTempPath("javadoc") /** - * Path to the temp folder used to gather the documentation generated by Dokka - * before submitting it to the GitHub Pages update. + * Path to the temp folder used to gather the HTML documentation + * generated by Dokka before submitting it to the GitHub Pages update. */ - internal val dokkaOutputFolder = LazyTempPath("dokka") + internal val htmlOutputFolder = LazyTempPath("html") /** * Applies the plugin to the specified [project]. @@ -127,12 +107,15 @@ class UpdateGitHubPages : Plugin { override fun apply(project: Project) { val extension = UpdateGitHubPagesExtension.createIn(project) project.afterEvaluate { - val projectVersion = project.version.toString() - if (projectVersion.isSnapshot()) { - registerNoOpTask() - } else { - registerTasks(extension) - } + //TODO:2025-11-20:alexander.yevsyukov: Remove this line and uncomment the below block + // when new publishing procedure is finalized. + registerTasks(extension) +// val projectVersion = project.version.toString() +// if (projectVersion.isSnapshot()) { +// registerNoOpTask() +// } else { +// registerTasks(extension) +// } } } @@ -141,6 +124,7 @@ class UpdateGitHubPages : Plugin { * the message telling the update is skipped, since the project is in * its `SNAPSHOT` version. */ + @Suppress("unused") private fun Project.registerNoOpTask() { tasks.register(updateGitHubPages) { doLast { @@ -154,41 +138,31 @@ class UpdateGitHubPages : Plugin { } private fun Project.registerTasks(extension: UpdateGitHubPagesExtension) { - val allowInternalJavadoc = extension.allowInternalJavadoc() rootFolder = extension.rootFolder() includedInputs = extension.includedInputs() - if (!allowInternalJavadoc) { - val doclet = ExcludeInternalDoclet(extension.excludeInternalDocletVersion) - doclet.registerTaskIn(this) - } - - tasks.registerCopyJavadoc(allowInternalJavadoc) + tasks.registerCopyJavadoc() tasks.registerCopyDokka() val updatePagesTask = tasks.registerUpdateTask() updatePagesTask.configure { - dependsOn(copyJavadoc) - dependsOn(copyDokka) + dependsOn(copyJavadocDocs) + dependsOn(copyHtmlDocs) } } - private fun TaskContainer.registerCopyJavadoc(allowInternalJavadoc: Boolean) { - val inputs = composeJavadocInputs(allowInternalJavadoc) + private fun TaskContainer.registerCopyJavadoc() { + val inputs = composeJavadocInputs() - register(copyJavadoc, Copy::class.java) { + register(copyJavadocDocs, Copy::class.java) { inputs.forEach { from(it) } into(javadocOutputFolder) } } - private fun TaskContainer.composeJavadocInputs(allowInternalJavadoc: Boolean): List { + private fun TaskContainer.composeJavadocInputs(): List { val inputs = mutableListOf() - if (allowInternalJavadoc) { - inputs.add(javadocTask()) - } else { - inputs.add(javadocTask(ExcludeInternalDoclet.taskName)) - } + inputs.add(dokkaJavadocTask()!!) inputs.addAll(includedInputs) return inputs } @@ -196,9 +170,9 @@ class UpdateGitHubPages : Plugin { private fun TaskContainer.registerCopyDokka() { val inputs = composeDokkaInputs() - register(copyDokka, Copy::class.java) { + register(copyHtmlDocs, Copy::class.java) { inputs.forEach { from(it) } - into(dokkaOutputFolder) + into(htmlOutputFolder) } } @@ -226,7 +200,7 @@ class UpdateGitHubPages : Plugin { } private fun cleanup() { - val folders = listOf(dokkaOutputFolder, javadocOutputFolder) + val folders = listOf(htmlOutputFolder, javadocOutputFolder) folders.forEach { it.toFile().deleteRecursively() } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt index 90eebc2..a849a83 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/github/pages/UpdateGitHubPagesExtension.kt @@ -38,65 +38,41 @@ import org.gradle.kotlin.dsl.property * Configures the `updateGitHubPages` extension. */ @Suppress("unused") -fun Project.updateGitHubPages(excludeInternalDocletVersion: String, - action: UpdateGitHubPagesExtension.() -> Unit) { +fun Project.updateGitHubPages( + action: UpdateGitHubPagesExtension.() -> Unit +) { apply() val extension = extensions.getByType(UpdateGitHubPagesExtension::class) - extension.excludeInternalDocletVersion = excludeInternalDocletVersion extension.action() } /** * The extension for configuring the [UpdateGitHubPages] plugin. + * + * @property rootFolder The root folder of the repository to which the updated `Project` belongs. + * @property includeInputs The external inputs, which output should be included + * into the GitHub Pages update. The values are interpreted according to + * [Copy.from][org.gradle.api.tasks.Copy.from] specification. + * This property is optional. */ -class UpdateGitHubPagesExtension -private constructor( - - /** - * Tells whether the types marked `@Internal` should be included into - * the doc generation. - */ - val allowInternalJavadoc: Property, - - /** - * The root folder of the repository to which the updated `Project` belongs. - */ +class UpdateGitHubPagesExtension private constructor( var rootFolder: Property, - - /** - * The external inputs, which output should be included into - * the GitHub Pages update. - * - * The values are interpreted according to - * [org.gradle.api.tasks.Copy.from] specification. - * - * This property is optional. - */ var includeInputs: SetProperty ) { - - /** - * The version of the - * [ExcludeInternalDoclet][io.spine.gradle.javadoc.ExcludeInternalDoclet] - * used when updating documentation at GitHub Pages. - * - * This value is used when adding dependency on the doclet when the plugin tasks - * are registered. Since the doclet dependency is required, its value passed as - * a parameter for the extension, rather than a property. - */ - internal lateinit var excludeInternalDocletVersion: String - internal companion object { - /** The name of the extension. */ + /** + * The name of the extension. + */ const val name = "updateGitHubPages" - /** Creates a new extension and adds it to the passed project. */ + /** + * Creates a new extension and adds it to the passed project. + */ fun createIn(project: Project): UpdateGitHubPagesExtension { val factory = project.objects val result = UpdateGitHubPagesExtension( - allowInternalJavadoc = factory.property(Boolean::class), rootFolder = factory.property(File::class), includeInputs = factory.setProperty(Any::class.java) ) @@ -105,27 +81,15 @@ private constructor( } } - /** - * Returns `true` if the `@Internal`-annotated code should be included into the - * generated documentation, `false` otherwise. - */ - fun allowInternalJavadoc(): Boolean { - return allowInternalJavadoc.get() - } - /** * Returns the local root folder of the repository, to which the handled Gradle * Project belongs. */ - fun rootFolder(): File { - return rootFolder.get() - } + fun rootFolder(): File = rootFolder.get() /** * Returns the external inputs, which results should be included into the * GitHub Pages update. */ - fun includedInputs(): Set { - return includeInputs.get() - } + fun includedInputs(): Set = includeInputs.get() } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt b/buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt new file mode 100644 index 0000000..fc00555 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/java/Linters.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.java + +import net.ltgt.gradle.errorprone.errorprone +import org.gradle.api.Project +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.invoke +import org.gradle.kotlin.dsl.named + +/** + * Disables Java linters in this [Project]. + * + * In particular, the following linters will be disabled: + * + * 1. CheckStyle. + * 2. PMD. + * 3. ErrorProne. + * + * Apply this configuration for modules that have original Flogger sources, + * which have not been migrated to Kotlin yet. They produce a lot of + * errors/warnings failing the build. + * + * Our own sources are mostly in Kotlin (as for `spine-logging` repo), + * so this action seems quite safe. + */ +// TODO:2023-09-22:yevhenii.nadtochii: Remove this piece of configuration. +// See issue: https://github.com/SpineEventEngine/logging/issues/56 +fun Project.disableLinters() { + tasks { + named("checkstyleMain") { enabled = false } + named("pmdMain") { enabled = false } + named("compileJava") { + options.errorprone.isEnabled.set(false) + } + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt index 541504c..c42c65c 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/ExcludeInternalDoclet.kt @@ -26,8 +26,7 @@ package io.spine.gradle.javadoc -import io.spine.dependency.local.ArtifactVersion -import io.spine.dependency.local.Spine +import io.spine.dependency.local.ToolBase import io.spine.gradle.javadoc.ExcludeInternalDoclet.Companion.taskName import io.spine.gradle.sourceSets import org.gradle.api.Project @@ -39,12 +38,9 @@ import org.gradle.external.javadoc.StandardJavadocDocletOptions * The doclet which removes Javadoc for `@Internal` things in the Java code. */ @Suppress("ConstPropertyName") -class ExcludeInternalDoclet( - @Deprecated("`Spine.ArtifactVersion.javadocTools` is used instead.") - val version: String = ArtifactVersion.javadocTools -) { +class ExcludeInternalDoclet { - private val dependency = Spine.javadocFilter + private val dependency = ToolBase.JavadocFilter.artifact companion object { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt index d2e4c90..9616e99 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/javadoc/JavadocConfig.kt @@ -26,11 +26,24 @@ package io.spine.gradle.javadoc +import io.spine.gradle.javadoc.JavadocConfig.tags import java.io.File import org.gradle.api.JavaVersion import org.gradle.api.Project +import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.external.javadoc.StandardJavadocDocletOptions +import productionModules + +/** + * Finds a [Javadoc] Gradle task by the passed name. + */ +fun TaskContainer.javadocTask(named: String) = this.getByName(named) as Javadoc + +/** + * Finds a default [Javadoc] Gradle task. + */ +fun TaskContainer.javadocTask() = this.getByName("javadoc") as Javadoc /** * Javadoc processing settings. @@ -58,17 +71,25 @@ object JavadocConfig { fun applyTo(project: Project) { val javadocTask = project.tasks.javadocTask() + if (!isProductionModule(project)) { + javadocTask.enabled = false + return + } discardJavaModulesInLinks(javadocTask) val docletOptions = javadocTask.options as StandardJavadocDocletOptions configureDoclet(docletOptions) } + private fun isProductionModule(project: Project) = project.run { + rootProject.productionModules.contains(this) + } + /** - * Discards using of Java 9 modules in URL links generated by javadoc for our codebase. + * Discards using of Java 9 modules in URL links generated by Javadoc for our codebase. * * This fixes navigation to classes through the search results. * - * The issue appeared after migration to Java 11. When javadoc is generated for a project + * The issue appeared after migration to Java 11. When Javadoc is generated for a project * that does not declare Java 9 modules, search results contain broken links with appended * `undefined` prefix to the URL. This `undefined` was meant to be a name of a Java 9 module. * @@ -78,9 +99,9 @@ object JavadocConfig { // We ask `Javadoc` task to modify "search.js" and override a method, responsible for // the formation of URL prefixes. We can't specify the option "--no-module-directories", - // because it leads to discarding of all module prefixes in generated links. That means, - // links to the types from the standard library would not work, as they are declared - // within modules since Java 9. + // because it leads to discarding of all module prefixes in generated links. + // That means links to the types from the standard library would not work, + // as they are declared within modules since Java 9. val discardModulePrefix = """ diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt index 65cdebd..4539aa9 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/kotlin/KotlinConfig.kt @@ -27,7 +27,7 @@ package io.spine.gradle.kotlin import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension /** @@ -52,12 +52,14 @@ fun KotlinJvmProjectExtension.applyJvmToolchain(version: String) = * Opts-in to experimental features that we use in our codebase. */ @Suppress("unused") -fun KotlinJvmCompilerOptions.setFreeCompilerArgs() { +fun KotlinCommonCompilerOptions.setFreeCompilerArgs() { freeCompilerArgs.addAll( listOf( "-Xskip-prerelease-check", "-Xjvm-default=all", "-Xinline-classes", + "-Xexpect-actual-classes", + "-Xcontext-parameters", "-opt-in=" + "kotlin.contracts.ExperimentalContracts," + "kotlin.io.path.ExperimentalPathApi," + diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt deleted file mode 100644 index 85ef9fc..0000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.gradle.protobuf - -import com.google.protobuf.gradle.GenerateProtoTask -import com.google.protobuf.gradle.ProtobufExtension -import io.spine.gradle.sourceSets -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING -import org.gradle.api.Project -import org.gradle.api.file.SourceDirectorySet -import org.gradle.api.tasks.SourceSet -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.getByType -import org.gradle.plugins.ide.idea.GenerateIdeaModule -import org.gradle.plugins.ide.idea.model.IdeaModel -import org.gradle.plugins.ide.idea.model.IdeaModule -import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask -import titleCaseFirstChar - -/** - * Obtains the path of the `generated` directory under the project root directory. - */ -private val Project.generatedDir: Path - get() = projectDir.resolve("generated").toPath() - -/** - * Obtains the `generated` directory for the source set of the task. - * - * If [language] is specified returns the subdirectory for this language. - */ -private fun GenerateProtoTask.generatedDir(language: String = ""): File { - val path = "${project.generatedDir}/${sourceSet.name}/$language" - return File(path) -} - -/** - * Configures a [GenerateProtoTask] for the code which cannot use Spine Model Compiler - * (e.g., Spine Base or Spine Validation modules). - * - * The task configuration consists of the following steps: - * - * 1. Adding `"kotlin"` to the list of involved `protoc` builtins. - * - * 2. Turning on the generation of a descriptor set file for each source set. - * These files are placed under the `build/descriptors` directory. - * - * 3. Removing source code generated for `com.google` package for both Java and Kotlin. - * This is done at the final steps of the code generation. - * - * 4. Making `processResource` tasks depend on corresponding `generateProto` tasks. - * If the source set of the configured task isn't `main`, appropriate infix for - * the task names is used. - * - * The usage of this extension in a module build file would be: - * ``` - * protobuf { - * generateProtoTasks.all().configureEach { - * setup() - * } - * } - * ``` - * Using the same code under `subprojects` in a root build file does not seem to work because - * test descriptor set files are not copied to resources. Performing this configuration from - * a module build script solves the issue. - * - * IMPORTANT: In addition to calling `setup`, a submodule must contain a descriptor set reference - * file (`desc.ref`) files placed under `resources`. The descriptor reference file must contain - * a reference to the descriptor set file generated by the corresponding `GenerateProtoTask`. - * - * For example, for the `test` source set, the reference would be `known_types_test.desc`, and - * for the `main` source set, the reference would be `known_types_main.desc`. - * - * See `io.spine.code.proto.DescriptorReference` and `io.spine.code.proto.FileDescriptors` classes - * under the `base` project for more details. - */ -@Suppress("unused") -fun GenerateProtoTask.setup() { - builtins.maybeCreate("kotlin") - setupDescriptorSetFileCreation() - - doLast { - copyGeneratedFiles() - } - - excludeProtocOutput() - - setupKotlinCompile() - dependOnProcessResourcesTask() - makeDirsForIdeaModule() -} - -/** - * Tell `protoc` to generate descriptor set files under the project build dir. - * - * The name of the descriptor set file to be generated - * is made to be unique via the project's Maven coordinates. - * - * As the last step of this task, writes a `desc.ref` file - * for the contextual source set, pointing to the generated descriptor set file. - * This is needed to allow other Spine libraries to locate and load the generated - * descriptor set files properly. - * - * Such a job is usually performed by Spine McJava plugin; - * however, it is not possible to use this plugin (or its code) - * in this repository due to cyclic dependencies. - */ -@Suppress( - "TooGenericExceptionCaught" /* Handling all file-writing failures in the same way.*/ -) -fun GenerateProtoTask.setupDescriptorSetFileCreation() { - // Tell `protoc` generate a descriptor set file. - // The name of the generated file reflects the Maven coordinates of the project. - val ssn = sourceSet.name - generateDescriptorSet = true - val buildDir = project.layout.buildDirectory.asFile.get().path - val descriptorsDir = "$buildDir/descriptors/${ssn}" - val descriptorName = project.descriptorSetName(sourceSet) - with(descriptorSetOptions) { - path = "$descriptorsDir/$descriptorName" - includeImports = true - includeSourceInfo = true - } - - // Add the descriptor set file into the resources. - project.sourceSets.named(ssn) { - resources.srcDirs(descriptorsDir) - } - - // Create a `desc.ref` in the same resource folder, - // with the name of the descriptor set file created above. - this.doLast { - val descRefFile = File(descriptorsDir, "desc.ref") - descRefFile.createNewFile() - try { - Files.write(descRefFile.toPath(), setOf(descriptorName), TRUNCATE_EXISTING) - } catch (e: Exception) { - project.logger.error("Error writing `${descRefFile.absolutePath}`.", e) - throw e - } - } -} - -/** - * Returns a name of the descriptor file for the given [sourceSet], - * reflecting the Maven coordinates of Gradle artifact, and the source set - * for which the descriptor set name is to be generated. - * - * The returned value is just a file name and does not contain a file path. - */ -private fun Project.descriptorSetName(sourceSet: SourceSet) = - arrayOf( - group.toString(), - name, - sourceSet.name, - version.toString() - ).joinToString(separator = "_", postfix = ".desc") - -/** - * Copies files from the [outputBaseDir][GenerateProtoTask.outputBaseDir] into - * a subdirectory of [generatedDir][Project.generatedDir] for - * the current [sourceSet][GenerateProtoTask.sourceSet]. - * - * Also removes sources belonging to the `com.google` package in the target directory. - */ -private fun GenerateProtoTask.copyGeneratedFiles() { - project.copy { - from(outputBaseDir) - into(generatedDir()) - } - deleteComGoogle("java") - deleteComGoogle("kotlin") -} - -/** - * Remove the code generated for Google Protobuf library types. - * - * Java code for the `com.google` package was generated because we wanted - * to have descriptors for all the types, including those from Google Protobuf library. - * We want all the descriptors so that they are included into the resources used by - * the `io.spine.type.KnownTypes` class. - * - * Now, as we have the descriptors _and_ excessive Java or Kotlin code, we delete it to avoid - * classes that duplicate those coming from Protobuf library JARs. - */ -private fun GenerateProtoTask.deleteComGoogle(language: String) { - val comDirectory = generatedDir(language).resolve("com") - val googlePackage = comDirectory.resolve("google") - - project.delete(googlePackage) - - // If the `com` directory becomes empty, delete it too. - if (comDirectory.exists() && comDirectory.isDirectory && comDirectory.list()!!.isEmpty()) { - project.delete(comDirectory) - } -} - -/** - * Exclude [GenerateProtoTask.outputBaseDir] from Java source set directories to avoid - * duplicated source code files. - */ -fun GenerateProtoTask.excludeProtocOutput() { - val protocOutputDir = File(outputBaseDir).parentFile - val java: SourceDirectorySet = sourceSet.java - - // Filter out directories belonging to `build/generated/source/proto`. - val newSourceDirectories = java.sourceDirectories - .filter { !it.residesIn(protocOutputDir) } - .toSet() - // Make sure we start from scratch. - // Not doing this failed the following, real, assignment sometimes. - java.setSrcDirs(listOf()) - java.srcDirs(newSourceDirectories) - - // Add copied files to the Java source set. - java.srcDir(generatedDir("java")) - java.srcDir(generatedDir("kotlin")) -} - -/** - * Make sure Kotlin compilation explicitly depends on this `GenerateProtoTask` to avoid racing. - */ -fun GenerateProtoTask.setupKotlinCompile() { - val kotlinCompile = project.kotlinCompilationTaskFor(sourceSet) - kotlinCompile?.dependsOn(this) -} - -/** - * Make the tasks `processResources` depend on `generateProto` tasks explicitly so that: - * 1) Descriptor set files get into resources, avoiding the racing conditions - * during the build. - * - * 2) We don't have the warning "Execution optimizations have been disabled..." issued - * by Gradle during the build because Protobuf Gradle Plugin does not set - * dependencies between `generateProto` and `processResources` tasks. - */ -fun GenerateProtoTask.dependOnProcessResourcesTask() { - val processResources = processResourceTaskName(sourceSet.name) - project.tasks[processResources].dependsOn(this) -} - -/** - * Obtains the name of the `processResource` task for the given source set name. - */ -private fun processResourceTaskName(sourceSetName: String): String { - val infix = - if (sourceSetName == "main") "" - else sourceSetName.titleCaseFirstChar() - return "process${infix}Resources" -} - -private fun Project.kotlinCompilationTaskFor(sourceSet: SourceSet): KotlinCompilationTask<*>? { - val taskName = sourceSet.getCompileTaskName("Kotlin") - return tasks.named(taskName, KotlinCompilationTask::class.java).orNull -} - -private fun File.residesIn(directory: File): Boolean = - canonicalFile.startsWith(directory.absolutePath) - -/** - * Ensures that generated directories for Java and Kotlin are created before [GenerateIdeaModule]. - * - * This works as advised by `Utils.groovy` from Protobuf Gradle plugin: - * ``` - * This is required because the IntelliJ IDEA plugin does not allow adding source directories - * that do not exist. The IntelliJ IDEA config files should be valid from the start even if - * a user runs './gradlew idea' before running './gradlew generateProto'. - * ``` - */ -fun GenerateProtoTask.makeDirsForIdeaModule() { - project.plugins.withId("idea") { - val javaDir = generatedDir("java") - val kotlinDir = generatedDir("kotlin") - project.tasks.withType(GenerateIdeaModule::class.java).forEach { - it.doFirst { - javaDir.mkdirs() - kotlinDir.mkdirs() - } - } - } -} - -/** - * Prints diagnostic output of `sourceDirs` and `generatedSourceDirs` of an [IdeaModule]. - * - * To get a handle on [IdeaModule] please use the following code: - * - * ```kotlin - * val module = project.extensions.findByType(IdeaModel::class.java)!!.module - * ``` - */ -@Suppress("unused") // To be used when debugging build scripts. -fun IdeaModule.printSourceDirectories() { - println("**** [IDEA] Source directories:") - sourceDirs.forEach { println(it) } - println() - println("**** [IDEA] Generated source directories:") - generatedSourceDirs.forEach { println(it) } - println() - println("**** [IDEA] Excluded directories:") - excludeDirs.forEach { println(it) } -} - -/** - * Obtains the directory where the Protobuf Gradle Plugin should place the generated code. - * - * The directory is fixed to be `$buildDir/generated/source/proto` and cannot be - * changed by the settings of the plugin. Even though [ProtobufExtension] has a property - * [generatedFilesBaseDir][ProtobufExtension.getGeneratedFilesBaseDir], which is supposed - * to be used for this purpose, it is declared with `@PackageScope` and thus cannot be - * accessed from outside the plugin. The Protobuf Gradle Plugin (at v0.9.2) does not - * modify the value of the property either. - */ -val Project.generatedSourceProtoDir: Path - get() = layout.buildDirectory.dir("generated/source/proto").get().asFile.toPath() - -/** - * Ensures that the sources generated by Protobuf Gradle Plugin - * are not included in the IDEA project. - * - * IDEA should only see the sources generated by ProtoData as - * we define in [GenerateProtoTask.excludeProtocOutput]. - */ -@Suppress("unused") -fun Project.configureIdea() { - - fun filterSources(sources: Set, excludeDir: File): Set = - sources.filter { !it.residesIn(excludeDir) }.toSet() - - pluginManager.withPlugin("idea") { - val idea = extensions.getByType() - with(idea.module) { - val protocOutput = file(generatedSourceProtoDir) - val protocTargets = protocTargets() - excludeWithNested(protocOutput.toPath(), protocTargets) - sourceDirs = filterSources(sourceDirs, protocOutput) - testSources.filter { !it.residesIn(protocOutput) } - generatedSourceDirs = generatedDir.resolve(protocTargets) - .map { it.toFile() } - .toSet() - } - } -} - -/** - * Lists target directories for Protobuf code generation. - * - * The directory names are in the following format: - * - * `/` - */ -private fun Project.protocTargets(): List { - val protobufTasks = tasks.withType(GenerateProtoTask::class.java) - val codegenTargets = sequence { - protobufTasks.forEach { task -> - val sourceSet = task.sourceSet.name - val builtins = task.builtins.map { builtin -> builtin.name } - val plugins = task.plugins.map { plugin -> plugin.name } - val combined = builtins + plugins - combined.forEach { subdir -> - yield(Paths.get(sourceSet, subdir)) - } - } - } - return codegenTargets.toList() -} - -private fun Path.resolve(subdirs: Iterable): List = - subdirs.map { - resolve(it) - } - -/** - * Excludes the given directory and its subdirectories from - * being seen as ones with the source code. - * - * The primary use of this extension is to exclude `build/generated/source/proto` and its - * subdirectories to avoid duplication of types in the generated code with those in - * produced by ProtoData under the `$projectDir/generated/` directory. - */ -private fun IdeaModule.excludeWithNested(directory: Path, subdirs: Iterable) { - excludeDirs.add(directory.toFile()) - directory.resolve(subdirs).forEach { - excludeDirs.add(it.toFile()) - } -} - -@Suppress("unused") // To be used when debugging build scripts. -private fun printExcluded(dir: Any) { - println(" [IDEA] Excluding directory: $dir") -} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt index 9e38068..9b4ba30 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CheckVersionIncrement.kt @@ -28,8 +28,9 @@ package io.spine.gradle.publish import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES import com.fasterxml.jackson.dataformat.xml.XmlMapper -import io.spine.gradle.Repository +import io.spine.gradle.repo.Repository import java.io.FileNotFoundException +import java.net.URI import java.net.URL import org.gradle.api.DefaultTask import org.gradle.api.GradleException @@ -58,10 +59,11 @@ open class CheckVersionIncrement : DefaultTask() { @TaskAction fun fetchAndCheck() { val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}" - checkInRepo(repository.snapshots, artifact) + val snapshots = repository.target(snapshots = true) + checkInRepo(snapshots, artifact) - if (repository.releases != repository.snapshots) { - checkInRepo(repository.releases, artifact) + if (!repository.hasOneTarget()) { + checkInRepo(repository.target(snapshots = false), artifact) } } @@ -74,16 +76,16 @@ open class CheckVersionIncrement : DefaultTask() { """ The version `$version` is already published to the Maven repository `$repoUrl`. Try incrementing the library version. - All available versions are: ${versions?.joinToString(separator = ", ")}. - - To disable this check, run Gradle with `-x $name`. + All available versions are: ${versions?.joinToString(separator = ", ")}. + + To disable this check, run Gradle with `-x $name`. """.trimIndent() ) } } private fun fetch(repository: String, artifact: String): MavenMetadata? { - val url = URL("$repository/$artifact") + val url = URI.create("$repository/$artifact").toURL() return MavenMetadata.fetchAndParse(url) } @@ -135,7 +137,7 @@ private data class MavenMetadata(var versioning: Versioning = Versioning()) { return try { val metadata = mapper.readValue(url, MavenMetadata::class.java) metadata - } catch (ignored: FileNotFoundException) { + } catch (_: FileNotFoundException) { null } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt index 1cfa7c2..67716d8 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudArtifactRegistry.kt @@ -28,8 +28,8 @@ package io.spine.gradle.publish import com.google.auth.oauth2.GoogleCredentials import com.google.cloud.artifactregistry.auth.DefaultCredentialProvider -import io.spine.gradle.Credentials -import io.spine.gradle.Repository +import io.spine.gradle.repo.Credentials +import io.spine.gradle.repo.Repository import java.io.IOException import org.gradle.api.Project @@ -51,13 +51,15 @@ import org.gradle.api.Project * Ordering said hooks is a non-trivial operation and the result is usually quite fragile. * Thus, we choose to do this small piece of configuration manually. */ +@Suppress("ConstPropertyName") // https://bit.ly/kotlin-prop-names internal object CloudArtifactRegistry { private const val spineRepoLocation = "https://europe-maven.pkg.dev/spine-event-engine" val repository = Repository( - releases = "${spineRepoLocation}/releases", - snapshots = "${spineRepoLocation}/snapshots", + name = "CloudArtifactRegistry", + releases = "$spineRepoLocation/releases", + snapshots = "$spineRepoLocation/snapshots", credentialValues = this::fetchGoogleCredentials ) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt index 7db15aa..624f5cb 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CloudRepo.kt @@ -26,15 +26,16 @@ package io.spine.gradle.publish -import io.spine.gradle.Repository +import io.spine.gradle.repo.Repository /** * CloudRepo Maven repository. * * There is a special treatment for this repository. Usually, fetching and publishing of artifacts * is performed via the same URL. But it is not true for CloudRepo. Fetching is performed via - * the public repository, and publishing via the private one. Their URLs differ in `/public` infix. + * the public repository and publishing via the private one. Their URLs differ in `/public` infix. */ +@Deprecated(message = "Please use `PublishingRepos.cloudArtifactRegistry` instead.") internal object CloudRepo { private const val name = "CloudRepo" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt new file mode 100644 index 0000000..152455d --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/CustomPublicationHandler.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.publish + +import io.spine.gradle.repo.Repository +import org.gradle.api.Project +import org.gradle.api.publish.maven.MavenPublication + +/** + * A handler for custom publications, which are declared under the [publications] + * section of a module. + * + * Such publications should be treated differently than [StandardJavaPublicationHandler], + * which is created for a module. Instead, since the publications are already declared, + * this class only [assigns Maven coordinates][copyProjectAttributes]. + * + * A module which declares custom publications must be specified in + * the [SpinePublishing.modulesWithCustomPublishing] property. + * + * If a module with [publications] declared locally is not specified as one with custom publishing, + * it may cause a name clash between an artifact produced by + * the [standard][org.gradle.api.publish.maven.MavenPublication] publication, and custom ones. + * To have both standard and custom publications, please specify custom artifact IDs or + * classifiers for each custom publication. + * + * @see StandardJavaPublicationHandler + */ +internal class CustomPublicationHandler private constructor( + project: Project, + destinations: Set +) : PublicationHandler(project, destinations) { + + override fun handlePublications() { + project.publications.forEach { + (it as MavenPublication).copyProjectAttributes() + } + } + + companion object : HandlerFactory() { + override fun create( + project: Project, + destinations: Set, + vararg params: Any + ): CustomPublicationHandler = CustomPublicationHandler(project, destinations) + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt index 1072139..df326b8 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/GitHubPackages.kt @@ -26,8 +26,8 @@ package io.spine.gradle.publish -import io.spine.gradle.Credentials -import io.spine.gradle.Repository +import io.spine.gradle.repo.Credentials +import io.spine.gradle.repo.Repository import io.spine.gradle.buildDirectory import net.lingala.zip4j.ZipFile import org.gradle.api.Project @@ -42,12 +42,12 @@ internal object GitHubPackages { */ fun repository(repoName: String): Repository { val githubActor: String = actor() + val url = "https://maven.pkg.github.com/SpineEventEngine/$repoName" return Repository( - name = "GitHub Packages", - releases = "https://maven.pkg.github.com/SpineEventEngine/$repoName", - snapshots = "https://maven.pkg.github.com/SpineEventEngine/$repoName", - credentialValues = { project -> project.credentialsWithToken(githubActor) } - ) + name = "GitHub-Packages", + releases = url, + snapshots = url + ) { project -> project.credentialsWithToken(githubActor) } } private fun actor(): String { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt index 1371184..1bf0c0b 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/JarDsl.kt @@ -26,37 +26,6 @@ package io.spine.gradle.publish -/** - * A DSL element of [SpinePublishing] extension which configures publishing of - * [dokkaKotlinJar] artifact. - * - * This artifact contains Dokka-generated documentation. By default, it is not published. - * - * Take a look at the [SpinePublishing.dokkaJar] for a usage example. - * - * @see [artifacts] - */ -class DokkaJar { - /** - * Enables publishing `JAR`s with Dokka-generated documentation for all published modules. - */ - @Suppress("unused") - @Deprecated("Please use `kotlin` and `java` flags instead.") - var enabled = false - - /** - * Controls whether [dokkaKotlinJar] artifact should be published. - * The default value is `true`. - */ - var kotlin = true - - /** - * Controls whether [dokkaJavaJar] artifact should be published. - * The default value is `false`. - */ - var java = false -} - /** * A DSL element of [SpinePublishing] extension which allows enabling publishing * of [testJar] artifact. @@ -80,88 +49,25 @@ class TestJar { var enabled = false } -/** - * A DSL element of [SpinePublishing] extension which allows disabling publishing - * of [protoJar] artifact. - * - * This artifact contains all the `.proto` definitions from `sourceSets.main.proto`. By default, - * it is published. - * - * Take a look on [SpinePublishing.protoJar] for a usage example. - * - * @see [artifacts] - */ -class ProtoJar { - - /** - * Set of modules, for which a proto JAR will not be published. - */ - var exclusions: Set = emptySet() - - /** - * Disables proto JAR publishing for all published modules. - */ - var disabled = false -} - /** * Flags for turning optional JAR artifacts in a project. + * + * @property sourcesJar Tells whether [sourcesJar] artifact should be published. + * Default value is `true`. + * @property publishTestJar Tells whether [testJar] artifact should be published. */ internal data class JarFlags( - - /** - * Tells whether [sourcesJar] artifact should be published. - * - * Default value is `true`. - */ val sourcesJar: Boolean = true, - - /** - * Tells whether [javadocJar] artifact should be published. - * - * Default value is `true`. - */ - val javadocJar: Boolean = true, - - /** - * Tells whether [protoJar] artifact should be published. - */ - val publishProtoJar: Boolean, - - /** - * Tells whether [testJar] artifact should be published. - */ val publishTestJar: Boolean, - - /** - * Tells whether [dokkaKotlinJar] artifact should be published. - */ - val publishDokkaKotlinJar: Boolean, - - /** - * Tells whether [dokkaJavaJar] artifact should be published. - */ - val publishDokkaJavaJar: Boolean ) { internal companion object { /** * Creates an instance of [JarFlags] for the project with the given name, * taking the setup parameters from JAR DSL elements. */ - fun create( - projectName: String, - protoJar: ProtoJar, - testJar: TestJar, - dokkaJar: DokkaJar - ): JarFlags { - val addProtoJar = (protoJar.exclusions.contains(projectName) || protoJar.disabled).not() + fun create(projectName: String, testJar: TestJar): JarFlags { val addTestJar = testJar.inclusions.contains(projectName) || testJar.enabled - return JarFlags( - sourcesJar = true, - javadocJar = true, - addProtoJar, addTestJar, - dokkaJar.kotlin, dokkaJar.java - ) + return JarFlags(sourcesJar = true, addTestJar) } } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt index 874b7f9..a32e969 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ProtoExts.kt @@ -35,11 +35,15 @@ import org.gradle.api.file.SourceDirectorySet import org.gradle.api.tasks.bundling.Jar /** - * Tells whether there are any Proto sources in "main" source set. + * Tells whether there are any Proto sources in the "main" source set. */ -internal fun Project.hasProto(): Boolean { +fun Project.hasProto(): Boolean { val protoSources = protoSources() - val result = protoSources.any { it.exists() } + val result = protoSources.any { + it.exists() + && it.isDirectory + && it.listFiles()?.isNotEmpty() ?: false + } return result } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt new file mode 100644 index 0000000..5b3bbe1 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationHandler.kt @@ -0,0 +1,259 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.publish + +import LicenseSettings +import io.spine.gradle.isSnapshot +import io.spine.gradle.repo.Repository +import io.spine.gradle.report.pom.InceptionYear +import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.invocation.BuildInvocationDetails +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.support.serviceOf + +/** + * The name of the Maven Publishing Gradle plugin. + */ +private const val MAVEN_PUBLISH = "maven-publish" + +/** + * Abstract base for handlers of publications in a project + * with [spinePublishing] settings declared. + * + * @param project The project to which the handler is applied. + * @param destinations The repositories for publishing artifacts of this project. + * In a multi-module project the destinations can be re-defined by + * specifying custom values in + * the [`spinePublishing`][io.spine.gradle.publish.SpinePublishing.destinations] + * extension applied to the subproject. + */ +sealed class PublicationHandler( + protected val project: Project, + protected var destinations: Set +) { + /** + * Remembers if the [apply] function was called by this handler. + */ + private var applied: Boolean = false + + /** + * Overwrites the [destinations] property with the given set. + */ + fun publishTo(alternativeDestinations: Set) { + if (alternativeDestinations.isEmpty()) { + project.logger.info( + "The project ${project.path} is not going to be published because" + + " the publication handler `${this@PublicationHandler}`" + + " got an empty set of new `destinations`." + ) + } + destinations = alternativeDestinations + } + + /** + * Configures the publication of the associated [project]. + */ + fun apply() { + synchronized(project) { + if (applied) { + return + } + project.run { + // We apply the `maven-publish` plugin for modules with standard + // publishing automatically because they don't need custom DSL + // in their `build.gradle.kts` files. + // All the job is done by the `SpinePublishing` extension and + // `StandardPublicationHandler` instance associated with this project. + if (!hasCustomPublishing) { + apply(plugin = MAVEN_PUBLISH) + } + // And we do not apply the plugin for modules with custom publishing + // because they will need the `maven-publish` DSL to tune the publishing. + // Therefore, we only arrange the execution of our code when the plugin + // is applied. + pluginManager.withPlugin(MAVEN_PUBLISH) { + handlePublications() + registerDestinations() + configurePublishTask(destinations) + applied = true + } + } + } + } + + /** + * Either handles publications already declared in the associated [project] + * or creates new ones. + */ + abstract fun handlePublications() + + /** + * Goes through the [destinations] and registers each as a repository for publishing + * in the given Gradle project. + */ + private fun registerDestinations() { + val repositories = project.publishingExtension.repositories + destinations.forEach { destination -> + repositories.register(project, destination) + } + } + + /** + * Copies the attributes of Gradle [Project] to this [MavenPublication]. + * + * The following project attributes are copied: + * * [group][Project.getGroup]; + * * [version][Project.getVersion]; + * * [description][Project.getDescription]. + * + * Also, this function adds the [artifactPrefix][SpinePublishing.artifactPrefix] to + * the [artifactId][MavenPublication.setArtifactId] of this publication, + * if the prefix is not added yet. + * + * Finally, the Apache Software License 2.0 is set as the only license + * under which the published artifact is distributed. + */ + protected fun MavenPublication.copyProjectAttributes() { + groupId = project.group.toString() + val prefix = project.spinePublishing.artifactPrefix + if (!artifactId.startsWith(prefix)) { + artifactId = prefix + artifactId + } + version = project.version.toString() + pom.description.set(project.description) + pom.inceptionYear.set(InceptionYear.value) + pom.licenses { + license { + name.set(LicenseSettings.name) + url.set(LicenseSettings.url) + distribution.set(LicenseSettings.url) + } + } + pom.scm { + DocumentationSettings.run { + url.set(repoUrl(project)) + connection.set(connectionUrl(project)) + developerConnection.set(developerConnectionUrl(project)) + } + } + } + + /** + * The abstract base for factories producing instances of classes + * derived from [io.spine.gradle.publish.PublicationHandler]. + * + * The factory maintains associations between a path of the project to + * its publication handler. + * + * If the handler already exists, its settings are updated when + * the [serving] factory method is called. + * + * Otherwise, a new handler is created and associated with the project. + * + * @param H The type of the publication handlers produced by this repository. + * @see serving + */ + abstract class HandlerFactory { + + /** + * Maps a project path suffixed with build start time to the associated publication handler. + * + * The suffix after the project path is needed to create a new handler + * for each build. We do not use Guava or other cache expecting the small amount + * of memory consumption of each publication handler. + */ + private val handlers = mutableMapOf() + + /** + * Computes the key for a publication handler taking the [project] and + * its build start time. + */ + private fun createKey(project: Project): String { + val buildService = project.gradle.serviceOf() + val buildStartedMillis = buildService.buildStartedTime + val localTime = java.time.Instant.ofEpochMilli(buildStartedMillis) + val key = "${project.path}-at-$localTime" + return key + } + + /** + * Obtains an instance of [PublicationHandler] for the given project. + * + * If the handler for the given [project] was already created, the handler + * gets new [destinations], [overwriting][publishTo] previously specified. + * + * @return the handler for the given project which would handle publishing to + * the specified [destinations]. + */ + fun serving(project: Project, destinations: Set, vararg params: Any): H { + synchronized(handlers) { + val key = createKey(project) + var handler = handlers[key] + if (handler == null) { + handler = create(project, destinations, *params) + handlers[key] = handler + } else { + handler.publishTo(destinations) + } + return handler + } + } + + /** + * Creates a new publication handler for the given project. + * + * @param project The project to which the handler applies. + * @param destinations The repositories for publishing artifacts of this project. + * @param params Optional parameters to be passed as constructor parameters for + * classes of the type [H]. + */ + protected abstract fun create( + project: Project, + destinations: Set, + vararg params: Any + ): H + } +} + +/** + * Adds a Maven repository to the project specifying credentials, if they are + * [available][Repository.credentials] from the root project. + */ +private fun RepositoryHandler.register(project: Project, repository: Repository) { + val isSnapshot = project.version.toString().isSnapshot() + val credentials = repository.credentials(project.rootProject) + maven { + name = repository.name(isSnapshot) + url = project.uri(repository.target(isSnapshot)) + credentials { + username = credentials?.username + password = credentials?.password + } + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/Publications.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/Publications.kt deleted file mode 100644 index c9acef8..0000000 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/Publications.kt +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2025, TeamDev. All rights reserved. - * - * 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.gradle.publish - -import io.spine.gradle.Repository -import io.spine.gradle.isSnapshot -import org.gradle.api.Project -import org.gradle.api.artifacts.dsl.RepositoryHandler -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.bundling.Jar -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.create - -/** - * The name of the Maven Publishing Gradle plugin. - */ -private const val MAVEN_PUBLISH = "maven-publish" - -/** - * Abstract base for handlers of publications in a project - * with [spinePublishing] settings declared. - */ -internal sealed class PublicationHandler( - protected val project: Project, - private val destinations: Set -) { - - fun apply() = with(project) { - if (!hasCustomPublishing) { - apply(plugin = MAVEN_PUBLISH) - } - - pluginManager.withPlugin(MAVEN_PUBLISH) { - handlePublications() - registerDestinations() - configurePublishTask(destinations) - } - } - - /** - * Either handles publications already declared in the given project, - * or creates new ones. - */ - abstract fun handlePublications() - - /** - * Goes through the [destinations] and registers each as a repository for publishing - * in the given Gradle project. - */ - private fun registerDestinations() { - val repositories = project.publishingExtension.repositories - destinations.forEach { destination -> - repositories.register(project, destination) - } - } - - /** - * Copies the attributes of Gradle [Project] to this [MavenPublication]. - * - * The following project attributes are copied: - * * [group][Project.getGroup]; - * * [version][Project.getVersion]; - * * [description][Project.getDescription]. - * - * Also, this function adds the [artifactPrefix][SpinePublishing.artifactPrefix] to - * the [artifactId][MavenPublication.setArtifactId] of this publication, - * if the prefix is not added yet. - * - * Finally, the Apache Software License 2.0 is set as the only license - * under which the published artifact is distributed. - */ - protected fun MavenPublication.copyProjectAttributes() { - groupId = project.group.toString() - val prefix = project.spinePublishing.artifactPrefix - if (!artifactId.startsWith(prefix)) { - artifactId = prefix + artifactId - } - version = project.version.toString() - pom.description.set(project.description) - - pom.licenses { - license { - name.set("The Apache Software License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - } - } - } -} - -/** - * Adds a Maven repository to the project specifying credentials, if they are - * [available][Repository.credentials] from the root project. - */ -private fun RepositoryHandler.register(project: Project, repository: Repository) { - val isSnapshot = project.version.toString().isSnapshot() - val target = if (isSnapshot) repository.snapshots else repository.releases - val credentials = repository.credentials(project.rootProject) - maven { - url = project.uri(target) - credentials { - username = credentials?.username - password = credentials?.password - } - } -} - -/** - * A publication for a typical Java project. - * - * In Gradle, to publish something, one should create a publication. - * A publication has a name and consists of one or more artifacts plus information about - * those artifacts โ€“ the metadata. - * - * An instance of this class represents [MavenPublication] named "mavenJava". It is generally - * accepted that a publication with this name contains a Java project published to one or - * more Maven repositories. - * - * By default, only a jar with the compilation output of `main` source set and its - * metadata files are published. Other artifacts are specified through the - * [constructor parameter][jarFlags]. Please, take a look on [specifyArtifacts] for additional info. - * - * @param jarFlags - * flags for additional JARs published along with the compilation output. - * @param destinations - * Maven repositories to which the produced artifacts will be sent. - * @see - * Maven Publish Plugin | Publications - */ -internal class StandardJavaPublicationHandler( - project: Project, - private val jarFlags: JarFlags, - destinations: Set, -) : PublicationHandler(project, destinations) { - - /** - * Creates a new "mavenJava" [MavenPublication] in the given project. - */ - override fun handlePublications() { - val jars = project.artifacts(jarFlags) - val publications = project.publications - publications.create("mavenJava") { - copyProjectAttributes() - specifyArtifacts(jars) - } - } - - /** - * Specifies which artifacts this [MavenPublication] will contain. - * - * A typical Maven publication contains: - * - * 1. Jar archives. For example, compilation output, sources, javadoc, etc. - * 2. Maven metadata file that has the ".pom" extension. - * 3. Gradle's metadata file that has the ".module" extension. - * - * Metadata files contain information about a publication itself, its artifacts, and their - * dependencies. Presence of ".pom" file is mandatory for publication to be consumed by - * `mvn` build tool itself or other build tools that understand Maven notation (Gradle, Ivy). - * The presence of ".module" is optional, but useful when a publication is consumed by Gradle. - * - * @see Maven โ€“ POM Reference - * @see - * Understanding Gradle Module Metadata - */ - private fun MavenPublication.specifyArtifacts(jars: Set>) { - - /* "java" component provides a jar with compilation output of "main" source set. - It is NOT defined as another `Jar` task intentionally. Doing that will leave the - publication without correct ".pom" and ".module" metadata files generated. - */ - val javaComponent = project.components.findByName("java") - javaComponent?.let { - from(it) - } - - /* Other artifacts are represented by `Jar` tasks. Those artifacts don't bring any other - metadata in comparison with `Component` (such as dependencies notation). - */ - jars.forEach { - artifact(it) - } - } -} - -/** - * A handler for custom publications, which are declared under the [publications] - * section of a module. - * - * Such publications should be treated differently than [StandardJavaPublicationHandler], - * which is created for a module. Instead, since the publications are already declared, - * this class only [assigns maven coordinates][copyProjectAttributes]. - * - * A module which declares custom publications must be specified in - * the [SpinePublishing.modulesWithCustomPublishing] property. - * - * If a module with [publications] declared locally is not specified as one with custom publishing, - * it may cause a name clash between an artifact produced by the [standard][MavenPublication] - * publication, and custom ones. To have both standard and custom publications, - * please specify custom artifact IDs or classifiers for each custom publication. - */ -internal class CustomPublicationHandler(project: Project, destinations: Set) : - PublicationHandler(project, destinations) { - - override fun handlePublications() { - project.publications.forEach { - (it as MavenPublication).copyProjectAttributes() - } - } -} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt index 62c639b..efe51d6 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingExts.kt @@ -26,8 +26,9 @@ package io.spine.gradle.publish -import dokkaKotlinJar -import io.spine.gradle.Repository +import htmlDocsJar +import io.spine.gradle.isSnapshot +import io.spine.gradle.repo.Repository import io.spine.gradle.sourceSets import java.util.* import org.gradle.api.InvalidUserDataException @@ -35,6 +36,7 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.publish.PublicationContainer import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar @@ -57,6 +59,13 @@ internal val Project.publishingExtension: PublishingExtension internal val Project.publications: PublicationContainer get() = publishingExtension.publications +/** + * Obtains an instance, if available, of [SpinePublishing] extension + * applied to this project. + */ +internal val Project.localSpinePublishing: SpinePublishing? + get() = extensions.findByType() + /** * Obtains [SpinePublishing] extension from this [Project]. * @@ -65,7 +74,7 @@ internal val Project.publications: PublicationContainer */ internal val Project.spinePublishing: SpinePublishing get() { - val local = this.extensions.findByType() + val local = localSpinePublishing if (local != null) { return local } @@ -78,9 +87,16 @@ internal val Project.spinePublishing: SpinePublishing /** * Tells if this project has custom publishing. + * + * For a multi-module project this is checked by presence of this project + * in the list of [SpinePublishing.modulesWithCustomPublishing] of the root project. + * + * In a single-module project, the value of the [SpinePublishing.customPublishing] + * property is returned. */ internal val Project.hasCustomPublishing: Boolean - get() = spinePublishing.modulesWithCustomPublishing.contains(name) + get() = rootProject.spinePublishing.modulesWithCustomPublishing.contains(name) + || spinePublishing.customPublishing private const val PUBLISH_TASK = "publish" @@ -93,7 +109,7 @@ private const val PUBLISH_TASK = "publish" * Please note, task execution would not copy publications to the local Maven cache. * * @see - * Tasks | Maven Publish Plugin + * Tasks | The Maven Publish Plugin */ internal val TaskContainer.publish: TaskProvider get() = named(PUBLISH_TASK) @@ -140,14 +156,45 @@ private fun TaskContainer.getOrCreatePublishTask(): TaskProvider = register(PUBLISH_TASK) } +@Suppress( + /* Several types of exceptions may be thrown, + and Kotlin does not have a multi-catch support yet. */ + "TooGenericExceptionCaught" +) private fun TaskContainer.registerCheckCredentialsTask( - destinations: Set -): TaskProvider = - register("checkCredentials") { - doLast { - destinations.forEach { it.ensureCredentials(project) } + destinations: Set, +): TaskProvider { + val checkCredentials = "checkCredentials" + try { + // The result of this call is ignored intentionally. + // + // We expect this line to fail with the exception + // in case the task with this name is NOT registered. + // + // Otherwise, we need to replace the existing task + // to avoid checking the credentials + // for some previously asked `destinations`. + named(checkCredentials) + val toConfigure = replace(checkCredentials) + toConfigure.doLastCredentialsCheck(destinations) + return named(checkCredentials) + } catch (_: Exception) { + return register(checkCredentials) { doLastCredentialsCheck(destinations) } + } +} + +private fun Task.doLastCredentialsCheck(destinations: Set) { + doLast { + if (logger.isDebugEnabled) { + val isSnapshot = project.version.toString().isSnapshot() + val destinationsStr = destinations.joinToString(", ") { it.target(isSnapshot) } + logger.debug( + "Project '${project.name}': checking the credentials for repos: $destinationsStr." + ) } + destinations.forEach { it.ensureCredentials(project) } } +} private fun Repository.ensureCredentials(project: Project) { val credentials = credentials(project) @@ -175,8 +222,8 @@ fun TaskContainer.excludeGoogleProtoFromArtifacts() { * Locates or creates `sourcesJar` task in this [Project]. * * The output of this task is a `jar` archive. The archive contains sources from `main` source set. - * The task makes sure that sources from the directories below will be included into - * a resulted archive: + * The task makes sure that sources from the directories below will be included + * in the resulting archive: * * - Kotlin * - Java @@ -185,7 +232,7 @@ fun TaskContainer.excludeGoogleProtoFromArtifacts() { * Java and Kotlin sources are default to `main` source set since it is created by `java` plugin. * For Proto sources to be included โ€“ [special treatment][protoSources] is needed. */ -internal fun Project.sourcesJar(): TaskProvider = tasks.getOrCreate("sourcesJar") { +fun Project.sourcesJar(): TaskProvider = tasks.getOrCreate("sourcesJar") { dependOnGenerateProto() archiveClassifier.set("sources") from(sourceSets["main"].allSource) // Puts Java and Kotlin sources. @@ -199,7 +246,7 @@ internal fun Project.sourcesJar(): TaskProvider = tasks.getOrCreate("source * The output of this task is a `jar` archive. The archive contains only * [Proto sources][protoSources] from `main` source set. */ -internal fun Project.protoJar(): TaskProvider = tasks.getOrCreate("protoJar") { +fun Project.protoJar(): TaskProvider = tasks.getOrCreate("protoJar") { dependOnGenerateProto() archiveClassifier.set("proto") from(protoSources()) @@ -220,14 +267,14 @@ internal fun Project.testJar(): TaskProvider = tasks.getOrCreate("testJar") * Locates or creates `javadocJar` task in this [Project]. * * The output of this task is a `jar` archive. The archive contains Javadoc, - * generated upon Java sources from `main` source set. If javadoc for Kotlin is also needed, - * apply Dokka plugin. It tunes `javadoc` task to generate docs upon Kotlin sources as well. + * generated upon Java sources from `main` source set. If Javadoc for Kotlin is also needed, + * apply the Dokka plugin. It tunes `javadoc` task to generate docs upon Kotlin sources as well. */ fun Project.javadocJar(): TaskProvider = tasks.getOrCreate("javadocJar") { archiveClassifier.set("javadoc") - val javadocFiles = layout.buildDirectory.files("/docs/javadoc") + val javadocFiles = layout.buildDirectory.dir("dokka/javadoc") from(javadocFiles) - dependsOn("javadoc") + dependsOn("dokkaGeneratePublicationJavadoc") } internal fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): TaskProvider = @@ -254,12 +301,12 @@ internal fun Project.artifacts(jarFlags: JarFlags): Set> { tasks.add(sourcesJar()) } - if (jarFlags.javadocJar) { - tasks.add(javadocJar()) - } + tasks.add(javadocJar()) + tasks.add(htmlDocsJar()) + // We don't want to have an empty "proto.jar" when a project doesn't have any Proto files. - if (hasProto() && jarFlags.publishProtoJar) { + if (hasProto()) { tasks.add(protoJar()) } @@ -269,10 +316,22 @@ internal fun Project.artifacts(jarFlags: JarFlags): Set> { tasks.add(testJar()) } - if (jarFlags.publishDokkaKotlinJar) { - tasks.add(dokkaKotlinJar()) - } - return tasks } +/** + * Adds the source code and documentation JARs to the publication. + */ +@Suppress("unused") +fun MavenPublication.addSourceAndDocJars(project: Project) { + val tasks = mutableSetOf>() + tasks.add(project.sourcesJar()) + tasks.add(project.javadocJar()) + tasks.add(project.htmlDocsJar()) + if (project.hasProto()) { + tasks.add(project.protoJar()) + } + tasks.forEach { + artifact(it) + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt index f383e68..eea6dc1 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/PublishingRepos.kt @@ -26,7 +26,7 @@ package io.spine.gradle.publish -import io.spine.gradle.Repository +import io.spine.gradle.repo.Repository /** * Repositories to which we may publish. diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt new file mode 100644 index 0000000..87157fd --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/ShadowJarExts.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.publish + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +/** + * Calls [ShadowJar.mergeServiceFiles] for the files we use in the Spine SDK. + */ +fun ShadowJar.handleMergingServiceFiles() { + ServiceFiles.all.forEach { + append(it) + } +} + +@Suppress("ConstPropertyName") +private object ServiceFiles { + + /** + * Files containing references to descriptor set files. + */ + private const val descriptorSetReferences = "desc.ref" + + private const val servicesDir = "META-INF/services" + /** + * Providers of custom Protobuf options introduced by the libraries. + */ + private const val optionProviders = "$servicesDir/io.spine.option.OptionsProvider" + + /** + * KSP symbol processor provider. + */ + private const val kspSymbolProcessorProviders = + "$servicesDir/com.google.devtools.ksp.KspSymbolProcessorProvider" + + /** + * Message routing setup classes generated by the Compiler for JVM. + */ + private const val routeSetupPackage = "io.spine.server.route.setup" + private const val routeSetupPrefix = "$servicesDir/$routeSetupPackage" + private const val commandRoutingSetupClasses = "$routeSetupPrefix.CommandRoutingSetup" + private const val eventRoutingSetupClasses = "$routeSetupPrefix.EventRoutingSetup" + private const val stateRoutingSetupClasses = "$routeSetupPrefix.StateRoutingSetup" + + val all = arrayOf( + descriptorSetReferences, + optionProviders, + kspSymbolProcessorProviders, + commandRoutingSetupClasses, + eventRoutingSetupClasses, + stateRoutingSetupClasses + ) +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt index 2f74536..480175f 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/SpinePublishing.kt @@ -28,9 +28,7 @@ package io.spine.gradle.publish -import dokkaJavaJar -import dokkaKotlinJar -import io.spine.gradle.Repository +import io.spine.gradle.repo.Repository import org.gradle.api.Project import org.gradle.api.publish.maven.plugins.MavenPublishPlugin import org.gradle.kotlin.dsl.apply @@ -44,75 +42,108 @@ import org.gradle.kotlin.dsl.findByType * * The extension can be configured for single- and multi-module projects. * + * ## Using in a multi-module project + * * When used with a multi-module project, the extension should be opened in a root project's * build file. The published modules are specified explicitly by their names: * - * ``` + * ```kotlin * spinePublishing { * modules = setOf( * "subprojectA", * "subprojectB", * ) - * destinations = setOf( - * PublishingRepos.cloudRepo, - * PublishingRepos.cloudArtifactRegistry, + * destinations = PublishingRepos.run { setOf( + * cloudArtifactRegistry, + * gitHub("") // The name of the GitHub repository of the project. + * )} + * } + * ``` + * + * ### Filtering out test-only modules + * + * Sometimes a functional or an integration test requires a significant amount of + * configuration code which is better understood when isolated into a separate module. + * Conventionally, we use the `-tests` suffix for naming such modules. + * + * In order to avoid publishing of such a test-only module, we use the following extensions + * for the Gradle [Project] class: [productionModules], [productionModuleNames]. + * So the above code for specifying the modules to publish could be rewritten as follows: + * + * ```kotlin + * spinePublishing { + * modules = productionModuleNames.toSet() + * } + * ``` + * This code works for most of the projects. + * + * ### Arranging custom publishing for a module + * ```kotlin + * + * 1. Modify the list of standardly published modules in the root project like this: + * + * ```kotlin + * spinePublishing { + * modules = productionModuleNames + * .minus("my-custom-module") + * .toSet() + * + * modulesWithCustomPublishing = setOf( + * "my-custom-module" * ) + * + * // ... * } * ``` + * 2. Arrange the custom publishing in the `my-custom-module` project. + * + * ## Using in a single-module project * * When used with a single-module project, the extension should be opened in a project's build file. * Only destinations should be specified: * - * ``` + * ```kotlin * spinePublishing { - * destinations = setOf( - * PublishingRepos.cloudRepo, - * PublishingRepos.cloudArtifactRegistry, - * ) + * destinations = PublishingRepos.run { setOf( + * cloudArtifactRegistry, + * gitHub("") + * )} * } * ``` * - * It is worth to mention, that publishing of a module can be configured only from a single place. + * ## Publishing modules + * + * It is worth mentioning that publishing of a module can be configured only from a single place. * For example, declaring `subprojectA` as published in a root project and opening * `spinePublishing` extension within `subprojectA` itself would lead to an exception. * - * In Gradle, in order to publish something somewhere one should create a publication. In each + * In Gradle, in order to publish something somewhere, one should create a publication. In each * of published modules, the extension will create a [publication][StandardJavaPublicationHandler] - * named "mavenJava". All artifacts, published by this extension belong to this publication. + * named "mavenJava". All artifacts published by this extension belong to this publication. * - * By default, along with the compilation output of "main" source set, the extension publishes + * ## Published artifacts + * + * By default, along with the compilation output of the `main` source set, the extension publishes * the following artifacts: * - * 1. [sourcesJar] โ€“ sources from "main" source set. Includes "hand-made" Java, - * Kotlin and Proto files. In order to include the generated code into this artifact, a module - * should specify those files as a part of "main" source set. - * - * Here's an example of how to do that: - * - * ``` - * sourceSets { - * val generatedDir by extra("$projectDir/generated") - * val generatedSpineDir by extra("$generatedDir/main/java") - * main { - * java.srcDir(generatedSpineDir) - * } - * } - * ``` - * 2. [protoJar] โ€“ only Proto sources from "main" source set. It's published only if - * Proto files are actually present in the source set. Publication of this artifact is optional - * and can be disabled via [SpinePublishing.protoJar]. - * 3. [javadocJar] - javadoc, generated upon Java sources from "main" source set. - * If javadoc for Kotlin is also needed, apply Dokka plugin. It tunes `javadoc` task to generate - * docs upon Kotlin sources as well. - * 4. [dokkaKotlinJar] - documentation generated by Dokka for Kotlin and Java sources + * 1. [sourcesJar] โ€” sources from the `main` source set. Includes handcrafted and generated + * code in Java, Kotlin, and `.proto` files. + * + * 2. [protoJar] โ€“ only `.proto` sources from the `main` source set. It is published only if + * Proto files are actually present in the source set. + * + * 3. [javadocJar] โ€” Javadoc, generated upon Java sources from the `main` source set. + * If Javadoc for Kotlin is also needed, apply the Dokka plugin. + * It tunes the `javadoc` task to generate docs upon Kotlin sources as well. + * + * 4. [htmlDocsJar] โ€” documentation generated by Dokka for Kotlin and Java sources * using the Kotlin API mode. - * 5. [dokkaJavaJar] - documentation generated by Dokka for Kotlin and Java sources - * * using the Java API mode. * * Additionally, [testJar] artifact can be published. This artifact contains compilation output - * of "test" source set. Use [SpinePublishing.testJar] to enable its publishing. + * of the `test` source set. Use [SpinePublishing.testJar] to enable its publishing. * * @see [artifacts] + * @see SpinePublishing */ fun Project.spinePublishing(block: SpinePublishing.() -> Unit) { apply() @@ -127,11 +158,19 @@ fun Project.spinePublishing(block: SpinePublishing.() -> Unit) { } /** - * A Gradle extension for setting up publishing of spine modules using `maven-publish` plugin. + * A Gradle extension for setting up publishing of modules of Spine SDK modules + * using `maven-publish` plugin. + * + * ### Implementation Note * - * @param project - * a project in which the extension is opened. By default, this project will be - * published as long as a [set][modules] of modules to publish is not specified explicitly. + * This extension is overloaded with responsibilities. + * It basically does what an extension AND a Gradle plugin would normally do. + * + * We [should introduce a plugin class](https://github.com/SpineEventEngine/config/issues/562) + * and move the code related to creating tasks or setting dependencies between them into the plugin. + * + * @param project The project in which the extension is opened. By default, this project will be + * published as long as a [set][modules] of modules to publish is not specified explicitly. * * @see spinePublishing */ @@ -145,9 +184,7 @@ open class SpinePublishing(private val project: Project) { const val DEFAULT_PREFIX = "spine-" } - private val protoJar = ProtoJar() private val testJar = TestJar() - private val dokkaJar = DokkaJar() /** * Set of modules to be published. @@ -163,18 +200,27 @@ open class SpinePublishing(private val project: Project) { var modules: Set = emptySet() /** - * Controls whether the published module needs standard publications. + * Controls whether the [module][project] needs standard publications. + * + * Default value is `false`. * - * If `true`, the module should configure publications on its own. - * Otherwise, the extension will configure standard [ones][StandardJavaPublicationHandler]. + * In a single module [project], settings this property to `true` it tells + * that the project configures the publication in a specific way and + * [CustomPublicationHandler] should be used. + * Otherwise, the extension will configure the + * [standard publication][StandardJavaPublicationHandler]. * - * This property is analogue of [modulesWithCustomPublishing] for projects, + * This property is an analogue of [modulesWithCustomPublishing] in + * [multi-module][Project.getSubprojects] projects, * for which [spinePublishing] is configured individually. * - * Setting of this property and having a non-empty [modules] will lead - * to an exception. + * Setting of this property to `true` and having a non-empty [modules] property + * in the project to which the extension is applied will lead to [IllegalStateException]. * - * Default value is `false`. + * Settings this property to `true` in a subproject serves only the documentation purposes. + * This subproject still must be listed in the [modulesWithCustomPublishing] property in + * the extension of the [rootProject][Project.getRootProject], so that its publication + * can be configured in a specific way. */ var customPublishing = false @@ -191,62 +237,23 @@ open class SpinePublishing(private val project: Project) { * Usually, Spine-related projects are published to one or more repositories, * declared in [PublishingRepos]: * - * ``` - * destinations = setOf( - * PublishingRepos.cloudRepo, - * PublishingRepos.cloudArtifactRegistry, - * PublishingRepos.gitHub("base"), - * ) + * ```kotlin + * destinations = PublishingRepos.run { setOf( + * cloudArtifactRegistry, + * gitHub("") // The name of the GitHub repository of the project. + * )} * ``` * - * Empty by default. + * If the property is not initialized, the destinations will be taken from + * the parent project. */ - var destinations: Set = emptySet() + lateinit var destinations: Set /** * A prefix to be added before the name of each artifact. */ var artifactPrefix: String = DEFAULT_PREFIX - /** - * Allows disabling publishing of [protoJar] artifact, containing all Proto sources - * from `sourceSets.main.proto`. - * - * Here's an example of how to disable it for some of the published modules: - * - * ``` - * spinePublishing { - * modules = setOf( - * "subprojectA", - * "subprojectB", - * ) - * protoJar { - * exclusions = setOf( - * "subprojectB", - * ) - * } - * } - * ``` - * - * For all modules, or when the extension is configured within a published module itself: - * - * ``` - * spinePublishing { - * protoJar { - * disabled = true - * } - * } - * ``` - * - * The resulting artifact is available under "proto" classifier. - * For example, in Gradle 7+, one could depend on it like this: - * - * ``` - * implementation("io.spine:spine-client:$version@proto") - * ``` - */ - fun protoJar(block: ProtoJar.() -> Unit) = protoJar.run(block) - /** * Allows enabling publishing of [testJar] artifact, containing compilation output * of "test" source set. @@ -277,8 +284,8 @@ open class SpinePublishing(private val project: Project) { * } * ``` * - * The resulting artifact is available under "test" classifier. For example, - * in Gradle 7+, one could depend on it like this: + * The resulting artifact is available under the "test" classifier. + * For example, in Gradle 7+, one could depend on it like this: * * ``` * implementation("io.spine:spine-client:$version@test") @@ -286,34 +293,6 @@ open class SpinePublishing(private val project: Project) { */ fun testJar(block: TestJar.() -> Unit) = testJar.run(block) - /** - * Configures publishing of [dokkaKotlinJar] and [dokkaJavaJar] artifacts, - * containing Dokka-generated documentation. - * - * By default, publishing of the [dokkaKotlinJar] artifact is enabled, and [dokkaJavaJar] - * is disabled. - * - * Remember that the Dokka Gradle plugin should be applied to publish this artifact as it is - * produced by the `dokkaHtml` task. It can be done by using the - * [io.spine.dependency.build.Dokka] dependency object or by applying the - * `buildSrc/src/main/kotlin/dokka-for-kotlin` or - * `buildSrc/src/main/kotlin/dokka-for-java` script plugins. - * - * Here's an example of how to use this option: - * - * ``` - * spinePublishing { - * dokkaJar { - * kotlin = false - * java = true - * } - * } - * ``` - * - * The resulting artifact is available under "dokka" classifier. - */ - fun dokkaJar(block: DokkaJar.() -> Unit) = dokkaJar.run(block) - /** * Called to notify the extension that its configuration is completed. * @@ -321,14 +300,13 @@ open class SpinePublishing(private val project: Project) { * `maven-publish` plugin for each published module. */ internal fun configured() { - ensureProtoJarExclusionsArePublished() ensureTestJarInclusionsArePublished() ensureModulesNotDuplicated() ensureCustomPublishingNotMisused() val projectsToPublish = projectsToPublish() projectsToPublish.forEach { project -> - val jarFlags = JarFlags.create(project.name, protoJar, testJar, dokkaJar) + val jarFlags = JarFlags.create(project.name, testJar) project.setUpPublishing(jarFlags) } } @@ -367,21 +345,22 @@ open class SpinePublishing(private val project: Project) { * * We selected to use [Project.afterEvaluate] so that we can configure publishing of multiple * modules from a root project. When we do this, we configure publishing for a module, - * build file of which has not been even evaluated yet. + * a build file of which has not been even evaluated yet. * * The simplest example here is specifying of `version` and `group` for Maven coordinates. - * Let's suppose, they are declared in a module's build file. It is a common practice. - * But publishing of the module is configured from a root project's build file. By the time, - * when we need to specify them, we just don't know them. As a result, we have to use - * [Project.afterEvaluate] in order to guarantee that a module will be configured by the time - * we configure publishing for it. + * Let's suppose they are declared in a module's build file. It is a common practice. + * But publishing of the module is configured from a root project's build file. + * By the time when we need to specify them, we just don't know them. + * As the result, we have to use [Project.afterEvaluate] in order to guarantee that + * the module will be configured by the time we configure publishing for it. */ private fun Project.setUpPublishing(jarFlags: JarFlags) { val customPublishing = modulesWithCustomPublishing.contains(name) || customPublishing + val destinations = project.publishTo() val handler = if (customPublishing) { - CustomPublicationHandler(project, destinations) + CustomPublicationHandler.serving(project, destinations) } else { - StandardJavaPublicationHandler(project, jarFlags, destinations) + StandardJavaPublicationHandler.serving(project, destinations, jarFlags) } afterEvaluate { handler.apply() @@ -389,27 +368,31 @@ open class SpinePublishing(private val project: Project) { } /** - * Obtains an artifact ID for the given project. + * Obtains the set of repositories for publishing. * - * It consists of a project's name and [prefix][artifactPrefix]: - * ``. + * If there is a local instance of [io.spine.gradle.publish.SpinePublishing] extension, + * the [destinations] are obtained from this instance. + * Otherwise, the function attempts to obtain it from a [parent project][Project.getParent]. + * If there is no a parent project, an empty set is returned. + * + * The normal execution should end up at the root project of a multi-module project + * if there are no custom destinations specified by the local extension. */ - fun artifactId(project: Project): String = "$artifactPrefix${project.name}" + private fun Project.publishTo(): Set { + val ext = localSpinePublishing + if (ext != null && ext::destinations.isInitialized) { + return destinations + } + return parent?.publishTo() ?: emptySet() + } /** - * Ensures that all modules, marked as excluded from [protoJar] publishing, - * are actually published. + * Obtains an artifact ID for the given project. * - * It makes no sense to tell a module don't publish [protoJar] artifact, if the module is not - * published at all. + * It consists of a project's name and [prefix][artifactPrefix]: + * ``. */ - private fun ensureProtoJarExclusionsArePublished() { - val nonPublishedExclusions = protoJar.exclusions.minus(modules) - if (nonPublishedExclusions.isNotEmpty()) { - throw IllegalStateException("One or more modules are marked as `excluded from proto " + - "JAR publication`, but they are not even published: $nonPublishedExclusions") - } - } + fun artifactId(project: Project): String = "$artifactPrefix${project.name}" /** * Ensures that all modules, marked as included into [testJar] publishing, @@ -431,7 +414,7 @@ open class SpinePublishing(private val project: Project) { /** * Ensures that publishing of a module is configured only from a single place. * - * We allow configuration of publishing from two places - a root project and module itself. + * We allow configuration of publishing from two places - a root project and the module itself. * Here we verify that publishing of a module is not configured in both places simultaneously. */ private fun ensureModulesNotDuplicated() { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt b/buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt new file mode 100644 index 0000000..17648f8 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/publish/StandardJavaPublicationHandler.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.publish + +import io.spine.gradle.repo.Repository +import org.gradle.api.Project +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.create + +/** + * A publication for a typical Java project. + * + * In Gradle, to publish something, one should create a publication. + * A publication has a name and consists of one or more artifacts plus information about + * those artifacts โ€“ the metadata. + * + * An instance of this class represents + * [MavenPublication][org.gradle.api.publish.maven.MavenPublication] + * named [`"mavenJava"`][PUBLICATION_NAME]. + * It is generally accepted that a publication with this name contains a Java project + * published to one or more Maven repositories. + * + * By default, only a jar with the compilation output of `main` source set and its + * metadata files are published. Other artifacts are specified through the + * [constructor parameter][jarFlags]. + * Please take a look on [specifyArtifacts] for additional info. + * + * @param jarFlags The flags for additional JARs published along with the compilation output. + * @param destinations Maven repositories to which the produced artifacts will be sent. + * @see + * The Maven Publish Plugin | Publications + * @see CustomPublicationHandler + */ +class StandardJavaPublicationHandler private constructor( + project: Project, + private val jarFlags: JarFlags, + destinations: Set, +) : PublicationHandler(project, destinations) { + + companion object : HandlerFactory() { + + /** + * The name of the publication created by [StandardJavaPublicationHandler]. + */ + const val PUBLICATION_NAME = "mavenJava" + + override fun create( + project: Project, + destinations: Set, + vararg params: Any + ): StandardJavaPublicationHandler { + return StandardJavaPublicationHandler(project, params[0] as JarFlags, destinations) + } + } + + /** + * Creates a new `"mavenJava"` [MavenPublication][org.gradle.api.publish.maven.MavenPublication] + * in the [project] associated with this publication handler. + */ + override fun handlePublications() { + val jars = project.artifacts(jarFlags) + val publications = project.publications + publications.create(PUBLICATION_NAME) { + copyProjectAttributes() + specifyArtifacts(jars) + } + } + + /** + * Specifies which artifacts this [MavenPublication] will contain. + * + * A typical Maven publication contains: + * + * 1. Jar archives. For example, compilation output, sources, javadoc, etc. + * 2. Maven metadata file that has the ".pom" extension. + * 3. Gradle's metadata file that has the ".module" extension. + * + * Metadata files contain information about a publication itself, its artifacts, and their + * dependencies. Presence of ".pom" file is mandatory for publication to be consumed by + * `mvn` build tool itself or other build tools that understand Maven notation (Gradle, Ivy). + * The presence of ".module" is optional, but useful when a publication is consumed by Gradle. + * + * @see Maven โ€“ POM Reference + * @see + * Understanding Gradle Module Metadata + */ + private fun MavenPublication.specifyArtifacts(jars: Set>) { + + /* + "java" component provides a jar with compilation output of "main" source set. + It is NOT defined as another `Jar` task intentionally. Doing that will leave the + publication without correct ".pom" and ".module" metadata files generated. + */ + val javaComponent = project.components.findByName("java") + javaComponent?.let { + from(it) + } + + /* + Other artifacts are represented by `Jar` tasks. Those artifacts do not bring any other + metadata in comparison with `Component` (such as the `dependencies` notation). + */ + jars.forEach { + artifact(it) + } + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/dokka/TaskContainerExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Credentials.kt similarity index 83% rename from buildSrc/src/main/kotlin/io/spine/gradle/dokka/TaskContainerExtensions.kt rename to buildSrc/src/main/kotlin/io/spine/gradle/repo/Credentials.kt index 02deead..1624b38 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/dokka/TaskContainerExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Credentials.kt @@ -24,13 +24,12 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.gradle.dokka - -import org.gradle.api.tasks.TaskContainer -import org.jetbrains.dokka.gradle.DokkaTask +package io.spine.gradle.repo /** - * Finds the `dokkaHtml` Gradle task. + * Password credentials for a Maven repository. */ -@Suppress("unused") -fun TaskContainer.dokkaHtmlTask() = this.getByName("dokkaHtml") as DokkaTask +data class Credentials( + val username: String?, + val password: String? +) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/RepoSlug.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/RepoSlug.kt similarity index 88% rename from buildSrc/src/main/kotlin/io/spine/gradle/RepoSlug.kt rename to buildSrc/src/main/kotlin/io/spine/gradle/repo/RepoSlug.kt index 71fbac3..d4a6bfd 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/RepoSlug.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/RepoSlug.kt @@ -24,13 +24,14 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.gradle +package io.spine.gradle.repo import org.gradle.api.GradleException /** * A name of a repository. */ +@Suppress("unused") class RepoSlug(val value: String) { companion object { @@ -44,12 +45,12 @@ class RepoSlug(val value: String) { /** * Reads `REPO_SLUG` environment variable and returns its value. * - * In case it is not set, a [GradleException] is thrown. + * In case it is not set, a [org.gradle.api.GradleException] is thrown. */ fun fromVar(): RepoSlug { val envValue = System.getenv(environmentVariable) if (envValue.isNullOrEmpty()) { - throw GradleException("`REPO_SLUG` environment variable is not set.") + throw GradleException("`$environmentVariable` environment variable is not set.") } return RepoSlug(envValue) } @@ -61,6 +62,6 @@ class RepoSlug(val value: String) { * Returns the GitHub URL to the project repository. */ fun gitHost(): String { - return "git@github.com-publish:${value}.git" + return "git@github-publish:${value}.git" } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt new file mode 100644 index 0000000..de91e33 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repositories.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@file:Suppress("TooManyFunctions") // Deprecated functions will be kept for a while. + +package io.spine.gradle.repo + +import io.spine.gradle.publish.PublishingRepos +import java.net.URI +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.kotlin.dsl.maven + +/** + * Registers the standard set of Maven repositories. + * + * To be used in `buildscript` clauses when a fully-qualified call must be made. + */ +@Suppress("unused") +@Deprecated( + message = "Please use `standardSpineSdkRepositories()`.", + replaceWith = ReplaceWith("standardSpineSdkRepositories()") +) +fun doApplyStandard(repositories: RepositoryHandler) = repositories.standardToSpineSdk() + +/** + * A scrambled version of PAT generated with the only "read:packages" scope. + * + * The scrambling around PAT is necessary because GitHub analyzes commits for the presence + * of tokens and invalidates them. + * + * @see + * How to make GitHub packages to the public + */ +private object Pat { + private const val shade = "_phg->8YlN->MFRA->gxIk->HVkm->eO6g->FqHJ->z8MS->H4zC->ZEPq" + private const val separator = "->" + private val chunks: Int = shade.split(separator).size - 1 + + fun credentials(): Credentials { + val pass = shade.replace(separator, "").splitAndReverse(chunks, "") + return Credentials("public", pass) + } + + /** + * Splits this string to the chunks, reverses each chunk, and joins them + * back to a string using the [separator]. + */ + private fun String.splitAndReverse(numChunks: Int, separator: String): String { + check(length / numChunks >= 2) { + "The number of chunks is too big. Must be <= ${length / 2}." + } + val chunks = chunked(length / numChunks) + val reversedChunks = chunks.map { chunk -> chunk.reversed() } + return reversedChunks.joinToString(separator) + } +} + +/** + * Adds a read-only view to all artifacts of the SpineEventEngine + * GitHub organization. + */ +fun RepositoryHandler.spineArtifacts(): MavenArtifactRepository = maven { + url = URI("https://maven.pkg.github.com/SpineEventEngine/*") + includeSpineOnly() + val pat = Pat.credentials() + credentials { + username = pat.username + password = pat.password + } +} + +val RepositoryHandler.intellijReleases: MavenArtifactRepository + get() = maven("https://www.jetbrains.com/intellij-repository/releases") + +val RepositoryHandler.intellijDependencies: MavenArtifactRepository + get() = maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") { + content { + includeGroupByRegex("com\\.jetbrains\\.intellij.*") + includeGroupByRegex("org\\.jetbrains\\.intellij.*") + includeGroupByRegex("com\\.intellij.*") + } + } + +/** + * Applies repositories commonly used by Spine Event Engine projects. + */ +fun RepositoryHandler.standardToSpineSdk() { + spineArtifacts() + + @Suppress("DEPRECATION") // Still use `CloudRepo` for earlier versions. + val spineRepos = listOf( + Repos.spine, + Repos.spineSnapshots, + Repos.artifactRegistry, + Repos.artifactRegistrySnapshots + ) + + spineRepos + .map { URI(it) } + .forEach { + maven { + url = it + includeSpineOnly() + } + } + + intellijReleases + intellijDependencies + + maven { + url = URI(Repos.sonatypeSnapshots) + } + + mavenCentral() + gradlePluginPortal() + mavenLocal().includeSpineOnly() +} + +@Deprecated( + message = "Please use `standardToSpineSdk() instead.", + replaceWith = ReplaceWith("standardToSpineSdk()") +) +fun RepositoryHandler.applyStandard() = this.standardToSpineSdk() + +/** + * Defines names of additional repositories commonly used in the Spine SDK projects. + * + * @see [applyStandard] + */ +@Suppress( + "DEPRECATION" /* Still need to use `CloudRepo` for older versions. */, + "ConstPropertyName" // https://bit.ly/kotlin-prop-names +) +private object Repos { + @Deprecated(message = "Please use `cloudArtifactRegistry.releases` instead.") + val spine = io.spine.gradle.publish.CloudRepo.published.target(snapshots = false) + + @Deprecated(message = "Please use `artifactRegistry.snapshots` instead.") + val spineSnapshots = io.spine.gradle.publish.CloudRepo.published.target(snapshots = true) + + val artifactRegistry = PublishingRepos.cloudArtifactRegistry.target(snapshots = false) + val artifactRegistrySnapshots = PublishingRepos.cloudArtifactRegistry.target(snapshots = true) + + const val sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots" +} + +/** + * Narrows down the search for this repository to Spine-related artifact groups. + */ +private fun MavenArtifactRepository.includeSpineOnly() { + content { + includeGroupByRegex("io\\.spine.*") + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt new file mode 100644 index 0000000..a586ffd --- /dev/null +++ b/buildSrc/src/main/kotlin/io/spine/gradle/repo/Repository.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.gradle.repo + +import java.io.File +import java.util.Properties +import org.gradle.api.Project + +/** + * A Maven repository. + * + * @param name The human-readable name which is also used in the publishing task names + * for identifying the target repository. + * The name must match the [regex]. + * @param releases The URL for publishing release versions of artifacts. + * @param snapshots The URL for publishing [snapshot][io.spine.gradle.isSnapshot] versions. + * @param credentialsFile The path to the file which contains the credentials for the registry. + * @param credentialValues The function to obtain an instance of [Credentials] from + * a Gradle [Project], if [credentialsFile] is not specified. + */ +data class Repository( + private val name: String, + private val releases: String, + private val snapshots: String, + private val credentialsFile: String? = null, + private val credentialValues: ((Project) -> Credentials?)? = null +) { + + companion object { + val regex = Regex("[A-Za-z0-9_\\-.]+") + } + + init { + require(regex.matches(name)) { + "The repository name `$name` does not match the regex `$regex`." + } + } + + /** + * Obtains the name of the repository. + * + * The name will be primarily used in the publishing tasks. + * + * @param snapshots If `true` this repository is used for publishing snapshots, + * and the suffix `-snapshots` will be added to the value of the [name] property. + * Otherwise, the function returns just [name]. + */ + fun name(snapshots: Boolean): String = name + if (snapshots) "-snapshots" else "" + + /** + * Obtains the target URL of the repository for publishing. + */ + fun target(snapshots: Boolean): String = if (snapshots) this.snapshots else releases + + /** + * Tells if release and snapshot versions are published to the same destination + * of this repository. + */ + fun hasOneTarget() = snapshots == releases + + /** + * Obtains the publishing password credentials to this repository. + * + * If the credentials are represented by a `.properties` file, reads the file and parses + * the credentials. The file must have properties `user.name` and `user.password`, which store + * the username and the password for the Maven repository auth. + */ + fun credentials(project: Project): Credentials? = when { + credentialValues != null -> credentialValues.invoke(project) + credentialsFile != null -> credsFromFile(credentialsFile, project) + else -> throw IllegalArgumentException( + "Credentials file or a supplier function should be passed." + ) + } + + private fun credsFromFile(fileName: String, project: Project): Credentials? { + val file = project.rootProject.file(fileName) + if (file.exists().not()) { + return null + } + + val log = project.logger + log.info("Using credentials from `$fileName`.") + val creds = file.parseCredentials() + log.info("Publishing build as `${creds.username}`.") + return creds + } + + private fun File.parseCredentials(): Credentials { + val properties = Properties().apply { load(inputStream()) } + val username = properties.getProperty("user.name") + val password = properties.getProperty("user.password") + return Credentials(username, password) + } + + override fun equals(other: Any?): Boolean = when { + this === other -> true + other !is Repository -> false + else -> name == other.name && + releases == other.releases && + snapshots == other.snapshots +} + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + releases.hashCode() + result = 31 * result + snapshots.hashCode() + return result + } + + override fun toString(): String { + return name + } +} diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt index a5b9e72..efdf605 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/CodebaseFilter.kt @@ -29,7 +29,6 @@ package io.spine.gradle.report.coverage import com.google.errorprone.annotations.CanIgnoreReturnValue import io.spine.gradle.report.coverage.FileFilter.generatedOnly import java.io.File -import kotlin.streams.toList import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileTree import org.gradle.api.file.FileTree diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt index de5d00b..65bf2ee 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/coverage/JacocoConfig.kt @@ -28,7 +28,7 @@ package io.spine.gradle.report.coverage import io.spine.dependency.test.Jacoco import io.spine.gradle.applyPlugin -import io.spine.gradle.findTask +import io.spine.gradle.getTask import io.spine.gradle.report.coverage.TaskName.check import io.spine.gradle.report.coverage.TaskName.copyReports import io.spine.gradle.report.coverage.TaskName.jacocoRootReport @@ -144,7 +144,7 @@ class JacocoConfig( private fun registerRootReport( tasks: TaskContainer, - copyReports: TaskProvider? + copyReports: TaskProvider ): TaskProvider { val allSourceSets = Projects(projects).sourceSets() val mainJavaSrcDirs = allSourceSets.mainJavaSrcDirs() @@ -181,7 +181,7 @@ class JacocoConfig( private fun registerCopy(tasks: TaskContainer): TaskProvider { val everyExecData = mutableListOf() projects.forEach { project -> - val jacocoTestReport = project.findTask(jacocoTestReport.name) + val jacocoTestReport = project.getTask(jacocoTestReport.name) val executionData = jacocoTestReport.executionData everyExecData.add(executionData) } @@ -194,7 +194,7 @@ class JacocoConfig( rename { "${UUID.randomUUID()}.exec" } - dependsOn(projects.map { it.findTask(jacocoTestReport.name) }) + dependsOn(projects.map { it.getTask(jacocoTestReport.name) }) } return copyReports } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt index 9b7a57f..5f18b95 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/LicenseReporter.kt @@ -30,7 +30,7 @@ import com.github.jk1.license.LicenseReportExtension import com.github.jk1.license.LicenseReportExtension.ALL import com.github.jk1.license.LicenseReportPlugin import io.spine.gradle.applyPlugin -import io.spine.gradle.findTask +import io.spine.gradle.getTask import java.io.File import org.gradle.api.Project import org.gradle.api.Task @@ -98,7 +98,7 @@ object LicenseReporter { } /** - * Tells to merge all per-project reports which were previously [generated][generateReportIn] + * Tells to merge all per-project reports that were previously [generated][generateReportIn] * for each of the subprojects of the root Gradle project. * * The merge result is placed according to [Paths]. @@ -109,10 +109,10 @@ object LicenseReporter { val rootProject = project.rootProject val mergeTask = rootProject.tasks.register(mergeTaskName) { val consolidationTask = this - val assembleTask = project.findTask("assemble") + val assembleTask = project.getTask("assemble") val sourceProjects: Iterable = sourceProjects(rootProject) sourceProjects.forEach { - val perProjectTask = it.findTask(projectTaskName) + val perProjectTask = it.getTask(projectTaskName) consolidationTask.dependsOn(perProjectTask) perProjectTask.dependsOn(assembleTask) } @@ -121,7 +121,7 @@ object LicenseReporter { } dependsOn(assembleTask) } - project.findTask("build") + project.getTask("build") .finalizedBy(mergeTask) } @@ -151,11 +151,18 @@ object LicenseReporter { sourceProjects: Iterable, rootProject: Project ) { - val paths = sourceProjects.map { - val buildDir = it.layout.buildDirectory.asFile.get() - "$buildDir/${Paths.relativePath}/${Paths.outputFilename}" - } - println("Merging the license reports from the all projects.") + val paths = sourceProjects + .map { + val buildDir = it.layout.buildDirectory.asFile.get() + "$buildDir/${Paths.relativePath}/${Paths.outputFilename}" + }.filter { + val exists = File(it).exists() + if (!exists) { + rootProject.logger.debug("License report file not found: $it") + } + exists + } + println("Merging the license reports from all projects.") val mergedContent = paths.joinToString("\n\n\n") { (File(it)).readText() } val output = File("${rootProject.rootDir}/${Paths.outputFilename}") output.writeText(mergedContent) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt index 0aca30f..91247e2 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/ModuleDataExtensions.kt @@ -91,7 +91,6 @@ private fun MarkdownDocument.print( return this } - /** * Prints the URL to the project which provides the dependency. * diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt index adda37b..15bd24d 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/license/Template.kt @@ -28,7 +28,7 @@ package io.spine.gradle.report.license import io.spine.docs.MarkdownDocument import io.spine.gradle.artifactId -import java.util.* +import java.util.Date import org.gradle.api.Project /** @@ -40,32 +40,33 @@ internal class Template( ) { private companion object { + @Suppress("ConstPropertyName") private const val longBreak = "\n\n" } - internal fun writeHeader() { + internal fun writeHeader() = with(project) { out.nl() - .h1( - "Dependencies of " + - "`${project.group}:${project.artifactId}:${project.version}`" - ) - .nl() + .h1("Dependencies of `$group:$artifactId:$version`") + .nl() } internal fun writeFooter() { + val currentTime = Date() out.text(longBreak) .text("The dependencies distributed under several licenses, ") .text("are used according their commercial-use-friendly license.") .text(longBreak) .text("This report was generated on ") - .bold("${Date()}") + .bold("$currentTime") .text(" using ") + .nl() .link( "Gradle-License-Report plugin", "https://github.com/jk1/Gradle-License-Report" ) .text(" by Evgeny Naumenko, ") .text("licensed under ") + .nl() .link( "Apache 2.0 License", "https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE" diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt index eda2493..79d00c6 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/DependencyWriter.kt @@ -121,11 +121,8 @@ fun Project.dependencies(): SortedSet { private fun Project.depsFromAllConfigurations(): Set { val result = mutableSetOf() this.configurations.forEach { configuration -> - if (configuration.isCanBeResolved) { - // Force resolution of the configuration. - configuration.resolvedConfiguration - } - configuration.dependencies.filter { it.isExternal() } + configuration.dependencies + .filter { it.isExternal() } .forEach { dependency -> val forcedVersion = configuration.forcedVersionOf(dependency) val moduleDependency = diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/InceptionYear.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/InceptionYear.kt index cb25b3d..ed94b29 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/InceptionYear.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/InceptionYear.kt @@ -35,7 +35,10 @@ import org.gradle.kotlin.dsl.withGroovyBuilder */ internal object InceptionYear { - private const val SPINE_INCEPTION_YEAR = "2015" + /** + * The year of the inception of Spine. + */ + const val value = "2015" /** * Returns a string containing the inception year of Spine in a `pom.xml` format. @@ -44,7 +47,7 @@ internal object InceptionYear { val writer = StringWriter() val xml = MarkupBuilder(writer) xml.withGroovyBuilder { - "inceptionYear" { xml.text(SPINE_INCEPTION_YEAR) } + "inceptionYear" { xml.text(value) } } return writer.toString() } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt index ba673bb..15059ea 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomFormatting.kt @@ -92,7 +92,7 @@ internal object PomFormatting { * Writes the specified lines using the specified [destination], dividing them * by platform-specific line separator. * - * The written lines are also padded with platform's line separator from both sides + * Each written line is followed by two platform-specific line separators. */ internal fun writeBlocks(destination: StringWriter, vararg lines: String) { lines.iterator().forEach { diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt index 9144e2f..6e986e1 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomGenerator.kt @@ -57,7 +57,8 @@ import org.gradle.api.plugins.BasePlugin * * By default, those values are taken from the `project` object, which may or may not include * them. If the project does not have these values, and they are not specified in the `ext` - * block, the resulting `pom.xml` file is going to contain empty blocks, e.g. ``. + * block, the resulting `pom.xml` file is going to contain empty blocks, + * e.g., ``. */ @Suppress("unused") object PomGenerator { @@ -68,8 +69,11 @@ object PomGenerator { fun applyTo(project: Project) { /** - * In some cases, the `base` plugin, which is by default is added by e.g. `java`, - * is not yet added. `base` plugin defines the `build` task. This generator needs it. + * In some cases, the `base` plugin, which by default is added by e.g. `java`, + * is not yet added. + * + * The `base` plugin defines the `build` task. + * This generator needs it. */ project.apply { plugin(BasePlugin::class.java) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt index 5312219..a0b1ade 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/report/pom/PomXmlWriter.kt @@ -35,9 +35,9 @@ import java.io.StringWriter /** * Writes the dependencies of a Gradle project and its subprojects as a `pom.xml` file. * - * The resulting file is not usable for `maven` build tasks, but serves rather as a description - * of the first-level dependencies for each project/subproject. Their transitive dependencies - * are not included into the result. + * The resulting file is not usable for `maven` build tasks but serves as a description + * of the first-level dependencies for each project or subproject. + * Their transitive dependencies are not included in the result. */ internal class PomXmlWriter internal constructor( @@ -51,12 +51,10 @@ internal constructor( *

If a file with the specified location exists, its contents will be substituted * with a new `pom.xml`. * - * @param file a file to write `pom.xml` contents to + * @param file a file to write `pom.xml` contents to. */ fun writeTo(file: File) { - val fileWriter = FileWriter(file) val out = StringWriter() - writeStart(out) writeBlocks( out, @@ -67,8 +65,9 @@ internal constructor( ) PomFormatting.writeEnd(out) - fileWriter.write(out.toString()) - fileWriter.close() + FileWriter(file).use { + it.write(out.toString()) + } } /** @@ -83,4 +82,3 @@ internal constructor( return destination.toString() } } - diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt index 971c4b4..30ac810 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/testing/Tasks.kt @@ -29,6 +29,7 @@ package io.spine.gradle.testing import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType /** * Registers [slowTest][SlowTest] and [fastTest][FastTest] tasks in this [TaskContainer]. @@ -45,10 +46,10 @@ import org.gradle.kotlin.dsl.register */ @Suppress("unused") fun TaskContainer.registerTestTasks() { - withType(Test::class.java).configureEach { + withType().configureEach { filter { - // There could be cases with no matching tests. E.g. tests could be based on Kotest, - // which has custom task types and names. + // There could be cases with no matching tests. + // E.g., tests could be based on Kotest, which has custom task types and names. isFailOnNoMatchingTests = false includeTestsMatching("*Test") includeTestsMatching("*Spec") diff --git a/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts new file mode 100644 index 0000000..de7f1bf --- /dev/null +++ b/buildSrc/src/main/kotlin/jacoco-kmm-jvm.gradle.kts @@ -0,0 +1,72 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import java.io.File +import org.gradle.kotlin.dsl.getValue +import org.gradle.kotlin.dsl.getting +import org.gradle.kotlin.dsl.jacoco +import org.gradle.testing.jacoco.tasks.JacocoReport + +plugins { + jacoco +} + +/** + * Configures [JacocoReport] task to run in a Kotlin KMM project for `commonMain` and `jvmMain` + * source sets. + * + * This script plugin must be applied using the following construct at the end of + * a `build.gradle.kts` file of a module: + * + * ```kotlin + * apply(plugin="jacoco-kmm-jvm") + * ``` + * Please do not apply this script plugin in the `plugins {}` block because `jacocoTestReport` + * task is not yet available at this stage. + */ +@Suppress("unused") +private val about = "" + +/** + * Configure the Jacoco task with custom input a KMM project + * to which this convention plugin is applied. + */ +@Suppress("unused") +val jacocoTestReport: JacocoReport by tasks.getting(JacocoReport::class) { + val buildDir = project.layout.buildDirectory.get().asFile.absolutePath + val classFiles = File("${buildDir}/classes/kotlin/jvm/") + .walkBottomUp() + .toSet() + classDirectories.setFrom(classFiles) + + val coverageSourceDirs = arrayOf( + "src/commonMain", + "src/jvmMain" + ) + sourceDirectories.setFrom(files(coverageSourceDirs)) + + executionData.setFrom(files("${buildDir}/jacoco/jvmTest.exec")) +} diff --git a/buildSrc/src/main/kotlin/jvm-module.gradle.kts b/buildSrc/src/main/kotlin/jvm-module.gradle.kts index 5b1577e..11696c1 100644 --- a/buildSrc/src/main/kotlin/jvm-module.gradle.kts +++ b/buildSrc/src/main/kotlin/jvm-module.gradle.kts @@ -24,65 +24,57 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import BuildSettings.javaVersion +import io.spine.dependency.boms.BomsPlugin import io.spine.dependency.build.CheckerFramework import io.spine.dependency.build.Dokka import io.spine.dependency.build.ErrorProne +import io.spine.dependency.build.JSpecify import io.spine.dependency.lib.Guava -import io.spine.dependency.lib.JavaX import io.spine.dependency.lib.Protobuf -import io.spine.dependency.local.Logging import io.spine.dependency.local.Reflect -import io.spine.dependency.local.TestLib -import io.spine.dependency.test.JUnit import io.spine.dependency.test.Jacoco -import io.spine.dependency.test.Kotest import io.spine.gradle.checkstyle.CheckStyleConfig import io.spine.gradle.github.pages.updateGitHubPages import io.spine.gradle.javac.configureErrorProne import io.spine.gradle.javac.configureJavac import io.spine.gradle.javadoc.JavadocConfig -import io.spine.gradle.kotlin.applyJvmToolchain import io.spine.gradle.kotlin.setFreeCompilerArgs import io.spine.gradle.report.license.LicenseReporter -import io.spine.gradle.testing.configureLogging -import io.spine.gradle.testing.registerTestTasks plugins { `java-library` id("net.ltgt.errorprone") id("pmd-settings") id("project-report") - id("dokka-for-java") kotlin("jvm") - id("io.kotest") - id("org.jetbrains.kotlinx.kover") id("detekt-code-analysis") - id("dokka-for-kotlin") + id("dokka-setup") + id("org.jetbrains.kotlinx.kover") + id("module-testing") } - +apply() LicenseReporter.generateReportIn(project) JavadocConfig.applyTo(project) CheckStyleConfig.applyTo(project) project.run { - configureJava(javaVersion) - configureKotlin(javaVersion) + configureJava() + configureKotlin() addDependencies() forceConfigurations() val generatedDir = "$projectDir/generated" setTaskDependencies(generatedDir) - setupTests() configureGitHubPages() } typealias Module = Project -fun Module.configureJava(javaVersion: JavaLanguageVersion) { +fun Module.configureJava() { java { - toolchain.languageVersion.set(javaVersion) + sourceCompatibility = BuildSettings.javaVersionCompat + targetCompatibility = BuildSettings.javaVersionCompat } tasks { @@ -93,9 +85,8 @@ fun Module.configureJava(javaVersion: JavaLanguageVersion) { } } -fun Module.configureKotlin(javaVersion: JavaLanguageVersion) { +fun Module.configureKotlin() { kotlin { - applyJvmToolchain(javaVersion.asInt()) explicitApi() compilerOptions { jvmTarget.set(BuildSettings.jvmTarget) @@ -105,12 +96,11 @@ fun Module.configureKotlin(javaVersion: JavaLanguageVersion) { kover { useJacoco(version = Jacoco.version) - } - - koverReport { - defaults { - xml { - onCheck = true + reports { + total { + xml { + onCheck = true + } } } } @@ -130,21 +120,8 @@ fun Module.addDependencies() = dependencies { api(Guava.lib) compileOnlyApi(CheckerFramework.annotations) - compileOnlyApi(JavaX.annotations) + api(JSpecify.annotations) ErrorProne.annotations.forEach { compileOnlyApi(it) } - - implementation(Logging.lib) - - testImplementation(Guava.testLib) - testImplementation(JUnit.runner) - testImplementation(JUnit.pioneer) - JUnit.api.forEach { testImplementation(it) } - - testImplementation(TestLib.lib) - testImplementation(Kotest.frameworkEngine) - testImplementation(Kotest.datatest) - testImplementation(Kotest.runnerJUnit5Jvm) - testImplementation(JUnit.runner) } fun Module.forceConfigurations() { @@ -154,8 +131,6 @@ fun Module.forceConfigurations() { all { resolutionStrategy { force( - JUnit.bom, - JUnit.runner, Dokka.BasePlugin.lib, Reflect.lib, ) @@ -164,18 +139,6 @@ fun Module.forceConfigurations() { } } -fun Module.setupTests() { - tasks { - registerTestTasks() - test.configure { - useJUnitPlatform { - includeEngines("junit-jupiter") - } - configureLogging() - } - } -} - fun Module.setTaskDependencies(generatedDir: String) { tasks { val cleanGenerated by registering(Delete::class) { @@ -196,9 +159,7 @@ fun Module.setTaskDependencies(generatedDir: String) { } fun Module.configureGitHubPages() { - val docletVersion = project.version.toString() - updateGitHubPages(docletVersion) { - allowInternalJavadoc.set(true) + updateGitHubPages { rootFolder.set(rootDir) } } diff --git a/buildSrc/src/main/kotlin/kmp-module.gradle.kts b/buildSrc/src/main/kotlin/kmp-module.gradle.kts new file mode 100644 index 0000000..4f7ec63 --- /dev/null +++ b/buildSrc/src/main/kotlin/kmp-module.gradle.kts @@ -0,0 +1,187 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import io.spine.dependency.boms.BomsPlugin +import io.spine.dependency.local.Reflect +import io.spine.dependency.local.TestLib +import io.spine.dependency.test.JUnit +import io.spine.dependency.test.Jacoco +import io.spine.dependency.test.Kotest +import io.spine.gradle.checkstyle.CheckStyleConfig +import io.spine.gradle.javac.configureJavac +import io.spine.gradle.kotlin.setFreeCompilerArgs +import io.spine.gradle.publish.IncrementGuard +import io.spine.gradle.report.license.LicenseReporter + +/** + * Configures this [Project] as a Kotlin Multiplatform module. + * + * By its nature, this script plugin is similar to `jvm-module`. It performs + * the basic module configuration. + * + * `jvm-module` is based on a mix of Java and Kotlin Gradle plugins. It allows + * usage of Kotlin and Java in a single module that is built for JVM. + * Whereas `kmp-module` is based on a Kotlin Multiplatform plugin. This plugin + * supports different compilation targets within a single module: JVM, IOS, + * Desktop, JS, etc. Also, it allows having some common sources in Kotlin + * that can be shared with target-specific code. They are located in + * `commonMain` and `commonTest` source sets. Each concrete target implicitly + * depends on them. + * + * As for now, this script configures only JVM target, but other targets + * will be added further. + * + * ### JVM target + * + * Sources for this target are placed in `jvmMain` and `jvmTest` directories. + * Java is allowed to be used in `jvm` sources, but Kotlin is a preference. + * Use Java only as a fall-back option where Kotlin is insufficient. + * Due to this, Java linters are not even configured by `kmp-module`. + * + * @see Kotlin Multiplatform docs + */ +@Suppress("unused") +val about = "" + +plugins { + kotlin("multiplatform") + id("detekt-code-analysis") + id("org.jetbrains.kotlinx.kover") + `project-report` +} +apply() +apply() + +project.forceConfigurations() + +fun Project.forceConfigurations() { + with(configurations) { + forceVersions() + all { + resolutionStrategy { + force( + Reflect.lib + ) + } + } + } +} + +/** + * Configures Kotlin Multiplatform plugin. + * + * Please note, this extension DOES NOT configure Kotlin for JVM. + * It configures KMP, in which Kotlin for JVM is only one of + * possible targets. + */ +@Suppress("UNUSED_VARIABLE") // Avoid warnings for source set vars. +kotlin { + // Enables explicit API mode for any Kotlin sources within the module. + explicitApi() + + compilerOptions { + setFreeCompilerArgs() + } + + // Enables and configures JVM target. + jvm { + compilerOptions { + jvmTarget.set(BuildSettings.jvmTarget) + } + } + + // Dependencies are specified per-target. + // Please note, common sources are implicitly available in all targets. + @Suppress("unused") // source set `val`s are used implicitly. + sourceSets { + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(Kotest.assertions) + implementation(Kotest.frameworkEngine) + implementation(Kotest.datatest) + } + } + val jvmTest by getting { + dependencies { + implementation(dependencies.enforcedPlatform(JUnit.bom)) + implementation(TestLib.lib) + implementation(JUnit.Jupiter.engine) + implementation(Kotest.runnerJUnit5Jvm) + } + } + } +} + +java { + sourceCompatibility = BuildSettings.javaVersionCompat + targetCompatibility = BuildSettings.javaVersionCompat +} + +/** + * Performs the standard task's configuration. + * + * Here's no difference with `jvm-module`, which does the same. + * + * Kotlin here is configured for both common and JVM-specific sources. + * Java is for JVM only. + * + * Also, Kotlin and Java share the same test executor (JUnit), so tests + * configuration is for both. + */ +tasks { + withType().configureEach { + configureJavac() + } +} + +/** + * Overrides the default location of Kotlin sources. + * + * The default configuration of Detekt assumes presence of Kotlin sources + * in `src/main/kotlin`, which is not the case for KMP. + */ +detekt { + source.setFrom( + "src/commonMain", + "src/jvmMain" + ) +} + +kover { + useJacoco(version = Jacoco.version) + reports { + total { + xml { + onCheck = true + } + } + } +} + +LicenseReporter.generateReportIn(project) +CheckStyleConfig.applyTo(project) diff --git a/buildSrc/src/main/kotlin/kmp-publish.gradle.kts b/buildSrc/src/main/kotlin/kmp-publish.gradle.kts new file mode 100644 index 0000000..e4c3aa1 --- /dev/null +++ b/buildSrc/src/main/kotlin/kmp-publish.gradle.kts @@ -0,0 +1,75 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.kotlin.dsl.`maven-publish` +import org.gradle.kotlin.dsl.named + +/** + * Configures publications for `kmp-module`. + * + * As for now, [spinePublishing][io.spine.gradle.publish.spinePublishing] + * doesn't support Kotlin Multiplatform modules. So, their publications are + * configured by this script plugin. Other publishing-related configuration + * is still performed by the extension. + * + * To publish a KMP module, one still needs to open and configure + * `spinePublishing` extension. Make sure `spinePublishing.customPublishing` + * property is set to `true`, and this script plugin is applied. + * + * For example: + * + * ``` + * plugins { + * `kmp-module` + * `kmp-publish` + * } + * + * spinePublishing { + * destinations = setOf(...) + * customPublishing = true + * } + * ``` + */ +@Suppress("unused") +val about = "" + +plugins { + `maven-publish` + id("dokka-setup") +} + +publishing.publications { + named("kotlinMultiplatform") { + // Although, the "common artifact" can't be used independently + // of target artifacts, it is published with documentation. + artifact(project.htmlDocsJar()) + } + named("jvm") { + // Includes Kotlin (JVM + common) and Java documentation. + artifact(project.htmlDocsJar()) + } +} diff --git a/buildSrc/src/main/kotlin/module-testing.gradle.kts b/buildSrc/src/main/kotlin/module-testing.gradle.kts new file mode 100644 index 0000000..ee3fe61 --- /dev/null +++ b/buildSrc/src/main/kotlin/module-testing.gradle.kts @@ -0,0 +1,120 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import io.spine.dependency.lib.Guava +import io.spine.dependency.local.TestLib +import io.spine.dependency.test.JUnit +import io.spine.dependency.test.JUnit.Jupiter +import io.spine.dependency.test.Kotest +import io.spine.dependency.test.Truth +import io.spine.gradle.testing.configureLogging +import io.spine.gradle.testing.registerTestTasks + +/** + * This convention plugin applies test dependencies and configures test-related tasks. + * + * The version of the [JUnit] platform must be applied via the [BomsPlugin][io.spine.dependency.boms.BomsPlugin]: + * + * ```kotlin + * apply() + * ``` + */ +@Suppress("unused") +private val about = "" + +plugins { + `java-library` +} + +project.run { + setupTests() + forceTestDependencies() +} + +dependencies { + forceJunitPlatform() + + testImplementation(Jupiter.api) + testImplementation(Jupiter.params) + testImplementation(JUnit.pioneer) + + testImplementation(Guava.testLib) + + testImplementation(TestLib.lib) + testImplementation(Kotest.assertions) + + testRuntimeOnly(Jupiter.engine) +} + +/** + * Forces the version of [JUnit] platform and its dependencies via [JUnit.bom]. + */ +private fun DependencyHandlerScope.forceJunitPlatform() { + testImplementation(enforcedPlatform(JUnit.bom)) +} + +typealias Module = Project + +/** + * Configure this module to run JUnit-based tests. + */ +fun Module.setupTests() { + tasks { + registerTestTasks() + test.configure { + useJUnitPlatform { + includeEngines("junit-jupiter") + } + configureLogging() + } + } +} + +/** + * Forces the versions of task dependencies that are used _in addition_ to + * the forced JUnit platform. + */ +@Suppress( + /* We're OK with incubating API for configurations. It does not seem to change recently. */ + "UnstableApiUsage" +) +fun Module.forceTestDependencies() { + configurations { + all { + resolutionStrategy { + forceTestDependencies() + } + } + } +} + +private fun ResolutionStrategy.forceTestDependencies() { + force( + Guava.testLib, + Truth.libs, + Kotest.assertions, + ) +} diff --git a/buildSrc/src/main/kotlin/module.gradle.kts b/buildSrc/src/main/kotlin/module.gradle.kts new file mode 100644 index 0000000..b24b5b2 --- /dev/null +++ b/buildSrc/src/main/kotlin/module.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// This is a template file for an actual script which should be +// defined by a project to which `config` is applied. +// +// The reason for having this file is that it is referenced as +// a plugin in `uber-jar-module.gradle.kts` diff --git a/buildSrc/src/main/kotlin/pmd-settings.gradle.kts b/buildSrc/src/main/kotlin/pmd-settings.gradle.kts index 0373ee0..938a764 100644 --- a/buildSrc/src/main/kotlin/pmd-settings.gradle.kts +++ b/buildSrc/src/main/kotlin/pmd-settings.gradle.kts @@ -42,7 +42,7 @@ pmd { ruleSets = listOf() // Load PMD settings. - val pmdSettings = file("$rootDir/config/quality/pmd.xml") + val pmdSettings = file("$rootDir/buildSrc/quality/pmd.xml") val textResource: TextResource = resources.text.fromFile(pmdSettings) ruleSetConfig = textResource diff --git a/buildSrc/src/main/kotlin/test-module.gradle.kts b/buildSrc/src/main/kotlin/test-module.gradle.kts new file mode 100644 index 0000000..d64de09 --- /dev/null +++ b/buildSrc/src/main/kotlin/test-module.gradle.kts @@ -0,0 +1,57 @@ +/* + * Copyright 2024, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import io.spine.dependency.local.Base +import io.spine.dependency.local.Validation +import io.spine.gradle.report.license.LicenseReporter + +plugins { + java + `java-test-fixtures` + id("module-testing") +} +LicenseReporter.generateReportIn(project) + +dependencies { + arrayOf( + Base.lib, + Validation.runtime + ).forEach { + testFixturesImplementation(it)?.because( + """ + We do not apply CoreJvm Compiler Gradle plugin which adds + the `implementation` dependency on Validation runtime automatically + (see `Project.configureValidation()` function in `CompilerConfigPlugin.kt`). + + In a test module we use vanilla `protoc` (via ProtoTap) and then run codegen + using the Spine Compiler `Pipeline` and the plugins of the module under the test. + + Because of this we need to add the dependencies above explicitly for the + generated code of test fixtures to compile. + """.trimIndent() + ) + } +} diff --git a/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts new file mode 100644 index 0000000..f3dda52 --- /dev/null +++ b/buildSrc/src/main/kotlin/uber-jar-module.gradle.kts @@ -0,0 +1,202 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@file:Suppress("UnstableApiUsage") // `configurations` block. + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import io.spine.gradle.publish.IncrementGuard +import io.spine.gradle.publish.SpinePublishing +import io.spine.gradle.publish.spinePublishing +import io.spine.gradle.report.license.LicenseReporter + +plugins { + id("module") + `maven-publish` + id("com.gradleup.shadow") + id("write-manifest") + `project-report` + idea +} +apply() +LicenseReporter.generateReportIn(project) + +spinePublishing { + artifactPrefix = "" + destinations = rootProject.the().destinations + customPublishing = true +} + +/** The ID of the far JAR artifact. */ +private val projectArtifact = project.name.replace(":", "") + +publishing { + val groupName = project.group.toString() + val versionName = project.version.toString() + + publications { + create("fatJar", MavenPublication::class) { + groupId = groupName + artifactId = projectArtifact + version = versionName + artifact(tasks.shadowJar) + } + } +} + +/** + * Declare dependency explicitly to address the Gradle error. + */ +@Suppress("unused") +val publishFatJarPublicationToMavenLocal: Task by tasks.getting { + dependsOn(tasks.shadowJar) +} + +// Disable the `jar` task to free up the name of the resulting archive. +tasks.jar { + enabled = false +} + +tasks.publish { + dependsOn(tasks.shadowJar) +} + +tasks.shadowJar { + excludeFiles() + setZip64(true) /* The archive has way too many items. So using the Zip64 mode. */ + archiveClassifier.set("") /** To prevent Gradle setting something like `osx-x86_64`. */ +} + +/** + * Exclude unwanted directories. + */ +@Suppress("LongMethod") +private fun ShadowJar.excludeFiles() { + exclude( + /* + Exclude IntelliJ Platform images and other resources associated with IntelliJ UI. + We do not call the UI, so they won't be used. + */ + "actions/**", + "chooser/**", + "codeStyle/**", + "codeStylePreview/**", + "codeWithMe/**", + "darcula/**", + "debugger/**", + "diff/**", + "duplicates/**", + "expui/**", + "extensions/**", + "fileTemplates/**", + "fileTypes/**", + "general/**", + "graph/**", + "gutter/**", + "hierarchy/**", + "icons/**", + "ide/**", + "idea/**", + "inlayProviders/**", + "inspectionDescriptions/**", + "inspectionReport/**", + "intentionDescriptions/**", + "javadoc/**", + "javaee/**", + "json/**", + "liveTemplates/**", + "mac/**", + "modules/**", + "nodes/**", + "objectBrowser/**", + "plugins/**", + "postfixTemplates/**", + "preferences/**", + "process/**", + "providers/**", + "runConfigurations/**", + "scope/**", + "search/**", + "toolbar/**", + "toolbarDecorator/**", + "toolwindows/**", + "vcs/**", + "webreferences/**", + "welcome/**", + "windows/**", + "xml/**", + + /* + Exclude `https://github.com/JetBrains/pty4j`. + We don't need the terminal. + */ + "resources/com/pti4j/**", + + /* Exclude the IntelliJ fork of + `http://www.sparetimelabs.com/purejavacomm/purejavacomm.php`. + It is the part of the IDEA's terminal implementation. + */ + "purejavacomm/**", + + /* Exclude IDEA project templates. */ + "resources/projectTemplates/**", + + /* + Exclude dynamic libraries. Should the tool users need them, + they would add them explicitly. + */ + "bin/**", + + /* + Exclude Google Protobuf definitions to avoid duplicates. + */ + "google/**", + "src/google/**", + + /** + * Exclude Spine Protobuf definitions to avoid duplications. + */ + "spine/**", + + /** + * Exclude Kotlin runtime because it will be provided. + */ + "kotlin/**", + "kotlinx/**", + + /** + * Exclude native libraries related to debugging. + */ + "win32-x86/**", + "win32-x86-64/**", + + /** + * Exclude the Windows process management (WinP) libraries. + * See: `https://github.com/jenkinsci/winp`. + */ + "winp.dll", + "winp.x64.dll", + ) +} diff --git a/change/build.gradle.kts b/change/build.gradle.kts index d5e48d5..952e20d 100644 --- a/change/build.gradle.kts +++ b/change/build.gradle.kts @@ -25,15 +25,13 @@ */ import io.spine.dependency.local.Base -import io.spine.dependency.local.Spine import io.spine.dependency.local.Time import io.spine.dependency.local.Validation import io.spine.gradle.publish.IncrementGuard -import io.spine.protodata.gradle.plugin.LaunchProtoData plugins { protobuf - id(mcJava.pluginId) + id(coreJvmCompiler.pluginId) `detekt-code-analysis` } diff --git a/change/src/main/java/io/spine/change/package-info.java b/change/src/main/java/io/spine/change/package-info.java index 5a45c63..7b33823 100644 --- a/change/src/main/java/io/spine/change/package-info.java +++ b/change/src/main/java/io/spine/change/package-info.java @@ -29,9 +29,9 @@ */ @CheckReturnValue -@ParametersAreNonnullByDefault +@NullMarked package io.spine.change; import com.google.errorprone.annotations.CheckReturnValue; -import javax.annotation.ParametersAreNonnullByDefault; +import org.jspecify.annotations.NullMarked; diff --git a/change/src/test/java/io/spine/change/ChangesTest.java b/change/src/test/java/io/spine/change/ChangesTest.java index 500f8f4..4fb1688 100644 --- a/change/src/test/java/io/spine/change/ChangesTest.java +++ b/change/src/test/java/io/spine/change/ChangesTest.java @@ -31,7 +31,7 @@ import com.google.protobuf.Timestamp; import io.spine.base.Time; import io.spine.testing.UtilityClassTest; -import io.spine.time.testing.Past; +import io.spine.testing.time.Past; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/config b/config index 99fb7ce..9a4fbe2 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 99fb7ce679e6c71713fd6c21c24e0cd19304b118 +Subproject commit 9a4fbe2bcae9a7b5f0d20159d41a8756d236e146 diff --git a/gradle.properties b/gradle.properties index 4c1c2c4..93bcb69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,28 +1,25 @@ -# -# Copyright 2022, TeamDev. All rights reserved. -# -# 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 -# -# Redistribution and use in source and/or binary forms, with or without -# modification, must retain the above copyright notice and the following -# disclaimer. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# +# Allow Gradle to auto-detect installed JDKs. +org.gradle.java.installations.auto-detect=true -# Dokka plugin eats more memory than usual. Therefore all builds should have enough. -org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m +# Optional: Allow Gradle to download JDKs if needed. +org.gradle.java.installations.auto-download=true + +# Use parallel builds for better performance. +org.gradle.parallel=true +#org.gradle.caching=true + +# Dokka plugin eats more memory than usual. Therefore, all builds should have enough. +org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC + +# suppress inspection "UnusedProperty" +# The below property enables generation of XML reports for tests. +# If this flag is false, it causes `KotlinTestReport` task to replace these reports with one +# consolidated HTML report. +# See: https://github.com/JetBrains/kotlin/blob/9fd05632f0d7f074b6544527e73eb0fbb2fb1ef2/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/testing/internal/KotlinTestReport.kt#L20 +# See: https://youtrack.jetbrains.com/issue/KT-32608 +kotlin.tests.individualTaskReports=true + +# Enables the Dokka migration mode from v1 to v2. +# For details please see: +# https://kotlinlang.org/docs/dokka-migration.html#enable-migration-helpers +org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c742b298b441bfb90dbc124400a3751b9..f8e1ee3125fe0768e9a76ee977ac089eb657005e 100644 GIT binary patch delta 37435 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 delta 35540 zcmXt;V|-nG^Yzo1jhhoUwr$(CZ5t;%vDLV-Z8x@UyRrTBzW$%*{eJbEnYGt^*YqBM zw=II#+I*M4lbaZqm7$}bMUtVTo|>3$P-a|U**UzOpc|2zl$)fHqWuPc1~N@ODAcBE z#Ud*{D5MU-BKv_+_~XX;#`znVq6`eIT^&E4AQ%{!09aC8GF}ojvJg-jf(a#OWo=2D zAqXputL(zvYOr1vweX~@RpZq;r z0$_%)Fo^oi>sq%1Si;q_MZObGwQ`Zy6xw4rs;UEDIu7f)HUoH$%hfq+}iE&UwOBzM)*Qks+P9QFb`A>77k|+m} zfcD=^W^ycw(mYF$m`}|02gjlo3~6IxDbkzzic~UO_tmUsFf>h2m@6{m&JZ#N=mn`UN-LMS z!{YaSCllPSe1m?g5s1ZE#4q)O_}{yM_d#wGe**(^{PyqNXp(AZk(2a_F@a{vItu7Y zD4*XkQk!WMf5YlKbu|^4(ZQ-ScB3$mszsBbL^G6hgk1!7v3BU>Gu;Z@BR`jmlUf@4 zK6*U!EUbpEq3KU$rQc0HO{S+C0J}fH7^BEQPd+}=G!Bj29PS}D)=?RCMVM^;E*W!2 zdG{NM`{_Y@-L+82kbLZ(ia=(Zz7;ra`QLQ*PCO`LRaPl@TePF$1>LoZ?B>UC=NfA- zI&|yBf-^fs%x!&5g)|`ydQtfueaMngdnxH{>C&SZ9c$~~7(uF&HZLGgO1oah!&|9&~<9-E`54LIS~igTU|rqedJE?Yzc^ zVP4AxQOOv^Hiss>U64&pCTU8UyJo5#0VJeUCNZNg_8nuiE0ML)J!%js-L?%Ihv|yp zNe_h{_7+92Zkn+g^scghb{t<4M zkQ8WO?Wh5UdWs}_0LZ1Qm=J02*IHHoIEQhH`%a^a{>u=?X!Qa zDRE#i_7!%=#Af;p_aTxOf&nCqk^JYdK z&FP!@10%uDh@Uf%WU~V>*Ll$%>u4w$te5027DqWQSLr++{$DRQ2!Fug5p!c>_0{XF6;`Ix$x?t65c zh#}&u`HX6+=cRX0FJiKaJ4OqiQZsK$C(bi}ntKXy*A6v?wRaH#T+?iLXc*2bwlp7= zT?6)wq$L1oDye0G0$t!uB#2<2i_kE^_&ktef1hkbzKol*ZG19ab=WdDldzO$#2G_G zngkF`H5xb$V-s>#UEqi5_pELoM)q9YpQCaRcbZJS<3a2Xe1~b7hguHxQrbIAY=p?6 zLCn5!QdqKSV+i)RMbql%3_dBYvZ(Tw;bW-Ro09_KqqmAb0Y)AKbC!;@T5vuMJV9mp zL2MhwBjHa8do*KKufpO)n+7~?EY4lGk;?D2XP{YU91lm1i1lB_fOGH}Lq^h8I|Ivr zn6;c*V(S3bVy1$9egZ|2syP~yks;!xOnBh8X_(g78h0SU3LInAo;7=rdfuIR5Am|8|mQmSw$aPpg_6pL74s^vWW8 z-yk4}j!ANJJ2^mT#;a!mDKv()TzTQMb)0pVHSY7ee_X-i^ae7qNmW53^$4YErLb)11MEBc$BV>^<6LgVfWG@z4#3|mrab7#;j;yISA#0aRGLeL2oCwV{h)AjR zJoOVXVGa>r3*V#QUyq|FLJT6bV=*BRKt!MHb_RztDa(*N3Rc}aBo@D5pA;?qNh;2e z{tM1<5=6bsW->ezOL~M{BSXGfok0oVCn~v2QF!^E#%Rcschavap3_vD)Y^}VIJa5JT}qMTZL@esP+mB)EsgPwioS0v zQaaLD&l|JMaFTe)VAP$ zZyP?j+aH^B+g~q-%V6yLUx*BZeC;(Ck2AkB_y^?Z76_z~LxIG3jVY6-ptG}7>`84L zind9lYVNG>VQLf^1V^VL^8Du^YhkMWy9tW)=#%AbrS!gt_Up}5T`$aYP3w`5B3}JJ z16eA?Ic6z1SlUP1sxq+?V;$G?IwZH2+KZa#%5M^85^xwnhI3=R1dfH#6Dy0Svih#Go!isiX;Q*t%$|20ZM=T?$6!Puh zG$OSAf&RY1-$5_k5TKduU$5+idTBYR>WnwlGt!8og=?sW*xD9h>S$Sl?D;remyEO# zgsKt?np=x8nk9A>CQh49p?&U~3!C_e!?~1i;meKC4R$Sk?>s%e2{mK_N-a zzS>e@U9?mtRkoMN7$MqWcx(hEgDC6DM5SQun@0w0p@j$zF;H~WfR5x4xGwD$eco8( zggj5uwDY{#iS;cut11gRjp7XNvMXhS6M0_5nG)T`8b@kD;c}YphFxjwJP}EBsx~W0 zU#;O$EpU5|4X#R5*}(cr9y<-b2*A!n%Zt?>LYM3l?7e{hSh_~H&uoieOhaGdpf+D> zDot{U&pR@2FLCFOt?^q=acvh>6!id+-uiB?m}`z`e|rU$w7kaDlB>sz35o}~#4E61 zs)6`6PwK>cWy^}1H`8S}nK*Q;^o){g9cPxU8b~FQT7}WhU>)x;=%8!1?`>gXgXE4z zQJAPkwXLB#@`Qv5OSPO>hp-G!?vdMXRA=2;zkx1_wTGzFJ{>GEOM6LKd`MmG9Zplu zg^x~jkv9+}uWHv4M(7?v0xjj?9=X!xtDN&0>_@C11+r^6BFp3XZgdwF--!v@e_%~u z0D97AZI%-c1^#@!F$l`oy=*FHnU}pGI43lf#9rJq65E$wTjI0ops4OeLL8qeYNM&a z#c3hi01U`!3YoMF#OLKHZ`lV@w_C@L2~Hkb>M)DaG=F%}lVIVrwmO984K+pLnmMTq zGGl}2U6JSLTyVP35SiLxZGM=)C{p1z0MVQgvED?v^-RQ6qomcr%`7@(;9_DMkS8c=FK@$VvTY}*ZmZC!cCt5HE@Do{f-=6-N@U!7L(^%A;p@C+mrwH zNKgGK?8$tIT5rBF?l>C_S0>YHaG*BNjTD?$S2keOVg?9N|4Tc|iM*G`%^MJA1&rr@ zwbLCcFlMG7HfiDvQX5P-*Lqz)k^%U z0-sOb4uXp1=6mGnS{Q!24v%%5Z}(O(e#TsWzn*9!se;GF>Bzuma(l_ znp38c6f;LX=Z|ly>V>aceR;z7f#70DYQZhZlwuZM4EE*doY(`4o@uQuUQ0Mc+}l_P z2b}yr$^8@1gf{l8p@f1 z+Z3Q{E(3o3@irpjwfi#^^r2LrBQgE4>r{y^anEAw52K?)KI@M7u)Dy?l|JwoD5dLm z#@eo2*krD-L!+A=sS%olwA|<$dtMy>T->MSa1-6h;w#>R1UrN4B*Yp@Wm0~QOvf#n{Uoyq`QnxwD|-( z=~tK>{w!_bA1xkv30#AK)ERi$tz!{De^(fIqjLU4=Q0pT5<{mMj}&2*;8E&{BK=q( z=wJ8=U398$9nae|+_mr%IsV9G;peonPp6=jPv+QP$fn_8&1i!<$Q~uX_gJ%Mi1Af; z*80UpA)l^}@{=X6(pQB4dC04P%d8D(FfilqVE_J!5Re#PNo~m(Nv}l8q!92r34!th z9F0Nl44WGZZ2_~Cvje6Du<<5N%$u4M)OC1ie~8(sgQUVkjB_Jj9g z51|#Y{=C;bY-!iKE(H}mVsO#{-SB19@ zzgO|e7g%BZ(s>acdAgF~Htp7C%OQ2j^#rlYAkYvTK+4gfbcXC_LvxU)$T+3Qo#kqBS z(xHP+&~N|N^n>*IOb5?g4Ltg)F?GPAJ6%uzZ{P`XP^S{T{4VRwXDHcAn<1FK?P zzzgft9$tSm#aH4sHjx1F&S<0>55G*WSO&5JTH|?m{Ly~#C*dseu$mLLjFoO5Cg!md z5q14g{LKM~BCO}bQdKPui(Edbxb7GpySz0vh4Ki=T0H9`{zbDHTiJW#*{z)ou_3{y z0C?OS@|DKJh&2g*L2j8V4q774{t@2?Se4y%JHwU7>>+-`o@J4^@&(r> zmV{J}yt9DTdO_X-T!o_(^S~GCJEFds{PI$RV*2QcFokT0zq1&@8twWlJ9CHKKtLA*3`>bYOtM*t0vt zVQQtEyid-PSMHPZ*5gwyVGmfeQQ+>~=&1HEq&DT5|) zk&d}_o#pxlRb{+#tc=D;;39;;@dOM#LxNkJkFZcYk=Ei12q=Ls68xFl_q%!njdC7g z+0-2StW$3uAJ7)&N0Bw1!-7AbY2k@pAzwql|4ARjh$CDohAzU`{1*bXF(mINju4-4 zcUer{;L)&`P^fgx93v=pWsE@JfGUbd+)ya-iDGuhqBQyVZCMElm zCBX(PK|V_~0q|b=JW{^-L^q4FB$sV=s;2-$R@XL}(3Uz82qp0iy*N{ zR{nLoG}M2a0|cc6uvZ?J2?akXeg-Ts^@luO`x%iCt&TFLm?F_Pw0vdA(lSoRJ8ufy z;N_0`E+sNm{z70cTv?bu7-1+N?5DTL^UkrC!;zQy<>l8WM8Nk-#%CA5j{+ae0bSjE z`Jf9y#GnVk11%y<59Vc~R0_#Xq0kND2oZY`CQ&D=@d@a4AVf87HH;XJc_(bqQ+P`= zyVxjvKQATyjo$|7kFjnUmupR8LAw z=92^R80CKS&;&(h?vaM*^Fj&vk_}DPTzUBIJChlY4I}(=#2*aL;S!POtlSX{imluW zfQ4=;x=s3Epmc>FI}FQp@i#Y1#?t2QmT4Tui|t9>q&TPH=kbg5FSc3{ zmV*rGal|H*+U+wwk9iOOZ#*^bw#6s0t$yxBs8hp)N@jU8v-RaLjh@g5TL-NZ#;gKf zm9?)4&=tV~Y=-{U?E;GTsqv`tHJg2ZW0tYHcsUX@nBYptZ#&blX%%=+Jp|KU0toil5z zZxz4uX{2@9SqJ}-I)2GLbX0I?75x!ck9NIZA0vO88N@%sKED~T1twbVAzw>iUp15Y zH<~A|@>pDQxm>Nj?H?~+fh`#^`ttIuJ9N(LZ17^{6gixk>(rbTUqQznB<%Zu&UJzu z$qIM!evf}gMrt6L0M6H*gI>)u#}!o{{;=y30OTuY*H%h1&!;Ih*s7l|s*@o@@a-2) zDS6j#LKI^y(AUtZ3tM0vD*5zm0I4UZS1g8zs}MaEvNvN1LA}>0dJUK@g{A3pWp;1X z<9s$@$&an@q1szWdl|_2njA5t{S$CkHyH;xdh2%Cuq@Ifz`^*PF>}hl@7;LH%>d<3 z!Jwey*ke%!fV%X)*S_RSys6Bg2-30>NffKS^@yQ2&N5>G5l=KcV)_UnGvG-2PzqBP zTO7LOllr4PZ{1W9;Dj0KkX_o(D3I59n*f5MHFtvXz>fUd>iNq(hejRq_LYas?HNCa zMvw@{3u{Ra9C?*0GntX}mFXv;9Y%{|n=s;!RR;hb)XXLA=S3M0_O>6}&znQM`*oCm z%R?7g(U-)>WfxEqzOf4e77J&)rAx#}Ex)x%H+V&CoI%|#B24&ygU0Z7NNx=y6%#CQ zj`hu38SX;Xd^5@(uy@u?;R=>mH0myx^Rc>{4 zQfFZ!Fy{yP@`d7!$`ly*`IDtT26b!%mb8VUMZTVB8KF~Ru{8q)iiDx% zOp+Muc+~;$Wc?6H#k#|G%G|r`A|0=Y*;&UYLJ>k2iO621t@3^QCum}qLg_+Y8rJDo zOq9SS+~Ye&1VZW^p-`c+&k85~Kx~U;Wq*7ksnz$^EAC#ywyn>a<2u-Lm3PcXkaJbXDPXYWj2X^f0yHSi}5%_@c1k&HGtfY$`GFF&WJ(r2kg zTN4Gij=@5eU0112o`DM=L5Gpya~>TfeJF(|DP>zfN&gz^lD_H_iJw!ZPXgr`+wis5 zq$4I!!p+ENDx*iHsopneyV{2UDj(l|Vpa(<78jOi^H1JMcRcDtzGs_ji7LBoT@CQ; zo5@IJn449^a5V+sGHi|sf7DLlSL$&FP2SbrpK6qO7kUz+5(6k7p=Wx{3aZoLx`$uG z#Q!GSSK=AF)gGi;s0&gqA`!xMjUehR>SqLMt{%Q^+TMLr1B(&GXLE6{v)(MfYPj!dTV&xny{hm-3u!n4uZPOi}(i)Ia5MjtVSkq z7r04~U6yjRQm{9_(A~0J>9HTf9NETBjO5>#Wmn~gHkX=G}QA=Zi7tMUhOH#%(NrX zK)a3Rm*ru7m2SCXh>d~HDV45+j7PS-!l&3a$sjzR`pSl_;k{Nc2AkvC1#0v%QZx-% z@M?1WZzoGv{xigO;5m0s??_K3i|sxbLM7FnGJ!8hgp)6M9{fFE2YTQUZ8Y$QK=1uIG1Vk zST;!>dB-$gX|#=T(71(c9!sqbqMZ9;i@r9kCo@E!{jEz%$03t`3PG#Y+Eca8U8>*o z*zyzOUp@;>kVhawL^6igpy_z^6zX9+5jr4ej)!NU3X40h(p z42;SkfJhX*Za=EGIo)&6a974Tp$ARS(=5&qDqhUbXur8bC(zQsMU=sIHUG#j^{QJ( zYro`-?j&2}T?kNr$4udl2z1dFz3OjOV^@+wZjRuK4rRPvDPN2sxJEG`if{p9arQ}B z(zqizQ&UpQrqAyw#+z=12D&)hR373V*x&8Gmv5m4Cq`hjleSrvTTzExezD4butIe$ z1Xd5@Pj^{)9@2F$2@@NX%wi2uWf3t`ZxpQWGTz${nU8N!g?F$SSsBAf51nO(CpG}$ zlMyffyiiN_zC4(0z|xNagY^GUAl}66gXC0_ThpDSn<8sF^WHM9N0l5SjHt)y#vfIy z&#kjF6&CM1o4@6P2As)~n=C?nZ`0m-`M5VHzc%#eFmTn`+$mqB*FJToYR#NPl_iObc6 zdb)_oJj9x{(NRQTBO|BeSN{4k=7cifUS}lYO&`*%@lpg1T4aV8qQ6)4?J1yOsMyF2t;<6ZWhIrzW1(P~HJhf%6^W0KL!Wd}V)S-vn6{ zH`S5$`59`#(ibWMOLD1KVJ2`&?b-lC5yCHxM?V)?mvikEzgyZJ?*bdPdtg`75spxx zze$sEts&&oLh>uTkTpj~_=pOp2d?(m;nO$Nmg$)$c~A1^4_eW%L>hqp(20Z2-!{F! z`?!x0#!i)zCQ!KyCTC>DX+Onz{D<)RRq&+d!W*AGvc{AQ%cjUUIdXemI_`XOnc}vL z#$I)B1a2CT=+3?o`wAx1u1a zsPsA>&iWjVH`uy1WJaT8GEQmO=!87?g(>-zY`bBb=?hqQKqpvBtGeAE+7ucF-R^+2 z?(m0gEK3yEOi>q~>o{xw9X|Q4=W?rxNXZzap)bisqp@eE3?fM_ZnnlkbZLd`BGiSoC8vDpx0}OJ6_Cpva5c{iTPt z-%1n5oKrgHNUR2t%GWQup}LvfsI6@Xpyk>)Ol{E$^XOyVKxYiYy6~&+2|~IJ%PFAe z33X1$A7$D=Yx&E0R*<|KInpiG&)jK)STfXMn$tSGgPYr0vI78_O2RTDy4_Rl7DgKZ zAu%Cmw&V1(E(MjC;VewEFe$Miz#odii|o6^T->Y_ge8Z>P;a22p)Tz}0lQ8T54IAn zyt8w+RUn{HFC~4HvzfK+)B+D^%?{S7`p9O<8*4c;zqQ($*#L6JyD_mtQv z<{ov(>yi%&!3K?gktLA3s}E(Z=(Yj>{4+uu?1o9`P*S=nPEdDtd*$bneZ-Th)bp^T zH&bxje0sy3p!1-aCqOd8c=K=?-AA*XCgOY*vDQ>uA=9KzlU&kX>2b5|14z=if} zXL?;;iA=oO-Rix!N9DG>;LY==3EX;e^%hLO@v`4DFp@;umy}J}5-Y`ZaR5s(l-wC{ z_eXR1OOQ4DlhH-q5N;58jyUkFNZ6#{QgfufD}L&3A%GuSP(Qpw=BX*lTmm{wNWv-Q z(pq-*Qs(|^XE?r9e*^~&OVUmahei*X4RW)BQ+}Gy`z;xH5wpUp@o#59!*sVAyW)wEs?HYcgdJjd8-QACdJ&(xXR211}+YSu!K&}%Ei1EaC zyq6%|5~Q7cLuPayBIFskU79Zt=m~p%Kp@5)<$$Ktl~{knH)yrT?dQ} z+65n^kaQC`dlr+1W^)9hgGIjsA^1j52nabAdQpAs?ymf57(sB$X+pt7MRoEKmGUO$ z5~ldm#@%he)@?*$oR?`c*uXJ6bxpCfwfH~k=KGAz%*+_n1^qTw_+nUEi6gAmF8UUn zTC4q)f6jvOMrTG?zw>OuD7S@q1->M(LU9GgJgPD{+x5BF9Gd8KyoVqem2fY9V> zTy59gN2I(}cvN0?=Y7J#KvV((u`8A88gCs7>qN4xAm#Z^@3O)9@+6mBVVOJ!MC;D zUPeZ{?Y9Cz>;U6#qP6(krN`9El*eP%$JN6)5RCTsDC5RjyBct>(d$~W5Z_`Iu6-&C z-Bd~-vMsj0G1U}bs1>7bM;aJkH>^f?60wtjLpr8)d8--0RYJIgwpAm>CE;Ub#jnXJ z)?NQe-^~+l08?xLbR-=C&?si-TH>7A~27w(;qeKg^ zPDlJuyp^YYr(6^+=6&oiy>)Qh?4a|S>faZqmzRVv1AQRBa6g&kfPkOV7h%LR&_mee zkse|MF4+_VKE}mu7fW+(r75cldB`hU#?aJ=;)P3z-CL%VlM`Dt`H_5>e~0v;k1|&j z#3(0=)O$qRhaDYDOfk%M1qmLw#P}GQMj4O1$h%D$gJ~Cer7`>So=kB0=ymp50wp1o z7=2?zxcZIeZhpq`NdxN^7Kj}%A9X&d>b(ifo@ zAb-?}MswSk;|jA|PBi=6c0YaRE6a~%@zNVF^d^z!Sp_lIjRALTJM?2rP;Z~}5n&=E zAJ+B-@9$>W9bJw9+$1d$6QC zPCA;0>3^=hdmgwi`N75LKAGj~#LP+NzU9ug>I`DOEB1Q5k0{#D*cq=Z7mmC5#W#j_ zPOjHcDi~oQ9srU1A!WU>t7k8f`-V??4r6N_!v8e!UDJn5ZB89U84b>r-gpzH&U z*P3Eub`a~%w6}lk{P^$4|u1IeOvGREdVi=1fRM=D3-u!nUa$vc2dXq z4p#YWJnYCOx)Cj_Jtmd9(Z%Gdy*R9~Wc}%LkV(9US&g!yx{T z=R%3d0+sasBe^7@gZ0#~gICB)X+fLShNMVr-G;KVj5s)~ep&JkEf5V2nX9YafES`q z_#A2Ch#K-w%6(cWHp@NIq)|SXDZZx(->;iPngFCqMqhs34Ld(XZ(Y3jOBkI%0A!3{ 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 D0|+Ag diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 965e398..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,13 +1,13 @@ #!/bin/sh # -# Copyright ยฉ 2015-2021 the original authors. +# Copyright ยฉ 2015 the original authors. # # 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 # -# https://www.apache.org/licenses/LICENSE-2.0 +# https://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, @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -210,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell From 889f7a70946fb85c01a187931c9b3cafd3290a22 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 22 Dec 2025 19:27:11 +0000 Subject: [PATCH 3/4] Update dependency reports --- dependencies.md | 515 ++++++++++++++++++++++++++++++------------------ pom.xml | 148 +++++++------- 2 files changed, 403 insertions(+), 260 deletions(-) diff --git a/dependencies.md b/dependencies.md index da43e70..edc85cd 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,25 +1,25 @@ -# Dependencies of `io.spine:spine-change:2.0.0-SNAPSHOT.200` +# Dependencies of `io.spine:spine-change:2.0.0-SNAPSHOT.205` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. * **Project URL:** [http://findbugs.sourceforge.net/](http://findbugs.sourceforge.net/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.10.1. - * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson) +1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.13.0. + * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.errorprone. **Name** : error_prone_annotations. **Version** : 2.36.0. * **Project URL:** [https://errorprone.info/error_prone_annotations](https://errorprone.info/error_prone_annotations) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : failureaccess. **Version** : 1.0.1. +1. **Group** : com.google.guava. **Name** : failureaccess. **Version** : 1.0.3. * **Project URL:** [https://github.com/google/guava/](https://github.com/google/guava/) - * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : guava. **Version** : 32.1.3-jre. +1. **Group** : com.google.guava. **Name** : guava. **Version** : 33.5.0-jre. * **Project URL:** [https://github.com/google/guava](https://github.com/google/guava) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -30,90 +30,122 @@ * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 3.25.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.1. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 3.25.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.1. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 3.25.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.1. + * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : org.checkerframework. **Name** : checker-qual. **Version** : 3.40.0. - * **Project URL:** [https://checkerframework.org/](https://checkerframework.org/) - * **License:** [The MIT License](http://opensource.org/licenses/MIT) - 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.1.20. - * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.ow2.asm. **Name** : asm. **Version** : 9.6. - * **Project URL:** [http://asm.ow2.io/](http://asm.ow2.io/) - * **License:** [BSD-3-Clause](https://asm.ow2.io/license.html) - * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu-jvm. **Version** : 0.29.0. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -## Compile, tests, and tooling -1. **Group** : com.beust. **Name** : jcommander. **Version** : 1.48. - * **Project URL:** [http://beust.com/jcommander](http://beust.com/jcommander) - * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-bom. **Version** : 1.10.2. + * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jspecify. **Name** : jspecify. **Version** : 1.0.0. + * **Project URL:** [http://jspecify.org/](http://jspecify.org/) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +## Compile, tests, and tooling +1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. * **Project URL:** [https://github.com/FasterXML/jackson-bom](https://github.com/FasterXML/jackson-bom) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.15.3. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-annotations. **Version** : 2.20. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.15.3. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-core. **Version** : 2.20.0. * **Project URL:** [https://github.com/FasterXML/jackson-core](https://github.com/FasterXML/jackson-core) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.15.3. +1. **Group** : com.fasterxml.jackson.core. **Name** : jackson-databind. **Version** : 2.20.0. * **Project URL:** [https://github.com/FasterXML/jackson](https://github.com/FasterXML/jackson) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.15.3. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-xml. **Version** : 2.20.0. * **Project URL:** [https://github.com/FasterXML/jackson-dataformat-xml](https://github.com/FasterXML/jackson-dataformat-xml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.15.3. +1. **Group** : com.fasterxml.jackson.dataformat. **Name** : jackson-dataformat-yaml. **Version** : 2.20.0. * **Project URL:** [https://github.com/FasterXML/jackson-dataformats-text](https://github.com/FasterXML/jackson-dataformats-text) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.15.3. +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-guava. **Version** : 2.20.0. + * **Project URL:** [https://github.com/FasterXML/jackson-datatypes-collections](https://github.com/FasterXML/jackson-datatypes-collections) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jdk8. **Version** : 2.20.0. + * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.fasterxml.jackson.datatype. **Name** : jackson-datatype-jsr310. **Version** : 2.20.0. + * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310](https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-kotlin. **Version** : 2.20.0. * **Project URL:** [https://github.com/FasterXML/jackson-module-kotlin](https://github.com/FasterXML/jackson-module-kotlin) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.fasterxml.woodstox. **Name** : woodstox-core. **Version** : 6.5.1. +1. **Group** : com.fasterxml.jackson.module. **Name** : jackson-module-parameter-names. **Version** : 2.20.0. + * **Project URL:** [https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names](https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : com.fasterxml.woodstox. **Name** : woodstox-core. **Version** : 7.1.1. * **Project URL:** [https://github.com/FasterXML/woodstox](https://github.com/FasterXML/woodstox) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 2.9.3. + * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + 1. **Group** : com.github.ben-manes.caffeine. **Name** : caffeine. **Version** : 3.0.5. * **Project URL:** [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -126,11 +158,15 @@ * **Project URL:** [https://www.github.com/KevinStern/software-and-algorithms](https://www.github.com/KevinStern/software-and-algorithms) * **License:** [MIT License](http://www.opensource.org/licenses/mit-license.php) +1. **Group** : com.github.oowekyala.ooxml. **Name** : nice-xml-messages. **Version** : 3.1. + * **Project URL:** [https://github.com/oowekyala/nice-xml-messages](https://github.com/oowekyala/nice-xml-messages) + * **License:** [MIT License](https://github.com/oowekyala/nice-xml-messages/tree/master/LICENSE) + 1. **Group** : com.google.android. **Name** : annotations. **Version** : 4.1.1.4. * **Project URL:** [http://source.android.com/](http://source.android.com/) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) -1. **Group** : com.google.api.grpc. **Name** : proto-google-common-protos. **Version** : 2.22.0. +1. **Group** : com.google.api.grpc. **Name** : proto-google-common-protos. **Version** : 2.59.2. * **Project URL:** [https://github.com/googleapis/sdk-platform-java](https://github.com/googleapis/sdk-platform-java) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -150,31 +186,27 @@ * **Project URL:** [http://findbugs.sourceforge.net/](http://findbugs.sourceforge.net/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.10.1. - * **Project URL:** [https://github.com/google/gson/gson](https://github.com/google/gson/gson) +1. **Group** : com.google.code.gson. **Name** : gson. **Version** : 2.13.0. + * **Project URL:** [https://github.com/google/gson](https://github.com/google/gson) * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing. **Version** : 2.1.20-1.0.31. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-aa-embeddable. **Version** : 2.1.20-1.0.31. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-aa-embeddable. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.1.20-1.0.31. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-api. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-cmdline. **Version** : 2.1.20-1.0.31. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-common-deps. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-common-deps. **Version** : 2.1.20-1.0.31. - * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-gradle-plugin. **Version** : 2.1.20-1.0.31. +1. **Group** : com.google.devtools.ksp. **Name** : symbol-processing-gradle-plugin. **Version** : 2.3.0. * **Project URL:** [https://goo.gle/ksp](https://goo.gle/ksp) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -213,15 +245,19 @@ 1. **Group** : com.google.googlejavaformat. **Name** : google-java-format. **Version** : 1.19.1. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : failureaccess. **Version** : 1.0.1. +1. **Group** : com.google.gradle. **Name** : osdetector-gradle-plugin. **Version** : 1.7.3. + * **Project URL:** [https://github.com/google/osdetector-gradle-plugin](https://github.com/google/osdetector-gradle-plugin) + * **License:** [Apache License 2.0](http://opensource.org/licenses/Apache-2.0) + +1. **Group** : com.google.guava. **Name** : failureaccess. **Version** : 1.0.3. * **Project URL:** [https://github.com/google/guava/](https://github.com/google/guava/) - * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : guava. **Version** : 32.1.3-jre. +1. **Group** : com.google.guava. **Name** : guava. **Version** : 33.5.0-jre. * **Project URL:** [https://github.com/google/guava](https://github.com/google/guava) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.guava. **Name** : guava-testlib. **Version** : 32.1.3-jre. +1. **Group** : com.google.guava. **Name** : guava-testlib. **Version** : 33.5.0-jre. * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : com.google.guava. **Name** : listenablefuture. **Version** : 9999.0-empty-to-avoid-conflict-with-guava. @@ -231,15 +267,20 @@ * **Project URL:** [https://github.com/google/j2objc/](https://github.com/google/j2objc/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 3.25.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-gradle-plugin. **Version** : 0.9.5. + * **Project URL:** [https://github.com/google/protobuf-gradle-plugin](https://github.com/google/protobuf-gradle-plugin) + * **License:** [BSD 3-Clause](http://opensource.org/licenses/BSD-3-Clause) + +1. **Group** : com.google.protobuf. **Name** : protobuf-java. **Version** : 4.33.1. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 3.25.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-java-util. **Version** : 4.33.1. * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) -1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 3.25.1. +1. **Group** : com.google.protobuf. **Name** : protobuf-kotlin. **Version** : 4.33.1. + * **Project URL:** [https://developers.google.com/protocol-buffers/](https://developers.google.com/protocol-buffers/) * **License:** [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) 1. **Group** : com.google.protobuf. **Name** : protoc. **Version** : 3.19.6. @@ -247,23 +288,31 @@ * **License:** [3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.truth. **Name** : truth. **Version** : 1.1.5. +1. **Group** : com.google.truth. **Name** : truth. **Version** : 1.4.4. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.truth.extensions. **Name** : truth-java8-extension. **Version** : 1.1.5. +1. **Group** : com.google.truth.extensions. **Name** : truth-java8-extension. **Version** : 1.4.4. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.truth.extensions. **Name** : truth-liteproto-extension. **Version** : 1.1.5. +1. **Group** : com.google.truth.extensions. **Name** : truth-liteproto-extension. **Version** : 1.4.4. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.google.truth.extensions. **Name** : truth-proto-extension. **Version** : 1.1.5. +1. **Group** : com.google.truth.extensions. **Name** : truth-proto-extension. **Version** : 1.4.4. * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : com.palantir.javaformat. **Name** : palantir-java-format. **Version** : 2.75.0. + * **Project URL:** [https://github.com/palantir/palantir-java-format](https://github.com/palantir/palantir-java-format) + * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + +1. **Group** : com.palantir.javaformat. **Name** : palantir-java-format-spi. **Version** : 2.75.0. + * **Project URL:** [https://github.com/palantir/palantir-java-format](https://github.com/palantir/palantir-java-format) + * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + 1. **Group** : com.puppycrawl.tools. **Name** : checkstyle. **Version** : 10.12.1. * **Project URL:** [https://checkstyle.org/](https://checkstyle.org/) * **License:** [LGPL-2.1+](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt) -1. **Group** : com.sksamuel.aedile. **Name** : aedile-core. **Version** : 1.3.1. +1. **Group** : com.sksamuel.aedile. **Name** : aedile-core. **Version** : 2.1.2. * **Project URL:** [http://www.github.com/sksamuel/aedile](http://www.github.com/sksamuel/aedile) * **License:** [The Apache 2.0 License](https://opensource.org/licenses/Apache-2.0) @@ -275,15 +324,15 @@ * **Project URL:** [http://github.com/square/javapoet/](http://github.com/square/javapoet/) * **License:** [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.squareup. **Name** : kotlinpoet. **Version** : 2.0.0. +1. **Group** : com.squareup. **Name** : kotlinpoet. **Version** : 2.2.0. * **Project URL:** [https://github.com/square/kotlinpoet](https://github.com/square/kotlinpoet) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.squareup. **Name** : kotlinpoet-jvm. **Version** : 2.0.0. +1. **Group** : com.squareup. **Name** : kotlinpoet-jvm. **Version** : 2.2.0. * **Project URL:** [https://github.com/square/kotlinpoet](https://github.com/square/kotlinpoet) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : com.squareup. **Name** : kotlinpoet-ksp. **Version** : 2.0.0. +1. **Group** : com.squareup. **Name** : kotlinpoet-ksp. **Version** : 2.2.0. * **Project URL:** [https://github.com/square/kotlinpoet](https://github.com/square/kotlinpoet) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -426,82 +475,78 @@ * **Project URL:** [https://detekt.dev](https://detekt.dev) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : io.grpc. **Name** : grpc-api. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-api. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-context. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-bom. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-core. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-context. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-inprocess. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-core. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-protobuf. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-inprocess. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-protobuf-lite. **Version** : 1.59.0. - * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) +1. **Group** : io.grpc. **Name** : grpc-kotlin-stub. **Version** : 1.4.1. + * **Project URL:** [https://github.com/grpc/grpc-kotlin](https://github.com/grpc/grpc-kotlin) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-stub. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-protobuf. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : grpc-util. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-protobuf-lite. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.grpc. **Name** : protoc-gen-grpc-java. **Version** : 1.59.0. +1. **Group** : io.grpc. **Name** : grpc-stub. **Version** : 1.76.0. * **Project URL:** [https://github.com/grpc/grpc-java](https://github.com/grpc/grpc-java) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-api. **Version** : 5.9.1. +1. **Group** : io.kotest. **Name** : kotest-assertions-core. **Version** : 6.0.4. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-api-jvm. **Version** : 5.9.1. +1. **Group** : io.kotest. **Name** : kotest-assertions-core-jvm. **Version** : 6.0.4. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-core. **Version** : 5.9.1. +1. **Group** : io.kotest. **Name** : kotest-assertions-shared. **Version** : 6.0.4. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-core-jvm. **Version** : 5.9.1. +1. **Group** : io.kotest. **Name** : kotest-assertions-shared-jvm. **Version** : 6.0.4. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-shared. **Version** : 5.9.1. +1. **Group** : io.kotest. **Name** : kotest-common. **Version** : 6.0.4. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-assertions-shared-jvm. **Version** : 5.9.1. +1. **Group** : io.kotest. **Name** : kotest-common-jvm. **Version** : 6.0.4. * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : io.kotest. **Name** : kotest-common. **Version** : 5.9.1. - * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) - * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-api. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : io.kotest. **Name** : kotest-common-jvm. **Version** : 5.9.1. - * **Project URL:** [https://github.com/kotest/kotest](https://github.com/kotest/kotest) - * **License:** [Apache-2.0](https://opensource.org/licenses/Apache-2.0) +1. **Group** : io.opentelemetry. **Name** : opentelemetry-context. **Version** : 1.41.0. + * **Project URL:** [https://github.com/open-telemetry/opentelemetry-java](https://github.com/open-telemetry/opentelemetry-java) + * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : io.perfmark. **Name** : perfmark-api. **Version** : 0.26.0. +1. **Group** : io.perfmark. **Name** : perfmark-api. **Version** : 0.27.0. * **Project URL:** [https://github.com/perfmark/perfmark](https://github.com/perfmark/perfmark) * **License:** [Apache 2.0](https://opensource.org/licenses/Apache-2.0) -1. **Group** : it.unimi.dsi. **Name** : fastutil-core. **Version** : 8.5.12. - * **Project URL:** [http://fastutil.di.unimi.it/](http://fastutil.di.unimi.it/) - * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) - 1. **Group** : javax.annotation. **Name** : javax.annotation-api. **Version** : 1.3.2. * **Project URL:** [http://jcp.org/en/jsr/detail?id=250](http://jcp.org/en/jsr/detail?id=250) * **License:** [CDDL + GPLv2 with classpath exception](https://github.com/javaee/javax.annotation/blob/master/LICENSE) @@ -510,35 +555,42 @@ * **Project URL:** [http://code.google.com/p/atinject/](http://code.google.com/p/atinject/) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : junit. **Name** : junit. **Version** : 4.13.1. +1. **Group** : junit. **Name** : junit. **Version** : 4.13.2. * **Project URL:** [http://junit.org](http://junit.org) * **License:** [Eclipse Public License 1.0](http://www.eclipse.org/legal/epl-v10.html) +1. **Group** : kr.motd.maven. **Name** : os-maven-plugin. **Version** : 1.7.1. + * **Project URL:** [https://github.com/trustin/os-maven-plugin/](https://github.com/trustin/os-maven-plugin/) + * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + 1. **Group** : net.sf.saxon. **Name** : Saxon-HE. **Version** : 12.2. * **Project URL:** [http://www.saxonica.com/](http://www.saxonica.com/) * **License:** [Mozilla Public License Version 2.0](http://www.mozilla.org/MPL/2.0/) -1. **Group** : net.sourceforge.pmd. **Name** : pmd-core. **Version** : 6.55.0. +1. **Group** : net.sf.saxon. **Name** : Saxon-HE. **Version** : 12.5. + * **Project URL:** [http://www.saxonica.com/](http://www.saxonica.com/) + * **License:** [Mozilla Public License Version 2.0](http://www.mozilla.org/MPL/2.0/) + +1. **Group** : net.sourceforge.pmd. **Name** : pmd-ant. **Version** : 7.12.0. * **License:** [BSD-style](http://pmd.sourceforge.net/license.html) -1. **Group** : net.sourceforge.pmd. **Name** : pmd-java. **Version** : 6.55.0. +1. **Group** : net.sourceforge.pmd. **Name** : pmd-core. **Version** : 7.12.0. * **License:** [BSD-style](http://pmd.sourceforge.net/license.html) -1. **Group** : net.sourceforge.saxon. **Name** : saxon. **Version** : 9.1.0.8. - * **Project URL:** [http://saxon.sourceforge.net/](http://saxon.sourceforge.net/) - * **License:** [Mozilla Public License Version 1.0](http://www.mozilla.org/MPL/MPL-1.0.txt) +1. **Group** : net.sourceforge.pmd. **Name** : pmd-java. **Version** : 7.12.0. + * **License:** [BSD-style](http://pmd.sourceforge.net/license.html) 1. **Group** : org.antlr. **Name** : antlr4-runtime. **Version** : 4.11.1. * **Project URL:** [https://www.antlr.org/](https://www.antlr.org/) * **License:** [BSD-3-Clause](https://www.antlr.org/license.html) -1. **Group** : org.antlr. **Name** : antlr4-runtime. **Version** : 4.7.2. +1. **Group** : org.antlr. **Name** : antlr4-runtime. **Version** : 4.9.3. * **Project URL:** [http://www.antlr.org](http://www.antlr.org) * **License:** [The BSD License](http://www.antlr.org/license.html) -1. **Group** : org.apache.commons. **Name** : commons-lang3. **Version** : 3.8.1. - * **Project URL:** [http://commons.apache.org/proper/commons-lang/](http://commons.apache.org/proper/commons-lang/) - * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.apache.commons. **Name** : commons-lang3. **Version** : 3.17.0. + * **Project URL:** [https://commons.apache.org/proper/commons-lang/](https://commons.apache.org/proper/commons-lang/) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : org.apache.httpcomponents.client5. **Name** : httpclient5. **Version** : 5.1.3. * **License:** [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -553,6 +605,23 @@ * **Project URL:** [https://github.com/apiguardian-team/apiguardian](https://github.com/apiguardian-team/apiguardian) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) +1. **Group** : org.bouncycastle. **Name** : bcpg-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcpkix-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcprov-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + +1. **Group** : org.bouncycastle. **Name** : bcutil-jdk18on. **Version** : 1.80. + * **Project URL:** [https://www.bouncycastle.org/download/bouncy-castle-java/](https://www.bouncycastle.org/download/bouncy-castle-java/) + * **License:** [Bouncy Castle Licence](https://www.bouncycastle.org/licence.html) + 1. **Group** : org.checkerframework. **Name** : checker-compat-qual. **Version** : 2.5.3. * **Project URL:** [https://checkerframework.org](https://checkerframework.org) * **License:** [GNU General Public License, version 2 (GPL2), with the classpath exception](http://www.gnu.org/software/classpath/license.html) @@ -566,33 +635,37 @@ * **License:** [MIT license](http://www.opensource.org/licenses/mit-license.php) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.codehaus.woodstox. **Name** : stax2-api. **Version** : 4.2.1. +1. **Group** : org.codehaus.woodstox. **Name** : stax2-api. **Version** : 4.2.2. * **Project URL:** [http://github.com/FasterXML/stax2-api](http://github.com/FasterXML/stax2-api) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) - * **License:** [The BSD License](http://www.opensource.org/licenses/bsd-license.php) + * **License:** [The BSD 2-Clause License](http://www.opensource.org/licenses/bsd-license.php) 1. **Group** : org.freemarker. **Name** : freemarker. **Version** : 2.3.32. * **Project URL:** [https://freemarker.apache.org/](https://freemarker.apache.org/) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.hamcrest. **Name** : hamcrest. **Version** : 2.2. +1. **Group** : org.functionaljava. **Name** : functionaljava. **Version** : 4.8. + * **Project URL:** [http://functionaljava.org/](http://functionaljava.org/) + * **License:** [The BSD3 License](https://github.com/functionaljava/functionaljava/blob/master/etc/LICENCE) + +1. **Group** : org.hamcrest. **Name** : hamcrest. **Version** : 3.0. * **Project URL:** [http://hamcrest.org/JavaHamcrest/](http://hamcrest.org/JavaHamcrest/) - * **License:** [BSD License 3](http://opensource.org/licenses/BSD-3-Clause) + * **License:** [BSD-3-Clause](https://raw.githubusercontent.com/hamcrest/JavaHamcrest/master/LICENSE) -1. **Group** : org.hamcrest. **Name** : hamcrest-core. **Version** : 2.2. +1. **Group** : org.hamcrest. **Name** : hamcrest-core. **Version** : 3.0. * **Project URL:** [http://hamcrest.org/JavaHamcrest/](http://hamcrest.org/JavaHamcrest/) - * **License:** [BSD License 3](http://opensource.org/licenses/BSD-3-Clause) + * **License:** [BSD-3-Clause](https://raw.githubusercontent.com/hamcrest/JavaHamcrest/master/LICENSE) -1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.12. +1. **Group** : org.jacoco. **Name** : org.jacoco.agent. **Version** : 0.8.13. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.12. +1. **Group** : org.jacoco. **Name** : org.jacoco.ant. **Version** : 0.8.13. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.12. +1. **Group** : org.jacoco. **Name** : org.jacoco.core. **Version** : 0.8.13. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) -1. **Group** : org.jacoco. **Name** : org.jacoco.report. **Version** : 0.8.12. +1. **Group** : org.jacoco. **Name** : org.jacoco.report. **Version** : 0.8.13. * **License:** [EPL-2.0](https://www.eclipse.org/legal/epl-2.0/) 1. **Group** : org.javassist. **Name** : javassist. **Version** : 3.28.0-GA. @@ -601,11 +674,11 @@ * **License:** [LGPL 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) * **License:** [MPL 1.1](http://www.mozilla.org/MPL/MPL-1.1.html) -1. **Group** : org.jboss.forge.roaster. **Name** : roaster-api. **Version** : 2.28.0.Final. +1. **Group** : org.jboss.forge.roaster. **Name** : roaster-api. **Version** : 2.29.0.Final. * **License:** [Eclipse Public License version 1.0](http://www.eclipse.org/legal/epl-v10.html) * **License:** [Public Domain](http://repository.jboss.org/licenses/cc0-1.0.txt) -1. **Group** : org.jboss.forge.roaster. **Name** : roaster-jdt. **Version** : 2.28.0.Final. +1. **Group** : org.jboss.forge.roaster. **Name** : roaster-jdt. **Version** : 2.29.0.Final. * **License:** [Eclipse Public License version 1.0](http://www.eclipse.org/legal/epl-v10.html) * **License:** [Public Domain](http://repository.jboss.org/licenses/cc0-1.0.txt) @@ -617,43 +690,39 @@ * **Project URL:** [https://github.com/JetBrains/java-annotations](https://github.com/JetBrains/java-annotations) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains. **Name** : markdown. **Version** : 0.5.2. +1. **Group** : org.jetbrains. **Name** : markdown. **Version** : 0.7.3. * **Project URL:** [https://github.com/JetBrains/markdown](https://github.com/JetBrains/markdown) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains. **Name** : markdown-jvm. **Version** : 0.5.2. +1. **Group** : org.jetbrains. **Name** : markdown-jvm. **Version** : 0.7.3. * **Project URL:** [https://github.com/JetBrains/markdown](https://github.com/JetBrains/markdown) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : analysis-kotlin-descriptors. **Version** : 1.9.20. - * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - -1. **Group** : org.jetbrains.dokka. **Name** : analysis-markdown. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : analysis-kotlin-symbols. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : dokka-base. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : analysis-markdown. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : dokka-core. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : dokka-base. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : gfm-plugin. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : dokka-core. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : javadoc-plugin. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : javadoc-plugin. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : jekyll-plugin. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : kotlin-as-java-plugin. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.dokka. **Name** : kotlin-as-java-plugin. **Version** : 1.9.20. +1. **Group** : org.jetbrains.dokka. **Name** : templating-plugin. **Version** : 2.1.0. * **Project URL:** [https://github.com/Kotlin/dokka](https://github.com/Kotlin/dokka) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -661,105 +730,149 @@ * **Project URL:** [https://github.com/JetBrains/intellij-deps-trove4j](https://github.com/JetBrains/intellij-deps-trove4j) * **License:** [GNU LESSER GENERAL PUBLIC LICENSE 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : abi-tools-api. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-bom. **Version** : 2.2.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-api. **Version** : 2.2.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-build-tools-impl. **Version** : 2.2.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.0.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-compiler-runner. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-client. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.0.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-daemon-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-klib-commonizer-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-metadata-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-reflect. **Version** : 2.2.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.0.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-script-runtime. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-compiler-impl-embeddable. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-scripting-jvm. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-common. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk7. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.1.20. +1. **Group** : org.jetbrains.kotlin. **Name** : kotlin-stdlib-jdk8. **Version** : 2.2.21. * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) - * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlin. **Name** : swift-export-embeddable. **Version** : 2.2.21. + * **Project URL:** [https://kotlinlang.org/](https://kotlinlang.org/) + * **License:** [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.23.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-bom. **Version** : 1.10.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu. **Version** : 0.29.0. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : atomicfu-jvm. **Version** : 0.29.0. + * **Project URL:** [https://github.com/Kotlin/kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-bom. **Version** : 1.10.2. * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core. **Version** : 1.10.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core. **Version** : 1.10.2. * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core-jvm. **Version** : 1.10.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-core-jvm. **Version** : 1.10.2. * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-jdk8. **Version** : 1.10.1. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-jdk8. **Version** : 1.10.2. * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-test. **Version** : 1.10.2. + * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-coroutines-test-jvm. **Version** : 1.10.2. + * **Project URL:** [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime. **Version** : 0.7.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-datetime-jvm. **Version** : 0.7.1. + * **Project URL:** [https://github.com/Kotlin/kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) + * **License:** [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-html-jvm. **Version** : 0.8.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) @@ -769,7 +882,7 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.html](https://github.com/Kotlin/kotlinx.html) * **License:** [The Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.6.3. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-bom. **Version** : 1.7.3. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -777,7 +890,7 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.6.3. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core. **Version** : 1.7.3. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -785,7 +898,7 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.6.3. +1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-core-jvm. **Version** : 1.7.3. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) @@ -793,18 +906,10 @@ * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json. **Version** : 1.6.3. - * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - 1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json-jvm. **Version** : 1.4.1. * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.jetbrains.kotlinx. **Name** : kotlinx-serialization-json-jvm. **Version** : 1.6.3. - * **Project URL:** [https://github.com/Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) - * **License:** [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) - 1. **Group** : org.jsoup. **Name** : jsoup. **Version** : 1.16.1. * **Project URL:** [https://jsoup.org/](https://jsoup.org/) * **License:** [The MIT License](https://jsoup.org/license) @@ -813,28 +918,36 @@ * **Project URL:** [http://jspecify.org/](http://jspecify.org/) * **License:** [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) -1. **Group** : org.junit. **Name** : junit-bom. **Version** : 5.10.0. - * **Project URL:** [https://junit.org/junit5/](https://junit.org/junit5/) +1. **Group** : org.junit. **Name** : junit-bom. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 5.10.0. - * **Project URL:** [https://junit.org/junit5/](https://junit.org/junit5/) +1. **Group** : org.junit-pioneer. **Name** : junit-pioneer. **Version** : 2.3.0. + * **Project URL:** [https://junit-pioneer.org/](https://junit-pioneer.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 5.10.0. - * **Project URL:** [https://junit.org/junit5/](https://junit.org/junit5/) +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-api. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 5.10.0. - * **Project URL:** [https://junit.org/junit5/](https://junit.org/junit5/) +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-engine. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 1.10.0. - * **Project URL:** [https://junit.org/junit5/](https://junit.org/junit5/) +1. **Group** : org.junit.jupiter. **Name** : junit-jupiter-params. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) -1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 1.10.0. - * **Project URL:** [https://junit.org/junit5/](https://junit.org/junit5/) +1. **Group** : org.junit.platform. **Name** : junit-platform-commons. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) + * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) + +1. **Group** : org.junit.platform. **Name** : junit-platform-engine. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) + * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) + +1. **Group** : org.junit.platform. **Name** : junit-platform-launcher. **Version** : 6.0.0. + * **Project URL:** [https://junit.org/](https://junit.org/) * **License:** [Eclipse Public License v2.0](https://www.eclipse.org/legal/epl-v20.html) 1. **Group** : org.opentest4j. **Name** : opentest4j. **Version** : 1.3.0. @@ -860,11 +973,19 @@ * **Project URL:** [https://github.com/hrldcpr/pcollections](https://github.com/hrldcpr/pcollections) * **License:** [The MIT License](https://opensource.org/licenses/mit-license.php) +1. **Group** : org.pcollections. **Name** : pcollections. **Version** : 4.0.2. + * **Project URL:** [https://github.com/hrldcpr/pcollections](https://github.com/hrldcpr/pcollections) + * **License:** [The MIT License](https://opensource.org/licenses/mit-license.php) + 1. **Group** : org.reflections. **Name** : reflections. **Version** : 0.10.2. * **Project URL:** [http://github.com/ronmamo/reflections](http://github.com/ronmamo/reflections) * **License:** [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) * **License:** [WTFPL](http://www.wtfpl.net/) +1. **Group** : org.slf4j. **Name** : jul-to-slf4j. **Version** : 1.7.36. + * **Project URL:** [http://www.slf4j.org](http://www.slf4j.org) + * **License:** [MIT License](http://www.opensource.org/licenses/mit-license.php) + 1. **Group** : org.snakeyaml. **Name** : snakeyaml-engine. **Version** : 2.7. * **Project URL:** [https://bitbucket.org/snakeyaml/snakeyaml-engine](https://bitbucket.org/snakeyaml/snakeyaml-engine) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -873,11 +994,17 @@ * **Project URL:** [https://github.com/xmlresolver/xmlresolver](https://github.com/xmlresolver/xmlresolver) * **License:** [Apache License version 2.0](https://www.apache.org/licenses/LICENSE-2.0) -1. **Group** : org.yaml. **Name** : snakeyaml. **Version** : 2.1. +1. **Group** : org.xmlresolver. **Name** : xmlresolver. **Version** : 5.2.2. + * **Project URL:** [https://github.com/xmlresolver/xmlresolver](https://github.com/xmlresolver/xmlresolver) + * **License:** [Apache License version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + +1. **Group** : org.yaml. **Name** : snakeyaml. **Version** : 2.4. * **Project URL:** [https://bitbucket.org/snakeyaml/snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) * **License:** [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Thu Mar 27 10:27:34 WET 2025** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file +This report was generated on **Mon Dec 22 19:25:55 WET 2025** using +[Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under +[Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/pom.xml b/pom.xml index 89126b0..72cc3ad 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine spine-change -2.0.0-SNAPSHOT.200 +2.0.0-SNAPSHOT.205 2015 @@ -26,37 +26,73 @@ all modules and does not describe the project structure per-subproject. io.spine spine-base - 2.0.0-SNAPSHOT.307 + 2.0.0-SNAPSHOT.383 compile - io.spine.validation - spine-validation-java-runtime - 2.0.0-SNAPSHOT.305 + io.spine + spine-validation-jvm-runtime + 2.0.0-SNAPSHOT.383 compile org.jetbrains.kotlin kotlin-stdlib - 2.1.20 + 2.2.21 compile + + com.google.guava + guava-testlib + 33.5.0-jre + test + + + io.kotest + kotest-assertions-core + 6.0.4 + test + io.spine.tools spine-testlib - 2.0.0-SNAPSHOT.185 + 2.0.0-SNAPSHOT.211 test io.spine.tools spine-time-testlib - 2.0.0-SNAPSHOT.200 + 2.0.0-SNAPSHOT.220 + test + + + org.junit + junit-bom + 6.0.0 + test + + + org.junit-pioneer + junit-pioneer + 2.3.0 + test + + + org.junit.jupiter + junit-jupiter-api + null test org.junit.jupiter junit-jupiter-engine - 5.10.0 + null + test + + + org.junit.jupiter + junit-jupiter-params + null test @@ -68,28 +104,18 @@ all modules and does not describe the project structure per-subproject. com.google.devtools.ksp symbol-processing - 2.1.20-1.0.31 + 2.3.0 com.google.devtools.ksp symbol-processing-api - 2.1.20-1.0.31 - - - com.google.devtools.ksp - symbol-processing-cmdline - 2.1.20-1.0.31 + 2.3.0 com.google.errorprone error_prone_core 2.36.0 - - com.google.errorprone - javac - 9+181-r4173-1 - com.google.protobuf protoc @@ -111,109 +137,99 @@ all modules and does not describe the project structure per-subproject. 1.23.8 - io.grpc - protoc-gen-grpc-java - 1.59.0 + io.spine.tools + compiler-cli-all + 2.0.0-SNAPSHOT.035 - io.spine.protodata - protodata-fat-cli - 0.93.4 + io.spine.tools + compiler-protoc-plugin + 2.0.0-SNAPSHOT.035 - io.spine.protodata - protodata-protoc - 0.93.4 + io.spine.tools + core-jvm-gradle-plugins + 2.0.0-SNAPSHOT.042 io.spine.tools - spine-dokka-extensions - 2.0.0-SNAPSHOT.6 + core-jvm-routing + 2.0.0-SNAPSHOT.042 io.spine.tools - spine-mc-java-plugins - 2.0.0-SNAPSHOT.306 + spine-dokka-extensions + 2.0.0-SNAPSHOT.7 io.spine.tools - spine-mc-java-routing - 2.0.0-SNAPSHOT.306 + validation-java-bundle + 2.0.0-SNAPSHOT.383 - io.spine.validation - spine-validation-java-bundle - 2.0.0-SNAPSHOT.302 + net.sourceforge.pmd + pmd-ant + 7.12.0 net.sourceforge.pmd pmd-java - 6.55.0 + 7.12.0 org.jacoco org.jacoco.agent - 0.8.12 + 0.8.13 org.jacoco org.jacoco.ant - 0.8.12 + 0.8.13 org.jetbrains.dokka - analysis-kotlin-descriptors - 1.9.20 + all-modules-page-plugin + 2.1.0 org.jetbrains.dokka - dokka-base - 1.9.20 + analysis-kotlin-symbols + 2.1.0 org.jetbrains.dokka - dokka-core - 1.9.20 + dokka-base + 2.1.0 org.jetbrains.dokka - gfm-plugin - 1.9.20 + dokka-core + 2.1.0 org.jetbrains.dokka javadoc-plugin - 1.9.20 - - - org.jetbrains.dokka - jekyll-plugin - 1.9.20 + 2.1.0 org.jetbrains.dokka - kotlin-as-java-plugin - 1.9.20 + templating-plugin + 2.1.0 org.jetbrains.kotlin kotlin-build-tools-impl - 2.1.20 + 2.2.21 org.jetbrains.kotlin kotlin-compiler-embeddable - 2.1.20 - - - org.jetbrains.kotlin - kotlin-klib-commonizer-embeddable - 2.1.20 + 2.2.21 org.jetbrains.kotlin kotlin-scripting-compiler-embeddable - 2.1.20 + 2.2.21 From 878e2cd63cfb052e3680236041156aa75f36a3c2 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Mon, 22 Dec 2025 19:30:58 +0000 Subject: [PATCH 4/4] Force Jackson parts --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 94618d2..4ed8383 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -147,9 +147,11 @@ allprojects { Coroutines.forceArtifacts(project, cfg, rs) Jackson.forceArtifacts(project, this@all, this@resolutionStrategy) Jackson.DataType.forceArtifacts(project, this@all, this@resolutionStrategy) + Jackson.DataFormat.forceArtifacts(project, this@all, this@resolutionStrategy) Grpc.forceArtifacts(project, this@all, this@resolutionStrategy) force( Jackson.annotations, + Jackson.bom, Grpc.bom, Kotlin.bom, KotlinPoet.lib,