Publish kotlin multiplatform library to Maven Central (InvalidMavenPublicationException multiple artifacts with the identical ...) - kotlin

As Jcenter will be shutdown soon I’m trying to migrate my libs to Maven Central. I have searched a lot to find any working script but with no luck. There is official docs, but it is like a joke, there just told to put maven-publish plugin to the gradle script and voila that’s it.
Currently I'm getting error:
Caused by: org.gradle.api.publish.maven.InvalidMavenPublicationException: Invalid publication 'js': multiple artifacts with the identical extension and classifier ('jar', 'sources').
My script looks like this:
plugins {
id("kotlin-multiplatform")
id("org.jetbrains.dokka") version "1.4.0-rc"
`maven-publish`
signing
}
kotlin {
sourceSets {
jvm()
js() {
nodejs()
browser()
}
linuxX64()
linuxArm64()
mingwX64()
macosX64()
iosArm64()
iosX64()
val commonMain by getting {
dependencies {
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val jsMain by getting {
dependencies {
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val jvmMain by getting {
dependencies {
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
}
}
val nativeMain by creating {
dependsOn(commonMain)
dependencies {
}
}
val linuxX64Main by getting {
dependsOn(nativeMain)
}
val linuxArm64Main by getting {
dependsOn(nativeMain)
}
val mingwX64Main by getting {
dependsOn(nativeMain)
}
val macosX64Main by getting {
dependsOn(nativeMain)
}
val iosArm64Main by getting {
dependsOn(nativeMain)
}
val iosX64Main by getting {
dependsOn(nativeMain)
}
}
}
tasks {
create<Jar>("javadocJar") {
dependsOn(dokkaJavadoc)
archiveClassifier.set("javadoc")
from(dokkaJavadoc.get().outputDirectory)
}
dokkaJavadoc {
println("Dokka !")
dokkaSourceSets {
create("commonMain") {
displayName = "common"
platform = "common"
}
}
}
}
// Publishing
val fis = FileInputStream("local.properties")
val properties = Properties().apply {
load(fis)
}
val ossUser = properties.getProperty("oss.user")
val ossPassword = properties.getProperty("oss.password")
extra["signing.keyId"] = properties.getProperty("signing.keyId")
extra["signing.password"] = properties.getProperty("signing.password")
extra["signing.secretKeyRingFile"] = properties.getProperty("signing.secretKeyRingFile")
val libraryVersion: String by project
val publishedGroupId: String by project
val artifactName: String by project
val libraryName: String by project
val libraryDescription: String by project
val siteUrl: String by project
val gitUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
val developerOrg: String by project
val developerName: String by project
val developerEmail: String by project
val developerId: String by project
project.group = publishedGroupId
project.version = libraryVersion
signing {
sign(publishing.publications)
}
publishing {
publications.withType(MavenPublication::class) {
groupId = publishedGroupId
artifactId = artifactName
version = libraryVersion
artifact(tasks["javadocJar"])
artifact(tasks["sourcesJar"])
pom {
name.set(libraryName)
description.set(libraryDescription)
url.set(siteUrl)
licenses {
license {
name.set(licenseName)
url.set(licenseUrl)
}
}
developers {
developer {
id.set(developerId)
name.set(developerName)
email.set(developerEmail)
}
}
organization {
name.set(developerOrg)
}
scm {
connection.set(gitUrl)
developerConnection.set(gitUrl)
url.set(siteUrl)
}
}
}
repositories {
maven("https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
name = "sonatype"
credentials {
username = ossUser
password = ossPassword
}
}
}
}
I also find this reddit topic with no solution, this article that doesn't work, and lot of others. There are tons of materials how to publish to bintray, but they are irrelevant now

It seems the issue was in this line artifact(tasks["sourcesJar"]) as this task already included.
Here I want to put my working script for uploading kotlin multiplatform library to Maven Central.
First of all we need to register Sonatype account, validate our domain, etc, here is a fresh article with detailed steps.
Then your project script build.gradle.kts may look like this:
import java.io.FileInputStream
import java.util.Properties
import org.gradle.api.publish.PublishingExtension
plugins {
id("kotlin-multiplatform")
id("org.jetbrains.dokka") version "1.4.0-rc"
id("io.codearte.nexus-staging") version "0.22.0"
`maven-publish`
signing
}
enum class OS {
LINUX, WINDOWS, MAC
}
fun getHostOsName(): OS =
System.getProperty("os.name").let { osName ->
when {
osName == "Linux" -> OS.LINUX
osName.startsWith("Windows") -> OS.WINDOWS
osName.startsWith("Mac") -> OS.MAC
else -> throw GradleException("Unknown OS: $osName")
}
}
kotlin {
sourceSets {
jvm()
js() {
browser()
nodejs()
}
when (getHostOsName()) {
OS.LINUX -> {
linuxX64()
linuxArm64()
}
OS.WINDOWS -> {
mingwX64()
}
OS.MAC -> {
macosX64()
iosArm64()
iosX64()
}
}
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
implementation(Libs.olekdia.common)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val jvmMain by getting {
dependencies {
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
}
}
val jsMain by getting {
dependencies {
}
}
val nativeMain by creating {
dependsOn(commonMain)
}
when (getHostOsName()) {
OS.LINUX -> {
val linuxX64Main by getting {
dependsOn(nativeMain)
}
val linuxArm64Main by getting {
dependsOn(nativeMain)
}
}
OS.WINDOWS -> {
val mingwX64Main by getting {
dependsOn(nativeMain)
}
}
OS.MAC -> {
val macosX64Main by getting {
dependsOn(nativeMain)
}
val iosArm64Main by getting {
dependsOn(nativeMain)
}
val iosX64Main by getting {
dependsOn(nativeMain)
}
}
}
}
}
tasks {
create<Jar>("javadocJar") {
dependsOn(dokkaJavadoc)
archiveClassifier.set("javadoc")
from(dokkaJavadoc.get().outputDirectory)
}
dokkaJavadoc {
dokkaSourceSets {
create("commonMain") {
displayName = "common"
platform = "common"
}
}
}
}
//--------------------------------------------------------------------------------------------------
// Publishing
//--------------------------------------------------------------------------------------------------
val fis = FileInputStream("local.properties")
val properties = Properties().apply {
load(fis)
}
val ossUser = properties.getProperty("oss.user")
val ossPassword = properties.getProperty("oss.password")
extra["signing.keyId"] = properties.getProperty("signing.keyId")
extra["signing.password"] = properties.getProperty("signing.password")
extra["signing.secretKeyRingFile"] = properties.getProperty("signing.secretKeyRingFile")
val libraryVersion: String by project
val publishedGroupId: String by project
val artifactName: String by project
val libraryName: String by project
val libraryDescription: String by project
val siteUrl: String by project
val gitUrl: String by project
val licenseName: String by project
val licenseUrl: String by project
val developerOrg: String by project
val developerName: String by project
val developerEmail: String by project
val developerId: String by project
project.group = publishedGroupId
project.version = libraryVersion
signing {
sign(publishing.publications)
}
afterEvaluate {
configure<PublishingExtension> {
publications.all {
val mavenPublication = this as? MavenPublication
mavenPublication?.artifactId =
"${project.name}${"-$name".takeUnless { "kotlinMultiplatform" in name }.orEmpty()}"
}
}
}
publishing {
publications.withType(MavenPublication::class) {
groupId = publishedGroupId
artifactId = artifactName
version = libraryVersion
artifact(tasks["javadocJar"])
pom {
name.set(libraryName)
description.set(libraryDescription)
url.set(siteUrl)
licenses {
license {
name.set(licenseName)
url.set(licenseUrl)
}
}
developers {
developer {
id.set(developerId)
name.set(developerName)
email.set(developerEmail)
}
}
organization {
name.set(developerOrg)
}
scm {
connection.set(gitUrl)
developerConnection.set(gitUrl)
url.set(siteUrl)
}
}
}
repositories {
maven("https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
name = "sonatype"
credentials {
username = ossUser
password = ossPassword
}
}
}
}
nexusStaging {
username = ossUser
password = ossPassword
packageGroup = publishedGroupId
}
Provide needed library details in gradle.properties:
libraryVersion = 0.1.1
libraryName = Your library name
libraryDescription = Your library description
publishedGroupId = com.yourdomain
artifactName = your-cool-librayr
siteUrl = https://gitlab.com/yourlibrayr
gitUrl = https://gitlab.com/yourlibrayr.git
developerId = ...
developerOrg = ...
developerName = Your Name
developerEmail = yourmail#mail.com
licenseName = The Apache Software License, Version 2.0
licenseUrl = http://www.apache.org/licenses/LICENSE-2.0.txt
allLicenses = ["Apache-2.0"]
kotlin.mpp.enableGranularSourceSetsMetadata = true
gnsp.disableApplyOnlyOnRootProjectEnforcement = true
Here gnsp.disableApplyOnlyOnRootProjectEnforcement = true property needed for declaring nexusStaging in subprojects.
And finally put your credits to local.properties:
oss.user=your_user_name
oss.password=your_pass
signing.keyId=last_8_numbers_of_key
signing.password=your_pass
signing.secretKeyRingFile=/path/to/keystorage.gpg
Now for publishing open terminal in project directory:
./gradlew build
./gradlew publish
./gradlew closeAndReleaseRepository
You could skip last command, and close and release staging packages from Nexus repository manager. That nexus-staging plugin is only needed to do it from command line.
I have tried to move publishing part of script to separate file, and include it with apply(from = "publish.gradle.kts"), but it didn't work, as it loses context in separate file
I use older version of dokka library (1.4.0-rc), as newer version could not generate javadocs for all platforms. And this javadocs is required by repository for publishing. As authors mentioned we could generate empty javadoc.jar file for that purpose.

Related

Kotlin Multiplatform. Cannot access class SqlDriver.Schema. Check your module classpath for missing or conflicting dependencies

I am trying to build a KMP library targeting iOS, Android, JS(Browser), Mac, Windows and Linux. For now I am only using Ktor and SQLDelight as a dependency. But getting the following issue in nativeMain's actual implementation while creating driver for SQLDelight
While the same code doesn't give any issue for iOS main which is also using the same NativeSqliteDriver (I need them separately since Ktor client for iOS and desktop platforms are separate).
Following is my build.gradle.kts
plugins {
kotlin("multiplatform") version "1.5.31"
id("maven-publish")
id("com.android.library")
kotlin("plugin.serialization") version "1.5.31"
id("com.squareup.sqldelight") version "1.5.3"
}
group = "me.group"
version = "1.0-SNAPSHOT"
val xcFrameworkName = "AddressLib"
repositories {
google()
mavenCentral()
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnit()
}
}
js(LEGACY) {
browser {
commonWebpackConfig {
cssSupport.enabled = true
}
}
}
val xcFramework = XCFramework(xcFrameworkName)
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
when {
hostOs == "Mac OS X" -> macosX64("native") {
binaries.framework(xcFrameworkName) {
xcFramework.add(this)
}
}
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
android()
ios {
binaries.framework(xcFrameworkName) {
xcFramework.add(this)
}
}
val coroutinesVersion = "1.5.2-native-mt"
val serializationVersion = "1.3.1"
val ktorVersion = "1.6.5"
val sqlDelightVersion = "1.5.3"
val napierVersion = "2.2.0"
val koinVersion = "3.1.4"
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-serialization:$ktorVersion")
implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("com.squareup.sqldelight:runtime:$sqlDelightVersion")
implementation("io.insert-koin:koin-core:$koinVersion")
implementation("io.github.aakira:napier:$napierVersion")
}
}
val commonTest by getting
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-java:$ktorVersion")
implementation("com.squareup.sqldelight:sqlite-driver:$sqlDelightVersion")
}
}
val jvmTest by getting
val jsMain by getting {
dependencies {
implementation("io.ktor:ktor-client-js:$ktorVersion")
implementation("com.squareup.sqldelight:sqljs-driver:$sqlDelightVersion")
}
}
val jsTest by getting
val nativeMain by getting {
dependencies {
implementation("io.ktor:ktor-client-curl:$ktorVersion")
implementation("com.squareup.sqldelight:native-driver:$sqlDelightVersion")
}
}
val nativeTest by getting
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("com.squareup.sqldelight:android-driver:$sqlDelightVersion")
}
}
val androidTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-ios:$ktorVersion")
implementation("com.squareup.sqldelight:native-driver:$sqlDelightVersion")
}
}
val iosTest by getting
}
sqldelight {
database("AddressDatabase") {
packageName = "com.library.address.database"
}
}
}
android {
compileSdkVersion(31)
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdkVersion(24)
targetSdkVersion(31)
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
publishing {
repositories {
maven {
credentials {
username = "<username>"
password = "<pwd>"
}
url = URI("https://mymavenrepo.com")
}
}
}
So it seems the issue was somewhat due to same dependency being added to the build gradle twice and it's corresponding code being added twice as well. To solve the same I had to make a separate source set like the following
val sqlDriverNativeMain by creating {
dependsOn(commonMain)
dependencies {
implementation("com.squareup.sqldelight:native-driver:$sqlDelightVersion")
}
}
val iosMain by getting {
dependsOn(sqlDriverNativeMain)
dependencies {
implementation("io.ktor:ktor-client-ios:$ktorVersion")
}
}
val nativeMain by getting {
dependsOn(sqlDriverNativeMain)
dependencies {
implementation("io.ktor:ktor-client-curl:$ktorVersion")
}
}
and after that move the driver creation code inside the sourceSet directory named sqlDriverNativeMain. This resolved the issue.

KSP on Kotlin Multiplatform fails on the kspJs with "Collection has more than one element."

I'm experimenting with KSP (Kotlin Symbol Processing) to see what it's capable of and I'm trying to get it working on a Kotlin Multiplatform project.
When I only enable kspJvm, it works perfectly, as soon as I enable kspJs as well, it fails with "Collection has more than one element."
I've recreated the issue in this demo github project:
https://github.com/janvladimirmostert/observable-demo
In my processor, I have the following config:
build.gradle.kts:
val kspVersion: String by project
group = "io.jvaas"
plugins {
kotlin("multiplatform")
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
}
sourceSets {
val commonMain by getting
val jvmMain by getting {
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion")
}
}
}
}
gradle.properties:
kotlinVersion=1.6.0
kspVersion=1.6.0-1.0.1
src/commonMain/kotlin/io/jvaas/observe/Observable.kt
package io.jvaas.observe
annotation class Observable
src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
io.jvaas.observe.ObservableProcessorProvider
src/jvmMain/kotlin/io/jvaas/observe/ObservableProcessor.kt
class ObservableProcessor(
val codeGenerator: CodeGenerator,
val logger: KSPLogger,
) : SymbolProcessor {
...
}
class ObservableProcessorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment
): SymbolProcessor {
return ObservableProcessor(environment.codeGenerator, environment.logger)
}
}
In my consumer I have the following:
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target.UMD
group = "com.od"
plugins {
application
id("com.google.devtools.ksp") version "1.6.0-1.0.1"
kotlin("plugin.serialization")
kotlin("multiplatform")
id("com.github.johnrengelman.shadow")
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
}
js(IR) {
browser {
binaries.executable()
webpackTask {
output.libraryTarget = UMD
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
val serializationVersion = "1.3.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
implementation("io.jvaas:jvaas-observe")
}
}
val commonTest by getting
val jvmMain by getting {
dependencies {
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
}
}
dependencies {
add("kspJvm", "io.jvaas:jvaas-observe")
// add("kspJs", "io.jvaas:jvaas-observe") // <--- fails if enabled
//ksp("io.jvaas:jvaas-observe")
}
application {
mainClassName = "com.od.demo.Main"
}
applications/od-server/src/commonMain/kotlin/com/od/demo/Blah.kt
package com.od.demo
import io.jvaas.observe.Observable
#Observable
class Blah {
var test1: String = ""
var test2: Int = 0
var test3: Array<String> = arrayOf()
}
This correctly gets processed when the kspJvm option is enabled and correctly outpus a file at
applications/od-server/build/generated/ksp/jvmMain/kotlin/com/od/demo/BlahO.kt
If I enable it for kspJs, it fails
add("kspJs", "io.jvaas:jvaas-observe")
Execution failed for task ':applications:od-server:compileProductionExecutableKotlinJs'.
> Failed to calculate the value of task ':applications:od-server:compileProductionExecutableKotlinJs' property 'entryModule$kotlin_gradle_plugin'.
> Collection has more than one element.
I've tried the usual gradle build --info / --debug / --scan but it's not clear where I can start looking to resolve this issue.
As mentioned above, I made a demo project to demonstrate the error:
https://github.com/janvladimirmostert/observable-demo
Any ideas on how to resolve that error?
Issue has been fixed in https://github.com/google/ksp/issues/744 but I'm not sure if it has been released yet.

How can I publish a javadoc.jar file with my Kotlin multiplatform project?

I am trying to publish my Kotlin multiplatform library to Maven Central via Sonatype. This repository requires me to include a javadoc.jar file with my artifacts. Unfortunately, the IntelliJ IDEA project wizard and the Kotlin multiplatform docs do not help me do that. When running the Gradle task dokkaJavadoc (for the official Kotlin documentation tool Dokka), I get the error "Dokka Javadoc plugin currently does not support generating documentation for multiplatform project."
I actually do not need genuine JavaDocs for publishing - an empty javadoc.jar or one with other docs generated by Dokka would suffice. Since I have been a longtime Maven user and these are my first steps with Gradle, I have no idea how to do that.
build.gradle.kts:
plugins {
kotlin("multiplatform") version "1.4.31"
id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
id("io.gitlab.arturbosch.detekt") version "1.15.0"
id("org.jetbrains.dokka") version "1.4.20"
id("maven-publish")
signing
}
group = "com.marcoeckstein"
version = "0.0.3-SNAPSHOT"
publishing {
publications {
create<MavenPublication>("maven") {
pom {
val projectGitUrl = "https://github.com/marco-eckstein/kotlin-lib"
name.set(rootProject.name)
description.set(
"A general-purpose multiplatform library. " +
"Implemented in Kotlin, usable also from Java, JavaScript and more."
)
url.set(projectGitUrl)
inceptionYear.set("2021")
licenses {
license {
name.set("MIT")
url.set("https://opensource.org/licenses/MIT")
}
}
developers {
developer {
id.set("marcoeckstein.com")
name.set("Marco Eckstein")
email.set("marco.eckstein#gmx.de")
url.set("https://www.marcoeckstein.com")
}
}
issueManagement {
system.set("GitHub")
url.set("$projectGitUrl/issues")
}
scm {
connection.set("scm:git:$projectGitUrl")
developerConnection.set("scm:git:$projectGitUrl")
url.set(projectGitUrl)
}
}
}
}
repositories {
maven {
name = "sonatypeStaging"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")
credentials(PasswordCredentials::class)
}
}
}
signing {
useGpgCmd()
sign(publishing.publications["maven"])
}
repositories {
mavenCentral()
jcenter()
}
kotlin {
targets.all {
compilations.all {
kotlinOptions {
allWarningsAsErrors = true
}
}
}
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(BOTH) {
browser {
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport.enabled = true
}
}
}
}
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
val commonMain by getting
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation("io.kotest:kotest-assertions-core:4.4.1")
}
}
val jvmMain by getting
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val nativeMain by getting
val nativeTest by getting
}
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
enableExperimentalRules.set(true)
verbose.set(true)
// ktlint.disabled_rules:
// filename:
// Caught more precisely (with desired exceptions) with detekt.
// import-ordering:
// ktlint's order is not supported (yet) by IntelliJ.
// See:
// - https://github.com/pinterest/ktlint/issues/527
// - https://youtrack.jetbrains.com/issue/KT-10974
// no-wildcard-imports:
// Not desired. We want them for Java statics and Enum members.
// experimental:annotation:
// Not desired.
// experimental:multiline-if-else:
// Not desired.
disabledRules.set(
setOf(
"filename",
"import-ordering",
"no-wildcard-imports",
"experimental:annotation",
"experimental:multiline-if-else"
)
)
additionalEditorconfigFile.set(file("$projectDir/.editorconfig"))
}
detekt {
input = files("$projectDir/src/")
config = files("$projectDir/detekt-config.yml")
buildUponDefaultConfig = true
}
tasks {
withType<io.gitlab.arturbosch.detekt.Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = "11"
}
}
Also posted at Kotlin Discussions.
This answer is a cross-post from Kotlin Discussions. Credit goes to Lamba92_v2 of the JetBrains Team, who linked his solution in his project kotlingram.
I noticed I had another issue related to publishing: Signatures and POM information where not applied to all modules. But given Lamba92_v2's code I could resolve all publishing-related issues:
plugins {
kotlin("multiplatform") version "1.4.31"
id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
id("io.gitlab.arturbosch.detekt") version "1.15.0"
id("org.jetbrains.dokka") version "1.4.20"
id("maven-publish")
signing
}
group = "com.marcoeckstein"
version = "0.0.3"
val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class)
val javadocJar: TaskProvider<Jar> by tasks.registering(Jar::class) {
dependsOn(dokkaHtml)
archiveClassifier.set("javadoc")
from(dokkaHtml.outputDirectory)
}
publishing {
publications.withType<MavenPublication> {
artifact(javadocJar)
pom {
val projectGitUrl = "https://github.com/marco-eckstein/kotlin-lib"
name.set(rootProject.name)
description.set(
"A general-purpose multiplatform library. " +
"Implemented in Kotlin, usable also from Java, JavaScript and more."
)
url.set(projectGitUrl)
inceptionYear.set("2021")
licenses {
license {
name.set("MIT")
url.set("https://opensource.org/licenses/MIT")
}
}
developers {
developer {
id.set("marcoeckstein.com")
name.set("Marco Eckstein")
email.set("marco.eckstein#gmx.de")
url.set("https://www.marcoeckstein.com")
}
}
issueManagement {
system.set("GitHub")
url.set("$projectGitUrl/issues")
}
scm {
connection.set("scm:git:$projectGitUrl")
developerConnection.set("scm:git:$projectGitUrl")
url.set(projectGitUrl)
}
}
the<SigningExtension>().sign(this)
}
repositories {
maven {
name = "sonatypeStaging"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")
credentials(PasswordCredentials::class)
}
}
}
signing {
useGpgCmd()
}
repositories {
mavenCentral()
jcenter()
}
kotlin {
targets.all {
compilations.all {
kotlinOptions {
allWarningsAsErrors = true
}
}
}
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(BOTH) {
browser {
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport.enabled = true
}
}
}
}
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
val commonMain by getting
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation("io.kotest:kotest-assertions-core:4.4.1")
}
}
val jvmMain by getting
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val nativeMain by getting
val nativeTest by getting
}
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
enableExperimentalRules.set(true)
verbose.set(true)
// ktlint.disabled_rules:
// filename:
// Caught more precisely (with desired exceptions) with detekt.
// import-ordering:
// ktlint's order is not supported (yet) by IntelliJ.
// See:
// - https://github.com/pinterest/ktlint/issues/527
// - https://youtrack.jetbrains.com/issue/KT-10974
// no-wildcard-imports:
// Not desired. We want them for Java statics and Enum members.
// experimental:annotation:
// Not desired.
// experimental:multiline-if-else:
// Not desired.
disabledRules.set(
setOf(
"filename",
"import-ordering",
"no-wildcard-imports",
"experimental:annotation",
"experimental:multiline-if-else"
)
)
additionalEditorconfigFile.set(file("$projectDir/.editorconfig"))
}
detekt {
input = files("$projectDir/src/")
config = files("$projectDir/detekt-config.yml")
buildUponDefaultConfig = true
}
tasks {
withType<io.gitlab.arturbosch.detekt.Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = "11"
}
}

