[Gradle] Implement KotlinJvmTarget.mainRun (KotlinJvmRun)

This task will also automatically support the use case of
a 'carrier' task for the IDE. The IDE can use this task
to execute generic main methods, providing a -P<taskName>.mainClass
property

^KT-58661 Verification Pending
This commit is contained in:
Sebastian Sellmair
2023-05-24 15:16:03 +02:00
committed by Space Team
parent 432c781ff7
commit e0389a0a87
13 changed files with 517 additions and 5 deletions
@@ -0,0 +1,78 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.mpp
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
import kotlin.io.path.appendText
@MppGradlePluginTests
class MppJvmRunIT : KGPBaseTest() {
@GradleTest
fun `test jvmRun`(version: GradleVersion) {
project("mppRunJvm", version) {
build("jvmRun", "-PmainClass=JvmMainKt") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: JvmMain")
}
build("jvmRun", "-PmainClass=CommonMainKt") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: CommonMain")
}
// No mainClass provided!
buildAndFail("jvmRun")
/* Provided mainClass in buildscript */
projectPath.resolve("multiplatform").resolve("build.gradle.kts").appendText(
"""
kotlin {
jvm().mainRun {
mainClass.set("JvmMainKt")
}
}
""".trimIndent()
)
build("jvmRun") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: JvmMain")
}
/* Overwrite buildscript mainClass via property */
build("jvmRun", "-PmainClass=CommonMainKt") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: CommonMain")
}
}
}
@GradleTestVersions(minVersion = TestVersions.Gradle.G_7_4)
@GradleTest
fun `test - jvmRun - works with Gradle configuration cache`(version: GradleVersion) {
project("mppRunJvm", version, buildOptions = defaultBuildOptions.copy(configurationCache = true)) {
build("jvmRun", "-DmainClass=JvmMainKt") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: JvmMain")
assertConfigurationCacheStored()
}
build("jvmRun", "-DmainClass=CommonMainKt") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: CommonMain")
assertConfigurationCacheReused()
}
build("jvmRun", "-DmainClass=JvmMainKt") {
assertOutputContains("Jvm: OK!")
assertOutputContains("Executed: JvmMain")
assertConfigurationCacheReused()
}
}
}
}
@@ -0,0 +1,7 @@
plugins {
kotlin("jvm")
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
}
@@ -0,0 +1,11 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
object Jvm {
operator fun invoke() = runBlocking {
withContext(Dispatchers.Default) {
println("Jvm: OK!")
}
}
}
@@ -0,0 +1,10 @@
plugins {
kotlin("multiplatform")
}
kotlin {
jvm()
sourceSets.getByName("jvmMain").dependencies {
implementation(project(":jvm"))
}
}
@@ -0,0 +1,4 @@
fun main() {
Jvm.invoke()
println("Executed: CommonMain")
}
@@ -0,0 +1,4 @@
fun main() {
Jvm.invoke()
println("Executed: JvmMain")
}
@@ -0,0 +1,9 @@
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
}
}
include(":multiplatform")
include(":jvm")
@@ -178,4 +178,12 @@ object KotlinToolingDiagnostics {
""".trimIndent()
)
}
object KotlinJvmMainRunTaskConflict : ToolingDiagnosticFactory(WARNING) {
operator fun invoke(targetName: String, taskName: String) = build(
"""
Target '$targetName': Unable to create run task '$taskName' as there is already such a task registered
""".trimIndent()
)
}
}
@@ -6,10 +6,13 @@
package org.jetbrains.kotlin.gradle.plugin.mpp
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.AbstractKotlinTargetConfigurator
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset
abstract class KotlinOnlyTargetPreset<R : KotlinOnlyTarget<T>, T : KotlinCompilation<*>>(
protected val project: Project
protected val project: Project,
) : KotlinTargetPreset<R> {
protected abstract fun createKotlinTargetConfigurator(): AbstractKotlinTargetConfigurator<R>
@@ -18,6 +18,7 @@ import org.gradle.api.tasks.testing.Test
import org.gradle.jvm.tasks.Jar
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.internal.JavaSourceSetsAccessor
@@ -25,20 +26,69 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.copyAttributes
import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmRunDsl
import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmRunDslImpl
import org.jetbrains.kotlin.gradle.targets.jvm.tasks.registerMainRunTask
import org.jetbrains.kotlin.gradle.tasks.withType
import org.jetbrains.kotlin.gradle.utils.Future
import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation
import org.jetbrains.kotlin.gradle.utils.future
import org.jetbrains.kotlin.utils.addToStdlib.cast
import java.util.concurrent.Callable
import javax.inject.Inject
import kotlin.reflect.full.functions
abstract class KotlinJvmTarget @Inject constructor(
project: Project
project: Project,
) : KotlinOnlyTarget<KotlinJvmCompilation>(project, KotlinPlatformType.jvm),
KotlinTargetWithTests<JvmClasspathTestRunSource, KotlinJvmTestRun> {
override lateinit var testRuns: NamedDomainObjectContainer<KotlinJvmTestRun>
internal val mainRun: Future<KotlinJvmRunDslImpl?> = project.future { registerMainRunTask() }
/**
* ### ⚠️ KotlinJvmTarget 'mainRun' is experimental
* The [KotlinJvmTarget], by default, creates a 'run' task called {targetName}Run, which will allows simple
* execution of the targets 'main' code.
*
* e.g.
* ```kotlin
* // build.gradle.kts
* kotlin {
* jvm().mainRun {
* mainClass.set("FooKt")
* }
* }
*
* // src/jvmMain/Foo
* fun main() {
* println("Hello from foo")
* }
* ```
*
* will be executable using
* ```text
* ./gradlew jvmRun
* > "Hello from foo"
* ```
*
* ### Running a different 'mainClass' from CLI:
* The execution of the main code allows providing a different 'mainClass' via CLI. *
* It accepts System Properties and Gradle Properties. However, when Gradle Configuration Cache is used,
* System Properties are the preferred way.
*
* ```text
* ./gradlew jvmRun -DmainClass="BarKt"
* ^
* Will execute the 'src/jvmMain/kotlin/Bar' main method.
* ```
*/
@ExperimentalKotlinGradlePluginApi
fun mainRun(configure: KotlinJvmRunDsl.() -> Unit) = project.launch {
mainRun.await()?.configure()
}
var withJavaEnabled = false
private set
@@ -106,7 +156,7 @@ abstract class KotlinJvmTarget @Inject constructor(
private fun setupJavaSourceSetSourcesAndResources(
javaSourceSet: SourceSet,
compilation: KotlinJvmCompilation
compilation: KotlinJvmCompilation,
) {
javaSourceSet.java.setSrcDirs(listOf("src/${compilation.defaultSourceSet.name}/java"))
compilation.defaultSourceSet.kotlin.srcDirs(javaSourceSet.java.sourceDirectories)
@@ -153,7 +203,7 @@ abstract class KotlinJvmTarget @Inject constructor(
private fun setupDependenciesCrossInclusionForJava(
compilation: KotlinJvmCompilation,
javaSourceSet: SourceSet
javaSourceSet: SourceSet,
) {
// Make sure Kotlin compilation dependencies appear in the Java source set classpaths:
@@ -0,0 +1,162 @@
package org.jetbrains.kotlin.gradle.targets.jvm.tasks
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.TaskProvider
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
import org.jetbrains.kotlin.gradle.plugin.await
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics.KotlinJvmMainRunTaskConflict
import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnostic
import org.jetbrains.kotlin.gradle.plugin.internal.configurationTimePropertiesAccessor
import org.jetbrains.kotlin.gradle.plugin.internal.usedAtConfigurationTime
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
import org.jetbrains.kotlin.gradle.utils.property
import org.jetbrains.kotlin.tooling.core.UnsafeApi
/**
* Run task for executing Kotlin/Jvm code.
* Shall be configured using the [KotlinJvmRunDsl]
*
* ### IDE carrier task:
* This task can act as 'carrier' task for IDE run gutters:
* It accepts configurable 'mainClass' when a Gradle property is provided as
* `<taskName>.mainClass=...`
*
* e.g.
* ```
* ./gradlew runJvm // <- executes the mainClass that is configured by default by the user
* ./gradlew runJvm -PrunJvm.mainClass="FooKt" // <- executes FooKt.main
* ./gradlew runJvm -PmainClass="FooKt" // <- excutes FooKt.main
* ```
*/
@InternalKotlinGradlePluginApi
open class KotlinJvmRun : JavaExec()
@ExperimentalKotlinGradlePluginApi
interface KotlinJvmRunDsl {
/**
* ## See [JavaExec.mainClass]
*/
val mainClass: Property<String>
/**
* ## See [JavaExec.args]
*/
fun args(vararg args: Any)
/**
* ## See [JavaExec.args]
*/
fun args(args: Iterable<*>)
/**
* ## See [JavaExec.setArgs]
*/
fun setArgs(args: Iterable<*>)
/**
* ## See [JavaExec.classpath]
*/
fun classpath(vararg paths: Any)
/**
* ## See [JavaExec.setClasspath]
*/
fun setClasspath(classpath: FileCollection)
/**
* Adds the runtime classpath of the given [compilation] to this run task
*/
fun classpath(compilation: KotlinCompilation<*>)
}
internal suspend fun KotlinJvmTarget.registerMainRunTask(): KotlinJvmRunDslImpl? {
/* Await all inputs from user */
KotlinPluginLifecycle.Stage.FinaliseDsl.await()
val mainCompilation = compilations.findByName("main") ?: return null
val taskName = lowerCamelCaseName(targetName, "run")
if (taskName in project.tasks.names) {
/* Proper warning tbd */
project.reportDiagnostic(KotlinJvmMainRunTaskConflict(targetName = name, taskName = taskName))
return null
}
return registerKotlinJvmRun(taskName, mainCompilation)
}
private fun KotlinJvmTarget.registerKotlinJvmRun(taskName: String, compilation: KotlinJvmCompilation): KotlinJvmRunDslImpl {
val mainClass = project.objects.property<String>()
val taskProvider = project.tasks.register(taskName, KotlinJvmRun::class.java)
/* Convenience helper for telling older Gradle versions, that this provider is used at configuration time */
val configurationTimePropertiesAccessor = project.configurationTimePropertiesAccessor
fun <T> Provider<T>.usedAtConfigurationTime() = usedAtConfigurationTime(configurationTimePropertiesAccessor)
taskProvider.configure { task ->
task.group = "run"
task.description = "Jvm Run task for target '${compilation.target.name}' and compilation '${compilation.name}'. " +
"This task can act as carrier for the IDE to execute jvm based code"
/**
* See [KotlinJvmRun]: This task will respect the '<taskName>.mainClass' property over
* the DSL configuration
*/
task.mainClass.value(
project.providers.gradleProperty("$taskName.mainClass").usedAtConfigurationTime()
.orElse(project.providers.gradleProperty("mainClass").usedAtConfigurationTime())
.orElse(project.providers.systemProperty("$taskName.mainClass").usedAtConfigurationTime())
.orElse(project.providers.systemProperty("mainClass").usedAtConfigurationTime())
.orElse(mainClass).usedAtConfigurationTime()
)
}
@OptIn(UnsafeApi::class)
return KotlinJvmRunDslImpl(taskProvider, mainClass)
.also { dsl -> dsl.classpath(compilation) }
}
/**
* Use [registerKotlinJvmRun] to create an instance
*/
internal class KotlinJvmRunDslImpl @UnsafeApi constructor(
val task: TaskProvider<KotlinJvmRun>,
override val mainClass: Property<String>,
) : KotlinJvmRunDsl {
override fun args(vararg args: Any) {
task.configure { it.args(*args) }
}
override fun args(args: Iterable<*>) {
task.configure { it.args(args) }
}
override fun setArgs(args: Iterable<*>) {
task.configure { it.setArgs(args) }
}
override fun classpath(vararg paths: Any) {
task.configure { it.classpath(*paths) }
}
override fun setClasspath(classpath: FileCollection) {
task.configure { it.setClasspath(classpath) }
}
override fun classpath(compilation: KotlinCompilation<*>) {
task.configure {
it.classpath(compilation.output.allOutputs)
it.classpath(compilation.internal.configurations.runtimeDependencyConfiguration)
}
}
}
@@ -0,0 +1,165 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("FunctionName")
package org.jetbrains.kotlin.gradle.unitTests
import org.gradle.api.tasks.JavaExec
import org.jetbrains.kotlin.gradle.dependencyResolutionTests.mavenCentralCacheRedirector
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.configurationResult
import org.jetbrains.kotlin.gradle.plugin.launch
import org.jetbrains.kotlin.gradle.util.*
import kotlin.test.*
class KotlinJvmRunTest {
@Test
fun `test - simple jvm target`() = buildProjectWithMPP().runLifecycleAwareTest {
val kotlin = project.multiplatformExtension
assertNotNull(kotlin.jvm())
project.configurationResult.await()
val task = project.tasks.findByName("jvmRun") ?: fail("Missing 'jvmRun' run task")
if (task !is JavaExec) fail("Expected $task to implement '${JavaExec::class}'")
assertNoDiagnostics()
}
@Test
fun `test - mainClass - set via DSL`() = buildProjectWithMPP().runLifecycleAwareTest {
val kotlin = project.multiplatformExtension
val mainRun = assertNotNull(kotlin.jvm().mainRun.await())
val mainRunTask = mainRun.task.get()
assertNull(
mainRunTask.mainClass.orNull,
"Expected mainClass to be null: No mainClass set via DSL or property"
)
// Set mainClass via DSL
mainRun.mainClass.set("Foo")
assertEquals("Foo", mainRunTask.mainClass.orNull)
}
@Test
fun `test - args`() = buildProjectWithMPP().runLifecycleAwareTest {
val kotlin = project.multiplatformExtension
val task = assertNotNull(kotlin.jvm().mainRun.await()).task.get()
/* Set properties using 'args()' */
kotlin.jvm().mainRun {
args("first", "second")
args(listOf("third", "fourth"))
}
launch {
assertEquals(listOf("first", "second", "third", "fourth"), task.args)
}
/* Set property using .setArgs */
kotlin.jvm().mainRun {
setArgs(listOf("1", "2"))
}
launch {
assertEquals(listOf("1", "2"), task.args)
}
}
@Test
fun `test - classpath - contains main compilation output by default`() = buildProjectWithMPP().runLifecycleAwareTest {
repositories.mavenLocal()
repositories.mavenCentralCacheRedirector()
val kotlin = project.multiplatformExtension
val mainRunTask = assertNotNull(kotlin.jvm().mainRun.await()).task.get()
val mainCompilation = kotlin.jvm().compilations.main
configurationResult.await()
mainCompilation.output.allOutputs.files.ifEmpty { fail("Expected some file in 'allOutputs'") }
if (!mainRunTask.classpath.files.containsAll(mainCompilation.output.allOutputs.files)) {
fail("Missing output from main compilation in '$mainRunTask'")
}
}
@Test
fun `test - classpath - contains main compilation runtime dependencies`() = buildProjectWithMPP().runLifecycleAwareTest {
repositories.mavenLocal()
repositories.mavenCentralCacheRedirector()
val kotlin = project.multiplatformExtension
kotlin.jvm().compilations.main.defaultSourceSet.dependencies {
implementation(project.files("implementation.jar"))
compileOnly(project.files("compileOnly.jar"))
runtimeOnly(project.files("runtimeOnly.jar"))
}
configurationResult.await()
val mainRunTask = assertNotNull(kotlin.jvm().mainRun.await()?.task?.get())
if (project.file("implementation.jar") !in mainRunTask.classpath) {
fail("Missing file from 'implementation' scope")
}
if (project.file("runtimeOnly.jar") !in mainRunTask.classpath) {
fail("Missing file from 'runtimeOnly' scope")
}
if (project.file("compileOnly.jar") in mainRunTask.classpath) {
fail("Unexpected file from 'compileOnly' scope")
}
}
@Test
fun `test - classpath - add file`() = buildProjectWithMPP().runLifecycleAwareTest {
repositories.mavenLocal()
repositories.mavenCentralCacheRedirector()
val kotlin = project.multiplatformExtension
kotlin.jvm().mainRun { classpath(file("custom.jar")) }
configurationResult.await()
val task = assertNotNull(kotlin.jvm().mainRun.await()).task.get()
if (file("custom.jar") !in task.classpath.files) {
fail("Missing custom.jar in classpath")
}
if (task.classpath.files.size <= 1) {
fail("Expected more files in classpath than just custom.jar")
}
}
@Test
fun `test - setClasspath`() = buildProjectWithMPP().runLifecycleAwareTest {
repositories.mavenLocal()
repositories.mavenCentralCacheRedirector()
val kotlin = project.multiplatformExtension
kotlin.jvm().mainRun {
setClasspath(project.files("a.jar", "b.jar"))
}
project.configurationResult.await()
assertEquals(
project.files("a.jar", "b.jar").files,
kotlin.jvm().mainRun.await()?.task?.get()?.classpath?.files
)
}
@Test
fun `test - jvmRun task is already registered`() = buildProjectWithMPP().runLifecycleAwareTest {
val kotlin = multiplatformExtension
kotlin.jvm()
tasks.register("jvmRun")
configurationResult.await()
checkDiagnostics("jvmRunTask-conflict")
}
}
@@ -0,0 +1 @@
[KotlinJvmMainRunTaskConflict | WARNING] Target 'jvm': Unable to create run task 'jvmRun' as there is already such a task registered