[WASM] Implement gradle plugin binaryen support

This commit is contained in:
Igor Yakovlev
2022-06-10 21:30:30 +02:00
parent 177177ee8c
commit 952b846a5c
10 changed files with 328 additions and 7 deletions
@@ -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("<JsEngine>", engine)
.replace("<ApplyBinaryen>", 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() {
@@ -27,6 +27,7 @@ tasks.named<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockCopyTask>("kotli
kotlin {
wasm {
<JsEngine>()
<ApplyBinaryen>
}
sourceSets {
@@ -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
)
@@ -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>(BinaryenExec::class.java) {
@Transient
@get:Internal
lateinit var binaryen: BinaryenRootExtension
init {
onlyIf {
!inputFileProperty.isPresent || inputFileProperty.asFile.map { it.exists() }.get()
}
}
@Input
var binaryenArgs: MutableList<String> = 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<String>()
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<BinaryenExec> {
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()
}
}
}
}
@@ -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
}
@@ -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<BinaryenEnv>(), 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<out Copy>
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"
}
}
@@ -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<Project> {
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<Download>("${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<Copy>(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<CleanDataTask>("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
}
}
}
@@ -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 {
@@ -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
}
@@ -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