diff --git a/build.gradle.kts b/build.gradle.kts index 0b4bf68..ef97a9e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,8 @@ -import com.saveourtool.osv4k.buildutils.configureDiktat -import com.saveourtool.osv4k.buildutils.configureVersioning import com.saveourtool.osv4k.buildutils.createDetektTask plugins { - // alias(libs.plugins.kotlin.multiplatform) - // alias(libs.plugins.kotlin.plugin.serialization) id("com.saveourtool.osv4k.buildutils.kotlin-library") + id("com.saveourtool.osv4k.buildutils.publishing-configuration") } group = "com.saveourtool.osv4k" @@ -14,11 +11,6 @@ repositories { mavenCentral() } -// version generation -configureVersioning() -// checks and validations - -configureDiktat() createDetektTask() kotlin { @@ -29,9 +21,11 @@ kotlin { kotlinOptions.jvmTarget = "1.8" } } - linuxX64() - mingwX64() - macosX64() + val nativeTargets = setOf( + linuxX64(), + mingwX64(), + macosX64(), + ) sourceSets { val commonMain by getting { dependencies { @@ -48,12 +42,8 @@ kotlin { val commonNonJvmMain by creating { dependsOn(commonMain) } - listOf( - "linuxX64", - "mingwX64", - "macosX64", - ).forEach { nonJvmTarget -> - getByName("${nonJvmTarget}Main").dependsOn(commonNonJvmMain) + nativeTargets.forEach { nativeTarget -> + getByName("${nativeTarget.name}Main").dependsOn(commonNonJvmMain) } @Suppress("UNUSED_VARIABLE") val jvmMain by getting { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0d89dee..ef8d80e 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(libs.diktat.gradle.plugin) implementation(libs.detekt.gradle.plugin) implementation(libs.kotlin.plugin.serialization) - implementation("io.github.gradle-nexus:publish-plugin:1.3.0") implementation("org.ajoberstar.reckon:reckon-gradle:0.18.0") + implementation(libs.dokka.gradle.plugin) + implementation(libs.gradle.nexus.publish.plugin) } diff --git a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/JacocoConfiguration.kt b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/JacocoConfiguration.kt index d480135..3b5ae7e 100644 --- a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/JacocoConfiguration.kt +++ b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/JacocoConfiguration.kt @@ -22,7 +22,7 @@ fun Project.configureJacoco() { apply() configure { - toolVersion = "0.8.8" + toolVersion = JacocoPlugin.DEFAULT_JACOCO_VERSION } val kotlin: KotlinMultiplatformExtension = extensions.getByType() @@ -34,11 +34,10 @@ fun Project.configureJacoco() { } val configure: JacocoReport.() -> Unit = { + dependsOn(jvmTestTask) executionData(jvmTestTask.extensions.getByType(JacocoTaskExtension::class.java).destinationFile) - // todo: include platform-specific source sets additionalSourceDirs( - kotlin.sourceSets["commonMain"].kotlin.sourceDirectories + - kotlin.sourceSets["commonNonJvmMain"].kotlin.sourceDirectories + kotlin.sourceSets["commonMain"].kotlin.sourceDirectories ) classDirectories.setFrom(fileTree("$buildDir/classes/kotlin/jvm/main").apply { exclude("**/*\$\$serializer.class") @@ -50,10 +49,10 @@ fun Project.configureJacoco() { } // `application` plugin creates jacocoTestReport task in plugin section (this is definitely incorrect behavior) - // AFTER that in "com.saveourtool.save.buildutils.kotlin-library" we try to register this task once again and fail + // AFTER that in "com.saveourtool.osv4k.buildutils.kotlin-library" we try to register this task once again and fail // so the order of plugins in `apply` is critically important - val jacocoTestReportTask = if (project.name == "save-cli") { - val jacocoTestReportTask by tasks.named("jacocoTestReport", configure) + val jacocoTestReportTask = if (project.name == "osv4k") { + val jacocoTestReportTask by tasks.register("jacocoTestReport", configure) jacocoTestReportTask } else { val jacocoTestReportTask by tasks.register("jacocoTestReport", configure) @@ -61,5 +60,4 @@ fun Project.configureJacoco() { } jvmTestTask.finalizedBy(jacocoTestReportTask) - jacocoTestReportTask.dependsOn(jvmTestTask) } diff --git a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/PublishingConfiguration.kt b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/PublishingConfiguration.kt deleted file mode 100644 index cd76917..0000000 --- a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/PublishingConfiguration.kt +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Publishing configuration file. - */ - -package com.saveourtool.osv4k.buildutils - -import io.github.gradlenexus.publishplugin.NexusPublishExtension -import io.github.gradlenexus.publishplugin.NexusPublishPlugin -import org.gradle.api.Project -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin -import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven -import org.gradle.api.publish.maven.tasks.PublishToMavenRepository -import org.gradle.api.tasks.bundling.Jar -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.extra -import org.gradle.kotlin.dsl.getByType -import org.gradle.kotlin.dsl.register -import org.gradle.kotlin.dsl.withType -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform -import org.gradle.plugins.signing.Sign -import org.gradle.plugins.signing.SigningExtension -import org.gradle.plugins.signing.SigningPlugin - -@Suppress( - "MISSING_KDOC_ON_FUNCTION", - "MISSING_KDOC_TOP_LEVEL", - "TOO_LONG_FUNCTION" -) -fun Project.configurePublishing() { - // If present, set properties from env variables. If any are absent, release will fail. - System.getenv("OSSRH_USERNAME")?.let { - extra.set("sonatypeUsername", it) - } - System.getenv("OSSRH_PASSWORD")?.let { - extra.set("sonatypePassword", it) - } - System.getenv("GPG_SEC")?.let { - extra.set("signingKey", it) - } - System.getenv("GPG_PASSWORD")?.let { - extra.set("signingPassword", it) - } - - if (this == rootProject) { - apply() - if (hasProperty("sonatypeUsername")) { - configureNexusPublishing() - } - } - - apply() - apply() - - configurePublications() - - if (hasProperty("signingKey")) { - configureSigning() - } - - // https://kotlinlang.org/docs/mpp-publish-lib.html#avoid-duplicate-publications - afterEvaluate { - val publicationsFromMainHost = listOf( - "jvm", - "js", - "linuxX64", "mingwX64", "macosX64", - "kotlinMultiplatform", "metadata", - ) - configure { - publications.matching { it.name in publicationsFromMainHost }.all { - val targetPublication = this@all - tasks.withType() - .matching { it.publication == targetPublication } - .configureEach { - onlyIf { - // main publishing CI job is executed on Linux host - DefaultNativePlatform.getCurrentOperatingSystem().isLinux.apply { - if (!this) { - logger.lifecycle("Publication ${(it as AbstractPublishToMaven).publication.name} is skipped on current host") - } - } - } - } - } - } - } -} - -@Suppress("TOO_LONG_FUNCTION", "GENERIC_VARIABLE_WRONG_DECLARATION") -private fun Project.configurePublications() { - val dokkaJarProvider = tasks.register("dokkaJar") { - group = "documentation" - archiveClassifier.set("javadoc") - from(tasks.findByName("dokkaHtml")) - } - configure { - repositories { - mavenLocal() - } - publications.withType().forEach { publication -> - publication.artifact(dokkaJarProvider) - publication.pom { - name.set(project.name) - description.set(project.description ?: project.name) - url.set("https://github.com/saveourtool/save") - licenses { - license { - name.set("MIT License") - url.set("http://www.opensource.org/licenses/mit-license.php") - distribution.set("repo") - } - } - developers { - developer { - id.set("petertrr") - name.set("Petr Trifanov") - email.set("peter.trifanov@gmail.com") - } - developer { - id.set("akuleshov7") - name.set("Andrey Kuleshov") - email.set("andrewkuleshov7@gmail.com") - } - } - scm { - url.set("https://github.com/saveourtool/save") - connection.set("scm:git:git://github.com/saveourtool/save.git") - } - } - } - } -} - -private fun Project.configureSigning() { - configure { - useInMemoryPgpKeys(property("signingKey") as String?, property("signingPassword") as String?) - logger.lifecycle("The following publications are getting signed: ${extensions.getByType().publications.map { it.name }}") - sign(*extensions.getByType().publications.toTypedArray()) - } - - tasks.withType().configureEach { - // Workaround for the problem described at https://github.com/saveourtool/save-cli/pull/501#issuecomment-1439705340. - // We have a single Javadoc artifact shared by all platforms, hence all publications depend on signing of this artifact. - // This causes weird implicit dependencies, like `publishJsPublication...` depends on `signJvmPublication`. - dependsOn(tasks.withType()) - } -} - -private fun Project.configureNexusPublishing() { - configure { - repositories { - sonatype { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - username.set(property("sonatypeUsername") as String) - password.set(property("sonatypePassword") as String) - } - } - } -} diff --git a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/kotlin-library.gradle.kts b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/kotlin-library.gradle.kts index 96497d0..207274d 100644 --- a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/kotlin-library.gradle.kts +++ b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/kotlin-library.gradle.kts @@ -7,7 +7,6 @@ package com.saveourtool.osv4k.buildutils -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest plugins { @@ -17,101 +16,13 @@ plugins { kotlin { jvmToolchain { - this.languageVersion.set(JavaLanguageVersion.of("11")) - } - jvm { - compilations.all { - kotlinOptions { - jvmTarget = "11" - freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" - } - } - } - val nativeTargets = listOf(linuxX64(), mingwX64(), macosX64()) - if (project.name == "save-common") { - // additionally, save-common should be available for JS too - // fixme: shouldn't rely on hardcoded project name here - js(IR).browser() - - // store yarn.lock in the root directory - rootProject.extensions.configure { - lockFileDirectory = rootProject.projectDir - } - } - - if (hasProperty("disableRedundantTargets") && (property("disableRedundantTargets") as String?) != "false") { - // with this flag we exclude targets that are present on multiple OS to speed up build - val currentOs = DefaultNativePlatform.getCurrentOperatingSystem() - val redundantTarget: String? = when { - currentOs.isWindows -> "linuxX64" - currentOs.isMacOsX -> "linuxX64" - currentOs.isLinux -> null - else -> throw GradleException("Unknown operating system ${currentOs.name}") - } - tasks.matching { redundantTarget != null && it.name.contains(redundantTarget, ignoreCase = true) } - .configureEach { - logger.lifecycle("Disabling task :${project.name}:$name on host $currentOs") - enabled = false - } - } - - /* - * Common structure for MPP libraries: - * common - * | - * nonJs - * / \ - * native JVM - * / | \ - * linux mingw macos - */ - sourceSets { - all { - languageSettings.optIn("kotlin.RequiresOptIn") - } - val commonMain by getting - val commonTest by getting { - dependencies { - implementation("io.kotest:kotest-assertions-core:5.6.2") - } - } - val commonNonJsMain by creating { - dependsOn(commonMain) - } - val commonNonJsTest by creating { - dependsOn(commonTest) - dependencies { - implementation(kotlin("test-common")) - implementation(kotlin("test-annotations-common")) - } - } - val jvmMain by getting { - dependsOn(commonNonJsMain) - } - val jvmTest by getting { - dependsOn(commonNonJsTest) - dependencies { - implementation(kotlin("test-junit5")) - implementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") - } - } - val nativeMain by creating { - dependsOn(commonNonJsMain) - } - val nativeTest by creating { - dependsOn(commonNonJsTest) - } - nativeTargets.forEach { - getByName("${it.name}Main").dependsOn(nativeMain) - } - nativeTargets.forEach { - getByName("${it.name}Test").dependsOn(nativeTest) - } + languageVersion.set(JavaLanguageVersion.of("17")) } } +configureVersioning() +// withJava() creates a task for jacoco // configureJacoco() -configurePublishing() configureDiktat() configureDetekt() diff --git a/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/publishing-configuration.gradle.kts b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/publishing-configuration.gradle.kts new file mode 100644 index 0000000..f5f2c74 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/saveourtool/osv4k/buildutils/publishing-configuration.gradle.kts @@ -0,0 +1,286 @@ +package com.saveourtool.osv4k.buildutils + +import io.github.gradlenexus.publishplugin.NexusPublishExtension +import io.github.gradlenexus.publishplugin.NexusPublishPlugin +import org.gradle.internal.logging.text.StyledTextOutput +import org.gradle.internal.logging.text.StyledTextOutput.Style.Failure +import org.gradle.internal.logging.text.StyledTextOutput.Style.Identifier +import org.gradle.internal.logging.text.StyledTextOutput.Style.Info +import org.gradle.internal.logging.text.StyledTextOutput.Style.Success +import org.gradle.internal.logging.text.StyledTextOutputFactory +import org.gradle.kotlin.dsl.support.serviceOf +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + +plugins { + `maven-publish` + signing + id("org.jetbrains.dokka") +} + +group = rootProject.group +version = rootProject.version + +configurePublishing() + +/** + * Configures all aspects of the publishing process. + */ +fun Project.configurePublishing() { + createPublications() + configureNexusPublishing() + configureGitHubPublishing() + configurePublications() + configureSigning() + + // https://kotlinlang.org/docs/mpp-publish-lib.html#avoid-duplicate-publications + afterEvaluate { + val publicationsFromMainHost = listOf( + "jvm", + "js", + "linuxX64", "mingwX64", "macosX64", + "kotlinMultiplatform", "metadata", + ) + configure { + publications.matching { it.name in publicationsFromMainHost }.all { + val targetPublication = this@all + tasks.withType() + .matching { it.publication == targetPublication } + .configureEach { + onlyIf { + // main publishing CI job is executed on Linux host + DefaultNativePlatform.getCurrentOperatingSystem().isLinux.apply { + if (!this) { + logger.lifecycle("Publication ${(it as AbstractPublishToMaven).publication.name} is skipped on current host") + } + } + } + } + } + } + } +} + +/** + * Creates the publications. + */ +fun Project.createPublications() { + if (this == rootProject) { + return + } + + publishing { + publications { + create("maven") { + from(components["java"]) + suppressPomMetadataWarningsFor("testFixturesApiElements") + suppressPomMetadataWarningsFor("testFixturesRuntimeElements") + } + } + } +} + +/** + * Configures Maven Central as the publish destination. + */ +fun Project.configureNexusPublishing() { + if (this != rootProject) { + return + } + + if (!hasProperties("sonatypeUsername", "sonatypePassword")) { + styledOut(logCategory = "nexus") + .style(Info) + .text("Skipping Nexus publishing configuration as either ") + .style(Identifier) + .text("sonatypeUsername") + .style(Info) + .text(" or ") + .style(Identifier) + .text("sonatypePassword") + .style(Info) + .text(" are not set") + .println() + return + } + + apply() + + configure { + this@configure.repositories { + sonatype { + /* + * The default is https://oss.sonatype.org/service/local/. + */ + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + /* + * The default is https://oss.sonatype.org/content/repositories/snapshots/. + */ + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + username.set(property("sonatypeUsername") as String) + password.set(property("sonatypePassword") as String) + } + } + } +} + +/** + * Configures GitHub Packages as the publish destination. + */ +fun Project.configureGitHubPublishing(): Unit = + publishing { + repositories { + maven { + name = "GitHub" + url = uri("https://maven.pkg.github.com/saveourtool/osv4k") + credentials { + username = findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") + password = findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") + } + } + } + } + +/** + * Configures all publications. The publications must already exist. + */ +@Suppress("TOO_LONG_FUNCTION") +fun Project.configurePublications() { + if (this == rootProject) { + return + } + + tasks.named("javadocJar").configure { + from(tasks.findByName("dokkaJavadoc")) + } + + configure { + publications.withType().configureEach { + pom { + val project = this@configurePublications + + name.set(project.name) + description.set(project.description ?: project.name) + url.set("https://github.com/saveourtool/osv4k") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/license/MIT") + distribution.set("repo") + } + } + developers { + developer { + id.set("nulls") + name.set("Nariman Abdullin") + email.set("nulls.narik@gmail.com") + } + } + scm { + url.set("https://github.com/saveourtool/osv4k") + connection.set("scm:git:https://github.com/saveourtool/osv4k.git") + developerConnection.set("scm:git:git@github.com:saveourtool/osv4k.git") + } + } + } + } +} + +/** + * Enables signing of the artifacts if the `signingKey` project property is set. + * + * Should be explicitly called after each custom `publishing {}` section. + */ +fun Project.configureSigning() { + if (this == rootProject) { + return + } + + System.getenv("GPG_SEC")?.let { + extra.set("signingKey", it) + } + System.getenv("GPG_PASSWORD")?.let { + extra.set("signingPassword", it) + } + + if (hasProperty("signingKey")) { + /* + * GitHub Actions. + */ + configureSigningCommon { + useInMemoryPgpKeys(property("signingKey") as String?, findProperty("signingPassword") as String?) + } + } else if ( + hasProperties( + "signing.keyId", + "signing.password", + "signing.secretKeyRingFile", + ) + ) { + /*- + * Pure-Java signing mechanism via `org.bouncycastle.bcpg`. + * + * Requires an 8-digit (short form) PGP key id and a present `~/.gnupg/secring.gpg` + * (for gpg 2.1, run + * `gpg --keyring secring.gpg --export-secret-keys >~/.gnupg/secring.gpg` + * to generate one). + */ + configureSigningCommon() + } else if (hasProperty("signing.gnupg.keyName")) { + /*- + * Use an external `gpg` executable. + * + * On Windows, you may need to additionally specify the path to `gpg` via + * `signing.gnupg.executable`. + */ + configureSigningCommon { + useGpgCmd() + } + } +} + +/** + * @param useKeys the block which configures the PGP keys. Use either + * [SigningExtension.useInMemoryPgpKeys], [SigningExtension.useGpgCmd], or an + * empty lambda. + * @see SigningExtension.useInMemoryPgpKeys + * @see SigningExtension.useGpgCmd + */ +@Suppress( + "MaxLineLength", + "SpreadOperator", +) +fun Project.configureSigningCommon(useKeys: SigningExtension.() -> Unit = {}) { + require(this != rootProject) + + configure { + useKeys() + val publications = extensions.getByType().publications + val publicationCount = publications.size + val message = "The following $publicationCount publication(s) are getting signed: ${publications.map(Named::getName)}" + val style = when (publicationCount) { + 0 -> Failure + else -> Success + } + styledOut(logCategory = "signing").style(style).println(message) + sign(*publications.toTypedArray()) + } +} + +/** + * Creates a styled text output. + * + * @param logCategory + * @return [StyledTextOutput] + */ +fun Project.styledOut(logCategory: String): StyledTextOutput = + serviceOf().create(logCategory) + +/** + * Determines if this project has all the given properties. + * + * @param propertyNames the names of the properties to locate. + * @return `true` if this project has all the given properties, `false` otherwise. + * @see Project.hasProperty + */ +fun Project.hasProperties(vararg propertyNames: String): Boolean = + propertyNames.asSequence().all(this::hasProperty) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4664f4..1dcfff7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,8 @@ kotlinx-datetime = "0.4.0" jackson = "2.15.2" diktat = "1.2.5" detekt = "1.23.0" +dokka = "1.8.20" +gradle-nexus-publish-plugin = "1.3.0" [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -16,6 +18,8 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v kotlin-plugin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } diktat-gradle-plugin = { module = "org.cqfn.diktat:diktat-gradle-plugin", version.ref = "diktat" } detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +gradle-nexus-publish-plugin = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "gradle-nexus-publish-plugin"} jetbrains-annotations = { module = "org.jetbrains:annotations", version = "24.0.1" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }