diff --git a/.gitignore b/.gitignore index ac787777..36cb4e42 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !**/src/main/** !**/src/test/** src/main/resources/application.properties +resource-server/src/main/resources/application.properties # compile artifacts .kotlin/ diff --git a/build.gradle.kts b/build.gradle.kts index 315e08a4..ba6b86dc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,9 @@ -import org.gradle.kotlin.dsl.support.serviceOf import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.tasks.bundling.BootJar +import java.io.FileInputStream +import java.util.Properties val policy: String by System.getProperties() @@ -41,9 +42,17 @@ allprojects { maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } - afterEvaluate { - dependencies { - dependencies { +} + +setOf( + rootProject, + project(":common"), + project(":executors"), + project(":indexation"), +).forEach { project -> + project.afterEvaluate { + project.dependencies { + project.dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2") implementation(libs.kotlin.idea) { isTransitive = false @@ -53,9 +62,21 @@ allprojects { } } -val resourceDependency: Configuration by configurations.creating { + +val kotlinComposeWasmStdlibTypeInfo: Configuration by configurations.creating { + isTransitive = false isCanBeResolved = true isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.TYPEINFO + ) + } } dependencies { @@ -84,7 +105,7 @@ dependencies { } testImplementation(libs.kotlinx.coroutines.test) - resourceDependency(libs.skiko.js.wasm.runtime) + kotlinComposeWasmStdlibTypeInfo(project(":cache-maker")) } fun buildPropertyFile() { @@ -109,17 +130,27 @@ fun generateProperties(prefix: String = "") = """ libraries.folder.compose-wasm=${prefix + libComposeWasm} libraries.folder.compose-wasm-compiler-plugins=${prefix + libComposeWasmCompilerPlugins} libraries.folder.compiler-plugins=${prefix + compilerPluginsForJVM} + caches.folder.compose-wasm=${prefix + cachesComposeWasm} spring.mvc.pathmatch.matching-strategy=ant_path_matcher server.compression.enabled=true server.compression.mime-types=application/json,text/javascript,application/wasm + skiko.version=${libs.versions.skiko.get()} """.trimIndent() +val composeWasmPropertiesUpdater by tasks.registering(ComposeWasmPropertiesUpdater::class) { + dependsOn(kotlinComposeWasmStdlibTypeInfo) + propertiesPath.set(rootDir.resolve("src/main/resources/${propertyFile}").absolutePath) + typeInfoFile.set(kotlinComposeWasmStdlibTypeInfo.singleFile) +} + tasks.withType { compilerOptions { freeCompilerArgs.set(listOf("-Xjsr305=strict")) } dependsOn(":executors:jar") dependsOn(":indexation:run") + dependsOn(kotlinComposeWasmStdlibTypeInfo) + dependsOn(composeWasmPropertiesUpdater) buildPropertyFile() } println("Using Kotlin compiler ${libs.versions.kotlin.get()}") @@ -131,13 +162,16 @@ tasks.withType { val buildLambda by tasks.creating(Zip::class) { val propertyFile = propertyFile - val propertyFileContent = generateProperties("/var/task/") + val propertyFileContent = generateProperties(lambdaPrefix) from(tasks.compileKotlin) from(tasks.processResources) { eachFile { if (name == propertyFile) { + val properties = Properties().apply { load(FileInputStream(file)) } + val composeWasmHash = properties.get("dependencies.compose.wasm") file.writeText(propertyFileContent) + file.appendText("\ndependencies.compose.wasm=$composeWasmHash") } } } @@ -152,18 +186,14 @@ val buildLambda by tasks.creating(Zip::class) { from(libJVMFolder) { into(libJVM) } from(compilerPluginsForJVMFolder) {into(compilerPluginsForJVM)} from(libComposeWasmCompilerPluginsFolder) { into(libComposeWasmCompilerPlugins) } + from(kotlinComposeWasmStdlibTypeInfo) { into(cachesComposeWasm) } into("lib") { from(configurations.compileClasspath) { exclude("tomcat-embed-*") } } } tasks.named("processResources") { - val archiveOperation = project.serviceOf() - from(resourceDependency.map { - archiveOperation.zipTree(it) - }) { - into("com/compiler/server") - } + dependsOn(composeWasmPropertiesUpdater) } tasks.withType { diff --git a/buildSrc/src/main/kotlin/CacheAttribute.kt b/buildSrc/src/main/kotlin/CacheAttribute.kt new file mode 100644 index 00000000..78af16d3 --- /dev/null +++ b/buildSrc/src/main/kotlin/CacheAttribute.kt @@ -0,0 +1,10 @@ +import org.gradle.api.attributes.Attribute + +enum class CacheAttribute { + FULL, + TYPEINFO; + + companion object { + val cacheAttribute = Attribute.of("org.jetbrains.kotlin-compiler-server.cache", CacheAttribute::class.java) + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/CategoryAttribute.kt b/buildSrc/src/main/kotlin/CategoryAttribute.kt new file mode 100644 index 00000000..a10431af --- /dev/null +++ b/buildSrc/src/main/kotlin/CategoryAttribute.kt @@ -0,0 +1,5 @@ +import org.gradle.api.attributes.Category +import org.gradle.api.model.ObjectFactory + +val ObjectFactory.categoryComposeCache + get() = named(Category::class.java, "compose-cache") \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/PropertiesUpdater.kt b/buildSrc/src/main/kotlin/PropertiesUpdater.kt new file mode 100644 index 00000000..e99ffcaa --- /dev/null +++ b/buildSrc/src/main/kotlin/PropertiesUpdater.kt @@ -0,0 +1,61 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileInputStream +import java.security.MessageDigest + +abstract class ComposeWasmPropertiesUpdater : DefaultTask() { + + @get:Input + abstract val propertiesPath: Property + + @get:InputFile + abstract val typeInfoFile: RegularFileProperty + + @get:Input + abstract val propertiesMap: MapProperty + + @get:OutputFile + val updatedPropertiesFile: RegularFileProperty = project.objects.fileProperty().fileProvider(propertiesPath.map { File(it) }) + + @TaskAction + fun updateProperties() { + val file = updatedPropertiesFile.get().asFile + + propertiesMap.get().let { + if (it.isNotEmpty()) { + file.writeText("") + it.forEach { (key, value) -> + file.appendText("$key=$value\n") + } + } + } + + file.appendText( + "\ndependencies.compose.wasm=${hashFileContent(typeInfoFile.get().asFile.absolutePath)}" + ) + } +} + +fun hashFileContent(filePath: String, hashAlgorithm: String = "SHA-256"): String { + val file = File(filePath) + val digest = MessageDigest.getInstance(hashAlgorithm) + + // Read the file content in chunks and update the digest + FileInputStream(file).use { fileInputStream -> + val buffer = ByteArray(1024) + var bytesRead: Int + while (fileInputStream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + + // Convert the resulting byte array to a readable hex string + return digest.digest().joinToString("") { "%02x".format(it) } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/lambda.kt b/buildSrc/src/main/kotlin/lambda.kt new file mode 100644 index 00000000..fbb23358 --- /dev/null +++ b/buildSrc/src/main/kotlin/lambda.kt @@ -0,0 +1 @@ +val lambdaPrefix = "/var/task/" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/properties.kt b/buildSrc/src/main/kotlin/properties.kt index 36e5a253..6c641beb 100644 --- a/buildSrc/src/main/kotlin/properties.kt +++ b/buildSrc/src/main/kotlin/properties.kt @@ -1,6 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.kotlin.dsl.provideDelegate -import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.the val indexes: String by System.getProperties() @@ -25,6 +25,8 @@ val Project.libComposeWasm get() = "$kotlinVersion-compose-wasm" val Project.libComposeWasmCompilerPlugins get() = "$kotlinVersion-compose-wasm-compiler-plugins" +val Project.cachesComposeWasm + get() = "$kotlinVersion-caches-compose-wasm" val Project.libJVMFolder get() = rootProject.layout.projectDirectory.dir(libJVM) @@ -42,4 +44,7 @@ val Project.libComposeWasmFolder get() = rootProject.layout.projectDirectory.dir(libComposeWasm) val Project.libComposeWasmCompilerPluginsFolder - get() = rootProject.layout.projectDirectory.dir(libComposeWasmCompilerPlugins) \ No newline at end of file + get() = rootProject.layout.projectDirectory.dir(libComposeWasmCompilerPlugins) + +val Project.cachesComposeWasmFolder + get() = rootProject.layout.projectDirectory.dir(cachesComposeWasm) \ No newline at end of file diff --git a/cache-maker/Dockerfile b/cache-maker/Dockerfile new file mode 100644 index 00000000..d5738098 --- /dev/null +++ b/cache-maker/Dockerfile @@ -0,0 +1,20 @@ +FROM amazoncorretto:17 + +ARG BASE_DIR +ARG KOTLIN_VERSION + +RUN if [ -z "$KOTLIN_VERSION" ]; then \ + echo "Error: KOTLIN_VERSION argument is not set. Use docker-build-incremental-cache.sh to build the image." >&2; \ + exit 1; \ + fi + +ENV KOTLIN_VERSION=$KOTLIN_VERSION + +RUN mkdir -p $BASE_DIR +WORKDIR $BASE_DIR +ADD . $BASE_DIR + +RUN sed -i 's@kotlin = ".*"@kotlin = "'$KOTLIN_VERSION'"@g' gradle/libs.versions.toml +RUN ./gradlew clean + +RUN ./gradlew :cache-maker:compileProductionExecutableKotlinWasmJs diff --git a/cache-maker/build.gradle.kts b/cache-maker/build.gradle.kts new file mode 100644 index 00000000..184191fc --- /dev/null +++ b/cache-maker/build.gradle.kts @@ -0,0 +1,93 @@ +plugins { + kotlin("multiplatform") +} + +kotlin { + wasmJs { + outputModuleName.set("stdlib") + binaries.executable().forEach { + it.linkTask.configure { + compilerOptions.freeCompilerArgs.add("-Xir-dce=false") + } + } + } + + sourceSets { + wasmJsMain { + dependencies { + implementation(libs.bundles.compose) + implementation(libs.kotlinx.coroutines.core.compose.wasm) + } + } + } +} + +val composeWasmStdlib: Provider = layout.buildDirectory + .dir("compose-wasm-stdlib-output") +val composeWasmStdlibTypeInfo: Provider = composeWasmStdlib + .map { it.file("stdlib.typeinfo.bin") } + +val buildComposeWasmStdlibModule by tasks.registering(Exec::class) { + + inputs.files(configurations.named("wasmJsRuntimeClasspath")) + + workingDir = rootDir + executable = "${project.name}/docker-build-incremental-cache.sh" + + val outputDir = composeWasmStdlib + + inputs.file(layout.projectDirectory.file("Dockerfile")) + inputs.file(layout.projectDirectory.file("docker-build-incremental-cache.sh")) + outputs.dir(outputDir) + + args(lambdaPrefix, outputDir.get().asFile.normalize().absolutePath) +} + +val prepareTypeInfoIntoComposeWasmCache by tasks.registering(Sync::class) { + dependsOn(buildComposeWasmStdlibModule) + from(composeWasmStdlibTypeInfo) + into(cachesComposeWasmFolder) +} + +val kotlinComposeWasmStdlibTypeInfo: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = false + isCanBeConsumed = true + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + } +} + +kotlinComposeWasmStdlibTypeInfo.outgoing.variants.create("stdlib") { + attributes { + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.FULL + ) + } + + artifact(composeWasmStdlib) { + builtBy(prepareTypeInfoIntoComposeWasmCache) + } +} + +kotlinComposeWasmStdlibTypeInfo.outgoing.variants.create("typeinfo") { + attributes { + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.TYPEINFO + ) + } + + artifact(cachesComposeWasmFolder.file("stdlib.typeinfo.bin")) { + builtBy(prepareTypeInfoIntoComposeWasmCache) + } +} + +// we don't need to build cache-maker +tasks.named("build") { + dependsOn.clear() +} diff --git a/cache-maker/docker-build-incremental-cache.sh b/cache-maker/docker-build-incremental-cache.sh new file mode 100755 index 00000000..59f24c49 --- /dev/null +++ b/cache-maker/docker-build-incremental-cache.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +kotlinVersion=$(awk '{ if ($1 == "kotlinWasmStdlibCompiler") { gsub(/"/, "", $2); print $2; } }' FS=' = ' ./gradle/libs.versions.toml) + +baseDir=$1 +targetDir=$2 + +echo "Kotlin Version for the docker: $kotlinVersion" +echo "Base directory: $baseDir" +echo "Target directory: $targetDir" + +image_tag=my-image-name:$(date +%s) + +docker build . --file cache-maker/Dockerfile --tag $image_tag --build-arg BASE_DIR=$baseDir --build-arg KOTLIN_VERSION=$kotlinVersion + +container=$(docker create $image_tag) + +docker cp $container:$baseDir/cache-maker/build/compileSync/wasmJs/main/productionExecutable/kotlin/. $targetDir + +docker start $container +docker stop $container +docker remove $container + +docker rmi $image_tag \ No newline at end of file diff --git a/cache-maker/src/wasmJsMain/kotlin/main.kt b/cache-maker/src/wasmJsMain/kotlin/main.kt new file mode 100644 index 00000000..3ea082c2 --- /dev/null +++ b/cache-maker/src/wasmJsMain/kotlin/main.kt @@ -0,0 +1,63 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +//sampleStart +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + CanvasBasedWindow { App() } +} + +@Composable +fun App() { + MaterialTheme { + var greetingText by remember { mutableStateOf("Hello World!") } + var showImage by remember { mutableStateOf(false) } + var counter by remember { mutableStateOf(0) } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Button(onClick = { + counter++ + greetingText = "Compose: ${Greeting().greet()}" + showImage = !showImage + }) { + Text(greetingText) + } + AnimatedVisibility(showImage) { + Text(counter.toString()) + } + } + } +} + +private val platform = object : Platform { + + override val name: String + get() = "Web with Kotlin/Wasm" +} + +fun getPlatform(): Platform = platform + +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +} + +interface Platform { + val name: String +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt b/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt new file mode 100644 index 00000000..8466fc20 --- /dev/null +++ b/common/src/main/kotlin/com/compiler/server/common/components/CliUtils.kt @@ -0,0 +1,118 @@ +package com.compiler.server.common.components + +import java.io.File +import java.nio.file.Path +import java.util.* +import kotlin.io.path.* + +@OptIn(ExperimentalPathApi::class) +fun usingTempDirectory(action: (path: Path) -> T): T { + val path = getTempDirectory() + path.createDirectories() + return try { + action(path) + } finally { + path.deleteRecursively() + } +} + +private fun getTempDirectory(): Path { + val dir = System.getProperty("java.io.tmpdir") + val sessionId = UUID.randomUUID().toString().replace("-", "") + return File(dir).canonicalFile.resolve(sessionId).toPath() +} + +fun compileWasmArgs( + moduleName: String, + filePaths: List, + klibPath: String, + compilerPlugins: List, + compilerPluginOptions: List, + dependencies: List, + icDir: Path?, + log: (String) -> Unit, +): List { + val compilerPluginsArgs: List = compilerPlugins + .takeIf { it.isNotEmpty() } + ?.let { plugins -> + plugins.map { + "-Xplugin=$it" + } + compilerPluginOptions.map { + "-P=$it" + } + } ?: emptyList() + val additionalCompilerArgumentsForKLib: List = mutableListOf( + "-Xreport-all-warnings", + "-Wextra", + "-Xwasm", + "-Xir-produce-klib-dir", + "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", + "-ir-output-dir=$klibPath", + "-ir-output-name=$moduleName", + ).also { + if (icDir != null) { + typeInfoArg(icDir, log)?.let { stdlibTypeInfoArg -> + it.add(stdlibTypeInfoArg) + } + } + } + compilerPluginsArgs + + return filePaths + additionalCompilerArgumentsForKLib +} + +fun linkWasmArgs( + moduleName: String, + klibPath: String, + dependencies: List, + icDir: Path?, + outputDir: Path, + debugInfo: Boolean, + log: (String) -> Unit, +): List { + return mutableListOf( + "-Xreport-all-warnings", + "-Wextra", + "-Xwasm", + "-Xir-produce-js", + "-Xinclude=$klibPath", + "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", + "-ir-output-dir=${(outputDir / "wasm").toFile().canonicalPath}", + "-ir-output-name=$moduleName", + ).also { + if (debugInfo) it.add("-Xwasm-generate-wat") + + if (icDir != null) { + typeInfoArg(icDir, log)?.let { stdlibTypeInfoArg -> + it.add(stdlibTypeInfoArg) + } + } else { + it.add("-Xir-dce") + } + } +} + +private fun typeInfoArg( + icDir: Path, + log: (String) -> Unit, +): String? { + val allTypeInfoFiles = icDir.toFile().listFiles() ?: run { + log("No typeinfo files in $icDir, probably you need to run :cache-maker:prepareTypeInfoIntoComposeWasmCache task") + return null + } + + val stdlibTypeInfo = allTypeInfoFiles + .firstOrNull { file -> file.name.endsWith(".typeinfo.bin") } + + if (stdlibTypeInfo == null) { + log("No typeinfo files in $icDir, probably you need to run :cache-maker:prepareTypeInfoIntoComposeWasmCache task") + return null + } + + if (allTypeInfoFiles.size > 1) { + log("There are more than 1 typeinfo files in $icDir: ${allTypeInfoFiles.joinToString(", ") { it.name }}") + } + + return "-Xwasm-typeinfo-file=${stdlibTypeInfo.normalize().absolutePath}" +} + +val PATH_SEPARATOR: String = File.pathSeparator diff --git a/common/src/main/kotlin/component/KotlinEnvironment.kt b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt similarity index 98% rename from common/src/main/kotlin/component/KotlinEnvironment.kt rename to common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt index b3a995b2..60c8d3bc 100644 --- a/common/src/main/kotlin/component/KotlinEnvironment.kt +++ b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironment.kt @@ -1,6 +1,7 @@ -package component +package com.compiler.server.common.components import com.intellij.openapi.util.Disposer +import component.CompilerPluginOption import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments import org.jetbrains.kotlin.cli.common.messages.MessageCollector @@ -32,6 +33,7 @@ class KotlinEnvironment( composeWasmCompilerPlugins: List, val compilerPlugins: List = emptyList(), composeWasmCompilerPluginsOptions: List, + val composeWasmCache: File, ) { companion object { /** diff --git a/indexation/src/main/kotlin/KotlinEnvironmentConfiguration.kt b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironmentConfiguration.kt similarity index 85% rename from indexation/src/main/kotlin/KotlinEnvironmentConfiguration.kt rename to common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironmentConfiguration.kt index 14ce8819..43110fce 100644 --- a/indexation/src/main/kotlin/KotlinEnvironmentConfiguration.kt +++ b/common/src/main/kotlin/com/compiler/server/common/components/KotlinEnvironmentConfiguration.kt @@ -1,7 +1,6 @@ -package indexation +package com.compiler.server.common.components import component.CompilerPluginOption -import component.KotlinEnvironment import java.io.File class KotlinEnvironmentConfiguration( @@ -14,6 +13,7 @@ class KotlinEnvironmentConfiguration( val wasmFile = File("$fileName-wasm") val composeWasmFile = File("$fileName-compose-wasm") val composeWasmCompilerPluginsFile = File("$fileName-compose-wasm-compiler-plugins") + val composeWasmCachesFile = File("$fileName-caches-compose-wasm") val classPath = listOfNotNull(jvmFile) .flatMap { @@ -39,12 +39,8 @@ class KotlinEnvironmentConfiguration( "generateDecoys", "false" ), - CompilerPluginOption( - "androidx.compose.compiler.plugins.kotlin", - "suppressKotlinVersionCompatibilityCheck", - version - ), - ) + ), + composeWasmCachesFile ) } } diff --git a/dependencies/build.gradle.kts b/dependencies/build.gradle.kts index 5e9358ec..7bde4e2a 100644 --- a/dependencies/build.gradle.kts +++ b/dependencies/build.gradle.kts @@ -122,6 +122,7 @@ dependencies { // compose kotlinComposeWasmDependency(libs.kotlin.stdlib.wasm.js) kotlinComposeWasmDependency(libs.bundles.compose) + kotlinComposeWasmDependency(libs.kotlinx.coroutines.core.compose.wasm) composeWasmCompilerPlugins(libs.kotlin.compose.compiler.plugin) } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 116440fc..861f7a8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,5 @@ systemProp.indexesJs=indexesJs.json systemProp.indexesWasm=indexesWasm.json systemProp.indexesComposeWasm=indexesComposeWasm.json -org.gradle.configuration-cache=true \ No newline at end of file +org.gradle.configuration-cache=true +org.gradle.jvmargs=-Xmx6048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx6048M" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac9ca3a1..ee1e2fa1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] -kotlin = "2.1.0" +kotlin = "2.2.0-dev-1011" +kotlinWasmStdlibCompiler = "2.2.0-dev-1010" kotlinIdeVersion = "1.9.20-506" kotlinIdeVersionWithSuffix = "231-1.9.20-506-IJ8109.175" spring-boot = "2.7.10" @@ -10,6 +11,7 @@ junit = "4.13.2" logstash-logback-encoder = "7.3" trove4j = "1.0.20221201" kotlinx-coroutines = "1.7.3" +kotlinx-coroutines-compose-wasm = "1.8.1" kotlinx-coroutines-test = "1.6.4" kotlinx-datetime = "0.6.0-RC.2" kotlinx-io = "0.6.0" @@ -39,6 +41,7 @@ kotlin-compiler-ide = { group = "org.jetbrains.kotlin", name = "kotlin-compiler- kotlin-idea = { group = "org.jetbrains.kotlin", name = "idea", version.ref = "kotlinIdeVersionWithSuffix" } kotlin-core = { group = "org.jetbrains.kotlin", name = "core", version.ref = "kotlinIdeVersionWithSuffix" } kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-core-compose-wasm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-wasm-js", version.ref = "kotlinx-coroutines-compose-wasm" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-io-bytestring = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-bytestring", version.ref = "kotlinx-io" } diff --git a/indexation/src/main/kotlin/JvmIndexationBuilder.kt b/indexation/src/main/kotlin/JvmIndexationBuilder.kt index 153a89ac..dc603946 100644 --- a/indexation/src/main/kotlin/JvmIndexationBuilder.kt +++ b/indexation/src/main/kotlin/JvmIndexationBuilder.kt @@ -1,7 +1,7 @@ package indexation +import com.compiler.server.common.components.KotlinEnvironment import model.ImportInfo -import component.KotlinEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM import org.jetbrains.kotlin.container.getService diff --git a/indexation/src/main/kotlin/Main.kt b/indexation/src/main/kotlin/Main.kt index 3e3efa90..f0bb1d7f 100644 --- a/indexation/src/main/kotlin/Main.kt +++ b/indexation/src/main/kotlin/Main.kt @@ -1,5 +1,7 @@ package indexation +import com.compiler.server.common.components.KotlinEnvironmentConfiguration + /** * First argument is path to folder with jars * Second argument is path to output file for jvm indexes diff --git a/indexation/src/main/kotlin/WebIndexationBuilder.kt b/indexation/src/main/kotlin/WebIndexationBuilder.kt index a0b409cf..77be751f 100644 --- a/indexation/src/main/kotlin/WebIndexationBuilder.kt +++ b/indexation/src/main/kotlin/WebIndexationBuilder.kt @@ -1,7 +1,7 @@ package indexation +import com.compiler.server.common.components.KotlinEnvironment import model.ImportInfo -import component.KotlinEnvironment import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport import org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser import org.jetbrains.kotlin.config.CompilerConfiguration diff --git a/resource-server/build.gradle.kts b/resource-server/build.gradle.kts new file mode 100644 index 00000000..39f187db --- /dev/null +++ b/resource-server/build.gradle.kts @@ -0,0 +1,109 @@ +import org.gradle.kotlin.dsl.support.serviceOf +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.nio.file.Files +import kotlin.io.path.createFile + +plugins { + alias(libs.plugins.spring.dependency.management) + alias(libs.plugins.spring.boot) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.plugin.spring) +} + +kotlin.jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + vendor.set(JvmVendorSpec.AMAZON) +} + +val resourceDependency: Configuration by configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false +} + +val kotlinComposeWasmStdlibTypeInfo: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.TYPEINFO + ) + } +} + +val kotlinComposeWasmStdlib: Configuration by configurations.creating { + isTransitive = false + isCanBeResolved = true + isCanBeConsumed = false + attributes { + attribute( + Category.CATEGORY_ATTRIBUTE, + objects.categoryComposeCache + ) + attribute( + CacheAttribute.cacheAttribute, + CacheAttribute.FULL + ) + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + + resourceDependency(libs.skiko.js.wasm.runtime) + kotlinComposeWasmStdlib(project(":cache-maker")) + kotlinComposeWasmStdlibTypeInfo(project(":cache-maker")) +} + +val composeWasmPropertiesUpdater by tasks.registering(ComposeWasmPropertiesUpdater::class) { + dependsOn(kotlinComposeWasmStdlibTypeInfo) + propertiesMap.put("spring.mvc.pathmatch.matching-strategy", "ant_path_matcher") + propertiesMap.put("server.port", "8081") + propertiesMap.put("skiko.version", libs.versions.skiko.get()) + + val applicationPropertiesPath = projectDir.resolve("src/main/resources/application.properties") + + propertiesPath.set(applicationPropertiesPath.normalize().absolutePath) + + val composeWasmStdlibTypeInfo: FileCollection = kotlinComposeWasmStdlibTypeInfo + + typeInfoFile.fileProvider( + provider { + composeWasmStdlibTypeInfo.singleFile + } + ) +} + +tasks.withType { + dependsOn(composeWasmPropertiesUpdater) +} + +tasks.named("processResources") { + dependsOn(composeWasmPropertiesUpdater) + val archiveOperation = project.serviceOf() + from(resourceDependency.map { + archiveOperation.zipTree(it) + }) { + into("com/compiler/server") + } + from(kotlinComposeWasmStdlib) { + into("com/compiler/server") + } +} + +tasks.withType { + useJUnitPlatform() + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(17)) + vendor.set(JvmVendorSpec.AMAZON) + }) +} \ No newline at end of file diff --git a/resource-server/src/main/kotlin/com/compiler/server/ResourceApplication.kt b/resource-server/src/main/kotlin/com/compiler/server/ResourceApplication.kt new file mode 100644 index 00000000..55362542 --- /dev/null +++ b/resource-server/src/main/kotlin/com/compiler/server/ResourceApplication.kt @@ -0,0 +1,11 @@ +package com.compiler.server + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class ResourceApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/resource-server/src/main/kotlin/com/compiler/server/configuration/CorsConfiguration.kt b/resource-server/src/main/kotlin/com/compiler/server/configuration/CorsConfiguration.kt new file mode 100644 index 00000000..237ad05f --- /dev/null +++ b/resource-server/src/main/kotlin/com/compiler/server/configuration/CorsConfiguration.kt @@ -0,0 +1,26 @@ +package com.compiler.server.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.filter.CorsFilter + +val ACCESS_CONTROL_ALLOW_ORIGIN_VALUE: String = System.getenv("ACCESS_CONTROL_ALLOW_ORIGIN_VALUE") ?: "*" +val ACCESS_CONTROL_ALLOW_HEADER_VALUE: String = System.getenv("ACCESS_CONTROL_ALLOW_HEADER_VALUE") ?: "*" + +@Configuration +class CorsConfiguration { + @Bean + fun corsFilter(): CorsFilter { + val source = UrlBasedCorsConfigurationSource() + val config = CorsConfiguration().apply { + addAllowedHeader(ACCESS_CONTROL_ALLOW_HEADER_VALUE) + addAllowedOrigin(ACCESS_CONTROL_ALLOW_ORIGIN_VALUE) + addAllowedMethod("GET") + addAllowedMethod("POST") + } + source.registerCorsConfiguration("/**", config) + return CorsFilter(source) + } +} diff --git a/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt b/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt new file mode 100644 index 00000000..6f170333 --- /dev/null +++ b/resource-server/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt @@ -0,0 +1,76 @@ +package com.compiler.server.controllers + +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.FileSystemResource +import org.springframework.core.io.Resource +import org.springframework.http.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.concurrent.TimeUnit + + +@RestController +@RequestMapping(value = ["/api/resource", "/api/**/resource"]) +class ResourceRestController( + @Value("\${skiko.version}") private val skikoVersion: String, + @Value("\${dependencies.compose.wasm}") private val dependenciesComposeWasm: String, +) { + @Suppress("unused") + @GetMapping("/skiko-{version}.mjs") + fun getSkikoMjs(@PathVariable version: String): ResponseEntity { + if (version != skikoVersion) { + throw IllegalArgumentException("Unexpected skiko version") + } + return cacheableResource("/com/compiler/server/skiko.mjs", MediaType("text", "javascript")) + } + + @Suppress("unused") + @GetMapping("/skiko-{version}.wasm") + fun getSkikoWasm(@PathVariable version: String): ResponseEntity { + if (version != skikoVersion) { + throw IllegalArgumentException("Unexpected skiko version") + } + return cacheableResource("/com/compiler/server/skiko.wasm", MediaType("application", "wasm")) + } + + @GetMapping("/stdlib-{hash}.mjs") + fun getStdlibMjs(@PathVariable hash: String): ResponseEntity { + if (hash != dependenciesComposeWasm) { + throw IllegalArgumentException("Unexpected stdlib") + } + return cacheableResource("/com/compiler/server/stdlib.uninstantiated.mjs", MediaType("text", "javascript")) + } + + @GetMapping("/stdlib-{hash}.wasm") + fun getStdlibWasm(@PathVariable hash: String): ResponseEntity { + if (hash != dependenciesComposeWasm) { + throw IllegalArgumentException("Unexpected stdlib") + } + return cacheableResource("/com/compiler/server/stdlib.wasm", MediaType("application", "wasm")) + } + + private fun cacheableResource(path: String, mediaType: MediaType): ResponseEntity { + return resource(path, mediaType) { + cacheControl = CacheControl.maxAge(365, TimeUnit.DAYS).headerValue + } + } + + private fun resource( + path: String, + mediaType: MediaType, + headers: HttpHeaders.() -> Unit = {}, + ): ResponseEntity { + val resourcePath = javaClass.getResource(path)?.path + ?: return ResponseEntity.internalServerError().build() + + val resource = FileSystemResource(resourcePath) + val headers = HttpHeaders().apply { + contentType = mediaType + headers() + } + + return ResponseEntity(resource, headers, HttpStatus.OK) + } +} diff --git a/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt b/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt new file mode 100644 index 00000000..cde6a5e7 --- /dev/null +++ b/resource-server/src/test/kotlin/com/compiler/server/SkikoResourceTest.kt @@ -0,0 +1,83 @@ +package com.compiler.server + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpHeaders +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import java.util.concurrent.TimeUnit + +@SpringBootTest +@AutoConfigureMockMvc +class SkikoResourceTest { + @Autowired + private lateinit var mockMvc: MockMvc + + @Value("\${skiko.version}") + private lateinit var skikoVersion: String + + @Value("\${dependencies.compose.wasm}") + private lateinit var stdlibHash: String + + @Test + fun `test caching headers for skiko mjs resource`() { + testCachingHeadersForResource( + "/api/resource/skiko-$skikoVersion.mjs", + "text/javascript" + ) + } + + @Test + fun `test caching headers for skiko wasm resource`() { + testCachingHeadersForResource( + "/api/resource/skiko-$skikoVersion.wasm", + "application/wasm" + ) + } + + @Test + fun `test caching headers for stdlib mjs resource`() { + testCachingHeadersForResource( + "/api/resource/stdlib-$stdlibHash.mjs", + "text/javascript" + ) + } + + @Test + fun `test caching headers for stdlib wasm resource`() { + testCachingHeadersForResource( + "/api/resource/stdlib-$stdlibHash.wasm", + "application/wasm" + ) + } + + private fun testCachingHeadersForResource( + resourceUrl: String, + contentType: String + ) { + val expectedCacheControl = "max-age=${TimeUnit.DAYS.toSeconds(365)}" + + mockMvc + .perform(MockMvcRequestBuilders.get(resourceUrl)) + .andExpect(MockMvcResultMatchers.status().isOk) // HTTP 200 status + .andExpect( + MockMvcResultMatchers.header().exists(HttpHeaders.CACHE_CONTROL) + ) + .andExpect( + MockMvcResultMatchers.header().string( + HttpHeaders.CACHE_CONTROL, + expectedCacheControl + ) + ) + .andExpect( + MockMvcResultMatchers.header().string( + HttpHeaders.CONTENT_TYPE, + contentType + ) + ) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 60d3b651..921f9315 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,8 @@ pluginManagement { gradlePluginPortal() mavenCentral() maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap") + + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") } } plugins { @@ -16,4 +18,6 @@ plugins { include(":executors") include(":indexation") include(":common") -include(":dependencies") \ No newline at end of file +include(":dependencies") +include(":cache-maker") +include(":resource-server") \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt b/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt index 7edd8710..6b3d7b04 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/CliUtils.kt @@ -12,10 +12,11 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.* import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.cli.common.messages.MessageRenderer import org.jetbrains.kotlin.psi.KtFile -import java.io.File import java.nio.file.Path -import java.util.* -import kotlin.io.path.* +import kotlin.io.path.Path +import kotlin.io.path.div +import kotlin.io.path.pathString +import kotlin.io.path.writeText private fun minusOne(value: Int) = if (value > 0) value - 1 else value @@ -111,23 +112,6 @@ fun CLICompiler<*>.tryCompilation(inputDirectory: Path, inputFiles: List usingTempDirectory(action: (path: Path) -> T): T { - val path = getTempDirectory() - path.createDirectories() - return try { - action(path) - } finally { - path.deleteRecursively() - } -} - -private fun getTempDirectory(): Path { - val dir = System.getProperty("java.io.tmpdir") - val sessionId = UUID.randomUUID().toString().replace("-", "") - return File(dir).canonicalFile.resolve(sessionId).toPath() -} - fun List.writeToIoFiles(inputDir: Path): List { val ioFiles = map { inputDir / it.name } for ((ioFile, ktFile) in ioFiles zip this) { @@ -135,5 +119,3 @@ fun List.writeToIoFiles(inputDir: Path): List { } return ioFiles } - -val PATH_SEPARATOR: String = File.pathSeparator diff --git a/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt b/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt index 8591b19e..cae2b53a 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/ErrorAnalyzer.kt @@ -1,12 +1,12 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.KotlinEnvironment import com.compiler.server.model.* import com.intellij.openapi.util.Pair import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiErrorElement import com.intellij.psi.PsiFile -import component.KotlinEnvironment import model.Completion import org.jetbrains.kotlin.analyzer.AnalysisResult import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport diff --git a/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt index 47c8e66b..f2a76d0b 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinCompiler.kt @@ -1,12 +1,14 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.KotlinEnvironment +import com.compiler.server.common.components.PATH_SEPARATOR +import com.compiler.server.common.components.usingTempDirectory import com.compiler.server.executor.CommandLineArgument import com.compiler.server.executor.JavaExecutor import com.compiler.server.model.JvmExecutionResult import com.compiler.server.model.OutputDirectory import com.compiler.server.model.bean.LibrariesFile import com.compiler.server.model.toExceptionDescriptor -import component.KotlinEnvironment import executors.JUnitExecutors import executors.JavaRunnerExecutor import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler diff --git a/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt index 4858bb6c..6ef414ec 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinEnvironment.kt @@ -1,15 +1,16 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.KotlinEnvironment +import com.compiler.server.model.bean.CachesFile import com.compiler.server.model.bean.LibrariesFile -import com.compiler.server.model.bean.VersionInfo import component.CompilerPluginOption -import component.KotlinEnvironment import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class KotlinEnvironmentConfiguration( - val librariesFile: LibrariesFile + val librariesFile: LibrariesFile, + val cachesFile: CachesFile, ) { @Bean fun kotlinEnvironment(): KotlinEnvironment { @@ -39,7 +40,8 @@ class KotlinEnvironmentConfiguration( "generateDecoys", "false" ), - ) + ), + cachesFile.composeWasm ) } } diff --git a/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt b/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt index d90c983a..47b2e908 100644 --- a/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt +++ b/src/main/kotlin/com/compiler/server/compiler/components/KotlinToJSTranslator.kt @@ -1,11 +1,14 @@ package com.compiler.server.compiler.components +import com.compiler.server.common.components.* import com.compiler.server.model.* import com.fasterxml.jackson.databind.ObjectMapper -import component.KotlinEnvironment import org.jetbrains.kotlin.cli.js.K2JSCompiler import org.jetbrains.kotlin.psi.KtFile +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service +import java.io.File +import java.nio.file.Path import kotlin.io.path.div import kotlin.io.path.readBytes import kotlin.io.path.readText @@ -14,6 +17,8 @@ import kotlin.io.path.readText class KotlinToJSTranslator( private val kotlinEnvironment: KotlinEnvironment, ) { + private val log = LoggerFactory.getLogger(KotlinToJSTranslator::class.java) + companion object { private const val JS_IR_CODE_BUFFER = "playground.output?.buffer_1;\n" @@ -47,28 +52,38 @@ class KotlinToJSTranslator( files: List, debugInfo: Boolean, projectType: ProjectType, - translate: (List, List, List, List, Boolean) -> CompilationResult + translate: ( + List, + List, + List, + List, + File?, + Boolean, + ) -> CompilationResult ): TranslationResultWithJsCode { return try { - val (dependencies, compilerPlugins, compilerPluginOptions) = when (projectType) { - ProjectType.WASM -> listOf( + val parameters: WasmParameters = when (projectType) { + ProjectType.WASM -> WasmParameters( kotlinEnvironment.WASM_LIBRARIES, emptyList(), - emptyList() + emptyList(), + null ) - ProjectType.COMPOSE_WASM -> listOf( + ProjectType.COMPOSE_WASM -> WasmParameters( kotlinEnvironment.COMPOSE_WASM_LIBRARIES, kotlinEnvironment.COMPOSE_WASM_COMPILER_PLUGINS, - kotlinEnvironment.composeWasmCompilerPluginOptions + kotlinEnvironment.composeWasmCompilerPluginOptions, + kotlinEnvironment.composeWasmCache, ) else -> throw IllegalStateException("Wasm should have wasm or compose-wasm project type") } val compilationResult = translate( files, - dependencies, - compilerPlugins, - compilerPluginOptions, - debugInfo + parameters.dependencies, + parameters.plugins, + parameters.pluginOptions, + parameters.cacheDir, + debugInfo, ) val wasmCompilationOutput = when (compilationResult) { is Compiled -> compilationResult.result @@ -137,6 +152,7 @@ class KotlinToJSTranslator( dependencies: List, compilerPlugins: List, compilerPluginOptions: List, + cacheDir: File?, debugInfo: Boolean, ): CompilationResult = usingTempDirectory { inputDir -> @@ -146,47 +162,47 @@ class KotlinToJSTranslator( val k2JSCompiler = K2JSCompiler() val filePaths = ioFiles.map { it.toFile().canonicalPath } val klibPath = (outputDir / "klib").toFile().canonicalPath - val compilerPluginsArgs: List = compilerPlugins - .takeIf { it.isNotEmpty() } - ?.let { plugins -> - plugins.map { - "-Xplugin=$it" - } + compilerPluginOptions.map { - "-P=$it" - } - } ?: emptyList() - val additionalCompilerArgumentsForKLib: List = listOf( - "-Xreport-all-warnings", - "-Wextra", - "-Xwasm", - "-Xir-produce-klib-dir", - "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", - "-ir-output-dir=$klibPath", - "-ir-output-name=$moduleName", - ) + compilerPluginsArgs - k2JSCompiler.tryCompilation(inputDir, ioFiles, filePaths + additionalCompilerArgumentsForKLib) - .flatMap { - k2JSCompiler.tryCompilation(inputDir, ioFiles, mutableListOf( - "-Xreport-all-warnings", - "-Wextra", - "-Xwasm", - "-Xir-produce-js", - "-Xir-dce", - "-Xinclude=$klibPath", - "-libraries=${dependencies.joinToString(PATH_SEPARATOR)}", - "-ir-output-dir=${(outputDir / "wasm").toFile().canonicalPath}", - "-ir-output-name=$moduleName", - ).also { if (debugInfo) it.add("-Xwasm-generate-wat") }) - } - .map { - WasmTranslationSuccessfulOutput( - jsCode = (outputDir / "wasm" / "$moduleName.uninstantiated.mjs").readText(), - jsInstantiated = (outputDir / "wasm" / "$moduleName.mjs").readText(), - wasm = (outputDir / "wasm" / "$moduleName.wasm").readBytes(), - wat = if (debugInfo) (outputDir / "wasm" / "$moduleName.wat").readText() else null, + val compileAction: (icDir: Path?) -> CompilationResult = { icDir -> + k2JSCompiler.tryCompilation( + inputDir, + ioFiles, + compileWasmArgs( + moduleName, + filePaths, + klibPath, + compilerPlugins, + compilerPluginOptions, + dependencies, + icDir, + log::warn, ) - } + ) + .flatMap { + k2JSCompiler.tryCompilation( + inputDir, ioFiles, + linkWasmArgs( + moduleName, + klibPath, + dependencies, + icDir, + outputDir, + debugInfo, + log::warn, + ) + ) + } + .map { + WasmTranslationSuccessfulOutput( + jsCode = (outputDir / "wasm" / "$moduleName.uninstantiated.mjs").readText(), + jsInstantiated = (outputDir / "wasm" / "$moduleName.mjs").readText(), + wasm = (outputDir / "wasm" / "$moduleName.wasm").readBytes(), + wat = if (debugInfo) (outputDir / "wasm" / "$moduleName.wat").readText() else null, + ) + } + } + + compileAction(cacheDir?.toPath()) } } } @@ -209,4 +225,27 @@ data class WasmTranslationSuccessfulOutput( val jsInstantiated: String, val wasm: ByteArray, val wat: String?, -) +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WasmTranslationSuccessfulOutput + + if (jsCode != other.jsCode) return false + if (jsInstantiated != other.jsInstantiated) return false + if (!wasm.contentEquals(other.wasm)) return false + if (wat != other.wat) return false + + return true + } + + override fun hashCode(): Int { + var result = jsCode.hashCode() + result = 31 * result + jsInstantiated.hashCode() + result = 31 * result + wasm.contentHashCode() + result = 31 * result + (wat?.hashCode() ?: 0) + return result + } + +} diff --git a/src/main/kotlin/com/compiler/server/compiler/components/WasmParameters.kt b/src/main/kotlin/com/compiler/server/compiler/components/WasmParameters.kt new file mode 100644 index 00000000..2cf0f2fa --- /dev/null +++ b/src/main/kotlin/com/compiler/server/compiler/components/WasmParameters.kt @@ -0,0 +1,10 @@ +package com.compiler.server.compiler.components + +import java.io.File + +data class WasmParameters( + val dependencies: List, + val plugins: List, + val pluginOptions: List, + val cacheDir: File? +) \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt b/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt index 0a342669..9727c7ed 100644 --- a/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt +++ b/src/main/kotlin/com/compiler/server/configuration/ApplicationConfiguration.kt @@ -1,5 +1,6 @@ package com.compiler.server.configuration +import com.compiler.server.model.bean.CachesFile import com.compiler.server.model.bean.LibrariesFile import com.compiler.server.model.bean.VersionInfo import org.springframework.beans.factory.annotation.Value @@ -12,10 +13,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import java.io.File @Configuration -@EnableConfigurationProperties(value = [LibrariesFolderProperties::class]) +@EnableConfigurationProperties(value = [LibrariesFolderProperties::class, CachesFolderProperties::class]) class ApplicationConfiguration( @Value("\${kotlin.version}") private val version: String, - private val librariesFolderProperties: LibrariesFolderProperties + private val librariesFolderProperties: LibrariesFolderProperties, + private val cachesFolderProperties: CachesFolderProperties, ) : WebMvcConfigurer { override fun addFormatters(registry: FormatterRegistry) { registry.addConverter(ProjectConverter()) @@ -36,6 +38,11 @@ class ApplicationConfiguration( File(librariesFolderProperties.composeWasmCompilerPlugins), File(librariesFolderProperties.compilerPlugins) ) + + @Bean + fun cachesFiles() = CachesFile( + File(cachesFolderProperties.composeWasm) + ) } @ConfigurationProperties(prefix = "libraries.folder") @@ -46,4 +53,9 @@ class LibrariesFolderProperties { lateinit var composeWasm: String lateinit var composeWasmCompilerPlugins: String lateinit var compilerPlugins: String +} + +@ConfigurationProperties(prefix = "caches.folder") +class CachesFolderProperties { + lateinit var composeWasm: String } \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt b/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt index 93a2bc1f..a01b8611 100644 --- a/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt +++ b/src/main/kotlin/com/compiler/server/controllers/ResourceRestController.kt @@ -1,37 +1,25 @@ package com.compiler.server.controllers -import org.springframework.core.io.FileSystemResource -import org.springframework.core.io.Resource -import org.springframework.http.* +import org.springframework.beans.factory.annotation.Value import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import java.util.concurrent.TimeUnit @RestController @RequestMapping(value = ["/api/resource", "/api/**/resource"]) -class ResourceRestController { - @GetMapping("/skiko.mjs") - fun getSkikoMjs(): ResponseEntity { - return cacheableResource("/com/compiler/server/skiko.mjs", MediaType("text", "javascript")) - } - - @GetMapping("/skiko.wasm") - fun getSkikoWasm(): ResponseEntity { - return cacheableResource("/com/compiler/server/skiko.wasm", MediaType("application", "wasm")) - } - - private fun cacheableResource(path: String, mediaType: MediaType): ResponseEntity { - val resourcePath = javaClass.getResource(path)?.path - ?: return ResponseEntity.internalServerError().build() - - val resource = FileSystemResource(resourcePath) - val headers = HttpHeaders().apply { - contentType = mediaType - cacheControl = CacheControl.maxAge(365, TimeUnit.DAYS).headerValue +class ResourceRestController( + @Value("\${skiko.version}") private val skikoVersion: String, + @Value("\${dependencies.compose.wasm}") private val dependenciesComposeWasm: String, +) { + @Suppress("unused") + @GetMapping("/skiko") + fun getVersionedSkikoMjs(): String { + return skikoVersion } - return ResponseEntity(resource, headers, HttpStatus.OK) - } + @GetMapping("/stdlib") + fun getStdlibMjs(): String { + return dependenciesComposeWasm + } } diff --git a/src/main/kotlin/com/compiler/server/model/bean/CachesFile.kt b/src/main/kotlin/com/compiler/server/model/bean/CachesFile.kt new file mode 100644 index 00000000..83f11d07 --- /dev/null +++ b/src/main/kotlin/com/compiler/server/model/bean/CachesFile.kt @@ -0,0 +1,5 @@ +package com.compiler.server.model.bean + +import java.io.File + +class CachesFile(val composeWasm: File) \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt b/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt index d0177c4f..f331563d 100644 --- a/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt +++ b/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt @@ -1,15 +1,16 @@ package com.compiler.server.service +import com.compiler.server.common.components.KotlinEnvironment import com.compiler.server.compiler.KotlinFile import com.compiler.server.compiler.components.* import com.compiler.server.model.* import com.compiler.server.model.bean.VersionInfo -import component.KotlinEnvironment import model.Completion import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory import org.springframework.stereotype.Component +import java.io.File @Component class KotlinProjectExecutor( @@ -101,7 +102,14 @@ class KotlinProjectExecutor( private fun convertWasmWithConverter( project: Project, debugInfo: Boolean, - converter: (List, List, List, List, Boolean) -> CompilationResult + converter: ( + List, + List, + List, + List, + File?, + Boolean, + ) -> CompilationResult ): TranslationResultWithJsCode { return kotlinEnvironment.environment { environment -> val files = getFilesFrom(project, environment).map { it.kotlinFile }