diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/FrameworkTestArguments.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/FrameworkTestArguments.kt new file mode 100644 index 00000000000..f1280723421 --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/FrameworkTestArguments.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2010-2022 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 kotlin.test + +@Suppress("EnumEntryName") +internal enum class IgnoredTestSuitesReporting { + skip, reportAsIgnoredTest, reportAllInnerTestsAsIgnored +} + +internal class FrameworkTestArguments( + val includedQualifiers: List, + val includedClassMethods: List>, + val excludedQualifiers: List, + val excludedClassMethods: List>, + val ignoredTestSuites: IgnoredTestSuitesReporting, + val dryRun: Boolean +) { + companion object { + fun parse(args: List): FrameworkTestArguments { + var isInclude = false + var isExclude = false + + val includesClassMethods = mutableListOf>() + val includesQualifiers = mutableListOf() + val excludesClassMethods = mutableListOf>() + val excludesQualifiers = mutableListOf() + + fun addToIncludeOrExcludeList(argument: String) { + if (argument.isEmpty()) return + if (argument[0].let { it != it.lowercaseChar() }){ + val dotIndex = argument.indexOf('.') + val listToAdd = if (isInclude) includesClassMethods else excludesClassMethods + if (dotIndex == -1) { + listToAdd.add(argument to "*") + } else { + if (dotIndex < 1 || dotIndex >= argument.lastIndex) return + val className = argument.substring(0, dotIndex) + val methodName = argument.substring(dotIndex + 1) + listToAdd.add(className to methodName) + } + } else { + (if (isInclude) includesQualifiers else excludesQualifiers).add(argument) + } + } + + var ignoredTestSuites: IgnoredTestSuitesReporting = IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored + var isIgnoredTestSuites = false + var dryRun = false + + for (arg in args) { + if (isInclude || isExclude) { + for (splitArg in arg.split(',')) { + addToIncludeOrExcludeList(splitArg) + } + isInclude = false + isExclude = false + continue + } + + if (isIgnoredTestSuites) { + val value = IgnoredTestSuitesReporting.values().firstOrNull { it.name == arg } + if (value != null) { + ignoredTestSuites = value + } + isIgnoredTestSuites = false + continue + } + + when (arg) { + "--include" -> isInclude = true + "--exclude" -> isExclude = true + "--ignoredTestSuites" -> isIgnoredTestSuites = true + "--dryRun" -> dryRun = true + } + } + + if (includesClassMethods.isEmpty() && includesQualifiers.isEmpty()) { + includesQualifiers.add("*") + } + + return FrameworkTestArguments( + includedQualifiers = includesQualifiers, + includedClassMethods = includesClassMethods, + excludedQualifiers = excludesQualifiers, + excludedClassMethods = excludesClassMethods, + ignoredTestSuites = ignoredTestSuites, + dryRun = dryRun + ) + } + } +} \ No newline at end of file diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt index 05a31afae1c..416fc0a6f47 100644 --- a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt @@ -6,6 +6,12 @@ package kotlin.test import kotlin.test.FrameworkAdapter +import kotlin.math.abs + +@JsFun("() => (typeof arguments !== 'undefined' && typeof arguments.join !== 'undefined') ? arguments.join(' ') : '' ") +private external fun d8Arguments(): String +@JsFun("() => (typeof process != 'undefined' && typeof process.argv != 'undefined') ? process.argv.slice(2).join(' ') : ''") +private external fun nodeArguments(): String internal class TeamcityAdapter : FrameworkAdapter { private enum class MessageType(val type: String) { @@ -30,41 +36,153 @@ internal class TeamcityAdapter : FrameworkAdapter { ?.replace("]", "|]") ?: "" + private val flowId: String = "flowId='wasmTcAdapter${abs(hashCode())}'" + private fun MessageType.report(name: String) { - println("##teamcity[$type name='${name.tcEscape()}']") + println("##teamcity[$type name='${name.tcEscape()}' $flowId]") } private fun MessageType.report(name: String, e: Throwable) { if (this == MessageType.Failed) { - println("##teamcity[$type name='${name.tcEscape()}' message='${e.message.tcEscape()}' details='${e.stackTraceToString().tcEscape()}']") + println("##teamcity[$type name='${name.tcEscape()}' message='${e.message.tcEscape()}' details='${e.stackTraceToString().tcEscape()}' $flowId]") } else { - println("##teamcity[$type name='${name.tcEscape()}' text='${e.message.tcEscape()}' errorDetails='${e.stackTraceToString().tcEscape()}' status='ERROR']") + println("##teamcity[$type name='${name.tcEscape()}' text='${e.message.tcEscape()}' errorDetails='${e.stackTraceToString().tcEscape()}' status='ERROR' $flowId]") + } + } + + private val testArguments: FrameworkTestArguments by lazy { + val arguments = d8Arguments().takeIf { it.isNotEmpty() } ?: nodeArguments() + FrameworkTestArguments.parse(arguments.split(' ')) + } + + private fun runSuite(name: String, suiteFn: () -> Unit) { + MessageType.SuiteStarted.report(name) + try { + suiteFn() + MessageType.SuiteFinished.report(name) + } catch (e: Throwable) { + MessageType.SuiteFinished.report(name, e) + } + } + + private fun runIgnoredSuite(name: String, suiteFn: () -> Unit) { + MessageType.SuiteStarted.report(name) + suiteFn() + MessageType.SuiteFinished.report(name) + } + + private var isUnderIgnoredSuit: Boolean = false + private var qualifiedName: String = "" + private var className: String = "" + private inline fun enterIfIncluded(name: String, isSuit: Boolean, body: () -> Unit) { + if (name.isEmpty()) { + body() + return + } + + val oldQualifiedName = qualifiedName + val oldClassName = className + try { + qualifiedName = if (oldQualifiedName.isEmpty()) name else "$oldQualifiedName.$name" + className = name + if (isSuit) { + body() + } else { + val runTest = testArguments.run { + val included = + includedClassMethods.any { it.first == oldClassName && name.matched(it.second) } || + includedQualifiers.any { oldQualifiedName.matched(it) || qualifiedName.matched(it) } + included && + excludedClassMethods.none { it.first == oldClassName && name.matched(it.second) } && + excludedQualifiers.none { oldQualifiedName.matched(it) || qualifiedName.matched(it) } + } + if (runTest) { + body() + } + } + } finally { + qualifiedName = oldQualifiedName + className = oldClassName } } override fun suite(name: String, ignored: Boolean, suiteFn: () -> Unit) { - MessageType.SuiteStarted.report(name) + if (isUnderIgnoredSuit) { + runIgnoredSuite(name, suiteFn) + return + } + if (!ignored) { - try { - suiteFn() - MessageType.SuiteFinished.report(name) - } catch (e: Throwable) { - MessageType.SuiteFinished.report(name, e) + enterIfIncluded(name, true) { + runSuite(name, suiteFn) + } + } else { + when (testArguments.ignoredTestSuites) { + IgnoredTestSuitesReporting.reportAsIgnoredTest -> { + MessageType.Ignored.report(name) + } + IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored -> { + var oldIsUnderIgnoredSuit = isUnderIgnoredSuit + isUnderIgnoredSuit = true + try { + runIgnoredSuite(name, suiteFn) + } finally { + isUnderIgnoredSuit = oldIsUnderIgnoredSuit + } + } + IgnoredTestSuitesReporting.skip -> { } } } } override fun test(name: String, ignored: Boolean, testFn: () -> Any?) { + if (isUnderIgnoredSuit) { + MessageType.Ignored.report(name) + return + } + if (ignored) { MessageType.Ignored.report(name) - } else { + return + } + + enterIfIncluded(name, false) { try { MessageType.Started.report(name) - testFn() + if (!testArguments.dryRun) { + testFn() + } } catch (e: Throwable) { MessageType.Failed.report(name, e) } MessageType.Finished.report(name) } } +} + +private fun String.matched(prefix: String, startSourceIndex: Int = 0, startPrefixIndex: Int = 0): Boolean { + var sourceIndex = startSourceIndex + var prefixIndex = startPrefixIndex + + if (prefixIndex == prefix.lastIndex && prefix[prefixIndex] == '*') return true + if (sourceIndex > this.lastIndex) return false + + while (prefixIndex <= prefix.lastIndex && sourceIndex <= this.lastIndex) { + val currentSearchSymbol = prefix[prefixIndex] + if (currentSearchSymbol == '*') { + if (prefixIndex == prefix.lastIndex) return true + val searchSymbolAfterStar = prefix[prefixIndex + 1] + val foundIndexOfSymbolAfterStar = this.indexOf(searchSymbolAfterStar, sourceIndex) + if (foundIndexOfSymbolAfterStar == -1) return false + if (this.matched(prefix, foundIndexOfSymbolAfterStar + 1, prefixIndex)) return true + sourceIndex = foundIndexOfSymbolAfterStar + } else { + if (currentSearchSymbol != this[sourceIndex]) return false + sourceIndex++ + } + prefixIndex++ + } + + return sourceIndex > this.lastIndex && + (prefixIndex > prefix.lastIndex || (prefixIndex == prefix.lastIndex && prefix[prefixIndex] == '*')) } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetContainerWithWasmPresetFunctions.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetContainerWithWasmPresetFunctions.kt index c8051a4202d..553ebd00b04 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetContainerWithWasmPresetFunctions.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinTargetContainerWithWasmPresetFunctions.kt @@ -8,16 +8,18 @@ package org.jetbrains.kotlin.gradle.dsl import groovy.lang.Closure import org.gradle.util.ConfigureUtil import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmTargetDsl import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTargetPreset +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinWasmTargetPreset interface KotlinTargetContainerWithWasmPresetFunctions : KotlinTargetContainerWithPresetFunctions { fun wasm( name: String = "wasm", - configure: KotlinJsTargetDsl.() -> Unit = { } + configure: KotlinWasmTargetDsl.() -> Unit = { } ): KotlinJsTargetDsl = configureOrCreate( name, - presets.getByName("wasm") as KotlinJsIrTargetPreset, + presets.getByName("wasm") as KotlinWasmTargetPreset, configure ) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt index 48e822464ab..9badcb76c5f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt @@ -37,6 +37,7 @@ import org.jetbrains.kotlin.gradle.plugin.sources.checkSourceSetVisibilityRequir import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService import org.jetbrains.kotlin.gradle.scripting.internal.ScriptingGradleSubplugin import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTargetPreset +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinWasmTargetPreset import org.jetbrains.kotlin.gradle.targets.native.tasks.artifact.registerKotlinArtifactsExtension import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool import org.jetbrains.kotlin.gradle.tasks.locateTask @@ -195,14 +196,13 @@ class KotlinMultiplatformPlugin : Plugin { with(project.multiplatformExtension.presets) { add(KotlinJvmTargetPreset(project)) add(KotlinJsTargetPreset(project).apply { irPreset = null }) - add(KotlinJsIrTargetPreset(project, isWasm = false).apply { mixedMode = false }) + add(KotlinJsIrTargetPreset(project).apply { mixedMode = false }) add( KotlinJsTargetPreset(project).apply { - irPreset = KotlinJsIrTargetPreset(project, isWasm = false) - .apply { mixedMode = true } + irPreset = KotlinJsIrTargetPreset(project).apply { mixedMode = true } } ) - add(KotlinJsIrTargetPreset(project, isWasm = true).apply { mixedMode = false }) + add(KotlinWasmTargetPreset(project)) add(KotlinAndroidTargetPreset(project)) add(KotlinJvmWithJavaTargetPreset(project)) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt index 23f26d6993e..fed4ab70f53 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/KotlinJsTarget.kt @@ -16,10 +16,7 @@ import org.jetbrains.kotlin.gradle.plugin.AbstractKotlinTargetConfigurator.Compa import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.LEGACY import org.jetbrains.kotlin.gradle.plugin.mpp.* -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBrowserDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsNodeDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetContainerDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.dsl.* import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsBinaryContainer import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget import org.jetbrains.kotlin.gradle.targets.js.subtargets.KotlinBrowserJs @@ -29,7 +26,6 @@ import org.jetbrains.kotlin.gradle.tasks.locateTask import org.jetbrains.kotlin.gradle.testing.internal.KotlinTestReport import org.jetbrains.kotlin.gradle.testing.testTaskName import org.jetbrains.kotlin.gradle.utils.dashSeparatedName -import org.jetbrains.kotlin.gradle.utils.listProperty import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName import org.jetbrains.kotlin.gradle.utils.setProperty import javax.inject.Inject @@ -143,6 +139,7 @@ constructor( private val propertiesProvider = PropertiesProvider(project) + //Browser private val browserLazyDelegate = lazy { project.objects.newInstance(KotlinBrowserJs::class.java, this).also { it.configure() @@ -170,6 +167,7 @@ constructor( irTarget?.browser(body) } + //node.js private val nodejsLazyDelegate = lazy { project.objects.newInstance(KotlinNodeJs::class.java, this).also { it.configure() diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Env.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Env.kt new file mode 100644 index 00000000000..2d9212e5bde --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Env.kt @@ -0,0 +1,14 @@ +package org.jetbrains.kotlin.gradle.targets.js.d8 + +import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore +import java.io.File +import java.net.URL + +data class D8Env( + val cleanableStore: CleanableStore, + val zipPath: File, + val targetPath: File, + val executablePath: File, + val isWindows: Boolean, + val downloadUrl: URL +) \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Exec.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Exec.kt new file mode 100644 index 00000000000..f6614021fc7 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Exec.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2020 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.targets.js.d8 + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.* +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.tasks.registerTask +import org.jetbrains.kotlin.gradle.utils.newFileProperty +import javax.inject.Inject + +open class D8Exec +@Inject +constructor( + private val compilation: KotlinJsCompilation +) : AbstractExecTask(D8Exec::class.java) { + @Transient + @get:Internal + lateinit var d8: D8RootExtension + + init { + onlyIf { + !inputFileProperty.isPresent || inputFileProperty.asFile.map { it.exists() }.get() + } + } + + @Input + var d8Args: MutableList = mutableListOf() + + @Optional + @PathSensitive(PathSensitivity.ABSOLUTE) + @InputFile + val inputFileProperty: RegularFileProperty = project.newFileProperty() + + override fun exec() { + val newArgs = mutableListOf() + newArgs.addAll(d8Args) + if (inputFileProperty.isPresent) { + val inputFile = inputFileProperty.asFile.get() + workingDir = inputFile.parentFile + if (compilation.target.platformType == KotlinPlatformType.wasm) { + newArgs.add("--module") + } + newArgs.add(inputFile.canonicalPath) + } + args?.let { + if (it.isNotEmpty()) { + newArgs.add("--") + newArgs.addAll(it) + } + } + this.args = newArgs + super.exec() + } + + companion object { + fun create( + compilation: KotlinJsCompilation, + name: String, + configuration: D8Exec.() -> Unit = {} + ): TaskProvider { + val target = compilation.target + val project = target.project + val d8 = D8RootPlugin.apply(project.rootProject) + return project.registerTask( + name, + listOf(compilation) + ) { + it.d8 = d8 + it.executable = d8.requireConfigured().executablePath.absolutePath + it.dependsOn(d8.setupTaskProvider) + it.dependsOn(compilation.compileKotlinTaskProvider) + it.configuration() + } + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Platform.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Platform.kt new file mode 100644 index 00000000000..a16edf3e008 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8Platform.kt @@ -0,0 +1,30 @@ +package org.jetbrains.kotlin.gradle.targets.js.d8 + +/** + * Provides platform and architecture names that is used to download D8. + */ +internal object D8Platform { + private val props = System.getProperties() + private fun property(name: String) = props.getProperty(name) ?: System.getProperty(name) + + const val WIN = "win" + const val LINUX = "linux" + const val DARWIN = "mac" + + val name: String = run { + val name = property("os.name").toLowerCase() + when { + name.contains("windows") -> WIN + name.contains("mac") -> DARWIN + name.contains("linux") -> LINUX + name.contains("freebsd") -> LINUX + else -> throw IllegalArgumentException("Unsupported OS: $name") + } + } + + const val X64 = "64" + const val X86 = "86" + + val architecture: String + get() = if (property("os.arch").contains("64")) X64 else X86 +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt new file mode 100644 index 00000000000..486a61fda9c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2010-2020 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.targets.js.d8 + +import org.gradle.api.Project +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.internal.ConfigurationPhaseAware +import org.jetbrains.kotlin.gradle.logging.kotlinInfo +import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore +import java.io.Serializable +import java.net.URL + +open class D8RootExtension(@Transient val rootProject: Project) : ConfigurationPhaseAware(), Serializable { + init { + check(rootProject.rootProject == rootProject) + } + + private val gradleHome = rootProject.gradle.gradleUserHomeDir.also { + rootProject.logger.kotlinInfo("Storing cached files in $it") + } + + var installationPath by Property(gradleHome.resolve("d8")) + var downloadBaseUrl by Property("https://storage.googleapis.com/chromium-v8/official/canary/") + var version by Property("10.2.9") + var edition by Property("rel") // rel or dbg + + val setupTaskProvider: TaskProvider + get() = rootProject.tasks.withType(Copy::class.java).named(D8RootPlugin.INSTALL_TASK_NAME) + + override fun finalizeConfiguration(): D8Env { + val platform = D8Platform.name + val architecture = D8Platform.architecture + val d8osString = platform + architecture + + val requiredVersionName = "v8-$d8osString-$edition-$version" + val requiredZipName = "$requiredVersionName.zip" + val cleanableStore = CleanableStore[installationPath.absolutePath] + val targetPath = cleanableStore[requiredVersionName].use() + val isWindows = D8Platform.name == D8Platform.WIN + + return D8Env( + cleanableStore = cleanableStore, + zipPath = cleanableStore[requiredZipName].use(), + targetPath = targetPath, + executablePath = targetPath.resolve(if (isWindows) "d8.exe" else "d8"), + isWindows = isWindows, + downloadUrl = URL("${downloadBaseUrl.trimEnd('/')}/$requiredZipName"), + ) + } + + companion object { + const val EXTENSION_NAME: String = "kotlinD8" + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootPlugin.kt new file mode 100644 index 00000000000..c057d710687 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootPlugin.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2010-2020 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.targets.js.d8 + +import de.undercouch.gradle.tasks.download.Download +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.BasePlugin +import org.gradle.api.tasks.Copy +import org.jetbrains.kotlin.gradle.targets.js.MultiplePluginDeclarationDetector +import org.jetbrains.kotlin.gradle.targets.js.d8.D8RootExtension.Companion.EXTENSION_NAME +import org.jetbrains.kotlin.gradle.tasks.CleanDataTask +import org.jetbrains.kotlin.gradle.tasks.registerTask + +open class D8RootPlugin : Plugin { + override fun apply(project: Project) { + MultiplePluginDeclarationDetector.detect(project) + + project.plugins.apply(BasePlugin::class.java) + + check(project == project.rootProject) { + "D8RootPlugin can be applied only to root project" + } + + val settings = project.extensions.create(EXTENSION_NAME, D8RootExtension::class.java, project) + + val downloadTask = project.registerTask("${TASKS_GROUP_NAME}Download") { + val env = settings.requireConfigured() + it.group = TASKS_GROUP_NAME + it.src(env.downloadUrl) + it.dest(env.zipPath) + it.overwrite(false) + it.description = "Download local d8 version" + } + + project.registerTask(INSTALL_TASK_NAME) { + val env = settings.requireConfigured() + it.onlyIf { env.zipPath.exists() && !env.executablePath.exists() } + it.group = TASKS_GROUP_NAME + it.from(project.zipTree(env.zipPath)) + it.into(env.targetPath) + it.dependsOn(downloadTask) + it.description = "Install local d8 version" + } + + project.registerTask("d8" + CleanDataTask.NAME_SUFFIX) { + it.cleanableStoreProvider = project.provider { settings.requireConfigured().cleanableStore } + it.group = TASKS_GROUP_NAME + it.description = "Clean unused local d8 version" + } + } + + companion object { + const val TASKS_GROUP_NAME: String = "d8" + const val INSTALL_TASK_NAME: String = "${TASKS_GROUP_NAME}Install" + + fun apply(rootProject: Project): D8RootExtension { + check(rootProject == rootProject.rootProject) + rootProject.plugins.apply(D8RootPlugin::class.java) + return rootProject.extensions.getByName(EXTENSION_NAME) as D8RootExtension + } + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dsl/KotlinWasmTargetDsl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dsl/KotlinWasmTargetDsl.kt new file mode 100644 index 00000000000..0d39607d592 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/dsl/KotlinWasmTargetDsl.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2022 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.targets.js.dsl + +import groovy.lang.Closure +import org.gradle.util.ConfigureUtil +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.targets.js.d8.D8Exec + +interface KotlinWasmSubTargetContainerDsl : KotlinTarget { + val d8: KotlinWasmD8Dsl + + val isD8Configured: Boolean + + fun whenD8Configured(body: KotlinWasmD8Dsl.() -> Unit) +} + +interface KotlinWasmTargetDsl : KotlinJsTargetDsl { + fun d8() = d8 { } + fun d8(body: KotlinWasmD8Dsl.() -> Unit) + fun d8(fn: Closure<*>) { + d8 { + ConfigureUtil.configure(fn, this) + } + } +} + +interface KotlinWasmD8Dsl : KotlinJsSubTargetDsl { + fun runTask(body: D8Exec.() -> Unit) +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt index 876422435eb..aeda6d8405a 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinBrowserJsIr.kt @@ -35,6 +35,8 @@ open class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) : KotlinJsIrSubTarget(target, "browser"), KotlinJsBrowserDsl { + private val nodeJs = NodeJsRootPlugin.apply(project.rootProject) + private val webpackTaskConfigurations: MutableList Unit> = mutableListOf() private val runTaskConfigurations: MutableList Unit> = mutableListOf() @@ -45,12 +47,25 @@ open class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) : override val testTaskDescription: String get() = "Run all ${target.name} tests inside browser using karma and webpack" - override fun configureDefaultTestFramework(it: KotlinJsTest) { - it.useKarma { - useChromeHeadless() + override fun configureTestDependencies(test: KotlinJsTest) { + test.dependsOn(nodeJs.npmInstallTaskProvider, nodeJs.nodeJsSetupTaskProvider) + } + + override fun configureDefaultTestFramework(test: KotlinJsTest) { + if (test.testFramework == null) { + test.useKarma { + useChromeHeadless() + } + } + + if (test.enabled) { + nodeJs.taskRequirements.addTaskRequirements(test) } } + override val additionalCompilerOption: String? + get() = "-Xwasm-launcher=esm".takeIf { target.platformType == KotlinPlatformType.wasm } + override fun commonWebpackConfig(body: KotlinWebpackConfig.() -> Unit) { webpackTaskConfigurations.add { webpackConfigApplier(body) @@ -282,20 +297,6 @@ open class KotlinBrowserJsIr @Inject constructor(target: KotlinJsIrTarget) : KotlinJsBinaryMode.DEVELOPMENT -> debugValue } - override fun addLinkOptions(compilation: KotlinJsIrCompilation) { - if (compilation.platformType != KotlinPlatformType.wasm) - return - - // Wasm requires different kinds of launchers for browser and nodejs. This might change in the future. - compilation.binaries - .withType(JsIrBinary::class.java) - .all { - it.linkTask.configure { linkTask -> - linkTask.kotlinOptions.freeCompilerArgs += "-Xwasm-launcher=esm" - } - } - } - companion object { private const val WEBPACK_TASK_NAME = "webpack" } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinD8Ir.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinD8Ir.kt new file mode 100644 index 00000000000..6d88bb140cb --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinD8Ir.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2022 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.targets.js.ir + +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.targets.js.d8.D8Exec +import org.jetbrains.kotlin.gradle.targets.js.d8.D8RootPlugin +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmD8Dsl +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinWasmD8 +import org.jetbrains.kotlin.gradle.tasks.withType +import javax.inject.Inject + +open class KotlinD8Ir @Inject constructor(target: KotlinJsIrTarget) : + KotlinJsIrSubTargetBase(target, "d8"), + KotlinWasmD8Dsl { + + private val d8 = D8RootPlugin.apply(project.rootProject) + + override val testTaskDescription: String + get() = "Run all ${target.name} tests inside d8 using the builtin test framework" + + override fun runTask(body: D8Exec.() -> Unit) { + project.tasks.withType().named(runTaskName).configure(body) + } + + override fun configureDefaultTestFramework(test: KotlinJsTest) { + test.testFramework = KotlinWasmD8(test) + } + + override fun configureTestDependencies(test: KotlinJsTest) { + test.dependsOn(d8.setupTaskProvider) + } + + override val additionalCompilerOption: String? + get() = "-Xwasm-launcher=d8".takeIf { target.platformType == KotlinPlatformType.wasm } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTarget.kt index 3ed8e882e65..b1f21c3cd8d 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTarget.kt @@ -11,9 +11,7 @@ import org.gradle.api.plugins.ExtensionAware import org.gradle.api.tasks.Copy import org.gradle.api.tasks.TaskProvider import org.gradle.language.base.plugins.LifecycleBasePlugin -import org.jetbrains.kotlin.gradle.plugin.AbstractKotlinTargetConfigurator -import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation -import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests +import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.isMain import org.jetbrains.kotlin.gradle.plugin.whenEvaluated import org.jetbrains.kotlin.gradle.targets.js.KotlinJsPlatformTestRun @@ -21,7 +19,6 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.Distribution import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalDistributionDsl import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetDsl -import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolverPlugin import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest @@ -38,8 +35,6 @@ abstract class KotlinJsIrSubTarget( ) : KotlinJsSubTargetDsl { val project get() = target.project - private val nodeJs = NodeJsRootPlugin.apply(project.rootProject) - abstract val testTaskDescription: String final override lateinit var testRuns: NamedDomainObjectContainer @@ -107,7 +102,16 @@ abstract class KotlinJsIrSubTarget( } } - abstract fun addLinkOptions(compilation: KotlinJsIrCompilation) + private fun addLinkOptions(compilation: KotlinJsIrCompilation) { + val additionalCompilerOption = additionalCompilerOption ?: return + compilation.binaries + .withType(JsIrBinary::class.java) + .all { + it.linkTask.configure { linkTask -> + linkTask.kotlinOptions.freeCompilerArgs += additionalCompilerOption + } + } + } private fun configureTestsRun(testRun: KotlinJsPlatformTestRun, compilation: KotlinJsIrCompilation) { fun KotlinJsPlatformTestRun.subtargetTestTaskName(): String = disambiguateCamelCased( @@ -139,7 +143,7 @@ abstract class KotlinJsIrSubTarget( ) ) - testJs.dependsOn(nodeJs.npmInstallTaskProvider, nodeJs.nodeJsSetupTaskProvider) + configureTestDependencies(testJs) testJs.onlyIf { task -> (task as KotlinJsTest).inputFileProperty @@ -166,18 +170,14 @@ abstract class KotlinJsIrSubTarget( project.whenEvaluated { testJs.configure { - if (it.testFramework == null) { - configureDefaultTestFramework(it) - } - - if (it.enabled) { - nodeJs.taskRequirements.addTaskRequirements(it) - } + configureDefaultTestFramework(it) } } } - protected abstract fun configureDefaultTestFramework(it: KotlinJsTest) + protected abstract fun configureDefaultTestFramework(test: KotlinJsTest) + protected abstract fun configureTestDependencies(test: KotlinJsTest) + protected abstract val additionalCompilerOption: String? private fun configureMain() { target.compilations.all { compilation -> diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTargetBase.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTargetBase.kt new file mode 100644 index 00000000000..659e76b0685 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrSubTargetBase.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2022 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.targets.js.ir + +import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExec +import org.jetbrains.kotlin.gradle.tasks.dependsOn +import org.jetbrains.kotlin.gradle.tasks.locateTask + +abstract class KotlinJsIrSubTargetBase(target: KotlinJsIrTarget, classifier: String) : + KotlinJsIrSubTarget(target, classifier) { + + protected val runTaskName = disambiguateCamelCased("run") + + override fun configureRun(compilation: KotlinJsIrCompilation) { + compilation.binaries + .withType(JsIrBinary::class.java) + .matching { it is Executable } + .all { developmentExecutable -> + configureRun(developmentExecutable) + } + } + + private fun configureRun(binary: JsIrBinary) { + val binaryRunName = disambiguateCamelCased( + binary.mode.name.toLowerCase(), + RUN_TASK_NAME + ) + locateOrRegisterRunTask(binary, binaryRunName) + + if (binary.mode == KotlinJsBinaryMode.DEVELOPMENT) { + val runName = disambiguateCamelCased(RUN_TASK_NAME) + locateOrRegisterRunTask(binary, runName) + } + } + + private fun locateOrRegisterRunTask( + binary: JsIrBinary, + name: String + ) { + val runTask = project.locateTask(name) + if (runTask == null) { + val runTaskHolder = NodeJsExec.create(binary.compilation, name) { + group = taskGroupName + inputFileProperty.set( + project.layout.file( + binary.linkSyncTask.map { + it.destinationDir + .resolve(binary.linkTask.get().outputFileProperty.get().name) + } + ) + ) + } + + target.runTask.dependsOn(runTaskHolder) + } + } + + override fun configureBuild(compilation: KotlinJsIrCompilation) { + compilation.binaries + .getIrBinaries(KotlinJsBinaryMode.PRODUCTION) + .matching { it is Executable } + .all { productionExecutable -> + project.tasks.named(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(productionExecutable.linkTask) + } + } + + override fun configureLibrary(compilation: KotlinJsIrCompilation) { + super.configureLibrary(compilation) + + compilation.binaries + .withType(JsIrBinary::class.java) + .matching { it is Library } + .all { binary -> + configureRun(binary) + } + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt index 1a81e460833..d7618badfd7 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTarget.kt @@ -19,10 +19,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinTargetWithBinaries import org.jetbrains.kotlin.gradle.targets.js.JsAggregatingExecutionSource import org.jetbrains.kotlin.gradle.targets.js.KotlinJsReportAggregatingTestRun import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBrowserDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsNodeDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetContainerDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.dsl.* import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.tasks.locateOrRegisterTask import org.jetbrains.kotlin.gradle.tasks.registerTask @@ -40,7 +37,9 @@ constructor( KotlinTargetWithBinaries(project, platformType), KotlinTargetWithTests, KotlinJsTargetDsl, - KotlinJsSubTargetContainerDsl { + KotlinWasmTargetDsl, + KotlinJsSubTargetContainerDsl, + KotlinWasmSubTargetContainerDsl { override lateinit var testRuns: NamedDomainObjectContainer internal set @@ -151,6 +150,7 @@ constructor( } } + //Browser private val browserLazyDelegate = lazy { commonLazy project.objects.newInstance(KotlinBrowserJsIr::class.java, this).also { @@ -173,6 +173,7 @@ constructor( body(browser) } + //node.js private val nodejsLazyDelegate = lazy { commonLazy project.objects.newInstance(KotlinNodeJsIr::class.java, this).also { @@ -196,11 +197,35 @@ constructor( body(nodejs) } + //d8 + private val d8LazyDelegate = lazy { + commonLazy + project.objects.newInstance(KotlinD8Ir::class.java, this).also { + it.configureSubTarget() + d8ConfiguredHandlers.forEach { handler -> + handler(it) + } + + d8ConfiguredHandlers.clear() + } + } + + private val d8ConfiguredHandlers = mutableListOf Unit>() + + override val d8 by d8LazyDelegate + + override val isD8Configured: Boolean + get() = d8LazyDelegate.isInitialized() + private fun KotlinJsIrSubTarget.configureSubTarget() { configureTestSideEffect configure() } + override fun d8(body: KotlinWasmD8Dsl.() -> Unit) { + body(d8) + } + override fun whenBrowserConfigured(body: KotlinJsBrowserDsl.() -> Unit) { if (browserLazyDelegate.isInitialized()) { browser(body) @@ -217,6 +242,14 @@ constructor( } } + override fun whenD8Configured(body: KotlinWasmD8Dsl.() -> Unit) { + if (d8LazyDelegate.isInitialized()) { + d8(body) + } else { + d8ConfiguredHandlers += body + } + } + override fun useCommonJs() { compilations.all { it.kotlinOptions.configureCommonJsOptions() diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt index 6d27b7a1597..f50967663b1 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsIrTargetPreset.kt @@ -19,8 +19,7 @@ import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheckWhenE import org.jetbrains.kotlin.statistics.metrics.StringMetrics open class KotlinJsIrTargetPreset( - project: Project, - isWasm: Boolean, + project: Project ) : KotlinOnlyTargetPreset( project ) { @@ -29,21 +28,9 @@ open class KotlinJsIrTargetPreset( open val isMpp: Boolean get() = true - override val platformType: KotlinPlatformType = - if (isWasm) - KotlinPlatformType.wasm - else - KotlinPlatformType.js + override val platformType: KotlinPlatformType = KotlinPlatformType.js override fun instantiateTarget(name: String): KotlinJsIrTarget { - if (platformType == KotlinPlatformType.wasm && !PropertiesProvider(project).wasmStabilityNoWarn) { - project.logger.warn( - """ - New 'wasm' target is Work-in-Progress and is subject to change without notice. - """.trimIndent() - ) - } - return project.objects.newInstance(KotlinJsIrTarget::class.java, project, platformType, mixedMode).apply { this.isMpp = this@KotlinJsIrTargetPreset.isMpp if (!mixedMode) { @@ -87,11 +74,7 @@ open class KotlinJsIrTargetPreset( return result } - override fun getName(): String = when (platformType) { - KotlinPlatformType.wasm -> WASM_PRESET_NAME - KotlinPlatformType.js -> JS_PRESET_NAME - else -> error("Unsupported platform type") - } + override fun getName(): String = JS_PRESET_NAME //TODO[Ilya Goncharov] remove public morozov public override fun createCompilationFactory( @@ -104,15 +87,13 @@ open class KotlinJsIrTargetPreset( "js", KotlinJsCompilerType.IR.lowerName ) - private const val WASM_PRESET_NAME = "wasm" } } class KotlinJsIrSingleTargetPreset( project: Project ) : KotlinJsIrTargetPreset( - project, - isWasm = false, + project ) { override val isMpp: Boolean get() = false diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt index 366c6eb3c56..75a2791fd45 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinNodeJsIr.kt @@ -5,114 +5,45 @@ package org.jetbrains.kotlin.gradle.targets.js.ir -import org.gradle.language.base.plugins.LifecycleBasePlugin import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsNodeDsl import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsExec +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest -import org.jetbrains.kotlin.gradle.tasks.dependsOn -import org.jetbrains.kotlin.gradle.tasks.locateTask +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinWasmNode import org.jetbrains.kotlin.gradle.tasks.withType import javax.inject.Inject open class KotlinNodeJsIr @Inject constructor(target: KotlinJsIrTarget) : - KotlinJsIrSubTarget(target, "node"), + KotlinJsIrSubTargetBase(target, "node"), KotlinJsNodeDsl { + + private val nodeJs = NodeJsRootPlugin.apply(project.rootProject) + override val testTaskDescription: String get() = "Run all ${target.name} tests inside nodejs using the builtin test framework" - private val runTaskName = disambiguateCamelCased("run") - override fun runTask(body: NodeJsExec.() -> Unit) { project.tasks.withType().named(runTaskName).configure(body) } - override fun configureDefaultTestFramework(it: KotlinJsTest) { - it.useMocha { } + override fun configureTestDependencies(test: KotlinJsTest) { + test.dependsOn(nodeJs.npmInstallTaskProvider, nodeJs.nodeJsSetupTaskProvider) } - override fun configureRun( - compilation: KotlinJsIrCompilation - ) { - compilation.binaries - .withType(JsIrBinary::class.java) - .matching { it is Executable } - .all { developmentExecutable -> - configureRun(developmentExecutable) + override fun configureDefaultTestFramework(test: KotlinJsTest) { + if (target.platformType != KotlinPlatformType.wasm) { + if (test.testFramework == null) { + test.useMocha { } } - } - - private fun configureRun(binary: JsIrBinary) { - val binaryRunName = disambiguateCamelCased( - binary.mode.name.toLowerCase(), - RUN_TASK_NAME - ) - locateOrRegisterRunTask(binary, binaryRunName) - - if (binary.mode == KotlinJsBinaryMode.DEVELOPMENT) { - val runName = disambiguateCamelCased( - RUN_TASK_NAME - ) - locateOrRegisterRunTask(binary, runName) + if (test.enabled) { + nodeJs.taskRequirements.addTaskRequirements(test) + } + } else { + test.testFramework = KotlinWasmNode(test) } } - private fun locateOrRegisterRunTask( - binary: JsIrBinary, - name: String - ) { - val runTask = project.locateTask(name) - if (runTask == null) { - val runTaskHolder = NodeJsExec.create(binary.compilation, name) { - group = taskGroupName - inputFileProperty.set( - project.layout.file( - binary.linkSyncTask.map { - it.destinationDir - .resolve(binary.linkTask.get().outputFileProperty.get().name) - } - ) - ) - } - - target.runTask.dependsOn(runTaskHolder) - } - } - - override fun configureBuild( - compilation: KotlinJsIrCompilation - ) { - compilation.binaries - .getIrBinaries(KotlinJsBinaryMode.PRODUCTION) - .matching { it is Executable } - .all { productionExecutable -> - project.tasks.named(LifecycleBasePlugin.ASSEMBLE_TASK_NAME).dependsOn(productionExecutable.linkTask) - } - } - - override fun configureLibrary(compilation: KotlinJsIrCompilation) { - super.configureLibrary(compilation) - - compilation.binaries - .withType(JsIrBinary::class.java) - .matching { it is Library } - .all { binary -> - configureRun(binary) - } - } - - override fun addLinkOptions(compilation: KotlinJsIrCompilation) { - if (compilation.platformType != KotlinPlatformType.wasm) - return - - // Wasm requires different kinds of launchers for browser and nodejs. This might change in the future. - compilation.binaries - .withType(JsIrBinary::class.java) - .all { - it.linkTask.configure { linkTask -> - linkTask.kotlinOptions.freeCompilerArgs += "-Xwasm-launcher=nodejs" - } - } - } + override val additionalCompilerOption: String? + get() = "-Xwasm-launcher=nodejs".takeIf { target.platformType == KotlinPlatformType.wasm } } \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinWasmTargetPreset.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinWasmTargetPreset.kt new file mode 100644 index 00000000000..83c256cab47 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinWasmTargetPreset.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2020 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.targets.js.ir + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.* +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCompilationFactory +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinOnlyTargetPreset +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.PublicationRegistrationMode +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.hasKpmModel +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.mapTargetCompilationsToKpmVariants +import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheckWhenEvaluated + +class KotlinWasmTargetPreset( + project: Project, +) : KotlinOnlyTargetPreset(project) { + override val platformType: KotlinPlatformType = KotlinPlatformType.wasm + + override fun instantiateTarget(name: String): KotlinJsIrTarget { + if (!PropertiesProvider(project).wasmStabilityNoWarn) { + project.logger.warn("New 'wasm' target is Work-in-Progress and is subject to change without notice.") + } + + val irTarget = project.objects.newInstance(KotlinJsIrTarget::class.java, project, KotlinPlatformType.wasm, false) + irTarget.isMpp = true + + project.runProjectConfigurationHealthCheckWhenEvaluated { + if (!irTarget.isBrowserConfigured && !irTarget.isNodejsConfigured && !irTarget.isD8Configured) { + project.logger.warn( + """ + Please choose a JavaScript environment to run tests. + kotlin { + wasm { + // To build distributions for and run tests on browser, Node.js or d8 use one: + browser() + nodejs() + d8() + } + } + """.trimIndent() + ) + } + } + + return irTarget + } + + override fun createKotlinTargetConfigurator(): AbstractKotlinTargetConfigurator = + KotlinJsIrTargetConfigurator() + + override fun createTarget(name: String): KotlinJsIrTarget { + val result = super.createTarget(name) + if (project.hasKpmModel) { + mapTargetCompilationsToKpmVariants(result, PublicationRegistrationMode.IMMEDIATE) + } + return result + } + + override fun getName(): String = WASM_PRESET_NAME + + public override fun createCompilationFactory( + forTarget: KotlinJsIrTarget + ): KotlinCompilationFactory = + KotlinJsIrCompilationFactory(project, forTarget) + + companion object { + private const val WASM_PRESET_NAME = "wasm" + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinWasmD8.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinWasmD8.kt new file mode 100644 index 00000000000..047097cbcd3 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinWasmD8.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2022 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.targets.js.testing + +import org.gradle.api.Project +import org.gradle.process.ProcessForkOptions +import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings +import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency +import org.jetbrains.kotlin.gradle.targets.js.d8.D8RootPlugin +import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm +import org.jetbrains.kotlin.gradle.targets.js.isTeamCity +import org.jetbrains.kotlin.gradle.utils.getValue + +internal class KotlinWasmD8(private val kotlinJsTest: KotlinJsTest) : KotlinJsTestFramework { + override val settingsState: String = "KotlinWasmD8" + override val compilation: KotlinJsCompilation = kotlinJsTest.compilation + private val project: Project = compilation.target.project + private val d8 = D8RootPlugin.apply(project.rootProject) + private val d8Executable by project.provider { d8.requireConfigured().executablePath } + + init { + kotlinJsTest.outputs.upToDateWhen { false } + } + + override fun createTestExecutionSpec( + task: KotlinJsTest, + forkOptions: ProcessForkOptions, + nodeJsArgs: MutableList, + debug: Boolean + ): TCServiceMessagesTestExecutionSpec { + val compiledFile = task.inputFileProperty.get().asFile + + forkOptions.executable = d8Executable.absolutePath + forkOptions.workingDir = compiledFile.parentFile + + val clientSettings = TCServiceMessagesClientSettings( + task.name, + testNameSuffix = task.targetName, + prependSuiteName = true, + stackTraceParser = ::parseNodeJsStackTraceAsJvm, + ignoreOutOfRootNodes = true, + escapeTCMessagesInLog = project.isTeamCity + ) + + val cliArgs = KotlinTestRunnerCliArgs( + include = task.includePatterns, + exclude = task.excludePatterns + ) + + val args = mutableListOf( + "--module", + compiledFile.absolutePath, + "--experimental-wasm-typed-funcref", + "--experimental-wasm-gc", + "--experimental-wasm-eh", + ) + + args.add("--") + args.addAll(cliArgs.toList()) + + val dryRunArgs = mutableListOf() + dryRunArgs.addAll(args) + dryRunArgs.add("--dryRun") + + return TCServiceMessagesTestExecutionSpec( + forkOptions = forkOptions, + args = args, + checkExitCode = false, + clientSettings = clientSettings, + dryRunArgs = dryRunArgs + ) + } + + override val requiredNpmDependencies: Set = emptySet() + + override fun getPath(): String = "${kotlinJsTest.path}:kotlinTestFrameworkStub" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinWasmNode.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinWasmNode.kt new file mode 100644 index 00000000000..08c974ff580 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/KotlinWasmNode.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2022 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.targets.js.testing + +import org.gradle.api.Project +import org.gradle.process.ProcessForkOptions +import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings +import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation +import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency +import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm +import org.jetbrains.kotlin.gradle.targets.js.isTeamCity + +internal class KotlinWasmNode(private val kotlinJsTest: KotlinJsTest) : KotlinJsTestFramework { + override val settingsState: String = "KotlinWasmNode" + override val compilation: KotlinJsCompilation = kotlinJsTest.compilation + private val project: Project = compilation.target.project + + init { + kotlinJsTest.outputs.upToDateWhen { false } + } + + override fun createTestExecutionSpec( + task: KotlinJsTest, + forkOptions: ProcessForkOptions, + nodeJsArgs: MutableList, + debug: Boolean + ): TCServiceMessagesTestExecutionSpec { + val compiledFile = task.inputFileProperty.get().asFile + + val clientSettings = TCServiceMessagesClientSettings( + task.name, + testNameSuffix = task.targetName, + prependSuiteName = true, + stackTraceParser = ::parseNodeJsStackTraceAsJvm, + ignoreOutOfRootNodes = true, + escapeTCMessagesInLog = project.isTeamCity + ) + + val cliArgs = KotlinTestRunnerCliArgs( + include = task.includePatterns, + exclude = task.excludePatterns + ) + + val args = mutableListOf() + with(args) { + add("--experimental-wasm-typed-funcref") + add("--experimental-wasm-gc") + add("--experimental-wasm-eh") + add(compiledFile.absolutePath) + addAll(cliArgs.toList()) + } + val dryRunArgs = mutableListOf() + dryRunArgs.addAll(args) + dryRunArgs.add("--dryRun") + + return TCServiceMessagesTestExecutionSpec( + forkOptions = forkOptions, + args = args, + checkExitCode = false, + clientSettings = clientSettings, + dryRunArgs = dryRunArgs + ) + } + + override val requiredNpmDependencies: Set = emptySet() + + override fun getPath(): String = "${kotlinJsTest.path}:kotlinTestFrameworkStub" +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/karma/KotlinKarma.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/karma/KotlinKarma.kt index d1aee133e26..0250a2eab2c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/karma/KotlinKarma.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/karma/KotlinKarma.kt @@ -19,14 +19,10 @@ import org.jetbrains.kotlin.gradle.internal.operation import org.jetbrains.kotlin.gradle.internal.processLogMessage import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec -import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutor.Companion.TC_PROJECT_PROPERTY import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation -import org.jetbrains.kotlin.gradle.targets.js.NpmPackageVersion -import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency -import org.jetbrains.kotlin.gradle.targets.js.appendConfigsFromDir +import org.jetbrains.kotlin.gradle.targets.js.* import org.jetbrains.kotlin.gradle.targets.js.internal.parseNodeJsStackTraceAsJvm -import org.jetbrains.kotlin.gradle.targets.js.jsQuoted import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.testing.* @@ -36,7 +32,6 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.WebpackMajorVersion.Compan import org.jetbrains.kotlin.gradle.tasks.KotlinTest import org.jetbrains.kotlin.gradle.testing.internal.reportsDir import org.jetbrains.kotlin.gradle.utils.appendLine -import org.jetbrains.kotlin.gradle.utils.isConfigurationCacheAvailable import org.jetbrains.kotlin.gradle.utils.property import org.slf4j.Logger import java.io.File @@ -66,13 +61,6 @@ class KotlinKarma( private var configDirectory: File by property { defaultConfigDirectory } - private val isTeamCity by lazy { - if (isConfigurationCacheAvailable(project.gradle)) { - project.providers.gradleProperty(TC_PROJECT_PROPERTY).forUseAtConfigurationTime().isPresent - } else { - project.hasProperty(TC_PROJECT_PROPERTY) - } - } override val requiredNpmDependencies: Set get() = requiredDependencies + webpackConfig.getRequiredDependencies(versions) @@ -392,7 +380,7 @@ class KotlinKarma( prependSuiteName = true, stackTraceParser = ::parseNodeJsStackTraceAsJvm, ignoreOutOfRootNodes = true, - escapeTCMessagesInLog = isTeamCity + escapeTCMessagesInLog = project.isTeamCity ) config.basePath = npmProject.nodeModulesDir.absolutePath diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/mocha/KotlinMocha.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/mocha/KotlinMocha.kt index 8dc1b5537b8..789e237a168 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/mocha/KotlinMocha.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/testing/mocha/KotlinMocha.kt @@ -9,7 +9,6 @@ import org.gradle.api.Project import org.gradle.process.ProcessForkOptions import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesClientSettings import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutionSpec -import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutor.Companion.TC_PROJECT_PROPERTY import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency @@ -20,8 +19,8 @@ import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTestFramework import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinTestRunnerCliArgs -import org.jetbrains.kotlin.gradle.utils.isConfigurationCacheAvailable import java.io.File +import org.jetbrains.kotlin.gradle.targets.js.isTeamCity class KotlinMocha(@Transient override val compilation: KotlinJsCompilation, private val basePath: String) : KotlinJsTestFramework { @@ -29,13 +28,6 @@ class KotlinMocha(@Transient override val compilation: KotlinJsCompilation, priv private val project: Project = compilation.target.project private val npmProject = compilation.npmProject private val versions = NodeJsRootPlugin.apply(project.rootProject).versions - private val isTeamCity by lazy { - if (isConfigurationCacheAvailable(project.gradle)) { - project.providers.gradleProperty(TC_PROJECT_PROPERTY).forUseAtConfigurationTime().isPresent - } else { - project.hasProperty(TC_PROJECT_PROPERTY) - } - } override val settingsState: String get() = "mocha" @@ -67,7 +59,7 @@ class KotlinMocha(@Transient override val compilation: KotlinJsCompilation, priv prependSuiteName = true, stackTraceParser = ::parseNodeJsStackTraceAsJvm, ignoreOutOfRootNodes = true, - escapeTCMessagesInLog = isTeamCity + escapeTCMessagesInLog = project.isTeamCity ) val cliArgs = KotlinTestRunnerCliArgs( @@ -80,7 +72,6 @@ class KotlinMocha(@Transient override val compilation: KotlinJsCompilation, priv val file = task.inputFileProperty.get().asFile.toString() val adapter = createAdapterJs(file, "kotlin-test-nodejs-runner", ADAPTER_NODEJS) - val args = mutableListOf( "--require", npmProject.require("source-map-support/register.js") diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/utils.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/utils.kt index 95965eb94e5..d96c3628d31 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/utils.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/utils.kt @@ -7,8 +7,11 @@ package org.jetbrains.kotlin.gradle.targets.js import org.gradle.internal.hash.FileHasher import org.gradle.internal.hash.Hashing.defaultFunction +import org.jetbrains.kotlin.gradle.internal.testing.TCServiceMessagesTestExecutor import org.jetbrains.kotlin.gradle.utils.appendLine +import org.jetbrains.kotlin.gradle.utils.isConfigurationCacheAvailable import java.io.File +import org.gradle.api.Project fun Appendable.appendConfigsFromDir(confDir: File) { val files = confDir.listFiles() ?: return @@ -58,3 +61,10 @@ fun FileHasher.calculateDirHash( const val JS = "js" const val JS_MAP = "js.map" const val META_JS = "meta.js" + +val Project.isTeamCity: Boolean + get() = if (isConfigurationCacheAvailable(project.gradle)) { + project.providers.gradleProperty(TCServiceMessagesTestExecutor.TC_PROJECT_PROPERTY).forUseAtConfigurationTime().isPresent + } else { + project.hasProperty(TCServiceMessagesTestExecutor.TC_PROJECT_PROPERTY) + }