Skip to content

[WIP] Re-add Kotlin and Scala support #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,20 @@ jobs:
**/build/reports/
**/build/test-results/

# test-sbt:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Set up JDK 21
# uses: actions/setup-java@v4
# with:
# cache: 'sbt'
# java-version: 21
# distribution: 'temurin'
# - name: Setup Gradle
# uses: gradle/actions/setup-gradle@v3
# - name: Setup sbt
# uses: sbt/setup-sbt@v1
# - name: Build and test
# run: sbt -v publishLocalGradleDependencies ++test
# working-directory: ./tasks-scala
test-sbt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
cache: 'sbt'
java-version: 21
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Setup sbt
uses: sbt/setup-sbt@v1
- name: Build and test
run: sbt -v publishLocalGradleDependencies ++test
working-directory: ./tasks-scala
19 changes: 19 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
val projectVersion = property("project.version").toString()

plugins {
id("org.jetbrains.dokka")
id("com.github.ben-manes.versions")
}

repositories {
mavenCentral()
}

buildscript {
dependencies {
classpath("org.jetbrains.dokka:dokka-base:2.0.0")
// classpath("org.jetbrains.dokka:kotlin-as-java-plugin:2.0.0")
}
}

//dokka {
// dokkaPublications.html {
// outputDirectory.set(rootDir.resolve("build/dokka"))
// outputDirectory.set(file("build/dokka"))
// }
//}

tasks.dokkaHtmlMultiModule {
outputDirectory.set(file("build/dokka"))
}

tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
fun isNonStable(version: String): Boolean {
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ kotlin.code.style=official

# TO BE modified whenever a new version is released
project.version=0.0.3

# https://kotlinlang.org/docs/dokka-migration.html#sync-your-project-with-gradle
org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
rootProject.name = "tasks"

include("tasks-jvm")
include("tasks-kotlin")
include("tasks-kotlin-coroutines")
include("tasks-scala")

pluginManagement {
repositories {
Expand Down
9 changes: 9 additions & 0 deletions tasks-kotlin-coroutines/api/tasks-kotlin-coroutines.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public final class org/funfix/tasks/kotlin/CoroutinesJvmKt {
public static final fun fromSuspended (Lorg/funfix/tasks/kotlin/Task$Companion;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun fromSuspended$default (Lorg/funfix/tasks/kotlin/Task$Companion;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun runSuspended (Lorg/funfix/tasks/jvm/Task;Ljava/util/concurrent/Executor;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun runSuspended$default (Lorg/funfix/tasks/jvm/Task;Ljava/util/concurrent/Executor;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public static final fun runSuspended-A-R0woo (Lorg/funfix/tasks/jvm/Task;Ljava/util/concurrent/Executor;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun runSuspended-A-R0woo$default (Lorg/funfix/tasks/jvm/Task;Ljava/util/concurrent/Executor;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

70 changes: 70 additions & 0 deletions tasks-kotlin-coroutines/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)

import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode

plugins {
id("tasks.kmp-project")
}

mavenPublishing {
pom {
name = "Tasks / Kotlin Coroutines"
description = "Integration with Kotlin's Coroutines"
}
}

kotlin {
sourceSets {
val commonMain by getting {
compilerOptions {
explicitApi = ExplicitApiMode.Strict
allWarningsAsErrors = true
}

dependencies {
implementation(project(":tasks-kotlin"))
implementation(libs.kotlinx.coroutines.core)
}
}

val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}

val jvmMain by getting {
compilerOptions {
explicitApi = ExplicitApiMode.Strict
allWarningsAsErrors = true
}

dependencies {
implementation(project(":tasks-jvm"))
implementation(project(":tasks-kotlin"))
implementation(libs.kotlinx.coroutines.core)
}
}

val jvmTest by getting {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}

val jsMain by getting {
compilerOptions {
explicitApi = ExplicitApiMode.Strict
allWarningsAsErrors = true
}

dependencies {
implementation(project(":tasks-kotlin"))
implementation(libs.kotlinx.coroutines.core)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.funfix.tasks.kotlin

import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

/**
* Similar with `runBlocking`, however this is a "suspended" function,
* to be executed in the context of [kotlinx.coroutines].
*
* NOTES:
* - The [CoroutineDispatcher], made available via the "coroutine context", is
* used to execute the task, being passed to the task's implementation as an
* `Executor`.
* - The coroutine's cancellation protocol cooperates with that of [Task],
* so cancelling the coroutine will also cancel the task (including the
* possibility for back-pressuring on the fiber's completion after
* cancellation).
*
* @param executor is an override of the `Executor` to be used for executing
* the task. If `null`, the `Executor` will be derived from the
* `CoroutineDispatcher`
*/
public expect suspend fun <T> Task<T>.runSuspended(
executor: Executor? = null
): T

/**
* See documentation for [Task.runSuspended].
*/
public expect suspend fun <T> PlatformTask<out T>.runSuspended(
executor: Executor? = null
): T

/**
* Creates a [Task] from a suspended block of code.
*/
public expect suspend fun <T> Task.Companion.fromSuspended(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: suspend () -> T
): Task<T>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.funfix.tasks.kotlin

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.coroutineContext

/**
* Internal API: gets the current [CoroutineDispatcher] from the coroutine context.
*/
internal suspend fun currentDispatcher(): CoroutineDispatcher {
// Access the coroutineContext to get the ContinuationInterceptor
val continuationInterceptor = coroutineContext[ContinuationInterceptor]
return continuationInterceptor as? CoroutineDispatcher ?: Dispatchers.Default
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
@file:OptIn(DelicateCoroutinesApi::class)

package org.funfix.tasks.kotlin

import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resumeWithException

public actual suspend fun <T> PlatformTask<out T>.runSuspended(
executor: Executor?
): T = run {
val executorOrDefault = executor ?: buildExecutor(currentDispatcher())
suspendCancellableCoroutine { cont ->
val contCallback = cont.asCompletionCallback()
try {
val token = this.invoke(executorOrDefault, contCallback)
cont.invokeOnCancellation {
token.cancel()
}
} catch (e: Throwable) {
UncaughtExceptionHandler.rethrowIfFatal(e)
contCallback(Outcome.Failure(e))
}
}
}

internal fun buildExecutor(dispatcher: CoroutineDispatcher): Executor =
DispatcherExecutor(dispatcher)

internal fun buildCoroutineDispatcher(
@Suppress("UNUSED_PARAMETER") executor: Executor
): CoroutineDispatcher =
// Building this CoroutineDispatcher from an Executor is problematic, and there's no
// point in even trying on top of JS engines.
Dispatchers.Default

private class DispatcherExecutor(val dispatcher: CoroutineDispatcher) : Executor {
override fun execute(command: Runnable) {
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
dispatcher.dispatch(
EmptyCoroutineContext,
kotlinx.coroutines.Runnable { command.run() }
)
} else {
command.run()
}
}

override fun toString(): String =
dispatcher.toString()
}

internal fun <T> CancellableContinuation<T>.asCompletionCallback(): Callback<T> {
var isActive = true
return { outcome ->
if (outcome is Outcome.Failure) {
UncaughtExceptionHandler.rethrowIfFatal(outcome.exception)
}
if (isActive) {
isActive = false
when (outcome) {
is Outcome.Success ->
resume(outcome.value) { _, _, _ ->
// on cancellation?
}
is Outcome.Failure ->
resumeWithException(outcome.exception)
is Outcome.Cancellation ->
resumeWithException(kotlinx.coroutines.CancellationException())
}
} else if (outcome is Outcome.Failure) {
UncaughtExceptionHandler.logOrRethrow(outcome.exception)
}
}
}

/**
* Creates a [Task] from a suspended block of code.
*/
public actual suspend fun <T> Task.Companion.fromSuspended(
coroutineContext: CoroutineContext,
block: suspend () -> T
): Task<T> =
Task.fromAsync { executor, callback ->
val job = GlobalScope.launch(
buildCoroutineDispatcher(executor) + coroutineContext
) {
try {
val r = block()
callback(Outcome.Success(r))
} catch (e: Throwable) {
UncaughtExceptionHandler.rethrowIfFatal(e)
when (e) {
is CancellationException, is TaskCancellationException ->
callback(Outcome.Cancellation)
else ->
callback(Outcome.Failure(e))
}
}
}
Cancellable {
job.cancel()
}
}

public actual suspend fun <T> Task<T>.runSuspended(executor: Executor?): T =
asPlatform.runSuspended(executor)
Loading
Loading