Unable to use kotlinx.serialization in multiplatform project

I am attempting to use kotlinx.serialization in a multiplatform (JVM/JS) project.
When I add #Serializable annotation to some data classes in some class in common module:
#Serializable
data class User(
val user: String
)
The build succeeds without errors, but it doesn't look like the encoders/decoders are generated.
In build/generated-src I don't see any related kotlin files which provide the encodeToString extension function, and when I try to use something like:
JSON.encodeToString(User(login = "X"))
I get an Unresolved reference error:
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public inline fun <reified T> StringFormat.encodeToString(value: TypeVariable(T)): String defined in kotlinx.serialization
Any help to resolve this would be appreciated. Thanks in advance.
My build.gradle.kts:
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
/* buildscript {
dependencies {
val kotlinVersion: String by System.getProperties()
classpath(kotlin("serialization", version = kotlinVersion))
}
} */
plugins {
val kotlinVersion: String by System.getProperties()
kotlin("multiplatform") version kotlinVersion
id("kotlinx-serialization") version kotlinVersion
val kvisionVersion: String by System.getProperties()
id("kvision") version kvisionVersion
}
version = "1.0.0-SNAPSHOT"
group = "tech.lorefnon"
repositories {
mavenCentral()
jcenter()
maven { url = uri("https://dl.bintray.com/kotlin/kotlin-eap") }
maven { url = uri("https://kotlin.bintray.com/kotlinx") }
maven { url = uri("https://dl.bintray.com/kotlin/kotlin-js-wrappers") }
maven { url = uri("https://dl.bintray.com/rjaros/kotlin") }
maven { url = uri("https://repo.spring.io/milestone") }
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
mavenLocal()
}
// Versions
val kotlinVersion: String by System.getProperties()
val kvisionVersion: String by System.getProperties()
val ktorVersion: String by project
val logbackVersion: String by project
val commonsCodecVersion: String by project
val webDir = file("src/frontendMain/web")
val mainClassName = "io.ktor.server.netty.EngineMain"
kotlin {
jvm("backend") {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjsr305=strict")
}
}
}
js("frontend") {
browser {
runTask {
outputFileName = "main.bundle.js"
sourceMaps = false
devServer = KotlinWebpackConfig.DevServer(
open = false,
port = 3000,
proxy = mapOf(
"/kv/*" to "http://localhost:8080",
"/login" to "http://localhost:8080",
"/logout" to "http://localhost:8080",
"/kvws/*" to mapOf("target" to "ws://localhost:8080", "ws" to true)
),
contentBase = listOf("$buildDir/processedResources/frontend/main")
)
}
webpackTask {
outputFileName = "main.bundle.js"
}
testTask {
useKarma {
useChromeHeadless()
}
}
}
binaries.executable()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
api("pl.treksoft:kvision-server-ktor:$kvisionVersion")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
}
kotlin.srcDir("build/generated-src/common")
kotlin.srcDir("src/commonMain/kotlin")
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val backendMain by getting {
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
implementation("com.auth0:java-jwt:3.11.0")
implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("io.ktor:ktor-auth:$ktorVersion")
implementation("io.ktor:ktor-auth-jwt:$ktorVersion")
implementation("ch.qos.logback:logback-classic:$logbackVersion")
implementation("commons-codec:commons-codec:$commonsCodecVersion")
implementation("org.redisson:redisson:3.14.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.2")
}
}
val backendTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
}
}
val frontendMain by getting {
resources.srcDir(webDir)
dependencies {
implementation("pl.treksoft:kvision:$kvisionVersion")
implementation("pl.treksoft:kvision-redux:${kvisionVersion}")
implementation("pl.treksoft:kvision-bootstrap:$kvisionVersion")
implementation("pl.treksoft:kvision-bootstrap-select:$kvisionVersion")
implementation("pl.treksoft:kvision-datacontainer:$kvisionVersion")
implementation("pl.treksoft:kvision-bootstrap-dialog:$kvisionVersion")
implementation("pl.treksoft:kvision-fontawesome:$kvisionVersion")
implementation("pl.treksoft:kvision-i18n:$kvisionVersion")
implementation(npm("redux-logger", "3.0.6"))
}
kotlin.srcDir("build/generated-src/frontend")
}
val frontendTest by getting {
dependencies {
implementation(kotlin("test-js"))
implementation("pl.treksoft:kvision-testutils:$kvisionVersion:tests")
}
}
}
}
fun getNodeJsBinaryExecutable(): String {
val nodeDir = NodeJsRootPlugin.apply(project).nodeJsSetupTaskProvider.get().destination
val isWindows = System.getProperty("os.name").toLowerCase().contains("windows")
val nodeBinDir = if (isWindows) nodeDir else nodeDir.resolve("bin")
val command = NodeJsRootPlugin.apply(project).nodeCommand
val finalCommand = if (isWindows && command == "node") "node.exe" else command
return nodeBinDir.resolve(finalCommand).absolutePath
}
afterEvaluate {
tasks {
getByName("frontendProcessResources", Copy::class) {
dependsOn("compileKotlinFrontend")
exclude("**/*.pot")
doLast("Convert PO to JSON") {
destinationDir.walkTopDown().filter {
it.isFile && it.extension == "po"
}.forEach {
exec {
executable = getNodeJsBinaryExecutable()
args(
"$buildDir/js/node_modules/gettext.js/bin/po2json",
it.absolutePath,
"${it.parent}/${it.nameWithoutExtension}.json"
)
println("Converted ${it.name} to ${it.nameWithoutExtension}.json")
}
it.delete()
}
}
}
create("frontendArchive", Jar::class).apply {
dependsOn("frontendBrowserProductionWebpack")
group = "package"
archiveAppendix.set("frontend")
val distribution =
project.tasks.getByName("frontendBrowserProductionWebpack", KotlinWebpack::class).destinationDirectory!!
from(distribution) {
include("*.*")
}
from(webDir)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
into("/assets")
inputs.files(distribution, webDir)
outputs.file(archiveFile)
manifest {
attributes(
mapOf(
"Implementation-Title" to rootProject.name,
"Implementation-Group" to rootProject.group,
"Implementation-Version" to rootProject.version,
"Timestamp" to System.currentTimeMillis()
)
)
}
}
getByName("backendProcessResources", Copy::class) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
getByName("backendJar").group = "package"
create("jar", Jar::class).apply {
dependsOn("frontendArchive", "backendJar")
group = "package"
manifest {
attributes(
mapOf(
"Implementation-Title" to rootProject.name,
"Implementation-Group" to rootProject.group,
"Implementation-Version" to rootProject.version,
"Timestamp" to System.currentTimeMillis(),
"Main-Class" to mainClassName
)
)
}
val dependencies = configurations["backendRuntimeClasspath"].filter { it.name.endsWith(".jar") } +
project.tasks["backendJar"].outputs.files +
project.tasks["frontendArchive"].outputs.files
dependencies.forEach {
if (it.isDirectory) from(it) else from(zipTree(it))
}
exclude("META-INF/*.RSA", "META-INF/*.SF", "META-INF/*.DSA")
inputs.files(dependencies)
outputs.file(archiveFile)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
create("backendRun", JavaExec::class) {
dependsOn("compileKotlinBackend")
group = "run"
main = mainClassName
classpath =
configurations["backendRuntimeClasspath"] + project.tasks["compileKotlinBackend"].outputs.files +
project.tasks["backendProcessResources"].outputs.files
workingDir = buildDir
}
getByName("compileKotlinBackend") {
dependsOn("compileKotlinMetadata")
}
getByName("compileKotlinFrontend") {
dependsOn("compileKotlinMetadata")
}
}
}
And gradle.properties:
javaVersion=1.8
#Plugins
systemProp.kotlinVersion=1.4.20
systemProp.serializationVersion=1.0.1
#Dependencies
systemProp.kvisionVersion=3.17.2
ktorVersion=1.4.3
commonsCodecVersion=1.10
logbackVersion=1.2.3
kotlin.mpp.stability.nowarn=true
kotlin.js.compiler=legacy
org.gradle.jvmargs=-Xmx2g
And settings.gradle.kts:
pluginManagement {
repositories {
mavenCentral()
jcenter()
maven { url = uri("https://plugins.gradle.org/m2/") }
maven { url = uri("https://dl.bintray.com/kotlin/kotlin-eap") }
maven { url = uri("https://kotlin.bintray.com/kotlinx") }
maven { url = uri("https://dl.bintray.com/rjaros/kotlin") }
mavenLocal()
}
resolutionStrategy {
eachPlugin {
when {
requested.id.id == "kotlinx-serialization" -> useModule("org.jetbrains.kotlin:kotlin-serialization:${requested.version}")
requested.id.id == "kvision" -> useModule("pl.treksoft:kvision-gradle-plugin:${requested.version}")
}
}
}
}
rootProject.name = "test"
I often face same problem. You just need to add import:
import kotlinx.serialization.encodeToString
You probably don't want to add import manually, I usually start typing encodeToStr and, wait suggestions to appear and choose encodeToString(value: T), this will add needed import.
I believe it's a bug that IDE don't give you a suggestion to import, and give us this error instead.

androidMain and android tests folder not recognised as module

I’ve been struggling with this issue since days but can’t find any solution so I hope I can get any help here.
I created a Multiplatform project with Mobile Application template from IntelliJ IDEA 2020.2.1 and since project setup androidMain and AndroidTest folders are not recognised as module. This seems not to be a problem when launching androidApp task or PackForXcode as references to expect/actual definitions are resolved but idea suggestions shows anyway how androidMain reference seems to be wrong as reference seems to be resolved from parent vpayConnectLib module as per screenshot.
I use a standard android() target
When launching compileDebugAndroidTestKotlinAndroid task this error is highlighted
I tried to create new projects with different templates but androidMain and androidTest folders are not considered module since the beginning with all projects template
Is this a know bug or I am doing something wrong? Here’s root project gradle file with versions I am using
Hre's my root project gradle file:
buildscript {
repositories {
gradlePluginPortal()
jcenter()
google()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0")
classpath("com.android.tools.build:gradle:4.0.1")
classpath("com.squareup.sqldelight:gradle-plugin:1.4.3")
classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8")
}
}
group = "com.retailinmotion"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
and here's shared library gradle file
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
id("com.android.library")
id("kotlin-android-extensions")
id("com.squareup.sqldelight")
id("org.sonarqube")
jacoco
}
group = "com.retailinmotion"
version = "1.0-SNAPSHOT"
repositories {
gradlePluginPortal()
google()
jcenter()
mavenCentral()
}
sqldelight {
database("vPayConnectDatabase") {
packageName = "com.retailinmotion.vpayconnect"
sourceFolders = listOf("sqldelight")
}
}
task<JacocoReport>("jacocoTestReport") {
dependsOn("testDebugUnitTest")
reports {
xml.isEnabled = true
csv.isEnabled = false
html.isEnabled = true
}
sourceDirectories.setFrom(files(fileTree("../vPayConnectLib")))
classDirectories.setFrom(files(fileTree("build/tmp/kotlin-classes/debug")))
executionData.setFrom(file("build/jacoco/testDebugUnitTest.exec"))
}
sonarqube {
properties {
property("sonar.sourceEncoding", "UTF-8")
property("sonar.java.coveragePlugin", "jacoco")
property("sonar.sources", "src/commonMain/kotlin, src/iosMain/kotlin, src/androidMain/kotlin")
property("sonar.tests", "src/commonTest/kotlin, src/iosTest/kotlin, src/androidTest/kotlin")
property("sonar.binaries", "build/tmp/kotlin-classes/debug")
property("sonar.java.binaries", "build/tmp/kotlin-classes/debug")
property("sonar.java.test.binaries", "build/tmp/kotlin-classes/debugUnitTest")
property("sonar.junit.reportPaths", "build/test-results/testDebugUnitTest")
property("sonar.jacoco.reportPaths", "build/jacoco/testDebugUnitTest.exec")
property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml")
}
}
kotlin {
android()
iosArm64("ios") {
binaries {
framework {
baseName = "vPayConnectLib"
}
}
compilations["main"].cinterops {
//import Datecs library
val datecs by creating {
packageName ("com.retailinmotion.datecs")
defFile = file("$projectDir/src/iosMain/c_interop/Datecs.def")
includeDirs ("$projectDir/src/iosMain/c_interop/Datecs/")
}
//import Datecs Utils library
val datecsutils by creating {
packageName ("com.retailinmotion.datecsutils")
defFile = file("$projectDir/src/iosMain/c_interop/DatecsUtils.def")
includeDirs ("$projectDir/src/iosMain/c_interop/DatecsUtils/")
}
//import Magtek library
val magtek by creating {
packageName ("com.retailinmotion.magtek")
defFile = file("$projectDir/src/iosMain/c_interop/Magtek.def")
includeDirs ("$projectDir/src/iosMain/c_interop/Magtek")
}
}
}
iosX64(){
binaries {
framework {
baseName = "vPayConnectLib"
}
}
compilations["main"].cinterops {
//import Datecs library
val datecs by creating {
packageName ("com.retailinmotion.datecs")
defFile = file("$projectDir/src/iosMain/c_interop/Datecs.def")
includeDirs ("$projectDir/src/iosMain/c_interop/Datecs/")
}
//import Datecs Utils library
val datecsutils by creating {
packageName ("com.retailinmotion.datecsutils")
defFile = file("$projectDir/src/iosMain/c_interop/DatecsUtils.def")
includeDirs ("$projectDir/src/iosMain/c_interop/DatecsUtils/")
}
//import Magtek library
val magtek by creating {
packageName ("com.retailinmotion.magtek")
defFile = file("$projectDir/src/iosMain/c_interop/Magtek.def")
includeDirs ("$projectDir/src/iosMain/c_interop/Magtek")
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.squareup.sqldelight:runtime:1.4.3")
}
}
val commonTest by getting {
dependsOn(commonMain)
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation("org.jetbrains.kotlin:kotlin-test-junit:1.4.10")
}
}
val androidMain by getting {
dependencies {
implementation("androidx.core:core-ktx:1.3.1")
implementation("com.squareup.sqldelight:android-driver:1.4.3")
}
}
val androidTest by getting {
dependsOn(androidMain)
}
val iosMain by getting {
dependencies {
implementation("com.squareup.sqldelight:native-driver:1.4.3")
}
}
val iosTest by getting {
dependsOn(iosMain)
}
val iosX64Main by getting {
dependsOn(iosMain)
}
val iosX64Test by getting {
dependsOn(iosTest)
}
}
tasks {
register("universalFramework", org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask::class) {
val debugMode = "DEBUG"
val mode = System.getenv("CONFIGURATION") ?: debugMode
baseName = "vPayConnectLib"
val iosArm64Framework = iosArm64("ios").binaries.getFramework(mode)
val iosX64Framework = iosX64().binaries.getFramework(mode)
from(
iosArm64Framework,
iosX64Framework
)
destinationDir = buildDir.resolve("xcode-universal-framework")
group = "Universal framework"
description = "Builds a universal (fat) $mode framework"
dependsOn(iosArm64Framework.linkTask)
dependsOn(iosX64Framework.linkTask)
}
}
}
android {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(24)
targetSdkVersion(29)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
}
}
val packForXcode by tasks.creating(Sync::class) {
group = "build"
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val framework = kotlin.targets.getByName<KotlinNativeTarget>("ios").binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
val targetDir = File(buildDir, "xcode-frameworks")
from({ framework.outputDirectory })
into(targetDir)
}
tasks.getByName("build").dependsOn(packForXcode)
Any help is appreciated,
Thanks