+
+
\ No newline at end of file
diff --git a/.lift.toml b/.lift.toml
new file mode 100644
index 00000000..178be588
--- /dev/null
+++ b/.lift.toml
@@ -0,0 +1 @@
+ignoreRules=["SpreadOperator"]
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 92e13828..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-language: java
-
-jdk:
- - openjdk8
-
-env:
- global:
- - PATH="$PATH:/usr/lib/dart/bin"
-
-before_install:
- - chmod +x ./gradlew
- - chmod +x ./config/scripts/publish-artifacts.sh
- - chmod +x ./config/scripts/update-apt.sh
- - ./config/scripts/update-apt.sh
- - sudo apt-get install dart
- - pub global activate protoc_plugin
- - pub global activate dart_code_gen
-
-install:
- - openssl aes-256-cbc -K $encrypted_484d6a99d515_key -iv $encrypted_484d6a99d515_iv -in credentials.tar.enc -out credentials.tar -d
- - tar xvf credentials.tar
-
-script:
- - ./gradlew check --stacktrace
-
- # The publishing script should be executed in `script` section in order to
- # fail the Travis build if execution of this script is failed.
- - ./config/scripts/publish-artifacts.sh
-
-after_success:
- # See: https://github.com/codecov/example-java/blob/master/.travis.yml
- - bash <(curl -s https://codecov.io/bash)
diff --git a/README.md b/README.md
index 87d96ad1..f4c733e0 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ In order to apply the plugin to a Gradle project, in `build.gralde` add the foll
```gradle
plugins {
- id("io.spine.tools.gradle.bootstrap").version("1.8.0")
+ id("io.spine.bootstrap").version("${spineVersion}")
}
```
@@ -65,7 +65,7 @@ For example, adding testing utilities would look like this:
```gradle
dependencies {
//...
- testImplementation("io.spine:spine-testutil-server:${spine.version()}")
+ testImplementation("io.spine.tools:spine-testutil-server:${spine.version()}")
}
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 26309bf7..2d2dc2e7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -24,79 +24,155 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-import io.spine.gradle.internal.DependencyResolution
-import io.spine.gradle.internal.Deps
-import io.spine.gradle.internal.PublishingRepos
+@file:Suppress("RemoveRedundantQualifierName") // To prevent IDEA replacing FQN imports.
+
+import io.spine.internal.dependency.CheckerFramework
+import io.spine.internal.dependency.ErrorProne
+import io.spine.internal.dependency.FindBugs
+import io.spine.internal.dependency.Guava
+import io.spine.internal.dependency.JUnit
+import io.spine.internal.dependency.Truth
+import io.spine.internal.gradle.publish.IncrementGuard
+import io.spine.internal.gradle.VersionWriter
+import io.spine.internal.gradle.applyStandard
+import io.spine.internal.gradle.checkstyle.CheckStyleConfig
+import io.spine.internal.gradle.excludeProtobufLite
+import io.spine.internal.gradle.forceVersions
+import io.spine.internal.gradle.javac.configureErrorProne
+import io.spine.internal.gradle.javac.configureJavac
+import io.spine.internal.gradle.javadoc.JavadocConfig
+import io.spine.internal.gradle.kotlin.applyJvmToolchain
+import io.spine.internal.gradle.kotlin.setFreeCompilerArgs
+import io.spine.internal.gradle.publish.PublishingRepos
+import io.spine.internal.gradle.publish.PublishingRepos.cloudArtifactRegistry
+import io.spine.internal.gradle.publish.PublishingRepos.cloudRepo
+import io.spine.internal.gradle.publish.PublishingRepos.gitHub
+import io.spine.internal.gradle.publish.SpinePublishing
+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.testing.configureLogging
+import io.spine.internal.gradle.testing.registerTestTasks
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
- java
+ `java-library`
idea
jacoco
- @Suppress("RemoveRedundantQualifierName") // Cannot use imports here.
- id("net.ltgt.errorprone").version(io.spine.gradle.internal.Deps.versions.errorPronePlugin)
+ id(io.spine.internal.dependency.ErrorProne.GradlePlugin.id)
+ kotlin("jvm")
}
-extra["credentialsPropertyFile"] = PublishingRepos.cloudRepo.credentials
-extra["projectsToPublish"] = listOf("plugin")
+spinePublishing {
+ modules = setOf("plugin")
+ destinations = setOf(
+ PublishingRepos.cloudArtifactRegistry,
+ PublishingRepos.gitHub("bootstrap")
+ )
+}
apply(from = "$rootDir/version.gradle.kts")
-val spineVersion: String by extra
-val spineBaseVersion: String by extra
-val pluginVersion: String by extra
+val bootstrapVersion: String by extra
allprojects {
+ apply {
+ plugin("jacoco")
+ plugin("idea")
+ plugin("project-report")
+ }
apply(from = "$rootDir/version.gradle.kts")
- apply(from = "$rootDir/config/gradle/dependencies.gradle")
+
+ repositories {
+ gitHub("base")
+ gitHub("tool-base")
+ gitHub("model-compiler")
+ gitHub("mc-java")
+ gitHub("mc-js")
+ gitHub("mc-dart")
+ applyStandard()
+ }
group = "io.spine.tools"
- version = pluginVersion
+ version = bootstrapVersion
}
+val baseVersion: String by extra
+
subprojects {
apply {
- plugin("java")
+ plugin("java-library")
+ plugin("kotlin")
plugin("idea")
plugin("net.ltgt.errorprone")
- plugin("pmd")
-
- from(Deps.scripts.slowTests(project))
- from(Deps.scripts.testOutput(project))
- from(Deps.scripts.javadocOptions(project))
- from(Deps.scripts.pmd(project))
- from(Deps.scripts.projectLicenseReport(project))
+ plugin("pmd-settings")
}
- java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ dependencies {
+ errorprone(ErrorProne.core)
+ compileOnlyApi(FindBugs.annotations)
+ compileOnlyApi(CheckerFramework.annotations)
+ ErrorProne.annotations.forEach { compileOnlyApi(it) }
+
+ implementation(Guava.lib)
+ implementation("io.spine:spine-base:$baseVersion")
+
+ testImplementation(Guava.testLib)
+ JUnit.api.forEach { testImplementation(it) }
+ Truth.libs.forEach { testImplementation(it) }
+ testRuntimeOnly(JUnit.runner)
}
- DependencyResolution.defaultRepositories(repositories)
+ val baseVersion: String by extra
+ val toolBaseVersion: String by extra
+ val mcVersion: String by extra
+ with(configurations) {
+ forceVersions()
+ excludeProtobufLite()
+ all {
+ resolutionStrategy {
+ force(
+ "io.spine:spine-base:$baseVersion",
+ "io.spine.tools:spine-testlib:$baseVersion",
+ "io.spine.tools:spine-tool-base:$toolBaseVersion",
+ "io.spine.tools:spine-plugin-base:$toolBaseVersion",
+ "io.spine.tools:spine-model-compiler:$mcVersion"
+ )
+ }
+ }
+ }
- dependencies {
- errorprone(Deps.build.errorProneCore)
- errorproneJavac(Deps.build.errorProneJavac)
+// java {
+// exposeTestArtifacts()
+// }
- implementation(Deps.build.guava)
- implementation("io.spine:spine-base:$spineBaseVersion")
+ tasks.withType {
+ configureJavac()
+ configureErrorProne()
+ }
- compileOnly(Deps.build.checkerAnnotations)
- compileOnly(Deps.build.jsr305Annotations)
- Deps.build.errorProneAnnotations.forEach { compileOnly(it) }
+ JavadocConfig.applyTo(project)
+ CheckStyleConfig.applyTo(project)
- testImplementation(Deps.test.guavaTestlib)
- testImplementation(Deps.test.junitPioneer)
- Deps.test.junit5Api.forEach { testImplementation(it) }
- Deps.test.truth.forEach { testImplementation(it) }
- testRuntimeOnly(Deps.test.junit5Runner)
+ val javaVersion = JavaVersion.VERSION_11.toString()
+ kotlin {
+ applyJvmToolchain(javaVersion)
+ explicitApi()
}
- DependencyResolution.forceConfiguration(configurations)
+ tasks.withType().configureEach {
+ kotlinOptions.jvmTarget = javaVersion
+ setFreeCompilerArgs()
+ }
- tasks.withType(Test::class) {
- useJUnitPlatform {
- includeEngines("junit-jupiter")
+ tasks {
+ registerTestTasks()
+ test {
+ useJUnitPlatform {
+ includeEngines("junit-jupiter")
+ }
+ configureLogging()
}
}
@@ -122,16 +198,12 @@ subprojects {
isDownloadSources = true
}
}
-}
-apply {
- from(Deps.scripts.publish(project))
- from(Deps.scripts.jacoco(project))
- from(Deps.scripts.repoLicenseReport(project))
- from(Deps.scripts.generatePom(project))
+ apply()
+ apply()
+ LicenseReporter.generateReportIn(project)
}
-rootProject.afterEvaluate {
- val pluginProject = project(":plugin")
- pluginProject.tasks["publish"].dependsOn(pluginProject.tasks["publishPlugins"])
-}
+JacocoConfig.applyTo(project)
+PomGenerator.applyTo(project)
+LicenseReporter.mergeAllReports(project)
diff --git a/buildSrc/aus.weis b/buildSrc/aus.weis
new file mode 100644
index 00000000..fa72e08d
Binary files /dev/null and b/buildSrc/aus.weis differ
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 9c82b3c7..6aacc165 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021, TeamDev. All rights reserved.
+ * Copyright 2022, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,17 +24,97 @@
* 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`
+ pmd
+ val licenseReportVersion = "2.1"
+ id("com.github.jk1.dependency-license-report").version(licenseReportVersion)
}
repositories {
mavenLocal()
- jcenter()
+ gradlePluginPortal()
+ mavenCentral()
}
-val jacksonVersion = "2.11.0"
+/**
+ * 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.13.0"
+
+val googleAuthToolVersion = "2.1.2"
+val licenseReportVersion = "2.1"
+val grGitVersion = "3.1.1"
+
+/**
+ * The version of the Kotlin Gradle plugin.
+ *
+ * Please check that this value matches one defined in
+ * [io.spine.internal.dependency.Kotlin.version].
+ */
+val kotlinVersion = "1.6.20"
+
+/**
+ * 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 = "31.0.1-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 errorProneVersion = "2.0.2"
+
+/**
+ * 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.8.18"
+
+/**
+ * The version of Dokka Gradle Plugins.
+ *
+ * Please keep in sync with [io.spine.internal.dependency.Dokka.version].
+ *
+ * @see
+ * Dokka Releases
+ */
+val dokkaVersion = "1.6.20"
dependencies {
+ implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion")
+ implementation("com.google.cloud.artifactregistry:artifactregistry-auth-common:$googleAuthToolVersion") {
+ exclude(group = "com.google.guava")
+ }
+ implementation("com.google.guava:guava:$guavaVersion")
+ api("com.github.jk1:gradle-license-report:$licenseReportVersion")
+ implementation("org.ajoberstar.grgit:grgit-core:${grGitVersion}")
+ implementation("net.ltgt.gradle:gradle-errorprone-plugin:${errorProneVersion}")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
+ implementation("gradle.plugin.com.google.protobuf:protobuf-gradle-plugin:$protobufPluginVersion")
+ implementation("org.jetbrains.dokka:dokka-gradle-plugin:${dokkaVersion}")
+ implementation("org.jetbrains.dokka:dokka-base:${dokkaVersion}")
}
diff --git a/buildSrc/src/main/groovy/checkstyle.gradle b/buildSrc/src/main/groovy/checkstyle.gradle
new file mode 100644
index 00000000..817825ef
--- /dev/null
+++ b/buildSrc/src/main/groovy/checkstyle.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021, 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 configures Gradle Checkstyle plugin.
+ */
+
+import io.spine.internal.dependency.CheckStyle
+
+println("`checkstyle.gradle` script is deprecated. Please use the `CheckStyleConfig` utility instead.")
+
+apply plugin: 'checkstyle'
+
+checkstyle {
+ toolVersion = "${CheckStyle.version}"
+ configFile = file("$rootDir/config/quality/checkstyle.xml")
+
+ // Disable checking the test sources.
+ checkstyleTest.enabled = false
+}
diff --git a/buildSrc/src/main/groovy/dart/build-tasks.gradle b/buildSrc/src/main/groovy/dart/build-tasks.gradle
new file mode 100644
index 00000000..42d8b107
--- /dev/null
+++ b/buildSrc/src/main/groovy/dart/build-tasks.gradle
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021, 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.apache.tools.ant.taskdefs.condition.Os
+
+println("`build-tasks.gradle` script is deprecated. " +
+ "Please use `DartTasks.build()` extension instead.")
+
+final def GROUP = 'Dart'
+final def packageIndex = "$projectDir/.packages" as File
+final def extension = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
+final def PUB_EXECUTABLE = 'pub' + extension
+
+task resolveDependencies(type: Exec) {
+ group = GROUP
+ description = 'Fetches the dependencies declared via `pubspec.yaml`.'
+
+ inputs.file "$projectDir/pubspec.yaml"
+ outputs.file packageIndex
+
+ commandLine PUB_EXECUTABLE, 'get'
+
+ mustRunAfter 'cleanPackageIndex'
+}
+
+tasks['assemble'].dependsOn 'resolveDependencies'
+
+task cleanPackageIndex(type: Delete) {
+ group = GROUP
+ description = 'Deletes the `.packages` file on this Dart module.'
+ delete = [packageIndex]
+}
+
+tasks['clean'].dependsOn 'cleanPackageIndex'
+
+task testDart(type: Exec) {
+ group = GROUP
+ description = 'Runs Dart tests declared in the `./test` directory. See `https://pub.dev/packages/test#running-tests`.'
+
+ commandLine PUB_EXECUTABLE, 'run', 'test'
+
+ dependsOn 'resolveDependencies'
+}
+
+tasks['check'].dependsOn 'testDart'
diff --git a/buildSrc/src/main/groovy/dart/pub-publish-tasks.gradle b/buildSrc/src/main/groovy/dart/pub-publish-tasks.gradle
new file mode 100644
index 00000000..0d614045
--- /dev/null
+++ b/buildSrc/src/main/groovy/dart/pub-publish-tasks.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021, 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.apache.tools.ant.taskdefs.condition.Os
+
+println("`pub-publish-tasks.gradle` script is deprecated. " +
+ "Please use `DartTasks.publish()` extension instead.")
+
+final def publicationDir = "$buildDir/pub/publication/$project.name"
+final def extension = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
+final def PUB_EXECUTABLE = 'pub' + extension
+
+task stagePubPublication(type: Copy) {
+ description = 'Prepares the Dart package for Pub publication.'
+
+ from fileTree(projectDir) {
+ include '**/*.dart', 'pubspec.yaml', '**/*.md'
+ exclude 'proto/', 'generated/', 'build/', '**/.*'
+ }
+ from "$rootDir/LICENSE"
+ into publicationDir
+
+ doLast {
+ logger.debug("Prepared Pub publication in directory `$publicationDir`.")
+ }
+
+ dependsOn 'assemble'
+}
+
+/**
+ * A Dart analog of {@code publish}.
+ */
+task publishToPub(type: Exec) {
+ description = 'Publishes this package to Pub.'
+
+ workingDir publicationDir
+ commandLine PUB_EXECUTABLE, 'publish', '--trace'
+ final sayYes = new ByteArrayInputStream('y'.getBytes())
+ standardInput(sayYes)
+
+ dependsOn 'stagePubPublication'
+}
+
+/**
+ * A Dart analog of {@code publishToMavenLocal}.
+ */
+task activateLocally(type: Exec) {
+ description = 'Activates this package locally.'
+
+ commandLine PUB_EXECUTABLE, 'global', 'activate', '--source', 'path', publicationDir, '--trace'
+
+ workingDir publicationDir
+ dependsOn 'stagePubPublication'
+}
+
+tasks['publish'].dependsOn 'publishToPub'
diff --git a/buildSrc/src/main/groovy/generate-pom.gradle b/buildSrc/src/main/groovy/generate-pom.gradle
new file mode 100644
index 00000000..b48d1b16
--- /dev/null
+++ b/buildSrc/src/main/groovy/generate-pom.gradle
@@ -0,0 +1,611 @@
+/*
+ * Copyright 2021, 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:noinspection GroovyVariableCanBeFinal
+
+import groovy.xml.MarkupBuilder
+import org.gradle.api.internal.artifacts.dependencies.AbstractExternalModuleDependency
+
+import java.util.function.Function
+
+import static java.util.stream.Collectors.toSet
+
+/**
+ * This script generates a {@code pom.xml} file that contains dependencies of the root project as
+ * well as the dependencies of its subprojects.
+ *
+ * The generated {@code pom.xml} is not usable for {@code maven} build tasks and is merely a
+ * description of project dependencies.
+ *
+ * Configures the {@code build} task to generate the {@code pom.xml} file.
+ *
+ * To generate the pom, {@code apply} from this file.
+ *
+ * Note that the generated {@code 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 {@code ext} block like so:
+ *
+ *
+ *
+ * By default, those values are taken from the {@code project} object, which may or may not include
+ * them. If the project does not have these values and they are not specified in the {@code ext}
+ * block, the result {@code pom.xml} file is going to contain empty blocks,
+ * e.g. {@code }
+ */
+
+println("`generate-pom.gradle` script is deprecated. Please use the `PomGenerator` utility instead.")
+
+// 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 script needs it.
+apply plugin: 'base'
+
+ext {
+ pomFile = "${projectDir}${File.separator}pom.xml"
+}
+
+task generatePom {
+
+ doLast {
+ delete pomFile
+ final ExtraPropertiesExtension extension = rootProject.ext
+ final RootProjectData projectData = RootProjectData.fromEither(project, extension)
+ final ProjectPomXml result = ProjectPomXml.from(projectData)
+ result.writeTo(pomFile)
+ }
+}
+
+build.finalizedBy generatePom
+generatePom.dependsOn assemble
+
+/**
+ * A {@code pom.xml} file that contains dependencies of the project and its subprojects.
+ *
+ *
It is not usable for {@code maven} build tasks and serves as a description of project first
+ * level dependencies, i.e. transitive dependencies are not included
+ */
+class ProjectPomXml {
+
+ private static final String XML_METADATA = ""
+ private static final String PROJECT_SCHEMA_LOCATION = ""
+ private static final String MODEL_VERSION = "4.0.0"
+ private static final String CLOSING_PROJECT_TAG = ""
+ private static final String SPINE_INCEPTION_YEAR = "2015"
+ private static final String NEW_LINE = System.lineSeparator()
+
+ private final Project project
+ private final String groupId
+ private final String artifactId
+ private final String version
+
+ private ProjectPomXml(final Project project,
+ final String groupId,
+ final String artifactId,
+ final String version) {
+ this.project = project
+ this.groupId = groupId
+ this.artifactId = artifactId
+ this.version = version
+ }
+
+ /** Creates a new instance based on the specified project data. */
+ static ProjectPomXml from(final RootProjectData projectData) {
+ return new ProjectPomXml(projectData.project(),
+ projectData.groupId(),
+ projectData.artifactId(),
+ projectData.version())
+ }
+
+ /**
+ * Writes the {@code 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
+ * {@code pom.xml}.
+ *
+ * @param filePath path to write {@code pom.xml} file to
+ */
+ void writeTo(final String filePath) {
+ final FileWriter fileWriter = new FileWriter(filePath)
+ final StringWriter stringWriter = new StringWriter()
+ writeHeader(stringWriter)
+
+ writeBlocks(stringWriter,
+ describingComment(),
+ rootProjectData(),
+ inceptionYear(),
+ licence(),
+ projectDependencies()
+ )
+ fileWriter.write(stringWriter.toString())
+ fileWriter.close()
+ }
+
+ /**
+ * Writes the specified lines using the specified writer, dividing them by platforms line
+ * separator.
+ *
+ * The written lines are also padded with platforms line separator from both sides
+ */
+ static void writeBlocks(final StringWriter writer, final String... lines) {
+ writer.write(NEW_LINE)
+ for (final String line : lines) {
+ writer.write(line)
+ writer.write(NEW_LINE)
+ writer.write(NEW_LINE)
+ }
+ writer.write(NEW_LINE)
+ }
+
+ /**
+ * Obtains a String that represents a tag with the inception year of Spine.
+ */
+ private static String inceptionYear() {
+ final Writer writer = new StringWriter()
+ final MarkupBuilder xmlBuilder = new MarkupBuilder(writer)
+ xmlBuilder.inceptionYear(SPINE_INCEPTION_YEAR)
+ return writer.toString()
+ }
+
+ /**
+ * Obtains licence information about Spine.
+ *
+ *
More on licences here.
+ */
+ private static String licence() {
+ final Writer writer = new StringWriter()
+ SpineLicenceAsXml.writeUsing(writer)
+ return writer.toString()
+ }
+
+ /**
+ * Obtains a string that contains project dependencies as XML.
+ *
+ *
Obtained string also contains a closing project tag.
+ */
+ private String projectDependencies() {
+ Writer writer = new StringWriter()
+ ProjectDependenciesAsXml projectDeps = ProjectDependenciesAsXml.of(project)
+ projectDeps.writeUsing(writer)
+ writer.write(NEW_LINE)
+ writer.write(CLOSING_PROJECT_TAG)
+ return writer.toString()
+ }
+
+ /**
+ * Obtains a description comment that describes the nature of the generated {@code pom.xml} file.
+ */
+ private static String describingComment() {
+ String description =
+ System.lineSeparator() +
+ "This file was generated using the Gradle `generatePom` task. " +
+ System.lineSeparator() +
+ "This file is not suitable for `maven` build tasks. It only describes the " +
+ "first-level dependencies of " +
+ System.lineSeparator() +
+ "all modules and does not describe the project " +
+ "structure per-subproject." +
+ System.lineSeparator()
+ String descriptionComment =
+ String.format("",
+ System.lineSeparator(),
+ description,
+ System.lineSeparator())
+ return descriptionComment
+ }
+
+ /**
+ * Obtains a string that contains the name and the version of the current project.
+ */
+ private String rootProjectData() {
+ Writer writer = new StringWriter()
+ MarkupBuilder xmlBuilder = new MarkupBuilder(writer)
+ xmlBuilder.groupId(this.groupId)
+ xmlBuilder.artifactId(this.artifactId)
+ xmlBuilder.version(this.version)
+ return writer.toString()
+ }
+
+ /**
+ * Writes the XML metadata using the specified writer.
+ */
+ private static void writeHeader(StringWriter stringWriter) {
+ stringWriter.write(XML_METADATA)
+ stringWriter.write(System.lineSeparator())
+ stringWriter.write(PROJECT_SCHEMA_LOCATION)
+ stringWriter.write(System.lineSeparator())
+ stringWriter.write(MODEL_VERSION)
+ stringWriter.write(System.lineSeparator())
+ }
+}
+
+/**
+ * Dependencies of the project expressed as XML.
+ *
+ *
Subprojects dependencies are included, transitive dependencies are not included.
+ *
+ *
+ */
+class ProjectDependenciesAsXml {
+
+ private final Set firstLevelDependencies
+
+ private ProjectDependenciesAsXml(Set dependencySet) {
+ this.firstLevelDependencies = new TreeSet<>(dependencySet)
+ }
+
+ /** Creates a new instance based on the specified project. */
+ static ProjectDependenciesAsXml of(Project project) {
+ Set dependencies = projectDependencies(project)
+ return new ProjectDependenciesAsXml(dependencies)
+ }
+
+ /**
+ * Writes the dependencies using the specified writer.
+ *
+ *
Used writer will not be closed.
+ */
+ void writeUsing(Writer writer) {
+ MarkupBuilder xmlBuilder = new MarkupBuilder(writer)
+ xmlBuilder.dependencies() {
+ firstLevelDependencies
+ .forEach { projectDep ->
+ xmlBuilder.dependency {
+ groupId(projectDep.dependency().group)
+ artifactId(projectDep.dependency().name)
+ version(projectDep.dependency().version)
+ if (projectDep.hasDefinedScope()) {
+ scope(projectDep.scopeName())
+ }
+ }
+ }
+ }
+ }
+
+ private static Set projectDependencies(Project project) {
+ Set firstLevelDependencies = new HashSet<>()
+ firstLevelDependencies.addAll(dependenciesFromAllConfigurations(project))
+ project.subprojects.forEach { subproject ->
+ Set subprojectDeps = dependenciesFromAllConfigurations(subproject)
+ firstLevelDependencies.addAll(subprojectDeps)
+ }
+ return firstLevelDependencies.stream().sorted().distinct().collect(toSet())
+ }
+
+ private static Set dependenciesFromAllConfigurations(Project project) {
+ Set result = new HashSet<>()
+ project.configurations.forEach { c ->
+ def configuration = c
+ if (isResolvable(c)) {
+ // Force configuration resolution.
+ configuration.resolvedConfiguration
+ }
+ configuration.dependencies.forEach {
+ if (isExternal(it)) {
+ DependencyWithScope dependency = DependencyWithScope.of(it, configuration)
+ result.add(dependency)
+ }
+ }
+ }
+ return result
+ }
+
+ static boolean isResolvable(Configuration config) {
+ config.hasProperty("canBeResolved") && config.canBeResolved
+ }
+
+ private static boolean isExternal(Dependency dependency) {
+ return AbstractExternalModuleDependency.isAssignableFrom(dependency.class)
+ }
+}
+
+/**
+ * A project dependency with its scope.
+ *
+ * @see
+ *
+ * More on dependency scopes .
+ */
+class DependencyWithScope implements Comparable {
+
+ private final Dependency dependency
+ private final DependencyScope scope
+
+ /**
+ * A map that contains the relations of known Gradle configuration names
+ * to their Maven dependency scope equivalents.
+ */
+ private static Map CONFIG_TO_SCOPE
+
+
+ /**
+ * Performs comparison of {@code DependencyWithScope} instances based on the following rules:
+ *
+ *
+ *
Compares the scope of the dependency first. Dependencies with lower scope
+ * {@linkplain #dependencyPriority priority} number goes first.
+ *
For dependencies with same scope does the lexicographical group name comparison.
+ *
For dependencies within the same group does the lexicographical artifact
+ * name comparison.
+ *
For dependencies with the same artifact name does the lexicographical artifact
+ * version comparison.
+ *
+ */
+ private static final Comparator COMPARATOR = Comparator
+ .comparingInt { it.dependencyPriority() }
+ .thenComparing((Function) { it.dependency().group })
+ .thenComparing((Function) { it.dependency().name })
+ .thenComparing((Function) { it.dependency().version })
+
+ static {
+ final DependencyScope compile = DependencyScope.compile
+ final DependencyScope runtime = DependencyScope.runtime
+ final DependencyScope provided = DependencyScope.provided
+
+ CONFIG_TO_SCOPE = new HashMap<>()
+
+ /*
+ * Configurations from the Gradle Java plugin that are known to be mapped to the `compile`
+ * scope.
+ *
+ * Dependencies with the `compile` Maven scope are propagated to dependent projects.
+ *
+ * More at https://docs.gradle.org/current/userguide/java_plugin.html#tab:configurations
+ */
+ CONFIG_TO_SCOPE.put("compile", compile)
+ CONFIG_TO_SCOPE.put("implementation", compile)
+ CONFIG_TO_SCOPE.put("api", compile)
+
+ /*
+ * Configurations from the Gradle Java plugin that are known to be mapped to the `runtime`
+ * scope.
+ *
+ * Dependencies with the `runtime` Maven scopes are required for execution only.
+ */
+ CONFIG_TO_SCOPE.put("runtime", runtime)
+ CONFIG_TO_SCOPE.put("runtimeOnly", runtime)
+ CONFIG_TO_SCOPE.put("runtimeClasspath", runtime)
+ CONFIG_TO_SCOPE.put("default", runtime)
+
+
+ /*
+ * Configurations from the Gradle Java plugin that are known to be mapped to the `provided`
+ * scope.
+ *
+ * Dependencies with the `provided` Maven scope are not propagated to dependent projects
+ * but are required during the compilation.
+ */
+ CONFIG_TO_SCOPE.put("compileOnly", provided)
+ CONFIG_TO_SCOPE.put("compileOnlyApi", provided)
+ CONFIG_TO_SCOPE.put("annotationProcessor", provided)
+ }
+
+ private DependencyWithScope(Dependency dependency, DependencyScope scope) {
+ this.dependency = dependency
+ this.scope = scope
+ }
+
+ /**
+ * Creates a new instance based on the specified dependency and its configuration.
+ *
+ *
The scope of the dependency is based on the name of the configuration.
+ */
+ static DependencyWithScope of(Dependency dependency, Configuration configuration) {
+ String configurationName = configuration.name
+ if (CONFIG_TO_SCOPE.containsKey(configurationName)) {
+ return new DependencyWithScope(dependency, CONFIG_TO_SCOPE.get(configurationName))
+ }
+ if (configurationName.toLowerCase().startsWith("test")) {
+ return new DependencyWithScope(dependency, DependencyScope.test)
+ }
+ return new DependencyWithScope(dependency, DependencyScope.undefined)
+ }
+
+ /**
+ * Obtains the layout priority of a scope.
+ *
+ * Layout priority determines what scopes come first in the generated {@code pom.xml} file.
+ * Dependencies with a lower priority number go on top.
+ */
+ int dependencyPriority() {
+ switch(scope) {
+ case DependencyScope.compile:
+ return 0
+ case DependencyScope.runtime:
+ return 1
+ case DependencyScope.test:
+ return 2
+ default:
+ return 3
+ }
+ }
+
+ /** Obtains the scope name of this dependency .*/
+ String scopeName() {
+ return scope.name()
+ }
+
+ /** Obtains the Gradle dependency. */
+ Dependency dependency() {
+ return this.dependency
+ }
+
+ /** Obtains the Maven scope of this dependency. */
+ DependencyScope scope() {
+ return this.scope
+ }
+
+ /**
+ * Returns {@code true} if this dependency has a defined scope, returns {@code false} otherwise.
+ */
+ boolean hasDefinedScope() { return scope != DependencyScope.undefined }
+
+ @Override
+ boolean equals(o) {
+ if (this.is(o)) return true
+ if (getClass() != o.class) return false
+
+ DependencyWithScope that = (DependencyWithScope) o
+
+ if (dependency.group != that.dependency.group) return false
+ if (dependency.name != that.dependency.name) return false
+ if (dependency.version != that.dependency.version) return false
+
+ return true
+ }
+
+ @Override
+ int hashCode() {
+ int result = (dependency != null ? dependency.hashCode() : 0)
+ return result
+ }
+
+ @Override
+ int compareTo(DependencyWithScope o) {
+ return COMPARATOR.compare(this, o)
+ }
+
+ /**
+ * A Maven dependency scope.
+ */
+ static enum 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
+ */
+ }
+}
+
+/**
+ * Information about the licences used by Spine in XML form.
+ */
+class SpineLicenceAsXml {
+
+ private static final String NAME = "Apache License, Version 2.0"
+ private static final String URL = "https://www.apache.org/licenses/LICENSE-2.0.txt"
+ private static final String DISTRIBUTION = "repo"
+
+ /** Prevents instantiation. */
+ private SpineLicenceAsXml() {
+ }
+
+ /**
+ * Writes information about the Spine licence using the specified writer.
+ */
+ static void writeUsing(Writer fileWriter) {
+ MarkupBuilder xmlBuilder = new MarkupBuilder(fileWriter)
+ xmlBuilder.licenses {
+ license {
+ name(NAME)
+ url(SpineLicenceAsXml.URL)
+ distribution(DISTRIBUTION)
+ }
+ }
+ }
+}
+
+/**
+ * Information about the root project.
+ *
+ *
Root project is the project for which the {@code generatePom} is executed.
+ *
+ *
Includes group ID, artifact name and the version.
+ */
+class RootProjectData {
+
+ private final Project project
+ private final String groupId
+ private final String artifactId
+ private final String version
+
+ private RootProjectData(Project project, String group, String artifactId, String version) {
+ this.project = project
+ this.groupId = group
+ this.artifactId = artifactId
+ this.version = version
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ *
Data from the specified project is prioritized.
+ * If a property (group ID, artifact name or version) is not found in the project,
+ * it is taken from the specified extension.
+ */
+ static RootProjectData fromEither(Project project, /* or */ ExtraPropertiesExtension extension) {
+ boolean groupMissingFromProject = project.group == null || project.group.isEmpty()
+ boolean nameMissingFromProject = project.name == null || project.name.isEmpty()
+ boolean versionMissingFromProject = project.version == null || project.version.isEmpty()
+
+ String groupId = groupMissingFromProject ? extension.groupId : project.group
+ String name = nameMissingFromProject ? extension.artifactId : project.name
+ String version = versionMissingFromProject ? extension.version : project.version
+
+ return new RootProjectData(project, groupId, name, version)
+ }
+
+ /** Obtains the project object matching the root project. */
+ Project project() {
+ return this.project
+ }
+
+ /** Obtains the group ID of the root project. */
+ String groupId() {
+ return this.groupId
+ }
+
+ /** Obtains the artifact ID of the root project. */
+ String artifactId() {
+ return this.artifactId
+ }
+
+ /** Obtains the version of the root project. */
+ String version() {
+ return this.version
+ }
+}
diff --git a/buildSrc/src/main/groovy/jacoco.gradle b/buildSrc/src/main/groovy/jacoco.gradle
new file mode 100644
index 00000000..dc22ee65
--- /dev/null
+++ b/buildSrc/src/main/groovy/jacoco.gradle
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+// Apply this script to enable the combined JaCoCo test report.
+//
+// This task combines the XML report results from all Java sub-projects.
+// Inspired by: https://gist.github.com/aalmiray/e6f54aa4b3803be0bcac
+//
+// This task runs after `:check` task.
+
+import groovy.io.FileType
+
+import java.util.stream.Collectors
+
+// Required to grab dependencies for `jacocoRootReport` task.
+repositories {
+ mavenCentral()
+}
+
+println("`jacoco.gradle` script is deprecated. Please use the `JacocoConfig` instead.")
+
+// Adds the `:check` task.
+apply plugin: 'base'
+
+final def javaProjects = subprojects.findAll { it.pluginManager.hasPlugin('jacoco') }
+final def jacocoReports = file("${rootProject.buildDir}/subreports/jacoco/")
+
+task copyReports(dependsOn: javaProjects.jacocoTestReport, type: Copy) {
+ description = "Copies JaCoCo reports from subprojects into a single directory in the root project."
+
+ from files(javaProjects.jacocoTestReport.executionData)
+ into jacocoReports
+
+ rename { "${UUID.randomUUID().toString()}.exec" }
+}
+
+// Create an combined coverage report across Java modules,
+// excluding the generated content from the coverage stats.
+//
+task jacocoRootReport(dependsOn: ':copyReports', type: JacocoReport) {
+
+ final def sourceDirs = CodebaseFilter.nonGeneratedOnly(files(javaProjects.sourceSets.main.java.srcDirs))
+ additionalSourceDirs.from sourceDirs
+ sourceDirectories.from sourceDirs
+ executionData.from fileTree(jacocoReports)
+
+ final def filter = new CodebaseFilter(project,
+ javaProjects.sourceSets.main.java.srcDirs,
+ javaProjects.sourceSets.main.output)
+ final def nonGeneratedFiles = files(filter.findNonGeneratedCompiledFiles())
+ classDirectories.from nonGeneratedFiles
+ additionalClassDirs.from nonGeneratedFiles
+
+ reports {
+ html.required.set(true)
+ xml.required.set(true)
+ csv.required.set(false)
+ }
+ onlyIf = {
+ true
+ }
+}
+
+check.dependsOn jacocoRootReport
+
+/**
+ * Serves to distinguish the {@code .java} and {@code .class} files built from
+ * the Protobuf definitions from the human-created production code.
+ */
+class CodebaseFilter {
+
+ private static final String GENERATED_PATH_MARKER = "generated"
+
+ private static final String JAVA_SRC_FOLDER_MARKER = "/java/"
+ private static final String SPINE_JAVA_SRC_FOLDER_MARKER = "main/spine/"
+ private static final String GRPC_SRC_FOLDER_MARKER = "/main/grpc/"
+
+ private static final String JAVA_OUTPUT_FOLDER_MARKER = "/main/"
+
+ private static final String JAVA_SOURCE_FILE_EXTENSION = ".java"
+ private static final String COMPILED_CLASS_FILE_EXTENSION = ".class"
+ private static final String ANONYMOUS_CLASS_MARKER = '$'
+ private static final String ANONYMOUS_CLASS_PATTERN = "\\${ANONYMOUS_CLASS_MARKER}"
+
+ private final Project project
+ private final def javaSrcDirs
+ private final def outputDirs
+
+ CodebaseFilter(final Project project, final javaSrcDirs, final outputDirs) {
+ this.project = project
+ this.javaSrcDirs = javaSrcDirs
+ this.outputDirs = outputDirs
+ }
+
+ static FileCollection nonGeneratedOnly(final FileCollection files) {
+ return files.filter {
+ !it.absolutePath.contains(GENERATED_PATH_MARKER)
+ }
+ }
+
+ static FileCollection generatedOnly(final FileCollection files) {
+ return files.filter {
+ it.absolutePath.contains(GENERATED_PATH_MARKER)
+ }
+ }
+
+ private static String parseClassName(final File file, final String sourceFolderMarker, final String extension) {
+
+ final def index = file.absolutePath.lastIndexOf(sourceFolderMarker)
+ if (index > 0) {
+ def filePathInFolder = file.absolutePath.substring(index + sourceFolderMarker.length())
+ if (filePathInFolder.endsWith(extension)) {
+ filePathInFolder = filePathInFolder.substring(0, filePathInFolder.length() - extension.length())
+
+ final def className = filePathInFolder.replace('/', '.')
+ return className
+ } else {
+ return null
+ }
+ } else {
+ return null
+ }
+ }
+
+ private LinkedList getGeneratedClassNames() {
+ final def sourceFiles = project.files(javaSrcDirs)
+ final def generatedSourceFiles = generatedOnly(sourceFiles)
+
+ final def generatedClassNames = []
+ generatedSourceFiles.each { final folder ->
+ if (folder.exists() && folder.isDirectory()) {
+ folder.eachFileRecurse(FileType.FILES) { final aFile ->
+ final def name = parseClassName(aFile, JAVA_SRC_FOLDER_MARKER, JAVA_SOURCE_FILE_EXTENSION)
+ if (name != null) {
+ generatedClassNames.add(name)
+ } else {
+ // Try another folder prefix; perhaps this file is gRPC service.
+ final def generatedByGrpc = parseClassName(aFile,
+ GRPC_SRC_FOLDER_MARKER,
+ JAVA_SOURCE_FILE_EXTENSION)
+ if (generatedByGrpc != null) {
+ generatedClassNames.add(generatedByGrpc)
+ } else {
+ // Try one more folder prefix; perhaps this file is generated by Spine.
+ final def generatedBySpine =
+ parseClassName(aFile, SPINE_JAVA_SRC_FOLDER_MARKER, JAVA_SOURCE_FILE_EXTENSION)
+ if (generatedBySpine != null) {
+ generatedClassNames.add(generatedBySpine)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return generatedClassNames
+ }
+
+ List findNonGeneratedCompiledFiles() {
+ log("Source dirs for code coverage calculation:")
+ final def srcDirs = project.files(javaSrcDirs)
+ srcDirs.each {
+ log(" - ${it}")
+ }
+
+ final def generatedClassNames = getGeneratedClassNames()
+ log(generatedClassNames.join('\n'))
+ final def nonGeneratedClassTree = outputDirs
+ .stream()
+ .flatMap { it.getClassesDirs().files.stream() }
+ .map { final srcFile ->
+ log("Filtering out the generated classes for ${srcFile}")
+
+ // Return the filtered `fileTree`s as a collected result.
+ return project.fileTree(dir: srcFile, exclude: { final details ->
+ def className = parseClassName(details.file, JAVA_OUTPUT_FOLDER_MARKER, COMPILED_CLASS_FILE_EXTENSION)
+
+ // Handling anonymous classes as well.
+ // They should be associated with the same `.java` file as their parent class.
+ if (className != null && className.contains(ANONYMOUS_CLASS_MARKER)) {
+ // Assuming there cannot be more than a single `$`.
+ className = className.split(ANONYMOUS_CLASS_PATTERN)[0]
+ }
+ return generatedClassNames.contains(className)
+ })
+ }.collect(Collectors.toList())
+ return nonGeneratedClassTree
+ }
+
+ private void log(final String message) {
+ project.logger.debug(message)
+ }
+}
diff --git a/buildSrc/src/main/groovy/javac-args.gradle b/buildSrc/src/main/groovy/javac-args.gradle
new file mode 100644
index 00000000..0a041e64
--- /dev/null
+++ b/buildSrc/src/main/groovy/javac-args.gradle
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021, 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 configures Java Compiler.
+ */
+
+println("`javac-args.gradle` script is deprecated. Please use `JavaCompile.configureJavac()` " +
+ "and `JavaCompile.configureErrorProne()` utilities instead.")
+
+tasks.withType(JavaCompile) {
+
+ if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
+ throw new GradleException("Spine Event Engine can be built with JDK 8 only." +
+ " Supporting JDK 11 and above at build-time is planned in 2.0 release." +
+ " Please use the pre-built binaries available in the Spine Maven repository." +
+ " See https://github.com/SpineEventEngine/base/issues/457.")
+ }
+
+ // Explicitly states the encoding of the source and test source files, ensuring
+ // correct execution of the `javac` task.
+ //
+ // Also promotes compiler warnings to errors, so that the build fails on warnings.
+ // This includes Error Prone warnings
+ options.encoding = 'UTF-8'
+ options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+ // The last command line argument is temporarily commented out because of
+ // this issue: https://github.com/SpineEventEngine/config/issues/173
+ // Please restore when the issue is resolved.
+ // << "-Werror"
+
+ // Configure Error Prone:
+ // 1. Exclude generated sources from being analyzed by Error Prone.
+ // 2. Turn the check off until Error Prone can handle `@Nested` JUnit classes.
+ // See issue: https://github.com/google/error-prone/issues/956
+ // 3. Turn off checks which report unused methods and unused method parameters.
+ // See issue: https://github.com/SpineEventEngine/config/issues/61
+ //
+ // For more config details see:
+ // https://github.com/tbroyer/gradle-errorprone-plugin/tree/master#usage
+
+ options.errorprone.errorproneArgs.addAll('-XepExcludedPaths:.*/generated/.*',
+ '-Xep:ClassCanBeStatic:OFF',
+ '-Xep:UnusedMethod:OFF',
+ '-Xep:UnusedVariable:OFF',
+ '-Xep:CheckReturnValue:OFF',
+ '-Xep:FloggerSplitLogStatement:OFF')
+}
diff --git a/buildSrc/src/main/groovy/js/build-tasks.gradle b/buildSrc/src/main/groovy/js/build-tasks.gradle
new file mode 100644
index 00000000..dafd8b47
--- /dev/null
+++ b/buildSrc/src/main/groovy/js/build-tasks.gradle
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2021, 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 declares common Gradle tasks required for JavaScript modules.
+ *
+ *
Most of the tasks launch a separate process which runs an NPM CLI command, so it's necessary
+ * that the NPM command line tool is installed.
+ *
+ *
This is a lifecycle task. It performs no action but triggers all the tasks which perform
+ * the compilation.
+ */
+task compileProtoToJs {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = "Compiles Protobuf sources to JavaScript."
+}
+
+/**
+ * Installs the module dependencies using the `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 `auditNodePackages` task.
+ */
+task installNodePackages {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Installs the module`s Node dependencies.'
+
+ inputs.file packageJsonFile
+ outputs.dir nodeModulesDir
+
+ doLast {
+ // To turn off npm audit when installing all packages. Use `auditNodePackages` task
+ // to check installed Node packages for vulnerabilities.
+ npm 'set', 'audit', 'false'
+ npm 'install'
+ }
+}
+
+/**
+ * Audits the module dependencies using the `npm audit` command.
+ *
+ * Sets the minimum level of vulnerability for `npm audit` to exit with a non-zero exit code
+ * to "high".
+ */
+task auditNodePackages {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Audits the module`s Node dependencies.'
+ dependsOn installNodePackages
+
+ inputs.dir nodeModulesDir
+
+ doLast {
+ npm 'set', 'audit-level', 'critical'
+ try {
+ npm 'audit'
+ } catch (final Exception ignored) {
+ npm 'audit', '--registry' 'http://registry.npmjs.eu'
+ }
+ }
+}
+
+check.dependsOn auditNodePackages
+
+/**
+ * Cleans output of `buildJs` and dependant tasks.
+ */
+task cleanJs {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Cleans the output of JavaScript build.'
+
+ clean.dependsOn cleanJs
+
+ doLast {
+ delete buildJs.outputs
+ delete compileProtoToJs.outputs
+ delete installNodePackages.outputs
+ }
+}
+
+/**
+ * Assembles the JS sources.
+ *
+ * This task is an analog of `build` for JS.
+ *
+ * To include a task into the JS build, depend `buildJs` onto that task.
+ */
+task buildJs {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = "Assembles the JavaScript source files."
+
+ dependsOn updatePackageVersion
+ dependsOn installNodePackages
+ dependsOn compileProtoToJs
+ assemble.dependsOn buildJs
+}
+
+/**
+ * Tests the JS sources.
+ *
+ * If `check` task is scheduled to run, this task is automatically added to
+ * the task graph and will be executed after `test` task completion.
+ */
+task testJs {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = "Tests the JavaScript source files."
+
+ dependsOn installNodePackages
+ dependsOn compileProtoToJs
+ check.dependsOn testJs
+
+ testJs.mustRunAfter test
+}
+
+idea.module {
+ excludeDirs += file(nodeModulesDir)
+}
diff --git a/buildSrc/src/main/groovy/js/configure-proto.gradle b/buildSrc/src/main/groovy/js/configure-proto.gradle
new file mode 100644
index 00000000..75d2950e
--- /dev/null
+++ b/buildSrc/src/main/groovy/js/configure-proto.gradle
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2021, 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.Scripts
+
+/**
+ * Configures how the Proto definitions of the project are compiled into JavaScript and their
+ * publishing.
+ *
+ * Use this script if the project contains Proto files that should be published as a separate NPM
+ * module.
+ *
+ * The prerequisites for using the script are:
+ *
+ * 1. The Spine Proto JS plugin applied to the project
+ * (see https://github.com/SpineEventEngine/base/tree/master/tools/mc-js).
+ *
+ * 2. The extension variable `versionToPublishJs` configured to represent the version under which
+ * the NPM packages should be published.
+ */
+
+println("`configure-proto.gradle` script is deprecated. Please use `javascript` extension instead.")
+
+ext {
+ genProtoBaseDir = "$projectDir/generated"
+ genProtoMain = "$genProtoBaseDir/main/js"
+ genProtoTest = "$genProtoBaseDir/test/js"
+}
+
+apply from: "$rootDir" + io.spine.internal.gradle.Scripts.commonPath + "js/npm-publish-tasks.gradle"
+
+/**
+ * Configures the JS code generation.
+ */
+protobuf {
+ generatedFilesBaseDir = genProtoBaseDir
+ protoc {
+ artifact = Protobuf.protoc
+ }
+ generateProtoTasks {
+ all().each { final task ->
+ task.builtins {
+ // For information on JavaScript code generation please see
+ // https://github.com/google/protobuf/blob/master/js/README.md
+ js {
+ option "import_style=commonjs"
+ }
+
+ task.generateDescriptorSet = true
+ final def testClassifier = task.sourceSet.name == "test" ? "_test" : ""
+ final def descriptorName =
+ "${project.group}_${project.name}_${project.version}${testClassifier}.desc"
+ task.descriptorSetOptions.path =
+ "${projectDir}/build/descriptors/${task.sourceSet.name}/${descriptorName}"
+ }
+ compileProtoToJs.dependsOn task
+ }
+ }
+}
+
+/**
+ * Configures the generation of additional features for the Proto messages via
+ * Spine Proto JS plugin.
+ */
+protoJs {
+ mainGenProtoDir = genProtoMain
+ testGenProtoDir = genProtoTest
+
+ generateParsersTask().dependsOn compileProtoToJs
+ buildJs.dependsOn generateParsersTask()
+}
+
+/**
+ * Prepares Proto files for publication, see {@code npm-publish-tasks.gradle}.
+ */
+prepareJsPublication {
+
+ doLast {
+ copy {
+ from (projectDir) {
+ include 'package.json'
+ include '.npmrc'
+ }
+
+ from (genProtoMain) {
+ include '**'
+ }
+
+ into publicationDirectory
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/js/js.gradle b/buildSrc/src/main/groovy/js/js.gradle
new file mode 100644
index 00000000..836bb729
--- /dev/null
+++ b/buildSrc/src/main/groovy/js/js.gradle
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+/**
+ * A script configuring the build of a JavaScript module, which is going to be published to NPM.
+ *
+ *
In particular, the script brings tasks to:
+ *
+ *
Install NPM dependencies.
+ *
Compile Protobuf sources to JavaScript.
+ *
Test and generate coverage reports for JavaScript sources.
+ *
Update the version in `package.json` depending on the `versionToPublishJs` property.
+ *
Clean up the build output of a JavaScript module.
+ *
+ *
+ *
The script is based on other scripts from `config` submodule.
+ */
+
+println("`js.gradle` script is deprecated. Please use `javascript` extension instead.")
+
+ext {
+ srcDir = "$projectDir/main"
+ testSrcDir = "$projectDir/test"
+ genProtoBaseDir = projectDir
+ genProtoSubDir = "proto"
+ genProtoMain = "$genProtoBaseDir/main/$genProtoSubDir"
+ genProtoTest = "$genProtoBaseDir/test/$genProtoSubDir"
+ nycOutputDir = "$projectDir/.nyc_output"
+}
+
+apply from: "$rootDir" + io.spine.internal.gradle.Scripts.commonPath + "js/npm-publish-tasks.gradle"
+
+/**
+ * Cleans old module dependencies and build outputs.
+ */
+task deleteCompiled {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Cleans old module dependencies and build outputs.'
+
+ clean.dependsOn deleteCompiled
+
+ doLast {
+ delete genProtoMain
+ delete genProtoTest
+ delete coverageJs.outputs
+ }
+}
+
+/**
+ * Customizes the task already defined in `config` module by running Webpack build.
+ */
+buildJs {
+
+ outputs.dir "$projectDir/dist"
+
+ doLast {
+ npm 'run', 'build'
+ npm 'run', 'build-dev'
+ }
+}
+
+/**
+ * Customizes the task already defined in `config` module by running
+ * the JavaScript tests.
+ */
+testJs {
+
+ doLast {
+ npm 'run', 'test'
+ }
+}
+
+/**
+ * Copies bundled JS sources to the temporary NPM publication directory.
+ */
+task copyBundledJs(type: Copy) {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Copies assembled JavaScript sources to the NPM publication directory.'
+
+ from buildJs.outputs
+ into "$publicationDirectory/dist"
+
+ dependsOn buildJs
+}
+
+/**
+ * Transpiles JS sources before publishing them to NPM.
+ *
+ * Puts the resulting files to the temporary NPM publication directory.
+ */
+task transpileSources {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = "Transpiles sources before publishing."
+
+ doLast {
+ npm 'run', 'transpile-before-publish'
+ }
+}
+
+/**
+ * Defines files to copy by the task.
+ */
+prepareJsPublication {
+
+ doLast {
+ copy {
+ from (projectDir) {
+ include 'package.json'
+ include '.npmrc'
+ }
+ into publicationDirectory
+ }
+ }
+
+ //TODO:2019-02-05:dmytro.grankin: temporarily don't publish a bundle, see https://github.com/SpineEventEngine/web/issues/61
+ //dependsOn copyBundledJs
+ dependsOn transpileSources
+}
+
+/**
+ * Runs the JavaScript tests and collects the code coverage.
+ */
+task coverageJs {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Runs the JS tests and collects the code coverage info.'
+
+ outputs.dir nycOutputDir
+
+ final def runsOnWindows = org.gradle.internal.os.OperatingSystem.current().isWindows()
+ final def coverageScript = runsOnWindows ? 'coverage:win' : 'coverage:unix'
+
+ doLast {
+ npm 'run', coverageScript
+ }
+
+ dependsOn buildJs
+
+ rootProject.check.dependsOn coverageJs
+}
+
+/**
+ * Generates the report on NPM dependencies and their licenses.
+ */
+task npmLicenseReport {
+
+ doLast {
+ npm 'run', 'license-report'
+ }
+}
+
+/**
+ * Runs NPM license report straight after the Gradle license report.
+ */
+//TODO:2021-09-22:alexander.yevsyukov: Resolve this dependency.
+//generateLicenseReport.finalizedBy npmLicenseReport
+
+apply plugin: 'io.spine.mc-js'
+
+protoJs {
+ generatedMainDir = genProtoMain
+ generatedTestDir = genProtoTest
+
+ generateParsersTask().dependsOn compileProtoToJs
+ buildJs.dependsOn generateParsersTask()
+ testJs.dependsOn buildJs
+}
+
+
+protobuf {
+ generatedFilesBaseDir = genProtoBaseDir
+ protoc {
+ artifact = io.spine.internal.dependency.Protobuf.compiler
+ }
+ generateProtoTasks {
+ all().each { final 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
+ js {
+ option "import_style=commonjs"
+ outputSubDir = genProtoSubDir
+ }
+
+ task.generateDescriptorSet = true
+ final def testClassifier = task.sourceSet.name == "test" ? "_test" : ""
+ final def descriptorName = "${project.group}_${project.name}_${project.version}${testClassifier}.desc"
+ task.descriptorSetOptions.path = "${projectDir}/build/descriptors/${task.sourceSet.name}/${descriptorName}"
+ }
+ compileProtoToJs.dependsOn task
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/js/npm-cli.gradle b/buildSrc/src/main/groovy/js/npm-cli.gradle
new file mode 100644
index 00000000..c2228449
--- /dev/null
+++ b/buildSrc/src/main/groovy/js/npm-cli.gradle
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2021, 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.apache.tools.ant.taskdefs.condition.Os
+
+/**
+ * The script allowing to run NPM commands from Gradle.
+ */
+
+println("`npm-cli.gradle` script is deprecated. Please use `javascript` extension instead.")
+
+/**
+ * The name of an environmental variable which contains the NPM auth token.
+ *
+ *
The token is used for publishing only.
+ *
+ *
It is required to set this variable before invoking NPM commands.
+ */
+ext.NPM_TOKEN_VARIABLE = "NPM_TOKEN"
+
+/**
+ * @return the value of `NPM_TOKEN` environmental variable or a stub value, if the token is not set
+ */
+String npmToken() {
+ final def tokenVarValue = System.getenv(NPM_TOKEN_VARIABLE)
+ final def token = tokenVarValue?.isEmpty() ? "PUBLISHING_FORBIDDEN" : tokenVarValue
+ return token
+}
+
+/**
+ * Executes the given command depending on the current OS.
+ *
+ * @param workingDirArg the directory to execute the command in
+ * @param windowsCommand the command to execute is the OS is Windows
+ * @param unixCommand the command to execute is the OS is Unix-like
+ * @param params the command params, platform-independent
+ */
+def execMultiplatform(final File workingDirArg,
+ final String windowsCommand,
+ final String unixCommand,
+ final String[] params) {
+ exec {
+ final String command = Os.isFamily(Os.FAMILY_WINDOWS) ? windowsCommand : unixCommand
+ final List resultingParams = [command] + Arrays.asList(params)
+ workingDir = workingDirArg
+ commandLine = resultingParams
+ environment NPM_TOKEN_VARIABLE, npmToken()
+ }
+}
+
+def runNpm(final File launchDir, final String[] params) {
+ execMultiplatform launchDir, 'npm.cmd', 'npm', params
+}
+
+ext {
+
+ /**
+ * Executes an {@code npm} CLI command.
+ *
+ * For example, to execute command {@code npm run compile}, invoke this function as follows:
+ * {@code executeNpm 'run', 'compile'}
+ *
+ * @param params the command parameters
+ */
+ executeNpm = { final File from = projectDir, final String... params ->
+ runNpm(from, params)
+ }
+}
diff --git a/buildSrc/src/main/groovy/js/npm-publish-tasks.gradle b/buildSrc/src/main/groovy/js/npm-publish-tasks.gradle
new file mode 100644
index 00000000..70421e27
--- /dev/null
+++ b/buildSrc/src/main/groovy/js/npm-publish-tasks.gradle
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+/**
+ * The script declares tasks for publishing to NPM.
+ *
+ *
In order to publish the NPM module, it is required that the {@code NPM_TOKEN} environment
+ * variable is set to a valid NPM auth token. If the token is not set, a dummy value is added to
+ * the NPM execution process, which is sufficient for the local development.
+ */
+
+println("`npm-publish-tasks.gradle` script is deprecated. " +
+ "Please use `javascript` extension instead.")
+
+apply from: "$rootDir" + io.spine.internal.gradle.Scripts.commonPath + "js/build-tasks.gradle"
+
+ext {
+ publicationDirectory = "$buildDir/npm-publication/"
+}
+
+/**
+ * A task to prepare files for publication.
+ *
+ *
Does nothing by default, so a user should configure this task
+ * to copy all required files to the {@code publicationDirectory}.
+ *
+ *
The task is performed before {@code link} and {@code publishJs} tasks.
+ *
+ *
The task isn't a {@code copy} task since it causes side effects
+ * like a removal of the publication directory. See https://github.com/gradle/gradle/issues/1012.
+ */
+task prepareJsPublication {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Prepares the NPM package for publish.'
+
+ dependsOn buildJs
+}
+
+/**
+ * Publishes the NPM package locally with `npm link`.
+ */
+task link {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = "Publishes the NPM package locally."
+
+ doLast {
+ executeNpm(publicationDirectory as File, 'link')
+ }
+
+ dependsOn prepareJsPublication
+}
+
+/**
+ * Publishes the NPM package with `npm publish`.
+ */
+task publishJs {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Publishes the NPM package.'
+
+ doLast {
+ executeNpm(publicationDirectory as File, 'publish')
+ }
+
+ dependsOn prepareJsPublication
+ publish.dependsOn publishJs
+}
diff --git a/buildSrc/src/main/groovy/js/update-package-version.gradle b/buildSrc/src/main/groovy/js/update-package-version.gradle
new file mode 100644
index 00000000..ddbd665d
--- /dev/null
+++ b/buildSrc/src/main/groovy/js/update-package-version.gradle
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021, 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 groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+
+/**
+ * This script declares a task to update the version in in a package.json file.
+ */
+
+println("`update-package-version.gradle` script is deprecated. " +
+ "Please use `javascript` extension instead.")
+
+/**
+ * Updates the package.json version to the specified one.
+ *
+ *
The path of the package.json and the version to set has default values,
+ * but can be redefined:
+ *
+ *
+ */
+task updatePackageVersion() {
+ group = JAVA_SCRIPT_TASK_GROUP
+ description = 'Updates the version in package.json.'
+
+ ext.packageJsonPath = packageJsonFile
+ ext.newVersion = versionToPublishJs
+
+ doLast {
+ updatePackageJsonVersion(packageJsonPath as String, newVersion as String)
+ }
+}
+
+void updatePackageJsonVersion(final String packageJsonPath, final String newVersion) {
+ def final packageJsonObject = readJsonFile(packageJsonPath)
+
+ packageJsonObject['version'] = newVersion
+ updatePackageJson(packageJsonObject, packageJsonPath)
+}
+
+Object readJsonFile(final String sourcePath) {
+ def final packageJsonFile = file(sourcePath)
+ return new JsonSlurper().parseText(packageJsonFile.text)
+}
+
+static void updatePackageJson(final jsonObject, final String destinationPath) {
+ def final updatedText = JsonOutput.toJson(jsonObject)
+ def final prettyText = prettify(updatedText)
+ new File(destinationPath).text = prettyText
+}
+
+static String prettify(final String jsonText) {
+ def prettyText = JsonOutput.prettyPrint(jsonText)
+ prettyText = prettyText.replace(' ', ' ')
+ prettyText = prettyText.replaceAll(/\{\s+}/, '{}')
+ prettyText = prettyText + '\n'
+ return prettyText
+}
diff --git a/buildSrc/src/main/groovy/license-report-common.gradle b/buildSrc/src/main/groovy/license-report-common.gradle
new file mode 100644
index 00000000..ce50599c
--- /dev/null
+++ b/buildSrc/src/main/groovy/license-report-common.gradle
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021, 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 defines the common configuration for license report scripts.
+ */
+
+println("`license-report-common.gradle` script is deprecated. " +
+ "Please use the `LicenseReporter` utility instead.")
+
+apply plugin: 'base'
+
+ext.licenseReportConfig = [
+ // The output filename
+ outputFilename : "license-report.md",
+
+ // The path to a directory, to which a per-project report is generated.
+ relativePath : "/reports/dependency-license/dependency"
+]
diff --git a/buildSrc/src/main/groovy/license-report-project.gradle b/buildSrc/src/main/groovy/license-report-project.gradle
new file mode 100644
index 00000000..cb845e2f
--- /dev/null
+++ b/buildSrc/src/main/groovy/license-report-project.gradle
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2021, 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:noinspection UnnecessaryQualifiedReference
+
+import com.github.jk1.license.*
+import com.github.jk1.license.render.ReportRenderer
+import io.spine.internal.gradle.publish.PublishExtension
+
+/**
+ * This script plugin generates the license report for all dependencies used in a project.
+ *
+ *
Transitive dependencies are included.
+ *
+ *
Use `generateLicenseReport` task to trigger the generation.
+ */
+
+buildscript {
+ repositories {
+ gradlePluginPortal()
+ }
+
+ dependencies {
+ classpath io.spine.internal.dependency.LicenseReport.lib
+ }
+}
+
+println("`license-report-project.gradle` script is deprecated. " +
+ "Please use the `LicenseReporter` utility instead.")
+
+
+apply plugin: io.spine.internal.dependency.LicenseReport.GradlePlugin.id
+
+final def commonPath = io.spine.internal.gradle.Scripts.commonPath
+apply from: "${rootDir}/${commonPath}/license-report-common.gradle"
+
+final reportOutputDir = "${project.buildDir}" + licenseReportConfig.relativePath
+licenseReport {
+ outputDir = "$reportOutputDir"
+ excludeGroups = ['io.spine', 'io.spine.tools', 'io.spine.gcloud']
+ configurations = ALL
+ renderers = [new MarkdownReportRenderer(licenseReportConfig.outputFilename)]
+}
+
+/**
+ * Renders the dependency report in markdown.
+ */
+class MarkdownReportRenderer implements ReportRenderer {
+
+ private Project project
+ private LicenseReportExtension config
+ private File output
+ private String fileName
+
+ MarkdownReportRenderer(final String fileName) {
+ this.fileName = fileName
+ }
+
+ @Input
+ String getFileNameCache() { return this.fileName }
+
+ void render(final ProjectData data) {
+ project = data.project
+ config = project.licenseReport
+ output = new File(config.outputDir, fileName)
+ final String prefix = prefixFor(project)
+ output.text = """
+ \n# Dependencies of `${project.group}:$prefix${project.name}:${project.version}`
+"""
+ printDependencies(data)
+ output << """
+
+ \n The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
+"""
+ output << "\n\nThis report was generated on **${new Date()}**"
+ output << " using [Gradle-License-Report plugin]" +
+ "(https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under" +
+ " [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE)."
+ }
+
+ /**
+ * Obtains the artifact prefix for the passed project.
+ *
+ *
If the {@code PublishExtension} has the property {@code spinePrefix} set to {@code true}
+ * returns {@code "spine-"}, otherwise an empty string.
+ */
+ private static String prefixFor(final Project project) {
+ final publishExtension = project.rootProject.extensions.findByType(PublishExtension.class)
+ final prefix = (publishExtension?.spinePrefix?.getOrElse(false) ?: false)
+ ? "spine-"
+ : ""
+ prefix
+ }
+
+ private void printDependencies(final ProjectData data) {
+
+ final runtimeDependencies = new HashSet()
+ final compileToolingDependencies = new HashSet()
+
+ data.configurations.each { final ConfigurationData config ->
+
+
+ if(["runtime", "runtimeClasspath"].indexOf(config.name) != -1) {
+ runtimeDependencies.addAll(config.dependencies)
+ } else {
+ compileToolingDependencies.addAll(config.dependencies)
+ }
+ }
+
+ output << "\n## Runtime"
+ runtimeDependencies.toArray().sort().each {
+ printModuleDependency(it)
+ }
+
+ output << "\n## Compile, tests and tooling"
+ compileToolingDependencies.toArray().sort().each {
+ printModuleDependency(it)
+ }
+ }
+
+ private void printModuleDependency(final ModuleData data) {
+ boolean projectUrlDone = false
+ output << "\n1."
+
+ if (data.group) {
+ output << " **Group:** $data.group"
+ }
+ if (data.name) {
+ output << " **Name:** $data.name"
+ }
+ if (data.version) {
+ output << " **Version:** $data.version"
+ }
+ if (data.poms.isEmpty() && data.manifests.isEmpty()) {
+ output << " **No license information found**"
+ return
+ }
+
+ if (!data.manifests.isEmpty() && !data.poms.isEmpty()) {
+ final ManifestData manifest = data.manifests.first()
+ final PomData pomData = data.poms.first()
+ if (manifest.url && pomData.projectUrl && manifest.url == pomData.projectUrl) {
+ output << "\n * **Project URL:** [$manifest.url]($manifest.url)"
+ projectUrlDone = true
+ }
+ }
+
+ if (!data.manifests.isEmpty()) {
+ final ManifestData manifest = data.manifests.first()
+ if (manifest.url && !projectUrlDone) {
+ output << "\n * **Manifest Project URL:** [$manifest.url]($manifest.url)"
+ }
+ if (manifest.license) {
+ if (manifest.license.startsWith("http")) {
+ output << "\n * **Manifest license URL:** [$manifest.license]($manifest.license)"
+
+ } else if (manifest.hasPackagedLicense) {
+ output << "\n * **Packaged License File:** [$manifest.license]($manifest.url)"
+ } else {
+ output << "\n * **Manifest License:** $manifest.license (Not packaged)"
+
+ }
+ }
+ }
+
+ if (!data.poms.isEmpty()) {
+ final PomData pomData = data.poms.first()
+ if (pomData.projectUrl && !projectUrlDone) {
+ output << "\n * **POM Project URL:** [$pomData.projectUrl]($pomData.projectUrl)"
+
+ }
+ if (pomData.licenses) {
+ pomData.licenses.each { final License license ->
+ output << "\n * **POM License: $license.name**"
+
+ if (license.url) {
+ if (license.url.startsWith("http")) {
+ output << " - [$license.url]($license.url)"
+ } else {
+ output << " **License:** $license.url"
+
+ }
+ }
+ }
+ }
+ }
+ output << '\n'
+ }
+}
diff --git a/buildSrc/src/main/groovy/license-report-repo.gradle b/buildSrc/src/main/groovy/license-report-repo.gradle
new file mode 100644
index 00000000..ed5a6b47
--- /dev/null
+++ b/buildSrc/src/main/groovy/license-report-repo.gradle
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021, 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 launches the per-project license report generation, concatenates the results
+ * and puts the resulting file into a repository root folder.
+ *
+ * If the repository consists just of a single root project, the generation is configured
+ * just for it alone.
+ *
+ * The script also configures the `build` task to be finalized by the license report routines.
+ *
+ * See `license-report-project.gradle` for a per-project license report generation.
+ */
+
+println("`license-report-repo.gradle` script is deprecated. " +
+ "Please use the `LicenseReporter` utility instead.")
+
+
+final def commonPath = io.spine.internal.gradle.Scripts.commonPath
+apply from: "${rootDir}/${commonPath}/license-report-common.gradle"
+
+task reportLicensesInRepo { final Task task ->
+ def targetProjects;
+ if (subprojects.isEmpty()) {
+ println "Configuring the license report for a single root project."
+ targetProjects = [task.project]
+ } else {
+ println "Configuring the license report for all subprojects of a root project."
+ targetProjects = subprojects
+ }
+
+ targetProjects.forEach {
+ final def generateLicenseReport = it.tasks.findByName('generateLicenseReport')
+ task.dependsOn(generateLicenseReport)
+ generateLicenseReport.dependsOn(it.tasks.findByName('assemble'))
+ }
+
+ doLast {
+ final paths = targetProjects.stream().collect {
+ "${it.buildDir}${licenseReportConfig.relativePath}/${licenseReportConfig.outputFilename}"
+ }
+
+ println "Aggregating the reports from the target projects."
+ final aggregatedContent = paths.collect { (new File(it)).getText() }.join("\n\n\n")
+
+ (new File("$rootDir/$licenseReportConfig.outputFilename")).text = aggregatedContent
+ }
+}
+
+build.finalizedBy reportLicensesInRepo
+reportLicensesInRepo.dependsOn assemble
diff --git a/buildSrc/src/main/groovy/publish-proto.gradle b/buildSrc/src/main/groovy/publish-proto.gradle
new file mode 100644
index 00000000..a29735ac
--- /dev/null
+++ b/buildSrc/src/main/groovy/publish-proto.gradle
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2021, 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 plugin enables a project to publish a JAR containing all the {@code .proto} definitions
+ * found in the project classpath, which is the definitions from {@code sourceSets.main.proto} and
+ * the proto files extracted from the JAR dependencies of the project.
+ *
+ *
The relative file paths are kept.
+ *
+ *
To depend onto such artifact of e.g. the spine-client module, use:
+ *
+ */
+
+buildscript {
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ }
+
+ dependencies {
+ //noinspection UnnecessaryQualifiedReference
+ classpath io.spine.internal.dependency.Guava.lib
+ }
+}
+
+task assembleProto(type: Jar) {
+ description "Assembles a JAR artifact with all Proto definitions from the classpath."
+ from { collectProto() }
+ include { isProtoFileOrDir(it.file) }
+ classifier 'proto'
+}
+
+artifacts {
+ archives assembleProto
+}
+
+/**
+ * Collects all the directories from current project and its dependencies (including zip tree
+ * directories) which contain {@code .proto} definitions.
+ *
+ *
The directories may in practice include files of other extension. The caller should take care
+ * of handling those files respectively.
+ *
+ *
It's guaranteed that there are no other Proto definitions in the current project classpath
+ * except those included into the returned {@code Collection}.
+ */
+Collection collectProto() {
+ final def dependencies = configurations.runtimeClasspath.files
+ final def jarFiles = dependencies.collect { JarFileName.ofFile(it) }
+ final def result = new HashSet<>()
+ for (final File jarFile in dependencies) {
+ if (jarFile.name.endsWith(".jar")) {
+ final def zipTree = zipTree(jarFile)
+ try {
+ for (final File file in zipTree) {
+ if (isProtoFile(file)) {
+ result.add(getProtoRoot(file, jarFiles))
+ }
+ }
+ } catch (GradleException e) {
+ /*
+ * As the :assembleProto task configuration is resolved first upon the project
+ * configuration (and we don't have the dependencies there yet) and then upon
+ * the execution, the task should complete successfully.
+ *
+ * To make sure the configuration phase passes, we suppress the GradleException
+ * thrown by `zipTree()` indicating that the given file, which is a dependency JAR
+ * file does not exist.
+ *
+ * Though, if this error is thrown on the execution phase, this IS an error. Thus,
+ * we log an error message.
+ *
+ * As a side effect, the message is shown upon `./gradlew clean build` or upon
+ * a newly created version of framework build etc.
+ */
+ logger.debug(
+ "${e.message}${System.lineSeparator()}The proto artifact may be corrupted."
+ )
+ }
+ }
+ }
+ result.addAll(sourceSets.main.proto.srcDirs)
+ return result
+}
+
+/**
+ * Returns the root directory containing a Proto package.
+ *
+ * @param member the member File of the Proto package
+ * @param jarNames the full listing of the project JAR dependencies
+ */
+static File getProtoRoot(final File member, final Collection jarNames) {
+ File pkg = member
+ while (!jarNames.contains(jarName(pkg.parentFile))) {
+ pkg = pkg.parentFile
+ }
+ return pkg.parentFile
+}
+
+/**
+ * Retrieves the name of the given folder trimmed by {@code ".jar"} suffix.
+ *
+ *
More formally, returns the name of the given {@link File} if the name does not contain
+ * {@code ".jar"} substring or the substring of the name containing the characters from the start
+ * to the {@code ".jar"} sequence (inclusively).
+ *
+ *
This transformation corresponds to finding the name of a JAR file which was extracted to
+ * the given directory with Gradle {@code zipTree()} API.
+ *
+ * @param jar the folder to get the JAR name for
+ */
+static JarFileName jarName(final File jar) {
+ final String unpackedJarInfix = ".jar"
+ final String name = jar.name
+ final int index = name.lastIndexOf(unpackedJarInfix)
+ if (index < 0) {
+ return null
+ } else {
+ return JarFileName.ofValue(name.substring(0, index + unpackedJarInfix.length()))
+ }
+}
+
+/**
+ * Checks if the given abstract pathname represents either a {@code .proto} file, or a directory
+ * containing proto files.
+ *
+ *
If {@code candidate} is a directory, scans its children recursively.
+ *
+ * @param candidate the {@link File} to check
+ * @return {@code true} if the {@code candidate} {@linkplain #isProtoFile is a Protobuf file} or
+ * a directory containing at least one Protobuf file
+ */
+static boolean isProtoFileOrDir(final File candidate) {
+ final Deque filesToCheck = new LinkedList<>()
+ filesToCheck.push(candidate)
+ if (candidate.isDirectory() && candidate.list().length == 0) {
+ return false
+ }
+ while (!filesToCheck.isEmpty()) {
+ final File file = filesToCheck.pop()
+ if (isProtoFile(file)) {
+ return true
+ }
+ if (file.isDirectory()) {
+ file.listFiles().each { filesToCheck.push(it) }
+ }
+ }
+ return false
+}
+
+/**
+ * Checks if the given file is a {@code .proto} file.
+ *
+ * @param file the file to check
+ * @return {@code true} if the {@code file} is a Protobuf file, {@code false} otherwise
+ */
+static boolean isProtoFile(final File file) {
+ return file.isFile() && file.name.endsWith(".proto")
+}
+
+/**
+ * The filename of a JAR dependency of the project.
+ */
+final class JarFileName {
+
+ final String value
+
+ private JarFileName(final String value) {
+ this.value = value
+ }
+
+ static JarFileName ofFile(final File jar) {
+ return new JarFileName(jar.name)
+ }
+
+ static JarFileName ofValue(final String value) {
+ return new JarFileName(value)
+ }
+
+ boolean equals(final o) {
+ if (this.is(o)) return true
+ if (getClass() != o.class) return false
+
+ final JarFileName that = (JarFileName) o
+
+ if (value != that.value) return false
+
+ return true
+ }
+
+ int hashCode() {
+ return (value != null ? value.hashCode() : 0)
+ }
+}
diff --git a/buildSrc/src/main/groovy/publish.gradle b/buildSrc/src/main/groovy/publish.gradle
new file mode 100644
index 00000000..4d4b59c1
--- /dev/null
+++ b/buildSrc/src/main/groovy/publish.gradle
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+/*
+ Apply this script to add ability to publish the needed artifacts.
+
+ To publish more artifacts for a certain project, add them to the archives configuration:
+ ```
+ artifacts {
+ archives myCustomJarTask
+ }
+ ```
+ */
+
+println("`publish.gradle` script is deprecated. Please use the `Publish` plugin instead.")
+
+task publish {
+ doLast {
+ // Keep the task for dynamic generation while publishing.
+ }
+}
+
+void dependPublish(final project) {
+ final credentialsTasks = getTasksByName("readPublishingCredentials", false)
+ project.getTasksByName("publish", false).each { final task ->
+ task.dependsOn credentialsTasks
+ }
+ publish.dependsOn project.getTasksByName("publish", false)
+}
+
+projectsToPublish.each {
+ project(":$it") { final currentProject ->
+ apply plugin: 'maven-publish'
+
+ logger.debug("Applying `maven-publish` plugin to ${currentProject.name}.")
+
+ currentProject.artifacts {
+ archives sourceJar
+ archives testOutputJar
+ archives javadocJar
+ }
+
+ final propertyName = "spinePrefix"
+ final ext = rootProject.ext
+ final boolean spinePrefix = ext.has(propertyName) ? ext.get(propertyName) : true
+
+ // Artifact IDs are composed as "spine-". Example:
+ //
+ // "spine-mc-java"
+ //
+ // That helps to distinguish resulting JARs in the final assembly, such as WAR package.
+ //
+ final String artifactIdForPublishing =
+ spinePrefix ?
+ "spine-${currentProject.name}" :
+ currentProject.name
+
+ final def publishingAction = {
+ currentProject.publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ groupId = "${currentProject.group}"
+ artifactId = "${artifactIdForPublishing}"
+ version = "${currentProject.version}"
+
+ from components.java
+
+ artifacts = configurations.archives.allArtifacts
+ }
+ }
+ }
+ }
+ if (currentProject.state.executed) {
+ publishingAction()
+ } else {
+ currentProject.afterEvaluate(publishingAction)
+ }
+
+ final boolean isSnapshots = currentProject.version.matches('.+[-\\.]SNAPSHOT([\\+\\.]\\d+)?')
+
+ publishing {
+ repositories {
+ maven {
+ final String publicRepo = (isSnapshots
+ ? publishToRepository.snapshots
+ : publishToRepository.releases
+ )
+
+ // Special treatment for CloudRepo URL.
+ // Reading is performed via public repositories, and publishing via private
+ // ones that differ in the `/public` infix.
+ final urlToPublish = publicRepo.replace("/public", "")
+
+ // Assign URL to the plugin property.
+ url = urlToPublish
+
+ final creds = rootProject.publishToRepository.credentials(project)
+
+ credentials {
+ username = creds.username
+ password = creds.password
+ }
+ }
+ }
+ }
+
+ dependPublish(project)
+ }
+}
diff --git a/buildSrc/src/main/groovy/run-build.gradle b/buildSrc/src/main/groovy/run-build.gradle
new file mode 100644
index 00000000..60d36979
--- /dev/null
+++ b/buildSrc/src/main/groovy/run-build.gradle
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+/**
+ * Apply this script if it is needed to execute {@code ./gradlew clean build} task
+ * from another project.
+ */
+
+configure(project.rootProject) {
+
+ ext {
+
+ /**
+ * Executes {@code ./gradlew clean build} command, if there is a {@code 'clean'} task.
+ * Otherwise, executes {@code ./gradlew build}.
+ *
+ * @param directory the project root directory in which to execute the build
+ */
+ runBuild = { final String directory ->
+ final boolean shouldClean = gradle.getTaskGraph().hasTask(':clean')
+ final def command = []
+ final def runsOnWindows = org.gradle.internal.os.OperatingSystem.current().isWindows()
+ final def script = runsOnWindows ? "gradlew.bat" : "gradlew"
+ command.add("$rootDir/$script".toString())
+ if (shouldClean) {
+ command.add('clean')
+ }
+ command.add('build')
+ command.add('--console=plain')
+ command.add('--debug')
+ command.add('--stacktrace')
+
+ // Ensure build error output log.
+ // Since we're executing this task in another process, we redirect error output to
+ // the file under the `build` directory.
+ final File buildDir = new File(directory, "build")
+ if (!buildDir.exists()) {
+ buildDir.mkdir()
+ }
+ final File errorOut = new File(buildDir, 'error-out.txt')
+ final File debugOut = new File(buildDir, 'debug-out.txt')
+
+ final def process = new ProcessBuilder()
+ .command(command)
+ .directory(file(directory))
+ .redirectError(errorOut)
+ .redirectOutput(debugOut)
+ .start()
+ if (!process.waitFor(10, TimeUnit.MINUTES)) {
+ /* 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
+ */
+ throw new GradleException("Build FAILED. See $errorOut for details.")
+ }
+ }
+ }
+}
+
diff --git a/buildSrc/src/main/groovy/slow-tests.gradle b/buildSrc/src/main/groovy/slow-tests.gradle
new file mode 100644
index 00000000..b45e902b
--- /dev/null
+++ b/buildSrc/src/main/groovy/slow-tests.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+println("`slow-tests.gradle` script is deprecated. " +
+ "Please use `TaskContainer.registerTestTasks()` instead.")
+
+final def slowTag = 'slow' // See io.spine.testing.SlowTest
+
+task fastTest(type: Test) {
+ description = 'Executes all JUnit tests but the ones tagged as "slow".'
+ group = 'Verification'
+
+ useJUnitPlatform {
+ excludeTags slowTag
+ }
+}
+
+task slowTest(type: Test) {
+ description = 'Executes JUnit tests tagged as "slow".'
+ group = 'Verification'
+
+ useJUnitPlatform {
+ includeTags slowTag
+ }
+ shouldRunAfter fastTest
+}
diff --git a/buildSrc/src/main/groovy/test-artifacts.gradle b/buildSrc/src/main/groovy/test-artifacts.gradle
new file mode 100644
index 00000000..a7dd13c9
--- /dev/null
+++ b/buildSrc/src/main/groovy/test-artifacts.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+// Apply this script if it is needed to use test classes of the current project in other projects.
+// The dependency looks like this:
+//
+// testCompile project(path: ":projectWithTests", configuration: 'testArtifacts')
+//
+
+println("`test-artifacts.gradle` script is deprecated. " +
+ "Please use the `Project.exposeTestArtifacts()` utility instead.")
+
+configurations {
+ testArtifacts.extendsFrom testRuntime
+}
+task testJar(type: Jar) {
+ classifier "test"
+ from sourceSets.test.output
+}
+artifacts {
+ testArtifacts testJar
+}
diff --git a/buildSrc/src/main/groovy/test-output.gradle b/buildSrc/src/main/groovy/test-output.gradle
new file mode 100644
index 00000000..3816b332
--- /dev/null
+++ b/buildSrc/src/main/groovy/test-output.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021, 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 plugin configured the test output as follows:
+ *
+ * - the standard streams of the tests execution are logged;
+ * - exceptions thrown in tests are logged;
+ * - after all the tests are executed, a short test summary is logged; the summary shown the number
+ * of tests and their results.
+ */
+
+println("`test-output.gradle` script is deprecated. Please use `Test.configureLogging()` instead.")
+
+tasks.withType(Test).each {
+ it.testLogging {
+ showStandardStreams = true
+ showExceptions = true
+ showStackTraces = true
+ showCauses = true
+ exceptionFormat = 'full'
+ }
+
+ it.afterSuite { final testDescriptor, final result ->
+ // If the descriptor has no parent, then it is the root test suite, i.e. it includes the
+ // info about all the run tests.
+ if (!testDescriptor.parent) {
+ logger.lifecycle(
+ """
+ Test summary:
+ >> ${result.testCount} tests
+ >> ${result.successfulTestCount} succeeded
+ >> ${result.failedTestCount} failed
+ >> ${result.skippedTestCount} skipped
+ """
+ )
+ }
+ }
+}
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 00000000..d82c3b4f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/dokka-for-java.gradle.kts
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import io.spine.internal.dependency.Dokka
+import java.time.LocalDate
+import org.jetbrains.dokka.base.DokkaBase
+import org.jetbrains.dokka.base.DokkaBaseConfiguration
+import org.jetbrains.dokka.gradle.DokkaTask
+
+plugins {
+ id("org.jetbrains.dokka")
+}
+
+dependencies {
+ /**
+ * To generate the documentation as seen from Java perspective, the kotlin-as-java plugin was
+ * added to the Dokka's classpath.
+ *
+ * @see
+ * Dokka output formats
+ */
+ 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
+ */
+ dokkaPlugin(Dokka.SpineExtensions.lib)
+}
+
+tasks.withType().configureEach {
+ dokkaSourceSets.configureEach {
+ skipEmptyPackages.set(true)
+ }
+
+ outputDirectory.set(buildDir.resolve("docs/dokka"))
+
+ val dokkaConfDir = rootDir.resolve("buildSrc/src/main/resources/dokka")
+
+ /**
+ * Dokka Base plugin allows to set a few properties to customize the output:
+ *
+ * - `customStyleSheets` property to which we can pass our css files overriding styles generated
+ * by Dokka;
+ * - `customAssets` property to provide resources. We need to provide an image with the name
+ * "logo-icon.svg" to overwrite the default one 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(file("${dokkaConfDir.resolve("styles/custom-styles.css")}"))
+ customAssets = listOf(file("${dokkaConfDir.resolve("assets/logo-icon.svg")}"))
+ separateInheritedMembers = true
+ footerMessage = "Copyright ${LocalDate.now().year}, TeamDev"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/force-jacoco.gradle.kts b/buildSrc/src/main/kotlin/force-jacoco.gradle.kts
new file mode 100644
index 00000000..310f4243
--- /dev/null
+++ b/buildSrc/src/main/kotlin/force-jacoco.gradle.kts
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// TODO:2021-07-05:dmytro.dashenkov: https://github.com/SpineEventEngine/config/issues/214.
+
+allprojects {
+ configurations.all {
+ resolutionStrategy {
+ eachDependency {
+ if (requested.group == "org.jacoco") {
+ useVersion("0.8.7")
+ }
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/internal/RunBuild.kt b/buildSrc/src/main/kotlin/io/spine/gradle/internal/RunBuild.kt
deleted file mode 100644
index ac8b1893..00000000
--- a/buildSrc/src/main/kotlin/io/spine/gradle/internal/RunBuild.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2021, 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.gradle.internal
-
-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
-import java.io.File
-
-/**
- * A Gradle task which runs another Gradle build.
- *
- * Launches Gradle wrapper under a given [directory] with the `build` task. 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`.
- */
-open class RunBuild : DefaultTask() {
-
- /**
- * Path to the directory which contains a Gradle wrapper script.
- */
- @Internal
- lateinit var directory: String
-
- @TaskAction
- private fun execute() {
- val runsOnWindows = OperatingSystem.current().isWindows()
- val script = if (runsOnWindows) "gradlew.bat" else "gradlew"
- val command = buildCommand(script)
-
- // Ensure build error output log.
- // Since we're executing this task in another process, we redirect error output to
- // the file under the `build` directory.
- val buildDir = File(directory, "build")
- if (!buildDir.exists()) {
- buildDir.mkdir()
- }
- val errorOut = File(buildDir, "error-out.txt")
- val debugOut = File(buildDir, "debug-out.txt")
-
- val process = buildProcess(command, errorOut, debugOut)
- if (process.waitFor() != 0) {
- throw GradleException("Build FAILED. See $errorOut for details.")
- }
- }
-
- private fun buildCommand(script: String): List {
- val command = mutableListOf()
- command.add("${project.rootDir}/$script")
- val shouldClean = project.gradle
- .taskGraph
- .hasTask(":clean")
- if (shouldClean) {
- command.add("clean")
- }
- command.add("build")
- command.add("--console=plain")
- command.add("--debug")
- command.add("--stacktrace")
- return command
- }
-
- private fun buildProcess(command: List, errorOut: File, debugOut: File) =
- ProcessBuilder()
- .command(command)
- .directory(project.file(directory))
- .redirectError(errorOut)
- .redirectOutput(debugOut)
- .start()
-}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt b/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt
deleted file mode 100644
index 0ae926bf..00000000
--- a/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Copyright 2021, 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.gradle.internal
-
-import org.gradle.api.Project
-import org.gradle.api.artifacts.ConfigurationContainer
-import org.gradle.api.artifacts.dsl.RepositoryHandler
-import java.net.URI
-
-/*
- * This file describes shared dependencies of Spine sub-projects.
- *
- * Inspired by dependency management of the Uber's NullAway project:
- * https://github.com/uber/NullAway/blob/master/gradle/dependencies.gradle
- */
-
-data class Repository(
- val releases: String,
- val snapshots: String,
- val credentials: String
-)
-
-/**
- * Repositories to which we may publish. Normally, only one repository will be used.
- *
- * See `publish.gradle` for details of the publishing process.
- */
-object PublishingRepos {
- val mavenTeamDev = Repository(
- releases = "http://maven.teamdev.com/repository/spine",
- snapshots = "http://maven.teamdev.com/repository/spine-snapshots",
- credentials = "credentials.properties"
- )
- val cloudRepo = Repository(
- releases = "https://spine.mycloudrepo.io/public/repositories/releases",
- snapshots = "https://spine.mycloudrepo.io/public/repositories/snapshots",
- credentials = "cloudrepo.properties"
- )
-}
-
-// Specific repositories.
-object Repos {
- val oldSpine: String = PublishingRepos.mavenTeamDev.releases
- val oldSpineSnapshots: String = PublishingRepos.mavenTeamDev.snapshots
-
- val spine: String = PublishingRepos.cloudRepo.releases
- val spineSnapshots: String = PublishingRepos.cloudRepo.snapshots
-
- val sonatypeSnapshots: String = "https://oss.sonatype.org/content/repositories/snapshots"
- val gradlePlugins = "https://plugins.gradle.org/m2/"
-}
-
-object Versions {
- val checkerFramework = "3.7.1"
- val errorProne = "2.4.0"
- val errorProneJavac = "9+181-r4173-1" // taken from here: https://github.com/tbroyer/gradle-errorprone-plugin/blob/v0.8/build.gradle.kts
- val errorPronePlugin = "1.3.0"
- val pmd = "6.24.0"
- val checkstyle = "8.29"
- val protobufPlugin = "0.8.13"
- val appengineApi = "1.9.82"
- val appenginePlugin = "2.2.0"
- val findBugs = "3.0.2"
- val guava = "30.0-jre"
- val protobuf = "3.13.0"
- val grpc = "1.28.1"
- val flogger = "0.5.1"
- val junit4 = "4.13.1"
- val junit5 = "5.7.0"
- val junitPlatform = "1.7.0"
- val junitPioneer = "1.0.0"
- val truth = "1.1"
- val httpClient = "1.34.2"
- val apacheHttpClient = "2.1.2"
- val firebaseAdmin = "6.12.2"
- val roaster = "2.21.2.Final"
- val licensePlugin = "1.13"
- val javaPoet = "1.13.0"
- val autoService = "1.0-rc7"
- val autoCommon = "0.10"
- val jackson = "2.9.10.5"
- val animalSniffer = "1.19"
- val apiguardian = "1.1.0"
- val javaxAnnotation = "1.3.2"
- val klaxon = "5.4"
- val ouathJwt = "3.11.0"
- val bouncyCastlePkcs = "1.66"
- val assertK = "0.23"
-
- /**
- * Version of the SLF4J library.
- *
- * Spine used to log with SLF4J. Now we use Flogger. Whenever a choice comes up, we recommend to
- * use the latter.
- *
- * Some third-party libraries may clash with different versions of the library. Thus, we specify
- * this version and force it via [forceConfiguration(..)][DependencyResolution.forceConfiguration].
- */
- @Deprecated("Use Flogger over SLF4J.", replaceWith = ReplaceWith("flogger"))
- val slf4j = "1.7.30"
-}
-
-object GradlePlugins {
- val errorProne = "net.ltgt.gradle:gradle-errorprone-plugin:${Versions.errorPronePlugin}"
- val protobuf = "com.google.protobuf:protobuf-gradle-plugin:${Versions.protobufPlugin}"
- val appengine = "com.google.cloud.tools:appengine-gradle-plugin:${Versions.appenginePlugin}"
- val licenseReport = "com.github.jk1:gradle-license-report:${Versions.licensePlugin}"
-}
-
-object Build {
- val errorProneJavac = "com.google.errorprone:javac:${Versions.errorProneJavac}"
- val errorProneAnnotations = listOf(
- "com.google.errorprone:error_prone_annotations:${Versions.errorProne}",
- "com.google.errorprone:error_prone_type_annotations:${Versions.errorProne}"
- )
- val errorProneCheckApi = "com.google.errorprone:error_prone_check_api:${Versions.errorProne}"
- val errorProneCore = "com.google.errorprone:error_prone_core:${Versions.errorProne}"
- val errorProneTestHelpers = "com.google.errorprone:error_prone_test_helpers:${Versions.errorProne}"
- val checkerAnnotations = "org.checkerframework:checker-qual:${Versions.checkerFramework}"
- val checkerDataflow = listOf(
- "org.checkerframework:dataflow:${Versions.checkerFramework}",
- "org.checkerframework:javacutil:${Versions.checkerFramework}"
- )
- val autoCommon = "com.google.auto:auto-common:${Versions.autoCommon}"
- val autoService = AutoService
- val jsr305Annotations = "com.google.code.findbugs:jsr305:${Versions.findBugs}"
- val guava = "com.google.guava:guava:${Versions.guava}"
- val flogger = "com.google.flogger:flogger:${Versions.flogger}"
- val protobuf = listOf(
- "com.google.protobuf:protobuf-java:${Versions.protobuf}",
- "com.google.protobuf:protobuf-java-util:${Versions.protobuf}"
- )
- val protoc = "com.google.protobuf:protoc:${Versions.protobuf}"
- val googleHttpClient = "com.google.http-client:google-http-client:${Versions.httpClient}"
- val googleHttpClientApache = "com.google.http-client:google-http-client-apache:${Versions.apacheHttpClient}"
- val appengineApi = "com.google.appengine:appengine-api-1.0-sdk:${Versions.appengineApi}"
- val firebaseAdmin = "com.google.firebase:firebase-admin:${Versions.firebaseAdmin}"
- val jacksonDatabind = "com.fasterxml.jackson.core:jackson-databind:${Versions.jackson}"
- val roasterApi = "org.jboss.forge.roaster:roaster-api:${Versions.roaster}"
- val roasterJdt = "org.jboss.forge.roaster:roaster-jdt:${Versions.roaster}"
- val animalSniffer = "org.codehaus.mojo:animal-sniffer-annotations:${Versions.animalSniffer}"
- val ci = "true".equals(System.getenv("CI"))
- val gradlePlugins = GradlePlugins
- @Deprecated("Use Flogger over SLF4J.", replaceWith = ReplaceWith("flogger"))
- @Suppress("DEPRECATION") // Version of SLF4J.
- val slf4j = "org.slf4j:slf4j-api:${Versions.slf4j}"
-
- object AutoService {
- val annotations = "com.google.auto.service:auto-service-annotations:${Versions.autoService}"
- val processor = "com.google.auto.service:auto-service:${Versions.autoService}"
- }
-}
-
-object Gen {
- val javaPoet = "com.squareup:javapoet:${Versions.javaPoet}"
- val javaxAnnotation = "javax.annotation:javax.annotation-api:${Versions.javaxAnnotation}"
-}
-
-object Publishing {
- val klaxon = "com.beust:klaxon:${Versions.klaxon}"
- val oauthJwt = "com.auth0:java-jwt:${Versions.ouathJwt}"
- val bouncyCastlePkcs = "org.bouncycastle:bcpkix-jdk15on:${Versions.bouncyCastlePkcs}"
- val assertK = "com.willowtreeapps.assertk:assertk-jvm:${Versions.assertK}"
-}
-
-object Grpc {
- val core = "io.grpc:grpc-core:${Versions.grpc}"
- val stub = "io.grpc:grpc-stub:${Versions.grpc}"
- val okHttp = "io.grpc:grpc-okhttp:${Versions.grpc}"
- val protobuf = "io.grpc:grpc-protobuf:${Versions.grpc}"
- val netty = "io.grpc:grpc-netty:${Versions.grpc}"
- val nettyShaded = "io.grpc:grpc-netty-shaded:${Versions.grpc}"
- val context = "io.grpc:grpc-context:${Versions.grpc}"
-
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("core"))
- val grpcCore = core
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("stub"))
- val grpcStub = stub
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("okHttp"))
- val grpcOkHttp = okHttp
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("protobuf"))
- val grpcProtobuf = protobuf
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("netty"))
- val grpcNetty = netty
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("nettyShaded"))
- val grpcNettyShaded = nettyShaded
- @Deprecated("Use the shorter form.", replaceWith = ReplaceWith("context"))
- val grpcContext = context
-}
-
-object Runtime {
-
- val flogger = Flogger
-
- object Flogger {
- val systemBackend = "com.google.flogger:flogger-system-backend:${Versions.flogger}"
- val log4J = "com.google.flogger:flogger-log4j:${Versions.flogger}"
- val slf4J = "com.google.flogger:slf4j-backend-factory:${Versions.flogger}"
- }
-
- @Deprecated("Use the `flogger` object.", replaceWith = ReplaceWith("flogger.systemBackend"))
- val floggerSystemBackend = flogger.systemBackend
- @Deprecated("Use the `flogger` object.", replaceWith = ReplaceWith("flogger.log4J"))
- val floggerLog4J = flogger.log4J
- @Deprecated("Use the `flogger` object.", replaceWith = ReplaceWith("flogger.slf4J"))
- val floggerSlf4J = flogger.slf4J
-}
-
-object Test {
- val junit4 = "junit:junit:${Versions.junit4}"
- val junit5Api = listOf(
- "org.junit.jupiter:junit-jupiter-api:${Versions.junit5}",
- "org.junit.jupiter:junit-jupiter-params:${Versions.junit5}",
- "org.apiguardian:apiguardian-api:${Versions.apiguardian}"
- )
- val junit5Runner = "org.junit.jupiter:junit-jupiter-engine:${Versions.junit5}"
- val junitPioneer = "org.junit-pioneer:junit-pioneer:${Versions.junitPioneer}"
- val guavaTestlib = "com.google.guava:guava-testlib:${Versions.guava}"
- val mockito = "org.mockito:mockito-core:2.12.0"
- val hamcrest = "org.hamcrest:hamcrest-all:1.3"
- val truth = listOf(
- "com.google.truth:truth:${Versions.truth}",
- "com.google.truth.extensions:truth-java8-extension:${Versions.truth}",
- "com.google.truth.extensions:truth-proto-extension:${Versions.truth}"
- )
- @Deprecated("Use Flogger over SLF4J.",
- replaceWith = ReplaceWith("Deps.runtime.floggerSystemBackend"))
- @Suppress("DEPRECATION") // Version of SLF4J.
- val slf4j = "org.slf4j:slf4j-jdk14:${Versions.slf4j}"
-}
-
-object Scripts {
-
- private const val COMMON_PATH = "/config/gradle/"
-
- fun testArtifacts(p: Project) = p.script("test-artifacts.gradle")
- fun testOutput(p: Project) = p.script("test-output.gradle")
- fun slowTests(p: Project) = p.script("slow-tests.gradle")
- fun javadocOptions(p: Project) = p.script("javadoc-options.gradle")
- fun filterInternalJavadocs(p: Project) = p.script("filter-internal-javadoc.gradle")
- fun jacoco(p: Project) = p.script("jacoco.gradle")
- fun publish(p: Project) = p.script("publish.gradle")
- fun publishProto(p: Project) = p.script("publish-proto.gradle")
- fun javacArgs(p: Project) = p.script("javac-args.gradle")
- fun jsBuildTasks(p: Project) = p.script("js/build-tasks.gradle")
- fun jsConfigureProto(p: Project) = p.script("js/configure-proto.gradle")
- fun npmPublishTasks(p: Project) = p.script("js/npm-publish-tasks.gradle")
- fun npmCli(p: Project) = p.script("js/npm-cli.gradle")
- fun updatePackageVersion(p: Project) = p.script("js/update-package-version.gradle")
- fun dartBuildTasks(p: Project) = p.script("dart/build-tasks.gradle")
- fun pubPublishTasks(p: Project) = p.script("dart/pub-publish-tasks.gradle")
- fun pmd(p: Project) = p.script("pmd.gradle")
- fun checkstyle(p: Project) = p.script("checkstyle.gradle")
- fun runBuild(p: Project) = p.script("run-build.gradle")
- fun modelCompiler(p: Project) = p.script("model-compiler.gradle")
- fun licenseReportCommon(p: Project) = p.script("license-report-common.gradle")
- fun projectLicenseReport(p: Project) = p.script("license-report-project.gradle")
- fun repoLicenseReport(p: Project) = p.script("license-report-repo.gradle")
- fun generatePom(p: Project) = p.script("generate-pom.gradle")
- fun updateGitHubPages(p: Project) = p.script("update-gh-pages.gradle")
-
- private fun Project.script(name: String) = "${rootDir}$COMMON_PATH$name"
-}
-
-object Deps {
- val build = Build
- val grpc = Grpc
- val gen = Gen
- val runtime = Runtime
- val test = Test
- val versions = Versions
- val scripts = Scripts
- val publishing = Publishing
-}
-
-object DependencyResolution {
-
- fun forceConfiguration(configurations: ConfigurationContainer) {
- configurations.all {
- resolutionStrategy {
- failOnVersionConflict()
- cacheChangingModulesFor(0, "seconds")
- @Suppress("DEPRECATION") // Force SLF4J version.
- force(
- Deps.build.slf4j,
- Deps.build.errorProneAnnotations,
- Deps.build.jsr305Annotations,
- Deps.build.checkerAnnotations,
- Deps.build.autoCommon,
- Deps.build.guava,
- Deps.build.animalSniffer,
- Deps.build.protobuf,
- Deps.test.guavaTestlib,
- Deps.test.truth,
- Deps.test.junit5Api,
- Deps.test.junit4,
-
- // Transitive dependencies of 3rd party components that we don't use directly.
- "org.junit.platform:junit-platform-commons:${Versions.junitPlatform}",
- "com.google.auto.value:auto-value-annotations:1.7.4",
- "com.google.auto.service:auto-service-annotations:1.0-rc7",
- "com.google.code.gson:gson:2.8.6",
- "com.google.j2objc:j2objc-annotations:1.3",
- "org.codehaus.plexus:plexus-utils:3.3.0",
- "com.squareup.okio:okio:1.17.5", // Last version before next major.
- "commons-cli:commons-cli:1.4",
-
- // Force discontinued transitive dependency until everybody migrates off it.
- "org.checkerframework:checker-compat-qual:2.5.5",
-
- "commons-logging:commons-logging:1.2",
-
- // Force the Gradle Protobuf plugin version.
- Deps.build.gradlePlugins.protobuf
- )
- }
- }
- }
-
- fun excludeProtobufLite(configurations: ConfigurationContainer) {
- excludeProtoLite(configurations, "runtime")
- excludeProtoLite(configurations, "testRuntime")
- }
-
- private fun excludeProtoLite(
- configurations: ConfigurationContainer,
- configurationName: String
- ) {
- configurations
- .named(configurationName).get()
- .exclude(mapOf("group" to "com.google.protobuf", "module" to "protobuf-lite"))
- }
-
- fun defaultRepositories(repositories: RepositoryHandler) {
- repositories.mavenLocal()
- repositories.maven {
- url = URI(Repos.spine)
- content {
- includeGroup("io.spine")
- includeGroup("io.spine.tools")
- includeGroup("io.spine.gcloud")
- }
- }
- repositories.jcenter()
- repositories.maven {
- url = URI(Repos.gradlePlugins)
- }
- }
-}
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 00000000..068ae59c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AnimalSniffer.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://www.mojohaus.org/animal-sniffer/animal-sniffer-maven-plugin/
+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 00000000..dd518a0c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ApacheHttp.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused")
+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 00000000..0d7fee39
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AppEngine.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://cloud.google.com/java/docs/reference
+@Suppress("unused")
+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/AssertK.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt
new file mode 100644
index 00000000..8d42b619
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AssertK.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Assertion library for tests in Kotlin
+ *
+ * [AssertK](https://github.com/willowtreeapps/assertk)
+ */
+@Suppress("unused")
+object AssertK {
+ private const val version = "0.25"
+ const val libJvm = "com.willowtreeapps.assertk:assertk-jvm:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoCommon.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoCommon.kt
new file mode 100644
index 00000000..4053ef58
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoCommon.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/auto
+object AutoCommon {
+ private const val version = "1.2.1"
+ const val lib = "com.google.auto:auto-common:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoService.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoService.kt
new file mode 100644
index 00000000..80b79975
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoService.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/auto
+object AutoService {
+ private const val version = "1.0.1"
+ const val annotations = "com.google.auto.service:auto-service-annotations:${version}"
+ @Suppress("unused")
+ const val processor = "com.google.auto.service:auto-service:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoValue.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoValue.kt
new file mode 100644
index 00000000..5b630bf1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/AutoValue.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/auto
+object AutoValue {
+ private const val version = "1.9"
+ 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 00000000..c0567edd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/BouncyCastle.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://www.bouncycastle.org/java.html
+@Suppress("unused")
+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 00000000..1e3effba
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckStyle.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://checkstyle.sourceforge.io/
+// See `io.spine.internal.gradle.checkstyle.CheckStyleConfig`.
+@Suppress("unused")
+object CheckStyle {
+ const val version = "10.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 00000000..75cc67f2
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CheckerFramework.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://checkerframework.org/
+object CheckerFramework {
+ private const val version = "3.21.3"
+ const val annotations = "org.checkerframework:checker-qual:${version}"
+ @Suppress("unused")
+ val dataflow = listOf(
+ "org.checkerframework:dataflow:${version}",
+ "org.checkerframework:javacutil:${version}"
+ )
+ /**
+ * This is discontinued artifact, which we do not use directly.
+ * This is a transitive dependency for us, which we force in
+ * [DependencyResolution.forceConfiguration]
+ */
+ const val compatQual = "org.checkerframework:checker-compat-qual:2.5.5"
+}
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 00000000..79a4036c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCli.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Commons CLI is a transitive dependency which we don't use directly.
+ * We `force` it in [DependencyResolution.forceConfiguration].
+ *
+ * [Commons CLI](https://commons.apache.org/proper/commons-cli/)
+ */
+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 00000000..97dbed87
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsCodec.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://commons.apache.org/proper/commons-codec/changes-report.html
+@Suppress("unused")
+object CommonsCodec {
+ private const val version = "1.15"
+ 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 00000000..e9d148f1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/CommonsLogging.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.
+ */
+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/Dokka.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt
new file mode 100644
index 00000000..9cdbf4d0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Dokka.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/Kotlin/dokka
+@Suppress("unused")
+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.6.20"
+
+ 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 in 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}"
+ }
+
+ /**
+ * To generate the documentation as seen from Java perspective 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.2"
+ 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 00000000..e62b4b41
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/ErrorProne.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://errorprone.info/
+@Suppress("unused")
+object ErrorProne {
+ // https://github.com/google/error-prone
+ private const val version = "2.13.1"
+ // 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 in 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 = "2.0.2"
+ 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 00000000..52b05f75
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/FindBugs.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * The FindBugs project is 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.
+ */
+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 00000000..5e471caa
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Firebase.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://firebase.google.com/docs/admin/setup#java
+@Suppress("unused")
+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 00000000..4332178f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Flogger.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/flogger
+object Flogger {
+ internal const val version = "0.7.4"
+ const val lib = "com.google.flogger:flogger:${version}"
+ @Suppress("unused")
+ object Runtime {
+ const val systemBackend = "com.google.flogger:flogger-system-backend:${version}"
+ const val log4J = "com.google.flogger:flogger-log4j:${version}"
+ const val slf4J = "com.google.flogger:slf4j-backend-factory:${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 00000000..af663dc1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleApis.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Provides dependencies on [GoogleApis projects](https://github.com/googleapis/).
+ */
+@Suppress("unused")
+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 00000000..a764af27
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/GoogleCloud.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused")
+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/Grpc.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt
new file mode 100644
index 00000000..c9d55667
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Grpc.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/grpc/grpc-java
+@Suppress("unused")
+object Grpc {
+ @Suppress("MemberVisibilityCanBePrivate")
+ const val version = "1.45.1"
+ 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 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 protobufPlugin = "io.grpc:protoc-gen-grpc-java:${version}"
+ const val netty = "io.grpc:grpc-netty:${version}"
+ const val nettyShaded = "io.grpc:grpc-netty-shaded:${version}"
+}
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 00000000..e161ee1e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Gson.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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)
+ */
+object Gson {
+ private const val version = "2.9.0"
+ 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 00000000..faaf3b8b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Guava.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.
+ */
+// https://github.com/google/guava
+object Guava {
+ private const val version = "31.1-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/HttpClient.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/HttpClient.kt
new file mode 100644
index 00000000..849f1f4a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/HttpClient.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Google implementations of [HTTP client](https://github.com/googleapis/google-http-java-client).
+ */
+@Suppress("unused")
+object HttpClient {
+ // https://github.com/googleapis/google-http-java-client
+ const val version = "1.41.5"
+ 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/J2ObjC.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/J2ObjC.kt
new file mode 100644
index 00000000..9ed18f26
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/J2ObjC.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.
+ */
+object J2ObjC {
+ // https://github.com/google/j2objc/releases
+ // `1.3.` is the latest version available from Maven Central.
+ // https://search.maven.org/artifact/com.google.j2objc/j2objc-annotations
+ private const val version = "1.3"
+ 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 00000000..0c436ce4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JUnit.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://junit.org/junit5/
+@Suppress("unused")
+object JUnit {
+ const val version = "5.8.2"
+ private const val platformVersion = "1.8.2"
+ 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 = "1.5.0"
+
+ 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 pioneer = "org.junit-pioneer:junit-pioneer:${pioneerVersion}"
+ const val platformCommons = "org.junit.platform:junit-platform-commons:${platformVersion}"
+ const val platformLauncher = "org.junit.platform:junit-platform-launcher:${platformVersion}"
+ const val params = "org.junit.jupiter:junit-jupiter-params:${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 00000000..71f7e08c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Jackson.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused")
+object Jackson {
+ private const val version = "2.13.2"
+ private const val databindVersion = "2.13.2.2"
+ // https://github.com/FasterXML/jackson-core
+ const val core = "com.fasterxml.jackson.core:jackson-core:${version}"
+ // https://github.com/FasterXML/jackson-databind
+ const val databind = "com.fasterxml.jackson.core:jackson-databind:${databindVersion}"
+ // https://github.com/FasterXML/jackson-dataformat-xml/releases
+ const val dataformatXml = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${version}"
+ // https://github.com/FasterXML/jackson-dataformats-text/releases
+ const val dataformatYaml = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${version}"
+ // https://github.com/FasterXML/jackson-module-kotlin/releases
+ const val moduleKotlin = "com.fasterxml.jackson.module:jackson-module-kotlin:${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 00000000..424c45fb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaJwt.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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")
+object JavaJwt {
+ private const val version = "3.19.1"
+ 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 00000000..e9bdb9b3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaPoet.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/square/javapoet
+@Suppress("unused")
+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 00000000..9fb7bd59
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/JavaX.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused")
+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 00000000..160fdbfb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Klaxon.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * A JSON parser in Kotlin
+ *
+ * [Klaxon](https://github.com/cbeust/klaxon)
+ */
+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/Kotlin.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt
new file mode 100644
index 00000000..0de6d842
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Kotlin.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/JetBrains/kotlin
+// https://github.com/Kotlin
+object Kotlin {
+ /**
+ * When changing the version, also change the version used in the `buildSrc/build.gradle.kts`.
+ */
+ @Suppress("MemberVisibilityCanBePrivate") // used directly from outside
+ const val version = "1.6.20"
+ const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${version}"
+ const val stdLib = "org.jetbrains.kotlin:kotlin-stdlib:${version}"
+ const val stdLibCommon = "org.jetbrains.kotlin:kotlin-stdlib-common:${version}"
+ const val stdLibJdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${version}"
+}
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 00000000..47fd7de4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/KotlinSemver.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/z4kn4fein/kotlin-semver
+@Suppress("unused")
+object KotlinSemver {
+ private const val version = "1.2.1"
+ const val lib = "io.github.z4kn4fein:semver:$version"
+}
diff --git a/buildSrc/src/main/kotlin/bootstrap-plugin.gradle.kts b/buildSrc/src/main/kotlin/io/spine/internal/dependency/LicenseReport.kt
similarity index 74%
rename from buildSrc/src/main/kotlin/bootstrap-plugin.gradle.kts
rename to buildSrc/src/main/kotlin/io/spine/internal/dependency/LicenseReport.kt
index 8431f593..8c2d5dbb 100644
--- a/buildSrc/src/main/kotlin/bootstrap-plugin.gradle.kts
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/LicenseReport.kt
@@ -24,18 +24,17 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-plugins {
- java
- `java-gradle-plugin`
-}
+package io.spine.internal.dependency
+
+// https://github.com/jk1/Gradle-License-Report
+@Suppress("unused")
+object LicenseReport {
+ private const val version = "1.16"
+ const val lib = "com.github.jk1:gradle-license-report:${version}"
-gradlePlugin {
- plugins {
- create("spineBootstrapPlugin") {
- id = "io.spine.tools.gradle.bootstrap"
- implementationClass = "io.spine.tools.gradle.bootstrap.BootstrapPlugin"
- displayName = "Spine Bootstrap"
- description = "Prepares a Gradle project for development on Spine."
- }
+ object GradlePlugin {
+ const val version = LicenseReport.version
+ const val id = "com.github.jk1.dependency-license-report"
+ const val lib = LicenseReport.lib
}
}
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 00000000..dfcd80ef
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Netty.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+@Suppress("unused")
+object Netty {
+ // https://github.com/netty/netty/releases
+ private const val version = "4.1.72.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 00000000..4ad91c6f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Okio.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Okio is a transitive dependency which we don't use directly.
+ * We `force` it in [DependencyResolution.forceConfiguration].
+ */
+object Okio {
+ // This is the last version before next major.
+ private const val version = "1.17.5"
+ const val lib = "com.squareup.okio:okio:${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 00000000..7f4bb16b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/OsDetector.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+object OsDetector {
+ // https://github.com/google/osdetector-gradle-plugin
+ const val version = "1.7.0"
+ 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 00000000..27d27206
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Plexus.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Plexus Utils is a transitive dependency which we don't use directly.
+ * We `force` it in [DependencyResolution.forceConfiguration].
+ *
+ * [Plexus Utils](https://codehaus-plexus.github.io/plexus-utils/)
+ */
+object Plexus {
+ private const val version = "3.4.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 00000000..badd7717
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Pmd.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://pmd.github.io/
+@Suppress("unused") // Will be used when `config/gradle/pmd.gradle` migrates to Kotlin.
+object Pmd {
+ const val version = "6.44.0"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Protobuf.kt
new file mode 100644
index 00000000..5987a368
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Protobuf.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/protocolbuffers/protobuf
+@Suppress("MemberVisibilityCanBePrivate") // used directly from outside
+object Protobuf {
+ private const val group = "com.google.protobuf"
+ const val version = "3.20.0"
+ val libs = listOf(
+ "${group}:protobuf-java:${version}",
+ "${group}:protobuf-java-util:${version}",
+ "${group}:protobuf-kotlin:${version}"
+ )
+ const val compiler = "${group}:protoc:${version}"
+
+ // https://github.com/google/protobuf-gradle-plugin/releases
+ object GradlePlugin {
+ /**
+ * The version of this plugin is already specified in `buildSrc/build.gradle.kts` file.
+ * Thus, when applying the plugin in projects build files, only the [id] should be used.
+ *
+ * When changing the version, also change the version used in the `build.gradle.kts`.
+ */
+ const val version = "0.8.18"
+ const val id = "com.google.protobuf"
+ const val lib = "${group}:protobuf-gradle-plugin:${version}"
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Roaster.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Roaster.kt
new file mode 100644
index 00000000..2d237060
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Roaster.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/forge/roaster
+@Suppress("unused")
+object Roaster {
+
+ /**
+ * Do not advance this version further because it would break compatibility with Java 8
+ * projects. Starting from the following version Roaster has a shaded version of Eclipse JFace
+ * built with Java 11.
+ *
+ * Please see [this issue][https://github.com/SpineEventEngine/config/issues/220] for details.
+ */
+ private const val version = "2.24.0.Final"
+
+ const val api = "org.jboss.forge.roaster:roaster-api:${version}"
+ const val jdt = "org.jboss.forge.roaster:roaster-jdt:${version}"
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/dependency/Slf4J.kt b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Slf4J.kt
new file mode 100644
index 00000000..4cfa0494
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Slf4J.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Spine used to log with SLF4J. Now we use Flogger. Whenever a choice comes up, we recommend to
+ * use the latter.
+ *
+ * Some third-party libraries may clash with different versions of the library. Thus, we specify
+ * this version and force it via [forceConfiguration(..)][DependencyResolution.forceConfiguration].
+ */
+@Deprecated("Use Flogger over SLF4J.", replaceWith = ReplaceWith("flogger"))
+object Slf4J {
+ private const val version = "1.7.30"
+ const val lib = "org.slf4j:slf4j-api:${version}"
+ const val jdk14 = "org.slf4j:slf4j-jdk14:${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 00000000..8d572d9b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/TestKitTruth.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+/**
+ * Gradle TestKit extension for Google Truth.
+ *
+ * Source code:
+ * https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/tree/main/testkit-truth
+ *
+ * Usage description:
+ * https://dev.to/autonomousapps/gradle-all-the-way-down-testing-your-gradle-plugin-with-gradle-testkit-2hmc
+ */
+@Suppress("unused")
+object TestKitTruth {
+ private const val version = "1.1"
+ 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 00000000..48c1f2df
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/dependency/Truth.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.dependency
+
+// https://github.com/google/truth
+object Truth {
+ private const val version = "1.1.3"
+ 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/gradle/Build.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt
new file mode 100644
index 00000000..c42a3c08
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Build.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..ad5c1c59
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Clean.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..8a3db510
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/ConfigTester.kt
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+@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")
+) {
+ 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 = 30
+ }
+ }
+
+ 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 {
+ Files.walk(sourceFolder).forEach { file: Path ->
+ if (ignoredFolder != null) {
+ if (file.toAbsolutePath().startsWith(ignoredFolder.toAbsolutePath())) {
+ return@forEach
+ }
+ }
+ try {
+ val destination = destinationFolder.resolve(sourceFolder.relativize(file))
+ if (Files.isDirectory(file)) {
+ if (!Files.exists(destination)) Files.createDirectory(destination)
+ return@forEach
+ }
+ Files.copy(file, destination)
+ } catch (e: Exception) {
+ throw IllegalStateException(
+ "Error copying folder `$sourceFolder` to `$destinationFolder`.", e
+ )
+ }
+ }
+ } catch (e: Exception) {
+ throw IllegalStateException(
+ "Error copying folder `$sourceFolder` to `$destinationFolder`.", e
+ )
+ }
+ }
+}
+
+/**
+ * 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/DependencyResolution.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/DependencyResolution.kt
new file mode 100644
index 00000000..168c0798
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/DependencyResolution.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle
+
+import io.spine.internal.dependency.AnimalSniffer
+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.CommonsLogging
+import io.spine.internal.dependency.ErrorProne
+import io.spine.internal.dependency.FindBugs
+import io.spine.internal.dependency.Flogger
+import io.spine.internal.dependency.Gson
+import io.spine.internal.dependency.Guava
+import io.spine.internal.dependency.J2ObjC
+import io.spine.internal.dependency.JUnit
+import io.spine.internal.dependency.Kotlin
+import io.spine.internal.dependency.Okio
+import io.spine.internal.dependency.Plexus
+import io.spine.internal.dependency.Protobuf
+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
+import org.gradle.api.artifacts.dsl.RepositoryHandler
+
+/**
+ * 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 SLF4J version.
+ force(
+ AnimalSniffer.lib,
+ AutoCommon.lib,
+ AutoService.annotations,
+ CheckerFramework.annotations,
+ ErrorProne.annotations,
+ ErrorProne.core,
+ Guava.lib,
+ FindBugs.annotations,
+ Flogger.lib,
+ Flogger.Runtime.systemBackend,
+ Kotlin.reflect,
+ Kotlin.stdLib,
+ Kotlin.stdLibCommon,
+ Kotlin.stdLibJdk8,
+ Protobuf.libs,
+ Protobuf.GradlePlugin.lib,
+ io.spine.internal.dependency.Slf4J.lib
+ )
+}
+
+private fun ResolutionStrategy.forceTestDependencies() {
+ force(
+ Guava.testLib,
+ JUnit.api,
+ JUnit.platformCommons,
+ JUnit.platformLauncher,
+ JUnit.legacy,
+ Truth.libs
+ )
+}
+
+/**
+ * Forces transitive dependencies of 3rd party components that we don't use directly.
+ */
+private fun ResolutionStrategy.forceTransitiveDependencies() {
+ force(
+ AutoValue.annotations,
+ Gson.lib,
+ J2ObjC.annotations,
+ Plexus.utils,
+ Okio.lib,
+ CommonsCli.lib,
+ CheckerFramework.compatQual,
+ CommonsLogging.lib
+ )
+}
+
+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")
+}
+
+@Suppress("unused")
+object DependencyResolution {
+ @Deprecated(
+ "Please use `configurations.forceVersions()`.",
+ ReplaceWith("configurations.forceVersions()")
+ )
+ fun forceConfiguration(configurations: ConfigurationContainer) {
+ configurations.forceVersions()
+ }
+
+ @Deprecated(
+ "Please use `configurations.excludeProtobufLite()`.",
+ ReplaceWith("configurations.excludeProtobufLite()")
+ )
+ fun excludeProtobufLite(configurations: ConfigurationContainer) {
+ configurations.excludeProtobufLite()
+ }
+
+ @Deprecated(
+ "Please use `applyStandard(repositories)` instead.",
+ replaceWith = ReplaceWith("applyStandard(repositories)")
+ )
+ fun defaultRepositories(repositories: RepositoryHandler) {
+ repositories.applyStandard()
+ }
+}
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 00000000..a2e34b3a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/ProjectExtensions.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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)
+ 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 00000000..48a49385
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RepoSlug.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..db807672
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Repositories.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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
+
+/**
+ * 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? {
+ if (credentialValues != null) {
+ return credentialValues.invoke(project)
+ }
+ credentialsFile!!
+ val log = project.logger
+ log.info("Using credentials from `$credentialsFile`.")
+ val file = project.rootProject.file(credentialsFile)
+ if (!file.exists()) {
+ return null
+ }
+ val creds = file.readCredentials()
+ log.info("Publishing build as `${creds.username}`.")
+ return creds
+ }
+
+ private fun File.readCredentials(): Credentials {
+ val properties = Properties()
+ properties.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 framework projects.
+ *
+ * @see [applyStandard]
+ */
+@Suppress("unused")
+object Repos {
+ @Deprecated(
+ message = "Please use another repository.",
+ replaceWith = ReplaceWith("artifactRegistry"),
+ level = DeprecationLevel.ERROR
+ )
+ val oldSpine = PublishingRepos.mavenTeamDev.releases
+
+ @Deprecated(
+ message = "Please use another repository.",
+ replaceWith = ReplaceWith("artifactRegistrySnapshots"),
+ level = DeprecationLevel.ERROR
+ )
+ val oldSpineSnapshots = PublishingRepos.mavenTeamDev.snapshots
+
+ val spine = CloudRepo.published.releases
+ val spineSnapshots = CloudRepo.published.snapshots
+
+ val artifactRegistry = PublishingRepos.cloudArtifactRegistry.releases
+ val artifactRegistrySnapshots = PublishingRepos.cloudArtifactRegistry.snapshots
+
+ @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 standard set of Maven repositories.
+ *
+ * To be used in `buildscript` clauses when a fully-qualified call must be made.
+ */
+@Suppress("unused")
+fun doApplyStandard(repositories: RepositoryHandler) = repositories.applyStandard()
+
+/**
+ * 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 or publish artifacts from
+ * the registered repository
+ * @see applyGitHubPackages
+ */
+@Suppress("unused")
+fun doApplyGitHubPackages(
+ repositories: RepositoryHandler,
+ shortRepositoryName: String,
+ project: Project
+) = repositories.applyGitHubPackages(shortRepositoryName, project)
+
+/**
+ * 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(shortRepositoryName: String, project: Project) {
+ val repository = gitHub(shortRepositoryName)
+ val credentials = repository.credentials(project)
+
+ credentials?.let {
+ spineMavenRepo(it, repository.releases)
+ spineMavenRepo(it, repository.snapshots)
+ }
+}
+
+/**
+ * Applies repositories commonly used by Spine Event Engine projects.
+ *
+ * Does not include the repositories hosted at GitHub Packages.
+ *
+ * @see applyGitHubPackages
+ */
+@Suppress("unused")
+fun RepositoryHandler.applyStandard() {
+
+ gradlePluginPortal()
+ mavenLocal()
+
+ val spineRepos = listOf(
+ Repos.spine,
+ Repos.spineSnapshots,
+ Repos.artifactRegistry,
+ Repos.artifactRegistrySnapshots
+ )
+
+ spineRepos
+ .map { URI(it) }
+ .forEach {
+ maven {
+ url = it
+ includeSpineOnly()
+ }
+ }
+
+ mavenCentral()
+ maven {
+ url = URI(Repos.sonatypeSnapshots)
+ }
+}
+
+/**
+ * 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 00000000..03cef701
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunBuild.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..12ad0111
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/RunGradle.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle
+
+import java.io.File
+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() {
+
+ /**
+ * 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 = 10
+
+ /**
+ * 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 `build` directory.
+ val buildDir = File(directory, "build")
+ if (!buildDir.exists()) {
+ buildDir.mkdir()
+ }
+ val errorOut = File(buildDir, "error-out.txt")
+ val debugOut = File(buildDir, "debug-out.txt")
+
+ 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()
+}
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 00000000..eced40e4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Runtime.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle
+
+import com.google.common.base.Joiner
+import io.spine.internal.dependency.Flogger
+import java.io.File
+import java.io.InputStream
+import java.io.StringWriter
+import java.util.*
+
+object Runtime {
+ @Suppress("unused")
+ val flogger = Flogger.Runtime
+}
+
+/**
+ * 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)
+ .directory(workingFolder)
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .redirectError(ProcessBuilder.Redirect.PIPE)
+ .start()
+
+ process.inputStream!!.pourTo(outWriter)
+ process.errorStream!!.pourTo(errWriter)
+ val exitCode = process.waitFor()
+
+ if (exitCode == 0) {
+ return outWriter.toString()
+ } else {
+ val cmdAsString = Joiner.on(" ").join(command.iterator())
+ val errorMsg = "Command `$cmdAsString` finished with exit code $exitCode:" +
+ " ${System.lineSeparator()}$errWriter" +
+ " ${System.lineSeparator()}$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/Scripts.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Scripts.kt
new file mode 100644
index 00000000..7baf9454
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/Scripts.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle
+
+import org.gradle.api.Project
+
+@Suppress("unused")
+object Scripts {
+ @Suppress("MemberVisibilityCanBePrivate") // is used from Groovy-based scripts.
+ const val commonPath = "/buildSrc/src/main/groovy/"
+
+ fun testArtifacts(p: Project) = p.script("test-artifacts.gradle")
+ fun testOutput(p: Project) = p.script("test-output.gradle")
+ fun slowTests(p: Project) = p.script("slow-tests.gradle")
+ fun jacoco(p: Project) = p.script("jacoco.gradle")
+ fun publish(p: Project) = p.script("publish.gradle")
+ fun publishProto(p: Project) = p.script("publish-proto.gradle")
+ fun javacArgs(p: Project) = p.script("javac-args.gradle")
+ fun jsBuildTasks(p: Project) = p.script("js/build-tasks.gradle")
+ fun jsConfigureProto(p: Project) = p.script("js/configure-proto.gradle")
+ fun npmPublishTasks(p: Project) = p.script("js/npm-publish-tasks.gradle")
+ fun npmCli(p: Project) = p.script("js/npm-cli.gradle")
+ fun updatePackageVersion(p: Project) = p.script("js/update-package-version.gradle")
+ fun dartBuildTasks(p: Project) = p.script("dart/build-tasks.gradle")
+ fun pubPublishTasks(p: Project) = p.script("dart/pub-publish-tasks.gradle")
+
+ @Deprecated("Use `pmd-settings` script plugin instead")
+ fun pmd(p: Project) = p.script("pmd.gradle")
+
+ fun runBuild(p: Project) = p.script("run-build.gradle")
+ fun licenseReportCommon(p: Project) = p.script("license-report-common.gradle")
+ fun projectLicenseReport(p: Project) = p.script("license-report-project.gradle")
+ fun repoLicenseReport(p: Project) = p.script("license-report-repo.gradle")
+
+ private fun Project.script(name: String) = "${rootDir}$commonPath${name}"
+}
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 00000000..ae19007b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/StringExtensions.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..e6967b47
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/TaskName.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..bcec4a56
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/VersionWriter.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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
+ private 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 00000000..10290712
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/base/Tasks.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..e3a0ef29
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/checkstyle/CheckStyleConfig.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..2f33b281
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartContext.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..7b10f64c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartEnvironment.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..cf585a9f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/DartExtension.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..061e2add
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/DartPlugins.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..350f0fbd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/plugin/Protobuf.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.dart.plugin
+
+import com.google.protobuf.gradle.builtins
+import com.google.protobuf.gradle.id
+import com.google.protobuf.gradle.plugins
+import com.google.protobuf.gradle.protobuf
+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)
+
+ project.protobuf {
+ generateProtoTasks.all().forEach { task ->
+ task.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 00000000..79c387c3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Build.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..5ebe9272
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/DartTasks.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..f12b96fd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/IntegrationTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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()
+ * }
+ * }
+ * ```
+ */
+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 00000000..92eb2904
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/dart/task/Publish.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..595e07a1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/fs/LazyTempPath.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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].
+ */
+class LazyTempPath(private val prefix: String) : Path {
+
+ private lateinit var tempPath: Path
+
+ private val delegate: Path
+ get() {
+ if (!::tempPath.isInitialized) {
+ tempPath = createTempDirectory(prefix)
+ }
+ return tempPath
+ }
+
+ 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()
+}
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 00000000..cac6e41a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/AuthorEmail.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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)
+ if (envValue.isNullOrEmpty()) {
+ throw IllegalStateException(
+ "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/Branch.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Branch.kt
new file mode 100644
index 00000000..c9c7e7db
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Branch.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+/**
+ * Names of branches involved when updating documentation.
+ */
+object Branch {
+
+ /** The branch to use when pushing the updates to the documentation. */
+ const val ghPages = "gh-pages"
+}
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 00000000..2823416d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/TaskName.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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"
+}
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 00000000..283405dd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/Update.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import io.spine.internal.gradle.Cli
+import io.spine.internal.gradle.RepoSlug
+import java.io.File
+import java.nio.file.Path
+import org.gradle.api.GradleException
+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)
+ val op = with(plugin) {
+ Operation(project, rootFolder, checkoutTempFolder, javadocOutputPath, logger)
+ }
+ op.run()
+}
+
+private class Operation(
+ private val project: Project,
+ private val rootFolder: File,
+ checkoutTempFolder: Path,
+ private val javadocOutputPath: Path,
+ private val logger: Logger
+) {
+
+ private val ghRepoFolder: File = File("${checkoutTempFolder}/${Branch.ghPages}")
+ private val docDirPostfix = "reference/$project.name"
+ private val mostRecentDocDir = File("$ghRepoFolder/$docDirPostfix")
+
+ fun run() {
+ SshKey(rootFolder).register()
+ checkoutDocs()
+ val generatedDocs = replaceMostRecentDocs()
+ copyIntoVersionDir(generatedDocs)
+ addCommitAndPush()
+ logger.debug("The GitHub Pages contents were successfully updated.")
+ }
+
+ /** Executes a command in the project [rootFolder]. */
+ private fun execute(vararg command: String): String = Cli(rootFolder).execute(*command)
+
+ /** Executes a command in the [ghRepoFolder] */
+ private fun pagesExecute(vararg command: String): String = Cli(ghRepoFolder).execute(*command)
+
+ private fun checkoutDocs() {
+ val gitHost = RepoSlug.fromVar().gitHost()
+
+ execute("git", "clone", gitHost, ghRepoFolder.absolutePath)
+ pagesExecute("git", "checkout", Branch.ghPages)
+ }
+
+ private fun replaceMostRecentDocs(): ConfigurableFileCollection {
+ logger.debug("Replacing the most recent docs in `$mostRecentDocDir`.")
+ val generatedDocs = project.files(javadocOutputPath)
+ copyDocs(generatedDocs, mostRecentDocDir)
+ return generatedDocs
+ }
+
+ private fun copyIntoVersionDir(generatedDocs: ConfigurableFileCollection) {
+ val versionedDocDir = File("$mostRecentDocDir/v/$project.version")
+ logger.debug("Storing the new version of docs in the directory `$versionedDocDir`.")
+ copyDocs(generatedDocs, versionedDocDir)
+ }
+
+ private fun addCommitAndPush() {
+ pagesExecute("git", "add", docDirPostfix)
+ configureCommitter()
+ commitAndPush()
+ }
+
+ private fun copyDocs(source: FileCollection, destination: File) {
+ destination.mkdir()
+ project.copy {
+ from(source)
+ into(destination)
+ }
+ }
+
+ /**
+ * Configures Git to publish the changes under "UpdateGitHubPages Plugin" Git user name
+ * and email stored in "FORMAL_GIT_HUB_PAGES_AUTHOR" env variable.
+ */
+ private fun configureCommitter() {
+ pagesExecute("git", "config", "user.name", "\"UpdateGitHubPages Plugin\"")
+ val authorEmail = AuthorEmail.fromVar().toString()
+ pagesExecute("git", "config", "user.email", authorEmail)
+ }
+
+ private fun commitAndPush() {
+ pagesExecute(
+ "git",
+ "commit",
+ "--allow-empty",
+ "--message=\"Update Javadoc for module ${project.name}" +
+ " as for version ${project.version}\""
+ )
+ pagesExecute("git", "push")
+ }
+}
+
+/**
+ * Registers SSH key for further operations with GitHub Pages.
+ */
+private class SshKey(private val rootFolder: 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()
+ val nl = System.lineSeparator()
+ sshConfigFile.appendText(
+ nl +
+ "Host github.com-publish" + nl +
+ "User git" + nl +
+ "IdentityFile ${gitHubAccessKey.absolutePath}" + nl
+ )
+
+ execute(
+ "${rootFolder.absolutePath}/config/scripts/register-ssh-key.sh",
+ gitHubAccessKey.absolutePath
+ )
+ }
+
+ /**
+ * Locates `deploy_key_rsa` in the [rootFolder] and returns it as a [File].
+ *
+ * If it is not found, a [GradleException] is thrown.
+ *
+ *
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`.
+ */
+ private fun gitHubKey(): File {
+ val gitHubAccessKey = File("${rootFolder.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
+ }
+
+ /** Executes a command in the project [rootFolder]. */
+ private fun execute(vararg command: String): String = Cli(rootFolder).execute(*command)
+}
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 00000000..e3c1e6ce
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPages.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.github.pages
+
+import io.spine.internal.gradle.fs.LazyTempPath
+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 Javadoc generated 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] option,
+ * which if `true`, includes the documentation for types marked `@Internal`.
+ * By default, this option is `false`.
+ *
+ * Usage:
+ * ```
+ * updateGitHubPages {
+ *
+ * // Include `@Internal`-annotated types.
+ * 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 decrypt 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 (Travis CI)"
+ * 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 javadocOutputPath = LazyTempPath("javadoc")
+
+ /**
+ * Path to the temp folder used checkout the original GitHub Pages branch.
+ */
+ internal val checkoutTempFolder = LazyTempPath("repoTemp")
+
+ /**
+ * 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)
+ }
+ }
+ }
+
+ 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)
+ val updatePagesTask = tasks.registerUpdateTask()
+ updatePagesTask.configure {
+ dependsOn(copyJavadoc)
+ }
+ }
+
+ private fun TaskContainer.registerCopyJavadoc(allowInternalJavadoc: Boolean) {
+ val inputs = composeInputs(allowInternalJavadoc)
+ register(copyJavadoc, Copy::class.java) {
+ doLast {
+ from(*inputs.toTypedArray())
+ into(javadocOutputPath)
+ }
+ }
+ }
+
+ private fun TaskContainer.composeInputs(allowInternalJavadoc: Boolean): MutableList {
+ val inputs = mutableListOf()
+ if (allowInternalJavadoc) {
+ inputs.add(javadocTask())
+ } else {
+ inputs.add(javadocTask(ExcludeInternalDoclet.taskName))
+ }
+ 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(checkoutTempFolder, javadocOutputPath)
+ folders.forEach {
+ it.toFile().deleteRecursively()
+ }
+ }
+}
+
+/**
+ * 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}`."
+ )
+ }
+ }
+}
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 00000000..ec654fbf
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/github/pages/UpdateGitHubPagesExtension.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 types 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 00000000..8f2344fe
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/java/Tasks.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..df31e581
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/ErrorProne.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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",
+ )
+ }
+}
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 00000000..839252d6
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javac/Javac.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..f7129683
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/Encoding.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..72d7f6a1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/ExcludeInternalDoclet.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.javadoc
+
+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.
+ */
+class ExcludeInternalDoclet(val version: String) {
+
+ private val dependency = "io.spine.tools:spine-javadoc-filter:${version}"
+
+ 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 00000000..f6002cad
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocConfig.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.
+ *
+ * OpenJDK SE 11 is used for the reference.
+ */
+ private const val standardLibraryAPI = "https://cr.openjdk.java.net/~iris/se/11/latestSpec/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 00000000..2a278bd9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/JavadocTag.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..f80d53ee
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javadoc/TaskContainerExtensions.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..8754173f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsContext.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..f2d10aca
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsEnvironment.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..4fe1d3ff
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/JsExtension.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..51d312c8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Idea.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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)
+ testSourceDirs.add(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 00000000..6a17820d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/JsPlugins.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..56b1263a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/McJs.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..d99d2a28
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/plugin/Protobuf.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.javascript.plugin
+
+import com.google.protobuf.gradle.builtins
+import com.google.protobuf.gradle.generateProtoTasks
+import com.google.protobuf.gradle.id
+import com.google.protobuf.gradle.protobuf
+import com.google.protobuf.gradle.protoc
+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)
+ }
+
+ project.protobuf {
+
+ 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 00000000..dbc770df
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Assemble.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..4fdd69f9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Check.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..6658b830
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Clean.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..fadcc09c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/IntegrationTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.base.build
+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
+
+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()
+ * }
+ * }
+ * ```
+ */
+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 00000000..b384fd3a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/JsTasks.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..99935edd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/LicenseReport.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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()
+ * }
+ * }
+ * ```
+ */
+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 00000000..486d831c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Publish.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..cbf3aaef
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/javascript/task/Webpack.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.javascript.task
+
+import io.spine.internal.gradle.named
+import io.spine.internal.gradle.register
+import io.spine.internal.gradle.TaskName
+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()
+ * }
+ * }
+ * ```
+ */
+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.
+ */
+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 00000000..05bafcb0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/kotlin/KotlinConfig.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.kotlin
+
+import org.gradle.jvm.toolchain.JavaLanguageVersion
+import org.gradle.jvm.toolchain.JavaToolchainSpec
+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 {
+ (this as JavaToolchainSpec).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.
+ */
+fun KotlinCompile.setFreeCompilerArgs() {
+ kotlinOptions {
+ freeCompilerArgs = listOf(
+ "-Xskip-prerelease-check",
+ "-Xjvm-default=all",
+ "-Xopt-in=kotlin.contracts.ExperimentalContracts",
+ "-Xopt-in=kotlin.ExperimentalStdlibApi"
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Artifacts.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Artifacts.kt
new file mode 100644
index 00000000..e6860cdd
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Artifacts.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.sourceSets
+import org.gradle.api.Project
+import org.gradle.api.file.FileTreeElement
+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.named
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.withType
+
+/**
+ * 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() }
+ }
+}
+
+/**
+ * Checks if the given file belongs to the Google `.proto` sources.
+ */
+private fun FileTreeElement.isGoogleProtoSource(): Boolean {
+ val pathSegments = relativePath.segments
+ return pathSegments.isNotEmpty() && pathSegments[0].equals("google")
+}
+
+/**
+ * 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() = tasks.getOrCreate("sourcesJar") {
+ archiveClassifier.set("sources")
+ from(sourceSets["main"].allSource) // Puts Java and Kotlin sources.
+ from(protoSources()) // Puts Proto sources.
+}
+
+/**
+ * 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() = tasks.getOrCreate("protoJar") {
+ 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() = 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.
+ */
+internal fun Project.javadocJar() = tasks.getOrCreate("javadocJar") {
+ archiveClassifier.set("javadoc")
+ from(files("$buildDir/docs/javadoc"))
+ dependsOn("javadoc")
+}
+
+/**
+ * Locates or creates `dokkaJar` task in this [Project].
+ *
+ * The output of this task is a `jar` archive. The archive contains the Dokka output, generated upon
+ * Java sources from `main` source set. Requires Dokka to be configured in the target project by
+ * applying `dokka-for-java` plugin.
+ */
+internal fun Project.dokkaJar() = tasks.getOrCreate("dokkaJar") {
+ archiveClassifier.set("dokka")
+ from(files("$buildDir/docs/dokka"))
+ dependsOn("dokkaHtml")
+}
+
+private fun TaskContainer.getOrCreate(name: String, init: Jar.() -> Unit): TaskProvider =
+ if (names.contains(name)) {
+ named(name)
+ } else {
+ register(name) {
+ init()
+ }
+ }
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
similarity index 88%
rename from buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt
rename to buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
index 67fca6a5..aa4da982 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CheckVersionIncrement.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021, TeamDev. All rights reserved.
+ * Copyright 2022, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,17 +24,18 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.gradle.internal
+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
-import java.io.FileNotFoundException
-import java.net.URL
/**
* A task which verifies that the current version of the library has not been published to the given
@@ -45,8 +46,8 @@ open class CheckVersionIncrement : DefaultTask() {
/**
* The Maven repository in which to look for published artifacts.
*
- * We only check the `releases` repository. Artifacts in `snapshots` repository still may be
- * overridden.
+ * We check both the `releases` and `snapshots` repositories. Artifacts in either of these repos
+ * may not be overwritten.
*/
@Input
lateinit var repository: Repository
@@ -57,7 +58,14 @@ open class CheckVersionIncrement : DefaultTask() {
@TaskAction
private fun fetchAndCheck() {
val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}"
- val repoUrl = repository.releases
+ 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
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 00000000..143ea25c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudArtifactRegistry.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..48cd8585
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/CloudRepo.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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/DokkaJar.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/DokkaJar.kt
new file mode 100644
index 00000000..1ddd1fb9
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/DokkaJar.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+/**
+ * A DSL element of [SpinePublishing] extension which configures publishing of [dokkaJar] 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 [registerArtifacts]
+ */
+class DokkaJar {
+ /**
+ * Enables publishing `JAR`s with Dokka-generated documentation for all published modules.
+ */
+ var enabled = false
+}
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 00000000..3d5518d8
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/GitHubPackages.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Credentials
+import io.spine.internal.gradle.Repository
+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()) {
+ // Use the personal access token for the `developers@spine.io` account.
+ // Only has the permission to read public GitHub packages.
+ val targetDir = "${buildDir}/token"
+ file(targetDir).mkdirs()
+ val fileToUnzip = "${rootDir}/buildSrc/aus.weis"
+
+ logger.info("GitHub Packages: reading token " +
+ "by unzipping `$fileToUnzip` into `$targetDir`.")
+ exec {
+ // Unzip with password "123", allow overriding, quietly,
+ // into the target dir, the given archive.
+ commandLine("unzip", "-P", "123", "-oq", "-d", targetDir, fileToUnzip)
+ }
+ val file = file("$targetDir/token.txt")
+ file.readText()
+ } else {
+ githubToken
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/internal/IncrementGuard.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
similarity index 95%
rename from buildSrc/src/main/kotlin/io/spine/gradle/internal/IncrementGuard.kt
rename to buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
index d3456e07..0423ee08 100644
--- a/buildSrc/src/main/kotlin/io/spine/gradle/internal/IncrementGuard.kt
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/IncrementGuard.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2021, TeamDev. All rights reserved.
+ * Copyright 2022, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,7 +24,9 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package io.spine.gradle.internal
+@file:Suppress("unused")
+
+package io.spine.internal.gradle.publish
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -52,7 +54,7 @@ class IncrementGuard : Plugin {
override fun apply(target: Project) {
val tasks = target.tasks
tasks.register(taskName, CheckVersionIncrement::class.java) {
- repository = PublishingRepos.cloudRepo
+ repository = CloudRepo.published
tasks.getByName("check").dependsOn(this)
shouldRunAfter("test")
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/MavenJavaPublication.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/MavenJavaPublication.kt
new file mode 100644
index 00000000..cb5fa2f0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/MavenJavaPublication.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.PublishingExtension
+import org.gradle.api.publish.maven.MavenPublication
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.kotlin.dsl.create
+import org.gradle.kotlin.dsl.get
+import org.gradle.kotlin.dsl.getByType
+
+/**
+ * A publication for a typical Java project.
+ *
+ * In Gradle, in order to publish something somewhere 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][jars]. Please, take a look on [specifyArtifacts] for additional info.
+ *
+ * See: [Maven Publish Plugin | Publications](https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven:publications)
+ *
+ * @param artifactId a name that a project is known by.
+ * @param jars list of artifacts to be published along with the compilation output.
+ * @param destinations Maven repositories to which the produced artifacts will be sent.
+ */
+internal class MavenJavaPublication(
+ private val artifactId: String,
+ private val jars: Set>,
+ private val destinations: Set,
+) {
+
+ /**
+ * Registers this publication in the given project.
+ *
+ * The only prerequisite for the project is to have `maven-publish` plugin applied.
+ */
+ fun registerIn(project: Project) {
+ createPublication(project)
+ registerDestinations(project)
+ }
+
+ /**
+ * Creates a new "mavenJava" [MavenPublication] in the given project.
+ */
+ private fun createPublication(project: Project) {
+ val gradlePublishing = project.extensions.getByType()
+ val gradlePublications = gradlePublishing.publications
+ gradlePublications.create("mavenJava") {
+ specifyMavenCoordinates(project)
+ specifyArtifacts(project)
+ }
+ }
+
+ private fun MavenPublication.specifyMavenCoordinates(project: Project) {
+ groupId = project.group.toString()
+ artifactId = this@MavenJavaPublication.artifactId
+ version = project.version.toString()
+ }
+
+ /**
+ * 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 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](https://maven.apache.org/pom.html)
+ * [Understanding Gradle Module Metadata](https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html)
+ */
+ private fun MavenPublication.specifyArtifacts(project: Project) {
+
+ // "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.
+ from(project.components["java"])
+
+ // 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)
+ }
+ }
+
+ /**
+ * Goes through the [destinations] and registers each as a repository for publishing
+ * in the given Gradle project.
+ */
+ private fun registerDestinations(project: Project) {
+ val gradlePublishing = project.extensions.getByType()
+ val isSnapshot = project.version.toString().isSnapshot()
+ val gradleRepositories = gradlePublishing.repositories
+ destinations.forEach { destination ->
+ gradleRepositories.register(destination, isSnapshot, project)
+ }
+ }
+
+ private fun RepositoryHandler.register(
+ repository: Repository,
+ isSnapshot: Boolean,
+ project: Project
+ ) {
+ 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
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoJar.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoJar.kt
new file mode 100644
index 00000000..b9ffb592
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoJar.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+/**
+ * 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 [registerArtifacts]
+ */
+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
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoLocators.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoLocators.kt
new file mode 100644
index 00000000..ef048f2b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/ProtoLocators.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.file.SourceDirectorySet
+import org.gradle.kotlin.dsl.get
+
+
+/**
+ * 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 Proto sources in "main" source set.
+ *
+ * "main" source set is added by `java` plugin. 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 mainSourceSet = sourceSets["main"]
+ val protoSourceDirs = mainSourceSet.extensions.findByName("proto") as SourceDirectorySet?
+ return protoSourceDirs?.srcDirs ?: emptySet()
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingConfig.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingConfig.kt
new file mode 100644
index 00000000..f2f252e0
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingConfig.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Repository
+import org.gradle.api.Project
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.kotlin.dsl.apply
+
+/**
+ * Information, required to set up publishing of a project using `maven-publish` plugin.
+ *
+ * @param artifactId a name that a project is known by.
+ * @param destinations set of repositories, to which the resulting artifacts will be sent.
+ * @param includeProtoJar tells whether [protoJar] artifact should be published.
+ * @param includeTestJar tells whether [testJar] artifact should be published.
+ * @param includeDokkaJar tells whether [dokkaJar] artifact should be published.
+ */
+internal class PublishingConfig(
+ val artifactId: String,
+ val destinations: Set,
+ val includeProtoJar: Boolean = true,
+ val includeTestJar: Boolean = false,
+ val includeDokkaJar: Boolean = false
+)
+
+/**
+ * Applies this configuration to the given project.
+ *
+ * This method does the following:
+ *
+ * 1. Applies `maven-publish` plugin to the project.
+ * 2. Registers [MavenJavaPublication] in Gradle's [PublicationContainer][org.gradle.api.publish.PublicationContainer].
+ * 4. Configures "publish" task.
+ *
+ * The actual list of resulted artifacts is determined by [registerArtifacts].
+ */
+internal fun PublishingConfig.apply(project: Project) = with(project) {
+ apply(plugin = "maven-publish")
+ createPublication(project)
+ configurePublishTask(destinations)
+}
+
+private fun PublishingConfig.createPublication(project: Project) {
+ val artifacts = project.registerArtifacts(includeProtoJar, includeTestJar, includeDokkaJar)
+ val publication = MavenJavaPublication(
+ artifactId = artifactId,
+ jars = artifacts,
+ destinations = destinations
+ )
+ publication.registerIn(project)
+}
+
+/**
+ * Registers [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.
+ *
+ * The list of additional artifacts to be registered:
+ *
+ * 1. [sourcesJar] – Java, Kotlin and Proto source files.
+ * 2. [protoJar] – only Proto source files.
+ * 3. [javadocJar] – documentation, generated upon Java files.
+ * 4. [testJar] – compilation output of "test" source set.
+ * 5. [dokkaJar] - documentation generated by Dokka.
+ *
+ * Registration of [protoJar], [testJar] and [dokkaJar] is optional. It can be controlled by the
+ * method's parameters.
+ *
+ * @return the list of the registered tasks.
+ */
+private fun Project.registerArtifacts(
+ includeProtoJar: Boolean = true,
+ includeTestJar: Boolean = false,
+ includeDokkaJar: Boolean = false
+): Set> {
+
+ val artifacts = mutableSetOf(
+ sourcesJar(),
+ javadocJar(),
+ )
+
+ // We don't want to have an empty "proto.jar" when a project doesn't have any Proto files.
+ if (hasProto() && includeProtoJar) {
+ artifacts.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 (includeTestJar) {
+ artifacts.add(testJar())
+ }
+
+ if(includeDokkaJar) {
+ artifacts.add(dokkaJar())
+ }
+
+ return artifacts
+}
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 00000000..5abdc10f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/PublishingRepos.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Repository
+
+/**
+ * Repositories to which we may publish.
+ */
+object PublishingRepos {
+
+ @Suppress("HttpUrlsUsage") // HTTPS is not supported by this repository.
+ val mavenTeamDev = Repository(
+ name = "maven.teamdev.com",
+ releases = "http://maven.teamdev.com/repository/spine",
+ snapshots = "http://maven.teamdev.com/repository/spine-snapshots",
+ credentialsFile = "credentials.properties"
+ )
+
+ 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 00000000..1b10f71e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/SpinePublishing.kt
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Repository
+import org.gradle.api.Project
+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][MavenJavaPublication]
+ * 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.
+ *
+ * Additionally, [testJar] artifact can be published. This artifact contains compilation output
+ * of "test" source set. Use [SpinePublishing.testJar] to enable its publishing.
+ *
+ * @see [registerArtifacts]
+ */
+fun Project.spinePublishing(configuration: SpinePublishing.() -> Unit) {
+ val name = SpinePublishing::class.java.simpleName
+ val extension = with(extensions) { findByType() ?: create(name, project) }
+ extension.run {
+ configuration()
+ 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 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 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.
+ *
+ * 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 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. I.e., in Gradle 7+, one could
+ * depend on it like this:
+ *
+ * ```
+ * implementation("io.spine:spine-client:$version@proto")
+ * ```
+ */
+ fun protoJar(configuration: ProtoJar.() -> Unit) = protoJar.run(configuration)
+
+ /**
+ * 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 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. I.e., in Gradle 7+, one could
+ * depend on it like this:
+ *
+ * ```
+ * implementation("io.spine:spine-client:$version@test")
+ * ```
+ */
+ fun testJar(configuration: TestJar.() -> Unit) = testJar.run(configuration)
+
+
+ /**
+ * Configures publishing of [dokkaJar] artifact, containing Dokka-generated documentation. By
+ * default, publishing of the artifact 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-java` script plugin for Java projects.
+ *
+ * Here's an example of how to use this option:
+ *
+ * ```
+ * spinePublishing {
+ * dokkaJar {
+ * enabled = true
+ * }
+ * }
+ * ```
+ *
+ * The resulting artifact is available under "dokka" classifier.
+ */
+ fun dokkaJar(configuration: DokkaJar.() -> Unit) = dokkaJar.run(configuration)
+
+ /**
+ * 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 protoJarExclusions = protoJar.exclusions
+ val testJarInclusions = testJar.inclusions
+ val publishedProjects = publishedProjects()
+
+ publishedProjects.forEach { project ->
+ val name = project.name
+ val includeProtoJar = (protoJarExclusions.contains(name) || protoJar.disabled).not()
+ val includeTestJar = (testJarInclusions.contains(name) || testJar.enabled)
+ setUpPublishing(project, includeProtoJar, includeTestJar, dokkaJar.enabled)
+ }
+ }
+
+ /**
+ * 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 publishedProjects() = modules.map { name -> project.project(name) }
+ .ifEmpty { setOf(project) }
+
+ /**
+ * Sets up `maven-publish` plugin for the given project.
+ *
+ * Firstly, an instance of [PublishingConfig] is assembled for the project. Then, this
+ * config is applied.
+ *
+ * This method utilizes `project.afterEvaluate` closure. General rule of thumb is to avoid using
+ * of this closure, as it configures a project when its configuration is considered completed.
+ * Which is quite counter-intuitive.
+ *
+ * The root cause why it is used here is a possibility to configure publishing of multiple
+ * modules from a root project. When this possibility is employed, in fact, we configure
+ * publishing for a module, build file of which has not been even evaluated by that time.
+ * That leads to an unexpected behavior.
+ *
+ * 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 setUpPublishing(
+ project: Project,
+ includeProtoJar: Boolean,
+ includeTestJar: Boolean,
+ includeDokkaJar: Boolean
+ ) {
+ val artifactId = artifactId(project)
+ val publishingConfig = PublishingConfig(
+ artifactId,
+ destinations,
+ includeProtoJar,
+ includeTestJar,
+ includeDokkaJar
+ )
+ project.afterEvaluate {
+ publishingConfig.apply(project)
+ }
+ }
+
+ /**
+ * Obtains an artifact ID for the given project.
+ *
+ * It consists of a project's name and [prefix][artifactPrefix]:
+ * ``.
+ */
+ internal 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()) {
+ throw IllegalStateException("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) {
+ throw IllegalStateException("Publishing of `$thisProject` module is already " +
+ "configured in a root project!")
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Tasks.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Tasks.kt
new file mode 100644
index 00000000..b132638c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/Tasks.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+import io.spine.internal.gradle.Repository
+import java.util.*
+import org.gradle.api.InvalidUserDataException
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+private const val PUBLISH = "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)
+
+/**
+ * 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() =
+ if (names.contains(PUBLISH)) {
+ named(PUBLISH)
+ } else {
+ register(PUBLISH)
+ }
+
+private fun TaskContainer.registerCheckCredentialsTask(destinations: Set) =
+ 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."
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/TestJar.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/TestJar.kt
new file mode 100644
index 00000000..97233992
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/publish/TestJar.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.publish
+
+/**
+ * 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 [registerArtifacts]
+ */
+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
+}
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 00000000..89e29129
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/CodebaseFilter.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..97ab594e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtension.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..4a427979
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileExtensions.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..1f4b3828
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/FileFilter.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..5e7fff47
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/JacocoConfig.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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)
+ project.afterEvaluate {
+ 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 00000000..00fdf243
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/PathMarker.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..76d34586
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/coverage/TaskName.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..65fd036c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Configuration.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..be3e250d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/LicenseReporter.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..ebd62d5b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/MarkdownReportRenderer.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..c219dec3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ModuleDataExtensions.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 metadata of the module to the specified [Markdown document][out].
+ */
+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 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 00000000..cf1f4602
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Paths.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 = "license-report.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 00000000..164df070
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/ProjectDependencies.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..4206f3d4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Tasks.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..f8f835ea
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/license/Template.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..6a11b7c1
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyScope.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..8772219a
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/DependencyWriter.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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
+ *
+ * ...
+ *
+ * ```
+ *
+ * @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)
+ }
+ return dependencies.toSortedSet()
+}
+
+/**
+ * Returns the scoped 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.forEach {
+ if (it.isExternal()) {
+ val dependency = ScopedDependency.of(it, configuration)
+ result.add(dependency)
+ }
+ }
+ }
+ return result
+}
+
+/**
+ * Tells whether the dependency is an external module dependency.
+ */
+private fun Dependency.isExternal(): Boolean {
+ return this.javaClass.kotlin.isSubclassOf(AbstractExternalModuleDependency::class)
+}
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 00000000..a1e14666
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/InceptionYear.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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 00000000..e34beb75
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/MarkupExtensions.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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/PomFormatting.kt b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt
new file mode 100644
index 00000000..3fca911c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomFormatting.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import java.io.StringWriter
+import java.lang.System.lineSeparator
+
+/**
+ * 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(
+ "",
+ 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 00000000..8b9ab504
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomGenerator.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.internal.gradle.report.pom
+
+import org.gradle.api.Project
+import org.gradle.api.plugins.BasePlugin
+import org.gradle.kotlin.dsl.extra
+
+/**
+ * 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 00000000..3df34034
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/internal/gradle/report/pom/PomXmlWriter.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.
+ *
+ *