diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml new file mode 100644 index 00000000..a9082bcd --- /dev/null +++ b/.github/workflows/platforms.yml @@ -0,0 +1,19 @@ +name: Verify Platforms Table + +on: + pull_request: + +permissions: + contents: read + +jobs: + verify-platforms-table: + name: Run Verification + runs-on: ubuntu-latest + steps: + - name: Checkout Sources + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Check Platforms Table + run: ./gradlew verifyPlatformTable --no-configuration-cache diff --git a/README.md b/README.md index 181b62fe..7732af78 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,8 @@ Build your RPC with already known language constructs and nothing more! First, create your RPC service and define some methods: ```kotlin -import kotlinx.rpc.RemoteService -import kotlinx.rpc.annotations.Rpc - @Rpc -interface AwesomeService : RemoteService { +interface AwesomeService { fun getNews(city: String): Flow suspend fun daysUntilStableRelease(): Int @@ -31,10 +28,7 @@ interface AwesomeService : RemoteService { ``` In your server code define how to respond by simply implementing the service: ```kotlin -class AwesomeServiceImpl( - val parameters: AwesomeParameters, - override val coroutineContext: CoroutineContext, -) : AwesomeService { +class AwesomeServiceImpl(val parameters: AwesomeParameters) : AwesomeService { override fun getNews(city: String): Flow { return flow { emit("Today is 23 degrees!") @@ -66,8 +60,8 @@ fun main() { } } - registerService { ctx -> - AwesomeServiceImpl(AwesomeParameters(false, null), ctx) + registerService { + AwesomeServiceImpl(AwesomeParameters(false, null)) } } } @@ -90,10 +84,8 @@ val service = rpcClient.withService() service.daysUntilStableRelease() -streamScoped { - service.getNews("KotlinBurg").collect { article -> - println(article) - } +service.getNews("KotlinBurg").collect { article -> + println(article) } ``` @@ -106,6 +98,33 @@ Check out our [getting started guide](https://kotlin.github.io/kotlinx-rpc) for To ensure that all IDE features of our compiler plugin work properly on IntelliJ-based IDEs, install the [Kotlin External FIR Support](https://plugins.jetbrains.com/plugin/26480-kotlin-external-fir-support?noRedirect=true) plugin. +## Kotlin compatibility +We support all stable Kotlin versions starting from 2.0.0: +- 2.0.0, 2.0.10, 2.0.20, 2.0.21 +- 2.1.0, 2.1.10, 2.1.20, 2.1.21 + +For a full compatibility checklist, +see [Versions](https://kotlin.github.io/kotlinx-rpc/versions.html). + +## Supported Platforms + +`kotlinx.rpc` is a KMP library, so we aim to support all available platforms. + +However, we are also a multi-module library, meaning that some modules may not support some platforms. + +Current high-level status: + +| Subsystem | Supported Platforms | Notes | +|-----------|--------------------------------------------------|---------------------------------------------------------------------------------------------| +| Core | Jvm, Js, WasmJs, WasmWasi, Apple, Linux, Windows | | +| kRPC | Jvm, Js, WasmJs, Apple, Linux, Windows | WasmWasi is blocked by [kotlin-logging](https://github.com/oshai/kotlin-logging/issues/433) | +| gRPC | Jvm | Projects with `kotlin("jvm")` **only**!
KMP support is in development | + +For more detailed module by module information, +check out our [platform support table](https://kotlin.github.io/kotlinx-rpc/platforms.html). + +For information about gRPC, see [gRPC Integration](#grpc-integration) + ### Gradle plugins `kotlinx.rpc` provides Gradle plugin `org.jetbrains.kotlinx.rpc.plugin` @@ -174,14 +193,6 @@ For more information on gRPC usage, see the [official documentation](https://kotlin.github.io/kotlinx-rpc/grpc-configuration.html). For a working example, see the [sample gRPC project](/samples/grpc-app). -## Kotlin compatibility -We support all stable Kotlin versions starting from 2.0.0: -- 2.0.0, 2.0.10, 2.0.20, 2.0.21 -- 2.1.0, 2.1.10, 2.1.20, 2.1.21 - -For a full compatibility checklist, -see [Versions](https://kotlin.github.io/kotlinx-rpc/versions.html). - ## JetBrains Product `kotlinx.rpc` is an official [JetBrains](https://jetbrains.com) product and is primarily developed by the team at JetBrains, with diff --git a/build.gradle.kts b/build.gradle.kts index a1112d53..74f2a63d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,9 @@ import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion import util.configureApiValidation import util.configureNpm import util.configureProjectReport +import util.registerDumpPlatformTableTask import util.libs +import util.registerVerifyPlatformTableTask plugins { alias(libs.plugins.serialization) apply false @@ -21,6 +23,9 @@ configureProjectReport() configureNpm() configureApiValidation() +registerDumpPlatformTableTask() +registerVerifyPlatformTableTask() + val kotlinVersion = rootProject.libs.versions.kotlin.lang.get() val kotlinCompiler = rootProject.libs.versions.kotlin.compiler.get() diff --git a/docs/pages/kotlinx-rpc/rpc.tree b/docs/pages/kotlinx-rpc/rpc.tree index 979360ba..aab8e867 100644 --- a/docs/pages/kotlinx-rpc/rpc.tree +++ b/docs/pages/kotlinx-rpc/rpc.tree @@ -39,6 +39,7 @@ + diff --git a/docs/pages/kotlinx-rpc/topics/0-8-0.topic b/docs/pages/kotlinx-rpc/topics/0-8-0.topic index b61dfafd..492c56f5 100644 --- a/docs/pages/kotlinx-rpc/topics/0-8-0.topic +++ b/docs/pages/kotlinx-rpc/topics/0-8-0.topic @@ -222,6 +222,29 @@ +
  • +

    + RpcCall changed parameter name and type for function's data: +

    + + + class RpcCall( + val descriptor: RpcServiceDescriptor<*>, + val callableName: String, + val data: Any?, + val serviceId: Long, + ) + + + class RpcCall( + val descriptor: RpcServiceDescriptor<*>, + val callableName: String, + val parameters: Array<Any?>, + val serviceId: Long, + ) + + +
  • For Ktor, HttpClient.rpc extension function is now non-suspendable. diff --git a/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic b/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic index be8df1ed..75888d10 100644 --- a/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic +++ b/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic @@ -10,11 +10,11 @@

    @Rpc - interface MyService : RemoteService + interface MyService class MyServiceImpl : MyService - fun <T : RemoteService> withService() {} + fun <T> withService() {}

    The compiler can't guarantee that the passed type parameter is the one for which the code generation was run: diff --git a/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic b/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic index d747c405..e28e765e 100644 --- a/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic +++ b/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic @@ -87,7 +87,7 @@ ) @Rpc - interface ImageService : RemoteService { + interface ImageService { suspend fun processImage(url: String): ProcessedImage } diff --git a/docs/pages/kotlinx-rpc/topics/platforms.topic b/docs/pages/kotlinx-rpc/topics/platforms.topic new file mode 100644 index 00000000..1511fbaa --- /dev/null +++ b/docs/pages/kotlinx-rpc/topics/platforms.topic @@ -0,0 +1,180 @@ + + + + + + +

    + kotlinx.rpc is a KMP library, so we aim to support all available platforms. + + However, we are also a multi-module library, meaning that some modules may not support some platforms. + + Current high-level status: +

    + + + + + + + + + + + + + + + + + + + + + +
    SubsystemSupported PlatformsNotes
    CoreJvm, Js, WasmJs, WasmWasi, Apple, Linux, Windows
    kRPCJvm, Js, WasmJs, Apple, Linux, Windows + WasmWasi is blocked by kotlin-logging. +
    gRPCJvm +

    + Projects with kotlin("jvm") only! +

    +

    + KMP support is in development +

    +
    + +

    + The following table contains a list of all published modules and their supported platforms: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ModuleJvmJsWasmNative
    corejvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • wasmWasi
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm32
  • watchosArm64
  • watchosDeviceArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • utilsjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • wasmWasi
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm32
  • watchosArm64
  • watchosDeviceArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-clientjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-corejvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-loggingjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-serverjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-testjvm
  • browser
  • node
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-ktor-clientjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-ktor-corejvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-ktor-serverjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-serialization-cborjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-serialization-corejvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-serialization-jsonjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • krpc-serialization-protobufjvm
  • browser
  • node
  • wasmJs
  • browser
  • d8
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • +

    + We are working on making the library more portable so that it can be used on more platforms. +

    + diff --git a/docs/pages/kotlinx-rpc/topics/strict-mode.topic b/docs/pages/kotlinx-rpc/topics/strict-mode.topic index 33e66068..8c3112a0 100644 --- a/docs/pages/kotlinx-rpc/topics/strict-mode.topic +++ b/docs/pages/kotlinx-rpc/topics/strict-mode.topic @@ -20,7 +20,7 @@

    Deprecation level: ERROR

    @Rpc - interface Service : RemoteService { + interface Service { suspend fun old(): StateFlow<Int> // deprecated suspend fun new(): Flow<Int> // use .stateIn on the client side @@ -32,7 +32,7 @@

    Deprecation level: ERROR

    @Rpc - interface Service : RemoteService { + interface Service { val old: Flow<Int> // deprecated suspend fun new(): Flow<Int> // store flow locally @@ -43,7 +43,7 @@

    Deprecation level: ERROR

    @Rpc - interface Service : RemoteService { + interface Service { suspend fun old(): Flow<Flow<Int>> // deprecated // no particular alternative, depends on the use case @@ -57,7 +57,7 @@ data class SpotifyWrapped(val myMusicFlow: Flow<Rap>, val extra: Data) @Rpc - interface Service : RemoteService { + interface Service { suspend fun old(): SpotifyWrapped // deprecated // one should consider message delivery order when calling these @@ -73,7 +73,7 @@ data class SpotifyWrapped(val extra: Data) @Rpc - interface Service : RemoteService { + interface Service { suspend fun old(): Flow<SpotifyWrapped> // deprecated fun new(): Flow<SpotifyWrapped> @@ -102,7 +102,7 @@

    @Rpc - interface Service : RemoteService { + interface Service { suspend fun oldClient(flow: Flow<Int>) suspend fun oldServer(): Flow<Int> } @@ -122,7 +122,7 @@

    @Rpc - interface Service : RemoteService { + interface Service { suspend fun newClient(flow: Flow<Int>) fun newServer(): Flow<Int> } diff --git a/gradle-conventions/common/src/main/kotlin/util/OptionalProperty.kt b/gradle-conventions/common/src/main/kotlin/util/OptionalProperty.kt index 4542b515..845c0d0e 100644 --- a/gradle-conventions/common/src/main/kotlin/util/OptionalProperty.kt +++ b/gradle-conventions/common/src/main/kotlin/util/OptionalProperty.kt @@ -6,24 +6,43 @@ package util import org.gradle.api.Project import kotlin.reflect.KProperty +import kotlin.text.replaceFirstChar + +class OptionalProperty(private val target: Project, private val subpaths: Array) { + private var cachedValue: Boolean? = null -class OptionalProperty(private val target: Project) { operator fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { - return getValue("kotlinx.rpc.${property.name}") + return cachedValue ?: target.getValue(property.name, subpaths).also { cachedValue = it } } +} + +private fun Project.getValue(propertyName: String, subpaths: Array): Boolean { + val subpathProperty = subpaths + .joinToString(".", postfix = ".") + .takeIf { it != "." && it.isNotEmpty() } + ?: "" + + val subpathCamelCase = subpaths + .joinToString("") { it.replaceFirstChar(Char::titlecase) } + .replaceFirstChar { it.lowercase() } + + val name = propertyName + .removePrefix(subpathCamelCase) + .replaceFirstChar { it.lowercase() } + + val fullName = "kotlinx.rpc.$subpathProperty$name" - fun getValue(propName: String): Boolean { - return when { - target.hasProperty(propName) -> (target.properties[propName] as String).toBoolean() - else -> false - } + return when { + hasProperty(fullName) -> (properties[fullName] as? String)?.toBooleanStrictOrNull() + ?: error("Invalid value for '$fullName' property: ${properties[fullName]}") + else -> false } } -fun Project.optionalProperty(): OptionalProperty { - return OptionalProperty(this) +fun Project.optionalProperty(vararg subpaths: String): OptionalProperty { + return OptionalProperty(this, subpaths) } -fun Project.optionalProperty(name: String): Boolean { - return OptionalProperty(this).getValue(name) +fun Project.optionalPropertyValue(name: String, vararg subpaths: String): Boolean { + return getValue(name, subpaths) } diff --git a/gradle-conventions/common/src/main/kotlin/util/ProjectKotlinConfig.kt b/gradle-conventions/common/src/main/kotlin/util/ProjectKotlinConfig.kt index 8028f1db..6e5724fc 100644 --- a/gradle-conventions/common/src/main/kotlin/util/ProjectKotlinConfig.kt +++ b/gradle-conventions/common/src/main/kotlin/util/ProjectKotlinConfig.kt @@ -35,12 +35,6 @@ private fun loadTargetsSinceKotlinLookupTable(rootDir: String): Map @@ -75,7 +77,7 @@ class ProjectKotlinConfig( !kotlinMasterBuild && targetFunction.parameters.size == 1 && isIncluded( targetName = targetFunction.name, lookupTable = nativeLookup, - ) + ) && !optionalPropertyValue(targetFunction.name, "exclude") }.map { function -> function.call(kmp) as KotlinTarget } @@ -83,21 +85,9 @@ class ProjectKotlinConfig( fun Project.withKotlinConfig(configure: ProjectKotlinConfig.() -> Unit) { val kotlinVersion: KotlinVersion by extra - val excludeJvm: Boolean by optionalProperty() - val excludeJs: Boolean by optionalProperty() - val excludeWasmJs: Boolean by optionalProperty() - val excludeWasmJsD8: Boolean by optionalProperty() - val excludeWasmWasi: Boolean by optionalProperty() - val excludeNative: Boolean by optionalProperty() ProjectKotlinConfig( project = project, kotlinVersion = kotlinVersion, - jvm = !excludeJvm, - js = !excludeJs, - wasmJs = !excludeWasmJs, - wasmJsD8 = !excludeWasmJsD8, - wasmWasi = !excludeWasmWasi, - native = !excludeNative, ).configure() } diff --git a/gradle-conventions/common/src/main/kotlin/util/platformTable.kt b/gradle-conventions/common/src/main/kotlin/util/platformTable.kt new file mode 100644 index 00000000..7078cc15 --- /dev/null +++ b/gradle-conventions/common/src/main/kotlin/util/platformTable.kt @@ -0,0 +1,309 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package util + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.the +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetType +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmJsTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmWasiTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget +import java.io.File +import java.nio.file.Files + +private val PLATFORM_TOPIC_PATH = File("docs/pages/kotlinx-rpc/topics/platforms.topic") + +private const val TABLE_START_TAG = "PLATFORMS_TABLE_START" +private const val TABLE_END_TAG = "PLATFORMS_TABLE_END" + +enum class TableColumn { + Jvm, Js, Wasm, Native; +} + +sealed interface PlatformTarget { + val name: String + val subtargets: List + + class Group( + override val name: String, + override val subtargets: List, + ) : PlatformTarget + + class LeafTarget(override val name: String) : PlatformTarget { + override val subtargets: List = emptyList() + } +} + +sealed interface PlatformsDescription { + object None : PlatformsDescription + + object JvmOnly : PlatformsDescription + + class Kmp(val platforms: Map) : PlatformsDescription +} + +abstract class DumpPlatformsTask : DefaultTask() { + @get:InputFile + abstract val input: Property + + @get:OutputFile + abstract val output: Property + + @TaskAction + fun dump() { + val input = input.get() + if (!input.exists()) { + throw GradleException("File ${input.absolutePath} doesn't exist") + } + + val output = output.get() + if (!output.exists()) { + Files.createFile(output.toPath()) + } + + val modules = collectPlatforms() + + updateTable(input, output, modules) + } + + private fun collectPlatforms(): Map { + return project.rootProject.allprojects.mapNotNull { subproject -> + if (!subproject.isPublicModule) { + return@mapNotNull null + } + + val platforms: PlatformsDescription = when { + subproject.plugins.hasPlugin(KOTLIN_JVM_PLUGIN_ID) -> { + PlatformsDescription.JvmOnly + } + + subproject.plugins.hasPlugin(KOTLIN_MULTIPLATFORM_PLUGIN_ID) -> { + platformsDescriptionKmp(subproject) + } + + else -> PlatformsDescription.None + } + + subproject.name to platforms + }.toMap() + } + + private fun platformsDescriptionKmp(subproject: Project): PlatformsDescription.Kmp { + val mapped = subproject + .the() + .targets + .groupBy { target -> + target.platformType + } + .mapValues { (platform, targets) -> + when (platform) { + KotlinPlatformType.jvm -> { + PlatformTarget.LeafTarget(TableColumn.Jvm.name.lowercase()) + } + + KotlinPlatformType.js -> { + val subtargets = targets.jsOrWasmSubTargets("js", subproject) { + it is KotlinJsIrTarget + } + + PlatformTarget.Group(TableColumn.Js.name, subtargets) + } + + KotlinPlatformType.wasm -> { + val jsSubtargets = targets.jsOrWasmSubTargets("wasmJs", subproject) { + it is KotlinWasmJsTargetDsl && it.wasmTargetType == KotlinWasmTargetType.JS + } + + val wasiSubtargets = targets.jsOrWasmSubTargets("wasmWasi", subproject) { + it is KotlinWasmWasiTargetDsl && it.wasmTargetType == KotlinWasmTargetType.WASI + } + + val wasmSubtargets = listOfNotNull( + PlatformTarget.Group("wasmJs", jsSubtargets).takeIf { jsSubtargets.isNotEmpty() }, + PlatformTarget.Group("wasmWasi", wasiSubtargets).takeIf { wasiSubtargets.isNotEmpty() }, + ) + + PlatformTarget.Group(TableColumn.Wasm.name, wasmSubtargets) + } + + KotlinPlatformType.native -> { + PlatformTarget.Group( + name = TableColumn.Native.name, + subtargets = listOf( + PlatformTarget.Group( + name = "apple", + subtargets = listOf( + targets.nativeGroup("ios"), + targets.nativeGroup("macos"), + targets.nativeGroup("watchos"), + targets.nativeGroup("tvos"), + ), + ), + targets.nativeGroup("linux"), + targets.nativeGroup("windows", "mingw"), + ) + ) + } + + KotlinPlatformType.common, KotlinPlatformType.androidJvm -> null + } + } + .mapNotNull { (platform, target) -> target?.let { platform to it } } + .toMap() + + return PlatformsDescription.Kmp(mapped) + } + + private fun List.jsOrWasmSubTargets( + name: String, + project: Project, + condition: (KotlinTarget) -> Boolean, + ): List { + val foundTargets = filter(condition) + .filterIsInstance() + + if (foundTargets.isEmpty()) { + return emptyList() + } + + if (foundTargets.size > 1) { + error("Multiple $name targets are not supported (project ${project.name})") + } + + val jsSubtargets = foundTargets.single().subTargets.map { subTargetWithBinary -> + PlatformTarget.LeafTarget(subTargetWithBinary.name) + } + + return jsSubtargets + } + + private fun List.nativeGroup(name: String, prefix: String = name): PlatformTarget.Group { + return PlatformTarget.Group( + name = name, + subtargets = filter { it.name.startsWith(prefix) }.map { PlatformTarget.LeafTarget(it.name) }, + ) + } + + private fun updateTable(input: File, output: File, modules: Map) { + val original = input.readLines(Charsets.UTF_8) + val tableStart = original.indexOfFirst { it.contains(TABLE_START_TAG) } + val tableEnd = original.indexOfFirst { it.contains(TABLE_END_TAG) } + + if (tableStart == -1 || tableEnd == -1) { + throw GradleException("Table start and end tags are not found in the file ${input.absolutePath}") + } + + val newLines = modules.mapNotNull { (moduleName, description) -> + if (description == PlatformsDescription.None) { + return@mapNotNull null + } + + buildString { + fun cell(text: String) { + appendLine("$text") + } + + fun writeRecursive(target: PlatformTarget?, topLevel: Boolean): String = buildString { + when (target) { + null -> append("-") + + is PlatformTarget.LeafTarget -> { + append(target.name) + } + + is PlatformTarget.Group -> { + if (!topLevel) { + append(target.name) + } + append("") + target.subtargets.forEach { subtarget -> + append("
  • ") + append(writeRecursive(subtarget, topLevel = false)) + append("
  • ") + } + append("") + } + } + } + + appendLine("") + + cell(moduleName) + + when (description) { + is PlatformsDescription.JvmOnly -> { + cell("Jvm Only") + cell("-") + cell("-") + cell("-") + } + + is PlatformsDescription.Kmp -> { + cell(writeRecursive(description.platforms[KotlinPlatformType.jvm], topLevel = true)) + cell(writeRecursive(description.platforms[KotlinPlatformType.js], topLevel = true)) + cell(writeRecursive(description.platforms[KotlinPlatformType.wasm], topLevel = true)) + cell(writeRecursive(description.platforms[KotlinPlatformType.native], topLevel = true)) + } + + is PlatformsDescription.None -> { + error("Unexpected None platforms description for module '$moduleName'") + } + } + + appendLine("") + } + } + + output.bufferedWriter(Charsets.UTF_8).use { writer -> + original.subList(0, tableStart + 1).forEach { line -> + writer.appendLine(line) + } + newLines.forEach { line -> + writer.appendLine(line) + } + original.subList(tableEnd, original.size).forEach { line -> + writer.appendLine(line) + } + } + } +} + +fun Project.registerDumpPlatformTableTask() { + tasks.register("dumpPlatformTable") { + input.set(PLATFORM_TOPIC_PATH) + output.set(PLATFORM_TOPIC_PATH) + } +} + +fun Project.registerVerifyPlatformTableTask() { + tasks.register("verifyPlatformTable") { + val tempFile = Files.createTempFile("platform-table-temp", ".topic").toFile() + + input.set(PLATFORM_TOPIC_PATH) + output.set(tempFile) + + doLast { + val input = input.get() + val output = output.get() + + if (input.readText() != output.readText()) { + throw GradleException( + "Platform table is not up-to-date. " + + "Run `./gradlew dumpPlatformTable --no-configuration-cache` to update it." + ) + } + } + } +} diff --git a/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts b/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts index 75bd4979..863f9c8b 100644 --- a/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts +++ b/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts @@ -2,6 +2,7 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import org.gradle.kotlin.dsl.registering import util.* val isGradlePlugin = project.name == "gradle-plugin" @@ -153,8 +154,8 @@ val sonatypeRepositoryUri: String? return "https://oss.sonatype.org/service/local/staging/deployByRepositoryId/$repositoryId" } -fun configureEmptyJavadocArtifact(): org.gradle.jvm.tasks.Jar { - val javadocJar by project.tasks.creating(Jar::class) { +fun configureEmptyJavadocArtifact(): TaskProvider { + val javadocJar by project.tasks.registering(Jar::class) { archiveClassifier.set("javadoc") // contents are deliberately left empty // https://central.sonatype.org/publish/requirements/#supply-javadoc-and-sources diff --git a/krpc/gradle.properties b/krpc/gradle.properties index ae9b5585..b41a299a 100644 --- a/krpc/gradle.properties +++ b/krpc/gradle.properties @@ -3,4 +3,6 @@ # # https://github.com/oshai/kotlin-logging/issues/433 -kotlinx.rpc.excludeWasmWasi=true +kotlinx.rpc.exclude.wasmWasi=true +kotlinx.rpc.exclude.watchosArm32=true +kotlinx.rpc.exclude.watchosDeviceArm64=true diff --git a/krpc/krpc-test/gradle.properties b/krpc/krpc-test/gradle.properties index 825c4ae9..db06764d 100644 --- a/krpc/krpc-test/gradle.properties +++ b/krpc/krpc-test/gradle.properties @@ -3,4 +3,4 @@ # # tests fail with some obscure reason -kotlinx.rpc.excludeWasmJsD8=true +kotlinx.rpc.exclude.wasmJsD8=true diff --git a/versions-root/targets-since-kotlin-lookup.json b/versions-root/targets-since-kotlin-lookup.json index 239c0e33..6248a73d 100644 --- a/versions-root/targets-since-kotlin-lookup.json +++ b/versions-root/targets-since-kotlin-lookup.json @@ -5,7 +5,7 @@ "wasmJs": "*", "wasmWasi": "*", - "mingwX64": "-", + "mingwX64": "*", "linuxX64": "*", "linuxArm64": "*", @@ -15,15 +15,20 @@ "iosSimulatorArm64": "*", "watchosX64": "*", - "watchosArm32": "-", + "watchosArm32": "*", "watchosArm64": "*", "watchosSimulatorArm64": "*", - "watchosDeviceArm64": "-", + "watchosDeviceArm64": "*", "tvosX64": "*", "tvosArm64": "*", - "tvosSimulatorArm64": "-", + "tvosSimulatorArm64": "*", "macosX64": "*", - "macosArm64": "*" + "macosArm64": "*", + + "androidNativeArm32": "-", + "androidNativeArm64": "-", + "androidNativeX86": "-", + "androidNativeX64": "-" }