diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt index 53bdd33ff42..217218029de 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/NewMultiplatformIT.kt @@ -1956,16 +1956,24 @@ class NewMultiplatformIT : BaseGradleIT() { } } - private fun testWasmTest(engine: String, name: String) = with( + private fun testWasmTest(engine: String, name: String, useBinaryen: Boolean) = with( Project("new-mpp-wasm-test", gradleVersionRequirement = GradleVersionRequired.AtLeast(TestVersions.Gradle.G_7_0)) ) { setupWorkingDir() gradleBuildScript().modify { transformBuildScriptWithPluginsDsl(it) .replace("", engine) + .replace("", if (useBinaryen) "applyBinaryen()" else "") } build(":wasm${name}Test") { assertTasksExecuted(":compileKotlinWasm") + if (useBinaryen) { + assertTasksExecuted(":compileTestProductionExecutableKotlinWasmOptimize") + assertTasksExecuted(":compileTestDevelopmentExecutableKotlinWasmOptimize") + } else { + assertTasksNotExecuted(":compileTestProductionExecutableKotlinWasmOptimize") + assertTasksNotExecuted(":compileTestDevelopmentExecutableKotlinWasmOptimize") + } assertTasksFailed(":wasm${name}Test") assertTestResults( "testProject/new-mpp-wasm-test/TEST-${engine}.xml", @@ -1975,10 +1983,16 @@ class NewMultiplatformIT : BaseGradleIT() { } @Test - fun testWasmNodeTest() = testWasmTest("nodejs", "Node") + fun testWasmNodeTest() = testWasmTest("nodejs", "Node", useBinaryen = false) @Test - fun testWasmD8Test() = testWasmTest("d8", "D8") + fun testWasmWithBinaryenNodeTest() = testWasmTest("nodejs", "Node", useBinaryen = true) + + @Test + fun testWasmD8Test() = testWasmTest("d8", "D8", useBinaryen = false) + + @Test + fun testWasmWithBinaryenD8Test() = testWasmTest("d8", "D8", useBinaryen = true) @Test fun testResolveMetadataCompileClasspathKt50925() { diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts index d8f48a09450..3982a62345c 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts @@ -27,6 +27,7 @@ tasks.named("kotli kotlin { wasm { () + } sourceSets { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenEnv.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenEnv.kt new file mode 100644 index 00000000000..cd06bb6f5e8 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenEnv.kt @@ -0,0 +1,14 @@ +package org.jetbrains.kotlin.gradle.targets.js.binaryen + +import org.jetbrains.kotlin.gradle.tasks.internal.CleanableStore +import java.io.File +import java.net.URL + +data class BinaryenEnv( + 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/binaryen/BinaryenExec.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenExec.kt new file mode 100644 index 00000000000..b203ff7d48c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenExec.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.binaryen + +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 BinaryenExec +@Inject +constructor() : AbstractExecTask(BinaryenExec::class.java) { + @Transient + @get:Internal + lateinit var binaryen: BinaryenRootExtension + + init { + onlyIf { + !inputFileProperty.isPresent || inputFileProperty.asFile.map { it.exists() }.get() + } + } + + @Input + var binaryenArgs: MutableList = mutableListOf( + "--enable-nontrapping-float-to-int", + "--enable-typed-function-references", + "--enable-gc", + "--enable-reference-types", + "--enable-exception-handling", + "--hybrid", + "-O3", + "--inline-functions-with-loops", + "--traps-never-happen", + "--fast-math", + ) + + @InputFile + val inputFileProperty: RegularFileProperty = project.newFileProperty() + + @OutputFile + val outputFileProperty: RegularFileProperty = project.newFileProperty() + + override fun exec() { + val inputFile = inputFileProperty.asFile.get() + val newArgs = mutableListOf() + newArgs.addAll(binaryenArgs) + newArgs.add(inputFile.canonicalPath) + newArgs.add("-o") + newArgs.add(outputFileProperty.asFile.get().canonicalPath) + workingDir = inputFile.parentFile + this.args = newArgs + super.exec() + } + + companion object { + fun create( + compilation: KotlinJsCompilation, + name: String, + configuration: BinaryenExec.() -> Unit = {} + ): TaskProvider { + val target = compilation.target + val project = target.project + val binaryen = BinaryenRootPlugin.apply(project.rootProject) + return project.registerTask( + name, + ) { + it.binaryen = binaryen + it.executable = binaryen.requireConfigured().executablePath.absolutePath + it.dependsOn(binaryen.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/binaryen/BinaryenPlatform.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenPlatform.kt new file mode 100644 index 00000000000..06537cee257 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenPlatform.kt @@ -0,0 +1,30 @@ +package org.jetbrains.kotlin.gradle.targets.js.binaryen + +/** + * Provides platform and architecture names that is used to download Binaryen. + */ +internal object BinaryenPlatform { + private val props = System.getProperties() + private fun property(name: String) = props.getProperty(name) ?: System.getProperty(name) + + const val WIN = "windows" + const val LINUX = "linux" + const val DARWIN = "macos" + + 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/binaryen/BinaryenRootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenRootExtension.kt new file mode 100644 index 00000000000..4c7dc05ebd4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenRootExtension.kt @@ -0,0 +1,59 @@ +/* + * 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.binaryen + +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.File +import java.io.Serializable +import java.net.URL + +open class BinaryenRootExtension(@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("binaryen")) + var downloadBaseUrl by Property("https://github.com/WebAssembly/binaryen/releases/download/") + var version by Property("109") + + val setupTaskProvider: TaskProvider + get() = rootProject.tasks.withType(Copy::class.java).named(BinaryenRootPlugin.INSTALL_TASK_NAME) + + override fun finalizeConfiguration(): BinaryenEnv { + val platform = BinaryenPlatform.name + val architecture = BinaryenPlatform.architecture + val requiredVersionName = "binaryen-version_$version-x86_$architecture-$platform" + val requiredZipName = "$requiredVersionName.tar.gz" + val cleanableStore = CleanableStore[installationPath.absolutePath] + val targetPath = cleanableStore[requiredVersionName].use() + val isWindows = BinaryenPlatform.name == BinaryenPlatform.WIN + + return BinaryenEnv( + cleanableStore = cleanableStore, + zipPath = cleanableStore[requiredZipName].use(), + targetPath = targetPath, + executablePath = targetPath + .resolve("binaryen-version_$version") + .resolve("bin") + .resolve(if (isWindows) "wasm-opt.exe" else "wasm-opt"), + isWindows = isWindows, + downloadUrl = URL("${downloadBaseUrl.trimEnd('/')}/version_$version/$requiredZipName"), + ) + } + + companion object { + const val EXTENSION_NAME: String = "kotlinBinaryen" + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenRootPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenRootPlugin.kt new file mode 100644 index 00000000000..5adadce7cb2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/binaryen/BinaryenRootPlugin.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.binaryen + +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.binaryen.BinaryenRootExtension.Companion.EXTENSION_NAME +import org.jetbrains.kotlin.gradle.tasks.CleanDataTask +import org.jetbrains.kotlin.gradle.tasks.registerTask + +open class BinaryenRootPlugin : Plugin { + override fun apply(project: Project) { + MultiplePluginDeclarationDetector.detect(project) + + project.plugins.apply(BasePlugin::class.java) + + check(project == project.rootProject) { + "BinaryenRootPlugin can be applied only to root project" + } + + val settings = project.extensions.create(EXTENSION_NAME, BinaryenRootExtension::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 binaryen 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.tarTree(env.zipPath)) + it.into(env.targetPath) + it.dependsOn(downloadTask) + it.description = "Install local binaryen version" + } + + project.registerTask("binaryen" + CleanDataTask.NAME_SUFFIX) { + it.cleanableStoreProvider = project.provider { settings.requireConfigured().cleanableStore } + it.group = TASKS_GROUP_NAME + it.description = "Clean unused local binaryen version" + } + } + + companion object { + const val TASKS_GROUP_NAME: String = "binaryen" + const val INSTALL_TASK_NAME: String = "${TASKS_GROUP_NAME}Install" + + fun apply(rootProject: Project): BinaryenRootExtension { + check(rootProject == rootProject.rootProject) + rootProject.plugins.apply(BinaryenRootPlugin::class.java) + return rootProject.extensions.getByName(EXTENSION_NAME) as BinaryenRootExtension + } + } +} 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 index 0d39607d592..aad0c2a0f43 100644 --- 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 @@ -8,6 +8,7 @@ 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.binaryen.BinaryenExec import org.jetbrains.kotlin.gradle.targets.js.d8.D8Exec interface KotlinWasmSubTargetContainerDsl : KotlinTarget { @@ -16,6 +17,8 @@ interface KotlinWasmSubTargetContainerDsl : KotlinTarget { val isD8Configured: Boolean fun whenD8Configured(body: KotlinWasmD8Dsl.() -> Unit) + + fun whenBinaryenApplied(body: () -> Unit) } interface KotlinWasmTargetDsl : KotlinJsTargetDsl { @@ -26,6 +29,8 @@ interface KotlinWasmTargetDsl : KotlinJsTargetDsl { ConfigureUtil.configure(fn, this) } } + + fun applyBinaryen() } interface KotlinWasmD8Dsl : KotlinJsSubTargetDsl { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsBinaryContainer.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsBinaryContainer.kt index 97b6bd6dec1..75536dae8cf 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsBinaryContainer.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/ir/KotlinJsBinaryContainer.kt @@ -10,10 +10,12 @@ import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinTargetWithBinaries import org.jetbrains.kotlin.gradle.plugin.mpp.isMain import org.jetbrains.kotlin.gradle.targets.js.KotlinJsTarget +import org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenExec import org.jetbrains.kotlin.gradle.targets.js.dsl.Distribution import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode.DEVELOPMENT @@ -21,9 +23,9 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBinaryMode.PRODUCTION import org.jetbrains.kotlin.gradle.targets.js.subtargets.DefaultDistribution import org.jetbrains.kotlin.gradle.targets.js.subtargets.KotlinJsSubTarget import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName +import java.io.File import javax.inject.Inject - open class KotlinJsBinaryContainer @Inject constructor( @@ -38,6 +40,30 @@ constructor( private val defaultCompilation: KotlinJsCompilation get() = target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + private fun configureBinaryen(binary: JsIrBinary) { + val linkTask = binary.linkTask + + val compiledMjsFile = linkTask.map { link -> + link.kotlinOptions.outputFile?.let(::File) + ?: link.destinationDirectory.locationOnly.get().asFile.resolve("${link.compilation.ownModuleName}.mjs") + } + + val compiledWasmFile = compiledMjsFile.map { + it.parentFile.resolve("${it.nameWithoutExtension}.wasm") + } + + //TODO This is temporary solution that overrides compiled files that triggers recompile and reoptimize wasm every time (when binaryen is enabled) + val binaryenTask = BinaryenExec.create(binary.compilation, "${linkTask.name}Optimize") { + inputFileProperty.fileProvider(compiledWasmFile) + outputFileProperty.fileProvider(compiledWasmFile) + } + + binary.compilation.compileKotlinTask.finalizedBy(binaryenTask) + binary.linkSyncTask.configure { + it.dependsOn(binaryenTask) + } + } + // For Groovy DSL @JvmOverloads fun executable( @@ -175,6 +201,12 @@ constructor( extensions.add(binary.name, binary) } + if (compilation.platformType == KotlinPlatformType.wasm && target is KotlinJsIrTarget && binary is JsIrBinary) { + target.whenBinaryenApplied { + configureBinaryen(binary) + } + } + return binary } 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 d7618badfd7..fd2af765f0d 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 @@ -144,12 +144,31 @@ constructor( task.from(project.tasks.named(compilation.processResourcesTaskName)) - task.into( - npmProject.dist - ) + task.into(npmProject.dist) } } + //Binaryen + private val applyBinaryenHandlers = mutableListOf<() -> Unit>() + + private var binaryenApplied: Boolean = false + + override fun whenBinaryenApplied(body: () -> Unit) { + if (binaryenApplied) { + body() + } else { + applyBinaryenHandlers += body + } + } + + override fun applyBinaryen() { + binaryenApplied = true + applyBinaryenHandlers.forEach { handler -> + handler() + } + browserConfiguredHandlers.clear() + } + //Browser private val browserLazyDelegate = lazy { commonLazy