+
+
\ No newline at end of file
diff --git a/.idea/live-templates/README.md b/.idea/live-templates/README.md
new file mode 100644
index 0000000..66713b3
--- /dev/null
+++ b/.idea/live-templates/README.md
@@ -0,0 +1,27 @@
+### Live Templates
+
+This directory contains two live template groups:
+
+1. `Spine.xml`: shortcuts for the repeated patterns used in the framework.
+2. `User.xml`: a single shortcut to generate TODO comments.
+
+### Instlallation
+
+Live templates are not picked up by IDEA automatically. They should be added manually.
+In order to add these templates, perform the following steps:
+
+1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA
+ [settings folder][settings_folder].
+2. Restart IntelliJ IDEA: `File -> Invalidate Caches -> Just restart`.
+3. Go to `Preferences -> Editor -> Live Templates`.
+4. Verify `User` and `Spine` template groups are present.
+
+[settings_folder]: https://www.jetbrains.com/help/idea/directories-used-by-the-ide-to-store-settings-caches-plugins-and-logs.html#config-directory
+
+### Configuring `User.todo` template
+
+1. Open the corresponding template: `Preferences -> Editor -> Live Templates -> User.todo`.
+2. Click on `Edit variables`.
+3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example,
+ for `jack.sparrow@teamdev.com` use the follwoing expression `"jack.sparrow"`.
+4. Verify that the template generates expected comments: `// TODO:2022-11-03:jack.sparrow: <...>`.
diff --git a/.idea/live-templates/Spine.xml b/.idea/live-templates/Spine.xml
new file mode 100644
index 0000000..369b72d
--- /dev/null
+++ b/.idea/live-templates/Spine.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0a1b5f2
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+developers@spine.io.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2185ef6
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+How to contribute
+==================
+Thank you for wanting to contribute to Spine. The following links will help you get started:
+ * [Wiki home][wiki-home] — the home of the framework developer's documentation.
+ * [Getting started with Spine in Java][quick-start] — this guide will walk you through
+ a minimal client-server “Hello World!” application in Java.
+ * [Introduction][docs-intro] — this section of the Spine Documentation will help you understand
+ the foundation of the framework.
+
+Pull requests
+-------------
+The work on an improvement starts with creating an issue that describes a bug or a feature. The issue will be used for communications on the proposed improvements.
+If code changes are going to be introduced, the issue should also have a link to the corresponding Pull Request.
+
+Code contributions should:
+ * Be accompanied by tests.
+ * Be licensed under the Apache v2.0 license with the appropriate copyright header for each file.
+ * Formatted according to the code style. See [Wiki home][wiki-home] for the links to
+ style guides of the programming languages used in the framework.
+
+Contributor License Agreement
+-----------------------------
+Contributions to the code of Spine Event Engine framework and its libraries must be accompanied by
+Contributor License Agreement (CLA).
+
+ * If you are an individual writing original source code and you're sure you own
+ the intellectual property, then you'll need to sign an individual CLA.
+
+ * If you work for a company which wants you to contribute your work,
+ then an authorized person from your company will need to sign a corporate CLA.
+
+Please [contact us][legal-email] for arranging the paper formalities.
+
+[wiki-home]: https://github.com/SpineEventEngine/SpineEventEngine.github.io/wiki
+[quick-start]: https://spine.io/docs/quick-start
+[docs-intro]: https://spine.io/docs/introduction
+[legal-email]: mailto:legal@teamdev.com
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..b880baa
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright {yyyy} {name of copyright owner}
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/README.md b/README.md
index 28ff23c..2be4398 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,115 @@
-# ProtoTap
-Utilities for tapping `protoc` output
+# ProtoTap — Gradle plugin for tapping Protobuf
+
+[![Ubuntu build][ubuntu-build-badge]][gh-actions]
+[![license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
+
+[gh-actions]: https://github.com/SpineEventEngine/ProtoTap/actions
+[ubuntu-build-badge]: https://github.com/SpineEventEngine/ProtoTap/actions/workflows/build-on-ubuntu.yml/badge.svg
+
+ProtoTap is a Gradle plugin that allows tapping output of Protobuf compiler used in the project to
+which the plugin is applied, and placing them as `test` resources.
+
+This way authors of code generators can focus on writing tests rather than on "plumbing" with
+[Protobuf Plugin for Gradle][protobuf-plugin] or creating a custom diagnostic plugin for
+Protobuf Compiler (`protoc`).
+
+## How it works
+
+ProtoTap interacts with [Protobuf Plugin for Gradle][protobuf-plugin] for copying generated code and
+related files to `test` resources of the project. Files are stored under the directory matching
+the following pattern:
+```
+$projectDir/build/resources/test/prototap/
+```
+
+## Basic features
+
+### Automatic selection of a source set
+When tuning code generation, the plugin attempts to find [`testFixtures`][test-fixtures] source set
+in the project first. If not found, the plugin attempts to find the `test` source set.
+
+Using other source sets requires explicit setting, as described in the [section](#settings) below.
+
+### Copying `protoc` output
+The code generated by `protoc` will appear in corresponding subdirectories of the `prototap`.
+The name of the subdirectory matches the name of the corresponding `protoc` built-in or plugin.
+For example, for `java` built-in it would be:
+
+```
+my-project
+ build
+ resources
+ test
+ prototap
+ java <-- The output of `java` built-in of `protoc`.
+ HelloWorld.java
+```
+After this, the code of your tests will be able to access the generated code as usual program
+resources.
+
+### Obtaining `CodeGeneratorRequest` file
+[`CodeGeneratorRequest`][codegen-request] is passed by `protoc` to its plugins when `.proto` files
+are processed. ProtoTap stores the binary version of the request in the file
+named `CodeGeneratorRequest.binbp` next to the directories with the generated code:
+
+```
+my-project
+ build
+ resources
+ test
+ prototap
+ java
+ CodeGeneratorRequest.binbp <—— The request file.
+```
+
+## Simplest usage
+
+Adding to your project via Kotlin DSL looks like this:
+```kotlin
+plugins {
+ id("io.spine.prototap") version "$version"
+}
+```
+The above snippet assumes that the plugin with the ID `"com.google.protobuf"` is already added at
+some level to your project.
+
+> [!TIP]
+> For the latest ProtoTap version please see [`version.gradle.kts`](version.gradle.kts).
+>
+> The plugin was developed and tested under Gradle 7.6.4.
+
+## Using plugin settings
+You can tune ProtoTap by using the following DSL:
+
+```kotlin
+prototap {
+ artifact.set("com.google.protobuf:protoc:3.25.1")
+ sourceSet.set(functionalTest)
+ generateDescriptorSet.set(true)
+}
+```
+The `artifact` property is a convenience shortcut for specifying the `protoc` artifact in case
+your project does not have explicit `protobuf` block.
+
+The `sourceSet` property is for specifying a source set with the proto files of interest, other
+than `testFixtures` or `test`.
+
+The `generateDescriptorSet` property makes the Protobuf Compiler produce a descriptor set file,
+which ProtoTap places next to `CodeGeneratorRequest`:
+
+```
+my-project
+ build
+ resources
+ test
+ prototap
+ java
+ CodeGeneratorRequest.binbp
+ FileDescriptorSet.binpb <—— The descriptor set file.
+```
+By default descriptor set files are not generated.
+
+[protobuf-plugin]: https://github.com/google/protobuf-gradle-plugin
+[test-fixtures]: https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures
+[codegen-request]: https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/compiler/plugin.proto
+[descriptor-set]: https://github.com/google/protobuf-gradle-plugin#generate-descriptor-set-files
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
new file mode 100644
index 0000000..e69de29
diff --git a/api/src/main/kotlin/io/spine/tools/prototap/Names.kt b/api/src/main/kotlin/io/spine/tools/prototap/Names.kt
new file mode 100644
index 0000000..9795a08
--- /dev/null
+++ b/api/src/main/kotlin/io/spine/tools/prototap/Names.kt
@@ -0,0 +1,79 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+package io.spine.tools.prototap
+
+/**
+ * Names for the resources ProtoTap produces.
+ */
+public object Names {
+
+ /**
+ * The ID of the ProtoTap Gradle Plugin.
+ */
+ public const val GRADLE_PLUGIN_ID: String = "io.spine.prototap"
+
+ /**
+ * The name of a Gradle project extension added by ProtoTap Gradle Plugin.
+ */
+ public const val GRADLE_EXTENSION_NAME: String = "prototap"
+
+ /**
+ * The name ProtoTap uses when passing itself to `protoc` compiler.
+ */
+ public const val PROTOC_PLUGIN_NAME: String = "prototap"
+
+ /**
+ * The name of the `testFixtures` source set used by default for
+ * tapping Protobuf compiler.
+ *
+ * This constant is used if the `sourceSet` property of
+ * the [prototap][GRADLE_EXTENSION_NAME] project extension
+ * added by the ProtoTap Gradle Plugin is not specified.
+ *
+ * If a project does not apply the `java-test-fixtures` Gradle plugin, then
+ * the [test][FALLBACK_SOURCE_SET_NAME] source set conventionally will be used.
+ */
+ public const val DEFAULT_SOURCE_SET_NAME: String = "testFixtures"
+
+ /**
+ * The name of the `test` source set which is used for tapping Protobuf compiler, if
+ * the project does not have the [testFixtures][DEFAULT_SOURCE_SET_NAME] source set.
+ *
+ * If the project does not have the `test` source set either, a build time error will occur.
+ */
+ public const val FALLBACK_SOURCE_SET_NAME: String = "test"
+
+ /**
+ * The classifier used for the executable fat JAR of the ProtoTap `protoc` plugin archive.
+ */
+ public const val PROTOC_PLUGIN_CLASSIFIER: String = "exe"
+
+ /**
+ * The name of the `java-test-fixtures` plugin.
+ */
+ public const val TEST_FIXTURES_PLUGIN_NAME: String = "java-test-fixtures"
+}
diff --git a/api/src/main/kotlin/io/spine/tools/prototap/Paths.kt b/api/src/main/kotlin/io/spine/tools/prototap/Paths.kt
new file mode 100644
index 0000000..bcb8e60
--- /dev/null
+++ b/api/src/main/kotlin/io/spine/tools/prototap/Paths.kt
@@ -0,0 +1,83 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+package io.spine.tools.prototap
+
+import io.spine.tools.prototap.Names.PROTOC_PLUGIN_NAME
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.io.path.pathString
+
+/**
+ * Constants and utility functions for calculating directory and file names
+ * used by ProtoTap Gradle and `protoc` plugins.
+ *
+ * This object may also be useful for users of ProtoTap to obtain its output.
+ */
+public object Paths {
+
+ /**
+ * The name of the subdirectory for collecting files.
+ */
+ public const val TARGET_DIR: String = PROTOC_PLUGIN_NAME
+
+ /**
+ * The name of the file containing `CodeGeneratorRequest` message obtained
+ * by the ProtoTap `protoc` plugins.
+ */
+ public const val CODE_GENERATOR_REQUEST_FILE: String = "CodeGeneratorRequest.binpb"
+
+ /**
+ * The name of the descriptor set file obtained by the ProtoTap Gradle plugin.
+ */
+ public const val DESCRIPTOR_SET_FILE: String = "FileDescriptorSet.binpb"
+
+ /**
+ * Obtains the path to the intermediate directory for storing some of the intercepted
+ * files before they are copied to the [outputRoot] directory.
+ */
+ public fun interimDir(buildDir: String): Path =
+ Paths.get("$buildDir/$TARGET_DIR")
+
+ /**
+ * Obtains the path to the root directory into which ProtoTap puts all the intercepted files.
+ *
+ * By convention, it is a subdirectory named [TARGET_DIR] placed under
+ * `$buildDir/resources/test` of a Gradle project.
+ */
+ public fun outputRoot(buildDir: String): Path =
+ Paths.get("$buildDir/resources/test/$TARGET_DIR")
+
+ /**
+ * Obtains a full path to the file with given [shortFileName] under
+ * the given [buildDir] of a Gradle project.
+ *
+ * By convention, the file is placed under the [TARGET_DIR] directory,
+ * which is created under `$buildDir/resources/test/`.
+ */
+ public fun outputFile(buildDir: String, shortFileName: String): String =
+ outputRoot(buildDir).resolve(shortFileName).pathString
+}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..818ba86
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("RemoveRedundantQualifierName") // To prevent IDEA replacing FQN imports.
+
+import io.spine.internal.gradle.publish.PublishingRepos
+import io.spine.internal.gradle.publish.spinePublishing
+import io.spine.internal.gradle.report.coverage.JacocoConfig
+import io.spine.internal.gradle.report.license.LicenseReporter
+import io.spine.internal.gradle.report.pom.PomGenerator
+import io.spine.internal.gradle.standardToSpineSdk
+
+buildscript {
+ standardSpineSdkRepositories()
+ doForceVersions(configurations)
+}
+
+plugins {
+ idea
+ jacoco
+ `project-report`
+ `gradle-doctor`
+}
+JacocoConfig.applyTo(project)
+PomGenerator.applyTo(project)
+LicenseReporter.mergeAllReports(project)
+
+spinePublishing {
+ modules = setOf(
+ "api",
+ "protoc-plugin",
+ )
+ modulesWithCustomPublishing = setOf(
+ "gradle-plugin"
+ )
+ destinations = with(PublishingRepos) {
+ setOf(
+ cloudArtifactRegistry,
+ gitHub("ProtoTap")
+ )
+ }
+ artifactPrefix = "prototap-"
+}
+
+allprojects {
+ apply(from = "$rootDir/version.gradle.kts")
+ group = "io.spine.tools"
+ version = extra["versionToPublish"]!!
+
+ repositories.standardToSpineSdk()
+}
+
+subprojects {
+ apply(plugin = "module")
+}
diff --git a/buildSrc/aus.weis b/buildSrc/aus.weis
new file mode 100644
index 0000000..fa72e08
Binary files /dev/null and b/buildSrc/aus.weis differ
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..d5e76bf
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,242 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+/**
+ * This script uses two declarations of the constant [licenseReportVersion] because
+ * currently there is no way to define a constant _before_ a build script of `buildSrc`.
+ * We cannot use imports or do something else before the `buildscript` or `plugin` clauses.
+ */
+
+plugins {
+ java
+ groovy
+ `kotlin-dsl`
+
+ // https://github.com/jk1/Gradle-License-Report/releases
+ id("com.github.jk1.dependency-license-report").version("2.1")
+
+ // https://github.com/johnrengelman/shadow/releases
+ id("com.github.johnrengelman.shadow").version("7.1.2")
+}
+
+repositories {
+ mavenLocal()
+ gradlePluginPortal()
+ mavenCentral()
+}
+
+/**
+ * The version of Jackson used by `buildSrc`.
+ *
+ * Please keep this value in sync. with `io.spine.internal.dependency.Jackson.version`.
+ * It's not a requirement, but would be good in terms of consistency.
+ */
+val jacksonVersion = "2.15.3"
+
+/**
+ * The version of Google Artifact Registry used by `buildSrc`.
+ *
+ * The version `2.1.5` is the latest before `2.2.0`, which introduces breaking changes.
+ *
+ * @see
+ * Google Artifact Registry at Maven
+ */
+val googleAuthToolVersion = "2.1.5"
+
+val licenseReportVersion = "2.1"
+
+val grGitVersion = "4.1.1"
+
+/**
+ * The version of the Kotlin Gradle plugin and Kotlin binaries used by the build process.
+ *
+ * This version may change from the [version of Kotlin][io.spine.internal.dependency.Kotlin.version]
+ * used by the project.
+ */
+val kotlinVersion = "1.8.22"
+
+/**
+ * The version of Guava used in `buildSrc`.
+ *
+ * Always use the same version as the one specified in [io.spine.internal.dependency.Guava].
+ * Otherwise, when testing Gradle plugins, clashes may occur.
+ */
+val guavaVersion = "32.1.3-jre"
+
+/**
+ * The version of ErrorProne Gradle plugin.
+ *
+ * Please keep in sync. with [io.spine.internal.dependency.ErrorProne.GradlePlugin.version].
+ *
+ * @see
+ * Error Prone Gradle Plugin Releases
+ */
+val errorPronePluginVersion = "3.1.0"
+
+/**
+ * The version of Protobuf Gradle Plugin.
+ *
+ * Please keep in sync. with [io.spine.internal.dependency.Protobuf.GradlePlugin.version].
+ *
+ * @see
+ * Protobuf Gradle Plugins Releases
+ */
+val protobufPluginVersion = "0.9.4"
+
+/**
+ * The version of Dokka Gradle Plugins.
+ *
+ * Please keep in sync with [io.spine.internal.dependency.Dokka.version].
+ *
+ * @see
+ * Dokka Releases
+ */
+val dokkaVersion = "1.9.10"
+
+/**
+ * The version of Detekt Gradle Plugin.
+ *
+ * @see Detekt Releases
+ */
+val detektVersion = "1.23.0"
+
+/**
+ * @see [io.spine.internal.dependency.Kotest]
+ */
+val kotestJvmPluginVersion = "0.4.10"
+
+/**
+ * @see [io.spine.internal.dependency.Kover]
+ */
+val koverVersion = "0.7.2"
+
+/**
+ * The version of the Shadow Plugin.
+ *
+ * `7.1.2` is the last version compatible with Gradle 7.x. Newer versions require Gradle v8.x.
+ *
+ * @see Shadow Plugin releases
+ */
+val shadowVersion = "7.1.2"
+
+configurations.all {
+ resolutionStrategy {
+ force(
+ "com.google.guava:guava:${guavaVersion}",
+ "com.google.protobuf:protobuf-gradle-plugin:$protobufPluginVersion",
+
+ // Force Kotlin lib versions avoiding using those bundled with Gradle.
+ "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion",
+ "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion",
+ "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
+ )
+ }
+}
+
+val jvmVersion = JavaLanguageVersion.of(11)
+
+java {
+ toolchain.languageVersion.set(jvmVersion)
+}
+
+tasks.withType {
+ kotlinOptions {
+ jvmTarget = jvmVersion.toString()
+ }
+}
+
+dependencies {
+ api("com.github.jk1:gradle-license-report:$licenseReportVersion")
+ dependOnAuthCommon()
+
+ listOf(
+ "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion",
+ "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion",
+ "com.github.jk1:gradle-license-report:$licenseReportVersion",
+ "com.google.guava:guava:$guavaVersion",
+ "com.google.protobuf:protobuf-gradle-plugin:$protobufPluginVersion",
+ "gradle.plugin.com.github.johnrengelman:shadow:${shadowVersion}",
+ "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion",
+ "io.kotest:kotest-gradle-plugin:$kotestJvmPluginVersion",
+ // https://github.com/srikanth-lingala/zip4j
+ "net.lingala.zip4j:zip4j:2.10.0",
+ "net.ltgt.gradle:gradle-errorprone-plugin:${errorPronePluginVersion}",
+ "org.ajoberstar.grgit:grgit-core:${grGitVersion}",
+ "org.jetbrains.dokka:dokka-base:${dokkaVersion}",
+ "org.jetbrains.dokka:dokka-gradle-plugin:${dokkaVersion}",
+ "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion",
+ "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion",
+ "org.jetbrains.kotlinx:kover-gradle-plugin:$koverVersion"
+ ).forEach {
+ implementation(it)
+ }
+}
+
+dependOnBuildSrcJar()
+
+/**
+ * Adds a dependency on a `buildSrc.jar`, iff:
+ * 1) the `src` folder is missing, and
+ * 2) `buildSrc.jar` is present in `buildSrc/` folder instead.
+ *
+ * This approach is used in the scope of integration testing.
+ */
+fun Project.dependOnBuildSrcJar() {
+ val srcFolder = this.rootDir.resolve("src")
+ val buildSrcJar = rootDir.resolve("buildSrc.jar")
+ if (!srcFolder.exists() && buildSrcJar.exists()) {
+ logger.info("Adding the pre-compiled 'buildSrc.jar' to 'implementation' dependencies.")
+ dependencies {
+ implementation(files("buildSrc.jar"))
+ }
+ }
+}
+
+/**
+ * Includes the `implementation` dependency on `artifactregistry-auth-common`,
+ * with the version defined in [googleAuthToolVersion].
+ *
+ * `artifactregistry-auth-common` has transitive dependency on Gson and Apache `commons-codec`.
+ * Gson from version `2.8.6` until `2.8.9` is vulnerable to Deserialization of Untrusted Data
+ * (https://devhub.checkmarx.com/cve-details/CVE-2022-25647/).
+ *
+ * Apache `commons-codec` before 1.13 is vulnerable to information exposure
+ * (https://devhub.checkmarx.com/cve-details/Cxeb68d52e-5509/).
+ *
+ * We use Gson `2.10.1` and we force it in `forceProductionDependencies()`.
+ * We use `commons-code` with version `1.16.0`, forcing it in `forceProductionDependencies()`.
+ *
+ * So, we should be safe with the current version `artifactregistry-auth-common` until
+ * we migrate to a later version.
+ */
+fun DependencyHandlerScope.dependOnAuthCommon() {
+ @Suppress("VulnerableLibrariesLocal", "RedundantSuppression")
+ implementation(
+ "com.google.cloud.artifactregistry:artifactregistry-auth-common:$googleAuthToolVersion"
+ ) {
+ exclude(group = "com.google.guava")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/BuildExtensions.kt b/buildSrc/src/main/kotlin/BuildExtensions.kt
new file mode 100644
index 0000000..9e51bca
--- /dev/null
+++ b/buildSrc/src/main/kotlin/BuildExtensions.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("UnusedReceiverParameter", "unused", "TopLevelPropertyNaming", "ObjectPropertyName")
+
+import io.spine.internal.dependency.ErrorProne
+import io.spine.internal.dependency.GradleDoctor
+import io.spine.internal.dependency.Kotest
+import io.spine.internal.dependency.Kover
+import io.spine.internal.dependency.ProtoData
+import io.spine.internal.dependency.Protobuf
+import io.spine.internal.dependency.Spine
+import io.spine.internal.gradle.standardToSpineSdk
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.kotlin.dsl.ScriptHandlerScope
+import org.gradle.plugin.use.PluginDependenciesSpec
+import org.gradle.plugin.use.PluginDependencySpec
+
+/**
+ * Applies [standard][standardToSpineSdk] repositories to this `buildscript`.
+ */
+fun ScriptHandlerScope.standardSpineSdkRepositories() {
+ repositories.standardToSpineSdk()
+}
+
+/**
+ * Provides shortcuts to reference our dependency objects.
+ *
+ * Dependency objects cannot be used under `plugins` section because `io` is a value
+ * declared in auto-generated `org.gradle.kotlin.dsl.PluginAccessors.kt` file.
+ * It conflicts with our own declarations.
+ *
+ * In such cases, a shortcut to apply a plugin can be created:
+ *
+ * ```
+ * val PluginDependenciesSpec.`gradle-doctor`: PluginDependencySpec
+ * get() = id(GradleDoctor.pluginId).version(GradleDoctor.version)
+ * ```
+ *
+ * But for some plugins, it's impossible to apply them directly to a project.
+ * For example, when a plugin is not published to Gradle Portal, it can only be
+ * applied with buildscript's classpath. Thus, it's needed to leave some freedom
+ * upon how to apply them. In such cases, just a shortcut to a dependency object
+ * can be declared, without applying of the plugin in-place.
+ */
+private const val ABOUT_DEPENDENCY_EXTENSIONS = ""
+
+/**
+ * Shortcut to [Spine.McJava] dependency object.
+ *
+ * This plugin is not published to Gradle Portal and cannot be applied directly to a project.
+ * Firstly, it should be put to buildscript's classpath and then applied by ID only.
+ */
+val PluginDependenciesSpec.mcJava: Spine.McJava
+ get() = Spine.McJava
+
+/**
+ * Shortcut to [ProtoData] dependency object.
+ *
+ * This plugin is in Gradle Portal. But when used in pair with [mcJava], it cannot be applied
+ * directly to a project. It is so, because [mcJava] uses [protoData] as its dependency.
+ * And buildscript's classpath ends up with both of them.
+ */
+val PluginDependenciesSpec.protoData: ProtoData
+ get() = ProtoData
+
+/**
+ * Provides shortcuts for applying plugins from our dependency objects.
+ *
+ * Dependency objects cannot be used under `plugins` section because `io` is a value
+ * declared in auto-generated `org.gradle.kotlin.dsl.PluginAccessors.kt` file.
+ * It conflicts with our own declarations.
+ *
+ * Declaring of top-level shortcuts eliminates need in applying plugins
+ * using fully-qualified name of dependency objects.
+ *
+ * It is still possible to apply a plugin with a custom version, if needed.
+ * Just declare a version again on the returned [PluginDependencySpec].
+ *
+ * For example:
+ *
+ * ```
+ * plugins {
+ * protobuf version("0.8.19-custom")
+ * }
+ * ```
+ */
+private const val ABOUT_PLUGIN_ACCESSORS = ""
+
+val PluginDependenciesSpec.errorprone: PluginDependencySpec
+ get() = id(ErrorProne.GradlePlugin.id)
+
+val PluginDependenciesSpec.protobuf: PluginDependencySpec
+ get() = id(Protobuf.GradlePlugin.id)
+
+val PluginDependenciesSpec.`gradle-doctor`: PluginDependencySpec
+ get() = id(GradleDoctor.pluginId).version(GradleDoctor.version)
+
+val PluginDependenciesSpec.kotest: PluginDependencySpec
+ get() = Kotest.MultiplatformGradlePlugin.let {
+ return id(it.id).version(it.version)
+ }
+
+val PluginDependenciesSpec.kover: PluginDependencySpec
+ get() = id(Kover.id).version(Kover.version)
+
+/**
+ * Configures the dependencies between third-party Gradle tasks
+ * and those defined via ProtoData and Spine Model Compiler.
+ *
+ * It is required in order to avoid warnings in build logs, detecting the undeclared
+ * usage of Spine-specific task output by other tasks,
+ * e.g. the output of `launchProtoData` is used by `compileKotlin`.
+ */
+@Suppress("unused")
+fun Project.configureTaskDependencies() {
+
+ /**
+ * Creates a dependency between the Gradle task of *this* name
+ * onto the task with `taskName`.
+ *
+ * If either of tasks does not exist in the enclosing `Project`,
+ * this method does nothing.
+ *
+ * This extension is kept local to `configureTaskDependencies` extension
+ * to prevent its direct usage from outside.
+ */
+ fun String.dependOn(taskName: String) {
+ val whoDepends = this
+ val dependOntoTask: Task? = tasks.findByName(taskName)
+ dependOntoTask?.let {
+ tasks.findByName(whoDepends)?.dependsOn(it)
+ }
+ }
+
+ afterEvaluate {
+ val launchProtoData = "launchProtoData"
+ val launchTestProtoData = "launchTestProtoData"
+ val generateProto = "generateProto"
+ val createVersionFile = "createVersionFile"
+ "compileKotlin".dependOn(launchProtoData)
+ "compileTestKotlin".dependOn(launchTestProtoData)
+ val sourcesJar = "sourcesJar"
+ sourcesJar.dependOn(generateProto)
+ sourcesJar.dependOn(launchProtoData)
+ sourcesJar.dependOn(createVersionFile)
+ sourcesJar.dependOn("prepareProtocConfigVersions")
+ val dokkaHtml = "dokkaHtml"
+ dokkaHtml.dependOn(generateProto)
+ dokkaHtml.dependOn(launchProtoData)
+ "dokkaJavadoc".dependOn(launchProtoData)
+ "publishPluginJar".dependOn(createVersionFile)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/BuildSettings.kt b/buildSrc/src/main/kotlin/BuildSettings.kt
new file mode 100644
index 0000000..1adbb92
--- /dev/null
+++ b/buildSrc/src/main/kotlin/BuildSettings.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+
+/**
+ * This object provides high-level constants, like version of JVM, to be used
+ * throughout the project.
+ */
+object BuildSettings {
+ private const val JVM_VERSION = 11
+ val javaVersion: JavaLanguageVersion = JavaLanguageVersion.of(JVM_VERSION)
+}
diff --git a/buildSrc/src/main/kotlin/DependencyResolution.kt b/buildSrc/src/main/kotlin/DependencyResolution.kt
new file mode 100644
index 0000000..aa2eb71
--- /dev/null
+++ b/buildSrc/src/main/kotlin/DependencyResolution.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import io.spine.internal.dependency.AnimalSniffer
+import io.spine.internal.dependency.Asm
+import io.spine.internal.dependency.AutoCommon
+import io.spine.internal.dependency.AutoService
+import io.spine.internal.dependency.AutoValue
+import io.spine.internal.dependency.CheckerFramework
+import io.spine.internal.dependency.CommonsCli
+import io.spine.internal.dependency.CommonsCodec
+import io.spine.internal.dependency.CommonsLogging
+import io.spine.internal.dependency.Dokka
+import io.spine.internal.dependency.ErrorProne
+import io.spine.internal.dependency.FindBugs
+import io.spine.internal.dependency.Gson
+import io.spine.internal.dependency.Guava
+import io.spine.internal.dependency.Hamcrest
+import io.spine.internal.dependency.J2ObjC
+import io.spine.internal.dependency.JUnit
+import io.spine.internal.dependency.Jackson
+import io.spine.internal.dependency.JavaDiffUtils
+import io.spine.internal.dependency.Kotest
+import io.spine.internal.dependency.Kotlin
+import io.spine.internal.dependency.Okio
+import io.spine.internal.dependency.OpenTest4J
+import io.spine.internal.dependency.Plexus
+import io.spine.internal.dependency.Protobuf
+import io.spine.internal.dependency.Slf4J
+import io.spine.internal.dependency.Truth
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.ConfigurationContainer
+import org.gradle.api.artifacts.ResolutionStrategy
+
+/**
+ * The function to be used in `buildscript` when a fully-qualified call must be made.
+ */
+@Suppress("unused")
+fun doForceVersions(configurations: ConfigurationContainer) {
+ configurations.forceVersions()
+}
+
+/**
+ * Forces dependencies used in the project.
+ */
+fun NamedDomainObjectContainer.forceVersions() {
+ all {
+ resolutionStrategy {
+ failOnVersionConflict()
+ cacheChangingModulesFor(0, "seconds")
+ forceProductionDependencies()
+ forceTestDependencies()
+ forceTransitiveDependencies()
+ }
+ }
+}
+
+private fun ResolutionStrategy.forceProductionDependencies() {
+ @Suppress("DEPRECATION") // Force versions of SLF4J and Kotlin libs.
+ force(
+ AnimalSniffer.lib,
+ AutoCommon.lib,
+ AutoService.annotations,
+ CheckerFramework.annotations,
+ Dokka.BasePlugin.lib,
+ ErrorProne.annotations,
+ ErrorProne.core,
+ FindBugs.annotations,
+ Gson.lib,
+ Guava.lib,
+ Kotlin.reflect,
+ Kotlin.stdLib,
+ Kotlin.stdLibCommon,
+ Kotlin.stdLibJdk7,
+ Kotlin.stdLibJdk8,
+ Protobuf.GradlePlugin.lib,
+ Protobuf.libs,
+ Slf4J.lib
+ )
+}
+
+private fun ResolutionStrategy.forceTestDependencies() {
+ force(
+ Guava.testLib,
+ JUnit.api,
+ JUnit.bom,
+ JUnit.Platform.commons,
+ JUnit.Platform.launcher,
+ JUnit.legacy,
+ Truth.libs,
+ Kotest.assertions,
+ )
+}
+
+/**
+ * Forces transitive dependencies of 3rd party components that we don't use directly.
+ */
+private fun ResolutionStrategy.forceTransitiveDependencies() {
+ force(
+ Asm.lib,
+ AutoValue.annotations,
+ CommonsCli.lib,
+ CommonsCodec.lib,
+ CommonsLogging.lib,
+ Gson.lib,
+ Hamcrest.core,
+ J2ObjC.annotations,
+ JUnit.Platform.engine,
+ JUnit.Platform.suiteApi,
+ JUnit.runner,
+ Jackson.annotations,
+ Jackson.bom,
+ Jackson.core,
+ Jackson.databind,
+ Jackson.dataformatXml,
+ Jackson.dataformatYaml,
+ Jackson.moduleKotlin,
+ JavaDiffUtils.lib,
+ Kotlin.jetbrainsAnnotations,
+ Okio.lib,
+ OpenTest4J.lib,
+ Plexus.utils,
+ )
+}
+
+@Suppress("unused")
+fun NamedDomainObjectContainer.excludeProtobufLite() {
+
+ fun excludeProtoLite(configurationName: String) {
+ named(configurationName).get().exclude(
+ mapOf(
+ "group" to "com.google.protobuf",
+ "module" to "protobuf-lite"
+ )
+ )
+ }
+
+ excludeProtoLite("runtimeOnly")
+ excludeProtoLite("testRuntimeOnly")
+}
diff --git a/buildSrc/src/main/kotlin/DokkaExts.kt b/buildSrc/src/main/kotlin/DokkaExts.kt
new file mode 100644
index 0000000..feb9eb0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/DokkaExts.kt
@@ -0,0 +1,216 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+import io.spine.internal.dependency.Dokka
+import io.spine.internal.gradle.publish.getOrCreate
+import java.io.File
+import java.time.LocalDate
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.dsl.DependencyHandler
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.configurationcache.extensions.capitalized
+import org.gradle.kotlin.dsl.DependencyHandlerScope
+import org.jetbrains.dokka.DokkaConfiguration
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import org.jetbrains.dokka.gradle.DokkaTask
+import org.jetbrains.dokka.gradle.GradleDokkaSourceSetBuilder
+
+/**
+ * To generate the documentation as seen from Java perspective, the `kotlin-as-java`
+ * plugin was added to the Dokka classpath.
+ *
+ * @see
+ * Dokka output formats
+ */
+fun DependencyHandlerScope.useDokkaForKotlinAsJava() {
+ dokkaPlugin(Dokka.KotlinAsJavaPlugin.lib)
+}
+
+/**
+ * To exclude pieces of code annotated with `@Internal` from the documentation
+ * a custom plugin is added to the Dokka's classpath.
+ *
+ * @see
+ * Custom Dokka Plugins
+ */
+fun DependencyHandlerScope.useDokkaWithSpineExtensions() {
+ dokkaPlugin(Dokka.SpineExtensions.lib)
+}
+
+private fun DependencyHandler.dokkaPlugin(dependencyNotation: Any): Dependency? =
+ add("dokkaPlugin", dependencyNotation)
+
+private fun Project.dokkaOutput(language: String): File =
+ buildDir.resolve("docs/dokka${language.capitalized()}")
+
+private fun Project.dokkaConfigFile(file: String): File {
+ val dokkaConfDir = project.rootDir.resolve("buildSrc/src/main/resources/dokka")
+ return dokkaConfDir.resolve(file)
+}
+
+private fun DokkaTask.configureFor(language: String) {
+ dokkaSourceSets.configureEach {
+ /**
+ * Configures links to the external Java documentation.
+ */
+ jdkVersion.set(BuildSettings.javaVersion.asInt())
+
+ skipEmptyPackages.set(true)
+
+ documentedVisibilities.set(
+ setOf(
+ DokkaConfiguration.Visibility.PUBLIC,
+ DokkaConfiguration.Visibility.PROTECTED
+ )
+ )
+ }
+
+ outputDirectory.set(project.dokkaOutput(language))
+
+ /**
+ * Dokka Base plugin allows to set a few properties to customize the output:
+ *
+ * - `customStyleSheets` property to which CSS files are passed overriding
+ * styles generated by Dokka;
+ * - `customAssets` property to provide resources. The image with the name
+ * "logo-icon.svg" is passed to override the default logo used by Dokka;
+ * - `separateInheritedMembers` when set to `true`, creates a separate tab in
+ * type-documentation for inherited members.
+ *
+ * @see
+ * Dokka modifying frontend assets
+ */
+ pluginConfiguration {
+ customStyleSheets = listOf(project.dokkaConfigFile("styles/custom-styles.css"))
+ customAssets = listOf(project.dokkaConfigFile("assets/logo-icon.svg"))
+ separateInheritedMembers = true
+ footerMessage = "Copyright ${LocalDate.now().year}, TeamDev"
+ }
+}
+
+/**
+ * Configures this [DokkaTask] to accept only Kotlin files.
+ */
+fun DokkaTask.configureForKotlin() {
+ configureFor("kotlin")
+}
+
+/**
+ * Configures this [DokkaTask] to accept only Java files.
+ */
+fun DokkaTask.configureForJava() {
+ configureFor("java")
+}
+
+/**
+ * Finds the `dokkaHtml` Gradle task.
+ */
+fun TaskContainer.dokkaHtmlTask(): DokkaTask? = this.findByName("dokkaHtml") as DokkaTask?
+
+/**
+ * Returns only Java source roots out of all present in the source set.
+ *
+ * It is a helper method for generating documentation by Dokka only for Java code.
+ * It is helpful when both Java and Kotlin source files are present in a source set.
+ * Dokka can properly generate documentation for either Kotlin or Java depending on
+ * the configuration, but not both.
+ */
+@Suppress("unused")
+internal fun GradleDokkaSourceSetBuilder.onlyJavaSources(): FileCollection {
+ return sourceRoots.filter(File::isJavaSourceDirectory)
+}
+
+private fun File.isJavaSourceDirectory(): Boolean {
+ return isDirectory && name == "java"
+}
+
+/**
+ * Locates or creates `dokkaKotlinJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains the Dokka output, generated upon
+ * Kotlin sources from `main` source set. Requires Dokka to be configured in the target project by
+ * applying `dokka-for-kotlin` plugin.
+ */
+fun Project.dokkaKotlinJar(): TaskProvider = tasks.getOrCreate("dokkaKotlinJar") {
+ archiveClassifier.set("dokka")
+ from(files(dokkaOutput("kotlin")))
+
+ tasks.dokkaHtmlTask()?.let{ dokkaTask ->
+ this@getOrCreate.dependsOn(dokkaTask)
+ }
+}
+
+/**
+ * Tells if this task belongs to the execution graph which contains publishing tasks.
+ *
+ * The task `"publishToMavenLocal"` is excluded from the check because it is a part of
+ * the local testing workflow.
+ */
+fun DokkaTask.isInPublishingGraph(): Boolean =
+ project.gradle.taskGraph.allTasks.any {
+ with(it.name) {
+ startsWith("publish") && !startsWith("publishToMavenLocal")
+ }
+ }
+
+/**
+ * Locates or creates `dokkaJavaJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains the Dokka output, generated upon
+ * Kotlin sources from `main` source set. Requires Dokka to be configured in the target project by
+ * applying `dokka-for-java` and/or `dokka-for-kotlin` script plugin.
+ */
+fun Project.dokkaJavaJar(): TaskProvider = tasks.getOrCreate("dokkaJavaJar") {
+ archiveClassifier.set("dokka-java")
+ from(files(dokkaOutput("java")))
+
+ tasks.dokkaHtmlTask()?.let{ dokkaTask ->
+ this@getOrCreate.dependsOn(dokkaTask)
+ }
+}
+
+/**
+ * Disables Dokka and Javadoc tasks in this `Project`.
+ *
+ * This function could be useful to improve build speed when building subprojects containing
+ * test environments or integration test projects.
+ */
+@Suppress("unused")
+fun Project.disableDocumentationTasks() {
+ gradle.taskGraph.whenReady {
+ tasks.forEach { task ->
+ val lowercaseName = task.name.toLowerCase()
+ if (lowercaseName.contains("dokka") || lowercaseName.contains("javadoc")) {
+ task.enabled = false
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/compile-protobuf.gradle.kts b/buildSrc/src/main/kotlin/compile-protobuf.gradle.kts
new file mode 100644
index 0000000..baffe25
--- /dev/null
+++ b/buildSrc/src/main/kotlin/compile-protobuf.gradle.kts
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import io.spine.internal.dependency.Protobuf
+import io.spine.internal.gradle.protobuf.setup
+
+plugins {
+ id("java-library")
+ id("com.google.protobuf")
+}
+
+
+// For generating test fixtures. See `src/test/proto`.
+protobuf {
+ configurations.excludeProtobufLite()
+ protoc {
+ artifact = Protobuf.compiler
+ }
+ generateProtoTasks.all().configureEach {
+ setup()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/config-tester.gradle.kts b/buildSrc/src/main/kotlin/config-tester.gradle.kts
new file mode 100644
index 0000000..14a236c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/config-tester.gradle.kts
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import io.spine.internal.gradle.ConfigTester
+import io.spine.internal.gradle.SpineRepos
+import io.spine.internal.gradle.cleanFolder
+import java.nio.file.Path
+import java.nio.file.Paths
+
+// A reference to `config` to use along with the `ConfigTester`.
+val config: Path = Paths.get("./")
+
+// A temp folder to use to check out the sources of other repositories with the `ConfigTester`.
+val tempFolder = File("./tmp")
+
+// Creates a Gradle task which checks out and builds the selected Spine repositories
+// with the local version of `config` and `config/buildSrc`.
+ConfigTester(config, tasks, tempFolder)
+ .addRepo(SpineRepos.baseTypes) // Builds `base-types` at `master`.
+ .addRepo(SpineRepos.base) // Builds `base` at `master`.
+ .addRepo(SpineRepos.coreJava) // Builds `core-java` at `master`.
+
+ // This is how one builds a specific branch of some repository:
+ // .addRepo(SpineRepos.coreJava, Branch("grpc-concurrency-fixes"))
+
+ // Register the produced task under the selected name to invoke manually upon need.
+ .registerUnder("buildDependants")
+
+// Cleans the temp folder used to check out the sources from Git.
+tasks.register("clean") {
+ doLast {
+ cleanFolder(tempFolder)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts b/buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts
new file mode 100644
index 0000000..89151fb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/detekt-code-analysis.gradle.kts
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+import io.gitlab.arturbosch.detekt.Detekt
+
+/**
+ * This script-plugin sets up Kotlin code analyzing with Detekt.
+ *
+ * After applying, Detekt is configured to use `${rootDir}/config/quality/detekt-config.yml` file.
+ * Projects can append their own config files to override some parts of the default one or drop
+ * it at all in a favor of their own one.
+ *
+ * An example of appending a custom config file to the default one:
+ *
+ * ```
+ * detekt {
+ * config.from("config/detekt-custom-config.yml")
+ * }
+ * ```
+ *
+ * To totally substitute it, just overwrite the corresponding property:
+ *
+ * ```
+ * detekt {
+ * config = files("config/detekt-custom-config.yml")
+ * }
+ * ```
+ *
+ * Also, it's possible to suppress Detekt findings using [baseline](https://detekt.dev/docs/introduction/baseline/)
+ * file instead of suppressions in source code.
+ *
+ * An example of passing a baseline file:
+ *
+ * ```
+ * detekt {
+ * baseline = file("config/detekt-baseline.yml")
+ * }
+ * ```
+ */
+@Suppress("unused")
+private val about = ""
+
+plugins {
+ id("io.gitlab.arturbosch.detekt")
+}
+
+detekt {
+ buildUponDefaultConfig = true
+ config.from(files("${rootDir}/config/quality/detekt-config.yml"))
+}
+
+tasks {
+ withType().configureEach {
+ reports {
+ html.required.set(true) // Only HTML report is generated.
+ xml.required.set(false)
+ txt.required.set(false)
+ sarif.required.set(false)
+ md.required.set(false)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/dokka-for-java.gradle.kts b/buildSrc/src/main/kotlin/dokka-for-java.gradle.kts
new file mode 100644
index 0000000..49ac6d3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/dokka-for-java.gradle.kts
@@ -0,0 +1,43 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+import org.jetbrains.dokka.gradle.DokkaTask
+
+plugins {
+ id("org.jetbrains.dokka") // Cannot use `Dokka` dependency object here yet.
+}
+
+dependencies {
+ useDokkaForKotlinAsJava()
+ useDokkaWithSpineExtensions()
+}
+
+tasks.withType().configureEach {
+ configureForJava()
+ onlyIf {
+ (it as DokkaTask).isInPublishingGraph()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/dokka-for-kotlin.gradle.kts b/buildSrc/src/main/kotlin/dokka-for-kotlin.gradle.kts
new file mode 100644
index 0000000..9ed226e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/dokka-for-kotlin.gradle.kts
@@ -0,0 +1,42 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+import org.jetbrains.dokka.gradle.DokkaTask
+
+plugins {
+ id("org.jetbrains.dokka") // Cannot use `Dokka` dependency object here yet.
+}
+
+dependencies {
+ useDokkaWithSpineExtensions()
+}
+
+tasks.withType().configureEach {
+ configureForKotlin()
+ onlyIf {
+ (it as DokkaTask).isInPublishingGraph()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt
new file mode 100644
index 0000000..09b2403
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://www.mojohaus.org/animal-sniffer/animal-sniffer-maven-plugin/
+@Suppress("unused", "ConstPropertyName")
+object AnimalSniffer {
+ private const val version = "1.21"
+ const val lib = "org.codehaus.mojo:animal-sniffer-annotations:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/ApacheHttp.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ApacheHttp.kt
new file mode 100644
index 0000000..1eff6a0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ApacheHttp.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused", "ConstPropertyName")
+object ApacheHttp {
+
+ // https://hc.apache.org/downloads.cgi
+ const val core = "org.apache.httpcomponents:httpcore:4.4.14"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AppEngine.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AppEngine.kt
new file mode 100644
index 0000000..be15a8c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AppEngine.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://cloud.google.com/java/docs/reference
+@Suppress("unused", "ConstPropertyName")
+object AppEngine {
+ private const val version = "1.9.82"
+ const val sdk = "com.google.appengine:appengine-api-1.0-sdk:${version}"
+
+ object GradlePlugin {
+ private const val version = "2.2.0"
+ const val lib = "com.google.cloud.tools:appengine-gradle-plugin:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Asm.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Asm.kt
new file mode 100644
index 0000000..82550ef
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Asm.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://asm.ow2.io/
+@Suppress("unused", "ConstPropertyName")
+object Asm {
+ private const val version = "9.2"
+ const val lib = "org.ow2.asm:asm:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt
new file mode 100644
index 0000000..bfc176e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Assertion library for tests in Kotlin
+ *
+ * [AssertK](https://github.com/willowtreeapps/assertk)
+ */
+@Deprecated("Please use Kotest assertions instead.")
+@Suppress("unused", "ConstPropertyName")
+object AssertK {
+ private const val version = "0.26.1"
+ const val libJvm = "com.willowtreeapps.assertk:assertk-jvm:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Auto.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Auto.kt
new file mode 100644
index 0000000..2167439
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Auto.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("unused", "ConstPropertyName")
+
+package io.spine.internal.dependency
+
+// https://github.com/google/auto
+object AutoCommon {
+ private const val version = "1.2.2"
+ const val lib = "com.google.auto:auto-common:${version}"
+}
+
+// https://github.com/google/auto
+object AutoService {
+ private const val version = "1.1.1"
+ const val annotations = "com.google.auto.service:auto-service-annotations:${version}"
+ @Suppress("unused")
+ const val processor = "com.google.auto.service:auto-service:${version}"
+}
+
+// https://github.com/google/auto
+object AutoValue {
+ private const val version = "1.10.2"
+ const val annotations = "com.google.auto.value:auto-value-annotations:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/BouncyCastle.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/BouncyCastle.kt
new file mode 100644
index 0000000..445d57e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/BouncyCastle.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://www.bouncycastle.org/java.html
+@Suppress("unused", "ConstPropertyName")
+object BouncyCastle {
+ const val libPkcsJdk15 = "org.bouncycastle:bcpkix-jdk15on:1.68"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckStyle.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckStyle.kt
new file mode 100644
index 0000000..d7c7988
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckStyle.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Dependencies on Checkstyle Java linter.
+ *
+ * @see Checkstyle
+ * @see [io.spine.internal.gradle.checkstyle.CheckStyleConfig]
+ */
+@Suppress("unused", "ConstPropertyName")
+object CheckStyle {
+ /**
+ * The version to be used in the project.
+ *
+ * `10.12.1` is the last version in `10.12.0`, which does not introduce
+ * capability conflict over `google-collections` with Guava.
+ *
+ * @see Checkstyle
+ */
+ const val version = "10.12.1"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckerFramework.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckerFramework.kt
new file mode 100644
index 0000000..7a3993b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckerFramework.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://checkerframework.org/
+@Suppress("unused", "ConstPropertyName")
+object CheckerFramework {
+ private const val version = "3.40.0"
+ const val annotations = "org.checkerframework:checker-qual:${version}"
+ @Suppress("unused")
+ val dataflow = listOf(
+ "org.checkerframework:dataflow:${version}",
+ "org.checkerframework:javacutil:${version}"
+ )
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCli.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCli.kt
new file mode 100644
index 0000000..5d8c092
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCli.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Commons CLI is a transitive dependency which we don't use directly.
+ * We `force` it in [forceVersions].
+ *
+ * [Commons CLI](https://commons.apache.org/proper/commons-cli/)
+ */
+@Suppress("unused", "ConstPropertyName")
+object CommonsCli {
+ private const val version = "1.5.0"
+ const val lib = "commons-cli:commons-cli:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCodec.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCodec.kt
new file mode 100644
index 0000000..641c9ee
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCodec.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://commons.apache.org/proper/commons-codec/changes-report.html
+@Suppress("unused", "ConstPropertyName")
+object CommonsCodec {
+ private const val version = "1.16.0"
+ const val lib = "commons-codec:commons-codec:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsLogging.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsLogging.kt
new file mode 100644
index 0000000..c63890f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsLogging.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * [Commons Logging](https://commons.apache.org/proper/commons-logging/) is a transitive
+ * dependency which we don't use directly. This object is used for forcing the version.
+ */
+@Suppress("unused", "ConstPropertyName")
+object CommonsLogging {
+ // https://commons.apache.org/proper/commons-logging/
+ private const val version = "1.2"
+ const val lib = "commons-logging:commons-logging:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Coroutines.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Coroutines.kt
new file mode 100644
index 0000000..3074c21
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Coroutines.kt
@@ -0,0 +1,41 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Kotlin Coroutines.
+ *
+ * @see GitHub projecet
+ */
+@Suppress("unused")
+object Coroutines {
+ const val version = "1.6.4"
+ const val jdk8 = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$version"
+ const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
+ const val bom = "org.jetbrains.kotlinx:kotlinx-coroutines-bom:$version"
+ const val coreJvm = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt
new file mode 100644
index 0000000..6158c67
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/Kotlin/dokka
+@Suppress("unused", "ConstPropertyName")
+object Dokka {
+ private const val group = "org.jetbrains.dokka"
+
+ /**
+ * When changing the version, also change the version used in the
+ * `buildSrc/build.gradle.kts`.
+ */
+ const val version = "1.9.10"
+
+ object GradlePlugin {
+ const val id = "org.jetbrains.dokka"
+
+ /**
+ * The version of this plugin is already specified in `buildSrc/build.gradle.kts`
+ * file. Thus, when applying the plugin to project's build files, only the [id]
+ * should be used.
+ */
+ const val lib = "${group}:dokka-gradle-plugin:${version}"
+ }
+
+ object BasePlugin {
+ const val lib = "${group}:dokka-base:${version}"
+ }
+
+ const val analysis = "org.jetbrains.dokka:dokka-analysis:${version}"
+
+ object CorePlugin {
+ const val lib = "${group}:dokka-core:${version}"
+ }
+
+ /**
+ * To generate the documentation as seen from the Java perspective, please use this plugin.
+ *
+ * @see
+ * Dokka output formats
+ */
+ object KotlinAsJavaPlugin {
+ const val lib = "${group}:kotlin-as-java-plugin:${version}"
+ }
+
+ /**
+ * Custom Dokka plugins developed for Spine-specific needs like excluding by
+ * `@Internal` annotation.
+ *
+ * @see
+ * Custom Dokka Plugins
+ */
+ object SpineExtensions {
+ private const val group = "io.spine.tools"
+
+ const val version = "2.0.0-SNAPSHOT.4"
+ const val lib = "${group}:spine-dokka-extensions:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt
new file mode 100644
index 0000000..f04f863
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://errorprone.info/
+@Suppress("unused", "ConstPropertyName")
+object ErrorProne {
+ // https://github.com/google/error-prone
+ private const val version = "2.23.0"
+ // https://github.com/tbroyer/gradle-errorprone-plugin/blob/v0.8/build.gradle.kts
+ private const val javacPluginVersion = "9+181-r4173-1"
+
+ val annotations = listOf(
+ "com.google.errorprone:error_prone_annotations:${version}",
+ "com.google.errorprone:error_prone_type_annotations:${version}"
+ )
+ const val core = "com.google.errorprone:error_prone_core:${version}"
+ const val checkApi = "com.google.errorprone:error_prone_check_api:${version}"
+ const val testHelpers = "com.google.errorprone:error_prone_test_helpers:${version}"
+ const val javacPlugin = "com.google.errorprone:javac:${javacPluginVersion}"
+
+ // https://github.com/tbroyer/gradle-errorprone-plugin/releases
+ object GradlePlugin {
+ const val id = "net.ltgt.errorprone"
+ /**
+ * The version of this plugin is already specified in `buildSrc/build.gradle.kts` file.
+ * Thus, when applying the plugin to projects build files, only the [id] should be used.
+ *
+ * When the plugin is used as a library (e.g., in tools), its version and the library
+ * artifacts are of importance.
+ */
+ const val version = "3.1.0"
+ const val lib = "net.ltgt.gradle:gradle-errorprone-plugin:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt
new file mode 100644
index 0000000..3c2a1af
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * The FindBugs project has been dead since 2017. It has a successor called SpotBugs,
+ * but we don't use it. We use ErrorProne for static analysis instead.
+ * The only reason for having this dependency is the annotations for null-checking
+ * introduced by JSR-305. These annotations are troublesome,
+ * but no alternatives are known for some of them so far.
+ * Please see [this issue](https://github.com/SpineEventEngine/base/issues/108) for more details.
+ */
+@Suppress("unused", "ConstPropertyName")
+object FindBugs {
+ private const val version = "3.0.2"
+ const val annotations = "com.google.code.findbugs:jsr305:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Firebase.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Firebase.kt
new file mode 100644
index 0000000..499c1ce
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Firebase.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://firebase.google.com/docs/admin/setup#java
+@Suppress("unused", "ConstPropertyName")
+object Firebase {
+ private const val adminVersion = "8.1.0"
+ const val admin = "com.google.firebase:firebase-admin:${adminVersion}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Flogger.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Flogger.kt
new file mode 100644
index 0000000..cdfadb9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Flogger.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/flogger
+@Deprecated("Please use Spine Logging library instead.")
+@Suppress("unused", "ConstPropertyName")
+object Flogger {
+ internal const val version = "0.7.4"
+ const val lib = "com.google.flogger:flogger:${version}"
+
+ object Runtime {
+ const val systemBackend = "com.google.flogger:flogger-system-backend:${version}"
+ const val log4j2Backend = "com.google.flogger:flogger-log4j2-backend:${version}"
+ const val slf4JBackend = "com.google.flogger:flogger-slf4j-backend:${version}"
+ const val grpcContext = "com.google.flogger:flogger-grpc-context:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleApis.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleApis.kt
new file mode 100644
index 0000000..421915c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleApis.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Provides dependencies on [GoogleApis projects](https://github.com/googleapis/).
+ */
+@Suppress("unused", "ConstPropertyName")
+object GoogleApis {
+
+ // https://github.com/googleapis/google-api-java-client
+ const val client = "com.google.api-client:google-api-client:1.32.2"
+
+ // https://github.com/googleapis/api-common-java
+ const val common = "com.google.api:api-common:2.1.1"
+
+ // https://github.com/googleapis/java-common-protos
+ const val commonProtos = "com.google.api.grpc:proto-google-common-protos:2.7.0"
+
+ // https://github.com/googleapis/gax-java
+ const val gax = "com.google.api:gax:2.7.1"
+
+ // https://github.com/googleapis/java-iam
+ const val protoAim = "com.google.api.grpc:proto-google-iam-v1:1.2.0"
+
+ // https://github.com/googleapis/google-oauth-java-client
+ const val oAuthClient = "com.google.oauth-client:google-oauth-client:1.32.1"
+
+ // https://github.com/googleapis/google-auth-library-java
+ object AuthLibrary {
+ const val version = "1.3.0"
+ const val credentials = "com.google.auth:google-auth-library-credentials:${version}"
+ const val oAuth2Http = "com.google.auth:google-auth-library-oauth2-http:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleCloud.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleCloud.kt
new file mode 100644
index 0000000..5091364
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleCloud.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused", "ConstPropertyName")
+object GoogleCloud {
+
+ // https://github.com/googleapis/java-core
+ const val core = "com.google.cloud:google-cloud-core:2.3.3"
+
+ // https://github.com/googleapis/java-pubsub/tree/main/proto-google-cloud-pubsub-v1
+ const val pubSubGrpcApi = "com.google.api.grpc:proto-google-cloud-pubsub-v1:1.97.0"
+
+ // https://github.com/googleapis/java-trace
+ const val trace = "com.google.cloud:google-cloud-trace:2.1.0"
+
+ // https://github.com/googleapis/java-datastore
+ const val datastore = "com.google.cloud:google-cloud-datastore:2.2.1"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/GradleDoctor.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GradleDoctor.kt
new file mode 100644
index 0000000..707fac3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GradleDoctor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Helps optimize Gradle Builds by ensuring recommendations at build time.
+ *
+ * See [plugin site](https://runningcode.github.io/gradle-doctor) for features and usage.
+ */
+@Suppress("unused", "ConstPropertyName")
+object GradleDoctor {
+ const val version = "0.8.1"
+ const val pluginId = "com.osacky.doctor"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt
new file mode 100644
index 0000000..32d2176
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/grpc/grpc-java
+@Suppress("unused", "ConstPropertyName")
+object Grpc {
+ @Suppress("MemberVisibilityCanBePrivate")
+ const val version = "1.59.0"
+ const val api = "io.grpc:grpc-api:${version}"
+ const val auth = "io.grpc:grpc-auth:${version}"
+ const val core = "io.grpc:grpc-core:${version}"
+ const val context = "io.grpc:grpc-context:${version}"
+ const val inProcess = "io.grpc:grpc-inprocess:${version}"
+ const val stub = "io.grpc:grpc-stub:${version}"
+ const val okHttp = "io.grpc:grpc-okhttp:${version}"
+ const val protobuf = "io.grpc:grpc-protobuf:${version}"
+ const val protobufLite = "io.grpc:grpc-protobuf-lite:${version}"
+ const val netty = "io.grpc:grpc-netty:${version}"
+ const val nettyShaded = "io.grpc:grpc-netty-shaded:${version}"
+
+ object ProtocPlugin {
+ const val id = "grpc"
+ const val artifact = "io.grpc:protoc-gen-grpc-java:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/GrpcKotlin.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GrpcKotlin.kt
new file mode 100644
index 0000000..a2fb7ef
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GrpcKotlin.kt
@@ -0,0 +1,43 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * gRPC-Kotlin/JVM.
+ *
+ * @see GitHub project
+ */
+@Suppress("unused")
+object GrpcKotlin {
+ const val version = "1.3.0"
+ const val stub = "io.grpc:grpc-kotlin-stub:$version"
+
+ object ProtocPlugin {
+ const val id = "grpckt"
+ const val artifact = "io.grpc:protoc-gen-grpc-kotlin:$version:jdk8@jar"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Gson.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Gson.kt
new file mode 100644
index 0000000..feb1008
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Gson.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Gson is a transitive dependency, which we don't use directly.
+ * We `force` it in [DependencyResolution.forceConfiguration()].
+ *
+ * [Gson](https://github.com/google/gson)
+ */
+@Suppress("unused", "ConstPropertyName")
+object Gson {
+ private const val version = "2.10.1"
+ const val lib = "com.google.code.gson:gson:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Guava.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Guava.kt
new file mode 100644
index 0000000..810a804
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Guava.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * The dependencies for Guava.
+ *
+ * When changing the version, also change the version used in the `build.gradle.kts`. We need
+ * to synchronize the version used in `buildSrc` and in Spine modules. Otherwise, when testing
+ * Gradle plugins, errors may occur due to version clashes.
+ *
+ * @see Guava at GitHub.
+ */
+@Suppress("unused", "ConstPropertyName")
+object Guava {
+ private const val version = "32.1.3-jre"
+ const val lib = "com.google.guava:guava:${version}"
+ const val testLib = "com.google.guava:guava-testlib:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Hamcrest.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Hamcrest.kt
new file mode 100644
index 0000000..297b969
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Hamcrest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * The dependency on the Hamcrest, which is transitive for us.
+ *
+ * If you need assertions in Java, please use Google [Truth] instead.
+ * For Kotlin, please use [Kotest].
+ */
+@Suppress("unused", "ConstPropertyName")
+object Hamcrest {
+ // https://github.com/hamcrest/JavaHamcrest/releases
+ private const val version = "2.2"
+ const val core = "org.hamcrest:hamcrest-core:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/HttpClient.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/HttpClient.kt
new file mode 100644
index 0000000..c481e16
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/HttpClient.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Google implementations of [HTTP client](https://github.com/googleapis/google-http-java-client).
+ */
+@Suppress("unused", "ConstPropertyName")
+object HttpClient {
+ // https://github.com/googleapis/google-http-java-client
+ const val version = "1.43.3"
+ const val google = "com.google.http-client:google-http-client:${version}"
+ const val jackson2 = "com.google.http-client:google-http-client-jackson2:${version}"
+ const val gson = "com.google.http-client:google-http-client-gson:${version}"
+ const val apache2 = "com.google.http-client:google-http-client-apache-v2:${version}"
+
+ const val apache = "com.google.http-client:google-http-client-apache:2.1.2"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/IntelliJ.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/IntelliJ.kt
new file mode 100644
index 0000000..1062c9b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/IntelliJ.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("ConstPropertyName")
+
+package io.spine.internal.dependency
+
+/**
+ * The components of the IntelliJ Platform.
+ *
+ * Make sure to add the `intellijReleases` and `jetBrainsCacheRedirector`
+ * repositories to your project. See `kotlin/Repositories.kt` for details.
+ */
+@Suppress("unused")
+object IntelliJ {
+
+ /**
+ * The version of the IntelliJ platform.
+ *
+ * This is the version used by Kotlin compiler `1.9.21`.
+ * Advance this version with caution because it may break the setup of
+ * IntelliJ platform standalone execution.
+ */
+ const val version = "213.7172.53"
+
+ object Platform {
+ private const val group = "com.jetbrains.intellij.platform"
+ const val core = "$group:core:$version"
+ const val util = "$group:util:$version"
+ const val coreImpl = "$group:core-impl:$version"
+ const val codeStyle = "$group:code-style:$version"
+ const val codeStyleImpl = "$group:code-style-impl:$version"
+ const val projectModel = "$group:project-model:$version"
+ const val projectModelImpl = "$group:project-model-impl:$version"
+ const val lang = "$group:lang:$version"
+ const val langImpl = "$group:lang-impl:$version"
+ const val ideImpl = "$group:ide-impl:$version"
+ const val ideCoreImpl = "$group:ide-core-impl:$version"
+ const val analysisImpl = "$group:analysis-impl:$version"
+ const val indexingImpl = "$group:indexing-impl:$version"
+ }
+
+ object Jsp {
+ private const val group = "com.jetbrains.intellij.jsp"
+ @Suppress("MemberNameEqualsClassName")
+ const val jsp = "$group:jsp:$version"
+ }
+
+ object Xml {
+ private const val group = "com.jetbrains.intellij.xml"
+ const val xmlPsiImpl = "$group:xml-psi-impl:$version"
+ }
+
+ object JavaPsi {
+ private const val group = "com.jetbrains.intellij.java"
+ const val api = "$group:java-psi:$version"
+ const val impl = "$group:java-psi-impl:$version"
+ }
+
+ object Java {
+ private const val group = "com.jetbrains.intellij.java"
+ @Suppress("MemberNameEqualsClassName")
+ const val java = "$group:java:$version"
+ const val impl = "$group:java-impl:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/J2ObjC.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/J2ObjC.kt
new file mode 100644
index 0000000..54522bc
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/J2ObjC.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * [J2ObjC](https://developers.google.com/j2objc) is a transitive dependency,
+ * which we don't use directly. This object is used for forcing the version.
+ */
+@Suppress("unused", "ConstPropertyName")
+object J2ObjC {
+ /**
+ * See [J2ObjC releases](https://github.com/google/j2objc/releases).
+ *
+ * `1.3` was the latest version available from Maven Central.
+ * Now `2.8` is the latest version available.
+ * As [HttpClient]
+ * [migrated](https://github.com/googleapis/google-http-java-client/releases/tag/v1.43.3) to v2,
+ * we set the latest v2 version as well.
+ *
+ * @see
+ * J2ObjC on Maven Central
+ */
+ private const val version = "2.8"
+ const val annotations = "com.google.j2objc:j2objc-annotations:${version}"
+ @Deprecated("Please use `annotations` instead.", ReplaceWith("annotations"))
+ const val lib = annotations
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt
new file mode 100644
index 0000000..122c657
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://junit.org/junit5/
+@Suppress("unused", "ConstPropertyName")
+object JUnit {
+ const val version = "5.10.0"
+ private const val legacyVersion = "4.13.1"
+
+ // https://github.com/apiguardian-team/apiguardian
+ private const val apiGuardianVersion = "1.1.2"
+
+ // https://github.com/junit-pioneer/junit-pioneer
+ private const val pioneerVersion = "2.0.1"
+
+ const val legacy = "junit:junit:${legacyVersion}"
+
+ val api = listOf(
+ "org.apiguardian:apiguardian-api:${apiGuardianVersion}",
+ "org.junit.jupiter:junit-jupiter-api:${version}",
+ "org.junit.jupiter:junit-jupiter-params:${version}"
+ )
+ const val bom = "org.junit:junit-bom:${version}"
+
+ const val runner = "org.junit.jupiter:junit-jupiter-engine:${version}"
+ const val params = "org.junit.jupiter:junit-jupiter-params:${version}"
+
+ const val pioneer = "org.junit-pioneer:junit-pioneer:${pioneerVersion}"
+
+ object Platform {
+ // https://junit.org/junit5/
+ const val version = "1.10.0"
+ internal const val group = "org.junit.platform"
+ const val commons = "$group:junit-platform-commons:$version"
+ const val launcher = "$group:junit-platform-launcher:$version"
+ const val engine = "$group:junit-platform-engine:$version"
+ const val suiteApi = "$group:junit-platform-suite-api:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt
new file mode 100644
index 0000000..1ef088c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/FasterXML/jackson/wiki/Jackson-Releases
+@Suppress("unused", "ConstPropertyName")
+object Jackson {
+ const val version = "2.15.3"
+ private const val databindVersion = "2.15.3"
+
+ private const val coreGroup = "com.fasterxml.jackson.core"
+ private const val dataformatGroup = "com.fasterxml.jackson.dataformat"
+ private const val moduleGroup = "com.fasterxml.jackson.module"
+
+ // https://github.com/FasterXML/jackson-core
+ const val core = "$coreGroup:jackson-core:${version}"
+ // https://github.com/FasterXML/jackson-databind
+ const val databind = "$coreGroup:jackson-databind:${databindVersion}"
+ // https://github.com/FasterXML/jackson-annotations
+ const val annotations = "$coreGroup:jackson-annotations:${version}"
+
+ // https://github.com/FasterXML/jackson-dataformat-xml/releases
+ const val dataformatXml = "$dataformatGroup:jackson-dataformat-xml:${version}"
+ // https://github.com/FasterXML/jackson-dataformats-text/releases
+ const val dataformatYaml = "$dataformatGroup:jackson-dataformat-yaml:${version}"
+
+ // https://github.com/FasterXML/jackson-module-kotlin/releases
+ const val moduleKotlin = "$moduleGroup:jackson-module-kotlin:${version}"
+
+ // https://github.com/FasterXML/jackson-bom
+ const val bom = "com.fasterxml.jackson:jackson-bom:${version}"
+
+ // https://github.com/FasterXML/jackson-jr
+ object Junior {
+ const val version = Jackson.version
+ const val group = "com.fasterxml.jackson.jr"
+ const val objects = "$group:jackson-jr-objects:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaDiffUtils.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaDiffUtils.kt
new file mode 100644
index 0000000..c04aedd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaDiffUtils.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * The dependency on the `java-diff-utils` library, which is transitive for us at the time
+ * of writing.
+ *
+ * It might become our dependency as a part of
+ * the [Spine Text](https://github.com/SpineEventEngine/text) library.
+ */
+@Suppress("unused", "ConstPropertyName")
+object JavaDiffUtils {
+
+ // https://github.com/java-diff-utils/java-diff-utils/releases
+ private const val version = "4.12"
+ const val lib = "io.github.java-diff-utils:java-diff-utils:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaJwt.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaJwt.kt
new file mode 100644
index 0000000..a60e726
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaJwt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * A Java implementation of JSON Web Token (JWT) - RFC 7519.
+ *
+ * [Java JWT](https://github.com/auth0/java-jwt)
+ */
+@Suppress("unused", "ConstPropertyName")
+object JavaJwt {
+
+ /**
+ * The last version in the v3.x.x series.
+ *
+ * There's a v4.x.x series (e.g., https://github.com/auth0/java-jwt/releases/tag/4.4.0), but
+ * it introduces breaking changes. Consider upgrading to it when we're ready to migrate.
+ */
+ private const val version = "3.19.4"
+
+ const val lib = "com.auth0:java-jwt:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaPoet.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaPoet.kt
new file mode 100644
index 0000000..44ddb2c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaPoet.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/square/javapoet
+@Suppress("unused", "ConstPropertyName")
+object JavaPoet {
+ private const val version = "1.13.0"
+ const val lib = "com.squareup:javapoet:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaX.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaX.kt
new file mode 100644
index 0000000..44cbbd5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaX.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused", "ConstPropertyName")
+object JavaX {
+ // This artifact which used to be a part of J2EE moved under Eclipse EE4J project.
+ // https://github.com/eclipse-ee4j/common-annotations-api
+ const val annotations = "javax.annotation:javax.annotation-api:1.3.2"
+
+ const val servletApi = "javax.servlet:javax.servlet-api:3.1.0"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Klaxon.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Klaxon.kt
new file mode 100644
index 0000000..a2d81ce
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Klaxon.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * A JSON parser in Kotlin.
+ *
+ * [Klaxon](https://github.com/cbeust/klaxon)
+ */
+@Suppress("unused", "ConstPropertyName")
+object Klaxon {
+ private const val version = "5.6"
+ const val lib = "com.beust:klaxon:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotest.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotest.kt
new file mode 100644
index 0000000..f64ff9a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("unused")
+
+package io.spine.internal.dependency
+
+/**
+ * Testing framework for Kotlin.
+ *
+ * @see Kotest site
+ */
+@Suppress("unused", "ConstPropertyName")
+object Kotest {
+ const val version = "5.8.0"
+ const val group = "io.kotest"
+ const val assertions = "$group:kotest-assertions-core:$version"
+ const val runnerJUnit5 = "$group:kotest-runner-junit5:$version"
+ const val runnerJUnit5Jvm = "$group:kotest-runner-junit5-jvm:$version"
+ const val frameworkApi = "$group:kotest-framework-api:$version"
+ const val datatest = "$group:kotest-framework-datatest:$version"
+ const val frameworkEngine = "$group:kotest-framework-engine:$version"
+
+ // https://plugins.gradle.org/plugin/io.kotest.multiplatform
+ object MultiplatformGradlePlugin {
+ const val version = Kotest.version
+ const val id = "io.kotest.multiplatform"
+ const val classpath = "$group:kotest-framework-multiplatform-plugin-gradle:$version"
+ }
+
+ // https://github.com/kotest/kotest-gradle-plugin
+ object JvmGradlePlugin {
+ const val version = "0.4.10"
+ const val id = "io.kotest"
+ const val classpath = "$group:kotest-gradle-plugin:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt
new file mode 100644
index 0000000..ec7f338
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/JetBrains/kotlin
+// https://github.com/Kotlin
+@Suppress("unused", "ConstPropertyName")
+object Kotlin {
+
+ /**
+ * When changing the version, also change the version used in the `buildSrc/build.gradle.kts`.
+ */
+ @Suppress("MemberVisibilityCanBePrivate") // used directly from the outside.
+ const val version = "1.9.23"
+
+ /**
+ * The version of the JetBrains annotations library, which is a transitive
+ * dependency for us via Kotlin libraries.
+ *
+ * @see Java Annotations
+ */
+ private const val annotationsVersion = "24.0.1"
+
+ private const val group = "org.jetbrains.kotlin"
+
+ const val stdLib = "$group:kotlin-stdlib:$version"
+ const val stdLibCommon = "$group:kotlin-stdlib-common:$version"
+
+ @Deprecated("Please use `stdLib` instead.")
+ const val stdLibJdk7 = "$group:kotlin-stdlib-jdk7:$version"
+
+ @Deprecated("Please use `stdLib` instead.")
+ const val stdLibJdk8 = "$group:kotlin-stdlib-jdk8:$version"
+
+ const val reflect = "$group:kotlin-reflect:$version"
+ const val testJUnit5 = "$group:kotlin-test-junit5:$version"
+
+ const val gradlePluginApi = "$group:kotlin-gradle-plugin-api:$version"
+ const val gradlePluginLib = "$group:kotlin-gradle-plugin:$version"
+
+ const val jetbrainsAnnotations = "org.jetbrains:annotations:$annotationsVersion"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinSemver.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinSemver.kt
new file mode 100644
index 0000000..c8e8029
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinSemver.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/z4kn4fein/kotlin-semver
+@Suppress("unused", "ConstPropertyName")
+object KotlinSemver {
+ private const val version = "1.4.2"
+ const val lib = "io.github.z4kn4fein:semver:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinX.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinX.kt
new file mode 100644
index 0000000..f10eb45
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinX.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused", "ConstPropertyName")
+object KotlinX {
+
+ const val group = "org.jetbrains.kotlinx"
+
+ object Coroutines {
+
+ // https://github.com/Kotlin/kotlinx.coroutines
+ const val version = "1.7.3"
+ const val core = "$group:kotlinx-coroutines-core:$version"
+ const val jdk8 = "$group:kotlinx-coroutines-jdk8:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kover.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kover.kt
new file mode 100644
index 0000000..30f81f5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kover.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/Kotlin/kotlinx-kover
+@Suppress("unused", "ConstPropertyName")
+object Kover {
+ const val version = "0.7.4"
+ const val id = "org.jetbrains.kotlinx.kover"
+ const val classpath = "org.jetbrains.kotlinx:kover-gradle-plugin:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Netty.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Netty.kt
new file mode 100644
index 0000000..eefcd75
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Netty.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused", "ConstPropertyName")
+object Netty {
+ // https://github.com/netty/netty/tags
+ private const val version = "4.1.100.Final"
+ const val common = "io.netty:netty-common:${version}"
+ const val buffer = "io.netty:netty-buffer:${version}"
+ const val transport = "io.netty:netty-transport:${version}"
+ const val handler = "io.netty:netty-handler:${version}"
+ const val codecHttp = "io.netty:netty-codec-http:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Okio.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Okio.kt
new file mode 100644
index 0000000..4ec7cbf
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Okio.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Okio is a transitive dependency, which we don't use directly.
+ *
+ * https://github.com/square/okio/tags
+ */
+@Suppress("unused", "ConstPropertyName")
+object Okio {
+ private const val version = "3.6.0"
+ const val lib = "com.squareup.okio:okio:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/OpenTest4J.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/OpenTest4J.kt
new file mode 100644
index 0000000..0b189f0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/OpenTest4J.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * The dependency on the OpenTest4j library, which is transitive for us.
+ */
+@Suppress("unused", "ConstPropertyName")
+object OpenTest4J {
+
+ // https://github.com/ota4j-team/opentest4j/releases
+ private const val version = "1.3.0"
+ const val lib = "org.opentest4j:opentest4j:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt
new file mode 100644
index 0000000..a11cec2
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused", "ConstPropertyName")
+object OsDetector {
+ // https://github.com/google/osdetector-gradle-plugin
+ const val version = "1.7.3"
+ const val id = "com.google.osdetector"
+ const val lib = "com.google.gradle:osdetector-gradle-plugin:${version}"
+ const val classpath = lib
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Plexus.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Plexus.kt
new file mode 100644
index 0000000..19305a1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Plexus.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("MaxLineLength")
+
+package io.spine.internal.dependency
+
+/**
+ * Plexus Utils is a transitive dependency, which we don't use directly.
+ *
+ * [Plexus Utils](https://github.com/codehaus-plexus/plexus-utils)
+ */
+@Suppress("unused", "ConstPropertyName")
+object Plexus {
+
+ /**
+ * This is the last version in the 3.x series.
+ *
+ * There's a major update to 4.x.
+ *
+ * @see plexus-utils-4.0.0
+ */
+ private const val version = "4.0.0"
+ const val utils = "org.codehaus.plexus:plexus-utils:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt
new file mode 100644
index 0000000..8d27f55
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("MaxLineLength")
+
+package io.spine.internal.dependency
+
+// https://github.com/pmd/pmd/releases
+@Suppress("unused", "ConstPropertyName")
+object Pmd {
+ /**
+ * This is the last version in the 6.x series.
+ *
+ * There's a major update to 7.x series.
+ *
+ * @see spine-base
+ */
+ const val base = "2.0.0-SNAPSHOT.199"
+
+ /**
+ * The version of [Spine.reflect].
+ *
+ * @see spine-reflect
+ */
+ const val reflect = "2.0.0-SNAPSHOT.183"
+
+ /**
+ * The version of [Spine.Logging].
+ *
+ * @see spine-logging
+ */
+ const val logging = "2.0.0-SNAPSHOT.233"
+
+ /**
+ * The version of [Spine.testlib].
+ *
+ * @see spine-testlib
+ */
+ const val testlib = "2.0.0-SNAPSHOT.184"
+
+ /**
+ * The version of `core-java`.
+ *
+ * @see [Spine.CoreJava.client]
+ * @see [Spine.CoreJava.server]
+ * @see core-java
+ */
+ const val core = "2.0.0-SNAPSHOT.176"
+
+ /**
+ * The version of [Spine.modelCompiler].
+ *
+ * @see spine-model-compiler
+ */
+ const val mc = "2.0.0-SNAPSHOT.133"
+
+ /**
+ * The version of [McJava].
+ *
+ * @see spine-mc-java
+ */
+ const val mcJava = "2.0.0-SNAPSHOT.205"
+
+ /**
+ * The version of [Spine.baseTypes].
+ *
+ * @see spine-base-types
+ */
+ const val baseTypes = "2.0.0-SNAPSHOT.126"
+
+ /**
+ * The version of [Spine.time].
+ *
+ * @see spine-time
+ */
+ const val time = "2.0.0-SNAPSHOT.135"
+
+ /**
+ * The version of [Spine.change].
+ *
+ * @see spine-change
+ */
+ const val change = "2.0.0-SNAPSHOT.118"
+
+ /**
+ * The version of [Spine.text].
+ *
+ * @see spine-text
+ */
+ const val text = "2.0.0-SNAPSHOT.6"
+
+ /**
+ * The version of [Spine.toolBase].
+ *
+ * @see spine-tool-base
+ */
+ const val toolBase = "2.0.0-SNAPSHOT.208"
+
+ /**
+ * The version of [Spine.javadocTools].
+ *
+ * @see spine-javadoc-tools
+ */
+ const val javadocTools = "2.0.0-SNAPSHOT.75"
+ }
+
+ const val base = "$group:spine-base:${ArtifactVersion.base}"
+
+ const val reflect = "$group:spine-reflect:${ArtifactVersion.reflect}"
+ const val baseTypes = "$group:spine-base-types:${ArtifactVersion.baseTypes}"
+ const val time = "$group:spine-time:${ArtifactVersion.time}"
+ const val change = "$group:spine-change:${ArtifactVersion.change}"
+ const val text = "$group:spine-text:${ArtifactVersion.text}"
+
+ const val testlib = "$toolsGroup:spine-testlib:${ArtifactVersion.testlib}"
+ const val testUtilTime = "$toolsGroup:spine-testutil-time:${ArtifactVersion.time}"
+ const val psiJava = "$toolsGroup:spine-psi-java:${ArtifactVersion.toolBase}"
+ const val psiJavaBundle = "$toolsGroup:spine-psi-java-bundle:${ArtifactVersion.toolBase}"
+ const val toolBase = "$toolsGroup:spine-tool-base:${ArtifactVersion.toolBase}"
+ const val pluginBase = "$toolsGroup:spine-plugin-base:${ArtifactVersion.toolBase}"
+ const val pluginTestlib = "$toolsGroup:spine-plugin-testlib:${ArtifactVersion.toolBase}"
+ const val modelCompiler = "$toolsGroup:spine-model-compiler:${ArtifactVersion.mc}"
+
+ /**
+ * Dependencies on the artifacts of the Spine Logging library.
+ *
+ * @see spine-logging
+ */
+ object Logging {
+ const val version = ArtifactVersion.logging
+ const val lib = "$group:spine-logging:$version"
+
+ const val log4j2Backend = "$group:spine-logging-log4j2-backend:$version"
+ const val stdContext = "$group:spine-logging-std-context:$version"
+ const val grpcContext = "$group:spine-logging-grpc-context:$version"
+ const val smokeTest = "$group:spine-logging-smoke-test:$version"
+
+ // Transitive dependencies.
+ // Make `public` and use them to force a version in a particular repository, if needed.
+ internal const val julBackend = "$group:spine-logging-jul-backend:$version"
+ internal const val middleware = "$group:spine-logging-middleware:$version"
+ internal const val platformGenerator = "$group:spine-logging-platform-generator:$version"
+ internal const val jvmDefaultPlatform = "$group:spine-logging-jvm-default-platform:$version"
+
+ @Deprecated(
+ message = "Please use `Logging.lib` instead.",
+ replaceWith = ReplaceWith("lib")
+ )
+ const val floggerApi = "$group:spine-flogger-api:$version"
+
+ @Deprecated(
+ message = "Please use `grpcContext` instead.",
+ replaceWith = ReplaceWith("grpcContext")
+ )
+ const val floggerGrpcContext = "$group:spine-flogger-grpc-context:$version"
+ }
+
+ /**
+ * Dependencies on Spine Model Compiler for Java.
+ *
+ * See [mc-java](https://github.com/SpineEventEngine/mc-java).
+ */
+ @Suppress("MemberVisibilityCanBePrivate") // `pluginLib()` is used by subprojects.
+ object McJava {
+ const val version = ArtifactVersion.mcJava
+ const val pluginId = "io.spine.mc-java"
+ val pluginLib = pluginLib(version)
+ fun pluginLib(version: String): String = "$toolsGroup:spine-mc-java-plugins:$version:all"
+ }
+
+ @Deprecated("Please use `javadocFilter` instead.", ReplaceWith("javadocFilter"))
+ const val javadocTools = "$toolsGroup::${ArtifactVersion.javadocTools}"
+ const val javadocFilter = "$toolsGroup:spine-javadoc-filter:${ArtifactVersion.javadocTools}"
+
+ const val client = CoreJava.client // Added for brevity.
+ const val server = CoreJava.server // Added for brevity.
+
+ /**
+ * Dependencies on `core-java` modules.
+ *
+ * See [`SpineEventEngine/core-java`](https://github.com/SpineEventEngine/core-java/).
+ */
+ object CoreJava {
+ const val version = ArtifactVersion.core
+ const val core = "$group:spine-core:$version"
+ const val client = "$group:spine-client:$version"
+ const val server = "$group:spine-server:$version"
+ const val testUtilServer = "$toolsGroup:spine-testutil-server:$version"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt
new file mode 100644
index 0000000..a345b0f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("MaxLineLength")
+
+package io.spine.internal.dependency
+
+/**
+ * Gradle TestKit extension for Google Truth.
+ *
+ * @see TestKit source code
+ * @see Usage description
+ */
+@Suppress("unused", "ConstPropertyName")
+object TestKitTruth {
+ private const val version = "1.20.0"
+ const val lib = "com.autonomousapps:testkit-truth:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt
new file mode 100644
index 0000000..3135417
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/truth
+@Suppress("unused", "ConstPropertyName")
+object Truth {
+ private const val version = "1.1.5"
+ val libs = listOf(
+ "com.google.truth:truth:${version}",
+ "com.google.truth.extensions:truth-java8-extension:${version}",
+ "com.google.truth.extensions:truth-proto-extension:${version}"
+ )
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Validation.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Validation.kt
new file mode 100644
index 0000000..ded15d8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Validation.kt
@@ -0,0 +1,64 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Dependencies on Spine Validation SDK.
+ *
+ * See [`SpineEventEngine/validation`](https://github.com/SpineEventEngine/validation/).
+ */
+@Suppress("unused", "ConstPropertyName")
+object Validation {
+ /**
+ * The version of the Validation library artifacts.
+ */
+ const val version = "2.0.0-SNAPSHOT.132"
+
+ /**
+ * The distinct version of the Validation library used by build tools during
+ * the transition from a previous version when breaking API changes are introduced.
+ *
+ * When Validation is used both for building the project and as a part of the project's
+ * transitional dependencies, this is the version used to build the project itself to
+ * avoid errors caused by incompatible API changes.
+ */
+ const val dogfoodingVersion = "2.0.0-SNAPSHOT.132"
+
+ const val group = "io.spine.validation"
+ private const val prefix = "spine-validation"
+
+ const val runtime = "$group:$prefix-java-runtime:$version"
+ const val java = "$group:$prefix-java:$version"
+
+ /** Obtains the artifact for the `java-bundle` artifact of the given version. */
+ fun javaBundle(version: String) = "$group:$prefix-java-bundle:$version"
+
+ val javaBundle = javaBundle(version)
+
+ const val model = "$group:$prefix-model:$version"
+ const val config = "$group:$prefix-configuration:$version"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt
new file mode 100644
index 0000000..f1c0331
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+@Suppress("unused")
+object Build {
+ val ci = "true".equals(System.getenv("CI"))
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt
new file mode 100644
index 0000000..1f9b1cc
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+import java.io.File
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * Cleans the folder and all of its content.
+ */
+fun cleanFolder(folder: File) {
+ if(!folder.exists()) {
+ return
+ }
+ if(!folder.isDirectory) {
+ throw IllegalArgumentException("A folder to clean " +
+ "must be supplied: `${folder.absolutePath}`.")
+ }
+ Files.walk(folder.toPath())
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt
new file mode 100644
index 0000000..65e00ad
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("unused") /* Some constants may be used throughout the Spine repos. */
+
+package io.spine.internal.gradle
+
+import java.io.File
+import java.net.URI
+import java.nio.file.Files
+import java.nio.file.Path
+import org.ajoberstar.grgit.Grgit
+import org.gradle.api.tasks.TaskContainer
+
+/**
+ * A tool to execute the Gradle `build` task in selected Git repositories
+ * with the local version of [config] contents.
+ *
+ * Checks out the content of selected repositories into the specified [tempFolder]. The folder
+ * is created if it does not exist. By default, uses `./tmp` as a temp folder.
+ *
+ * Replaces the `config` and `buildSrc` folders in the checked out repository by the local versions
+ * of code. If the repository-under-test already contains its own `buildSrc` or `config` folders,
+ * they are NOT overwritten, but rather renamed into `buildSrc-original` and `config-original`
+ * accordingly. This allows further tracing if the build fails.
+ *
+ * Uses Gradle's [tasks] container to register itself as a Gradle task.
+ *
+ * This tool uses `println`s to print out its state. This is done to simplify the configuration
+ * and dependencies.
+ *
+ * When running the Gradle build for each repository, a [RunBuild] task is used. Error and debug
+ * logs of each Gradle test build are written according to this task's implementation.
+ */
+class ConfigTester(
+ private val config: Path,
+ private val tasks: TaskContainer,
+ private val tempFolder: File = File("./tmp")
+) {
+
+ companion object {
+
+ /**
+ * Gradle build timeout.
+ */
+ private const val BUILD_TIMEOUT_MINUTES = 30L
+ }
+
+ private val buildSrc: Path = config.resolve("buildSrc")
+
+ /**
+ * Git repositories to test.
+ */
+ private val repos: MutableList = ArrayList()
+
+ /**
+ * Adds a Git [repo] into the test build by its URI.
+ *
+ * The `master` branch is used as the one to checkout.
+ */
+ fun addRepo(repo: URI): ConfigTester {
+ repos.add(GitRepository(repo))
+ return this
+ }
+
+ /**
+ * Adds a test
+ */
+ fun addRepo(repo: URI, branch: Branch): ConfigTester {
+ repos.add(GitRepository(repo, branch))
+ return this
+ }
+
+ fun registerUnder(taskName: String) {
+ val tasksPerRepo = repos.map { testWithConfig(it) }
+
+ tasks.register(taskName) {
+ for (repoTaskName in tasksPerRepo) {
+ dependsOn(repoTaskName)
+ }
+ }
+ }
+
+ private fun testWithConfig(gitRepo: GitRepository): String {
+ val runGradleName = runGradleTask(gitRepo)
+ doRegisterRunBuild(runGradleName, gitRepo)
+
+ val executeBuildName = executeBuildTask(gitRepo)
+ doRegisterExecuteBuild(executeBuildName, gitRepo, runGradleName)
+ return executeBuildName
+ }
+
+ private fun doRegisterExecuteBuild(
+ executeBuildName: String,
+ gitRepo: GitRepository,
+ runGradleName: String
+ ) {
+ tasks.register(executeBuildName) {
+ doLast {
+ println(" *** Testing `config` and `config/buildSrc` with `${gitRepo.name}`. ***")
+ val ignoredFolder = tempFolder.toPath()
+ gitRepo.checkout(tempFolder)
+ .replaceBuildSrc(buildSrc, ignoredFolder).replaceConfig(config, ignoredFolder)
+ }
+ finalizedBy(runGradleName)
+ }
+ }
+
+ private fun doRegisterRunBuild(
+ runGradleName: String,
+ gitRepo: GitRepository,
+ ) {
+ tasks.register(runGradleName, RunBuild::class.java) {
+ doFirst {
+ println("`${gitRepo.name}`: starting Gradle build...")
+ }
+ doLast {
+ println("*** `${gitRepo.name}`: Gradle build completed. ***")
+ }
+ directory = gitRepo.prepareCheckout(tempFolder).absolutePath
+ maxDurationMins = BUILD_TIMEOUT_MINUTES
+ }
+ }
+
+ private fun runGradleTask(repo: GitRepository): String {
+ return "run-gradle-${repo.name}"
+ }
+
+ private fun executeBuildTask(repo: GitRepository): String {
+ return "execute-build-${repo.name}"
+ }
+}
+
+/**
+ * A repository of source code hosted using Git.
+ */
+class GitRepository(
+
+ /**
+ * URI pointing to the location of the repository.
+ */
+ private val uri: URI,
+
+ /**
+ * A branch to checkout.
+ *
+ * By default, points to `master`.
+ */
+ private val branch: Branch = Branch("master"),
+) {
+ /**
+ * The name of this repository.
+ */
+ val name: String
+
+ init {
+ name = repoName(uri)
+ }
+
+ fun prepareCheckout(destinationFolder: File): File {
+ if (!destinationFolder.exists()) {
+ destinationFolder.mkdirs()
+ }
+
+ val result = destinationFolder.toPath().resolve(name)
+ Files.createDirectories(result)
+ return result.toFile()
+ }
+
+ /**
+ * Performs the checkout of the source code for this repository
+ * 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
+ * the `destinationFolder/foobar` folder.
+ *
+ * If the supplied folder does not exist, it is created.
+ */
+ fun checkout(destinationFolder: File): ClonedRepo {
+ val preparedFolder = prepareCheckout(destinationFolder).toPath()
+ println(
+ "Checking out the `$uri` repository at `${branch.name}` " +
+ "to `${preparedFolder.toAbsolutePath()}`."
+ )
+
+ Grgit.clone(
+ mapOf(
+ "dir" to preparedFolder,
+ "uri" to uri
+ )
+ ).checkout(
+ mapOf(
+ "branch" to branch.name
+ )
+ )
+ return ClonedRepo(this, preparedFolder)
+ }
+
+ private fun repoName(resourceLocation: URI): String {
+ var path = resourceLocation.path
+ if (path.endsWith('/')) {
+ path = path.substring(0, path.length - 1)
+ }
+ val fromLastSlash = path.lastIndexOf('/') + 1
+ val repoName = path.substring(fromLastSlash)
+ return repoName
+ }
+
+ /**
+ * Returns a new Git repository pointing to some particular Git [branch].
+ */
+ fun at(branch: Branch): GitRepository {
+ return GitRepository(uri, branch)
+ }
+}
+
+/**
+ * The cloned Git repository.
+ */
+class ClonedRepo(
+
+ /**
+ * Origin Git repository which is cloned.
+ */
+ private val repo: GitRepository,
+
+ /**
+ * The location into which the [repo] is cloned.
+ */
+ private val location: Path
+) {
+
+ /**
+ * Replaces the `buildSrc` folder in this cloned repository by the contents
+ * of the folder defined by the [source].
+ *
+ * [source] is expected to be another `buildSrc` folder.
+ *
+ * The original `buildSrc` folder, if it exists in this cloned repo, is renamed
+ * to `buildSrc-original`.
+ *
+ * Optionally, takes an [ignoredFolder] which will be excluded from the [source] paths
+ * when copying.
+ *
+ *
+ * Returns this instance of `ClonedRepo`, for call chaining.
+ */
+ fun replaceBuildSrc(source: Path, ignoredFolder: Path?): ClonedRepo {
+ replaceFolder("buildSrc", source, ignoredFolder)
+ return this
+ }
+
+ /**
+ * Replaces the `config` folder in this cloned repository by the contents
+ * of the folder defined by the [source].
+ *
+ * [source] is expected to be another `config` folder.
+ *
+ * The original `config` folder, if it exists in this cloned repo, is renamed
+ * to `config-original`.
+ *
+ * Optionally, takes an [ignoredFolder] which will be excluded from the [source] paths
+ * when copying.
+ *
+ * Returns this instance of `ClonedRepo`, for call chaining.
+ */
+ fun replaceConfig(source: Path, ignoredFolder: Path?): ClonedRepo {
+ replaceFolder("config", source, ignoredFolder)
+ return this
+ }
+
+ private fun replaceFolder(folderName: String, source: Path, ignoredFolder: Path?) {
+ val folder = location.resolve(folderName)
+ val rawFolder = folder.toFile()
+ if (rawFolder.exists() && rawFolder.isDirectory) {
+ val toRenameInto = location.resolve(folderName + "-original")
+ println("Renaming ${folder.toAbsolutePath()} into ${toRenameInto.toAbsolutePath()}.")
+ rawFolder.renameTo(toRenameInto.toFile())
+ }
+ println(
+ "Copying the files from ${source.toAbsolutePath()} " +
+ "into ${folder.toAbsolutePath()}."
+ )
+ copyFolder(source, ignoredFolder, folder)
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private fun copyFolder(sourceFolder: Path, ignoredFolder: Path?, destinationFolder: Path) {
+ try {
+ copyRecursively(sourceFolder, ignoredFolder, destinationFolder)
+ } catch (e: Exception) {
+ throw IllegalStateException(
+ "Error copying folder `$sourceFolder` to `$destinationFolder`.", e
+ )
+ }
+ }
+
+ private fun copyRecursively(sourceFolder: Path, ignoredFolder: Path?, destinationFolder: Path) {
+ fun Path.isIgnored(): Boolean = ignoredFolder
+ ?.let { toAbsolutePath().startsWith(it.toAbsolutePath()) }
+ ?: false
+
+ val flattenedTree = Files.walk(sourceFolder).filter { it.isIgnored().not() }
+ val filesToDestinations = flattenedTree.map { file ->
+ val destination = destinationFolder.resolve(sourceFolder.relativize(file))
+ file to destination
+ }
+
+ val directories = filesToDestinations.filter { Files.isDirectory(it.first) }
+ directories.forEach { Files.createDirectories(it.second) }
+
+ val files = filesToDestinations.filter { Files.isDirectory(it.first).not() }
+ files.forEach { Files.copy(it.first, it.second) }
+ }
+}
+
+/**
+ * Spine repositories at GitHub.
+ *
+ * The list is expected to grow over time.
+ */
+object SpineRepos {
+
+ const val libsOrg: String = "https://github.com/SpineEventEngine/"
+ const val examplesOrg: String = "https://github.com/spine-examples/"
+
+ val base: URI = library("base")
+ val baseTypes: URI = library("base-types")
+ val coreJava: URI = library("core-java")
+ val web: URI = library("web")
+
+ private fun library(repo: String) = URI(libsOrg + repo)
+ private fun example(repo: String) = URI(examplesOrg + repo)
+}
+
+/**
+ * A name of a Git branch.
+ */
+data class Branch(val name: String)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt
new file mode 100644
index 0000000..2ef4bd8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+import io.spine.internal.gradle.publish.SpinePublishing
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.kotlin.dsl.findByType
+import org.gradle.kotlin.dsl.getByType
+
+/**
+ * This file contains extension methods and properties for the Gradle `Project`.
+ */
+
+/**
+ * Obtains the Java plugin extension of the project.
+ */
+val Project.javaPluginExtension: JavaPluginExtension
+ get() = extensions.getByType()
+
+/**
+ * Obtains source set container of the Java project.
+ */
+val Project.sourceSets: SourceSetContainer
+ get() = javaPluginExtension.sourceSets
+
+/**
+ * Applies the specified Gradle plugin to this project by the plugin [class][cls].
+ */
+fun Project.applyPlugin(cls: Class>) {
+ this.apply {
+ plugin(cls)
+ }
+}
+
+/**
+ * Finds the task of type `T` in this project by the task name.
+ *
+ * The task must be present. Also, a caller is responsible for using the proper value of
+ * the generic parameter `T`.
+ */
+@Suppress("UNCHECKED_CAST") /* See the method docs. */
+fun Project.findTask(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
+}
+
+/**
+ * Obtains Maven artifact ID of this [Project].
+ *
+ * The method checks if [SpinePublishing] extension is configured upon this project. If yes,
+ * returns [SpinePublishing.artifactId] for the project. Otherwise, a project's name is returned.
+ */
+val Project.artifactId: String
+ get() {
+
+ // Publishing of a project can be configured either from the project itself or
+ // from its root project. This is why it is required to check both places.
+
+ val spinePublishing = extensions.findByType()
+ ?: rootProject.extensions.findByType()
+
+ val artifactId = spinePublishing?.artifactId(this)
+ return artifactId ?: name
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt
new file mode 100644
index 0000000..b713a3b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+import org.gradle.api.GradleException
+
+/**
+ * A name of a repository.
+ */
+class RepoSlug(val value: String) {
+
+ companion object {
+
+ /**
+ * The name of the environment variable containing the repository slug, for which
+ * the Gradle build is performed.
+ */
+ private const val environmentVariable = "REPO_SLUG"
+
+ /**
+ * Reads `REPO_SLUG` environment variable and returns its value.
+ *
+ * In case it is not set, a [GradleException] is thrown.
+ */
+ fun fromVar(): RepoSlug {
+ val envValue = System.getenv(environmentVariable)
+ if (envValue.isNullOrEmpty()) {
+ throw GradleException("`REPO_SLUG` environment variable is not set.")
+ }
+ return RepoSlug(envValue)
+ }
+ }
+
+ override fun toString(): String = value
+
+ /**
+ * Returns the GitHub URL to the project repository.
+ */
+ fun gitHost(): String {
+ return "git@github.com-publish:${value}.git"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt
new file mode 100644
index 0000000..6d04ae7
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("TooManyFunctions") // Deprecated functions will be kept for a while.
+
+package io.spine.internal.gradle
+
+import io.spine.internal.gradle.publish.CloudRepo
+import io.spine.internal.gradle.publish.PublishingRepos
+import io.spine.internal.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/internal/gradle/RunBuild.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunBuild.kt
new file mode 100644
index 0000000..a39ffed
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunBuild.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+/**
+ * Runs the `build` task via Gradle Wrapper.
+ */
+open class RunBuild : RunGradle() {
+
+ init {
+ task("build")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt
new file mode 100644
index 0000000..926b237
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+import java.io.File
+import java.io.FileOutputStream
+import java.util.concurrent.TimeUnit
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.TaskAction
+import org.gradle.internal.os.OperatingSystem
+
+/**
+ * A Gradle task which runs another Gradle build.
+ *
+ * Launches Gradle wrapper under a given [directory] with the specified [taskNames] names.
+ * The `clean` task is also run if current build includes a `clean` task.
+ *
+ * The build writes verbose log into `$directory/build/debug-out.txt`.
+ * The error output is written into `$directory/build/error-out.txt`.
+ */
+@Suppress("unused")
+open class RunGradle : DefaultTask() {
+
+ companion object {
+
+ /**
+ * Default Gradle build timeout.
+ */
+ private const val BUILD_TIMEOUT_MINUTES: Long = 10
+ }
+
+ /**
+ * Path to the directory which contains a Gradle wrapper script.
+ */
+ @Internal
+ lateinit var directory: String
+
+ /**
+ * The names of the tasks to be passed to the Gradle Wrapper script.
+ */
+ private lateinit var taskNames: List
+
+ /**
+ * For how many minutes to wait for the Gradle build to complete.
+ */
+ @Internal
+ var maxDurationMins: Long = BUILD_TIMEOUT_MINUTES
+
+ /**
+ * Names of Gradle properties to copy into the launched build.
+ *
+ * The properties are looked up in the root project. If a property is not found, it is ignored.
+ *
+ * See [Gradle doc](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties)
+ * for more info about Gradle properties.
+ */
+ @Internal
+ var includeGradleProperties: MutableSet = mutableSetOf()
+
+ /**
+ * Specifies task names to be passed to the Gradle Wrapper script.
+ */
+ fun task(vararg tasks: String) {
+ taskNames = tasks.asList()
+ }
+
+ /**
+ * Sets the maximum time to wait until the build completion in minutes
+ * and specifies task names to be passed to the Gradle Wrapper script.
+ */
+ fun task(maxDurationMins: Long, vararg tasks: String) {
+ taskNames = tasks.asList()
+ this.maxDurationMins = maxDurationMins
+ }
+
+ @TaskAction
+ private fun execute() {
+ // Ensure build error output log.
+ // Since we're executing this task in another process, we redirect error output to
+ // the file under the `_out` directory. Using the `build` directory for this purpose
+ // proved to cause problems under Windows when executing the `clean` command, which
+ // fails because another process holds files.
+ val buildDir = File(directory, "_out")
+ if (!buildDir.exists()) {
+ buildDir.mkdir()
+ }
+ val errorOut = File(buildDir, "error-out.txt")
+ errorOut.truncate()
+ val debugOut = File(buildDir, "debug-out.txt")
+ debugOut.truncate()
+
+ val command = buildCommand()
+ val process = startProcess(command, errorOut, debugOut)
+
+ /* The timeout is set because of Gradle process execution under Windows.
+ See the following locations for details:
+ https://github.com/gradle/gradle/pull/8467#issuecomment-498374289
+ https://github.com/gradle/gradle/issues/3987
+ https://discuss.gradle.org/t/weirdness-in-gradle-exec-on-windows/13660/6
+ */
+ val completed = process.waitFor(maxDurationMins, TimeUnit.MINUTES)
+ val exitCode = process.exitValue()
+ if (!completed || exitCode != 0) {
+ val errorOutExists = errorOut.exists()
+ if (errorOutExists) {
+ logger.error(errorOut.readText())
+ }
+ throw GradleException("Child build process FAILED." +
+ " Exit code: $exitCode." +
+ if (errorOutExists) " See $errorOut for details."
+ else " $errorOut file was not created."
+ )
+ }
+ }
+
+ private fun buildCommand(): List {
+ val script = buildScript()
+ val command = mutableListOf()
+ command.add("${project.rootDir}/$script")
+ val shouldClean = project.gradle
+ .taskGraph
+ .hasTask(":clean")
+ if (shouldClean) {
+ command.add("clean")
+ }
+ command.addAll(taskNames)
+ command.add("--console=plain")
+ command.add("--debug")
+ command.add("--stacktrace")
+ command.add("--no-daemon")
+ addProperties(command)
+ return command
+ }
+
+ private fun addProperties(command: MutableList) {
+ val rootProject = project.rootProject
+ includeGradleProperties
+ .filter { rootProject.hasProperty(it) }
+ .map { name -> name to rootProject.property(name).toString() }
+ .forEach { (name, value) -> command.add("-P$name=$value") }
+ }
+
+ private fun buildScript(): String {
+ val runsOnWindows = OperatingSystem.current().isWindows
+ return if (runsOnWindows) "gradlew.bat" else "gradlew"
+ }
+
+ private fun startProcess(command: List, errorOut: File, debugOut: File) =
+ ProcessBuilder()
+ .command(command)
+ .directory(project.file(directory))
+ .redirectError(errorOut)
+ .redirectOutput(debugOut)
+ .start()
+}
+
+private fun File.truncate() {
+ val stream = FileOutputStream(this)
+ stream.use {
+ it.channel.truncate(0)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt
new file mode 100644
index 0000000..6594a2a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+import java.io.File
+import java.io.InputStream
+import java.io.StringWriter
+import java.lang.ProcessBuilder.Redirect.PIPE
+import java.util.*
+
+/**
+ * Utilities for working with processes from Gradle code.
+ */
+@Suppress("unused")
+private const val ABOUT = ""
+
+/**
+ * Executor of CLI commands.
+ *
+ * Uses the passed [workingFolder] as the directory in which the commands are executed.
+ */
+class Cli(private val workingFolder: File) {
+
+ /**
+ * Executes the given terminal command and retrieves the command output.
+ *
+ *
{@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.
+ *
+ * @param command the command to execute
+ * @return the command line output
+ * @throws IllegalStateException upon an execution error
+ */
+ fun execute(vararg command: String): String {
+ val outWriter = StringWriter()
+ val errWriter = StringWriter()
+
+ val process = ProcessBuilder(*command).apply {
+ directory(workingFolder)
+ redirectOutput(PIPE)
+ redirectError(PIPE)
+ }.start()
+
+ val exitCode = process.run {
+ inputStream!!.pourTo(outWriter)
+ errorStream!!.pourTo(errWriter)
+ waitFor()
+ }
+
+ if (exitCode == 0) {
+ return outWriter.toString()
+ } else {
+ val commandLine = command.joinToString(" ")
+ val nl = System.lineSeparator()
+ val errorMsg = "Command `$commandLine` finished with exit code $exitCode:" +
+ "$nl$errWriter" +
+ "$nl$outWriter."
+ throw IllegalStateException(errorMsg)
+ }
+ }
+}
+
+/**
+ * Asynchronously reads all lines from this [InputStream] and appends them
+ * to the passed [StringWriter].
+ */
+fun InputStream.pourTo(dest: StringWriter) {
+ Thread {
+ val sc = Scanner(this)
+ while (sc.hasNextLine()) {
+ dest.append(sc.nextLine())
+ }
+ }.start()
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt
new file mode 100644
index 0000000..9d5253b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+/**
+ * Returns `true` if the version of a project contains `snapshot` (in any case),
+ * `false` otherwise.
+ */
+fun String.isSnapshot(): Boolean {
+ return contains("snapshot", ignoreCase = true)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt
new file mode 100644
index 0000000..24b587f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle
+
+import kotlin.reflect.KClass
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.kotlin.dsl.named
+import org.gradle.kotlin.dsl.register
+
+/**
+ * A name and a type of a Gradle task.
+ */
+internal class TaskName(
+ val value: String,
+ val clazz: KClass,
+) {
+ companion object {
+
+ fun of(name: String) = TaskName(name, Task::class)
+
+ fun of(name: String, clazz: KClass) = TaskName(name, clazz)
+ }
+}
+
+/**
+ * Locates [the task][TaskName] in this [TaskContainer].
+ */
+internal fun TaskContainer.named(name: TaskName) = named(name.value, name.clazz)
+
+/**
+ * Registers [the task][TaskName] in this [TaskContainer].
+ */
+internal fun TaskContainer.register(name: TaskName, init: T.() -> Unit) =
+ register(name.value, name.clazz, init)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt
new file mode 100644
index 0000000..3dc7ef2
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.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/internal/gradle/base/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/base/Tasks.kt
new file mode 100644
index 0000000..6fb9fbb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/base/Tasks.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.base
+
+import org.gradle.api.Task
+import org.gradle.api.tasks.Delete
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.named
+
+/**
+ * Locates `clean` task in this [TaskContainer].
+ *
+ * The task deletes the build directory and everything in it,
+ * i.e. the path specified by the `Project.getBuildDir()` project property.
+ *
+ * @see
+ * Tasks | The Base Plugin
+ */
+val TaskContainer.clean: TaskProvider
+ get() = named("clean")
+
+/**
+ * Locates `check` task in this [TaskContainer].
+ *
+ * This is a lifecycle task that performs no action itself.
+ *
+ * Plugins and build authors should attach their verification tasks,
+ * such as ones that run tests, to this lifecycle task using `check.dependsOn(myTask)`.
+ *
+ * @see
+ * Tasks | The Base Plugin
+ */
+val TaskContainer.check: TaskProvider
+ get() = named("check")
+
+/**
+ * Locates `assemble` task in this [TaskContainer].
+ *
+ * This is a lifecycle task that performs no action itself.
+ *
+ * Plugins and build authors should attach their assembling tasks that produce distributions and
+ * other consumable artifacts to this lifecycle task using `assemble.dependsOn(myTask)`.
+ *
+ * @see
+ * Tasks | The Base Plugin
+ */
+val TaskContainer.assemble: TaskProvider
+ get() = named("assemble")
+
+/**
+ * Locates `build` task in this [TaskContainer].
+ *
+ * Intended to build everything, including running all tests, producing the production artifacts
+ * and generating documentation. One will probably rarely attach concrete tasks directly
+ * to `build` as [assemble][io.spine.internal.gradle.base.assemble] and
+ * [check][io.spine.internal.gradle.base.check] are typically more appropriate.
+ *
+ * @see
+ * Tasks | The Base Plugin
+ */
+val TaskContainer.build: TaskProvider
+ get() = named("build")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt
new file mode 100644
index 0000000..42d1f62
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.checkstyle
+
+import io.spine.internal.dependency.CheckStyle
+import org.gradle.api.Project
+import org.gradle.api.plugins.quality.Checkstyle
+import org.gradle.api.plugins.quality.CheckstyleExtension
+import org.gradle.api.plugins.quality.CheckstylePlugin
+import org.gradle.kotlin.dsl.the
+
+/**
+ * Configures the Checkstyle plugin.
+ *
+ * Usage:
+ * ```
+ * CheckStyleConfig.applyTo(project)
+ * ```
+ *
+ * Please note, the checks of the `test` sources are disabled.
+ *
+ * Also, this type is named in double-camel-case to avoid re-declaration due to a clash
+ * with some Gradle-provided types.
+ */
+@Suppress("unused")
+object CheckStyleConfig {
+
+ /**
+ * Applies the configuration to the passed [project].
+ */
+ fun applyTo(project: Project) {
+ project.apply {
+ plugin(CheckstylePlugin::class.java)
+ }
+
+ val configDir = project.rootDir.resolve("config/quality/")
+
+ with(project.the()) {
+ toolVersion = CheckStyle.version
+ configDirectory.set(configDir)
+ }
+
+ project.afterEvaluate {
+ // Disables checking the test sources.
+ val checkstyleTest = project.tasks.findByName("checkstyleTest") as Checkstyle
+ checkstyleTest.enabled = false
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt
new file mode 100644
index 0000000..ed2eff7
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart
+
+import org.gradle.api.Project
+import org.gradle.api.tasks.Exec
+
+/**
+ * Provides access to the current [DartEnvironment] and shortcuts for running `pub` tool.
+ */
+open class DartContext(dartEnv: DartEnvironment, internal val project: Project)
+ : DartEnvironment by dartEnv
+{
+ /**
+ * Executes `pub` command in this [Exec] task.
+ *
+ * The Dart ecosystem uses packages to manage shared software such as libraries and tools.
+ * To get or publish Dart packages, the `pub` package manager is to be used.
+ */
+ fun Exec.pub(vararg args: Any) = commandLine(pubExecutable, *args)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt
new file mode 100644
index 0000000..160e1ff
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart
+
+import java.io.File
+import org.apache.tools.ant.taskdefs.condition.Os
+
+/**
+ * Describes the environment in which Dart code is assembled and processed during the build.
+ *
+ * Consists of two parts describing:
+ *
+ * 1. The module itself.
+ * 2. Tools and their input/output files.
+ */
+interface DartEnvironment {
+
+ /*
+ * A module itself
+ ******************/
+
+ /**
+ * Module's root catalog.
+ */
+ val projectDir: File
+
+ /**
+ * Module's name.
+ */
+ val projectName: String
+
+ /**
+ * A directory which all artifacts are generated into.
+ *
+ * Default value: "$projectDir/build".
+ */
+ val buildDir: File
+ get() = projectDir.resolve("build")
+
+ /**
+ * A directory where artifacts for further publishing would be prepared.
+ *
+ * Default value: "$buildDir/pub/publication/$projectName".
+ */
+ val publicationDir: File
+ get() = buildDir
+ .resolve("pub")
+ .resolve("publication")
+ .resolve(projectName)
+
+ /**
+ * A directory which contains integration test Dart sources.
+ *
+ * Default value: "$projectDir/integration-test".
+ */
+ val integrationTestDir: File
+ get() = projectDir.resolve("integration-test")
+
+ /*
+ * Tools and their input/output files
+ *************************************/
+
+ /**
+ * Name of an executable for running `pub` tool.
+ *
+ * Default value:
+ *
+ * 1. "pub.bat" for Windows.
+ * 2. "pub" for other Oss.
+ */
+ val pubExecutable: String
+ get() = if (isWindows()) "pub.bat" else "pub"
+
+ /**
+ * Dart module's metadata file.
+ *
+ * Every pub package needs some metadata so it can specify its dependencies. Pub packages that
+ * are shared with others also need to provide some other information so users can discover
+ * them. All of this metadata goes in the package’s `pubspec`.
+ *
+ * Default value: "$projectDir/pubspec.yaml".
+ *
+ * See [The pubspec file | Dart](https://dart.dev/tools/pub/pubspec)
+ */
+ val pubSpec: File
+ get() = projectDir.resolve("pubspec.yaml")
+
+ /**
+ * Module dependencies' index that maps resolved package names to location URIs.
+ *
+ * By default, pub creates a [packageConfig] file in the `.dart_tool/` directory for this.
+ * Before the [packageConfig], pub used to create this [packageIndex] file in the root
+ * directory.
+ *
+ * As for Dart 2.14, `pub` still updates the deprecated file for backwards compatibility.
+ *
+ * Default value: "$projectDir/.packages".
+ */
+ val packageIndex: File
+ get() = projectDir.resolve(".packages")
+
+ /**
+ * Module dependencies' index that maps resolved package names to location URIs.
+ *
+ * Default value: "$projectDir/.dart_tool/package_config.json".
+ */
+ val packageConfig: File
+ get() = projectDir
+ .resolve(".dart_tool")
+ .resolve("package_config.json")
+}
+
+/**
+ * Allows overriding [DartEnvironment]'s defaults.
+ *
+ * Please note, not all properties of the environment can be overridden. Properties that describe
+ * `pub` tool's input/output files can NOT be overridden because `pub` itself doesn't allow to
+ * specify them for its execution.
+ *
+ * The next properties could not be overridden:
+ *
+ * 1. [DartEnvironment.pubSpec].
+ * 2. [DartEnvironment.packageIndex].
+ * 3. [DartEnvironment.packageConfig].
+ */
+class ConfigurableDartEnvironment(initialEnv: DartEnvironment)
+ : DartEnvironment by initialEnv
+{
+ /*
+ * A module itself
+ ******************/
+
+ override var projectDir = initialEnv.projectDir
+ override var projectName = initialEnv.projectName
+ override var buildDir = initialEnv.buildDir
+ override var publicationDir = initialEnv.publicationDir
+ override var integrationTestDir = initialEnv.integrationTestDir
+
+ /*
+ * Tools and their input/output files
+ *************************************/
+
+ override var pubExecutable = initialEnv.pubExecutable
+}
+
+internal fun isWindows(): Boolean = Os.isFamily(Os.FAMILY_WINDOWS)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt
new file mode 100644
index 0000000..d4bd4d8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart
+
+import io.spine.internal.gradle.dart.task.DartTasks
+import io.spine.internal.gradle.dart.plugin.DartPlugins
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.findByType
+
+/**
+ * Configures [DartExtension] that facilitates configuration of Gradle tasks and plugins
+ * to build Dart projects.
+ *
+ * The whole structure of the extension looks as follows:
+ *
+ * ```
+ * dart {
+ * environment {
+ * // ...
+ * }
+ * plugins {
+ * // ...
+ * }
+ * tasks {
+ * // ...
+ * }
+ * }
+ * ```
+ *
+ * ### Environment
+ *
+ * One of the main features of this extension is [DartEnvironment]. Environment describes a module
+ * itself and used tools with their input/output files.
+ *
+ * The extension is shipped with a pre-configured environment. So, no pre-configuration is required.
+ * Most properties in [DartEnvironment] have calculated defaults right in the interface.
+ * Only two properties need explicit override.
+ *
+ * The extension defines them as follows:
+ *
+ * 1. [DartEnvironment.projectDir] –> `project.projectDir`.
+ * 2. [DartEnvironment.projectName] —> `project.name`.
+ *
+ * There are two ways to modify the environment:
+ *
+ * 1. Modify [DartEnvironment] interface directly. Go with this option when it is a global change
+ * that should affect all projects which use this extension.
+ * 2. Use [DartExtension.environment] scope — for temporary and custom overridings.
+ *
+ * An example of a property overriding:
+ *
+ * ```
+ * dart {
+ * environment {
+ * integrationTestDir = projectDir.resolve("tests")
+ * }
+ * }
+ * ```
+ *
+ * Please note, environment should be set up firstly to have the effect on the parts
+ * of the extension that use it.
+ *
+ * ### Tasks and Plugins
+ *
+ * The spirit of tasks configuration in this extension is extracting the code that defines and
+ * registers tasks into extension functions upon `DartTasks` in `buildSrc`. Those extensions should
+ * be named after a task it registers or a task group if several tasks are registered at once.
+ * Then this extension is called in a project's `build.gradle.kts`.
+ *
+ * `DartTasks` and `DartPlugins` scopes extend [DartContext] which provides access
+ * to the current [DartEnvironment] and shortcuts for running `pub` tool.
+ *
+ * Below is the simplest example of how to create a primitive `printPubVersion` task.
+ *
+ * Firstly, a corresponding extension function should be defined in `buildSrc`:
+ *
+ * ```
+ * fun DartTasks.printPubVersion() =
+ * register("printPubVersion") {
+ * pub("--version")
+ * }
+ * ```
+ *
+ * Secondly, in a project's `build.gradle.kts` this extension is called:
+ *
+ * ```
+ * dart {
+ * tasks {
+ * printPubVersion()
+ * }
+ * }
+ * ```
+ *
+ * An extension function is not restricted to register exactly one task. If several tasks can
+ * be grouped into a logical bunch, they should be registered together:
+ *
+ * ```
+ * fun DartTasks.build() {
+ * assembleDart()
+ * testDart()
+ * generateCoverageReport()
+ * }
+ *
+ * private fun DartTasks.assembleDart() = ...
+ *
+ * private fun DartTasks.testDart() = ...
+ *
+ * private fun DartTasks.generateCoverageReport() = ...
+ * ```
+ *
+ * This section is mostly dedicated to tasks. But tasks and plugins are configured
+ * in a very similar way. So, everything above is also applicable to plugins. More detailed
+ * guides can be found in docs to `DartTasks` and `DartPlugins`.
+ *
+ * @see [ConfigurableDartEnvironment]
+ * @see [DartTasks]
+ * @see [DartPlugins]
+ */
+fun Project.dart(configuration: DartExtension.() -> Unit) {
+ extensions.run {
+ configuration.invoke(
+ findByType() ?: create("dartExtension", project)
+ )
+ }
+}
+
+/**
+ * Scope for performing Dart-related configuration.
+ *
+ * @see [dart]
+ */
+open class DartExtension(project: Project) {
+
+ private val environment = ConfigurableDartEnvironment(
+ object : DartEnvironment {
+ override val projectDir = project.projectDir
+ override val projectName = project.name
+ }
+ )
+
+ private val tasks = DartTasks(environment, project)
+ private val plugins = DartPlugins(environment, project)
+
+ /**
+ * Overrides default values of [DartEnvironment].
+ *
+ * Please note, environment should be set up firstly to have the effect on the parts
+ * of the extension that use it.
+ */
+ fun environment(overridings: ConfigurableDartEnvironment.() -> Unit) =
+ environment.run(overridings)
+
+ /**
+ * Configures [Dart-related plugins][DartPlugins].
+ */
+ fun plugins(configurations: DartPlugins.() -> Unit) = plugins.run(configurations)
+
+ /**
+ * Configures [Dart-related tasks][DartTasks].
+ */
+ fun tasks(configurations: DartTasks.() -> Unit) = tasks.run(configurations)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt
new file mode 100644
index 0000000..43d8bb0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart.plugin
+
+import io.spine.internal.gradle.dart.DartContext
+import io.spine.internal.gradle.dart.DartEnvironment
+import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionContainer
+import org.gradle.api.plugins.PluginContainer
+import org.gradle.api.tasks.TaskContainer
+
+/**
+ * A scope for applying and configuring Dart-related plugins.
+ *
+ * The scope extends [DartContext] and provides shortcuts for key project's containers:
+ *
+ * 1. [plugins].
+ * 2. [extensions].
+ * 3. [tasks].
+ *
+ * Let's imagine one wants to apply and configure `FooBar` plugin. To do that, several steps
+ * should be completed:
+ *
+ * 1. Declare the corresponding extension function upon [DartContext] named after the plugin.
+ * 2. Apply and configure the plugin inside that function.
+ * 3. Call the resulted extension in your `build.gradle.kts` file.
+ *
+ * Here's an example of `dart/plugin/FooBar.kt`:
+ *
+ * ```
+ * fun DartPlugins.fooBar() {
+ * plugins.apply("com.fooBar")
+ * extensions.configure {
+ * // ...
+ * }
+ * }
+ * ```
+ *
+ * And here's how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.dart.dart
+ * import io.spine.internal.gradle.dart.plugins.fooBar
+ *
+ * // ...
+ *
+ * dart {
+ * plugins {
+ * fooBar()
+ * }
+ * }
+ * ```
+ */
+class DartPlugins(dartEnv: DartEnvironment, project: Project) : DartContext(dartEnv, project) {
+
+ internal val plugins = project.plugins
+ internal val extensions = project.extensions
+ internal val tasks = project.tasks
+
+ internal fun plugins(configurations: PluginContainer.() -> Unit) =
+ plugins.run(configurations)
+
+ internal fun extensions(configurations: ExtensionContainer.() -> Unit) =
+ extensions.run(configurations)
+
+ internal fun tasks(configurations: TaskContainer.() -> Unit) =
+ tasks.run(configurations)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt
new file mode 100644
index 0000000..65f6fb2
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart.plugin
+
+import com.google.protobuf.gradle.id
+import com.google.protobuf.gradle.ProtobufExtension
+import com.google.protobuf.gradle.remove
+import io.spine.internal.dependency.Protobuf
+
+/**
+ * Applies `protobuf` plugin and configures `GenerateProtoTask` to work with a Dart module.
+ *
+ * @see DartPlugins
+ */
+fun DartPlugins.protobuf() {
+
+ plugins.apply(Protobuf.GradlePlugin.id)
+
+ val protobufExtension = project.extensions.getByType(ProtobufExtension::class.java)
+ protobufExtension.apply {
+ generateProtoTasks.all().configureEach {
+ apply {
+ plugins { id("dart") }
+ builtins { remove("java") }
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt
new file mode 100644
index 0000000..55ec5ca
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart.task
+
+import io.spine.internal.gradle.TaskName
+import io.spine.internal.gradle.base.assemble
+import io.spine.internal.gradle.base.check
+import io.spine.internal.gradle.base.clean
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import org.gradle.api.tasks.Delete
+import org.gradle.api.tasks.Exec
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers tasks for building Dart projects.
+ *
+ * List of tasks to be created:
+ *
+ * 1. [TaskContainer.cleanPackageIndex].
+ * 2. [TaskContainer.resolveDependencies].
+ * 3. [TaskContainer.testDart].
+ *
+ * An example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.dart.dart
+ * import io.spine.internal.gradle.dart.task.build
+ *
+ * // ...
+ *
+ * dart {
+ * tasks {
+ * build()
+ * }
+ * }
+ * ```
+ *
+ * @param configuration any additional configuration related to the module's building.
+ */
+fun DartTasks.build(configuration: DartTasks.() -> Unit = {}) {
+
+ cleanPackageIndex().also {
+ clean.configure {
+ dependsOn(it)
+ }
+ }
+ resolveDependencies().also {
+ assemble.configure {
+ dependsOn(it)
+ }
+ }
+ testDart().also {
+ check.configure {
+ dependsOn(it)
+ }
+ }
+
+ configuration()
+}
+
+private val resolveDependenciesName = TaskName.of("resolveDependencies", Exec::class)
+
+/**
+ * Locates `resolveDependencies` task in this [TaskContainer].
+ *
+ * The task fetches dependencies declared via `pubspec.yaml` using `pub get` command.
+ */
+val TaskContainer.resolveDependencies: TaskProvider
+ get() = named(resolveDependenciesName)
+
+private fun DartTasks.resolveDependencies(): TaskProvider =
+ register(resolveDependenciesName) {
+
+ description = "Fetches dependencies declared via `pubspec.yaml`."
+ group = DartTasks.Group.build
+
+ mustRunAfter(cleanPackageIndex)
+
+ inputs.file(pubSpec)
+ outputs.file(packageIndex)
+
+ pub("get")
+ }
+
+private val cleanPackageIndexName = TaskName.of("cleanPackageIndex", Delete::class)
+
+/**
+ * Locates `cleanPackageIndex` task in this [TaskContainer].
+ *
+ * The task deletes the resolved module dependencies' index.
+ *
+ * The standard configuration file that contains index is `package_config.json`. For backwards
+ * compatability `pub` still updates the deprecated `.packages` file. The task deletes both files.
+ */
+val TaskContainer.cleanPackageIndex: TaskProvider
+ get() = named(cleanPackageIndexName)
+
+private fun DartTasks.cleanPackageIndex(): TaskProvider =
+ register(cleanPackageIndexName) {
+
+ description = "Deletes the resolved `.packages` and `package_config.json` files."
+ group = DartTasks.Group.build
+
+ delete(
+ packageIndex,
+ packageConfig
+ )
+ }
+
+private val testDartName = TaskName.of("testDart", Exec::class)
+
+/**
+ * Locates `testDart` task in this [TaskContainer].
+ *
+ * The task runs Dart tests declared in the `./test` directory.
+ */
+val TaskContainer.testDart: TaskProvider
+ get() = named(testDartName)
+
+private fun DartTasks.testDart(): TaskProvider =
+ register(testDartName) {
+
+ description = "Runs Dart tests declared in the `./test` directory."
+ group = DartTasks.Group.build
+
+ dependsOn(resolveDependencies)
+
+ pub("run", "test")
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt
new file mode 100644
index 0000000..50574de
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart.task
+
+import io.spine.internal.gradle.dart.DartContext
+import io.spine.internal.gradle.dart.DartEnvironment
+import org.gradle.api.Project
+import org.gradle.api.tasks.TaskContainer
+
+/**
+ * A scope for registering and configuring Dart-related tasks.
+ *
+ * The scope provides:
+ *
+ * 1. Access to the current [DartContext].
+ * 2. Project's [TaskContainer].
+ * 3. Default task groups.
+ *
+ * Supposing, one needs to create a new task that would participate in building. Let the task name
+ * be `testDart`. To do that, several steps should be completed:
+ *
+ * 1. Define the task name and type using [TaskName][io.spine.internal.gradle.TaskName].
+ * 2. Create a public typed reference for the task upon [TaskContainer]. It would facilitate
+ * referencing to the new task, so that external tasks could depend on it. This reference
+ * should be documented.
+ * 3. Implement an extension upon [DartTasks] to register the task.
+ * 4. Call the resulted extension from `build.gradle.kts`.
+ *
+ * Here's an example of `testDart()` extension:
+ *
+ * ```
+ * import io.spine.internal.gradle.named
+ * import io.spine.internal.gradle.register
+ * import io.spine.internal.gradle.TaskName
+ * import org.gradle.api.Task
+ * import org.gradle.api.tasks.TaskContainer
+ * import org.gradle.api.tasks.Exec
+ *
+ * // ...
+ *
+ * private val testDartName = TaskName.of("testDart", Exec::class)
+ *
+ * /**
+ * * Locates `testDart` task in this [TaskContainer].
+ * *
+ * * The task runs Dart tests declared in the `./test` directory.
+ * */
+ * val TaskContainer.testDart: TaskProvider
+ * get() = named(testDartName)
+ *
+ * fun DartTasks.testDart() =
+ * register(testDartName) {
+ *
+ * description = "Runs Dart tests declared in the `./test` directory."
+ * group = DartTasks.Group.build
+ *
+ * // ...
+ * }
+ * ```
+ *
+ * And here's how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.dart.dart
+ * import io.spine.internal.gradle.dart.task.testDart
+ *
+ * // ...
+ *
+ * dart {
+ * tasks {
+ * testDart()
+ * }
+ * }
+ * ```
+ *
+ * Declaring typed references upon [TaskContainer] is optional. But it is highly encouraged
+ * to reference other tasks by such extensions instead of hard-typed string values.
+ */
+class DartTasks(dartEnv: DartEnvironment, project: Project)
+ : DartContext(dartEnv, project), TaskContainer by project.tasks
+{
+ /**
+ * Default task groups for tasks that participate in building a Dart module.
+ *
+ * @see [org.gradle.api.Task.getGroup]
+ */
+ internal object Group {
+ const val build = "Dart/Build"
+ const val publish = "Dart/Publish"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt
new file mode 100644
index 0000000..69c691a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart.task
+
+import io.spine.internal.gradle.TaskName
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import org.gradle.api.tasks.Exec
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+private val integrationTestName = TaskName.of("integrationTest", Exec::class)
+
+/**
+ * Locates `integrationTest` task in this [TaskContainer].
+ *
+ * The task runs integration tests of the `spine-dart` library against a sample
+ * Spine-based application. The tests are run in Chrome browser because they use `WebFirebaseClient`
+ * which only works in web environment.
+ *
+ * A sample Spine-based application is run from the `test-app` module before integration
+ * tests start and is stopped as the tests complete.
+ */
+val TaskContainer.integrationTest: TaskProvider
+ get() = named(integrationTestName)
+
+/**
+ * Registers [TaskContainer.integrationTest] task.
+ *
+ * Please note, this task depends on [build] tasks. Therefore, building tasks should be applied in
+ * the first place.
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.dart.dart
+ * import io.spine.internal.gradle.task.build
+ * import io.spine.internal.gradle.task.integrationTest
+ *
+ * // ...
+ *
+ * dart {
+ * tasks {
+ * build()
+ * integrationTest()
+ * }
+ * }
+ * ```
+ */
+@Suppress("unused")
+fun DartTasks.integrationTest() =
+ register(integrationTestName) {
+
+ dependsOn(
+ resolveDependencies,
+ ":test-app:appBeforeIntegrationTest"
+ )
+
+ pub(
+ "run",
+ "test",
+ integrationTestDir,
+ "-p",
+ "chrome"
+ )
+
+ finalizedBy(":test-app:appAfterIntegrationTest")
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt
new file mode 100644
index 0000000..08b7b00
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.dart.task
+
+import io.spine.internal.gradle.TaskName
+import io.spine.internal.gradle.base.assemble
+import io.spine.internal.gradle.publish.publish
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Exec
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers tasks for publishing Dart projects.
+ *
+ * Please note, this task group depends on [build] tasks. Therefore, building tasks should
+ * be applied in the first place.
+ *
+ * List of tasks to be created:
+ *
+ * 1. [TaskContainer.stagePubPublication].
+ * 2. [TaskContainer.activateLocally].
+ * 3. [TaskContainer.publishToPub].
+ *
+ * Usage example:
+ *
+ * ```
+ * import io.spine.internal.gradle.dart.dart
+ * import io.spine.internal.gradle.dart.task.build
+ * import io.spine.internal.gradle.dart.task.publish
+ *
+ * // ...
+ *
+ * dart {
+ * tasks {
+ * build()
+ * publish()
+ * }
+ * }
+ * ```
+ */
+fun DartTasks.publish() {
+
+ stagePubPublication()
+ activateLocally()
+
+ publishToPub().also {
+ publish.configure {
+ dependsOn(it)
+ }
+ }
+}
+
+private val stagePubPublicationName = TaskName.of("stagePubPublication", Copy::class)
+
+/**
+ * Locates `stagePubPublication` in this [TaskContainer].
+ *
+ * The task prepares the Dart package for Pub publication in the
+ * [publication directory][io.spine.internal.gradle.dart.DartEnvironment.publicationDir].
+ */
+val TaskContainer.stagePubPublication: TaskProvider
+ get() = named(stagePubPublicationName)
+
+private fun DartTasks.stagePubPublication(): TaskProvider =
+ register(stagePubPublicationName) {
+
+ description = "Prepares the Dart package for Pub publication."
+ group = DartTasks.Group.publish
+
+ dependsOn(assemble)
+
+ // Beside `.dart` sources itself, `pub` package manager conventions require:
+ // 1. README.md and CHANGELOG.md to build a page at `pub.dev/packages/;`.
+ // 2. `pubspec` file to fill out details about your package on the right side of your
+ // package’s page.
+ // 3. LICENSE file.
+
+ from(project.projectDir) {
+ include("**/*.dart", "pubspec.yaml", "**/*.md")
+ exclude("proto/", "generated/", "build/", "**/.*")
+ }
+ from("${project.rootDir}/LICENSE")
+ into(publicationDir)
+
+ doLast {
+ logger.debug("Pub publication is prepared in directory `{}`.", publicationDir)
+ }
+ }
+
+private val publishToPubName = TaskName.of("publishToPub", Exec::class)
+
+/**
+ * Locates `publishToPub` task in this [TaskContainer].
+ *
+ * The task publishes the prepared publication to Pub using `pub publish` command.
+ */
+val TaskContainer.publishToPub: TaskProvider
+ get() = named(publishToPubName)
+
+private fun DartTasks.publishToPub(): TaskProvider =
+ register(publishToPubName) {
+
+ description = "Publishes the prepared publication to Pub."
+ group = DartTasks.Group.publish
+
+ dependsOn(stagePubPublication)
+
+ val sayYes = "y".byteInputStream()
+ standardInput = sayYes
+
+ workingDir(publicationDir)
+
+ pub("publish", "--trace")
+ }
+
+private val activateLocallyName = TaskName.of("activateLocally", Exec::class)
+
+/**
+ * Locates `activateLocally` task in this [TaskContainer].
+ *
+ * Makes this package available in the command line as an executable.
+ *
+ * The `dart run` command supports running a Dart program — located in a file, in the current
+ * package, or in one of the dependencies of the current package - from the command line.
+ * To run a program from an arbitrary location, the package should be "activated".
+ *
+ * See [dart pub global | Dart](https://dart.dev/tools/pub/cmd/pub-global)
+ */
+val TaskContainer.activateLocally: TaskProvider
+ get() = named(activateLocallyName)
+
+private fun DartTasks.activateLocally(): TaskProvider =
+ register(activateLocallyName) {
+
+ description = "Activates this package locally."
+ group = DartTasks.Group.publish
+
+ dependsOn(stagePubPublication)
+
+ workingDir(publicationDir)
+ pub(
+ "global",
+ "activate",
+ "--source",
+ "path",
+ publicationDir,
+ "--trace"
+ )
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt
new file mode 100644
index 0000000..325d8e9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.fs
+
+import java.io.File
+import java.net.URI
+import java.nio.file.FileSystem
+import java.nio.file.Files.createTempDirectory
+import java.nio.file.LinkOption
+import java.nio.file.Path
+import java.nio.file.WatchEvent
+import java.nio.file.WatchKey
+import java.nio.file.WatchService
+
+/**
+ * A path to a temporary folder, which is not created until it is really used.
+ *
+ * After the first usage, the instances of this type delegate all calls to the internally
+ * created instance of [Path] created with [createTempDirectory].
+ */
+@Suppress("TooManyFunctions")
+class LazyTempPath(private val prefix: String) : Path {
+
+ private val delegate: Path by lazy { createTempDirectory(prefix) }
+
+ override fun compareTo(other: Path): Int = delegate.compareTo(other)
+
+ override fun iterator(): MutableIterator = delegate.iterator()
+
+ override fun register(
+ watcher: WatchService,
+ events: Array>,
+ vararg modifiers: WatchEvent.Modifier?
+ ): WatchKey = delegate.register(watcher, events, *modifiers)
+
+ override fun register(watcher: WatchService, vararg events: WatchEvent.Kind<*>?): WatchKey =
+ delegate.register(watcher, *events)
+
+ override fun getFileSystem(): FileSystem = delegate.fileSystem
+
+ override fun isAbsolute(): Boolean = delegate.isAbsolute
+
+ override fun getRoot(): Path = delegate.root
+
+ override fun getFileName(): Path = delegate.fileName
+
+ override fun getParent(): Path = delegate.parent
+
+ override fun getNameCount(): Int = delegate.nameCount
+
+ override fun getName(index: Int): Path = delegate.getName(index)
+
+ override fun subpath(beginIndex: Int, endIndex: Int): Path =
+ delegate.subpath(beginIndex, endIndex)
+
+ override fun startsWith(other: Path): Boolean = delegate.startsWith(other)
+
+ override fun startsWith(other: String): Boolean = delegate.startsWith(other)
+
+ override fun endsWith(other: Path): Boolean = delegate.endsWith(other)
+
+ override fun endsWith(other: String): Boolean = delegate.endsWith(other)
+
+ override fun normalize(): Path = delegate.normalize()
+
+ override fun resolve(other: Path): Path = delegate.resolve(other)
+
+ override fun resolve(other: String): Path = delegate.resolve(other)
+
+ override fun resolveSibling(other: Path): Path = delegate.resolveSibling(other)
+
+ override fun resolveSibling(other: String): Path = delegate.resolveSibling(other)
+
+ override fun relativize(other: Path): Path = delegate.relativize(other)
+
+ override fun toUri(): URI = delegate.toUri()
+
+ override fun toAbsolutePath(): Path = delegate.toAbsolutePath()
+
+ override fun toRealPath(vararg options: LinkOption?): Path = delegate.toRealPath(*options)
+
+ override fun toFile(): File = delegate.toFile()
+
+ override fun toString(): String = delegate.toString()
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/Branch.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/Branch.kt
new file mode 100644
index 0000000..63bae4e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/Branch.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.git
+
+/**
+ * Branch names.
+ */
+object Branch {
+
+ /**
+ * The default branch.
+ */
+ const val master = "master"
+
+ /**
+ * The branch used for publishing documentation to GitHub Pages.
+ */
+ const val documentation = "gh-pages"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/Repository.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/Repository.kt
new file mode 100644
index 0000000..229973d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/Repository.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.git
+
+import io.spine.internal.gradle.Cli
+import io.spine.internal.gradle.fs.LazyTempPath
+
+/**
+ * Interacts with a real Git repository.
+ *
+ * Clones the repository with the provided SSH URL in a temporal folder. Provides
+ * functionality to configure a user, checkout branches, commit changes and push them
+ * to the remote repository.
+ *
+ * It is assumed that before using this class an appropriate SSH key that has
+ * sufficient rights to perform described above operations was registered
+ * in `ssh-agent`.
+ *
+ * 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.
+ */
+class Repository private constructor(
+
+ /**
+ * The GitHub SSH URL to the underlying repository.
+ */
+ 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
+
+) : AutoCloseable {
+
+ /**
+ * Path to the temporal folder for a clone of the underlying repository.
+ */
+ val location = LazyTempPath("repoTemp")
+
+ /**
+ * Clones the repository with [the SSH url][sshUrl] into the [temporal folder][location].
+ */
+ private fun clone() {
+ repoExecute("git", "clone", sshUrl, ".")
+ }
+
+ /**
+ * Executes a command in the [location].
+ */
+ private fun repoExecute(vararg command: String): String =
+ Cli(location.toFile()).execute(*command)
+
+ /**
+ * Checks out the branch by its name.
+ */
+ fun checkout(branch: String) {
+ repoExecute("git", "checkout", branch)
+
+ currentBranch = branch
+ }
+
+ /**
+ * Configures the username and the email of the user.
+ *
+ * Overwrites `user.name` and `user.email` settings locally in [location] with
+ * values from [user]. These settings determine what ends up in author and
+ * committer fields of a commit.
+ */
+ fun configureUser(user: UserInfo) {
+ repoExecute("git", "config", "user.name", user.name)
+ repoExecute("git", "config", "user.email", user.email)
+
+ this.user = user
+ }
+
+ /**
+ * Stages all changes and commits with the provided message.
+ */
+ fun commitAllChanges(message: String) {
+ stageAllChanges()
+ commit(message)
+ }
+
+ private fun stageAllChanges() {
+ repoExecute("git", "add", "--all")
+ }
+
+ private fun commit(message: String) {
+ repoExecute(
+ "git",
+ "commit",
+ "--allow-empty",
+ "--message=${message}"
+ )
+ }
+
+ /**
+ * Pushes local repository to the remote.
+ */
+ fun push() {
+ repoExecute("git", "push")
+ }
+
+ override fun close() {
+ location.toFile().deleteRecursively()
+ }
+
+ 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.
+ *
+ * @throws IllegalArgumentException if SSH URL is an empty string.
+ */
+ fun of(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)
+ repo.clone()
+ repo.configureUser(user)
+
+ if (branch != Branch.master) {
+ repo.checkout(branch)
+ }
+
+ return repo
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/UserInfo.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/UserInfo.kt
new file mode 100644
index 0000000..898eb5b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/git/UserInfo.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.git
+
+/**
+ * Contains information about a Git user.
+ *
+ * Determines the author and committer fields of a commit.
+ *
+ * @constructor throws an [IllegalArgumentException] if the name or the email
+ * is an empty string.
+ */
+data class UserInfo(val name: String, val email: String) {
+ init {
+ require(name.isNotBlank()) { "Name cannot be an empty string." }
+ require(email.isNotBlank()) { "Email cannot be an empty string." }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt
new file mode 100644
index 0000000..0f48f00
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+/**
+ * An author of updates to GitHub pages.
+ */
+class AuthorEmail(val value: String) {
+
+ companion object {
+
+ /**
+ * The name of the environment variable that contains the email to use for authoring
+ * the commits to the GitHub Pages branch.
+ */
+ @Suppress("MemberVisibilityCanBePrivate") // for documentation purposes.
+ const val environmentVariable = "FORMAL_GIT_HUB_PAGES_AUTHOR"
+
+ /**
+ * Obtains the author from the system [environment variable][environmentVariable].
+ */
+ fun fromVar() : AuthorEmail {
+ val envValue = System.getenv(environmentVariable)
+ check(envValue != null && envValue.isNotBlank()) {
+ "Unable to obtain an author from `${environmentVariable}`."
+ }
+ return AuthorEmail(envValue)
+ }
+ }
+
+ override fun toString(): String = value
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/RepositoryExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/RepositoryExtensions.kt
new file mode 100644
index 0000000..4dacb62
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/RepositoryExtensions.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import io.spine.internal.gradle.RepoSlug
+import io.spine.internal.gradle.git.Branch
+import io.spine.internal.gradle.git.Repository
+import io.spine.internal.gradle.git.UserInfo
+
+/**
+ * Clones the current project repository with the branch dedicated to publishing
+ * documentation to GitHub Pages checked out.
+ *
+ * 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
+ * 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 {
+ val host = RepoSlug.fromVar().gitHost()
+
+ val username = "UpdateGitHubPages Plugin"
+ val userEmail = AuthorEmail.fromVar().toString()
+ val user = UserInfo(username, userEmail)
+
+ val branch = Branch.documentation
+
+ return of(host, user, branch)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/SshKey.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/SshKey.kt
new file mode 100644
index 0000000..7b1746a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/SshKey.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import io.spine.internal.gradle.Cli
+import java.io.File
+import org.gradle.api.GradleException
+
+/**
+ * Registers SSH key for further operations with GitHub Pages.
+ */
+internal class SshKey(private val rootProjectFolder: File) {
+ /**
+ * Creates an SSH key with the credentials and registers it by invoking the
+ * `register-ssh-key.sh` script.
+ */
+ fun register() {
+ val gitHubAccessKey = gitHubKey()
+ val sshConfigFile = sshConfigFile()
+ sshConfigFile.appendPublisher(gitHubAccessKey)
+
+ execute(
+ "${rootProjectFolder.absolutePath}/config/scripts/register-ssh-key.sh",
+ gitHubAccessKey.absolutePath
+ )
+ }
+
+ /**
+ * Locates `deploy_key_rsa` in the [rootProjectFolder] and returns it as a [File].
+ *
+ * A CI instance comes with an RSA key. However, of course, the default key has
+ * no privileges in Spine repositories. Thus, we add our own RSA key —
+ * `deploy_rsa_key`. It must have `write` rights in the associated repository.
+ * Also, we don't want that key to be used for anything else but GitHub Pages
+ * publishing.
+ *
+ * Thus, we configure the SSH agent to use the `deploy_rsa_key` only for specific
+ * references, namely in `github.com-publish`.
+ *
+ * @throws GradleException if `deploy_key_rsa` is not found.
+ */
+ private fun gitHubKey(): File {
+ val gitHubAccessKey = File("${rootProjectFolder.absolutePath}/deploy_key_rsa")
+
+ if (!gitHubAccessKey.exists()) {
+ throw GradleException(
+ "File $gitHubAccessKey does not exist. It should be encrypted" +
+ " in the repository and decrypted on CI."
+ )
+ }
+ return gitHubAccessKey
+ }
+
+ private fun sshConfigFile(): File {
+ val sshConfigFile = File("${System.getProperty("user.home")}/.ssh/config")
+
+ if (!sshConfigFile.exists()) {
+ val parentDir = sshConfigFile.canonicalFile.parentFile
+ parentDir.mkdirs()
+ sshConfigFile.createNewFile()
+ }
+
+ return sshConfigFile
+ }
+
+ private fun File.appendPublisher(privateKey: File) {
+ val nl = System.lineSeparator()
+ this.appendText(
+ nl +
+ "Host github.com-publish" + nl +
+ "User git" + nl +
+ "IdentityFile ${privateKey.absolutePath}" + nl
+ )
+ }
+
+ /**
+ * Executes a command in the project [rootProjectFolder].
+ */
+ private fun execute(vararg command: String): String = Cli(rootProjectFolder).execute(*command)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt
new file mode 100644
index 0000000..98e4772
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+object TaskName {
+
+ /**
+ * The name of the task which updates the GitHub Pages.
+ */
+ const val updateGitHubPages = "updateGitHubPages"
+
+ /**
+ * The name of the helper task to gather the generated Javadoc before updating
+ * GitHub Pages.
+ */
+ const val copyJavadoc = "copyJavadoc"
+
+ /**
+ * The name of the helper task to gather Dokka-generated documentation before
+ * updating GitHub Pages.
+ */
+ const val copyDokka = "copyDokka"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt
new file mode 100644
index 0000000..f27858f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import io.spine.internal.gradle.git.Repository
+import java.io.File
+import java.nio.file.Path
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.FileCollection
+import org.gradle.api.logging.Logger
+
+/**
+ * Performs the update of GitHub pages.
+ */
+fun Task.updateGhPages(project: Project) {
+ val plugin = project.plugins.getPlugin(UpdateGitHubPages::class.java)
+
+ with(plugin) {
+ SshKey(rootFolder).register()
+ }
+
+ val repository = Repository.forPublishingDocumentation()
+
+ val updateJavadoc = with(plugin) {
+ UpdateJavadoc(project, javadocOutputFolder, repository, logger)
+ }
+
+ val updateDokka = with(plugin) {
+ UpdateDokka(project, dokkaOutputFolder, repository, logger)
+ }
+
+ repository.use {
+ updateJavadoc.run()
+ updateDokka.run()
+ repository.push()
+ }
+}
+
+private abstract class UpdateDocumentation(
+ private val project: Project,
+ private val docsSourceFolder: Path,
+ private val repository: Repository,
+ private val logger: Logger
+) {
+
+ /**
+ * The folder under the repository's root(`/`) for storing documentation.
+ *
+ * The value should not contain any leading or trailing file separators.
+ *
+ * The absolute path to the project's documentation is made by appending its
+ * name to the end, making `/docsDestinationFolder/project.name`.
+ */
+ protected abstract val docsDestinationFolder: String
+
+ /**
+ * The name of the tool used to generate the documentation to update.
+ *
+ * This name will appear in logs as part of a message.
+ */
+ protected abstract val toolName: String
+
+ private val mostRecentFolder by lazy {
+ File("${repository.location}/${docsDestinationFolder}/${project.name}")
+ }
+
+ private fun logDebug(message: () -> String) {
+ if (logger.isDebugEnabled) {
+ logger.debug(message())
+ }
+ }
+
+ fun run() {
+ val module = project.name
+ logDebug {"Update of the $toolName documentation for module `$module` started." }
+
+ val documentation = replaceMostRecentDocs()
+ copyIntoVersionDir(documentation)
+
+ val version = project.version
+ val updateMessage =
+ "Update `$toolName` documentation for module `$module` as for version $version"
+ repository.commitAllChanges(updateMessage)
+
+ logDebug { "Update of the `$toolName` documentation for `$module` successfully finished." }
+ }
+
+ private fun replaceMostRecentDocs(): ConfigurableFileCollection {
+ val generatedDocs = project.files(docsSourceFolder)
+
+ logDebug {
+ "Replacing the most recent `$toolName` documentation in `${mostRecentFolder}`."
+ }
+ copyDocs(generatedDocs, mostRecentFolder)
+
+ return generatedDocs
+ }
+
+ private fun copyDocs(source: FileCollection, destination: File) {
+ destination.mkdir()
+ project.copy {
+ from(source)
+ into(destination)
+ }
+ }
+
+ private fun copyIntoVersionDir(generatedDocs: ConfigurableFileCollection) {
+ val versionedDocDir = File("$mostRecentFolder/v/${project.version}")
+
+ logDebug {
+ "Storing the new version of `$toolName` documentation in `${versionedDocDir}`."
+ }
+ copyDocs(generatedDocs, versionedDocDir)
+ }
+}
+
+private class UpdateJavadoc(
+ project: Project,
+ docsSourceFolder: Path,
+ repository: Repository,
+ logger: Logger
+) : UpdateDocumentation(project, docsSourceFolder, repository, logger) {
+
+ override val docsDestinationFolder: String
+ get() = "reference"
+ override val toolName: String
+ get() = "Javadoc"
+}
+
+private class UpdateDokka(
+ project: Project,
+ docsSourceFolder: Path,
+ repository: Repository,
+ logger: Logger
+) : UpdateDocumentation(project, docsSourceFolder, repository, logger) {
+
+ override val docsDestinationFolder: String
+ get() = "dokka-reference"
+ override val toolName: String
+ get() = "Dokka"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt
new file mode 100644
index 0000000..ba8c290
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import dokkaHtmlTask
+import io.spine.internal.gradle.fs.LazyTempPath
+import io.spine.internal.gradle.github.pages.TaskName.copyDokka
+import io.spine.internal.gradle.github.pages.TaskName.copyJavadoc
+import io.spine.internal.gradle.github.pages.TaskName.updateGitHubPages
+import io.spine.internal.gradle.isSnapshot
+import io.spine.internal.gradle.javadoc.ExcludeInternalDoclet
+import io.spine.internal.gradle.javadoc.javadocTask
+import java.io.File
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.tasks.Copy
+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.
+ *
+ * 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)
+ * }
+ * ```
+ *
+ * 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
+ * it on CI upon publication. Also, the script uses the `FORMAL_GIT_HUB_PAGES_AUTHOR`
+ * environment variable to set the author email for the commits. The `gh-pages`
+ * branch itself should exist before the plugin is run.
+ *
+ * NOTE: when changing the value of "FORMAL_GIT_HUB_PAGES_AUTHOR", one also must change
+ * the SSH private (encrypted `deploy_key_rsa`) and the public
+ * ("GitHub Pages publisher" on GitHub) keys.
+ *
+ * Another requirement is an environment variable `REPO_SLUG`, which is set by the CI
+ * environment, such as `Publish` GitHub Actions workflow. It points to the repository
+ * for which the update is executed. E.g.:
+ *
+ * ```
+ * REPO_SLUG: SpineEventEngine/base
+ * ```
+ *
+ * @see UpdateGitHubPagesExtension for the extension which is used to configure
+ * this plugin
+ */
+class UpdateGitHubPages : Plugin {
+
+ /**
+ * Root folder of the repository, to which this `Project` belongs.
+ */
+ internal lateinit var rootFolder: File
+
+ /**
+ * The external inputs to include into the publishing.
+ *
+ * The inputs are evaluated according to [Copy.from] specification.
+ */
+ private lateinit var includedInputs: Set
+
+ /**
+ * Path to the temp folder used to gather the Javadoc output 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.
+ */
+ internal val dokkaOutputFolder = LazyTempPath("dokka")
+
+ /**
+ * Applies the plugin to the specified [project].
+ *
+ * If the project version says it is a snapshot, the plugin registers a no-op task.
+ *
+ * Even in such a case, the extension object is still created in the given project
+ * to allow customization of the parameters in its build script for later usage
+ * when the project version changes to non-snapshot.
+ */
+ override fun apply(project: Project) {
+ val extension = UpdateGitHubPagesExtension.createIn(project)
+ project.afterEvaluate {
+ val projectVersion = project.version.toString()
+ if (projectVersion.isSnapshot()) {
+ registerNoOpTask()
+ } else {
+ registerTasks(extension)
+ }
+ }
+ }
+
+ /**
+ * Registers `updateGitHubPages` task which performs no actual update, but prints
+ * the message telling the update is skipped, since the project is in
+ * its `SNAPSHOT` version.
+ */
+ private fun Project.registerNoOpTask() {
+ tasks.register(updateGitHubPages) {
+ doLast {
+ val project = this@registerNoOpTask
+ println(
+ "GitHub Pages update will be skipped since this project is a snapshot: " +
+ "`${project.name}-${project.version}`."
+ )
+ }
+ }
+ }
+
+ 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.registerCopyDokka()
+
+ val updatePagesTask = tasks.registerUpdateTask()
+ updatePagesTask.configure {
+ dependsOn(copyJavadoc)
+ dependsOn(copyDokka)
+ }
+ }
+
+ private fun TaskContainer.registerCopyJavadoc(allowInternalJavadoc: Boolean) {
+ val inputs = composeJavadocInputs(allowInternalJavadoc)
+
+ register(copyJavadoc, Copy::class.java) {
+ inputs.forEach { from(it) }
+ into(javadocOutputFolder)
+ }
+ }
+
+ private fun TaskContainer.composeJavadocInputs(allowInternalJavadoc: Boolean): List {
+ val inputs = mutableListOf()
+ if (allowInternalJavadoc) {
+ inputs.add(javadocTask())
+ } else {
+ inputs.add(javadocTask(ExcludeInternalDoclet.taskName))
+ }
+ inputs.addAll(includedInputs)
+ return inputs
+ }
+
+ private fun TaskContainer.registerCopyDokka() {
+ val inputs = composeDokkaInputs()
+
+ register(copyDokka, Copy::class.java) {
+ inputs.forEach { from(it) }
+ into(dokkaOutputFolder)
+ }
+ }
+
+ private fun TaskContainer.composeDokkaInputs(): List {
+ val inputs = mutableListOf()
+
+ dokkaHtmlTask()?.let {
+ inputs.add(it)
+ }
+ inputs.addAll(includedInputs)
+
+ return inputs
+ }
+
+ private fun TaskContainer.registerUpdateTask(): TaskProvider {
+ return register(updateGitHubPages) {
+ doLast {
+ try {
+ updateGhPages(project)
+ } finally {
+ cleanup()
+ }
+ }
+ }
+ }
+
+ private fun cleanup() {
+ val folders = listOf(dokkaOutputFolder, javadocOutputFolder)
+ folders.forEach {
+ it.toFile().deleteRecursively()
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt
new file mode 100644
index 0000000..d523d5e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.SetProperty
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.property
+
+/**
+ * Configures the `updateGitHubPages` extension.
+ */
+@Suppress("unused")
+fun Project.updateGitHubPages(excludeInternalDocletVersion: String,
+ action: UpdateGitHubPagesExtension.() -> Unit) {
+ apply()
+
+ val extension = extensions.getByType(UpdateGitHubPagesExtension::class)
+ extension.excludeInternalDocletVersion = excludeInternalDocletVersion
+ extension.action()
+}
+
+/**
+ * The extension for configuring the [UpdateGitHubPages] plugin.
+ */
+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.
+ */
+ 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.internal.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. */
+ const val name = "updateGitHubPages"
+
+ /** 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)
+ )
+ project.extensions.add(result.javaClass, name, result)
+ return result
+ }
+ }
+
+ /**
+ * 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()
+ }
+
+ /**
+ * Returns the external inputs, which results should be included into the
+ * GitHub Pages update.
+ */
+ fun includedInputs(): Set {
+ return includeInputs.get()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt
new file mode 100644
index 0000000..3e0aad9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.java
+
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.testing.Test
+import org.gradle.kotlin.dsl.named
+
+/**
+ * Locates `test` task in this [TaskContainer].
+ *
+ * Runs the unit tests using JUnit or TestNG.
+ *
+ * Depends on `testClasses`, and all tasks which produce the test runtime classpath.
+ *
+ * @see
+ * Tasks | The Java Plugin
+ */
+val TaskContainer.test: TaskProvider
+ get() = named("test")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt
new file mode 100644
index 0000000..1489c08
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javac
+
+import net.ltgt.gradle.errorprone.errorprone
+import org.gradle.api.tasks.compile.JavaCompile
+import org.gradle.process.CommandLineArgumentProvider
+
+/**
+ * Configures Error Prone for this `JavaCompile` task.
+ *
+ * Specifies the arguments for the compiler invocations. In particular, this configuration
+ * overrides a number of Error Prone defaults. See [ErrorProneConfig] for the details.
+ *
+ * Please note that while `ErrorProne` is a standalone Gradle plugin,
+ * it still has to be configured through `JavaCompile` task options.
+ *
+ * Here's an example of how to use it:
+ *
+ * ```
+ * tasks {
+ * withType {
+ * configureErrorProne()
+ * }
+ * }
+ *```
+ */
+@Suppress("unused")
+fun JavaCompile.configureErrorProne() {
+ options.errorprone
+ .errorproneArgumentProviders
+ .add(ErrorProneConfig.ARGUMENTS)
+}
+
+/**
+ * The knowledge that is required to set up `Error Prone`.
+ */
+private object ErrorProneConfig {
+
+ /**
+ * Command line options for the `Error Prone` compiler.
+ */
+ val ARGUMENTS = CommandLineArgumentProvider {
+ listOf(
+
+ // Exclude generated sources from being analyzed by ErrorProne.
+ // Include all directories started from `generated`, such as `generated-proto`.
+ "-XepExcludedPaths:.*/generated.*/.*",
+
+ // Turn the check off until ErrorProne can handle `@Nested` JUnit classes.
+ // See issue: https://github.com/google/error-prone/issues/956
+ "-Xep:ClassCanBeStatic:OFF",
+
+ // Turn off checks that report unused methods and method parameters.
+ // See issue: https://github.com/SpineEventEngine/config/issues/61
+ "-Xep:UnusedMethod:OFF",
+ "-Xep:UnusedVariable:OFF",
+
+ "-Xep:CheckReturnValue:OFF",
+ "-Xep:FloggerSplitLogStatement:OFF",
+ "-Xep:FloggerLogString:OFF"
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt
new file mode 100644
index 0000000..de02687
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javac
+
+import org.gradle.api.tasks.compile.JavaCompile
+import org.gradle.process.CommandLineArgumentProvider
+
+/**
+ * Configures the `javac` tool through this `JavaCompile` task.
+ *
+ * The following steps are performed:
+ *
+ * 1. Passes a couple of arguments to the compiler. See [JavacConfig] for more details;
+ * 2. Sets the UTF-8 encoding to be used when reading Java source files.
+ *
+ * Here's an example of how to use it:
+ *
+ *```
+ * tasks {
+ * withType {
+ * configureJavac()
+ * }
+ * }
+ *```
+ */
+@Suppress("unused")
+fun JavaCompile.configureJavac() {
+ with(options) {
+ encoding = JavacConfig.SOURCE_FILES_ENCODING
+ compilerArgumentProviders.add(JavacConfig.COMMAND_LINE)
+ }
+}
+
+/**
+ * The knowledge that is required to set up `javac`.
+ */
+private object JavacConfig {
+ const val SOURCE_FILES_ENCODING = "UTF-8"
+ val COMMAND_LINE = CommandLineArgumentProvider {
+ listOf(
+
+ // Protobuf Compiler generates the code, which uses the deprecated `PARSER` field.
+ // See issue: https://github.com/SpineEventEngine/config/issues/173
+ // "-Werror",
+
+ "-Xlint:unchecked",
+ "-Xlint:deprecation",
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt
new file mode 100644
index 0000000..bf8601b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javadoc
+
+/**
+ * The encoding to use in Javadoc processing.
+ */
+data class Encoding(val name: String)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt
new file mode 100644
index 0000000..4d5702b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javadoc
+
+import io.spine.internal.dependency.Spine
+import io.spine.internal.gradle.javadoc.ExcludeInternalDoclet.Companion.taskName
+import io.spine.internal.gradle.sourceSets
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.tasks.javadoc.Javadoc
+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 = Spine.ArtifactVersion.javadocTools
+) {
+
+ private val dependency = Spine.javadocFilter
+
+ companion object {
+
+ /**
+ * The name of the custom configuration in scope of which the exclusion of
+ * `@Internal` types is performed.
+ */
+ private const val configurationName = "excludeInternalDoclet"
+
+ /**
+ * The fully-qualified class name of the doclet.
+ */
+ const val className = "io.spine.tools.javadoc.ExcludeInternalDoclet"
+
+ /**
+ * The name of the helper task which configures the Javadoc processing
+ * to exclude `@Internal` types.
+ */
+ const val taskName = "noInternalJavadoc"
+
+ private fun createConfiguration(project: Project): Configuration {
+ return project.configurations.create(configurationName)
+ }
+ }
+
+ /**
+ * Creates a custom Javadoc task for the [project] which excludes the types
+ * annotated as `@Internal`.
+ *
+ * The task is registered under [taskName].
+ */
+ fun registerTaskIn(project: Project) {
+ val configuration = addTo(project)
+ project.appendCustomJavadocTask(configuration)
+ }
+
+ /**
+ * Creates a configuration for the doclet in the given project and adds it to its dependencies.
+ *
+ * @return added configuration
+ */
+ private fun addTo(project: Project): Configuration {
+ val configuration = createConfiguration(project)
+ project.dependencies.add(configuration.name, dependency)
+ return configuration
+ }
+}
+
+private fun Project.appendCustomJavadocTask(excludeInternalDoclet: Configuration) {
+ val javadocTask = tasks.javadocTask()
+ tasks.register(taskName, Javadoc::class.java) {
+
+ source = sourceSets.getByName("main").allJava.filter {
+ !it.absolutePath.contains("generated")
+ }.asFileTree
+
+ classpath = javadocTask.classpath
+
+ options {
+ encoding = JavadocConfig.encoding.name
+
+ // Doclet fully qualified name.
+ doclet = ExcludeInternalDoclet.className
+
+ // Path to the JAR containing the doclet.
+ docletpath = excludeInternalDoclet.files.toList()
+ }
+
+ val docletOptions = options as StandardJavadocDocletOptions
+ JavadocConfig.registerCustomTags(docletOptions)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt
new file mode 100644
index 0000000..debd78d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javadoc
+
+import java.io.File
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.api.tasks.javadoc.Javadoc
+import org.gradle.external.javadoc.StandardJavadocDocletOptions
+
+/**
+ * Javadoc processing settings.
+ *
+ * This type is named with `Config` suffix to avoid its confusion with the standard `Javadoc` type.
+ */
+@Suppress("unused")
+object JavadocConfig {
+
+ /**
+ * Link to the documentation for Java 11 Standard Library API.
+ *
+ * Oracle JDK SE 11 is used for the reference.
+ */
+ private const val standardLibraryAPI = "https://docs.oracle.com/en/java/javase/11/docs/api/"
+
+ @Suppress("MemberVisibilityCanBePrivate") // opened to be visible from docs.
+ val tags = listOf(
+ JavadocTag("apiNote", "API Note"),
+ JavadocTag("implSpec", "Implementation Requirements"),
+ JavadocTag("implNote", "Implementation Note")
+ )
+
+ val encoding = Encoding("UTF-8")
+
+ fun applyTo(project: Project) {
+ val javadocTask = project.tasks.javadocTask()
+ discardJavaModulesInLinks(javadocTask)
+ val docletOptions = javadocTask.options as StandardJavadocDocletOptions
+ configureDoclet(docletOptions)
+ }
+
+ /**
+ * 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
+ * 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.
+ *
+ * See: [Issue #334](https://github.com/SpineEventEngine/config/issues/334)
+ */
+ private fun discardJavaModulesInLinks(javadoc: Javadoc) {
+
+ // 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.
+
+ val discardModulePrefix = """
+
+ getURLPrefix = function(ui) {
+ return "";
+ };
+ """.trimIndent()
+
+ javadoc.doLast {
+ val destinationDir = javadoc.destinationDir!!.absolutePath
+ val searchScript = File("$destinationDir/search.js")
+ searchScript.appendText(discardModulePrefix)
+ }
+ }
+
+ private fun configureDoclet(docletOptions: StandardJavadocDocletOptions) {
+ docletOptions.encoding = encoding.name
+ reduceParamWarnings(docletOptions)
+ registerCustomTags(docletOptions)
+ linkStandardLibraryAPI(docletOptions)
+ }
+
+ /**
+ * Configures `javadoc` tool to avoid numerous warnings for missing `@param` tags.
+ *
+ * As suggested by Stephen Colebourne:
+ * [https://blog.joda.org/2014/02/turning-off-doclint-in-jdk-8-javadoc.html]
+ *
+ * See also:
+ * [https://github.com/GPars/GPars/blob/master/build.gradle#L268]
+ */
+ private fun reduceParamWarnings(docletOptions: StandardJavadocDocletOptions) {
+ if (JavaVersion.current().isJava8Compatible) {
+ docletOptions.addStringOption("Xdoclint:none", "-quiet")
+ }
+ }
+
+ /**
+ * Registers custom [tags] for the passed doclet options.
+ */
+ fun registerCustomTags(docletOptions: StandardJavadocDocletOptions) {
+ docletOptions.tags = tags.map { it.toString() }
+ }
+
+ /**
+ * Links the documentation for Java 11 Standard Library API.
+ *
+ * This documentation is used to be referenced to when navigating to the types from the
+ * standard library (`String`, `List`, etc.).
+ *
+ * OpenJDK SE 11 is used for the reference.
+ */
+ private fun linkStandardLibraryAPI(docletOptions: StandardJavadocDocletOptions) {
+ docletOptions.addStringOption("link", standardLibraryAPI)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt
new file mode 100644
index 0000000..8eb8b17
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javadoc
+
+/**
+ * The Javadoc tag.
+ */
+class JavadocTag(val name: String, val title: String) {
+
+ override fun toString(): String {
+ return "${name}:a:${title}:"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt
new file mode 100644
index 0000000..3897118
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javadoc
+
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.javadoc.Javadoc
+
+/**
+ * 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
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt
new file mode 100644
index 0000000..75cd401
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript
+
+import java.io.File
+import org.gradle.api.Project
+
+/**
+ * Provides access to the current [JsEnvironment] and shortcuts for running `npm` tool.
+ */
+open class JsContext(jsEnv: JsEnvironment, internal val project: Project)
+ : JsEnvironment by jsEnv
+{
+ /**
+ * Executes `npm` command in a separate process.
+ *
+ * [JsEnvironment.projectDir] is used as a working directory.
+ */
+ fun npm(vararg args: String) = projectDir.npm(*args)
+
+ /**
+ * Executes `npm` command in a separate process.
+ *
+ * This [File] is used as a working directory.
+ */
+ fun File.npm(vararg args: String) = project.exec {
+
+ workingDir(this@npm)
+ commandLine(npmExecutable)
+ args(*args)
+
+ // Using private packages in a CI/CD workflow | npm Docs
+ // https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow
+
+ environment["NPM_TOKEN"] = npmAuthToken
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt
new file mode 100644
index 0000000..bce4fa8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript
+
+import java.io.File
+import org.apache.tools.ant.taskdefs.condition.Os
+
+/**
+ * Describes the environment in which JavaScript code is assembled and processed during the build.
+ *
+ * Consists of three parts describing:
+ *
+ * 1. A module itself.
+ * 2. Tools and their input/output files.
+ * 3. Code generation.
+ */
+interface JsEnvironment {
+
+ /*
+ * A module itself
+ ******************/
+
+ /**
+ * Module's root catalog.
+ */
+ val projectDir: File
+
+ /**
+ * Module's version.
+ */
+ val moduleVersion: String
+
+ /**
+ * Module's production sources directory.
+ *
+ * Default value: "$projectDir/main".
+ */
+ val srcDir: File
+ get() = projectDir.resolve("main")
+
+ /**
+ * Module's test sources directory.
+ *
+ * Default value: "$projectDir/test".
+ */
+ val testSrcDir: File
+ get() = projectDir.resolve("test")
+
+ /**
+ * A directory which all artifacts are generated into.
+ *
+ * Default value: "$projectDir/build".
+ */
+ val buildDir: File
+ get() = projectDir.resolve("build")
+
+ /**
+ * A directory where artifacts for further publishing would be prepared.
+ *
+ * Default value: "$buildDir/npm-publication".
+ */
+ val publicationDir: File
+ get() = buildDir.resolve("npm-publication")
+
+ /*
+ * Tools and their input/output files
+ *************************************/
+
+ /**
+ * Name of an executable for running `npm`.
+ *
+ * Default value:
+ *
+ * 1. "nmp.cmd" for Windows.
+ * 2. "npm" for other OSs.
+ */
+ val npmExecutable: String
+ get() = if (isWindows()) "npm.cmd" else "npm"
+
+ /**
+ * An access token that allows installation and/or publishing modules.
+ *
+ * During installation a token is required only if dependencies from private
+ * repositories are used.
+ *
+ * Default value is read from the environmental variable - `NPM_TOKEN`.
+ * "PUBLISHING_FORBIDDEN" stub value would be assigned in case `NPM_TOKEN` variable is not set.
+ *
+ * See [Creating and viewing access tokens | npm Docs](https://docs.npmjs.com/creating-and-viewing-access-tokens).
+ */
+ val npmAuthToken: String
+ get() = System.getenv("NPM_TOKEN") ?: "PUBLISHING_FORBIDDEN"
+
+ /**
+ * A directory where `npm` puts downloaded module's dependencies.
+ *
+ * Default value: "$projectDir/node_modules".
+ */
+ val nodeModules: File
+ get() = projectDir.resolve("node_modules")
+
+ /**
+ * Module's descriptor used by `npm`.
+ *
+ * Default value: "$projectDir/package.json".
+ */
+ val packageJson: File
+ get() = projectDir.resolve("package.json")
+
+ /**
+ * `npm` gets its configuration settings from the command line, environment variables,
+ * and `npmrc` file.
+ *
+ * Default value: "$projectDir/.npmrc".
+ *
+ * See [npmrc | npm Docs](https://docs.npmjs.com/cli/v8/configuring-npm/npmrc).
+ */
+ val npmrc: File
+ get() = projectDir.resolve(".npmrc")
+
+ /**
+ * A cache directory in which `nyc` tool outputs raw coverage report.
+ *
+ * Default value: "$projectDir/.nyc_output".
+ *
+ * See [istanbuljs/nyc](https://github.com/istanbuljs/nyc).
+ */
+ val nycOutput: File
+ get() = projectDir.resolve(".nyc_output")
+
+ /**
+ * A directory in which `webpack` would put a ready-to-use bundle.
+ *
+ * Default value: "$projectDir/dist"
+ *
+ * See [webpack - npm](https://www.npmjs.com/package/webpack).
+ */
+ val webpackOutput: File
+ get() = projectDir.resolve("dist")
+
+ /**
+ * A directory where bundled artifacts for further publishing would be prepared.
+ *
+ * Default value: "$publicationDir/dist".
+ */
+ val webpackPublicationDir: File
+ get() = publicationDir.resolve("dist")
+
+ /*
+ * Code generation
+ ******************/
+
+ /**
+ * Name of a directory that contains generated code.
+ *
+ * Default value: "proto".
+ */
+ val genProtoDirName: String
+ get() = "proto"
+
+ /**
+ * Directory with production Protobuf messages compiled into JavaScript.
+ *
+ * Default value: "$srcDir/$genProtoDirName".
+ */
+ val genProtoMain: File
+ get() = srcDir.resolve(genProtoDirName)
+
+ /**
+ * Directory with test Protobuf messages compiled into JavaScript.
+ *
+ * Default value: "$testSrcDir/$genProtoDirName".
+ */
+ val genProtoTest: File
+ get() = testSrcDir.resolve(genProtoDirName)
+}
+
+/**
+ * Allows overriding [JsEnvironment]'s defaults.
+ *
+ * All of declared properties can be split into two groups:
+ *
+ * 1. The ones that *define* something - can be overridden.
+ * 2. The ones that *describe* something - can NOT be overridden.
+ *
+ * Overriding a "defining" property affects the way `npm` tool works.
+ * In contrary, overriding a "describing" property does not affect the tool.
+ * Such properties just describe how the used tool works.
+ *
+ * Therefore, overriding of "describing" properties leads to inconsistency with expectations.
+ *
+ * The next properties could not be overridden:
+ *
+ * 1. [JsEnvironment.nodeModules].
+ * 2. [JsEnvironment.packageJson].
+ * 3. [JsEnvironment.npmrc].
+ * 4. [JsEnvironment.nycOutput].
+ */
+class ConfigurableJsEnvironment(initialEnvironment: JsEnvironment)
+ : JsEnvironment by initialEnvironment
+{
+ /*
+ * A module itself
+ ******************/
+
+ override var projectDir = initialEnvironment.projectDir
+ override var moduleVersion = initialEnvironment.moduleVersion
+ override var srcDir = initialEnvironment.srcDir
+ override var testSrcDir = initialEnvironment.testSrcDir
+ override var buildDir = initialEnvironment.buildDir
+ override var publicationDir = initialEnvironment.publicationDir
+
+ /*
+ * Tools and their input/output files
+ *************************************/
+
+ override var npmExecutable = initialEnvironment.npmExecutable
+ override var npmAuthToken = initialEnvironment.npmAuthToken
+ override var webpackOutput = initialEnvironment.webpackOutput
+ override var webpackPublicationDir = initialEnvironment.webpackPublicationDir
+
+ /*
+ * Code generation
+ ******************/
+
+ override var genProtoDirName = initialEnvironment.genProtoDirName
+ override var genProtoMain = initialEnvironment.genProtoMain
+ override var genProtoTest = initialEnvironment.genProtoTest
+}
+
+internal fun isWindows(): Boolean = Os.isFamily(Os.FAMILY_WINDOWS)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt
new file mode 100644
index 0000000..22ebf52
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript
+
+import io.spine.internal.gradle.javascript.task.JsTasks
+import io.spine.internal.gradle.javascript.plugin.JsPlugins
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.extra
+import org.gradle.kotlin.dsl.findByType
+
+/**
+ * Configures [JsExtension] that facilitates configuration of Gradle tasks and plugins
+ * to build JavaScripts projects.
+ *
+ * The whole structure of the extension looks as follows:
+ *
+ * ```
+ * javascript {
+ * environment {
+ * // ...
+ * }
+ * tasks {
+ * // ...
+ * }
+ * plugins {
+ * // ...
+ * }
+ * }
+ * ```
+ *
+ * ### Environment
+ *
+ * One of the main features of this extension is [JsEnvironment]. Environment describes a module
+ * itself, used tools with their input/output files, code generation.
+ *
+ * The extension is shipped with a pre-configured environment. So, no pre-configuration is required.
+ * Most properties in [JsEnvironment] have calculated defaults right in the interface.
+ * Only two properties need explicit override.
+ *
+ * The extension defines them as follows:
+ *
+ * 1. [JsEnvironment.projectDir] –> `project.projectDir`.
+ * 2. [JsEnvironment.moduleVersion] —> `project.extra["versionToPublishJs"]`.
+ *
+ * There are two ways to modify the environment:
+ *
+ * 1. Update [JsEnvironment] directly. Go with this option when it is a global change
+ * that should affect all projects which use this extension.
+ * 2. Use [JsExtension.environment] scope — for temporary and custom overridings.
+ *
+ * An example of a property overriding:
+ *
+ * ```
+ * javascript {
+ * environment {
+ * moduleVersion = "$moduleVersion-SPECIAL_EDITION"
+ * }
+ * }
+ * ```
+ *
+ * Please note, environment should be set up firstly to have the effect on the parts
+ * of the extension that use it.
+ *
+ * ### Tasks and Plugins
+ *
+ * The spirit of tasks configuration in this extension is extracting the code that defines and
+ * registers tasks into extension functions upon `JsTasks` in `buildSrc`. Those extensions should be
+ * named after a task it registers or a task group if several tasks are registered at once.
+ * Then this extension is called in a project's `build.gradle.kts`.
+ *
+ * `JsTasks` and `JsPlugins` scopes extend [JsContext] which provides access
+ * to the current [JsEnvironment] and shortcuts for running `npm` tool.
+ *
+ * Below is the simplest example of how to create a primitive `printNpmVersion` task.
+ *
+ * Firstly, a corresponding extension function should be defined in `buildSrc`:
+ *
+ * ```
+ * fun JsTasks.printNpmVersion() =
+ * register("printNpmVersion") {
+ * doLast {
+ * npm("--version")
+ * }
+ * }
+ * ```
+ *
+ * Secondly, in a project's `build.gradle.kts` this extension is called:
+ *
+ * ```
+ * javascript {
+ * tasks {
+ * printNpmVersion()
+ * }
+ * }
+ * ```
+ *
+ * An extension function is not restricted to register exactly one task. If several tasks can
+ * be grouped into a logical bunch, they should be registered together:
+ *
+ * ```
+ * fun JsTasks.build() {
+ * assembleJs()
+ * testJs()
+ * generateCoverageReport()
+ * }
+ *
+ * private fun JsTasks.assembleJs() = ...
+ *
+ * private fun JsTasks.testJs() = ...
+ *
+ * private fun JsTasks.generateCoverageReport() = ...
+ * ```
+ *
+ * This section is mostly dedicated to tasks. But tasks and plugins are configured
+ * in a very similar way. So, everything above is also applicable to plugins. More detailed
+ * guides can be found in docs to `JsTasks` and `JsPlugins`.
+ *
+ * @see [ConfigurableJsEnvironment]
+ * @see [JsTasks]
+ * @see [JsPlugins]
+ */
+fun Project.javascript(configuration: JsExtension.() -> Unit) {
+ extensions.run {
+ configuration.invoke(
+ findByType() ?: create("jsExtension", project)
+ )
+ }
+}
+
+/**
+ * Scope for performing JavaScript-related configuration.
+ *
+ * @see [javascript]
+ */
+open class JsExtension(internal val project: Project) {
+
+ private val configurableEnvironment = ConfigurableJsEnvironment(
+ object : JsEnvironment {
+ override val projectDir = project.projectDir
+ override val moduleVersion = project.extra["versionToPublishJs"].toString()
+ }
+ )
+
+ val environment: JsEnvironment = configurableEnvironment
+ val tasks: JsTasks = JsTasks(environment, project)
+ val plugins: JsPlugins = JsPlugins(environment, project)
+
+ /**
+ * Overrides default values of [JsEnvironment].
+ *
+ * Please note, environment should be set up firstly to have the effect on the parts
+ * of the extension that use it.
+ */
+ fun environment(overridings: ConfigurableJsEnvironment.() -> Unit) =
+ configurableEnvironment.run(overridings)
+
+ /**
+ * Configures [JavaScript-related tasks][JsTasks].
+ */
+ fun tasks(configurations: JsTasks.() -> Unit) =
+ tasks.run(configurations)
+
+ /**
+ * Configures [JavaScript-related plugins][JsPlugins].
+ */
+ fun plugins(configurations: JsPlugins.() -> Unit) =
+ plugins.run(configurations)
+
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt
new file mode 100644
index 0000000..5e51155
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.plugin
+
+import org.gradle.kotlin.dsl.configure
+import org.gradle.plugins.ide.idea.model.IdeaModel
+
+/**
+ * Applies and configures `idea` plugin to work with a JavaScript module.
+ *
+ * In particular, this method:
+ *
+ * 1. Specifies directories for production and test sources.
+ * 2. Excludes directories with generated code and build artifacts.
+ *
+ * @see JsPlugins
+ */
+fun JsPlugins.idea() {
+
+ plugins {
+ apply("org.gradle.idea")
+ }
+
+ extensions.configure {
+
+ module {
+ sourceDirs.add(srcDir)
+ testSources.from(testSrcDir)
+
+ excludeDirs.addAll(
+ listOf(
+ nodeModules,
+ nycOutput,
+ genProtoMain,
+ genProtoTest
+ )
+ )
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt
new file mode 100644
index 0000000..60a088a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.plugin
+
+import io.spine.internal.gradle.javascript.JsContext
+import io.spine.internal.gradle.javascript.JsEnvironment
+import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionContainer
+import org.gradle.api.plugins.PluginContainer
+import org.gradle.api.tasks.TaskContainer
+
+/**
+ * A scope for applying and configuring JavaScript-related plugins.
+ *
+ * The scope extends [JsContext] and provides shortcuts for key project's containers:
+ *
+ * 1. [plugins].
+ * 2. [extensions].
+ * 3. [tasks].
+ *
+ * Let's imagine one wants to apply and configure `FooBar` plugin. To do that, several steps
+ * should be completed:
+ *
+ * 1. Declare the corresponding extension function upon [JsPlugins] named after the plugin.
+ * 2. Apply and configure the plugin inside that function.
+ * 3. Call the resulted extension in your `build.gradle.kts` file.
+ *
+ * Here's an example of `javascript/plugin/FooBar.kt`:
+ *
+ * ```
+ * fun JsPlugins.fooBar() {
+ * plugins.apply("com.fooBar")
+ * extensions.configure {
+ * // ...
+ * }
+ * }
+ * ```
+ *
+ * And here's how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.plugins.fooBar
+ *
+ * // ...
+ *
+ * javascript {
+ * plugins {
+ * fooBar()
+ * }
+ * }
+ * ```
+ */
+class JsPlugins(jsEnv: JsEnvironment, project: Project) : JsContext(jsEnv, project) {
+
+ internal val plugins = project.plugins
+ internal val extensions = project.extensions
+ internal val tasks = project.tasks
+
+ internal fun plugins(configurations: PluginContainer.() -> Unit) =
+ plugins.run(configurations)
+
+ internal fun extensions(configurations: ExtensionContainer.() -> Unit) =
+ extensions.run(configurations)
+
+ internal fun tasks(configurations: TaskContainer.() -> Unit) =
+ tasks.run(configurations)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt
new file mode 100644
index 0000000..61e59d5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.plugin
+
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.withGroovyBuilder
+
+/**
+ * Applies `mc-js` plugin and specifies directories for generated code.
+ *
+ * @see JsPlugins
+ */
+fun JsPlugins.mcJs() {
+
+ plugins {
+ apply("io.spine.mc-js")
+ }
+
+ // Temporarily use GroovyInterop.
+ // Currently, it is not possible to obtain `McJsPlugin` on the classpath of `buildSrc`.
+ // See issue: https://github.com/SpineEventEngine/config/issues/298
+
+ project.withGroovyBuilder {
+ "protoJs" {
+ setProperty("generatedMainDir", genProtoMain)
+ setProperty("generatedTestDir", genProtoTest)
+ }
+ }
+}
+
+/**
+ * Locates `generateJsonParsers` in this [TaskContainer].
+ *
+ * The task generates JSON-parsing code for JavaScript messages compiled from Protobuf.
+ */
+val TaskContainer.generateJsonParsers: TaskProvider
+ get() = named("generateJsonParsers")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt
new file mode 100644
index 0000000..c50ddd9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.plugin
+
+import com.google.protobuf.gradle.ProtobufExtension
+import com.google.protobuf.gradle.id
+import com.google.protobuf.gradle.remove
+import io.spine.internal.dependency.Protobuf
+
+/**
+ * Applies and configures `protobuf` plugin to work with a JavaScript module.
+ *
+ * In particular, this method:
+ *
+ * 1. Specifies an executable for `protoc` compiler.
+ * 2. Configures `GenerateProtoTask`.
+ *
+ * @see JsPlugins
+ */
+fun JsPlugins.protobuf() {
+
+ plugins {
+ apply(Protobuf.GradlePlugin.id)
+ }
+
+ val protobufExt = project.extensions.getByType(ProtobufExtension::class.java)
+ protobufExt.apply {
+
+ generatedFilesBaseDir = projectDir.path
+
+ protoc {
+ artifact = Protobuf.compiler
+ }
+
+ generateProtoTasks {
+ all().forEach { task ->
+
+ task.builtins {
+
+ // Do not use java builtin output in this project.
+
+ remove("java")
+
+ // For information on JavaScript code generation please see
+ // https://github.com/google/protobuf/blob/master/js/README.md
+
+ id("js") {
+ option("import_style=commonjs")
+ outputSubDir = genProtoDirName
+ }
+ }
+
+ val sourceSet = task.sourceSet.name
+ val testClassifier = if (sourceSet == "test") "_test" else ""
+ val artifact = "${project.group}_${project.name}_${moduleVersion}"
+ val descriptor = "$artifact$testClassifier.desc"
+
+ task.generateDescriptorSet = true
+ task.descriptorSetOptions.path =
+ "${projectDir}/build/descriptors/${sourceSet}/${descriptor}"
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt
new file mode 100644
index 0000000..4739307
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.google.protobuf.gradle.GenerateProtoTask
+import io.spine.internal.gradle.base.assemble
+import io.spine.internal.gradle.javascript.plugin.generateJsonParsers
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import io.spine.internal.gradle.TaskName
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.withType
+
+/**
+ * Registers tasks for assembling JavaScript artifacts.
+ *
+ * Please note, this task group depends on [mc-js][io.spine.internal.gradle.javascript.plugin.mcJs]
+ * and [protobuf][io.spine.internal.gradle.javascript.plugin.protobuf]` plugins. Therefore,
+ * these plugins should be applied in the first place.
+ *
+ * List of tasks to be created:
+ *
+ * 1. [TaskContainer.assembleJs].
+ * 2. [TaskContainer.compileProtoToJs].
+ * 3. [TaskContainer.installNodePackages].
+ * 4. [TaskContainer.updatePackageVersion].
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.assemble
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * assemble()
+ * }
+ * }
+ * ```
+ *
+ * @param configuration any additional configuration related to the module's assembling.
+ */
+fun JsTasks.assemble(configuration: JsTasks.() -> Unit = {}) {
+
+ installNodePackages()
+
+ compileProtoToJs().also {
+ generateJsonParsers.configure {
+ dependsOn(it)
+ }
+ }
+
+ updatePackageVersion()
+
+ assembleJs().also {
+ assemble.configure {
+ dependsOn(it)
+ }
+ }
+
+ configuration()
+}
+
+private val assembleJsName = TaskName.of("assembleJs")
+
+/**
+ * Locates `assembleJs` task in this [TaskContainer].
+ *
+ * It is a lifecycle task that produces consumable JavaScript artifacts.
+ */
+val TaskContainer.assembleJs: TaskProvider
+ get() = named(assembleJsName)
+
+private fun JsTasks.assembleJs() =
+ register(assembleJsName) {
+
+ description = "Assembles JavaScript sources into consumable artifacts."
+ group = JsTasks.Group.assemble
+
+ dependsOn(
+ installNodePackages,
+ compileProtoToJs,
+ updatePackageVersion,
+ generateJsonParsers
+ )
+ }
+
+private val compileProtoToJsName = TaskName.of("compileProtoToJs")
+
+/**
+ * Locates `compileProtoToJs` task in this [TaskContainer].
+ *
+ * The task is responsible for compiling Protobuf messages into JavaScript. It aggregates the tasks
+ * provided by `protobuf` plugin that perform actual compilation.
+ */
+val TaskContainer.compileProtoToJs: TaskProvider
+ get() = named(compileProtoToJsName)
+
+private fun JsTasks.compileProtoToJs() =
+ register(compileProtoToJsName) {
+
+ description = "Compiles Protobuf messages into JavaScript."
+ group = JsTasks.Group.assemble
+
+ withType()
+ .forEach { dependsOn(it) }
+ }
+
+private val installNodePackagesName = TaskName.of("installNodePackages")
+
+/**
+ * Locates `installNodePackages` task in this [TaskContainer].
+ *
+ * The task installs Node packages which this module depends on using `npm install` command.
+ *
+ * The `npm install` command is executed with the vulnerability check disabled since
+ * it cannot fail the task execution despite on vulnerabilities found.
+ *
+ * To check installed Node packages for vulnerabilities execute
+ * [TaskContainer.auditNodePackages] task.
+ *
+ * See [npm-install | npm Docs](https://docs.npmjs.com/cli/v8/commands/npm-install).
+ */
+val TaskContainer.installNodePackages: TaskProvider
+ get() = named(installNodePackagesName)
+
+private fun JsTasks.installNodePackages() =
+ register(installNodePackagesName) {
+
+ description = "Installs module`s Node dependencies."
+ group = JsTasks.Group.assemble
+
+ inputs.file(packageJson)
+ outputs.dir(nodeModules)
+
+ doLast {
+ npm("set", "audit", "false")
+ npm("install")
+ }
+ }
+
+private val updatePackageVersionName = TaskName.of("updatePackageVersion")
+
+/**
+ * Locates `updatePackageVersion` task in this [TaskContainer].
+ *
+ * The task sets the module's version in `package.json` to the value of
+ * [moduleVersion][io.spine.internal.gradle.javascript.JsEnvironment.moduleVersion]
+ * specified in the current `JsEnvironment`.
+ */
+val TaskContainer.updatePackageVersion: TaskProvider
+ get() = named(updatePackageVersionName)
+
+private fun JsTasks.updatePackageVersion() =
+ register(updatePackageVersionName) {
+
+ description = "Sets a module's version in `package.json`."
+ group = JsTasks.Group.assemble
+
+ doLast {
+ val objectNode = ObjectMapper()
+ .readValue(packageJson, ObjectNode::class.java)
+ .put("version", moduleVersion)
+
+ packageJson.writeText(
+
+ // We are going to stick to JSON formatting used by `npm` itself.
+ // So that modifying the line with the version would ONLY affect a single line
+ // when comparing two files i.e. in Git.
+
+ (objectNode.toPrettyString() + '\n')
+ .replace("\" : ", "\": ")
+ )
+ }
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt
new file mode 100644
index 0000000..c15f3e2
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.base.check
+import io.spine.internal.gradle.java.test
+import io.spine.internal.gradle.javascript.isWindows
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import io.spine.internal.gradle.TaskName
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers tasks for verifying a JavaScript module.
+ *
+ * Please note, this task group depends on [assemble] tasks. Therefore, assembling tasks should
+ * be applied in the first place.
+ *
+ * List of tasks to be created:
+ *
+ * 1. [TaskContainer.checkJs].
+ * 2. [TaskContainer.auditNodePackages].
+ * 3. [TaskContainer.testJs].
+ * 4. [TaskContainer.coverageJs].
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.assemble
+ * import io.spine.internal.gradle.javascript.task.check
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * assemble()
+ * check()
+ * }
+ * }
+ * ```
+ *
+ * @param configuration any additional configuration related to the module's verification.
+ */
+fun JsTasks.check(configuration: JsTasks.() -> Unit = {}) {
+
+ auditNodePackages()
+ coverageJs()
+ testJs()
+
+ checkJs().also {
+ check.configure {
+ dependsOn(it)
+ }
+ }
+
+ configuration()
+}
+
+private val checkJsName = TaskName.of("checkJs")
+
+/**
+ * Locates `checkJs` task in this [TaskContainer].
+ *
+ * The task runs tests, audits NPM modules and creates a test-coverage report.
+ */
+val TaskContainer.checkJs: TaskProvider
+ get() = named(checkJsName)
+
+private fun JsTasks.checkJs() =
+ register(checkJsName) {
+
+ description = "Runs tests, audits NPM modules and creates a test-coverage report."
+ group = JsTasks.Group.check
+
+ dependsOn(
+ auditNodePackages,
+ coverageJs,
+ testJs,
+ )
+ }
+
+private val auditNodePackagesName = TaskName.of("auditNodePackages")
+
+/**
+ * Locates `auditNodePackages` task in this [TaskContainer].
+ *
+ * The task audits the module dependencies using the `npm audit` command.
+ *
+ * The `audit` command submits a description of the dependencies configured in the module
+ * to a public registry and asks for a report of known vulnerabilities. If any are found,
+ * then the impact and appropriate remediation will be calculated.
+ *
+ * @see npm-audit | npm Docs
+ */
+val TaskContainer.auditNodePackages: TaskProvider
+ get() = named(auditNodePackagesName)
+
+private fun JsTasks.auditNodePackages() =
+ register(auditNodePackagesName) {
+
+ description = "Audits the module's Node dependencies."
+ group = JsTasks.Group.check
+
+ inputs.dir(nodeModules)
+
+ doLast {
+
+ // `critical` level is set as the minimum level of vulnerability for `npm audit`
+ // to exit with a non-zero code.
+
+ npm("set", "audit-level", "critical")
+
+ try {
+ npm("audit")
+ } catch (ignored: Exception) {
+ npm("audit", "--registry", "https://registry.npmjs.eu")
+ }
+ }
+
+ dependsOn(installNodePackages)
+ }
+
+private val coverageJsName = TaskName.of("coverageJs")
+
+/**
+ * Locates `coverageJs` task in this [TaskContainer].
+ *
+ * The task runs the JavaScript tests and collects the code coverage.
+ */
+val TaskContainer.coverageJs: TaskProvider
+ get() = named(coverageJsName)
+
+private fun JsTasks.coverageJs() =
+ register(coverageJsName) {
+
+ description = "Runs the JavaScript tests and collects the code coverage."
+ group = JsTasks.Group.check
+
+ outputs.dir(nycOutput)
+
+ doLast {
+ npm("run", if (isWindows()) "coverage:win" else "coverage:unix")
+ }
+
+ dependsOn(assembleJs)
+ }
+
+private val testJsName = TaskName.of("testJs")
+
+/**
+ * Locates `testJs` task in this [TaskContainer].
+ *
+ * The task runs JavaScript tests.
+ */
+val TaskContainer.testJs: TaskProvider
+ get() = named(testJsName)
+
+private fun JsTasks.testJs() =
+ register(testJsName) {
+
+ description = "Runs JavaScript tests."
+ group = JsTasks.Group.check
+
+ doLast {
+ npm("run", "test")
+ }
+
+ dependsOn(assembleJs)
+ mustRunAfter(test)
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt
new file mode 100644
index 0000000..4680591
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.base.clean
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import io.spine.internal.gradle.TaskName
+import org.gradle.api.tasks.Delete
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers tasks for deleting output of JavaScript builds.
+ *
+ * Please note, this task group depends on [assemble] tasks. Therefore, assembling tasks should
+ * be applied in the first place.
+ *
+ * List of tasks to be created:
+ *
+ * 1. [TaskContainer.cleanJs].
+ * 2. [TaskContainer.cleanGenerated].
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.assemble
+ * import io.spine.internal.gradle.javascript.task.clean
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * assemble()
+ * clean()
+ * }
+ * }
+ * ```
+ */
+fun JsTasks.clean() {
+
+ cleanGenerated()
+
+ cleanJs().also {
+ clean.configure {
+ dependsOn(it)
+ }
+ }
+}
+
+private val cleanJsName = TaskName.of("cleanJs", Delete::class)
+
+/**
+ * Locates `cleanJs` task in this [TaskContainer].
+ *
+ * The task deletes output of `assembleJs` task and output of its dependants.
+ */
+val TaskContainer.cleanJs: TaskProvider
+ get() = named(cleanJsName)
+
+private fun JsTasks.cleanJs() =
+ register(cleanJsName) {
+
+ description = "Cleans output of `assembleJs` task and output of its dependants."
+ group = JsTasks.Group.clean
+
+ delete(
+ assembleJs.map { it.outputs },
+ compileProtoToJs.map { it.outputs },
+ installNodePackages.map { it.outputs },
+ )
+
+ dependsOn(
+ cleanGenerated
+ )
+ }
+
+private val cleanGeneratedName = TaskName.of("cleanGenerated", Delete::class)
+
+/**
+ * Locates `cleanGenerated` task in this [TaskContainer].
+ *
+ * The task deletes directories with generated code and reports.
+ */
+val TaskContainer.cleanGenerated: TaskProvider
+ get() = named(cleanGeneratedName)
+
+private fun JsTasks.cleanGenerated() =
+ register(cleanGeneratedName) {
+
+ description = "Cleans generated code and reports."
+ group = JsTasks.Group.clean
+
+ delete(
+ genProtoMain,
+ genProtoTest,
+ nycOutput,
+ )
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt
new file mode 100644
index 0000000..cf94995
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.TaskName
+import io.spine.internal.gradle.base.build
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+private val integrationTestName = TaskName.of("integrationTest")
+
+/**
+ * Locates `integrationTest` task in this [TaskContainer].
+ *
+ * The task runs integration tests of the `spine-web` library against
+ * a sample Spine-based application.
+ *
+ * A sample Spine-based application is run from the `test-app` module before integration
+ * tests and is stopped as the tests complete.
+ *
+ * See also: `./integration-tests/README.MD`
+ */
+val TaskContainer.integrationTest: TaskProvider
+ get() = named(integrationTestName)
+
+/**
+ * Registers [TaskContainer.integrationTest] task.
+ *
+ * The task runs integration tests of the `spine-web` library against
+ * a sample Spine-based application.
+ *
+ * Please note, this task depends on [assemble] and `client-js:publishJsLocally` tasks.
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.integrationTest
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * assemble()
+ * integrationTest()
+ * }
+ * }
+ * ```
+ */
+@Suppress("unused")
+fun JsTasks.integrationTest() {
+
+ linkSpineWebModule()
+
+ register(integrationTestName) {
+
+ // Find a way to run the same tests against `spine-web` in `client-js` module
+ // to recover coverage.
+ // See issue: https://github.com/SpineEventEngine/web/issues/96
+
+ description = "Runs integration tests of the `spine-web` library " +
+ "against the sample application."
+ group = JsTasks.Group.check
+
+ dependsOn(build, linkSpineWebModule, ":test-app:appBeforeIntegrationTest")
+
+ doLast {
+ npm("run", "test")
+ }
+
+ finalizedBy(":test-app:appAfterIntegrationTest")
+ }
+}
+
+private val linkSpineWebModuleName = TaskName.of("linkSpineWebModule")
+
+/**
+ * Locates `linkSpineWebModule` task in this [TaskContainer].
+ *
+ * The task installs unpublished artifact of `spine-web` library as a module dependency.
+ *
+ * Creates a symbolic link from globally-installed `spine-web` library to `node_modules` of
+ * the current project.
+ *
+ * See also: [npm-link | npm Docs](https://docs.npmjs.com/cli/v8/commands/npm-link)
+ */
+val TaskContainer.linkSpineWebModule: TaskProvider
+ get() = named(linkSpineWebModuleName)
+
+private fun JsTasks.linkSpineWebModule() =
+ register(linkSpineWebModuleName) {
+
+ description = "Install unpublished artifact of `spine-web` library as a module dependency."
+ group = JsTasks.Group.assemble
+
+ dependsOn(":client-js:publishJsLocally")
+
+ doLast {
+ npm("run", "installLinkedLib")
+ }
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt
new file mode 100644
index 0000000..b13d996
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.javascript.JsEnvironment
+import io.spine.internal.gradle.javascript.JsContext
+import org.gradle.api.Project
+import org.gradle.api.tasks.TaskContainer
+
+/**
+ * A scope for registering and configuring JavaScript-related tasks.
+ *
+ * The scope provides:
+ *
+ * 1. Access to the current [JsContext].
+ * 2. Project's [TaskContainer].
+ * 3. Default task groups.
+ *
+ * Supposing, one needs to create a new task that would participate in building. Let the task name
+ * be `bundleJs`. To do that, several steps should be completed:
+ *
+ * 1. Define the task name and type using [TaskName][io.spine.internal.gradle.TaskName].
+ * 2. Create a public typed reference for the task upon [TaskContainer]. It would facilitate
+ * referencing to the new task, so that external tasks could depend on it. This reference
+ * should be documented.
+ * 3. Implement an extension upon [JsTasks] to register the task.
+ * 4. Call the resulted extension from `build.gradle.kts`.
+ *
+ * Here's an example of `bundleJs()` extension:
+ *
+ * ```
+ * import io.spine.internal.gradle.named
+ * import io.spine.internal.gradle.register
+ * import io.spine.internal.gradle.TaskName
+ * import org.gradle.api.Task
+ * import org.gradle.api.tasks.TaskContainer
+ * import org.gradle.api.tasks.Exec
+ *
+ * // ...
+ *
+ * private val bundleJsName = TaskName.of("bundleJs", Exec::class)
+ *
+ * /**
+ * * Locates `bundleJs` task in this [TaskContainer].
+ * *
+ * * The task bundles JS sources using `webpack` tool.
+ * */
+ * val TaskContainer.bundleJs: TaskProvider
+ * get() = named(bundleJsName)
+ *
+ * fun JsTasks.bundleJs() =
+ * register(bundleJsName) {
+ *
+ * description = "Bundles JS sources using `webpack` tool."
+ * group = JsTasks.Group.build
+ *
+ * // ...
+ * }
+ * ```
+ *
+ * And here's how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.bundleJs
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * bundleJs()
+ * }
+ * }
+ * ```
+ *
+ * Declaring typed references upon [TaskContainer] is optional. But it is highly encouraged
+ * to reference other tasks by such extensions instead of hard-typed string values.
+ */
+class JsTasks(jsEnv: JsEnvironment, project: Project)
+ : JsContext(jsEnv, project), TaskContainer by project.tasks
+{
+ /**
+ * Default task groups for tasks that participate in building a JavaScript module.
+ *
+ * @see [org.gradle.api.Task.getGroup]
+ */
+ internal object Group {
+ const val assemble = "JavaScript/Assemble"
+ const val check = "JavaScript/Check"
+ const val clean = "JavaScript/Clean"
+ const val build = "JavaScript/Build"
+ const val publish = "JavaScript/Publish"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt
new file mode 100644
index 0000000..2a868d4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import io.spine.internal.gradle.report.license.generateLicenseReport
+import io.spine.internal.gradle.TaskName
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers [npmLicenseReport] task for including NPM dependencies into license reports.
+ *
+ * The task depends on [generateLicenseReport].
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.clean
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * licenseReport()
+ * }
+ * }
+ * ```
+ */
+@Suppress("unused")
+fun JsTasks.licenseReport() {
+ npmLicenseReport().also {
+ generateLicenseReport.configure {
+ finalizedBy(it)
+ }
+ }
+}
+
+private val npmLicenseReportName = TaskName.of("npmLicenseReport")
+
+/**
+ * Locates `npmLicenseReport` task in this [TaskContainer].
+ *
+ * The task generates the report on NPM dependencies and their licenses.
+ */
+val TaskContainer.npmLicenseReport: TaskProvider
+ get() = named(npmLicenseReportName)
+
+private fun JsTasks.npmLicenseReport() =
+ register(npmLicenseReportName) {
+
+ description = "Generates the report on NPM dependencies and their licenses."
+ group = JsTasks.Group.build
+
+ doLast {
+
+ // The script below generates license report for NPM dependencies and appends it
+ // to the report for Java dependencies generated by `generateLicenseReport` task.
+
+ npm("run", "license-report")
+ }
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt
new file mode 100644
index 0000000..01a67f5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.publish.publish
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import io.spine.internal.gradle.TaskName
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Registers tasks for publishing a JavaScript module.
+ *
+ * Please note, this task group depends on [assemble] tasks. Therefore, assembling tasks should
+ * be applied in the first place.
+ *
+ * List of tasks to be created:
+ *
+ * 1. [TaskContainer.publishJs].
+ * 2. [TaskContainer.publishJsLocally].
+ * 3. [TaskContainer.prepareJsPublication].
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.assemble
+ * import io.spine.internal.gradle.javascript.task.publish
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * assemble()
+ * publish()
+ * }
+ * }
+ * ```
+ */
+fun JsTasks.publish() {
+
+ transpileSources()
+ prepareJsPublication()
+ publishJsLocally()
+
+ publishJs().also {
+ publish.configure {
+ dependsOn(it)
+ }
+ }
+}
+
+private val transpileSourcesName = TaskName.of("transpileSources")
+
+/**
+ * Locates `transpileSources` task in this [TaskContainer].
+ *
+ * The task transpiles JavaScript sources using Babel before their publishing.
+ */
+val TaskContainer.transpileSources: TaskProvider
+ get() = named(transpileSourcesName)
+
+private fun JsTasks.transpileSources() =
+ register(transpileSourcesName) {
+
+ description = "Transpiles JavaScript sources using Babel before their publishing."
+ group = JsTasks.Group.publish
+
+ doLast {
+ npm("run", "transpile-before-publish")
+ }
+ }
+
+private val prepareJsPublicationName = TaskName.of("prepareJsPublication")
+
+/**
+ * Locates `prepareJsPublication` task in this [TaskContainer].
+ *
+ * This is a lifecycle task that prepares the NPM package in
+ * [publicationDirectory][io.spine.internal.gradle.javascript.JsEnvironment.publicationDir]
+ * of the current `JsEnvironment`.
+ */
+val TaskContainer.prepareJsPublication: TaskProvider
+ get() = named(prepareJsPublicationName)
+
+private fun JsTasks.prepareJsPublication() =
+ register(prepareJsPublicationName) {
+
+ description = "Prepares the NPM package for publishing."
+ group = JsTasks.Group.publish
+
+ // We need to copy two files into a destination directory without overwriting its content.
+ // Default `Copy` task is not used since it overwrites the content of a destination
+ // when copying there.
+ // See issue: https://github.com/gradle/gradle/issues/1012
+
+ doLast {
+ project.copy {
+ from(
+ packageJson,
+ npmrc
+ )
+
+ into(publicationDir)
+ }
+ }
+
+ dependsOn(
+ assembleJs,
+ transpileSources
+ )
+ }
+
+private val publishJsLocallyName = TaskName.of("publishJsLocally")
+
+/**
+ * Locates `publishJsLocally` task in this [TaskContainer].
+ *
+ * The task publishes the prepared NPM package locally using `npm link`.
+ *
+ * @see npm-link | npm Docs
+ */
+val TaskContainer.publishJsLocally: TaskProvider
+ get() = named(publishJsLocallyName)
+
+private fun JsTasks.publishJsLocally() =
+ register(publishJsLocallyName) {
+
+ description = "Publishes the NPM package locally with `npm link`."
+ group = JsTasks.Group.publish
+
+ doLast {
+ publicationDir.npm("link")
+ }
+
+ dependsOn(prepareJsPublication)
+ }
+
+private val publishJsName = TaskName.of("publishJs")
+
+/**
+ * Locates `publishJs` task in this [TaskContainer].
+ *
+ * The task publishes the prepared NPM package from
+ * [publicationDirectory][io.spine.internal.gradle.javascript.JsEnvironment.publicationDir]
+ * using `npm publish`.
+ *
+ * Please note, in order to publish an NMP package, a valid
+ * [npmAuthToken][io.spine.internal.gradle.javascript.JsEnvironment.npmAuthToken] should be
+ * set. If no token is set, a default dummy value is quite enough for the local development.
+ *
+ * @see npm-publish | npm Docs
+ */
+val TaskContainer.publishJs: TaskProvider
+ get() = named(publishJsName)
+
+private fun JsTasks.publishJs() =
+ register(publishJsName) {
+
+ description = "Publishes the NPM package with `npm publish`."
+ group = JsTasks.Group.publish
+
+ doLast {
+ publicationDir.npm("publish")
+ }
+
+ dependsOn(prepareJsPublication)
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt
new file mode 100644
index 0000000..99da3f1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.TaskName
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Configures `assembleJs` task and creates `copyBundledJs` task to work with `webpack` bundler.
+ *
+ * Please note, this task group depends on [assemble] and [publish] tasks. Therefore, those tasks
+ * should be applied in the first place.
+ *
+ * In particular, this method:
+ *
+ * 1. Extends `assembleJs` task to bundle sources during assembling.
+ * 2. Creates `copyBundledJs` task and binds it to `prepareJsPublication` task execution.
+ *
+ * Here's an example of how to apply it in `build.gradle.kts`:
+ *
+ * ```
+ * import io.spine.internal.gradle.javascript.javascript
+ * import io.spine.internal.gradle.javascript.task.assemble
+ * import io.spine.internal.gradle.javascript.task.publish
+ * import io.spine.internal.gradle.javascript.task.webpack
+ *
+ * // ...
+ *
+ * javascript {
+ * tasks {
+ * assemble()
+ * publish()
+ * webpack()
+ * }
+ * }
+ * ```
+ */
+@Suppress("unused")
+fun JsTasks.webpack() {
+
+ assembleJs.configure {
+
+ outputs.dir(webpackOutput)
+
+ doLast {
+ npm("run", "build")
+ npm("run", "build-dev")
+ }
+ }
+
+ // Temporarily don't publish a bundle.
+ // See: https://github.com/SpineEventEngine/web/issues/61
+
+ copyBundledJs()/*.also {
+ prepareJsPublication.configure {
+ dependsOn(it)
+ }
+ }*/
+}
+
+private val copyBundledJsName = TaskName.of("copyBundledJs", Copy::class)
+
+/**
+ * Locates `copyBundledJs` task in this [TaskContainer].
+ *
+ * The task copies bundled JavaScript sources to the publication directory.
+ */
+@Suppress("unused")
+val TaskContainer.copyBundledJs: TaskProvider
+ get() = named(copyBundledJsName)
+
+private fun JsTasks.copyBundledJs() =
+ register(copyBundledJsName) {
+
+ description = "Copies bundled JavaScript sources to the NPM publication directory."
+ group = JsTasks.Group.publish
+
+ from(assembleJs.map { it.outputs })
+ into(webpackPublicationDir)
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt
new file mode 100644
index 0000000..b2964ce
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.kotlin
+
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+/**
+ * Sets [Java toolchain](https://kotlinlang.org/docs/gradle.html#gradle-java-toolchains-support)
+ * to the specified version (e.g. 11 or 8).
+ */
+fun KotlinJvmProjectExtension.applyJvmToolchain(version: Int) {
+ jvmToolchain {
+ languageVersion.set(JavaLanguageVersion.of(version))
+ }
+}
+
+/**
+ * Sets [Java toolchain](https://kotlinlang.org/docs/gradle.html#gradle-java-toolchains-support)
+ * to the specified version (e.g. "11" or "8").
+ */
+@Suppress("unused")
+fun KotlinJvmProjectExtension.applyJvmToolchain(version: String) =
+ applyJvmToolchain(version.toInt())
+
+/**
+ * Opts-in to experimental features that we use in our codebase.
+ */
+@Suppress("unused")
+fun KotlinCompile.setFreeCompilerArgs() {
+ kotlinOptions {
+ freeCompilerArgs = listOf(
+ "-Xskip-prerelease-check",
+ "-Xjvm-default=all",
+ "-Xinline-classes",
+ "-opt-in=" +
+ "kotlin.contracts.ExperimentalContracts," +
+ "kotlin.io.path.ExperimentalPathApi," +
+ "kotlin.ExperimentalUnsignedTypes," +
+ "kotlin.ExperimentalStdlibApi," +
+ "kotlin.experimental.ExperimentalTypeInference",
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/protobuf/ProtoTaskExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/protobuf/ProtoTaskExtensions.kt
new file mode 100644
index 0000000..0c4c8b4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/protobuf/ProtoTaskExtensions.kt
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.protobuf
+
+import com.google.protobuf.gradle.GenerateProtoTask
+import io.spine.internal.gradle.sourceSets
+import java.io.File
+import java.nio.file.Files
+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.configurationcache.extensions.capitalized
+import org.gradle.kotlin.dsl.get
+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.dsl.KotlinCompile
+
+/**
+ * Obtains the name of the `generated` directory under the project root directory.
+ */
+private val Project.generatedDir: String
+ get() = "${projectDir}/generated"
+
+/**
+ * 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 protobuf code generation task for the code which cannot use Spine Model Compiler
+ * (e.g. the `base` project).
+ *
+ * The task configuration consists of the following steps:
+ *
+ * 1. Adding `"kotlin"` to the list of involved `protoc` builtins.
+ *
+ * 2. Generation of descriptor set file is turned on 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 {
+ * for (task in all()) {
+ * task.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()
+ configureIdeaDirs()
+}
+
+/**
+ * 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 per 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 in order 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.*/)
+private fun GenerateProtoTask.setupDescriptorSetFileCreation() {
+ // Tell `protoc` generate descriptor set file.
+ // The name of the generated file reflects project's Maven coordinates.
+ val ssn = sourceSet.name
+ generateDescriptorSet = true
+ val descriptorsDir = "${project.buildDir}/descriptors/${ssn}"
+ val descriptorName = project.descriptorSetName(sourceSet)
+ with(descriptorSetOptions) {
+ path = "$descriptorsDir/$descriptorName"
+ includeImports = true
+ includeSourceInfo = true
+ }
+
+ // Make the descriptor set file included 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.
+ */
+private 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()
+ 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.
+ */
+private fun GenerateProtoTask.setupKotlinCompile() {
+ val kotlinCompile = project.kotlinCompileFor(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.
+ */
+private 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.capitalized()
+ return "process${infix}Resources"
+}
+
+/**
+ * Attempts to obtain the Kotlin compilation Gradle task for the given source set.
+ *
+ * Typically, the task is named by a pattern: `compileKotlin`, or just
+ * `compileKotlin` if the source set name is `"main"`. If the task does not fit this described
+ * pattern, this method will not find it.
+ */
+private fun Project.kotlinCompileFor(sourceSet: SourceSet): KotlinCompile<*>? {
+ val taskName = sourceSet.getCompileTaskName("Kotlin")
+ return tasks.findByName(taskName) as KotlinCompile<*>?
+}
+
+private fun File.residesIn(directory: File): Boolean =
+ canonicalFile.startsWith(directory.absolutePath)
+
+private fun GenerateProtoTask.configureIdeaDirs() = project.plugins.withId("idea") {
+ val module = project.extensions.findByType(IdeaModel::class.java)!!.module
+
+ // Make IDEA forget about sources under `outputBaseDir`.
+ val protocOutputDir = File(outputBaseDir).parentFile
+ module.generatedSourceDirs.removeIf { dir ->
+ dir.residesIn(protocOutputDir)
+ }
+
+ module.sourceDirs.removeIf { dir ->
+ dir.residesIn(protocOutputDir)
+ }
+
+ val javaDir = generatedDir("java")
+ val kotlinDir = generatedDir("kotlin")
+
+ // 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'.
+ project.tasks.withType(GenerateIdeaModule::class.java).forEach {
+ it.doFirst {
+ javaDir.mkdirs()
+ kotlinDir.mkdirs()
+ }
+ }
+
+ if (isTest) {
+ module.testSources.run {
+ from(javaDir)
+ from(kotlinDir)
+ }
+ } else {
+ module.sourceDirs.run {
+ add(javaDir)
+ add(kotlinDir)
+ }
+ }
+
+ module.generatedSourceDirs.run {
+ add(javaDir)
+ add(kotlinDir)
+ }
+}
+
+/**
+ * Prints diagnostic output of `sourceDirs` and `generatedSourceDirs` of an [IdeaModule].
+ *
+ * The warning `"unused"` is suppressed because this function is not used in
+ * the production mode.
+ */
+@Suppress("unused")
+private fun IdeaModule.printSourceDirectories() {
+ println("**** [IDEA] Source directories:")
+ sourceDirs.forEach { println(it) }
+ println()
+ println("**** [IDEA] Generated source directories:")
+ generatedSourceDirs.forEach { println(it) }
+ println()
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
new file mode 100644
index 0000000..a81cbdc
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+import com.fasterxml.jackson.dataformat.xml.XmlMapper
+import io.spine.internal.gradle.Repository
+import java.io.FileNotFoundException
+import java.net.URL
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * A task which verifies that the current version of the library has not been published to the given
+ * Maven repository yet.
+ */
+open class CheckVersionIncrement : DefaultTask() {
+
+ /**
+ * The Maven repository in which to look for published artifacts.
+ *
+ * We check both the `releases` and `snapshots` repositories. Artifacts in either of these repos
+ * may not be overwritten.
+ */
+ @Input
+ lateinit var repository: Repository
+
+ @Input
+ val version: String = project.version as String
+
+ @TaskAction
+ fun fetchAndCheck() {
+ val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}"
+ checkInRepo(repository.snapshots, artifact)
+
+ if (repository.releases != repository.snapshots) {
+ checkInRepo(repository.releases, artifact)
+ }
+ }
+
+ private fun checkInRepo(repoUrl: String, artifact: String) {
+ val metadata = fetch(repoUrl, artifact)
+ val versions = metadata?.versioning?.versions
+ val versionExists = versions?.contains(version) ?: false
+ if (versionExists) {
+ throw GradleException("""
+ Version `$version` is already published to maven repository `$repoUrl`.
+ Try incrementing the library version.
+ 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")
+ return MavenMetadata.fetchAndParse(url)
+ }
+
+ private fun Project.artifactPath(): String {
+ val group = this.group as String
+ val name = "spine-${this.name}"
+
+ val pathElements = ArrayList(group.split('.'))
+ pathElements.add(name)
+ val path = pathElements.joinToString(separator = "/")
+ return path
+ }
+}
+
+private data class MavenMetadata(var versioning: Versioning = Versioning()) {
+
+ companion object {
+
+ const val FILE_NAME = "maven-metadata.xml"
+
+ private val mapper = XmlMapper()
+
+ init {
+ mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
+ }
+
+ /**
+ * Fetches the metadata for the repository and parses the document.
+ *
+ *
If the document could not be found, assumes that the module was never
+ * released and thus has no metadata.
+ */
+ fun fetchAndParse(url: URL): MavenMetadata? {
+ return try {
+ val metadata = mapper.readValue(url, MavenMetadata::class.java)
+ metadata
+ } catch (ignored: FileNotFoundException) {
+ null
+ }
+ }
+ }
+}
+
+private data class Versioning(var versions: List = listOf())
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt
new file mode 100644
index 0000000..4751d56
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import com.google.auth.oauth2.GoogleCredentials
+import com.google.cloud.artifactregistry.auth.DefaultCredentialProvider
+import io.spine.internal.gradle.Credentials
+import io.spine.internal.gradle.Repository
+import java.io.IOException
+import org.gradle.api.Project
+
+/**
+ * The experimental Google Cloud Artifact Registry repository.
+ *
+ * In order to successfully publish into this repository, a service account key is needed.
+ * The published must create a service account, grant it the permission to write into
+ * Artifact Registry, and generate a JSON key.
+ * Then, the key must be placed somewhere on the file system and the environment variable
+ * `GOOGLE_APPLICATION_CREDENTIALS` must be set to point at the key file.
+ * Once these preconditions are met, publishing becomes possible.
+ *
+ * Google provides a Gradle plugin for configuring the publishing repository credentials
+ * automatically. We achieve the same goal by assembling the credentials manually. We do so
+ * in order to fit the Google Cloud Artifact Registry repository into the standard frame of
+ * the Maven [Repository]-s. Applying the plugin would take a substantial effort due to the fact
+ * that both our publishing scripts and the Google's plugin use `afterEvaluate { }` hooks.
+ * 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.
+ */
+internal object CloudArtifactRegistry {
+
+ private const val spineRepoLocation = "https://europe-maven.pkg.dev/spine-event-engine"
+
+ val repository = Repository(
+ releases = "${spineRepoLocation}/releases",
+ snapshots = "${spineRepoLocation}/snapshots",
+ credentialValues = this::fetchGoogleCredentials
+ )
+
+ private fun fetchGoogleCredentials(p: Project): Credentials? {
+ return try {
+ val googleCreds = DefaultCredentialProvider()
+ val creds = googleCreds.credential as GoogleCredentials
+ creds.refreshIfExpired()
+ Credentials("oauth2accesstoken", creds.accessToken.tokenValue)
+ } catch (e: IOException) {
+ p.logger.info("Unable to fetch credentials for Google Cloud Artifact Registry." +
+ " Reason: '${e.message}'." +
+ " The debug output may contain more details.")
+ null
+ }
+ }
+}
+
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt
new file mode 100644
index 0000000..148ebce
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.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
+ * public repository, and publishing via private one. Their URLs differ in `/public` infix.
+ */
+internal object CloudRepo {
+
+ private const val name = "CloudRepo"
+ private const val credentialsFile = "cloudrepo.properties"
+ private const val publicUrl = "https://spine.mycloudrepo.io/public/repositories"
+ private val privateUrl = publicUrl.replace("/public", "")
+
+ /**
+ * CloudRepo repository for fetching of artifacts.
+ *
+ * Use this instance to depend on artifacts from this repository.
+ */
+ val published = Repository(
+ name = name,
+ releases = "$publicUrl/releases",
+ snapshots = "$publicUrl/snapshots",
+ credentialsFile = credentialsFile
+ )
+
+ /**
+ * CloudRepo repository for publishing of artifacts.
+ *
+ * Use this instance to push new artifacts to this repository.
+ */
+ val destination = Repository(
+ name = name,
+ releases = "$privateUrl/releases",
+ snapshots = "$privateUrl/snapshots",
+ credentialsFile = credentialsFile
+ )
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt
new file mode 100644
index 0000000..fef0682
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Credentials
+import io.spine.internal.gradle.Repository
+import net.lingala.zip4j.ZipFile
+import org.gradle.api.Project
+
+/**
+ * Maven repositories of Spine Event Engine projects hosted at GitHub Packages.
+ */
+internal object GitHubPackages {
+
+ /**
+ * Obtains an instance of the GitHub Packages repository with the given name.
+ */
+ fun repository(repoName: String): Repository {
+ val githubActor: String = actor()
+ 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) }
+ )
+ }
+
+ private fun actor(): String {
+ var githubActor: String? = System.getenv("GITHUB_ACTOR")
+ githubActor = if (githubActor.isNullOrEmpty()) {
+ "developers@spine.io"
+ } else {
+ githubActor
+ }
+ return githubActor
+ }
+}
+
+/**
+ * This is a trick. Gradle only supports password or AWS credentials.
+ * Thus, we pass the GitHub token as a "password".
+ *
+ * See https://docs.github.com/en/actions/guides/publishing-java-packages-with-gradle#publishing-packages-to-github-packages
+ */
+private fun Project.credentialsWithToken(githubActor: String) = Credentials(
+ username = githubActor,
+ password = readGitHubToken()
+)
+
+private fun Project.readGitHubToken(): String {
+ val githubToken: String? = System.getenv("GITHUB_TOKEN")
+ return if (githubToken.isNullOrEmpty()) {
+ readTokenFromArchive()
+ } else {
+ githubToken
+ }
+}
+
+/**
+ * Read the personal access token for the `developers@spine.io` account which
+ * has only the permission to read public GitHub packages.
+ *
+ * The token is extracted from the archive called `aus.weis` stored under `buildSrc`.
+ * The archive has such an unusual name to avoid scanning for tokens placed in repositories
+ * which is performed by GitHub. Since we do not violate any security, it is OK to
+ * use such a workaround.
+ */
+private fun Project.readTokenFromArchive(): String {
+ val targetDir = "${buildDir}/token"
+ file(targetDir).mkdirs()
+ val fileToUnzip = "${rootDir}/buildSrc/aus.weis"
+
+ logger.info(
+ "GitHub Packages: reading token by unzipping `$fileToUnzip` into `$targetDir`."
+ )
+ ZipFile(fileToUnzip, "123".toCharArray()).extractAll(targetDir)
+ val file = file("$targetDir/token.txt")
+ val result = file.readText()
+ return result
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
new file mode 100644
index 0000000..b97a5ee
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+@file:Suppress("unused")
+
+package io.spine.internal.gradle.publish
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * Gradle plugin which adds a [CheckVersionIncrement] task.
+ *
+ * The task is called `checkVersionIncrement` inserted before the `check` task.
+ */
+class IncrementGuard : Plugin {
+
+ companion object {
+ const val taskName = "checkVersionIncrement"
+ }
+
+ /**
+ * Adds the [CheckVersionIncrement] task to the project.
+ *
+ * Only adds the check if the project is built on Travis CI and the job is a pull request.
+ *
+ * The task only runs on non-master branches on GitHub Actions. This is done
+ * to prevent unexpected CI fails when re-building `master` multiple times, creating git
+ * tags, and in other cases that go outside of the "usual" development cycle.
+ */
+ override fun apply(target: Project) {
+ val tasks = target.tasks
+ tasks.register(taskName, CheckVersionIncrement::class.java) {
+ repository = CloudRepo.published
+ tasks.getByName("check").dependsOn(this)
+
+ shouldRunAfter("test")
+ if (!shouldCheckVersion()) {
+ logger.info(
+ "The build does not represent a GitHub Actions feature branch job, " +
+ "the `checkVersionIncrement` task is disabled."
+ )
+ this.enabled = false
+ }
+ }
+ }
+
+ /**
+ * Returns `true` if the current build is a GitHub Actions build which represents a push
+ * to a feature branch.
+ *
+ * Returns `false` if the associated reference is not a branch (e.g. a tag) or if it has
+ * the name which ends with `master` or `main`.
+ *
+ * For example, on the following branches the method would return `false`:
+ *
+ * 1. `master`.
+ * 2. `main`.
+ * 3. `2.x-jdk8-master`.
+ * 4. `2.x-jdk8-main`.
+ *
+ * @see
+ * List of default environment variables provided for GitHub Actions builds
+ */
+ private fun shouldCheckVersion(): Boolean {
+ val event = System.getenv("GITHUB_EVENT_NAME")
+ val reference = System.getenv("GITHUB_REF")
+ if (event != "push" || reference == null) {
+ return false
+ }
+ val branch = branchName(reference)
+ return when {
+ branch == null -> false
+ branch.endsWith("master") -> false
+ branch.endsWith("main") -> false
+ else -> true
+ }
+ }
+
+ private fun branchName(gitHubRef: String): String? {
+ val matches = Regex("refs/heads/(.+)").matchEntire(gitHubRef)
+ val branch = matches?.let { it.groupValues[1] }
+ return branch
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/JarDsl.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/JarDsl.kt
new file mode 100644
index 0000000..2bc2870
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/JarDsl.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.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.
+ *
+ * This artifact contains compilation output of `test` source set. By default, it is not published.
+ *
+ * Take a look on [SpinePublishing.testJar] for a usage example.
+
+ * @see [artifacts]
+ */
+class TestJar {
+
+ /**
+ * Set of modules, for which a test JAR will be published.
+ */
+ var inclusions: Set = emptySet()
+
+ /**
+ * Enables test JAR publishing for all published modules.
+ */
+ 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.
+ */
+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()
+ val addTestJar = testJar.inclusions.contains(projectName) || testJar.enabled
+ return JarFlags(
+ sourcesJar = true,
+ javadocJar = true,
+ addProtoJar, addTestJar,
+ dokkaJar.kotlin, dokkaJar.java
+ )
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoExts.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoExts.kt
new file mode 100644
index 0000000..97e3319
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoExts.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.sourceSets
+import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.file.SourceDirectorySet
+import org.gradle.api.tasks.bundling.Jar
+
+/**
+ * Tells whether there are any Proto sources in "main" source set.
+ */
+internal fun Project.hasProto(): Boolean {
+ val protoSources = protoSources()
+ val result = protoSources.any { it.exists() }
+ return result
+}
+
+/**
+ * Locates directories with proto sources under the "main" source sets.
+ *
+ * Special treatment for Proto sources is needed, because they are not Java-related, and,
+ * thus, not included in `sourceSets["main"].allSource`.
+ */
+internal fun Project.protoSources(): Set {
+ val mainSourceSets = sourceSets.filter {
+ ss -> ss.name.endsWith("main", ignoreCase = true)
+ }
+
+ val protoExtensions = mainSourceSets.mapNotNull {
+ it.extensions.findByName("proto") as SourceDirectorySet?
+ }
+
+ val protoDirs = mutableSetOf()
+ protoExtensions.forEach {
+ protoDirs.addAll(it.srcDirs)
+ }
+
+ return protoDirs
+}
+
+/**
+ * Checks if the given file belongs to the Google `.proto` sources.
+ */
+internal fun FileTreeElement.isGoogleProtoSource(): Boolean {
+ val pathSegments = relativePath.segments
+ return pathSegments.isNotEmpty() && pathSegments[0].equals("google")
+}
+
+/**
+ * The reference to the `generateProto` task of a `main` source set.
+ */
+internal fun Project.generateProto(): Task? = tasks.findByName("generateProto")
+
+/**
+ * Makes this [Jar] task depend on the [generateProto] task, if it exists in the same project.
+ */
+internal fun Jar.dependOnGenerateProto() {
+ project.generateProto()?.let {
+ this.dependsOn(it)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Publications.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Publications.kt
new file mode 100644
index 0000000..12c0563
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Publications.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Repository
+import io.spine.internal.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.
+ */
+ 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)
+ }
+}
+
+/**
+ * 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 ".pom" extension.
+ * 3. Gradle's metadata file that has ".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).
+ * 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/internal/gradle/publish/PublishingExts.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingExts.kt
new file mode 100644
index 0000000..78e7395
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingExts.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import dokkaKotlinJar
+import io.spine.internal.gradle.Repository
+import io.spine.internal.gradle.sourceSets
+import java.util.*
+import org.gradle.api.InvalidUserDataException
+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.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.kotlin.dsl.get
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.named
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.the
+import org.gradle.kotlin.dsl.withType
+
+/**
+ * Obtains [PublishingExtension] of this project.
+ */
+internal val Project.publishingExtension: PublishingExtension
+ get() = extensions.getByType()
+
+/**
+ * Obtains [PublicationContainer] of this project.
+ */
+internal val Project.publications: PublicationContainer
+ get() = publishingExtension.publications
+
+/**
+ * Obtains [SpinePublishing] extension from the root project.
+ */
+internal val Project.spinePublishing: SpinePublishing
+ get() = this.rootProject.the()
+
+/**
+ * Tells if this project has custom publishing.
+ */
+internal val Project.hasCustomPublishing: Boolean
+ get() = spinePublishing.modulesWithCustomPublishing.contains(name)
+
+private const val PUBLISH_TASK = "publish"
+
+/**
+ * Locates `publish` task in this [TaskContainer].
+ *
+ * This task publishes all defined publications to all defined repositories. To achieve that,
+ * the task depends on all `publish`*PubName*`PublicationTo`*RepoName*`Repository` tasks.
+ *
+ * Please note, task execution would not copy publications to the local Maven cache.
+ *
+ * @see
+ * Tasks | Maven Publish Plugin
+ */
+internal val TaskContainer.publish: TaskProvider
+ get() = named(PUBLISH_TASK)
+
+/**
+ * Sets dependencies for `publish` task in this [Project].
+ *
+ * This method performs the following:
+ *
+ * 1. When this [Project] is not a root, makes `publish` task in a root project
+ * depend on a local `publish`.
+ * 2. Makes local `publish` task verify that credentials are present for each
+ * of destination repositories.
+ */
+internal fun Project.configurePublishTask(destinations: Set) {
+ attachCredentialsVerification(destinations)
+ bindToRootPublish()
+}
+
+private fun Project.attachCredentialsVerification(destinations: Set) {
+ val checkCredentials = tasks.registerCheckCredentialsTask(destinations)
+ val localPublish = tasks.publish
+ localPublish.configure { dependsOn(checkCredentials) }
+}
+
+private fun Project.bindToRootPublish() {
+ if (project == rootProject) {
+ return
+ }
+
+ val localPublish = tasks.publish
+ val rootPublish = rootProject.tasks.getOrCreatePublishTask()
+ rootPublish.configure { dependsOn(localPublish) }
+}
+
+/**
+ * Use this task accessor when it is not guaranteed that the task is present
+ * in this [TaskContainer].
+ */
+private fun TaskContainer.getOrCreatePublishTask(): TaskProvider =
+ if (names.contains(PUBLISH_TASK)) {
+ named(PUBLISH_TASK)
+ } else {
+ register(PUBLISH_TASK)
+ }
+
+private fun TaskContainer.registerCheckCredentialsTask(
+ destinations: Set
+): TaskProvider =
+ register("checkCredentials") {
+ doLast {
+ destinations.forEach { it.ensureCredentials(project) }
+ }
+ }
+
+private fun Repository.ensureCredentials(project: Project) {
+ val credentials = credentials(project)
+ if (Objects.isNull(credentials)) {
+ throw InvalidUserDataException(
+ "No valid credentials for repository `${this}`. Please make sure " +
+ "to pass username/password or a valid `.properties` file."
+ )
+ }
+}
+
+/**
+ * Excludes Google `.proto` sources from all artifacts.
+ *
+ * Goes through all registered `Jar` tasks and filters out Google's files.
+ */
+@Suppress("unused")
+fun TaskContainer.excludeGoogleProtoFromArtifacts() {
+ withType().configureEach {
+ exclude { it.isGoogleProtoSource() }
+ }
+}
+
+/**
+ * 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:
+ *
+ * - Kotlin
+ * - Java
+ * - Proto
+ *
+ * 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") {
+ dependOnGenerateProto()
+ archiveClassifier.set("sources")
+ from(sourceSets["main"].allSource) // Puts Java and Kotlin sources.
+ from(protoSources()) // Puts Proto sources.
+ exclude("desc.ref", "*.desc") // Exclude descriptor files and the descriptor reference.
+}
+
+/**
+ * Locates or creates `protoJar` task in this [Project].
+ *
+ * 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") {
+ dependOnGenerateProto()
+ archiveClassifier.set("proto")
+ from(protoSources())
+}
+
+/**
+ * Locates or creates `testJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains compilation output
+ * of `test` source set.
+ */
+internal fun Project.testJar(): TaskProvider = tasks.getOrCreate("testJar") {
+ archiveClassifier.set("test")
+ from(sourceSets["test"].output)
+}
+
+/**
+ * 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.
+ */
+fun Project.javadocJar(): TaskProvider = tasks.getOrCreate("javadocJar") {
+ archiveClassifier.set("javadoc")
+ from(files("$buildDir/docs/javadoc"))
+ dependsOn("javadoc")
+}
+
+internal fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): TaskProvider =
+ if (names.contains(name)) {
+ named(name)
+ } else {
+ register(name) {
+ init()
+ }
+ }
+
+/**
+ * Obtains as a set of [Jar] tasks, output of which is used as Maven artifacts.
+ *
+ * By default, only a jar with Java compilation output is included into publication. This method
+ * registers tasks which produce additional artifacts according to the values of [jarFlags].
+ *
+ * @return the list of the registered tasks.
+ */
+internal fun Project.artifacts(jarFlags: JarFlags): Set> {
+ val tasks = mutableSetOf>()
+
+ if (jarFlags.sourcesJar) {
+ tasks.add(sourcesJar())
+ }
+
+ if (jarFlags.javadocJar) {
+ tasks.add(javadocJar())
+ }
+
+ // We don't want to have an empty "proto.jar" when a project doesn't have any Proto files.
+ if (hasProto() && jarFlags.publishProtoJar) {
+ tasks.add(protoJar())
+ }
+
+ // Here, we don't have the corresponding `hasTests()` check, since this artifact is disabled
+ // by default. And turning it on means "We have tests and need them to be published."
+ if (jarFlags.publishTestJar) {
+ tasks.add(testJar())
+ }
+
+ if (jarFlags.publishDokkaKotlinJar) {
+ tasks.add(dokkaKotlinJar())
+ }
+
+ return tasks
+}
+
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingRepos.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingRepos.kt
new file mode 100644
index 0000000..36fcac4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingRepos.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Repository
+
+/**
+ * Repositories to which we may publish.
+ */
+object PublishingRepos {
+
+ val cloudRepo = CloudRepo.destination
+
+ val cloudArtifactRegistry = CloudArtifactRegistry.repository
+
+ /**
+ * Obtains a GitHub repository by the given name.
+ */
+ fun gitHub(repoName: String): Repository = GitHubPackages.repository(repoName)
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt
new file mode 100644
index 0000000..f55b85f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt
@@ -0,0 +1,428 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+
+package io.spine.internal.gradle.publish
+
+import dokkaJavaJar
+import dokkaKotlinJar
+import io.spine.internal.gradle.Repository
+import org.gradle.api.Project
+import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.findByType
+
+/**
+ * Configures [SpinePublishing] extension.
+ *
+ * This extension sets up publishing of artifacts to Maven repositories.
+ *
+ * The extension can be configured for single- and multi-module projects.
+ *
+ * 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:
+ *
+ * ```
+ * spinePublishing {
+ * modules = setOf(
+ * "subprojectA",
+ * "subprojectB",
+ * )
+ * destinations = setOf(
+ * PublishingRepos.cloudRepo,
+ * PublishingRepos.cloudArtifactRegistry,
+ * )
+ * }
+ * ```
+ *
+ * When used with a single-module project, the extension should be opened in a project's build file.
+ * Only destinations should be specified:
+ *
+ * ```
+ * spinePublishing {
+ * destinations = setOf(
+ * PublishingRepos.cloudRepo,
+ * PublishingRepos.cloudArtifactRegistry,
+ * )
+ * }
+ * ```
+ *
+ * It is worth to mention, 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
+ * of published modules, the extension will create a [publication][StandardJavaPublicationHandler]
+ * 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
+ * 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
+ * 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.
+ *
+ * @see [artifacts]
+ */
+fun Project.spinePublishing(block: SpinePublishing.() -> Unit) {
+ apply()
+ val name = SpinePublishing::class.java.simpleName
+ val extension = with(extensions) {
+ findByType() ?: create(name, project)
+ }
+ extension.run {
+ block()
+ configured()
+ }
+}
+
+/**
+ * A Gradle extension for setting up publishing of spine modules using `maven-publish` plugin.
+ *
+ * @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.
+ *
+ * @see spinePublishing
+ */
+open class SpinePublishing(private val project: Project) {
+
+ private val protoJar = ProtoJar()
+ private val testJar = TestJar()
+ private val dokkaJar = DokkaJar()
+
+ /**
+ * Set of modules to be published.
+ *
+ * Both the module's name or path can be used.
+ *
+ * Use this property if the extension is configured from a root project's build file.
+ *
+ * If left empty, the [project], in which the extension is opened, will be published.
+ *
+ * Empty by default.
+ */
+ var modules: Set = emptySet()
+
+ /**
+ * Set of modules that have custom publications and do not need standard ones.
+ *
+ * Empty by default.
+ */
+ var modulesWithCustomPublishing: Set = emptySet()
+
+ /**
+ * Set of repositories, to which the resulting artifacts will be sent.
+ *
+ * Usually, Spine-related projects are published to one or more repositories,
+ * declared in [PublishingRepos]:
+ *
+ * ```
+ * destinations = setOf(
+ * PublishingRepos.cloudRepo,
+ * PublishingRepos.cloudArtifactRegistry,
+ * PublishingRepos.gitHub("base"),
+ * )
+ * ```
+ *
+ * Empty by default.
+ */
+ var destinations: Set = emptySet()
+
+ /**
+ * A prefix to be added before the name of each artifact.
+ *
+ * The default value is "spine-".
+ */
+ var artifactPrefix: String = "spine-"
+
+ /**
+ * 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.
+ *
+ * Here's an example of how to enable it for some of the published modules:
+ *
+ * ```
+ * spinePublishing {
+ * modules = setOf(
+ * "subprojectA",
+ * "subprojectB",
+ * )
+ * testJar {
+ * inclusions = setOf(
+ * "subprojectB",
+ * )
+ * }
+ * }
+ * ```
+ *
+ * For all modules, or when the extension is configured within a published module itself:
+ *
+ * ```
+ * spinePublishing {
+ * testJar {
+ * enabled = true
+ * }
+ * }
+ * ```
+ *
+ * The resulting artifact is available under "test" classifier. For example,
+ * in Gradle 7+, one could depend on it like this:
+ *
+ * ```
+ * implementation("io.spine:spine-client:$version@test")
+ * ```
+ */
+ 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.internal.dependency.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.
+ *
+ * On this stage the extension will validate the received configuration and set up
+ * `maven-publish` plugin for each published module.
+ */
+ internal fun configured() {
+ ensureProtoJarExclusionsArePublished()
+ ensureTestJarInclusionsArePublished()
+ ensuresModulesNotDuplicated()
+
+ val projectsToPublish = projectsToPublish()
+ projectsToPublish.forEach { project ->
+ val jarFlags = JarFlags.create(project.name, protoJar, testJar, dokkaJar)
+ project.setUpPublishing(jarFlags)
+ }
+ }
+
+ /**
+ * Maps the names of published modules to [Project] instances.
+ *
+ * The method considers two options:
+ *
+ * 1. The [set][modules] of subprojects to publish is not empty. It means that the extension
+ * is opened from a root project.
+ * 2. The [set][modules] is empty. Then, the [project] in which the extension is opened
+ * will be published.
+ *
+ * @see modules
+ */
+ private fun projectsToPublish(): Collection {
+ if (project.subprojects.isEmpty()) {
+ return setOf(project)
+ }
+ return modules.union(modulesWithCustomPublishing)
+ .map { name -> project.project(name) }
+ .ifEmpty { setOf(project) }
+ }
+
+ /**
+ * Sets up `maven-publish` plugin for the given project.
+ *
+ * Firstly, an instance of [PublicationHandler] is created for the project depending
+ * on the nature of the publication process configured.
+ * Then, this the handler is scheduled to apply on [Project.afterEvaluate].
+ *
+ * General rule of thumb is to avoid using [Project.afterEvaluate] of this closure,
+ * as it configures a project when its configuration is considered completed.
+ * Which is quite counter-intuitive.
+ *
+ * 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.
+ *
+ * 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.
+ */
+ private fun Project.setUpPublishing(jarFlags: JarFlags) {
+ val customPublishing = modulesWithCustomPublishing.contains(name)
+ val handler = if (customPublishing) {
+ CustomPublicationHandler(project, destinations)
+ } else {
+ StandardJavaPublicationHandler(project, jarFlags, destinations)
+ }
+ afterEvaluate {
+ handler.apply()
+ }
+ }
+
+ /**
+ * Obtains an artifact ID for the given project.
+ *
+ * It consists of a project's name and [prefix][artifactPrefix]:
+ * ``.
+ */
+ fun artifactId(project: Project): String = "$artifactPrefix${project.name}"
+
+ /**
+ * Ensures that all modules, marked as excluded from [protoJar] publishing,
+ * are actually published.
+ *
+ * It makes no sense to tell a module don't publish [protoJar] artifact, if the module is not
+ * published at all.
+ */
+ 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")
+ }
+ }
+
+ /**
+ * Ensures that all modules, marked as included into [testJar] publishing,
+ * are actually published.
+ *
+ * It makes no sense to tell a module publish [testJar] artifact, if the module is not
+ * published at all.
+ */
+ private fun ensureTestJarInclusionsArePublished() {
+ val nonPublishedInclusions = testJar.inclusions.minus(modules)
+ if (nonPublishedInclusions.isNotEmpty()) {
+ error(
+ "One or more modules are marked as `included into test JAR publication`," +
+ " but they are not even published: $nonPublishedInclusions."
+ )
+ }
+ }
+
+ /**
+ * 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.
+ * Here we verify that publishing of a module is not configured in both places simultaneously.
+ */
+ private fun ensuresModulesNotDuplicated() {
+ val rootProject = project.rootProject
+ if (rootProject == project) {
+ return
+ }
+
+ val rootExtension = with(rootProject.extensions) { findByType() }
+ rootExtension?.let { rootPublishing ->
+ val thisProject = setOf(project.name, project.path)
+ if (thisProject.minus(rootPublishing.modules).size != 2) {
+ error(
+ "Publishing of `$thisProject` module is already configured in a root project!"
+ )
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt
new file mode 100644
index 0000000..4b0a9cc
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import io.spine.internal.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
+import org.gradle.api.tasks.SourceSetOutput
+
+/**
+ * Serves to distinguish the `.java` and `.class` files built on top of the Protobuf definitions
+ * from the human-created production code.
+ *
+ * Works on top of the passed [source][srcDirs] and [output][outputDirs] directories, by analyzing
+ * the source file names and finding the corresponding compiler output.
+ */
+internal class CodebaseFilter(
+ private val project: Project,
+ private val srcDirs: Set,
+ private val outputDirs: Set
+) {
+
+ /**
+ * Returns the file tree containing the compiled `.class` files which were produced
+ * from the human-written production code.
+ *
+ * Such filtering excludes the output obtained from the generated sources.
+ */
+ internal fun humanProducedCompiledFiles(): List {
+ log("Source dirs for the code coverage calculation:")
+ this.srcDirs.forEach {
+ log(" - $it")
+ }
+
+ val generatedClassNames = generatedClassNames()
+ val humanProducedTree = outputDirs
+ .stream()
+ .flatMap { it.classesDirs.files.stream() }
+ .map { srcFile ->
+ log("Filtering out the generated classes for ${srcFile}.")
+ project.fileTree(srcFile).without(generatedClassNames)
+ }.toList()
+ return humanProducedTree
+ }
+
+ private fun generatedClassNames(): List {
+ val generatedSourceFiles = generatedOnly(srcDirs)
+ val generatedNames = mutableListOf()
+ generatedSourceFiles
+ .filter { it.exists() && it.isDirectory }
+ .forEach { folder ->
+ folder.walk()
+ .filter { !it.isDirectory }
+ .forEach { file ->
+ file.parseName(
+ File::asJavaClassName,
+ File::asGrpcClassName,
+ File::asSpineClassName
+ )?.let { clsName ->
+ generatedNames.add(clsName)
+ }
+ }
+ }
+ return generatedNames
+ }
+
+ private fun log(message: String) {
+ project.logger.info(message)
+ }
+}
+
+/**
+ * Excludes the elements which [Java compiled file names][File.asJavaCompiledClassName]
+ * are present among the passed [names].
+ *
+ * Returns the same instance of `ConfigurableFileTree`, for call chaining.
+ */
+@CanIgnoreReturnValue
+private fun ConfigurableFileTree.without(names: List): ConfigurableFileTree {
+ this.exclude { element ->
+ val className = element.file.asJavaCompiledClassName()
+ names.contains(className)
+ }
+ return this
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt
new file mode 100644
index 0000000..75763dd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+/**
+ * File extensions.
+ */
+internal enum class FileExtension(val value: String) {
+
+ /**
+ * Extension of a Java source file.
+ */
+ JAVA_SOURCE(".java"),
+
+ /**
+ * Extension of a Java compiled file.
+ */
+ COMPILED_CLASS(".class");
+
+ /**
+ * The number of symbols in the extension.
+ */
+ val length: Int
+ get() = this.value.length
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt
new file mode 100644
index 0000000..69193ba
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+import io.spine.internal.gradle.report.coverage.FileExtension.COMPILED_CLASS
+import io.spine.internal.gradle.report.coverage.FileExtension.JAVA_SOURCE
+import io.spine.internal.gradle.report.coverage.PathMarker.ANONYMOUS_CLASS
+import io.spine.internal.gradle.report.coverage.PathMarker.GENERATED
+import io.spine.internal.gradle.report.coverage.PathMarker.GRPC_SRC_FOLDER
+import io.spine.internal.gradle.report.coverage.PathMarker.JAVA_OUTPUT_FOLDER
+import io.spine.internal.gradle.report.coverage.PathMarker.JAVA_SRC_FOLDER
+import io.spine.internal.gradle.report.coverage.PathMarker.SPINE_JAVA_SRC_FOLDER
+import java.io.File
+
+/**
+ * This file contains extension methods and properties for `java.io.File`.
+ */
+
+/**
+ * Parses the name of a class from the absolute path of this file.
+ *
+ * Treats the fragment between the [precedingMarker] and [extension] as the value to look for.
+ * In case the fragment is located and it contains `/` symbols, they are treated
+ * as Java package delimiters and are replaced by `.` symbols before returning the value.
+ *
+ * If the absolute path of this file has either no [precedingMarker] or no [extension],
+ * returns `null`.
+ */
+internal fun File.parseClassName(
+ precedingMarker: PathMarker,
+ extension: FileExtension
+): String? {
+ val index = this.absolutePath.lastIndexOf(precedingMarker.infix)
+ return if (index > 0) {
+ var inFolder = this.absolutePath.substring(index + precedingMarker.length)
+ if (inFolder.endsWith(extension.value)) {
+ inFolder = inFolder.substring(0, inFolder.length - extension.length)
+ inFolder.replace('/', '.')
+ } else {
+ null
+ }
+ } else {
+ null
+ }
+}
+
+/**
+ * Attempts to parse the file name with either of the specified [parsers],
+ * in their respective order.
+ *
+ * Returns the first non-`null` parsed value.
+ *
+ * If none of the parsers returns non-`null` value, returns `null`.
+ */
+internal fun File.parseName(vararg parsers: (file: File) -> String?): String? {
+ for (parser in parsers) {
+ val className = parser.invoke(this)
+ if (className != null) {
+ return className
+ }
+ }
+ return null
+}
+
+/**
+ * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
+ * treating it as a path to a human-produced `.java` file.
+ */
+internal fun File.asJavaClassName(): String? =
+ this.parseClassName(JAVA_SRC_FOLDER, JAVA_SOURCE)
+
+/**
+ * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
+ * treating it as a path to a compiled `.class` file.
+ *
+ * If the `.class` file corresponds to the anonymous class, only the name of the parent
+ * class is returned.
+ */
+internal fun File.asJavaCompiledClassName(): String? {
+ var className = this.parseClassName(JAVA_OUTPUT_FOLDER, COMPILED_CLASS)
+ if (className != null && className.contains(ANONYMOUS_CLASS.infix)) {
+ className = className.split(ANONYMOUS_CLASS.infix)[0]
+ }
+ return className
+}
+
+/**
+ * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
+ * treating it as a path to a gRPC-generated `.java` file.
+ */
+internal fun File.asGrpcClassName(): String? =
+ this.parseClassName(GRPC_SRC_FOLDER, JAVA_SOURCE)
+
+/**
+ * Attempts to parse the Java fully-qualified class name from the absolute path of this file,
+ * treating it as a path to a Spine-generated `.java` file.
+ */
+internal fun File.asSpineClassName(): String? =
+ this.parseClassName(SPINE_JAVA_SRC_FOLDER, JAVA_SOURCE)
+
+/**
+ * Tells whether this file is a part of the generated sources, and not produced by a human.
+ */
+internal val File.isGenerated
+ get() = this.absolutePath.contains(GENERATED.infix)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileFilter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileFilter.kt
new file mode 100644
index 0000000..cc80ea1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileFilter.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+import java.io.File
+
+/**
+ * Utilities for filtering the groups of `File`s.
+ */
+internal object FileFilter {
+
+ /**
+ * Excludes the generated files from this file collection, leaving only those which were
+ * created by human beings.
+ */
+ fun producedByHuman(files: Iterable): Iterable {
+ return files.filter { !it.isGenerated }
+ }
+
+ /**
+ * Filters this file collection so that only generated files are present.
+ */
+ fun generatedOnly(files: Iterable): Iterable {
+ return files.filter { it.isGenerated }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt
new file mode 100644
index 0000000..4d789a5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+import io.spine.internal.gradle.applyPlugin
+import io.spine.internal.gradle.findTask
+import io.spine.internal.gradle.report.coverage.TaskName.check
+import io.spine.internal.gradle.report.coverage.TaskName.copyReports
+import io.spine.internal.gradle.report.coverage.TaskName.jacocoRootReport
+import io.spine.internal.gradle.report.coverage.TaskName.jacocoTestReport
+import io.spine.internal.gradle.sourceSets
+import java.io.File
+import java.util.*
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.plugins.BasePlugin
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.SourceSetOutput
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.get
+import org.gradle.testing.jacoco.plugins.JacocoPlugin
+import org.gradle.testing.jacoco.tasks.JacocoReport
+
+/**
+ * Configures JaCoCo plugin to produce `jacocoRootReport` task which accumulates
+ * the test coverage results from all subprojects in a multi-project Gradle build.
+ *
+ * Users must apply `jacoco` plugin to all the subprojects, for which the report aggregation
+ * is required.
+ *
+ * In a single-module Gradle project, this utility is NOT needed. Just a plain `jacoco` plugin
+ * applied to the project is sufficient.
+ *
+ * Therefore, tn case this utility is applied to a single-module Gradle project,
+ * an `IllegalStateException` is thrown.
+ */
+@Suppress("unused")
+class JacocoConfig(
+ private val rootProject: Project,
+ private val reportsDir: File,
+ private val projects: Iterable
+) {
+
+ companion object {
+
+ /**
+ * A folder under the `buildDir` of the [rootProject] to which the reports will
+ * be copied when aggregating the coverage reports.
+ *
+ * If it does not exist, it will be created.
+ */
+ private const val reportsDirSuffix = "subreports/jacoco/"
+
+ /**
+ * Applies the JaCoCo plugin to the Gradle project.
+ *
+ * If the passed project has no subprojects, an `IllegalStateException` is thrown,
+ * telling that this utility should NOT be used.
+ *
+ * Registers `jacocoRootReport` task which aggregates all coverage reports
+ * from the subprojects.
+ */
+ fun applyTo(project: Project) {
+ project.applyPlugin(BasePlugin::class.java)
+ val javaProjects: Iterable = eligibleProjects(project)
+ val reportsDir = project.rootProject.buildDir.resolve(reportsDirSuffix)
+ JacocoConfig(project.rootProject, reportsDir, javaProjects).configure()
+ }
+
+ /**
+ * For a multi-module Gradle project, returns those subprojects of the passed [project]
+ * which have JaCoCo plugin applied.
+ *
+ * Throws an exception in case this project has no subprojects.
+ */
+ private fun eligibleProjects(project: Project): Iterable {
+ val projects: Iterable =
+ if (project.subprojects.isNotEmpty()) {
+ project.subprojects.filter {
+ it.pluginManager.hasPlugin(JacocoPlugin.PLUGIN_EXTENSION_NAME)
+ }
+ } else {
+ throw IllegalStateException(
+ "In a single-module Gradle project, `JacocoConfig` is NOT needed." +
+ " Please apply `jacoco` plugin instead."
+ )
+ }
+ return projects
+ }
+ }
+
+ private fun configure() {
+ val tasks = rootProject.tasks
+ val copyReports = registerCopy(tasks)
+ val rootReport = registerRootReport(tasks, copyReports)
+ rootProject
+ .findTask(check.name)
+ .dependsOn(rootReport)
+ }
+
+ private fun registerRootReport(
+ tasks: TaskContainer,
+ copyReports: TaskProvider?
+ ): TaskProvider {
+ val allSourceSets = Projects(projects).sourceSets()
+ val mainJavaSrcDirs = allSourceSets.mainJavaSrcDirs()
+ val humanProducedSourceFolders = FileFilter.producedByHuman(mainJavaSrcDirs)
+
+ val filter = CodebaseFilter(rootProject, mainJavaSrcDirs, allSourceSets.mainOutputs())
+ val humanProducedCompiledFiles = filter.humanProducedCompiledFiles()
+
+ val rootReport = tasks.register(jacocoRootReport.name, JacocoReport::class.java) {
+ dependsOn(copyReports)
+
+ additionalSourceDirs.from(humanProducedSourceFolders)
+ sourceDirectories.from(humanProducedSourceFolders)
+ executionData.from(rootProject.fileTree(reportsDir))
+
+ classDirectories.from(humanProducedCompiledFiles)
+ additionalClassDirs.from(humanProducedCompiledFiles)
+
+ reports {
+ html.required.set(true)
+ xml.required.set(true)
+ csv.required.set(false)
+ }
+ onlyIf { true }
+ }
+ return rootReport
+ }
+
+ private fun registerCopy(tasks: TaskContainer): TaskProvider {
+ val everyExecData = mutableListOf()
+ projects.forEach { project ->
+ val jacocoTestReport = project.findTask(jacocoTestReport.name)
+ val executionData = jacocoTestReport.executionData
+ everyExecData.add(executionData)
+ }
+
+ val originalLocation = rootProject.files(everyExecData)
+
+ val copyReports = tasks.register(copyReports.name, Copy::class.java) {
+ from(originalLocation)
+ into(reportsDir)
+ rename {
+ "${UUID.randomUUID()}.exec"
+ }
+ dependsOn(projects.map { it.findTask(jacocoTestReport.name) })
+ }
+ return copyReports
+ }
+}
+
+/**
+ * Extensions for working with groups of Gradle `Project`s.
+ */
+private class Projects(
+ private val projects: Iterable
+) {
+
+ /**
+ * Returns all source sets for this group of projects.
+ */
+ fun sourceSets(): SourceSets {
+ val sets = projects.asSequence().map { it.sourceSets }.toList()
+ return SourceSets(sets)
+ }
+}
+
+/**
+ * Extensions for working with several of Gradle `SourceSetContainer`s.
+ */
+private class SourceSets(
+ private val sourceSets: Iterable
+) {
+
+ /**
+ * Returns all Java source folders corresponding to the `main` source set type.
+ */
+ fun mainJavaSrcDirs(): Set {
+ return sourceSets
+ .asSequence()
+ .flatMap { it["main"].allJava.srcDirs }
+ .toSet()
+ }
+
+ /**
+ * Returns all source set outputs corresponding to the `main` source set type.
+ */
+ fun mainOutputs(): Set {
+ return sourceSets
+ .asSequence()
+ .map { it["main"].output }
+ .toSet()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/PathMarker.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/PathMarker.kt
new file mode 100644
index 0000000..06b0bc5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/PathMarker.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+/**
+ * Fragments of file path which allow to detect the type of the file.
+ */
+internal enum class PathMarker(val infix: String) {
+
+ /**
+ * Generated files.
+ */
+ GENERATED("generated"),
+
+ /**
+ * Files produced by humans and written in Java.
+ */
+ JAVA_SRC_FOLDER("/java/"),
+
+ /**
+ * Java source files generated by Spine framework.
+ */
+ SPINE_JAVA_SRC_FOLDER("main/spine/"),
+
+ /**
+ * Java source files generated by gRPC plugin.
+ */
+ GRPC_SRC_FOLDER("/main/grpc/"),
+
+ /**
+ * Among compiler output folders, highlights those containing the compilation result
+ * of human-produced Java files.
+ */
+ JAVA_OUTPUT_FOLDER("/main/"),
+
+ /**
+ * Anonymous class.
+ */
+ ANONYMOUS_CLASS("$");
+
+ /**
+ * The number of symbols in the marker.
+ */
+ val length: Int
+ get() = this.infix.length
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/TaskName.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/TaskName.kt
new file mode 100644
index 0000000..c81c24c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/TaskName.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.coverage
+
+/**
+ * The names of Gradle tasks involved into the JaCoCo reporting.
+ */
+@Suppress("EnumEntryName", "EnumNaming") /* Dubbing the actual values in Gradle. */
+internal enum class TaskName {
+ jacocoRootReport,
+ copyReports,
+
+ check,
+ jacocoTestReport
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt
new file mode 100644
index 0000000..32b24b2
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import com.github.jk1.license.ConfigurationData
+
+/**
+ * The names of Gradle `Configuration`s.
+ */
+@Suppress("EnumEntryName", "EnumNaming")
+/* Dubbing the actual values in Gradle. */
+internal enum class Configuration {
+ runtime,
+ runtimeClasspath
+}
+
+/**
+ * Tells whether this configuration data is one of the passed `Configuration` types.
+ */
+internal fun ConfigurationData.isOneOf(vararg configs: Configuration): Boolean {
+ configs.forEach {
+ if (it.name == this.name) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt
new file mode 100644
index 0000000..f7f1a31
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import com.github.jk1.license.LicenseReportExtension
+import com.github.jk1.license.LicenseReportExtension.ALL
+import com.github.jk1.license.LicenseReportPlugin
+import io.spine.internal.gradle.applyPlugin
+import io.spine.internal.gradle.findTask
+import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.kotlin.dsl.the
+
+/**
+ * Generates the license report for all Java dependencies used in a single Gradle project
+ * and in a repository.
+ *
+ * Transitive dependencies are included.
+ *
+ * The output file is placed to the root folder of the root Gradle project.
+ *
+ * Usage:
+ *
+ * ```
+ * // ...
+ * subprojects {
+ *
+ * LicenseReporter.generateReportIn(project)
+ * }
+ *
+ * // ...
+ *
+ * LicenseReporter.mergeAllReports(project)
+ *
+ * ```
+ */
+object LicenseReporter {
+
+ /**
+ * The name of the Gradle task which generates the reports for a specific Gradle project.
+ */
+ private const val projectTaskName = "generateLicenseReport"
+
+ /**
+ * The name of the Gradle task merging the license reports across all Gradle projects
+ * in the repository into a single report file.
+ */
+ private const val mergeTaskName = "mergeAllLicenseReports"
+
+ /**
+ * Enables the generation of the license report for a single Gradle project.
+ *
+ * Registers `generateLicenseReport` task, which is later picked up
+ * by the [merge task][mergeAllReports].
+ */
+ fun generateReportIn(project: Project) {
+ project.applyPlugin(LicenseReportPlugin::class.java)
+ val reportOutputDir = project.buildDir.resolve(Paths.relativePath)
+
+ with(project.the()) {
+ outputDir = reportOutputDir.absolutePath
+ excludeGroups = arrayOf("io.spine", "io.spine.tools", "io.spine.gcloud")
+ configurations = ALL
+
+ renderers = arrayOf(MarkdownReportRenderer(Paths.outputFilename))
+ }
+ }
+
+ /**
+ * Tells to merge all per-project reports which were previously [generated][generateReportIn]
+ * for each of the subprojects of the root Gradle project.
+ *
+ * The merge result is placed according to [Paths].
+ *
+ * Registers a `mergeAllLicenseReports` which is specified to be executed after `build`.
+ */
+ fun mergeAllReports(project: Project) {
+ val rootProject = project.rootProject
+ val mergeTask = rootProject.tasks.register(mergeTaskName) {
+ val consolidationTask = this
+ val assembleTask = project.findTask("assemble")
+ val sourceProjects: Iterable = sourceProjects(rootProject)
+ sourceProjects.forEach {
+ val perProjectTask = it.findTask(projectTaskName)
+ consolidationTask.dependsOn(perProjectTask)
+ perProjectTask.dependsOn(assembleTask)
+ }
+ doLast {
+ mergeReports(sourceProjects, rootProject)
+ }
+ dependsOn(assembleTask)
+ }
+ project.findTask("build")
+ .finalizedBy(mergeTask)
+ }
+
+ /**
+ * Determines the source projects for which the resulting report will be produced.
+ */
+ private fun Task.sourceProjects(rootProject: Project): Iterable {
+ val targetProjects: Iterable = if (rootProject.subprojects.isEmpty()) {
+ rootProject.logger.debug(
+ "The license report will be produced for a single root project."
+ )
+ listOf(this.project)
+ } else {
+ rootProject.logger.debug(
+ "The license report will be produced for all subprojects of a root project."
+ )
+ rootProject.subprojects
+ }
+ return targetProjects
+ }
+
+ /**
+ * Merges the license reports from all [sourceProjects] into a single file under
+ * the [rootProject]'s root directory.
+ */
+ private fun mergeReports(
+ sourceProjects: Iterable,
+ rootProject: Project
+ ) {
+ val paths = sourceProjects.map {
+ "${it.buildDir}/${Paths.relativePath}/${Paths.outputFilename}"
+ }
+ println("Merging the license reports from the 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/internal/gradle/report/license/MarkdownReportRenderer.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/MarkdownReportRenderer.kt
new file mode 100644
index 0000000..e1cd8af
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/MarkdownReportRenderer.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import com.github.jk1.license.LicenseReportExtension
+import com.github.jk1.license.ProjectData
+import com.github.jk1.license.render.ReportRenderer
+import io.spine.internal.markup.MarkdownDocument
+import java.io.File
+import org.gradle.api.Project
+
+/**
+ * Renders the dependency report for a single [project][ProjectData] in Markdown.
+ */
+internal class MarkdownReportRenderer(
+ private val filename: String
+) : ReportRenderer {
+
+ override fun render(data: ProjectData) {
+ val project = data.project
+ val outputFile = outputFile(project)
+ val document = MarkdownDocument()
+ val template = Template(project, document)
+
+ template.writeHeader()
+ ProjectDependencies.of(data).printTo(document)
+ template.writeFooter()
+
+ document.writeToFile(outputFile)
+ }
+
+ private fun outputFile(project: Project): File {
+ val config =
+ project.extensions.findByName("licenseReport") as LicenseReportExtension
+ return File(config.outputDir).resolve(filename)
+ }
+}
+
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt
new file mode 100644
index 0000000..9adbb3f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import com.github.jk1.license.ModuleData
+import io.spine.internal.markup.MarkdownDocument
+import kotlin.reflect.KCallable
+
+/**
+ * This file declares the Kotlin extensions that help printing `ModuleData` in Markdown format.
+ */
+
+/**
+ * Prints several of the module data dependencies under the section with the passed [title].
+ */
+internal fun MarkdownDocument.printSection(
+ title: String,
+ modules: Iterable
+): MarkdownDocument {
+ this.h2(title)
+ modules.forEach {
+ printModule(it)
+ }
+ return this
+}
+
+/**
+ * Prints the module metadata to this [MarkdownDocument].
+ */
+private fun MarkdownDocument.printModule(module: ModuleData) {
+ ol()
+
+ this.print(ModuleData::getGroup, module, "Group")
+ .print(ModuleData::getName, module, "Name")
+ .print(ModuleData::getVersion, module, "Version")
+
+ val projectUrl = module.projectUrl()
+ val licenses = module.licenses()
+
+ if (projectUrl.isNullOrEmpty() && licenses.isEmpty()) {
+ bold("No license information found")
+ return
+ }
+
+ @SuppressWarnings("MagicNumber") /* As per the original document layout. */
+ val listIndent = 5
+ printProjectUrl(projectUrl, listIndent)
+ printLicenses(licenses, listIndent)
+
+ nl()
+}
+
+/**
+ * Prints the value of the [ModuleData] property by the passed [getter].
+ *
+ * The property is printed with the passed [title].
+ */
+private fun MarkdownDocument.print(
+ getter: KCallable<*>,
+ module: ModuleData,
+ title: String
+): MarkdownDocument {
+ val value = getter.call(module)
+ if (value != null) {
+ space().bold(title).and().text(": $value.")
+ }
+ return this
+}
+
+
+/**
+ * Prints the URL to the project which provides the dependency.
+ *
+ * If the passed project URL is `null` or empty, it is not printed.
+ */
+@Suppress("SameParameterValue" /* Indentation is consistent across the list. */)
+private fun MarkdownDocument.printProjectUrl(projectUrl: String?, indent: Int) {
+ if (!projectUrl.isNullOrEmpty()) {
+ ul(indent).bold("Project URL:").and().link(projectUrl)
+ }
+}
+
+/**
+ * Prints the links to the source code licenses.
+ */
+@Suppress("SameParameterValue" /* Indentation is consistent across the list. */)
+private fun MarkdownDocument.printLicenses(licenses: Set, indent: Int) {
+ for (license in licenses) {
+ ul(indent).bold("License:").and()
+ if (license.url.isNullOrEmpty()) {
+ text(license.text)
+ } else {
+ link(license.text, license.url)
+ }
+ }
+}
+
+/**
+ * Searches for the URL of the project in the module's metadata.
+ *
+ * Returns `null` if none is found.
+ */
+private fun ModuleData.projectUrl(): String? {
+ val pomUrl = this.poms.firstOrNull()?.projectUrl
+ if (!pomUrl.isNullOrBlank()) {
+ return pomUrl
+ }
+ return this.manifests.firstOrNull()?.url
+}
+
+/**
+ * Collects the links to the source code licenses, under which the module dependency is distributed.
+ */
+private fun ModuleData.licenses(): Set {
+ val result = mutableSetOf()
+
+ val manifestLicense: License? = manifests.firstOrNull()?.let { manifest ->
+ val value = manifest.license
+ if (!value.isNullOrBlank()) {
+ if (value.startsWith("http")) {
+ License(value, value)
+ } else {
+ License(value, manifest.url)
+ }
+ }
+ null
+ }
+ manifestLicense?.let { result.add(it) }
+
+ val pomLicenses = poms.firstOrNull()?.licenses?.map { license ->
+ License(license.name, license.url)
+ }
+ pomLicenses?.let {
+ result.addAll(it)
+ }
+ return result.toSet()
+}
+
+/**
+ * The source code license with the URL leading to the license text, as defined
+ * by the project's dependency.
+ *
+ * The URL to the license text may be not defined.
+ */
+private data class License(val text: String, val url: String?)
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt
new file mode 100644
index 0000000..c352ffd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+/**
+ * Filesystem paths used by [LicenseReporter].
+ */
+internal object Paths {
+
+ /**
+ * The output filename of the license report.
+ *
+ * The file with this name is placed to the root folder of the root Gradle project —
+ * as the result of the [LicenseReporter] work.
+ *
+ * Its contents describe the licensing information for each of the Java dependencies
+ * which are referenced by Gradle projects in the repository.
+ */
+ internal const val outputFilename = "dependencies.md"
+
+ /**
+ * The path to a directory, to which a per-project report is generated.
+ */
+ internal const val relativePath = "reports/dependency-license/dependency"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt
new file mode 100644
index 0000000..fc90bb3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import com.github.jk1.license.ModuleData
+import com.github.jk1.license.ProjectData
+import io.spine.internal.markup.MarkdownDocument
+
+/**
+ * Dependencies of some [Gradle project][ProjectData] classified by the Gradle configuration
+ * (such as "runtime") to which they are bound.
+ */
+internal class ProjectDependencies
+private constructor(
+ private val runtime: Iterable,
+ private val compileTooling: Iterable
+) {
+
+ internal companion object {
+
+ /**
+ * Creates an instance of [ProjectDependencies] by sorting the module dependencies.
+ */
+ fun of(data: ProjectData): ProjectDependencies {
+ val runtimeDeps = mutableListOf()
+ val compileToolingDeps = mutableListOf()
+ data.configurations.forEach { config ->
+ if (config.isOneOf(Configuration.runtime, Configuration.runtimeClasspath)) {
+ runtimeDeps.addAll(config.dependencies)
+ } else {
+ compileToolingDeps.addAll(config.dependencies)
+ }
+ }
+ return ProjectDependencies(runtimeDeps.toSortedSet(), compileToolingDeps.toSortedSet())
+ }
+ }
+
+ /**
+ * Prints the project dependencies along with the licensing information,
+ * splitting them into "Runtime" and "Compile, tests, and tooling" sections.
+ */
+ internal fun printTo(out: MarkdownDocument) {
+ out.printSection("Runtime", runtime)
+ .printSection("Compile, tests, and tooling", compileTooling)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt
new file mode 100644
index 0000000..6a57be5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Locates `generateLicenseReport` in this [TaskContainer].
+ *
+ * The task generates a license report for a specific Gradle project. License report includes
+ * information of all dependencies and their licenses.
+ */
+val TaskContainer.generateLicenseReport: TaskProvider
+ get() = named("generateLicenseReport")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt
new file mode 100644
index 0000000..3eccbca
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.license
+
+import io.spine.internal.gradle.artifactId
+import io.spine.internal.markup.MarkdownDocument
+import java.util.*
+import org.gradle.api.Project
+
+/**
+ * The template text pieces of the license report.
+ */
+internal class Template(
+ private val project: Project,
+ private val out: MarkdownDocument
+) {
+
+ private companion object {
+ private const val longBreak = "\n\n"
+ }
+
+ internal fun writeHeader() {
+ out.nl()
+ .h1(
+ "Dependencies of " +
+ "`${project.group}:${project.artifactId}:${project.version}`"
+ )
+ .nl()
+ }
+
+ internal fun writeFooter() {
+ 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()}")
+ .text(" using ")
+ .link(
+ "Gradle-License-Report plugin",
+ "https://github.com/jk1/Gradle-License-Report"
+ )
+ .text(" by Evgeny Naumenko, ")
+ .text("licensed under ")
+ .link(
+ "Apache 2.0 License",
+ "https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE"
+ )
+ .text(".")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt
new file mode 100644
index 0000000..9c1f1ed
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+/**
+ * A Maven dependency scope.
+ */
+@Suppress("EnumEntryName", "EnumNaming") /* Dubbing the actual values in Gradle. */
+enum class DependencyScope {
+ undefined,
+ compile,
+ provided,
+ runtime,
+ test,
+ system
+
+ /**
+ `import` is also a scope, however, it can't be used outside the ``
+ section, which is outside of the scope of this script
+ **/
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt
new file mode 100644
index 0000000..fb1920b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import groovy.xml.MarkupBuilder
+import java.io.Writer
+import java.util.*
+import kotlin.reflect.full.isSubclassOf
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.internal.artifacts.dependencies.AbstractExternalModuleDependency
+import org.gradle.kotlin.dsl.withGroovyBuilder
+
+/**
+ * Writes the dependencies of a Gradle project in a `pom.xml` format.
+ *
+ * Includes the dependencies of the subprojects. Does not include
+ * the transitive dependencies.
+ *
+ * ```
+ *
+ *
+ * io.spine
+ * base
+ * 2.0.0-pre1
+ *
+ * ...
+ *
+ * ```
+ *
+ * When there are several versions of the same dependency, only the one with
+ * the newest version is retained.
+ *
+ * @see PomGenerator
+ */
+internal class DependencyWriter
+private constructor(
+ private val dependencies: SortedSet
+) {
+ internal companion object {
+
+ /**
+ * Creates the `ProjectDependenciesAsXml` for the passed [project].
+ */
+ fun of(project: Project): DependencyWriter {
+ return DependencyWriter(project.dependencies())
+ }
+ }
+
+ /**
+ * Writes the dependencies in their `pom.xml` format to the passed [out] writer.
+ *
+ *
Used writer will not be closed.
+ */
+ fun writeXmlTo(out: Writer) {
+ val xml = MarkupBuilder(out)
+ xml.withGroovyBuilder {
+ "dependencies" {
+ dependencies.forEach { scopedDep ->
+ val dependency = scopedDep.dependency()
+ "dependency" {
+ "groupId" { xml.text(dependency.group) }
+ "artifactId" { xml.text(dependency.name) }
+ "version" { xml.text(dependency.version) }
+ if (scopedDep.hasDefinedScope()) {
+ "scope" { xml.text(scopedDep.scopeName()) }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Returns the [scoped dependencies][ScopedDependency] of a Gradle project.
+ */
+fun Project.dependencies(): SortedSet {
+ val dependencies = mutableSetOf()
+ dependencies.addAll(this.depsFromAllConfigurations())
+
+ this.subprojects.forEach { subproject ->
+ val subprojectDeps = subproject.depsFromAllConfigurations()
+ dependencies.addAll(subprojectDeps)
+ }
+
+ val result = deduplicate(dependencies)
+ .map { it.scoped }
+ .toSortedSet()
+ return result
+}
+
+/**
+ * Returns the external dependencies of the project from all the project configurations.
+ */
+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() }
+ .forEach { dependency ->
+ val moduleDependency = ModuleDependency(project, configuration, dependency)
+ result.add(moduleDependency)
+ }
+ }
+ return result
+}
+
+/**
+ * Tells whether the dependency is an external module dependency.
+ */
+private fun Dependency.isExternal(): Boolean {
+ return this.javaClass.kotlin.isSubclassOf(AbstractExternalModuleDependency::class)
+}
+
+/**
+ * Filters out duplicated dependencies by group and name.
+ *
+ * When there are several versions of the same dependency, the method will retain only
+ * the one with the newest version.
+ *
+ * Sometimes, a project uses several versions of the same dependency. This may happen
+ * when different modules of the project use different versions of the same dependency.
+ * But for our `pom.xml`, which has clearly representative character, a single version
+ * of a dependency is quite enough.
+ *
+ * The rejected duplicates are logged.
+ */
+private fun Project.deduplicate(dependencies: Set): List {
+ val groups = dependencies.distinctBy { it.gav }
+ .groupBy { it.run { "$group:$name" } }
+
+ logDuplicates(groups)
+
+ val filtered = groups.map { group ->
+ group.value.maxByOrNull { dep -> dep.version!! }!!
+ }
+ return filtered
+}
+
+private fun Project.logDuplicates(dependencies: Map>) {
+ dependencies.filter { it.value.size > 1 }
+ .forEach { (dependency, versions) -> logDuplicate(dependency, versions) }
+}
+
+private fun Project.logDuplicate(dependency: String, versions: List) {
+ logger.lifecycle("")
+ logger.lifecycle("The project uses several versions of `$dependency` dependency.")
+
+ versions.forEach {
+ logger.lifecycle(
+ "module: {}, configuration: {}, version: {}",
+ it.module.name,
+ it.configuration.name,
+ it.version
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/InceptionYear.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/InceptionYear.kt
new file mode 100644
index 0000000..ab4bc1c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/InceptionYear.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import groovy.xml.MarkupBuilder
+import java.io.StringWriter
+import org.gradle.kotlin.dsl.withGroovyBuilder
+
+/**
+ * Information about the Spine's inception year.
+ */
+internal object InceptionYear {
+
+ private const val SPINE_INCEPTION_YEAR = "2015"
+
+ /**
+ * Returns a string containing the inception year of Spine in a `pom.xml` format.
+ */
+ override fun toString(): String {
+ val writer = StringWriter()
+ val xml = MarkupBuilder(writer)
+ xml.withGroovyBuilder {
+ "inceptionYear" { xml.text(SPINE_INCEPTION_YEAR) }
+ }
+ return writer.toString()
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/MarkupExtensions.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/MarkupExtensions.kt
new file mode 100644
index 0000000..214931a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/MarkupExtensions.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import groovy.xml.MarkupBuilder
+
+/**
+ * This file contains extension methods and properties for the Groovy's `MarkupBuilder`.
+ */
+
+/**
+ * Yields a [value] to the document by converting it to string.
+ */
+fun MarkupBuilder.text(value: Any?) = this.mkp.yield(value.toString())
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ModuleDependency.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ModuleDependency.kt
new file mode 100644
index 0000000..0dbb11c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/ModuleDependency.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.Dependency
+
+/**
+ * A module's dependency.
+ *
+ * Contains information about a module and configuration, from which
+ * the dependency comes.
+ */
+internal class ModuleDependency(
+ val module: Project,
+ val configuration: Configuration,
+ private val dependency: Dependency,
+
+) : Dependency by dependency, Comparable {
+
+ companion object {
+ private val COMPARATOR = compareBy { it.module }
+ .thenBy { it.configuration.name }
+ .thenBy { it.group }
+ .thenBy { it.name }
+ .thenBy { it.version }
+ }
+
+ /**
+ * A project dependency with its [scope][DependencyScope].
+ *
+ * Doesn't contain any info about an origin module and configuration.
+ */
+ val scoped = ScopedDependency.of(dependency, configuration)
+
+ /**
+ * GAV coordinates of this dependency.
+ *
+ * Gradle's [Dependency] is a mutable object. Its properties can change their
+ * values with time. In parcticular, the version can be changed as more
+ * configurations are getting resolved. This is why this property is calculated.
+ */
+ val gav: String
+ get() = "$group:$name:$version"
+
+ override fun compareTo(other: ModuleDependency): Int = COMPARATOR.compare(this, other)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ModuleDependency
+
+ if (module != other.module) return false
+ if (configuration != other.configuration) return false
+ if (dependency != other.dependency) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = module.hashCode()
+ result = 31 * result + configuration.hashCode()
+ result = 31 * result + dependency.hashCode()
+ return result
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt
new file mode 100644
index 0000000..d25ebd6
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import java.io.StringWriter
+import java.lang.System.lineSeparator
+import java.util.*
+
+/**
+ * Helps to format the `pom.xml` file according to its expected XML structure.
+ */
+internal object PomFormatting {
+
+ private val NL = lineSeparator()
+ private const val XML_METADATA = ""
+ private const val PROJECT_SCHEMA_LOCATION = ""
+ private const val MODEL_VERSION = "4.0.0"
+ private const val CLOSING_PROJECT_TAG = ""
+
+ /**
+ * Writes the starting segment of `pom.xml`.
+ */
+ internal fun writeStart(dest: StringWriter) {
+ dest.write(
+ XML_METADATA,
+ NL,
+ PROJECT_SCHEMA_LOCATION,
+ NL,
+ MODEL_VERSION,
+ NL,
+ describingComment(),
+ NL
+ )
+ }
+
+ /**
+ * Obtains a description comment that describes the nature of the generated `pom.xml` file.
+ */
+ private fun describingComment(): String {
+ val description = NL +
+ "This file was generated using the Gradle `generatePom` task. " +
+ NL +
+ "This file is not suitable for `maven` build tasks. It only describes the " +
+ "first-level dependencies of " +
+ NL +
+ "all modules and does not describe the project " +
+ "structure per-subproject." +
+ NL
+ return String.format(
+ Locale.US,
+ "",
+ NL, description, NL
+ )
+ }
+
+ /**
+ * Writes the closing segment of `pom.xml`.
+ */
+ internal fun writeEnd(dest: StringWriter) {
+ dest.write(CLOSING_PROJECT_TAG)
+ }
+
+ /**
+ * 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
+ */
+ internal fun writeBlocks(destination: StringWriter, vararg lines: String) {
+ lines.iterator().forEach {
+ destination.write(it, NL, NL)
+ }
+ }
+
+ /**
+ * Writes each of the passed sequences.
+ */
+ private fun StringWriter.write(vararg content: String) {
+ content.forEach {
+ this.write(it)
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt
new file mode 100644
index 0000000..3535922
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import org.gradle.api.Project
+import org.gradle.api.plugins.BasePlugin
+
+/**
+ * Generates a `pom.xml` file that contains dependencies of the root project as
+ * well as the dependencies of its subprojects.
+ *
+ * Usage:
+ * ```
+ * PomGenerator.applyTo(project)
+ * ```
+ *
+ * The generated `pom.xml` is not usable for Maven build tasks and is merely a
+ * description of project dependencies.
+ *
+ * Configures the `build` task to generate the `pom.xml` file.
+ *
+ * Note that the generated `pom.xml` includes the group ID, artifact ID and the version of the
+ * project this script was applied to. In case you want to override the default values, do so in
+ * the `ext` block like so:
+ *
+ * ```
+ * ext {
+ * groupId = 'custom-group-id'
+ * artifactId = 'custom-artifact-id'
+ * version = 'custom-version'
+ * }
+ * ```
+ *
+ * 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. ``.
+ */
+@Suppress("unused")
+object PomGenerator {
+
+ /**
+ * Configures the generator for the passed [project].
+ */
+ 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.
+ */
+ project.apply {
+ plugin(BasePlugin::class.java)
+ }
+
+ val task = project.tasks.create("generatePom")
+ task.doLast {
+ val pomFile = project.projectDir.resolve("pom.xml")
+ project.delete(pomFile)
+
+ val projectData = project.metadata()
+ val writer = PomXmlWriter(projectData)
+ writer.writeTo(pomFile)
+ }
+
+ val buildTask = project.tasks.findByName("build")!!
+ buildTask.finalizedBy(task)
+
+ val assembleTask = project.tasks.findByName("assemble")!!
+ task.dependsOn(assembleTask)
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt
new file mode 100644
index 0000000..c16fe76
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023, 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.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import io.spine.internal.gradle.report.pom.PomFormatting.writeBlocks
+import io.spine.internal.gradle.report.pom.PomFormatting.writeStart
+import java.io.File
+import java.io.FileWriter
+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.
+ */
+internal class PomXmlWriter
+internal constructor(
+ private val projectMetadata: ProjectMetadata
+) {
+
+ /**
+ * Writes the `pom.xml` file containing dependencies of this project
+ * and its subprojects to the specified location.
+ *
+ *
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
+ */
+ fun writeTo(file: File) {
+ val fileWriter = FileWriter(file)
+ val out = StringWriter()
+
+ writeStart(out)
+ writeBlocks(
+ out,
+ projectMetadata.toString(),
+ InceptionYear.toString(),
+ SpineLicense.toString(),
+ projectDependencies()
+ )
+ PomFormatting.writeEnd(out)
+
+ fileWriter.write(out.toString())
+ fileWriter.close()
+ }
+
+ /**
+ * Obtains a string that contains project dependencies as XML.
+ *
+ *