diff --git a/benchmarks/bench-graalvm/build.gradle.kts b/benchmarks/bench-graalvm/build.gradle.kts index eda628008..e7bb5cca8 100644 --- a/benchmarks/bench-graalvm/build.gradle.kts +++ b/benchmarks/bench-graalvm/build.gradle.kts @@ -39,6 +39,10 @@ sourceSets.all { resources.setSrcDirs(listOf("jmh/resources")) } +allOpen { + annotation("org.openjdk.jmh.annotations.BenchmarkMode") +} + dependencies { implementation(libs.kotlinx.benchmark.runtime) implementation(libs.kotlinx.coroutines.test) @@ -72,6 +76,11 @@ benchmark { warmups = 5 iterations = 5 } + create("entry") { + include("EntryBenchmark") + warmups = 5 + iterations = 5 + } } targets { register("main") { @@ -109,6 +118,7 @@ tasks.withType(JMHTask::class).configureEach { jvmArgsAppend.addAll( listOf( "-XX:+UnlockExperimentalVMOptions", + "-Djava.util.concurrent.ForkJoinPool.common.parallelism=1", "-Djava.library.path=${javaLibPath.get()}", ), ) @@ -126,5 +136,33 @@ tasks.withType().configureEach jvmTarget = JvmTarget.fromTarget(javaLanguageVersion) javaParameters = true incremental = true + freeCompilerArgs.add("-Xskip-prerelease-check") } } + +fun checkNatives() { + if (!File(nativesPath).exists()) { + error("Natives not found at $nativesPath; please run `./gradlew natives -Pelide.release=true`") + } +} + +val projectRootPath: String = rootProject.layout.projectDirectory.asFile.absolutePath + +tasks.withType().configureEach { + doFirst { + checkNatives() + } + jvmArgs( + listOf( + "--enable-preview", + "--enable-native-access=ALL-UNNAMED", + "-XX:+UseG1GC", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+TrustFinalNonStaticFields", + "-Djava.library.path=${javaLibPath.get()}", + "-Delide.disableStreams=true", + "-Delide.project.root=$projectRootPath", + ), + ) +} diff --git a/benchmarks/bench-graalvm/jmh/src/benchmarks/entry/EntryBenchmark.kt b/benchmarks/bench-graalvm/jmh/src/benchmarks/entry/EntryBenchmark.kt index 9828a46ee..3e721230f 100644 --- a/benchmarks/bench-graalvm/jmh/src/benchmarks/entry/EntryBenchmark.kt +++ b/benchmarks/bench-graalvm/jmh/src/benchmarks/entry/EntryBenchmark.kt @@ -11,8 +11,75 @@ * License for the specific language governing permissions and limitations under the License. */ +@file:Suppress("unused", "FunctionName") + package benchmarks.entry +import java.io.PrintStream +import java.util.concurrent.TimeUnit +import kotlinx.benchmark.Benchmark +import kotlinx.benchmark.BenchmarkMode +import kotlinx.benchmark.Mode +import kotlinx.benchmark.OutputTimeUnit +import kotlinx.benchmark.Scope +import kotlinx.benchmark.Setup +import kotlinx.benchmark.State +import kotlinx.benchmark.Warmup + /** Benchmarks for Elide's entrypoint and related DI loading. */ +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +@Warmup(iterations = 10) class EntryBenchmark { + companion object { + @JvmStatic var originalOut: PrintStream? = null + @JvmStatic var originalErr: PrintStream? = null + @JvmStatic val projectDir: String = + requireNotNull(System.getProperty("elide.project.root")) { "Please set -Delide.project.root" } + + @JvmStatic val jsHello = "$projectDir/tools/scripts/hello.js" + @JvmStatic val tsHello = "$projectDir/tools/scripts/hello.ts" + } + + @Setup fun patchStdout() { + System.setProperty("elide.disableStreams", "true") + originalOut = System.out + originalErr = System.err + System.setOut(PrintStream(object : PrintStream(originalOut) { + override fun println(x: Any?) { + // Do nothing + } + })) + System.setErr(PrintStream(object : PrintStream(originalErr) { + override fun println(x: Any?) { + // Do nothing + } + })) + } + + @Setup fun unpatchStdout() { + System.setOut(originalOut) + System.setErr(originalErr) + } + + @Benchmark fun help(): Int { + return elide.tool.cli.entry(arrayOf("--help"), installStatics = false) + } + + @Benchmark fun exit(): Int { + return elide.tool.cli.entry(arrayOf("--exit"), installStatics = false) + } + + @Benchmark fun js(): Int { + return elide.tool.cli.entry(arrayOf("run", jsHello), installStatics = false).also { + assert(it == 0) + } + } + + @Benchmark fun ts(): Int { + return elide.tool.cli.entry(arrayOf("run", tsHello), installStatics = false).also { + assert(it == 0) + } + } } diff --git a/config/jfr/complete.jfc b/config/jfr/complete.jfc new file mode 100644 index 000000000..53d861cea --- /dev/null +++ b/config/jfr/complete.jfc @@ -0,0 +1,1128 @@ + + + + + + true + 1000 ms + + + + true + everyChunk + + + + true + 1000 ms + + + + true + everyChunk + + + + true + 1000 ms + + + + true + 10 s + + + + true + 10 s + + + + true + true + + + + true + + + + true + true + 20 ms + + + + true + true + 20 ms + + + + false + true + + + + false + + + + true + true + 20 ms + + + + true + true + + + + true + true + 20 ms + + + + true + true + 20 ms + + + + false + true + 20 ms + + + + true + true + + + + false + false + 0 ms + + + + false + false + 0 ms + + + + false + + + + false + + + + false + + + + false + + + + true + true + + + + true + true + 0 ms + + + + true + true + + + + true + true + 0 ms + + + + true + true + 0 ms + + + + true + + + + true + + + + true + beginChunk + + + + true + beginChunk + + + + true + 10 ms + + + + true + 20 ms + + + + true + 10 ms + + + + false + 10 ms + + + + false + 10 ms + + + + true + 10 ms + + + + true + true + + + + true + everyChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + everyChunk + + + + true + everyChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + false + + + + true + everyChunk + + + + true + everyChunk + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + true + + + + true + true + + + + true + + + + true + 0 ms + + + + true + 0 ms + true + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + 0 ms + + + + true + + + + true + + + + true + + + + true + + + + true + + + + true + true + + + + true + + + + true + everyChunk + + + + true + + + + true + everyChunk + + + + true + + + + true + true + 1 h + + + + true + 1000 ms + + + + true + 1000 ms + + + + true + beginChunk + + + + true + 1000 ms + + + + true + 100 ms + + + + true + 10 s + + + + true + + + + true + + + + true + + + + true + beginChunk + + + + true + everyChunk + + + + true + + + + true + beginChunk + + + + true + beginChunk + + + + true + beginChunk + + + + true + 30 s + + + + true + 30 s + + + + true + 30 s + + + + true + 30 s + + + + true + beginChunk + + + + true + 10 s + + + + true + 1000 ms + + + + true + 10 s + + + + true + beginChunk + + + + true + endChunk + + + + true + true + + + + true + 5 s + + + + true + 10 s + + + + true + beginChunk + + + + true + everyChunk + + + + true + everyChunk + + + + true + true + + + + true + true + + + + true + 1000000000/s + true + + + + true + everyChunk + + + + true + true + 0 ms + + + + true + true + 0 ms + + + + true + endChunk + + + + true + endChunk + + + + true + true + 20 ms + + + + true + true + 20 ms + + + + true + true + 20 ms + + + + true + true + 20 ms + + + + true + true + 20 ms + + + + false + true + + + + false + + + + true + beginChunk + + + + false + true + + + + false + true + + + + false + true + + + + false + true + + + + false + true + + + + true + true + + + + true + true + + + + true + 1000 ms + + + + true + + + + true + + + + false + 0 ns + + + + true + + + + true + + + + true + true + 0 ms + + + + true + true + 1 ms + + + + true + 0 ms + + + + true + 0 ms + + + + false + 0 ms + + + + false + 0 ms + + + + false + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + 0 ms + + + + true + false + + + + true + 0 ns + true + + + + true + 5 s + + + + true + 1 s + true + + + + true + endChunk + + + + true + endChunk + + + + true + endChunk + + + + true + true + forRemoval + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 ms + + 20 ms + + 20 ms + + true + + + + diff --git a/config/run/CLI (Help).run.xml b/config/run/CLI (Help).run.xml new file mode 100644 index 000000000..6b22f034c --- /dev/null +++ b/config/run/CLI (Help).run.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/packages/cli/build.gradle.kts b/packages/cli/build.gradle.kts index 59f17e2ef..c474044eb 100644 --- a/packages/cli/build.gradle.kts +++ b/packages/cli/build.gradle.kts @@ -11,9 +11,11 @@ * License for the specific language governing permissions and limitations under the License. */ @file:Suppress( - "DSL_SCOPE_VIOLATION", + "unused", "UnstableApiUsage", "UNUSED_PARAMETER", + "SpreadOperator", + "MagicNumber", ) import io.micronaut.gradle.MicronautRuntime @@ -29,6 +31,7 @@ import org.jetbrains.kotlin.konan.target.HostManager import java.nio.file.Files import java.nio.file.Path import java.util.LinkedList +import kotlin.collections.listOf import elide.internal.conventions.kotlin.KotlinTarget import elide.toolchain.host.Criteria import elide.toolchain.host.TargetCriteria @@ -50,6 +53,7 @@ plugins { alias(libs.plugins.micronaut.graalvm) alias(libs.plugins.micronaut.aot) alias(libs.plugins.elide.conventions) + id("com.jprofiler") version ("14.0.6") } // Flags affecting this build script: @@ -154,6 +158,11 @@ val enableJnaJpms = false val enableJnaStatic = false val enableSbom = oracleGvm val enableSbomStrict = false +val enableJfr = true +val enableNmt = false +val enableHeapDump = true +val enableJmx = false +val enableVerboseClassLoading = false val jniDebug = false val glibcTarget = "glibc" val dumpPointsTo = false @@ -187,6 +196,7 @@ val exclusions = listOfNotNull( // Java Launcher (GraalVM at either EA or LTS) val edgeJvmTarget = 25 val stableJvmTarget = 23 + val edgeJvm = JavaVersion.toVersion(edgeJvmTarget) val stableJvm = JavaVersion.toVersion(stableJvmTarget) val selectedJvmTarget = if (enableEdge) edgeJvmTarget else stableJvmTarget @@ -224,9 +234,7 @@ val jvmOnlyCompileArgs: List = listOfNotNull( val jvmCompileArgs = listOfNotNull( "--enable-preview", - "--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED", - "--add-opens=java.base/java.lang=ALL-UNNAMED", - "--add-exports=java.base/jdk.internal.module=ALL-UNNAMED", +// "--add-exports=java.base/jdk.internal.module=ALL-UNNAMED", // "--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk=ALL-UNNAMED", // "--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted=ALL-UNNAMED", // "--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted.c=ALL-UNNAMED", @@ -238,10 +246,13 @@ val jvmCompileArgs = listOfNotNull( ) else emptyList()) val jvmRuntimeArgs = listOf( + "-XX:+UnlockExperimentalVMOptions", "-XX:ParallelGCThreads=2", "-XX:ConcGCThreads=2", "-XX:ReservedCodeCacheSize=512m", "-XX:+TrustFinalNonStaticFields", + // "--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED", + // "--add-opens=java.base/java.lang=ALL-UNNAMED", ) val nativeEnabledModules = listOf( @@ -255,19 +266,9 @@ val nativeCompileJvmArgs = listOf( "-J$it" }) -val jvmModuleArgs = listOf( - "--add-opens=java.base/java.io=ALL-UNNAMED", - "--add-opens=java.base/java.nio=ALL-UNNAMED", -).plus(jvmCompileArgs).plus(jvmRuntimeArgs) +val jvmModuleArgs = jvmCompileArgs.plus(jvmRuntimeArgs) val ktCompilerArgs = mutableListOf( - "-Xallow-unstable-dependencies", - "-Xcontext-receivers", - "-Xemit-jvm-type-annotations", - "-Xlambdas=indy", - "-Xsam-conversions=indy", - "-Xjsr305=strict", - "-Xjvm-default=all", "-Xjavac-arguments=${jvmCompileArgs.joinToString(",")}}", // opt-in to Elide's delicate runtime API @@ -320,11 +321,9 @@ kapt { } kotlin { - target.compilations.all { - compilerOptions { - allWarningsAsErrors = true - freeCompilerArgs.set(freeCompilerArgs.get().plus(ktCompilerArgs).toSortedSet().toList()) - } + compilerOptions { + allWarningsAsErrors = true + freeCompilerArgs.set(freeCompilerArgs.get().plus(ktCompilerArgs).toSortedSet().toList()) } } @@ -336,6 +335,10 @@ sourceSets { } } +jprofiler { + installDir = file("/opt/jprofiler14/") +} + val stamp = (project.properties["elide.stamp"] as? String ?: "false").toBooleanStrictOrNull() ?: false val cliVersion = if (stamp) { libs.versions.elide.asProvider().get() @@ -735,15 +738,17 @@ val preinitializedContexts = if (!enablePreinit) emptyList() else listOfNotNull( onlyIf(enablePreinitializeAll && enableJvm, "java"), ) +val macOsxPlatform = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform" + val experimentalLlvmEdgeArgs = listOfNotNull( "-H:-CheckToolchain", ).plus(listOfNotNull( "-fuse-ld=lld", - "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", - "-I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include", - "-L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib", - "-L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks", - "-F/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks", + "-I$macOsxPlatform/Developer/SDKs/MacOSX.sdk", + "-I$macOsxPlatform/Developer/SDKs/MacOSX.sdk/usr/include", + "-L$macOsxPlatform/Developer/SDKs/MacOSX.sdk/usr/lib", + "-L$macOsxPlatform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks", + "-F$macOsxPlatform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks", "-Wno-nullability-completeness", "-Wno-deprecated-declarations", "-Wno-availability", @@ -770,6 +775,13 @@ if (!pluginApiHeader.exists()) { } val initializeAtBuildtime: List = listOf( + "elide.tool.io.RuntimeWorkdirManager", + "elide.tool.io.RuntimeWorkdirManager\$Companion", + "io.micronaut.context.DefaultApplicationContextBuilder", + "elide.tool.err.DefaultErrorHandler", + "elide.tool.err.DefaultErrorHandler\$Companion", + "elide.tool.err.DefaultStructuredErrorRecorder", + "elide.tool.err.DefaultStructuredErrorRecorder\$Companion", "elide.runtime.core.internals.graalvm.GraalVMEngine\$Companion", "org.fusesource.jansi.io.AnsiOutputStream", "com.google.common.jimfs.SystemJimfsFileSystemProvider", @@ -1033,6 +1045,16 @@ val sharedLibFlags = listOf( "--H:+JNIExportSymbols", ) +val nativeMonitoring = listOfNotNull( + onlyIf(enableJfr, "jfr"), + onlyIf(enableNmt, "nmt"), + onlyIf(enableHeapDump, "heapdump"), + "jvmstat", + onlyIf(enableJmx, "jmxserver"), + onlyIf(enableJmx, "jmxclient"), + onlyIf(enableJmx, "threaddump"), +).joinToString(",") + val commonNativeArgs = listOfNotNull( // Debugging flags: // "--verbose", @@ -1063,6 +1085,7 @@ val commonNativeArgs = listOfNotNull( "--link-at-build-time=org.pkl", "--link-at-build-time=picocli", "--enable-native-access=org.graalvm.truffle,ALL-UNNAMED", + "--enable-monitoring=${nativeMonitoring}", "-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk=ALL-UNNAMED", "-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted=ALL-UNNAMED", "-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted.c=ALL-UNNAMED", @@ -1082,7 +1105,19 @@ val commonNativeArgs = listOfNotNull( "-H:ExcludeResources=META-INF/.*kotlin_module", "-H:ExcludeResources=com/sun/jna/.*", "-H:ExcludeResources=META-INF/native/libsqlite.*", + "-H:ExcludeResources=lib/linux-x86_64/libbrotli.so", + "-H:ExcludeResources=META-INF/native/x86_64-unknown-linux-gnu/libsqlitejdbc.so", "-H:ExcludeResources=META-INF/micronaut/io.micronaut.core.graal.GraalReflectionConfigurer/.*", + "-H:ExcludeResources=META-INF/elide/embedded/runtime/wasm/runtime.json", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/.*", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/coll/zh.res", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/coll/ucadata.icu", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/brkitr/khmerdict.dict", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/coll/ko.res", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/brkitr/burmesedict.dict", + "-H:ExcludeResources=org/graalvm/shadowed/com/ibm/icu/impl/data/icudt74b/brkitr/thaidict.dict", + "-H:ExcludeResources=META-INF/native/libnetty_transport_native_epoll_aarch_64.so", + "-H:ExcludeResources=META-INF/native/libnetty_transport_native_io_uring_x86_64.so", "-Delide.strict=true", "-Delide.js.vm.enableStreams=true", "-Delide.mosaic=$enableMosaic", @@ -1135,6 +1170,11 @@ val commonNativeArgs = listOfNotNull( "-Dmicronaut.executors.io.threads=2", "-Dmicronaut.executors.io.type=VIRTUAL", "-Dmicronaut.executors.scheduled.threads=1", + "-Djansi.eager=false", + "-Djava.util.concurrent.ForkJoinPool.common.parallelism=1", + "-Dkotlinx.coroutines.scheduler.core.pool.size=2", + "-Dkotlinx.coroutines.scheduler.max.pool.size=2", + "-Dkotlinx.coroutines.scheduler.default.name=ElideDefault", onlyIf(enablePreinit, "-Dpolyglot.image-build-time.PreinitializeContexts=$preinitContextsList"), onlyIf(enablePreinit, "-Dpolyglot.image-build-time.PreinitializeContextsWithNative=true"), onlyIf(enablePreinit, "-Dpolyglot.image-build-time.PreinitializeAllowExperimentalOptions=true"), @@ -1175,7 +1215,7 @@ val debugFlags: List = listOfNotNull( ).onlyIf(isDebug) val experimentalFlags = listOf( - "-H:+SupportContinuations", // -H:+SupportContinuations is in use, but is not supported together with Truffle JIT compilation + "-H:+SupportContinuations", // -H:+SupportContinuations is in use, but is not supported together with Truffle JIT... "-H:+UseStringInlining", // String inlining optimization is not supported when just-in-time compilation is used // Not enabled for regular builds yet @@ -1252,11 +1292,15 @@ val releaseCFlags: List = listOf( ) // PGO profiles to specify in release mode. -val profiles: List = listOf( +val profiles: List = if (enableEdge) listOf( "js-brotli.iprof", "pkl-eval.iprof", "ts-hello.iprof", "ts-sqlite.iprof", +) else listOf( + "js-path-fs.iprof", + "js-sqlite.iprof", + "pkl-eval.iprof", ) // GVM release flags @@ -1315,8 +1359,7 @@ val jvmDefs = mutableMapOf( "org.sqlite.lib.exportPath" to nativesPath, "io.netty.native.workdir" to nativesPath, "io.netty.native.deleteLibAfterLoading" to false.toString(), - "java.util.concurrent.ForkJoinPool.common.parallelism" to "4", - "java.util.concurrent.ForkJoinPool.common.maximumSpares" to "128", + "java.util.concurrent.ForkJoinPool.common.parallelism" to "2", // "java.util.concurrent.ForkJoinPool.common.threadFactory" to "", // "java.util.concurrent.ForkJoinPool.common.exceptionHandler" to "", ) @@ -1391,7 +1434,6 @@ val linuxOnlyArgs = defaultPlatformArgs.plus( "-H:NativeLinkerOption=/home/sam/workspace/elide/target/x86_64-unknown-linux-gnu/debug/libjs.so", "-H:NativeLinkerOption=/home/sam/workspace/elide/target/x86_64-unknown-linux-gnu/debug/libposix.so", "-H:NativeLinkerOption=/home/sam/workspace/elide/target/x86_64-unknown-linux-gnu/debug/libterminal.so", - // "/home/sam/workspace/elide/target/x86_64-unknown-linux-gnu/debug/build/terminal-543d7c2278c5a0d2/out/libterminalcore.a", "-H:ExcludeResources=.*dylib", "-H:ExcludeResources=.*jnilib", "-H:ExcludeResources=.*dll", @@ -1932,6 +1974,10 @@ tasks { "-verbose:jni", "-Xlog:library=trace", ).onlyIf(jniDebug) + ).plus( + listOf( + "-verbose:class", + ).onlyIf(enableVerboseClassLoading) )) standardInput = System.`in` @@ -2017,7 +2063,11 @@ tasks { systemProperty(it.key, it.value) } - jvmArgs(jvmModuleArgs) + jvmArgs(jvmModuleArgs.plus( + listOf( + "-verbose:class", + ).onlyIf(enableVerboseClassLoading) + )) standardInput = System.`in` standardOutput = System.out @@ -2041,6 +2091,7 @@ tasks { } withType().configureEach { + options.release.set(selectedJvmTarget) options.compilerArgs.addAll(jvmCompileArgs.plus(jvmOnlyCompileArgs)) } @@ -2156,3 +2207,19 @@ fun BuildNativeImageTask.createFinalizer() { tasks.withType().all { createFinalizer() } + +//val runProfiled by tasks.registering(com.profiler.gradle.JavaProfile::class.java) { +// mainClass = "com.mycorp.MyMainClass" +// classpath = sourceSets.main.runtimeClasspath +// offline = true +// sessionId = 80 +// configFile = file("path/to/jprofiler_config.xml") +//} + +//task run(type: com.jprofiler.gradle.JavaProfile) { +// mainClass = 'com.mycorp.MyMainClass' +// classpath sourceSets.main.runtimeClasspath +// offline = true +// sessionId = 80 +// configFile = file('path/to/jprofiler_config.xml') +//} diff --git a/packages/cli/detekt-baseline.xml b/packages/cli/detekt-baseline.xml index 1515d5a0c..0abe3e236 100644 --- a/packages/cli/detekt-baseline.xml +++ b/packages/cli/detekt-baseline.xml @@ -3,79 +3,35 @@ ComplexCondition:RuntimeWorkdirManager.kt$RuntimeWorkdirManager$(base != null && (base.parentFile == null || base.absolutePath == "/")) || files.isEmpty() - ComplexCondition:ToolShellCommand.kt$ToolShellCommand.LanguageSelector$ENABLE_JVM && (jvm || kotlin || (alias != null && jvmAliases.contains(alias))) - ComplexCondition:ToolShellCommand.kt$ToolShellCommand.LanguageSelector$ENABLE_JVM && (kotlin || (alias != null && ktAliases.contains(alias))) - ComplexCondition:ToolShellCommand.kt$ToolShellCommand.LanguageSelector$ENABLE_PYTHON && (python || (alias != null && pyAliases.contains(alias))) - ComplexCondition:ToolShellCommand.kt$ToolShellCommand.LanguageSelector$ENABLE_RUBY && (ruby || (alias != null && rbAliases.contains(alias))) - ComplexCondition:ToolShellCommand.kt$ToolShellCommand.LanguageSelector$ENABLE_TYPESCRIPT && (typescript || (alias != null && tsAliases.contains(alias))) - ConstructorParameterNaming:AbstractSubcommand.kt$AbstractSubcommand.DefaultOutputController$private val _logger: Logger - ConstructorParameterNaming:AbstractSubcommand.kt$AbstractSubcommand.DefaultOutputController$private val _settings: ToolState.OutputSettings = _state.output - ConstructorParameterNaming:AbstractSubcommand.kt$AbstractSubcommand.DefaultOutputController$private val _state: State - ConstructorParameterNaming:AbstractSubcommand.kt$AbstractSubcommand.ToolExecutionContextImpl$private val _state: T - FunctionParameterNaming:Statics.kt$Statics$`in`: InputStream LargeClass:ToolShellCommand.kt$ToolShellCommand : AbstractSubcommand LoopWithTooManyJumpStatements:ToolShellCommand.kt$ToolShellCommand$while - MagicNumber:Elide.kt$Elide$30 - MagicNumber:Mosaic.kt$20 - MagicNumber:Mosaic.kt$250 - MagicNumber:Mosaic.kt$40 - MagicNumber:Mosaic.kt$500 - MagicNumber:NativeUtil.kt$NativeUtil$8192 MagicNumber:RuntimeWorkdirManager.kt$RuntimeWorkdirManager$15 MagicNumber:ToolShellCommand.kt$ToolShellCommand$1000L MagicNumber:ToolShellCommand.kt$ToolShellCommand$120 MagicNumber:ToolShellCommand.kt$ToolShellCommand$40 MagicNumber:ToolShellCommand.kt$ToolShellCommand$80 MatchingDeclarationName:SanityTests.kt$SanitySelfTest : SelfTest - MaxLineLength:ToolShellCommand.kt$ToolShellCommand$" or: elide @|bold,fg(cyan) run|shell|serve|start|@ [OPTIONS] [@|bold,fg(cyan) -c|@|@|bold,fg(cyan) --code|@ CODE]" - ObjectPropertyNaming:Statics.kt$Statics$val `in`: InputStream get() = delegatedInStream.get() ?: System.`in` ReturnCount:ExecutionController.kt$ExecutionController$private fun toHost(polyglotException: PolyglotException): Throwable - ReturnCount:NativeUtil.kt$NativeUtil$@Suppress("LongParameterList") @JvmStatic internal fun loadOrCopy( workdir: File, path: String, libName: String, loader: ClassLoader, allCandidatePaths: Sequence<Path>, forceCopy: Boolean = false, forceLoad: Boolean = false, loadFromPath: Boolean = true, ): Pair<Boolean, Boolean> ReturnCount:RuntimeWorkdirManager.kt$RuntimeWorkdirManager$private fun nearestDirectoryWithAnyOfTheseFiles( files: Array<String>, base: File? = null, depth: Int? = null, ): File? - SpreadOperator:Elide.kt$Elide.Companion$(*args) SpreadOperator:ToolInvokeCommand.kt$ToolInvokeCommand$(*selectedTools.map { it to buildArgs(action, it) }.toTypedArray()) - SpreadOperator:main.kt$(*args) SwallowedException:DefaultProjectManager.kt$DefaultProjectManager.Companion$ioe: IOException SwallowedException:DefaultProjectManager.kt$DefaultProjectManager.Companion$thr: Throwable - SwallowedException:Elide.kt$Elide.Companion$uoe: UnsupportedOperationException - SwallowedException:NativeEngine.kt$NativeEngine$err: Throwable - SwallowedException:NativeUtil.kt$NativeUtil$thr: Throwable SwallowedException:PersistedError.kt$PersistedError.RuntimeInfo$err: Throwable - SwallowedException:ToolShellCommand.kt$ToolShellCommand$eof: EndOfFileException - SwallowedException:ToolShellCommand.kt$ToolShellCommand$err: IOException - SwallowedException:ToolShellCommand.kt$ToolShellCommand$exc: NoSuchElementException - SwallowedException:ToolShellCommand.kt$ToolShellCommand$ioe: Exception - SwallowedException:ToolShellCommand.kt$ToolShellCommand$userInterrupt: UserInterruptException - ThrowsCount:NativeUtil.kt$NativeUtil$@Suppress("LongParameterList") @JvmStatic internal fun loadOrCopy( workdir: File, path: String, libName: String, loader: ClassLoader, allCandidatePaths: Sequence<Path>, forceCopy: Boolean = false, forceLoad: Boolean = false, loadFromPath: Boolean = true, ): Pair<Boolean, Boolean> - TooGenericExceptionCaught:AbstractSubcommand.kt$AbstractSubcommand$err: Throwable TooGenericExceptionCaught:AbstractToolCommand.kt$AbstractToolCommand$err: Throwable TooGenericExceptionCaught:CommandTestRunner.kt$CommandTestRunner$thr: Throwable TooGenericExceptionCaught:DefaultProjectManager.kt$DefaultProjectManager.Companion$thr: Throwable - TooGenericExceptionCaught:Mosaic.kt$err: Throwable - TooGenericExceptionCaught:NativeEngine.kt$NativeEngine$err: Throwable - TooGenericExceptionCaught:NativeUtil.kt$NativeUtil$e: Exception - TooGenericExceptionCaught:NativeUtil.kt$NativeUtil$ex: Throwable - TooGenericExceptionCaught:NativeUtil.kt$NativeUtil$thr: Throwable TooGenericExceptionCaught:PersistedError.kt$PersistedError.RuntimeInfo$err: Throwable TooGenericExceptionCaught:TestContext.kt$TestContext.Companion$err: Throwable TooGenericExceptionCaught:ToolInvokeCommand.kt$ToolInvokeCommand$err: Throwable - TooGenericExceptionCaught:ToolShellCommand.kt$ToolShellCommand$e: Exception - TooGenericExceptionCaught:ToolShellCommand.kt$ToolShellCommand$exc: Exception - TooGenericExceptionCaught:ToolShellCommand.kt$ToolShellCommand$ioe: Exception - TooGenericExceptionCaught:main.kt$err: RuntimeException - TooGenericExceptionCaught:main.kt$err: Throwable TopLevelPropertyNaming:ToolInvokeCommand.kt$private const val jsHint = "package.json" TopLevelPropertyNaming:ToolInvokeCommand.kt$private const val pyHint = "requirements.txt" UnusedParameter:AbstractToolCommand.kt$AbstractToolCommand$args: Array<String> - UnusedParameter:Elide.kt$Elide.Companion$win32: Boolean = false - UnusedParameter:Mosaic.kt$totalTests: Int - UnusedParameter:NativeEngine.kt$NativeEngine$extraProps: List<Pair<String, String>> - UnusedPrivateMember:Mosaic.kt$@Composable private fun Summary(start: Long, totalTests: Int, tests: List<Test>) + UnusedParameter:LanguageSelector.kt$LanguageSelector$project: () -> ProjectInfo? UnusedPrivateProperty:AbstractToolCommand.kt$AbstractToolCommand$// Initialization state. private val initialized: AtomicBoolean = AtomicBoolean(false) UnusedPrivateProperty:ExecutionController.kt$ExecutionController$private val tRunException: Lazy<Value> = Lazy.of { loadClass("jdk.jshell.spi.ExecutionControl\$RunException") } UnusedPrivateProperty:ToolInvokeCommand.kt$// Sub-commands for Orogene. private val orogeneActions = sortedSetOf( "add", "apply", "login", "logout", "ping", "reapply", "remove", "view", "help", ) UnusedPrivateProperty:ToolInvokeCommand.kt$// Sub-commands for Uv. private val uvActions = sortedSetOf( "pip", "tool", "toolchain", "venv", "cache", "self", "version", "help", ) VariableNaming:AbstractScriptEngineFactory.kt$AbstractScriptEngineFactory.PolyglotContext$private val `in`: PolyglotReader - VariableNaming:ToolShellCommand.kt$ToolShellCommand.InspectorConfig$/** Specifies whether the inspector should suspend for internal (facade) sources. */ @Option( names = ["--inspect:internal"], description = ["Specifies whether the inspector should suspend for internal (facade) sources"], defaultValue = "false", hidden = false, ) internal var `internal`: Boolean = false + VariableNaming:InspectorConfig.kt$InspectorConfig$/** Specifies whether the inspector should suspend for internal (facade) sources. */ @Option( names = ["--inspect:internal"], description = ["Specifies whether the inspector should suspend for internal (facade) sources"], defaultValue = "false", hidden = false, ) internal var `internal`: Boolean = false diff --git a/packages/cli/profiles-25x.zip b/packages/cli/profiles-25x.zip new file mode 100644 index 000000000..b26b8bb4c Binary files /dev/null and b/packages/cli/profiles-25x.zip differ diff --git a/packages/cli/profiles.zip b/packages/cli/profiles.zip index b26b8bb4c..c640ded33 100644 Binary files a/packages/cli/profiles.zip and b/packages/cli/profiles.zip differ diff --git a/packages/cli/src/main/java/elide/tool/cli/InertLoggerConfigurator.java b/packages/cli/src/main/java/elide/tool/cli/InertLoggerConfigurator.java new file mode 100644 index 000000000..83b0263c2 --- /dev/null +++ b/packages/cli/src/main/java/elide/tool/cli/InertLoggerConfigurator.java @@ -0,0 +1,8 @@ +package elide.tool.cli; + +/** + * Held as an inert class for the purpose of `java.logging` initialization. + */ +public class InertLoggerConfigurator { + public InertLoggerConfigurator() {} +} diff --git a/packages/cli/src/main/kotlin/dev/elide/cli/bridge/CliNativeBridge.kt b/packages/cli/src/main/kotlin/dev/elide/cli/bridge/CliNativeBridge.kt index f7271b3bb..87303c5c8 100644 --- a/packages/cli/src/main/kotlin/dev/elide/cli/bridge/CliNativeBridge.kt +++ b/packages/cli/src/main/kotlin/dev/elide/cli/bridge/CliNativeBridge.kt @@ -2,7 +2,6 @@ package dev.elide.cli.bridge -import com.aayushatharva.brotli4j.Brotli4jLoader import org.graalvm.nativeimage.ImageInfo /** @@ -14,9 +13,6 @@ object CliNativeBridge { // If flipped, the CLI native bridge will only be eagerly loaded under native conditions. private const val EAGER_LOAD_NATIVE_ONLY = false - /** Token expected for the tooling API at version 1. */ - const val VERSION_V1: String = "v1" - /** Native platform-agnostic library name for Elide's umbrella library. */ private const val NATIVE_LIB_NAME = "umbrella" @@ -28,16 +24,11 @@ object CliNativeBridge { if (!initialized && (!EAGER_LOAD_NATIVE_ONLY || ImageInfo.inImageCode())) { System.loadLibrary(NATIVE_LIB_NAME) initialized = true - val init = initializeNative().also { loadThirdPartyNatives() } + val init = initializeNative() assert(init == 0) { "Failed to initialize native layer; got code $init" } } } - // Load third-party native libraries. - private fun loadThirdPartyNatives() { - Brotli4jLoader.ensureAvailability() - } - /** Initialize the native runtime layer; any non-zero return value indicates an error. */ private external fun initializeNative(): Int @@ -64,8 +55,4 @@ object CliNativeBridge { /** Run the Uv entrypoint. */ external fun runUv(args: Array): Int - - init { - initialize() - } } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/AbstractSubcommand.kt b/packages/cli/src/main/kotlin/elide/tool/cli/AbstractSubcommand.kt index cc1530271..2d75cb5d1 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/AbstractSubcommand.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/AbstractSubcommand.kt @@ -32,8 +32,7 @@ import java.util.* import java.util.concurrent.Executors import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ThreadFactory -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference +import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext import elide.runtime.Logger @@ -70,7 +69,6 @@ import org.graalvm.polyglot.Engine as VMEngine private const val enableVirtualThreads = true private const val enableFixedThreadPool = false private const val enableFlexibleThreadPool = true - private val _stdout = System.out private val _stderr = System.err private val _stdin = System.`in` @@ -111,19 +109,25 @@ import org.graalvm.polyglot.Engine as VMEngine private val _cpus = Runtime.getRuntime().availableProcessors() - private val threadFactory: ToolThreadFactory = ToolThreadFactory( - enableVirtualThreads, - DefaultErrorHandler.acquire(), - ) - private val threadedExecutor: ListeningExecutorService = when { - enableVirtualThreads -> MoreExecutors.listeningDecorator(Executors.newThreadPerTaskExecutor(threadFactory)) - enableFixedThreadPool -> MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(_cpus, threadFactory)) - enableFlexibleThreadPool -> MoreExecutors.listeningDecorator( - MoreExecutors.getExitingScheduledExecutorService(ScheduledThreadPoolExecutor(_cpus, threadFactory)), + private val threadFactory: ToolThreadFactory by lazy { + ToolThreadFactory( + enableVirtualThreads, + DefaultErrorHandler.acquire(), ) - else -> MoreExecutors.listeningDecorator(Executors.newCachedThreadPool(threadFactory)) } - private val dispatcher: CoroutineDispatcher = threadedExecutor.asCoroutineDispatcher() + + private val threadedExecutor: ListeningExecutorService by lazy { + when { + enableVirtualThreads -> MoreExecutors.listeningDecorator(Executors.newThreadPerTaskExecutor(threadFactory)) + enableFixedThreadPool -> MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(_cpus, threadFactory)) + enableFlexibleThreadPool -> MoreExecutors.listeningDecorator( + MoreExecutors.getExitingScheduledExecutorService(ScheduledThreadPoolExecutor(_cpus, threadFactory)), + ) + else -> MoreExecutors.listeningDecorator(Executors.newCachedThreadPool(threadFactory)) + } + } + + private val dispatcher: CoroutineDispatcher by lazy { threadedExecutor.asCoroutineDispatcher() } private val logging: Logger by lazy { Statics.logging @@ -251,6 +255,7 @@ import org.graalvm.polyglot.Engine as VMEngine } /** Default output controller implementation. */ + @Suppress("ConstructorParameterNaming") protected open class DefaultOutputController ( private val _state: State, private val _logger: Logger, @@ -297,7 +302,7 @@ import org.graalvm.polyglot.Engine as VMEngine } /** Private implementation of tool execution context. */ - protected abstract class ToolExecutionContextImpl constructor (private val _state: T) : ToolContext { + protected abstract class ToolExecutionContextImpl (private val data: T) : ToolContext { internal companion object { @JvmStatic fun forSuite( state: T, @@ -314,7 +319,7 @@ import org.graalvm.polyglot.Engine as VMEngine } /** Return the calculated tool state. */ - override val state: T get() = _state + override val state: T get() = data } // Shared resources which should be closed at the conclusion of processing. @@ -334,7 +339,7 @@ import org.graalvm.polyglot.Engine as VMEngine * * @see createEngine */ - protected val engine: AtomicReference = AtomicReference() + private val engine = atomic(null) /** Controller for tool output. */ protected lateinit var out: OutputController @@ -349,7 +354,7 @@ import org.graalvm.polyglot.Engine as VMEngine protected lateinit var context: ToolContext /** Whether this is an interactive session. */ - val interactive: AtomicBoolean = AtomicBoolean(false) + private var interactive = false /** Debug flag status. */ val debug: Boolean get() = commons.debug @@ -363,8 +368,14 @@ import org.graalvm.polyglot.Engine as VMEngine /** Pretty output flag status. */ val pretty: Boolean get() = commons.pretty + internal fun enableInteractive() { + interactive = true + } + + internal fun isInteractive(): Boolean = interactive + // Base execution context. - private val baseExecContext: CoroutineContext = Dispatchers.Default + CoroutineName("elide") + private val baseExecContext: CoroutineContext = Dispatchers.Unconfined + CoroutineName("elide") override val coroutineContext: CoroutineContext get() = baseExecContext /** @@ -378,6 +389,10 @@ import org.graalvm.polyglot.Engine as VMEngine configureEngine(langs) } + internal fun engineSafe(): PolyglotEngine? = engine.value + + internal fun engine(): PolyglotEngine = engine.value!! + // Build an initial `ToolState` instance from the main tool. private fun materializeInitialState(): ToolState { return ToolState.EMPTY @@ -431,23 +446,16 @@ import org.graalvm.polyglot.Engine as VMEngine close() // @TODO(sgammon): base injection bug - if (!Statics.args.get().contains("--quiet") && interactive.get()) runBlocking { - out.pretty( - { - line("Exiting session. Have a great day! \uD83D\uDC4B") - }, - { - line("Exited session") - }, - ) + if (Statics.args.contains("--verbose") && interactive && !Statics.disableStreams) { + println("Exiting session. Have a great day! \uD83D\uDC4B") } }, ) } @Synchronized protected fun resolveEngine(langs: EnumSet): PolyglotEngine { - return when (val ready = engine.get()) { - null -> createEngine(langs).also { engine.set(it) } + return when (val ready = engine.value) { + null -> createEngine(langs).also { engine.value = it } else -> ready } } @@ -477,10 +485,11 @@ import org.graalvm.polyglot.Engine as VMEngine /** * TBD. */ + @Suppress("TooGenericExceptionCaught") override fun close() { sharedResources.forEach { try { - logging.trace("Cleaning shared resource", it) + logging.trace("Cleaning shared resource {}", it) it.close() } catch (err: Throwable) { logging.error("Caught exception while closing shared resource '$it' (ignored)", err) diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/AbstractToolCommand.kt b/packages/cli/src/main/kotlin/elide/tool/cli/AbstractToolCommand.kt index 189577321..b380432e8 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/AbstractToolCommand.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/AbstractToolCommand.kt @@ -190,7 +190,7 @@ abstract class AbstractToolCommand: * * @return Exit code from running the command. */ - suspend fun exec(args: Array): CommandResult = execute(Dispatchers.Default) { + suspend fun exec(args: Array): CommandResult = execute(Dispatchers.Unconfined) { invoke(it).also(commandResult::set) // enter context and invoke } @@ -200,7 +200,7 @@ abstract class AbstractToolCommand: * @return Exit code from running the command. */ override fun call(): Int { - return execute(Dispatchers.Default) { + return execute(Dispatchers.Unconfined) { invoke(it).also(commandResult::set) // enter context and invoke }.exitCode } @@ -235,7 +235,7 @@ abstract class AbstractToolCommand: */ abstract suspend fun Context.invoke(state: CommandState): CommandResult - override suspend fun enter(): CommandResult = execute(Dispatchers.Default) { + override suspend fun enter(): CommandResult = execute(Dispatchers.Unconfined) { invoke(it).also(commandResult::set) // enter context and invoke } } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/Elide.kt b/packages/cli/src/main/kotlin/elide/tool/cli/Elide.kt index 3f6fe37a2..ef14986f0 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/Elide.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/Elide.kt @@ -11,7 +11,7 @@ * License for the specific language governing permissions and limitations under the License. */ -@file:Suppress("MnInjectionPoints", "MaxLineLength") +@file:Suppress("MnInjectionPoints", "MaxLineLength", "unused", "NOTHING_TO_INLINE") package elide.tool.cli @@ -23,7 +23,6 @@ import io.micronaut.context.BeanContext import io.micronaut.context.annotation.ContextConfigurer import org.graalvm.nativeimage.ImageInfo import org.graalvm.nativeimage.ProcessProperties -import org.slf4j.bridge.SLF4JBridgeHandler import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.Help @@ -49,6 +48,18 @@ import elide.tool.engine.NativeEngine import elide.tool.err.DefaultErrorHandler import elide.tool.io.RuntimeWorkdirManager +// Default timeout to apply to non-server commands. +private const val DEFAULT_CMD_TIMEOUT = 30 + +private val applicationContextBuilder = ApplicationContext + .builder() + .environments("cli") + .defaultEnvironments("cli") + .eagerInitAnnotated(Eager::class.java) + .eagerInitSingletons(false) + .eagerInitConfiguration(true) + .deduceEnvironment(false) + .deduceCloudEnvironment(false) /** Entrypoint for the main Elide command-line tool. */ @Command( @@ -92,18 +103,38 @@ import elide.tool.io.RuntimeWorkdirManager @Suppress("MemberVisibilityCanBePrivate") @Context @Singleton class Elide : ToolCommandBase() { companion object { - init { - SLF4JBridgeHandler.removeHandlersForRootLogger() - SLF4JBridgeHandler.install() - initializeNatives() - } - /** Name of the tool. */ const val TOOL_NAME: String = "elide" // Whether to log early init messages. private const val INIT_LOGGING: Boolean = false + // Application context; initialized early for CLI use. + @Volatile private lateinit var applicationContext: ApplicationContext + + private val bundle = ResourceBundle.getBundle("ElideTool", Locale.US) + private val errHandler = DefaultErrorHandler.acquire() + + // Builder for the CLI entrypoint. + private val cliBuilder get() = CommandLine(Elide::class.java, object: CommandLine.IFactory { + override fun create(cls: Class?): K? { + return MicronautFactory(applicationContext).create(cls) + } + }).apply { + setResourceBundle(bundle) + setExitCodeExceptionMapper(errHandler) + setCommandName(TOOL_NAME) + setAbbreviatedOptionsAllowed(true) + setAbbreviatedSubcommandsAllowed(true) + setCaseInsensitiveEnumValuesAllowed(true) + setEndOfOptionsDelimiter("--") + setPosixClusteredShortOptionsAllowed(true) + setUsageHelpAutoWidth(true) + } + + // Whether native libraries have loaded. + @Volatile private var nativeLibsLoaded: Boolean = false + // Properties which cannot be set by users. private val blocklistedProperties = sortedSetOf( "elide.js.vm.enableStreams", @@ -112,34 +143,47 @@ import elide.tool.io.RuntimeWorkdirManager // Init logging. @JvmStatic private fun initLog(message: String) { if (INIT_LOGGING) { - System.err.println("[elide:init] $message") + System.err.println("[entry] $message") + } + } + + @JvmStatic fun requestNatives(server: Boolean = false, tooling: Boolean = false) { + if (!nativeLibsLoaded) { + nativeLibsLoaded = true + initializeNatives(server, tooling) } } - @JvmStatic private fun initializeNatives() { + @JvmStatic @Synchronized private fun initializeNatives(server: Boolean, tooling: Boolean) { // load natives initLog("Initializing natives") - NativeEngine.boot(RuntimeWorkdirManager.acquire()) { - listOf( - "elide.js.vm.enableStreams" to "true", - "jdk.httpclient.allowRestrictedHeaders" to "Host,Content-Length", // needed for fetch - "io.netty.allocator.maxOrder" to "3", - "io.netty.serviceThreadPrefix" to "elide-svc", - "io.netty.native.deleteLibAfterLoading" to "true", // reversed bc of bug (actually does not delete) - "io.netty.buffer.bytebuf.checkAccessible" to "false", - org.fusesource.jansi.AnsiConsole.JANSI_MODE to org.fusesource.jansi.AnsiConsole.JANSI_MODE_FORCE, - org.fusesource.jansi.AnsiConsole.JANSI_GRACEFUL to "false", - ) + + NativeEngine.boot( + { RuntimeWorkdirManager.acquire() }, + server = server, + tooling = tooling, + ) { + emptyList() + }.also { + if (tooling) { + dev.elide.cli.bridge.CliNativeBridge.initialize() + } } } @JvmStatic private fun initializeTerminal() { + if (Statics.disableStreams) { + return + } assert(!ImageInfo.inImageBuildtimeCode()) initLog("Initializing ANSI system console") org.fusesource.jansi.AnsiConsole.systemInstall() } - @JvmStatic private fun initializeTerminalNative(win32: Boolean = false) { + @JvmStatic private fun initializeTerminalNative() { + if (Statics.disableStreams) { + return + } // this trick is necessary to capture the out/err streams set up by `AnsiConsole`, which performs those steps in // private code; after the streams are captured, they are uninstalled and instead mounted via the native image // I/O stream wrappers, which will delegate to the assigned streams anyway. @@ -199,7 +243,7 @@ import elide.tool.io.RuntimeWorkdirManager val runner = { when { // build-time code cannot swap the out/err streams, but instead must delegate via the native I/O wrapper. - ImageInfo.inImageBuildtimeCode() -> initializeTerminalNative(isWindows) + ImageInfo.inImageBuildtimeCode() -> initializeTerminalNative() // otherwise, we can simply swap the streams so that the ANSI terminal takes over. !org.fusesource.jansi.AnsiConsole.isInstalled() -> initializeTerminal() @@ -215,21 +259,10 @@ import elide.tool.io.RuntimeWorkdirManager } } - private fun cleanup() { - // no-op - } - /** CLI entrypoint and [args]. */ - @JvmStatic fun entry(args: Array): Int = try { - // load and install libraries - Statics.args.set(args.toList()) - - val binPath = ProcessHandle.current().info().command().orElse(null) - installStatics(binPath, args, System.getProperty("user.dir")) + @JvmStatic fun entry(args: Array): Int { initLog("Firing entrypoint") - exec(args) - } finally { - cleanup() + return exec(args) } /** Unwind and clean up shared resources on exit. */ @@ -242,55 +275,51 @@ import elide.tool.io.RuntimeWorkdirManager // Private execution entrypoint for customizing core Picocli settings. @Suppress("SpreadOperator") - @JvmStatic internal fun exec(args: Array): Int = ApplicationContext - .builder() - .environments("cli") - .defaultEnvironments("cli") - .eagerInitAnnotated(Eager::class.java) - .eagerInitSingletons(false) - .eagerInitConfiguration(true) - .deduceEnvironment(false) - .deduceCloudEnvironment(false) - .args(*args) - .also { initLog("Starting application context") } - .start() - .also { initLog("Application context started; loading tool entrypoint") } - .use { - val locale = Locale.of("en", "US") - Locale.setDefault(locale) - initLog("Preparing CLI configuration (locale: $locale)") - val bundle = ResourceBundle.getBundle("ElideTool", locale) - initLog("Loaded resource bundle") - val errHandler = DefaultErrorHandler.acquire() - initLog("Constructing CLI and Micronaut factory") - CommandLine(Elide::class.java, MicronautFactory(it)) - .setCommandName(TOOL_NAME) - .setResourceBundle(bundle) - .setAbbreviatedOptionsAllowed(true) - .setAbbreviatedSubcommandsAllowed(true) - .setCaseInsensitiveEnumValuesAllowed(true) - .setEndOfOptionsDelimiter("--") - .setExitCodeExceptionMapper(errHandler) - .setPosixClusteredShortOptionsAllowed(true) - .setUsageHelpAutoWidth(true) - .setColorScheme( - Help.defaultColorScheme( - if (args.find { arg -> arg == "--no-pretty" || arg == "--pretty=false" } != null || - System.getenv("NO_COLOR") != null) { - Help.Ansi.OFF - } else { - Help.Ansi.ON - }, - ), - ).let { - initLog("Entering application context") - it.execute(*args).also { - initLog("Finished execution") - } - } - }.also { - initLog("Exiting application context") + @JvmStatic internal inline fun exec(args: Array): Int { + // special case: exit immediately with `0` if `--exit` is provided as the first and only argument. + if (args.size == 1 && args[0] == "--exit") { + return 0 } + initLog("Preparing context") + return applicationContextBuilder + .args(*args) + .also { initLog("Starting application context") } + .start() + .also { initLog("Application context started; loading tool entrypoint") } + .use { + val locale = Locale.of("en", "US") + Locale.setDefault(locale) + initLog("Preparing CLI configuration (locale: $locale)") + + applicationContext = it + cliBuilder.apply { + setColorScheme( + Help.defaultColorScheme( + if (args.find { arg -> arg == "--no-pretty" || arg == "--pretty=false" } != null || + System.getenv("NO_COLOR") != null) { + Help.Ansi.OFF + } else { + Help.Ansi.ON + }, + ) + ) + }.let { + initLog("Entering application context") + + // special case: print `--help` directly to the statically-assigned out-stream; needed for testing and + // other monkeypatch-oriented use cases + if (args.isNotEmpty() && args[0] == "--help") { + it.usage(Statics.err) + return 0 + } + it.execute(*args).also { + initLog("Finished execution") + } + } + }.also { + initLog("Exiting application context") + } + } /** Configures the Micronaut binary. */ @ContextConfigurer internal class ToolConfigurator: ApplicationContextConfigurer { @@ -321,7 +350,7 @@ import elide.tool.io.RuntimeWorkdirManager defaultValue = "30", scope = ScopeType.INHERIT, ) - internal var timeout: Int = 30 + internal var timeout: Int = DEFAULT_CMD_TIMEOUT /** Source file shortcut alias. */ @Parameters( @@ -345,7 +374,7 @@ import elide.tool.io.RuntimeWorkdirManager override suspend fun CommandContext.invoke(state: CommandState): CommandResult { // proxy to the `shell` command for a naked run - return beanContext.getBean(ToolShellCommand::class.java).apply { + val bean = beanContext.getBean(ToolShellCommand::class.java).apply { this@Elide.srcfile?.let { if (!ToolShellCommand.languageAliasToEngineId.contains(it)) { runnable = it @@ -356,7 +385,8 @@ import elide.tool.io.RuntimeWorkdirManager languageHint = it } } - call() - }.commandResult.get() + } + bean.call() + return bean.commandResult.get() } } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/Statics.kt b/packages/cli/src/main/kotlin/elide/tool/cli/Statics.kt index c1b9e5f1a..e6c7ef17b 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/Statics.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/Statics.kt @@ -11,19 +11,24 @@ * License for the specific language governing permissions and limitations under the License. */ +@file:Suppress("FunctionParameterNaming", "ObjectPropertyNaming") + package elide.tool.cli import java.io.InputStream +import java.io.OutputStream import java.io.PrintStream import java.util.concurrent.atomic.AtomicReference +import kotlinx.atomicfu.atomic import elide.runtime.Logger import elide.runtime.Logging /** Internal static tools and utilities used across the Elide CLI. */ internal object Statics { - private val delegatedInStream: AtomicReference = AtomicReference() - private val delegatedOutStream: AtomicReference = AtomicReference() - private val delegatedErrStream: AtomicReference = AtomicReference() + val disableStreams = System.getProperty("elide.disableStreams") == "true" + private val delegatedInStream = atomic(null) + private val delegatedOutStream = atomic(null) + private val delegatedErrStream = atomic(null) /** Main tool logger. */ internal val logging: Logger by lazy { @@ -37,24 +42,47 @@ internal object Statics { /** Whether to disable color output and syntax highlighting. */ internal val noColor: Boolean by lazy { - System.getenv("NO_COLOR") != null || args.get().let { args -> + System.getenv("NO_COLOR") != null || args.let { args -> args.contains("--no-pretty") || args.contains("--no-color") } } + private val initialArgs = atomic>(emptyArray()) + /** Invocation args. */ - internal val args: AtomicReference> = AtomicReference(emptyList()) + internal val args: Array = initialArgs.value - /** Main top-level tool. */ - val base: AtomicReference = AtomicReference() + // Stream which drops all data. + private val noOpStream by lazy { + PrintStream(object : OutputStream() { + override fun write(b: Int) = Unit + }) + } - val `in`: InputStream get() = delegatedInStream.get() ?: System.`in` - val out: PrintStream get() = delegatedOutStream.get() ?: System.out - val err: PrintStream get() = delegatedErrStream.get() ?: System.err + val `in`: InputStream get() = + delegatedInStream.value ?: System.`in` + + val out: PrintStream get() = + when (disableStreams) { + true -> noOpStream + else -> delegatedOutStream.value ?: System.out + } + + val err: PrintStream get() = + when (disableStreams) { + true -> noOpStream + else -> delegatedErrStream.value ?: System.err + } + + internal fun mountArgs(args: Array) { + check(initialArgs.value.isEmpty()) { "Args are not initialized yet!" } + initialArgs.value = args + } internal fun assignStreams(out: PrintStream, err: PrintStream, `in`: InputStream) { - delegatedOutStream.set(out) - delegatedErrStream.set(err) - delegatedInStream.set(`in`) + if (disableStreams) return + delegatedOutStream.value = out + delegatedErrStream.value = err + delegatedInStream.value = `in` } } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/discord/ToolDiscordCommand.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/discord/ToolDiscordCommand.kt index 1c4128629..00ddde7e9 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/discord/ToolDiscordCommand.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/discord/ToolDiscordCommand.kt @@ -33,7 +33,7 @@ import elide.tool.cli.ToolState mixinStandardHelpOptions = true, ) @Introspected -@Singleton internal class ToolDiscordCommand : AbstractSubcommand() { +@Singleton internal open class ToolDiscordCommand : AbstractSubcommand() { companion object { private const val REDIRECT_TARGET = "https://elide.dev/discord" private val REDIRECT_URI = URI(REDIRECT_TARGET) diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/pkl/ToolPklCommand.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/pkl/ToolPklCommand.kt index fcd3b04cc..dcd2c2b03 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/pkl/ToolPklCommand.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/pkl/ToolPklCommand.kt @@ -35,7 +35,7 @@ import elide.tool.cli.ToolState class ToolPklCommand : AbstractSubcommand() { @Suppress("TooGenericExceptionCaught") override suspend fun CommandContext.invoke(state: ToolContext): CommandResult { - val args = Statics.args.get().let { args -> + val args = Statics.args.let { args -> args.drop(args.indexOf("pkl") + 1) } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/DebugConfig.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/DebugConfig.kt new file mode 100644 index 000000000..cdedae6bd --- /dev/null +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/DebugConfig.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +package elide.tool.cli.cmd.repl + +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.ReflectiveAccess +import picocli.CommandLine.Option +import elide.runtime.core.PolyglotEngineConfiguration +import elide.runtime.plugins.debug.debug + +/** Specifies settings for the Debug Adapter Protocol host. */ +@Introspected @ReflectiveAccess class DebugConfig { + /** Specifies whether the debugger should suspend immediately at execution start. */ + @Option( + names = ["--debug:suspend"], + description = ["Whether the debugger should suspend execution immediately."], + defaultValue = "false", + ) + internal var suspend: Boolean = false + + /** Specifies whether the debugger should suspend for internal (facade) sources. */ + @Option( + names = ["--debug:wait"], + description = ["Whether to wait for the debugger to attach before executing any code at all."], + defaultValue = "false", + ) + internal var wait: Boolean = false + + /** Specifies the port the debugger should bind to. */ + @Option( + names = ["--debug:port"], + description = ["Set the port the debugger binds to"], + defaultValue = "4711", + ) + internal var port: Int = 0 + + /** Specifies the host the debugger should bind to. */ + @Option( + names = ["--debug:host"], + description = ["Set the host the debugger binds to"], + defaultValue = "localhost", + ) + internal var host: String = "" + + /** Apply these settings to the root engine configuration container. */ + internal fun apply(config: PolyglotEngineConfiguration) { + // install and configure the Debug plugin + config.debug { + debugAdapter { + suspend = this@DebugConfig.suspend + waitAttached = wait + + host = this@DebugConfig.host + port = this@DebugConfig.port + } + } + } +} diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/EnvironmentConfig.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/EnvironmentConfig.kt new file mode 100644 index 000000000..05c5698f4 --- /dev/null +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/EnvironmentConfig.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +package elide.tool.cli.cmd.repl + +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.ReflectiveAccess +import picocli.CommandLine.Option +import elide.runtime.core.PolyglotEngineConfiguration +import elide.runtime.plugins.env.EnvConfig.EnvVariableSource.DOTENV +import elide.runtime.plugins.env.environment +import elide.tool.cli.cfg.ElideCLITool +import elide.tool.project.ProjectInfo + +/** Specifies settings for application environment. */ +@Introspected @ReflectiveAccess class EnvironmentConfig { + /** Specifies whether the runtime should honor dotenv files. */ + @Option( + names = ["--env:dotenv"], + description = ["Whether to honor .env files; defaults to `true`"], + defaultValue = "true", + ) + internal var dotenv: Boolean = true + + /** Specifies whether the runtime should honor dotenv files. */ + @Option( + names = ["--env"], + description = ["Additional environment variables to set, in x=y format"], + arity = "0..N", + ) + internal var envVars: Map = emptyMap() + + /** Apply these settings to created execution contexts. */ + @Suppress("KotlinConstantConditions") + internal fun apply( + project: ProjectInfo?, + config: PolyglotEngineConfiguration, + host: Boolean = false, + dotenv: Boolean = true, + ) = config.environment { + // inject `NODE_ENV` + environment("NODE_ENV", if (ElideCLITool.ELIDE_RELEASE_TYPE == "DEV") { + "development" + } else { + "production" + }) + + if (host) System.getenv().entries.forEach { + mapToHostEnv(it.key) + } + + // apply project-level environment variables first (if applicable) + project?.env?.vars?.forEach { + if (it.value.isPresent) { + if (it.value.source == DOTENV && !dotenv) { + return@forEach // skip .env vars if so instructed + } + environment(it.key, it.value.value) + } + } + + // apply manually-installed environment variables + envVars.forEach { + if (it.value.isNotBlank() && it.value.isNotBlank()) { + environment(it.key, it.value) + } + } + } +} diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/InspectorConfig.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/InspectorConfig.kt new file mode 100644 index 000000000..22a204453 --- /dev/null +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/InspectorConfig.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +package elide.tool.cli.cmd.repl + +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.ReflectiveAccess +import picocli.CommandLine.Option +import elide.runtime.core.PolyglotEngineConfiguration +import elide.runtime.plugins.debug.debug + +/** Specifies settings for the Chrome DevTools inspector. */ +@Introspected @ReflectiveAccess class InspectorConfig { + @Option( + names = ["--inspect"], + description = ["Whether to enable the Chrome Devtools inspector"], + defaultValue = "false", + ) + internal var enabled: Boolean = false + + /** Specifies whether the inspector should suspend immediately at execution start. */ + @Option( + names = ["--inspect:suspend"], + description = ["Whether the inspector should suspend execution immediately."], + defaultValue = "false", + ) + internal var suspend: Boolean = false + + /** Specifies whether the inspector should suspend for internal (facade) sources. */ + @Option( + names = ["--inspect:internal"], + description = ["Specifies whether the inspector should suspend for internal (facade) sources"], + defaultValue = "false", + hidden = false, + ) + internal var `internal`: Boolean = false + + /** Specifies whether the inspector should suspend for internal (facade) sources. */ + @Option( + names = ["--inspect:wait"], + description = ["Whether to wait for the inspector to attach before executing any code at all."], + defaultValue = "false", + ) + internal var wait: Boolean = false + + /** Specifies the port the inspector should bind to. */ + @Option( + names = ["--inspect:port"], + description = ["Set the port the inspector binds to"], + defaultValue = "4200", + ) + internal var port: Int = 0 + + /** Specifies the host the inspector should bind to. */ + @Option( + names = ["--inspect:host"], + description = ["Set the host the inspector binds to"], + defaultValue = "localhost", + ) + internal var host: String = "" + + /** Specifies the path the inspector should bind to. */ + @Option( + names = ["--inspect:path"], + description = ["Set a custom path for the inspector"], + ) + internal var path: String? = null + + /** Specifies paths where sources are available. */ + @Option( + names = ["--inspect:sources"], + arity = "0..N", + description = ["Add a source directory to the inspector path. Specify 0-N times."], + ) + internal var sources: List = emptyList() + + /** Apply these settings to the root engine configuration container. */ + internal fun apply(config: PolyglotEngineConfiguration) { + if (!enabled) return + + // install and configure the Debug plugin + config.debug { + chromeInspector { + enabled = this@InspectorConfig.enabled + + suspend = this@InspectorConfig.suspend + internal = this@InspectorConfig.internal + waitAttached = wait + + host = this@InspectorConfig.host + port = this@InspectorConfig.port + + path = this@InspectorConfig.path + sourcePaths = sources + } + } + } +} diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/LanguageSelector.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/LanguageSelector.kt new file mode 100644 index 000000000..841ec1140 --- /dev/null +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/LanguageSelector.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024-2025 Elide Technologies, Inc. + * + * Licensed under the MIT license (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://opensource.org/license/mit/ + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under the License. + */ + +package elide.tool.cli.cmd.repl + +import com.google.common.collect.Sets +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.ReflectiveAccess +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Option +import java.util.EnumSet +import elide.tool.cli.GuestLanguage +import elide.tool.cli.GuestLanguage.JS +import elide.tool.cli.GuestLanguage.JVM +import elide.tool.cli.GuestLanguage.KOTLIN +import elide.tool.cli.GuestLanguage.PYTHON +import elide.tool.cli.GuestLanguage.RUBY +import elide.tool.cli.GuestLanguage.TYPESCRIPT +import elide.tool.cli.GuestLanguage.WASM +import elide.tool.cli.cmd.repl.ToolShellCommand.Companion.languageAliasToEngineId +import elide.tool.project.ProjectInfo + +/** Allows selecting a language by name. */ +@Introspected @ReflectiveAccess class LanguageSelector { + /** Specifies the guest language(s) to support. */ + @Option( + names = ["--language", "-l"], + description = ["Specify language by name. Options: \${COMPLETION-CANDIDATES}."], + ) + internal var language: EnumSet? = null + + /** Flag for a JavaScript VM. */ + @Option( + names = ["--js", "--javascript", "-js"], + description = ["Equivalent to passing '--language=JS'."], + ) + internal var javascript: Boolean = false + + /** Flag for JavaScript with TypeScript support/ */ + @Option( + names = ["--ts", "--typescript", "-ts"], + description = ["Equivalent to passing '--language=TYPESCRIPT'."], + ) + internal var typescript: Boolean = false + + /** Flag for JVM support. */ + @Option( + names = ["--jvm", "--java", "-java"], + description = ["Equivalent to passing '--language=JVM'."], + ) + internal var jvm: Boolean = false + + /** Flag for Kotlin support. */ + @Option( + names = ["--kotlin", "--kt", "-kt"], + description = ["Equivalent to passing '--language=KOTLIN'."], + ) + internal var kotlin: Boolean = jvm + + /** Flag for Ruby support. */ + @Option( + names = ["--ruby", "--rb", "-rb"], + description = ["Equivalent to passing '--language=RUBY'."], + ) + internal var ruby: Boolean = false + + /** Flag for Python support. */ + @Option( + names = ["--python", "--py", "-py"], + description = ["Equivalent to passing '--language=PYTHON'."], + ) + internal var python: Boolean = false + + /** Flag for WebAssembly support. */ + @Option( + names = ["--wasm"], + description = ["Equivalent to passing '--language=WASM'."], + ) + internal var wasm: Boolean = false + + /** Flag for LLVM support. */ + @Option( + names = ["--llvm"], + description = ["Equivalent to passing '--language=LLVM'."], + ) + internal var llvm: Boolean = false + + private fun maybeMatchLanguagesByAlias( + first: String?, + second: String?, + langs: EnumSet, + ): GuestLanguage? { + val maybeResolvedFirst = first?.ifBlank { null }?.let { + languageAliasToEngineId[it.trim().lowercase()] + } + val maybeResolvedSecond = second?.ifBlank { null }?.let { + languageAliasToEngineId[it.trim().lowercase()] + } + return langs.firstOrNull { + it.id == maybeResolvedFirst || it.id == maybeResolvedSecond + } + } + + // Resolve the primary interactive language. + internal fun primary( + spec: CommandSpec?, + langs: EnumSet, + project: () -> ProjectInfo?, + languageHint: String?, + ): GuestLanguage { + // languages by flags + val explicitlySelectedLanguagesByBoolean = listOf( + JS to javascript, + TYPESCRIPT to typescript, + RUBY to ruby, + PYTHON to python, + JVM to jvm, + KOTLIN to kotlin, + WASM to wasm, + ).filter { + langs.contains(it.first) // is it supported? + }.filter { + it.second // was it requested? + }.map { + it.first + }.toSet() + + // languages by name + val explicitlySelectedLanguagesBySet = Sets.intersection(language ?: emptySet(), langs) + + // language by alias + val candidateArgs = spec?.commandLine()?.parseResult?.originalArgs() + val languageHintMatch = langs.firstOrNull { it.id == languageHint } + val candidateByName = languageHintMatch ?: maybeMatchLanguagesByAlias( + candidateArgs?.firstOrNull(), + candidateArgs?.getOrNull(1), + langs, + ) + + val selected = ( + // `elide python` et al take maximum precedence + candidateByName ?: + + // then languages specified via the `--languages` flag ("named") + explicitlySelectedLanguagesBySet.firstOrNull() ?: + + // then languages specified via boolean flags like `--python` + explicitlySelectedLanguagesByBoolean.firstOrNull() + ) + return when { + // if there is an explicitly selected language, and it is supported, use it + selected != null && langs.contains(selected) -> selected + + // otherwise, we default to javascript + else -> JS + } + } +} diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/ToolShellCommand.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/ToolShellCommand.kt index 080bc428e..1ca7cec7a 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/ToolShellCommand.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/repl/ToolShellCommand.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Elide Technologies, Inc. + * Copyright (c) 2024-2025 Elide Technologies, Inc. * * Licensed under the MIT license (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at @@ -13,15 +13,14 @@ @file:Suppress( "UNUSED_PARAMETER", + "MaxLineLength", ) package elide.tool.cli.cmd.repl import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.ConsoleAppender -import com.google.common.collect.Sets import io.micronaut.core.annotation.Introspected -import io.micronaut.core.annotation.ReflectiveAccess import io.micronaut.core.io.IOUtils import org.graalvm.polyglot.PolyglotException import org.graalvm.polyglot.Source @@ -43,7 +42,6 @@ import org.jline.utils.AttributedString import org.jline.utils.AttributedStyle import org.slf4j.LoggerFactory import picocli.CommandLine.* -import picocli.CommandLine.Model.CommandSpec import java.io.* import java.net.URI import java.nio.charset.StandardCharsets @@ -52,12 +50,12 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.* import java.util.concurrent.Phaser -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.TimeUnit import java.util.function.Supplier -import javax.script.ScriptEngineManager +import java.util.stream.Stream import javax.tools.ToolProvider +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.future.asCompletableFuture import kotlin.io.path.Path import kotlin.io.path.absolutePathString import kotlin.io.path.exists @@ -76,14 +74,10 @@ import elide.runtime.gvm.GuestError import elide.runtime.gvm.GraalVMGuest import elide.runtime.gvm.internals.IntrinsicsManager import elide.runtime.intrinsics.server.http.HttpServerAgent -import elide.runtime.plugins.debug.debug -import elide.runtime.plugins.env.EnvConfig.EnvVariableSource.DOTENV -import elide.runtime.plugins.env.environment import elide.runtime.plugins.vfs.VfsListener import elide.runtime.plugins.vfs.vfs import elide.tool.cli.* import elide.tool.cli.GuestLanguage.* -import elide.tool.cli.cfg.ElideCLITool import elide.tool.cli.cfg.ElideCLITool.GVM_RESOURCES import elide.tool.cli.err.ShellError import elide.tool.cli.options.AccessControlOptions @@ -148,6 +142,12 @@ private typealias ContextAccessor = () -> PolyglotContext Logging.of(ToolShellCommand::class) } + private val tsAliases = setOf("ts", "typescript") + private val pyAliases = setOf("py", "python") + private val rbAliases = setOf("rb", "ruby") + private val jvmAliases = setOf("jvm", "java") + private val ktAliases = setOf("kt", "kotlin") + // Maps language aliases to the engine they should invoke. internal val languageAliasToEngineId: SortedMap = sortedMapOf( "py" to "python", @@ -162,477 +162,79 @@ private typealias ContextAccessor = () -> PolyglotContext "kotlin" to "jvm", "kt" to "jvm", ) - } - - /** [SystemRegistryImpl] that filters for special REPL commands. */ - private class ReplSystemRegistry( - parser: Parser, - terminal: Terminal, - workDir: Supplier, - configPath: ConfigurationPath, - ) : SystemRegistryImpl(parser, terminal, workDir, configPath) { - override fun isCommandOrScript(command: String): Boolean { - return command.startsWith("!") || super.isCommandOrScript(command) - } - } - - /** Allows selecting a language by name. */ - @Introspected @ReflectiveAccess class LanguageSelector { - /** Specifies the guest language(s) to support. */ - @Option( - names = ["--language", "-l"], - description = ["Specify language by name. Options: \${COMPLETION-CANDIDATES}."], - ) - internal var language: EnumSet? = null - - /** Flag for a JavaScript VM. */ - @Option( - names = ["--js", "--javascript", "-js"], - description = ["Equivalent to passing '--language=JS'."], - ) - internal var javascript: Boolean = false - - /** Flag for JavaScript with TypeScript support/ */ - @Option( - names = ["--ts", "--typescript", "-ts"], - description = ["Equivalent to passing '--language=TYPESCRIPT'."], - ) - internal var typescript: Boolean = false - - /** Flag for JVM support. */ - @Option( - names = ["--jvm", "--java", "-java"], - description = ["Equivalent to passing '--language=JVM'."], - ) - internal var jvm: Boolean = false - - /** Flag for Kotlin support. */ - @Option( - names = ["--kotlin", "--kt", "-kt"], - description = ["Equivalent to passing '--language=KOTLIN'."], - ) - internal var kotlin: Boolean = jvm - - /** Flag for Ruby support. */ - @Option( - names = ["--ruby", "--rb", "-rb"], - description = ["Equivalent to passing '--language=RUBY'."], - ) - internal var ruby: Boolean = false - - /** Flag for Python support. */ - @Option( - names = ["--python", "--py", "-py"], - description = ["Equivalent to passing '--language=PYTHON'."], - ) - internal var python: Boolean = false - - /** Flag for WebAssembly support. */ - @Option( - names = ["--wasm"], - description = ["Equivalent to passing '--language=WASM'."], - ) - internal var wasm: Boolean = false - - /** Flag for LLVM support. */ - @Option( - names = ["--llvm"], - description = ["Equivalent to passing '--language=LLVM'."], - ) - internal var llvm: Boolean = false - - private fun maybeMatchLanguagesByAlias( - first: String?, - second: String?, - langs: EnumSet, - ): GuestLanguage? { - val maybeResolvedFirst = first?.ifBlank { null }?.let { - languageAliasToEngineId[it.trim().lowercase()] - } - val maybeResolvedSecond = second?.ifBlank { null }?.let { - languageAliasToEngineId[it.trim().lowercase()] - } - return langs.firstOrNull { - it.id == maybeResolvedFirst || it.id == maybeResolvedSecond - } - } - - // Resolve the primary interactive language. - internal fun primary( - spec: CommandSpec?, - langs: EnumSet, - project: ProjectInfo?, - languageHint: String?, - ): GuestLanguage { - // languages by flags - val explicitlySelectedLanguagesByBoolean = listOf( - JS to javascript, - TYPESCRIPT to typescript, - RUBY to ruby, - PYTHON to python, - JVM to jvm, - KOTLIN to kotlin, - WASM to wasm, - ).filter { - langs.contains(it.first) // is it supported? - }.filter { - it.second // was it requested? - }.map { - it.first - }.toSet() - - // languages by name - val explicitlySelectedLanguagesBySet = Sets.intersection(language ?: emptySet(), langs) - - // language by alias - val candidateArgs = spec?.commandLine()?.parseResult?.originalArgs() - val languageHintMatch = langs.firstOrNull { it.id == languageHint } - val candidateByName = languageHintMatch ?: maybeMatchLanguagesByAlias( - candidateArgs?.firstOrNull(), - candidateArgs?.getOrNull(1), - langs, - ) - - val selected = ( - // `elide python` et al take maximum precedence - candidateByName ?: - - // then languages specified via the `--languages` flag ("named") - explicitlySelectedLanguagesBySet.firstOrNull() ?: - - // then languages specified via boolean flags like `--python` - explicitlySelectedLanguagesByBoolean.firstOrNull() - ) - return when { - // if there is an explicitly selected language, and it is supported, use it - selected != null && langs.contains(selected) -> selected - - // otherwise, we default to javascript - else -> JS - } - } - - private val tsAliases = setOf("ts", "typescript") - private val pyAliases = setOf("py", "python") - private val rbAliases = setOf("rb", "ruby") - private val jvmAliases = setOf("jvm", "java") - private val ktAliases = setOf("kt", "kotlin") - - // Resolve the specified language. - @Suppress("ComplexCondition") - internal fun resolveLangs(project: ProjectInfo? = null, alias: String? = null): EnumSet { - return EnumSet.noneOf(GuestLanguage::class.java).apply { - add(JS) - add(WASM) - if (ENABLE_TYPESCRIPT && (typescript || (alias != null && tsAliases.contains(alias)))) add(TYPESCRIPT) - if (ENABLE_PYTHON && (python || (alias != null && pyAliases.contains(alias)))) add(PYTHON) - if (ENABLE_RUBY && (ruby || (alias != null && rbAliases.contains(alias)))) add(RUBY) - if (ENABLE_JVM && (jvm || kotlin || (alias != null && jvmAliases.contains(alias)))) add(JVM) - if (ENABLE_JVM && (kotlin || (alias != null && ktAliases.contains(alias)))) add(KOTLIN) - } - } - } - - /** Specifies settings for the Chrome DevTools inspector. */ - @Introspected @ReflectiveAccess class InspectorConfig { - @Option( - names = ["--inspect"], - description = ["Whether to enable the Chrome Devtools inspector"], - defaultValue = "false", - ) - internal var enabled: Boolean = false - /** Specifies whether the inspector should suspend immediately at execution start. */ - @Option( - names = ["--inspect:suspend"], - description = ["Whether the inspector should suspend execution immediately."], - defaultValue = "false", - ) - internal var suspend: Boolean = false - - /** Specifies whether the inspector should suspend for internal (facade) sources. */ - @Option( - names = ["--inspect:internal"], - description = ["Specifies whether the inspector should suspend for internal (facade) sources"], - defaultValue = "false", - hidden = false, - ) - internal var `internal`: Boolean = false - - /** Specifies whether the inspector should suspend for internal (facade) sources. */ - @Option( - names = ["--inspect:wait"], - description = ["Whether to wait for the inspector to attach before executing any code at all."], - defaultValue = "false", - ) - internal var wait: Boolean = false + // Whether the last-seen command was a user exit. + private val exitSeen = atomic(false) - /** Specifies the port the inspector should bind to. */ - @Option( - names = ["--inspect:port"], - description = ["Set the port the inspector binds to"], - defaultValue = "4200", - ) - internal var port: Int = 0 + // Count of statement lines seen; used as an offset for the length of `allSeenStatements`. + private val statementCounter = atomic(0) - /** Specifies the host the inspector should bind to. */ - @Option( - names = ["--inspect:host"], - description = ["Set the host the inspector binds to"], - defaultValue = "localhost", - ) - internal var host: String = "" + // Language-specific syntax highlighter. + private val langSyntax = atomic(null) - /** Specifies the path the inspector should bind to. */ - @Option( - names = ["--inspect:path"], - description = ["Set a custom path for the inspector"], - ) - internal var path: String? = null + // Main operating terminal. + private val terminal = atomic(null) - /** Specifies paths where sources are available. */ - @Option( - names = ["--inspect:sources"], - arity = "0..N", - description = ["Add a source directory to the inspector path. Specify 0-N times."], - ) - internal var sources: List = emptyList() + // Active line reader. + private val lineReader = atomic(null) - /** Apply these settings to the root engine configuration container. */ - internal fun apply(config: PolyglotEngineConfiguration) { - if (!enabled) return + // Server manager + private val serverAgent: HttpServerAgent by lazy { HttpServerAgent() } - // install and configure the Debug plugin - config.debug { - chromeInspector { - enabled = this@InspectorConfig.enabled + // Synchronization primitive used to coordinate server behavior + private val phaser = atomic(Phaser(1)) - suspend = this@InspectorConfig.suspend - internal = this@InspectorConfig.internal - waitAttached = wait + // Whether a server is running. + private val serverRunning = atomic(false) - host = this@InspectorConfig.host - port = this@InspectorConfig.port + // Active project configuration. + private val activeProject = atomic(null) - path = this@InspectorConfig.path - sourcePaths = sources - } - } - } } - /** Specifies settings for the Debug Adapter Protocol host. */ - @Introspected @ReflectiveAccess class DebugConfig { - /** Specifies whether the debugger should suspend immediately at execution start. */ - @Option( - names = ["--debug:suspend"], - description = ["Whether the debugger should suspend execution immediately."], - defaultValue = "false", - ) - internal var suspend: Boolean = false - - /** Specifies whether the debugger should suspend for internal (facade) sources. */ - @Option( - names = ["--debug:wait"], - description = ["Whether to wait for the debugger to attach before executing any code at all."], - defaultValue = "false", - ) - internal var wait: Boolean = false - - /** Specifies the port the debugger should bind to. */ - @Option( - names = ["--debug:port"], - description = ["Set the port the debugger binds to"], - defaultValue = "4711", - ) - internal var port: Int = 0 - - /** Specifies the host the debugger should bind to. */ - @Option( - names = ["--debug:host"], - description = ["Set the host the debugger binds to"], - defaultValue = "localhost", - ) - internal var host: String = "" - - /** Apply these settings to the root engine configuration container. */ - internal fun apply(config: PolyglotEngineConfiguration) { - // install and configure the Debug plugin - config.debug { - debugAdapter { - suspend = this@DebugConfig.suspend - waitAttached = wait - - host = this@DebugConfig.host - port = this@DebugConfig.port - } - } + /** [SystemRegistryImpl] that filters for special REPL commands. */ + private class ReplSystemRegistry( + parser: Parser, + terminal: Terminal, + workDir: Supplier, + configPath: ConfigurationPath, + ) : SystemRegistryImpl(parser, terminal, workDir, configPath) { + override fun isCommandOrScript(command: String): Boolean { + return command.startsWith("!") || super.isCommandOrScript(command) } } - /** Specifies settings for application environment. */ - @Introspected @ReflectiveAccess class EnvironmentConfig { - /** Specifies whether the runtime should honor dotenv files. */ - @Option( - names = ["--env:dotenv"], - description = ["Whether to honor .env files; defaults to `true`"], - defaultValue = "true", - ) - internal var dotenv: Boolean = true - - /** Specifies whether the runtime should honor dotenv files. */ - @Option( - names = ["--env"], - description = ["Additional environment variables to set, in x=y format"], - arity = "0..N", - ) - internal var envVars: Map = emptyMap() - - /** Apply these settings to created execution contexts. */ - @Suppress("KotlinConstantConditions") - internal fun apply( - project: ProjectInfo?, - config: PolyglotEngineConfiguration, - host: Boolean = false, - dotenv: Boolean = true, - ) = config.environment { - // inject `NODE_ENV` - environment("NODE_ENV", if (ElideCLITool.ELIDE_RELEASE_TYPE == "DEV") { - "development" - } else { - "production" - }) - - if (host) System.getenv().entries.forEach { - mapToHostEnv(it.key) - } - - // apply project-level environment variables first (if applicable) - project?.env?.vars?.forEach { - if (it.value.isPresent) { - if (it.value.source == DOTENV && !dotenv) { - return@forEach // skip .env vars if so instructed - } - environment(it.key, it.value.value) - } - } - - // apply manually-installed environment variables - envVars.forEach { - if (it.value.isNotBlank() && it.value.isNotBlank()) { - environment(it.key, it.value) - } - } + // Resolve the specified language. + @Suppress("ComplexCondition") + internal fun resolveLangs(alias: String? = null): EnumSet { + return EnumSet.noneOf(GuestLanguage::class.java).apply { + add(JS) + add(WASM) + if (ENABLE_TYPESCRIPT && (language.typescript || (alias != null && tsAliases.contains(alias)))) add(TYPESCRIPT) + if (ENABLE_PYTHON && (language.python || (alias != null && pyAliases.contains(alias)))) add(PYTHON) + if (ENABLE_RUBY && (language.ruby || (alias != null && rbAliases.contains(alias)))) add(RUBY) + if (ENABLE_JVM && (language.jvm || language.kotlin || (alias != null && jvmAliases.contains(alias)))) add(JVM) + if (ENABLE_JVM && (language.kotlin || (alias != null && ktAliases.contains(alias)))) add(KOTLIN) } } - internal sealed class LanguageSettings - - /** Settings which apply to Python only. */ - @Introspected @ReflectiveAccess class PythonSettings : LanguageSettings() { - /** Whether to activate Python debug mode. */ - @Option( - names = ["--py:debug"], - description = ["Activate Python debug mode"], - defaultValue = "false", - ) - internal var debug: Boolean = false - } - - /** Settings which apply to Ruby only. */ - @Introspected @ReflectiveAccess class RubySettings : LanguageSettings() { - /** Whether to activate Ruby debug mode. */ - @Option( - names = ["--rb:debug"], - description = ["Activate Ruby debug mode"], - defaultValue = "false", - ) - internal var debug: Boolean = false - } - - /** Settings which apply to JVM languages only. */ - @Introspected @ReflectiveAccess class JvmSettings : LanguageSettings() { - /** Whether to activate JVM debug mode. */ - @Option( - names = ["--jvm:debug"], - description = ["Activate JVM debug mode"], - defaultValue = "false", - ) - internal var debug: Boolean = false - } - - /** Settings which apply to Kotlin. */ - @Introspected @ReflectiveAccess class KotlinSettings : LanguageSettings() { - /** Whether to activate Kotlin debug mode. */ - @Option( - names = ["--kt:debug"], - description = ["Activate Kotlin debug mode"], - defaultValue = "false", - ) - internal var debug: Boolean = false - } - - /** Settings which apply to Groovy. */ - @Introspected @ReflectiveAccess class GroovySettings : LanguageSettings() { - /** Whether to activate Groovy debug mode. */ - @Option( - names = ["--groovy:debug"], - description = ["Activate Groovy debug mode"], - defaultValue = "false", - ) - internal var debug: Boolean = false - } - - /** Settings which apply to WASM. */ - @Introspected @ReflectiveAccess class WasmSettings : LanguageSettings() { - /** Whether to activate WASM debug mode. */ - @Option( - names = ["--wasm:debug"], - description = ["Activate WASM debug mode"], - defaultValue = "false", - ) - internal var debug: Boolean = false - } - - // Whether the last-seen command was a user exit. - private val exitSeen = AtomicBoolean(false) - // Last-seen statement executed by the VM. private val allSeenStatements = LinkedList() - // Count of statement lines seen; used as an offset for the length of `allSeenStatements`. - private val statementCounter = AtomicInteger(0) - - // Language-specific syntax highlighter. - private val langSyntax: AtomicReference = AtomicReference(null) - - // Main operating terminal. - private val terminal: AtomicReference = AtomicReference(null) - - // Active line reader. - private val lineReader: AtomicReference = AtomicReference(null) - // Intrinsics manager - @Inject internal lateinit var intrinsicsManager: IntrinsicsManager + @Inject internal lateinit var mainIntrinsicsManager: Stream // Event listeners for the vfs - @Inject internal lateinit var vfsListeners: List - - // Server manager - private val serverAgent: HttpServerAgent = HttpServerAgent() - - // Synchronization primitive used to coordinate server behavior - private val phaser: AtomicReference = AtomicReference(Phaser(1)) - - // Whether a server is running. - private val serverRunning: AtomicBoolean = AtomicBoolean(false) + @Inject internal lateinit var registeredVfsListeners: Stream - // Active project configuration. - private val activeProject: AtomicReference = AtomicReference(null) + // Intrinsics manager + private val intrinsicsManager: Supplier get() = Supplier { + mainIntrinsicsManager.findFirst().orElseThrow() + } - // Main script engine manager. - private val scriptEngineManager: ScriptEngineManager by lazy { - ScriptEngineManager(this::class.java.classLoader) + // Event listeners for the vfs + private val vfsListeners: Supplier> get() = Supplier { + registeredVfsListeners.toList() } /** Specifies the guest language to run. */ @@ -713,42 +315,6 @@ private typealias ContextAccessor = () -> PolyglotContext heading = "%nEngine: JavaScript%n", ) internal var jsSettings: EngineJavaScriptOptions = EngineJavaScriptOptions() - /** Settings specific to Python. */ - @ArgGroup( - validate = false, - heading = "%nEngine: Python%n", - ) internal var pythonSettings: PythonSettings = PythonSettings() - - /** Settings specific to Ruby. */ - @ArgGroup( - validate = false, - heading = "%nEngine: Ruby%n", - ) internal var rubySettings: RubySettings = RubySettings() - - /** Settings specific to Ruby. */ - @ArgGroup( - validate = false, - heading = "%nEngine: JVM%n", - ) internal var jvmSettings: JvmSettings = JvmSettings() - - /** Settings specific to Kotlin. */ - @ArgGroup( - validate = false, - heading = "%nEngine: Kotlin%n", - ) internal var kotlinSettings: KotlinSettings = KotlinSettings() - - /** Settings specific to Groovy. */ - @ArgGroup( - validate = false, - heading = "%nEngine: Groovy%n", - ) internal var groovySettings: GroovySettings = GroovySettings() - - /** Settings specific to Groovy. */ - @ArgGroup( - validate = false, - heading = "%nEngine: WASM%n", - ) internal var wasmSettings: WasmSettings = WasmSettings() - /** File to run within the VM. */ @Parameters( index = "0", @@ -782,8 +348,8 @@ private typealias ContextAccessor = () -> PolyglotContext // Executed when a guest statement is entered. private fun onStatementEnter(event: ExecutionEvent) { if (verbose) { - val highlighter = langSyntax.get() - val lineReader = lineReader.get() + val highlighter = langSyntax.value + val lineReader = lineReader.value val txt: CharSequence? = event.location.characters if (!txt.isNullOrBlank()) { @@ -797,8 +363,8 @@ private typealias ContextAccessor = () -> PolyglotContext } private fun printHighlighted(txt: String) { - val highlighter = langSyntax.get() - val lineReader = lineReader.get() + val highlighter = langSyntax.value + val lineReader = lineReader.value if (txt.isNotBlank()) { if (highlighter != null && lineReader != null) { @@ -809,12 +375,13 @@ private typealias ContextAccessor = () -> PolyglotContext } } + @Suppress("unused") private fun onUserInteractiveSource(code: String, source: Source) { printHighlighted(code) } // Execute a single chunk of code, or literal statement. - @Suppress("SameParameterValue") private fun executeOneChunk( + @Suppress("SameParameterValue", "unused") private fun executeOneChunk( languages: EnumSet, primaryLanguage: GuestLanguage, ctxAccessor: ContextAccessor, @@ -851,7 +418,7 @@ private typealias ContextAccessor = () -> PolyglotContext } when (val throwable = processUserCodeError(primaryLanguage, exc)) { null -> { - logging.trace("Caught exception from code statement ${statementCounter.get()}", exc) + logging.trace("Caught exception from code statement ${statementCounter.value}", exc) throw exc } @@ -872,7 +439,8 @@ private typealias ContextAccessor = () -> PolyglotContext code: String, ) { if (serveMode()) { - serverRunning.set(true) + assert(!serverRunning.value) { "Server is already running" } + serverRunning.value = true executeSource( "stdin", languages, @@ -897,6 +465,7 @@ private typealias ContextAccessor = () -> PolyglotContext // Build or detect a terminal to use for interactive REPL use. private fun buildTerminal(): Terminal { return TerminalBuilder.builder() + .ffm(true) // prefer ffm .jansi(true) .jna(false) // not supported on M1 (crashes) .color(pretty) @@ -908,7 +477,7 @@ private typealias ContextAccessor = () -> PolyglotContext handle(Terminal.Signal.INT) { executeThread.interrupt() } - terminal.set(this) + terminal.value = this } } @@ -984,8 +553,8 @@ private typealias ContextAccessor = () -> PolyglotContext .option(LineReader.Option.ERASE_LINE_ON_FINISH, true) .build() - lineReader.set(reader) - langSyntax.set(languageHighlighter) + lineReader.value =reader + langSyntax.value = languageHighlighter builtins.setLineReader(reader) history.attach(reader) reader.apply { @@ -994,6 +563,7 @@ private typealias ContextAccessor = () -> PolyglotContext } // Initialize nano-rc syntax highlighting configurations. + @Suppress("TooGenericExceptionCaught") private fun initNanorcConfig(rootPath: Path, userHome: String): File? { if (!rootPath.exists() || Statics.noColor) { logging.debug("Syntax highlighting disabled by flags, missing root, or NO_COLOR") @@ -1102,9 +672,9 @@ private typealias ContextAccessor = () -> PolyglotContext // otherwise, calculate lines val totalLines = endLine - startLine + (contextLines * 2) val ctxLines = ArrayList(totalLines) - val baseOnHand = minOf(0, statementCounter.get()) + val baseOnHand = minOf(0, statementCounter.value) val errorTail = errorBase + (exc.sourceLocation.endLine - exc.sourceLocation.startLine) - val topLine = maxOf(statementCounter.get(), errorTail) + val topLine = maxOf(statementCounter.value, errorTail) return when { // cannot resolve: we don't have those lines (they are too early for our buffer) @@ -1115,7 +685,7 @@ private typealias ContextAccessor = () -> PolyglotContext if (allSeenStatements.isNotEmpty()) { ctxLines.addAll(allSeenStatements.subList(errorBase, minOf(topLine, allSeenStatements.size))) (errorBase + 1) to ctxLines - } else when (val target = runnable?.ifBlank { null }) { + } else when (runnable?.ifBlank { null }) { // @TODO implement null -> (errorBase + 1) to emptyList() else -> (-1) to emptyList() @@ -1127,7 +697,7 @@ private typealias ContextAccessor = () -> PolyglotContext // Determine error printer settings for this run. private fun errPrinterSettings(): ErrPrinter.ErrPrinterSettings = ErrPrinter.ErrPrinterSettings( enableColor = pretty, - maxLength = terminal.get()?.width ?: ErrPrinter.DEFAULT_MAX_WIDTH, + maxLength = terminal.value?.width ?: ErrPrinter.DEFAULT_MAX_WIDTH, ) // Given an error, render a table explaining the error, along with any source context if we have it. @@ -1147,8 +717,8 @@ private typealias ContextAccessor = () -> PolyglotContext // print full stack traces raw in debug or verbose mode mode exc.printStackTrace() } - val term = terminal.get() - val reader = lineReader.get() + val term = terminal.value + val reader = lineReader.value // begin calculating with source context val middlePrefix = "║ " @@ -1331,7 +901,7 @@ private typealias ContextAccessor = () -> PolyglotContext is GuestError -> displayFormattedError( exc, exc.message ?: "An error was thrown", - stacktrace = !interactive.get(), + stacktrace = !isInteractive(), ) else -> displayFormattedError( @@ -1347,7 +917,7 @@ private typealias ContextAccessor = () -> PolyglotContext exc.isGuestException -> displayFormattedError( exc, exc.message ?: "An error was thrown", - stacktrace = !interactive.get(), + stacktrace = !isInteractive(), ) // @TODO(sgammon): interrupts @@ -1364,7 +934,7 @@ private typealias ContextAccessor = () -> PolyglotContext } // in interactive sessions, return `null` if the error is non-fatal; this tells the outer execution loop to ignore // the exception (since it has been printed to the user), and continue with the interactive session. - return if (interactive.get()) { + return if (isInteractive()) { null } else { ShellError.USER_CODE_ERROR.raise(exc) @@ -1436,12 +1006,12 @@ private typealias ContextAccessor = () -> PolyglotContext // if the user had broken with ctrl+c before, they've now processed code, so we should reset the `exitSeen` // cookie state for the next exit opportunity. - if (exitSeen.get()) exitSeen.set(false) + if (exitSeen.value) exitSeen.value = false // if we are running in modes which show the result, print it if (!quiet && primaryLanguage.id != RUBY.id) showValue(result) - } catch (exc: NoSuchElementException) { + } catch (_: NoSuchElementException) { logging.debug("User expressed no input: exiting.") break } catch (exc: PolyglotException) { @@ -1456,19 +1026,19 @@ private typealias ContextAccessor = () -> PolyglotContext else -> throw throwable } } - } catch (userInterrupt: UserInterruptException) { - if (exitSeen.get()) { + } catch (_: UserInterruptException) { + if (exitSeen.value) { break // we've already seen the exit, so we need to break } else try { - exitSeen.compareAndExchange(false, true) + exitSeen.compareAndSet(false, true) println("ctrl+c caught; do it again to exit") continue - } catch (ioe: Exception) { + } catch (_: Exception) { break // just in case } - } catch (eof: EndOfFileException) { + } catch (_: EndOfFileException) { break // user sent ctrl+d - } catch (interrupt: InterruptedException) { + } catch (_: InterruptedException) { println("Interrupted (sys)") logging.debug("Session interrupted; concluding") break // exit on interrupt @@ -1503,24 +1073,24 @@ private typealias ContextAccessor = () -> PolyglotContext language: GuestLanguage, script: File, ): Source { - logging.debug("Reading executable user script at path '${script.path}' (language: ${language.id})") + logging.debug { "Reading executable user script at path '${script.path}' (language: ${language.id})" } if (!script.exists()) { - logging.debug("Script file does not exist") + logging.debug { "Script file does not exist" } throw ShellError.FILE_NOT_FOUND.asError() } else { - logging.trace("Script check: File exists") + logging.trace { "Script check: File exists" } } if (!script.canRead()) { - logging.debug("Script file cannot be read") + logging.debug { "Script file cannot be read" } throw ShellError.FILE_NOT_READABLE.asError() } else { - logging.trace("Script check: File is readable") + logging.trace { "Script check: File is readable" } } if (!script.isFile) { - logging.debug("Script file is not a regular file") + logging.debug { "Script file is not a regular file" } throw ShellError.NOT_A_FILE.asError() } else { - logging.trace("Script check: File is a regular file") + logging.trace { "Script check: File is a regular file" } } // type check: first, check file extension @@ -1552,15 +1122,18 @@ private typealias ContextAccessor = () -> PolyglotContext "Failed to resolve target language for source file" } - return Source.newBuilder(targetLang.symbol, script) - .encoding(StandardCharsets.UTF_8) - .internal(false) - .cached(true) - .build() + return script.bufferedReader(StandardCharsets.UTF_8).use { reader -> + Source.newBuilder(targetLang.symbol, reader, script.name) + .encoding(StandardCharsets.UTF_8) + .internal(false) + .cached(true) + .uri(script.toURI()) + .build() + } } private val commandSpecifiesServer: Boolean by lazy { - Statics.args.get().let { + Statics.args.let { it.contains("serve") || it.contains("start") } } @@ -1588,9 +1161,8 @@ private typealias ContextAccessor = () -> PolyglotContext // initialize the server intrinsic and run using the provided source serverAgent.run(entrypoint = source) { resolvePolyglotContext(langs) } - phaser.get().register() - - serverRunning.set(true) + phaser.value.register() + serverRunning.value = true } catch (exc: PolyglotException) { processUserCodeError(language, exc)?.let { throw it } } @@ -1618,6 +1190,7 @@ private typealias ContextAccessor = () -> PolyglotContext } // Read an executable script, and then execute the script; if it's a server, delegate to `readStartServer`. + @Suppress("TooGenericExceptionCaught") private fun executeSource( label: String, languages: EnumSet, @@ -1670,20 +1243,11 @@ private typealias ContextAccessor = () -> PolyglotContext else -> File(bundleSpec).toURI() } - // Resolve the primary interactive language for the provided `file`. - @Suppress("UNUSED_VARIABLE") - private fun primaryFromFile(file: File): GuestLanguage? { - return when (val engine = scriptEngineManager.getEngineByExtension(file.extension)) { - null -> null - else -> TODO("") - } - } - // Resolve the default language to use when interpreting a given `fileInput`, or use a sensible fallback from the set // of supported languages. If JS is disabled, there can only be one language; otherwise the default language is JS. If // a file is provided with a specific matching file extension for a given language, that language is used. private fun resolvePrimaryLanguage( - project: ProjectInfo?, + project: () -> ProjectInfo?, languageSelector: LanguageSelector?, languages: EnumSet, fileInput: File?, @@ -1701,25 +1265,15 @@ private typealias ContextAccessor = () -> PolyglotContext languageSelector != null -> languageSelector.primary(commandSpec, languages, project, languageHint) // we have to have at least one language - languages.size == 0 -> error("Cannot start VM with no enabled guest languages") + languages.isEmpty() -> error("Cannot start VM with no enabled guest languages") // otherwise, if JS is included in the set of languages, that is the default. else -> if (languages.contains(JS)) JS else languages.first() } - private fun ignoreNotInstalled(block: () -> Unit) { - try { - block() - } catch (exc: LinkageError) { - logging.debug("Failed to link runtime plugin: ${exc.message}") - } catch (exc: ClassNotFoundException) { - logging.debug("Failed to install runtime plugin: ${exc.message}") - } - } - override fun PolyglotEngineConfiguration.configureEngine(langs: EnumSet) { // grab project configurations, if available - val project = activeProject.get() + val project = activeProject.value if (project != null) logging.debug("Resolved project info: {}", project) // conditionally apply debugging settings @@ -1758,11 +1312,11 @@ private typealias ContextAccessor = () -> PolyglotContext // configure support for guest languages val versionProp = VERSION_INSTRINSIC_NAME to Elide.version() - val intrinsics = intrinsicsManager.resolver() + val intrinsics = intrinsicsManager.get().resolver() // resolve entrypoint arguments val cmd = ProcessHandle.current().info().command().orElse("elide") - val args = Statics.args.get() ?: emptyList() + val args = Statics.args langs.forEach { lang -> when (lang) { @@ -1863,7 +1417,7 @@ private typealias ContextAccessor = () -> PolyglotContext val file = try { logging.trace("Checking bundle at URI '{}'", uri) File(uri) - } catch (err: IOException) { + } catch (_: IOException) { throw ShellError.BUNDLE_NOT_FOUND.asError() } @@ -1879,13 +1433,13 @@ private typealias ContextAccessor = () -> PolyglotContext } // register listeners - vfsListeners.forEach(::listener) + vfsListeners.get().forEach(::listener) } } - /** @inheritDoc */ override suspend fun CommandContext.invoke(state: ToolContext): CommandResult { logging.debug("Shell/run command invoked") + Elide.requestNatives(server = true, tooling = true) // resolve project configuration val projectConfigJob = projectManager.resolveProjectAsync() @@ -1900,11 +1454,20 @@ private typealias ContextAccessor = () -> PolyglotContext } // resolve the language to use - val project = projectConfigJob.await() + val projectFut = projectConfigJob.asCompletableFuture() + val project = { projectFut.get(1, TimeUnit.SECONDS) } + + // apply project configurations to context, if needed + projectFut.whenComplete { value, err -> + if (err == null && value != null) { + activeProject.value = value + } + } + logging.trace("All supported languages: ${allSupported.joinToString(", ") { it.id }}") val supportedEnginesAndLangs = supported.flatMap { listOf(it.first.engine, it.first.id) }.toSortedSet() val allSupportedLangs = EnumSet.copyOf(supported.map { it.first }.toSortedSet()) - val langs = language.resolveLangs(project, languageHint) + val langs = resolveLangs(languageHint) // make sure each requested language is supported langs.forEach { @@ -1940,16 +1503,8 @@ private typealias ContextAccessor = () -> PolyglotContext ) } - // apply project configurations to context, if needed - project?.let { prj -> - activeProject.set(prj) - } - resolveEngine(onByDefaultLangs).unwrap().use { withDeferredContext(onByDefaultLangs) { - // activate interactive behavior - interactive.compareAndSet(false, true) - // warn about experimental status, as applicable if (verbose && experimentalLangs.isNotEmpty()) { logging.warn( @@ -1961,6 +1516,9 @@ private typealias ContextAccessor = () -> PolyglotContext when (val scriptTargetOrCode = runnable) { // run in interactive mode null, "-" -> if (useStdin || runnable == "-") { + // activate interactive behavior + enableInteractive() + // consume from stdin primaryLang(null).let { lang -> input.buffer.use { buffer -> @@ -1969,16 +1527,19 @@ private typealias ContextAccessor = () -> PolyglotContext langs, lang, it, - Source.create(lang.symbol, buffer.readText()), + Source.newBuilder(lang.symbol, buffer, "stdin") + .cached(false) + .buildLiteral(), ) } } } else if (!serveMode()) { logging.debug("Beginning interactive guest session") + enableInteractive() beginInteractiveSession( langs, primaryLang(null), - engine.get(), + engine(), it, ) } else { @@ -2035,10 +1596,10 @@ private typealias ContextAccessor = () -> PolyglotContext } // don't exit if we have a running server - if (serverRunning.get()) { + if (serverRunning.value) { // wait for all tasks to arrive logging.debug("Waiting for long-lived tasks to arrive") - phaser.get().arriveAndAwaitAdvance() + phaser.value.arriveAndAwaitAdvance() logging.debug("Exiting") } return success() diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/tool/ToolInvokeCommand.kt b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/tool/ToolInvokeCommand.kt index 4c373df56..742c87546 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/cmd/tool/ToolInvokeCommand.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/cmd/tool/ToolInvokeCommand.kt @@ -266,7 +266,6 @@ private enum class ToolAction ( UV -> dev.elide.cli.bridge.CliNativeBridge::runUv OXC, BIOME -> error("Tool '$selectedTool' is not supported yet") }.let { tool -> - dev.elide.cli.bridge.CliNativeBridge.initialize() val version = dev.elide.cli.bridge.CliNativeBridge.apiVersion() if (verbose) output { @@ -343,7 +342,9 @@ private enum class ToolAction ( } override suspend fun CommandContext.invoke(state: ToolContext): CommandResult { - dev.elide.cli.bridge.CliNativeBridge.initialize() + // tools typically require native access; force early init + Elide.requestNatives(server = false, tooling = true) + val version = dev.elide.cli.bridge.CliNativeBridge.apiVersion() val tools = dev.elide.cli.bridge.CliNativeBridge.supportedTools() val versions = tools.associateWith { dev.elide.cli.bridge.CliNativeBridge.toolVersion(it) } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/main.kt b/packages/cli/src/main/kotlin/elide/tool/cli/main.kt index 5a50f1eb9..e0ca270f1 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/main.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/main.kt @@ -11,61 +11,72 @@ * License for the specific language governing permissions and limitations under the License. */ +@file:Suppress("NOTHING_TO_INLINE") + package elide.tool.cli import com.github.ajalt.clikt.core.PrintHelpMessage import com.github.ajalt.clikt.parsers.CommandLineParser import com.github.ajalt.clikt.parsers.flatten -import dev.elide.cli.bridge.CliNativeBridge import io.micronaut.configuration.picocli.MicronautFactory import io.micronaut.context.ApplicationContext +import org.slf4j.bridge.SLF4JBridgeHandler import picocli.CommandLine -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference +import kotlinx.atomicfu.atomic import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlin.system.exitProcess import elide.annotations.Eager +import elide.tool.cli.Elide.Companion.installStatics // Whether to enable the experimental V2 entrypoint through Clikt. private val ENABLE_CLI_ENTRY_V2 = System.getenv("ELIDE_EXPERIMENTAL")?.ifBlank { null } != null +// Whether to log early init messages. +private const val EARLY_INIT_LOG = false + // Whether to exist after completion. -val exitOnComplete: AtomicBoolean = AtomicBoolean(true) +internal var exitOnComplete = true // Exit code for this run. -val exitCode: AtomicInteger = AtomicInteger(0) +@Volatile internal var exitCode = 0 // Unhandled error that caused exit, if any. -val unhandledExc: AtomicReference = AtomicReference(null) +internal val unhandledExc = atomic(null) -// Read the old entrypoint factory. -private fun sorryIHaveToFactory(args: Array): CommandLine = ApplicationContext +private inline fun earlyLog(msg: String) { + if (EARLY_INIT_LOG) println("[init] $msg") +} + +@Suppress("SpreadOperator") +private fun createApplicationContext(args: Array) = ApplicationContext .builder() .eagerInitAnnotated(Eager::class.java) .args(*args) - .start().use { CommandLine(Elide::class.java, MicronautFactory(it)) } + +// Read the old entrypoint factory. +private fun sorryIHaveToFactory(args: Array): CommandLine = + createApplicationContext(args) + .start() + .use { CommandLine(Elide::class.java, MicronautFactory(it)) } // Run the Clikt or regular entrypoint. -private suspend inline fun runInner(args: Array): Int = when (ENABLE_CLI_ENTRY_V2) { +@Suppress("TooGenericExceptionCaught") +private inline fun runInner(args: Array): Int = when (ENABLE_CLI_ENTRY_V2) { false -> Elide.entry(args) - true -> ApplicationContext - .builder() - .eagerInitAnnotated(Eager::class.java) - .args(*args).start().use { applicationContext -> + true -> createApplicationContext(args).start().use { applicationContext -> MicronautFactory(applicationContext).use { factory -> runCatching { val procInfo = ProcessHandle.current().info() val cmd = procInfo.command().orElse("elide") - Elide.installStatics( + installStatics( cmd, args, System.getProperty("user.dir"), ) CommandLineParser - .parse(factory.create(Elide::class.java), args.toList().also { Statics.args.set(it) }) + .parse(factory.create(Elide::class.java), args.toList()) }.onFailure { println("Failed to parse arguments: ${it.message}") it.printStackTrace() @@ -76,8 +87,10 @@ private suspend inline fun runInner(args: Array): Int = when (ENABLE_CLI .first() .command - cmd.enter().exitCode - } catch (err: PrintHelpMessage) { + runBlocking(Dispatchers.Unconfined) { + cmd.enter().exitCode + } + } catch (_: PrintHelpMessage) { try { sorryIHaveToFactory(args).usage(System.out) } catch (err: Throwable) { @@ -98,14 +111,43 @@ private suspend inline fun runInner(args: Array): Int = when (ENABLE_CLI internal object NativeEntry { @JvmName("enter") @JvmStatic - fun enter(args: Array): Int = runBlocking(Dispatchers.Default) { - runInner(args) - } + fun enter(args: Array): Int = entry(args, true) } // Perform early startup initialization tasks. -private fun initialize() { - CliNativeBridge.initialize() +@Volatile var entryInitialized: Boolean = false + +inline fun setStaticProperties() { + System.setProperty("elide.js.vm.enableStreams", "true") + System.setProperty("java.util.logging.config.class", "elide.tool.cli.InertLoggerConfigurator") + System.setProperty("jdk.httpclient.allowRestrictedHeaders", "Host,Content-Length") + System.setProperty("jansi.eager", "false") + System.setProperty("io.netty.allocator.maxOrder", "3") + System.setProperty("io.netty.serviceThreadPrefix", "elide-svc") + System.setProperty("io.netty.native.deleteLibAfterLoading", "true") + System.setProperty("io.netty.buffer.bytebuf.checkAccessible", "false") + System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "2") + System.setProperty("kotlinx.coroutines.scheduler.core.pool.size", "2") + System.setProperty("kotlinx.coroutines.scheduler.max.pool.size", "2") + System.setProperty("kotlinx.coroutines.scheduler.default.name", "ElideDefault") + System.setProperty(org.fusesource.jansi.AnsiConsole.JANSI_MODE, org.fusesource.jansi.AnsiConsole.JANSI_MODE_FORCE) + System.setProperty(org.fusesource.jansi.AnsiConsole.JANSI_GRACEFUL, "false") +} + +fun initializeEntry(args: Array, installStatics: Boolean = true) { + if (entryInitialized) return + entryInitialized = true + + earlyLog("Setting static properties") + setStaticProperties() + if (installStatics) { + earlyLog("Installing statics") + Statics.mountArgs(args) + val binPath = ProcessHandle.current().info().command().orElse(null) + installStatics(binPath, args, System.getProperty("user.dir")) + earlyLog("Installing bridge handler for SLF4j") + SLF4JBridgeHandler.install() + } } /** @@ -117,20 +159,41 @@ private fun initialize() { * @param args Arguments to run with. */ @Suppress("TooGenericExceptionCaught") -suspend fun main(args: Array): Unit = try { +fun entry(args: Array, installStatics: Boolean): Int = runBlocking(Dispatchers.Unconfined) { + try { + initializeEntry(args, installStatics) + runInner(args) + } catch (err: RuntimeException) { + unhandledExc.compareAndSet(null, err) + throw err + } +} + +/** + * Main entrypoint for Elide on the command line. + * + * This entrypoint method will call [exitProcess] with the exit code of the program; for testing or API-based dispatch, + * see other entrypoints. + * + * @param args Arguments to run with. + */ +@Suppress("TooGenericExceptionCaught") +fun main(args: Array): Unit = try { // perform early init - initialize() + earlyLog("Initializing entrypoint") + initializeEntry(args) // run the entrypoint - exitCode.set(try { + exitCode = try { + earlyLog("Passing to inner entrypoint") runInner(args) } catch (err: RuntimeException) { unhandledExc.compareAndSet(null, err) 1 - }) + } } finally { Elide.close() }.also { // exit the process if the global exit flag is set - if (exitOnComplete.get()) exitProcess(exitCode.get()) + if (exitOnComplete) exitProcess(exitCode) } diff --git a/packages/cli/src/main/kotlin/elide/tool/cli/options/EngineJavaScriptOptions.kt b/packages/cli/src/main/kotlin/elide/tool/cli/options/EngineJavaScriptOptions.kt index f11578e55..33b952816 100644 --- a/packages/cli/src/main/kotlin/elide/tool/cli/options/EngineJavaScriptOptions.kt +++ b/packages/cli/src/main/kotlin/elide/tool/cli/options/EngineJavaScriptOptions.kt @@ -16,9 +16,9 @@ package elide.tool.cli.options import io.micronaut.core.annotation.Introspected import io.micronaut.core.annotation.ReflectiveAccess import picocli.CommandLine.Option -//import tools.elide.assets.EmbeddedScriptMetadata.JsScriptMetadata.JsLanguageLevel import elide.runtime.gvm.GuestLanguage import elide.runtime.plugins.js.JavaScriptConfig +import elide.runtime.plugins.js.JavaScriptVersion /** JavaScript engine options. */ @Introspected @ReflectiveAccess class EngineJavaScriptOptions : AbstractEngineOptions() { @@ -41,12 +41,12 @@ import elide.runtime.plugins.js.JavaScriptConfig internal var strict: Boolean = true /** Whether to activate JS strict mode. */ -// @Option( -// names = ["--js:ecma"], -// description = ["ECMA standard to use for JavaScript."], -// defaultValue = "ES2022", -// ) -// internal var ecma: JsLanguageLevel = JsLanguageLevel.ES2023 + @Option( + names = ["--js:ecma"], + description = ["ECMA standard to use for JavaScript."], + defaultValue = "ES2024", + ) + internal var ecma: JavaScriptVersion = JavaScriptVersion.ES2024 /** Whether to activate NPM support. */ @Option( @@ -72,9 +72,32 @@ import elide.runtime.plugins.js.JavaScriptConfig ) internal var wasm: Boolean = true + /** Whether to activate JS strict mode. */ + @Option( + names = ["--js:experimental-disable-polyfills"], + description = ["Disable JavaScript polyfills"], + defaultValue = "false", + ) + internal var experimentalDisablePolyfills: Boolean = false + + /** Whether to activate JS strict mode. */ + @Option( + names = ["--js:experimental-disable-vfs"], + description = ["Disable JavaScript VFS"], + defaultValue = "false", + ) + internal var experimentalDisableVfs: Boolean = false + /** Apply these settings to the configuration for the JavaScript runtime plugin. */ internal fun apply(config: JavaScriptConfig) { -// config.language = JavaScriptVersion.valueOf(ecma.name) + config.language = JavaScriptVersion.valueOf(ecma.name) config.wasm = wasm + config.esm = esm + if (experimentalDisableVfs || experimentalDisablePolyfills) { + config.experimental { + disableVfs = experimentalDisableVfs + disablePolyfills = experimentalDisablePolyfills + } + } } } diff --git a/packages/cli/src/main/kotlin/elide/tool/engine/NativeEngine.kt b/packages/cli/src/main/kotlin/elide/tool/engine/NativeEngine.kt index 08d5dde60..e2072203f 100644 --- a/packages/cli/src/main/kotlin/elide/tool/engine/NativeEngine.kt +++ b/packages/cli/src/main/kotlin/elide/tool/engine/NativeEngine.kt @@ -151,14 +151,14 @@ object NativeEngine { @JvmStatic private fun ensureLoadableFromNatives( group: String, libs: List, - workdir: File, + workdirProvider: () -> File, loader: ClassLoader, allCandidatePaths: Sequence, forceLoad: Boolean = false, ) = libs.map { val (loaded, copied) = try { NativeUtil.loadOrCopy( - workdir, + workdirProvider, it.path, it.name, loader, @@ -183,7 +183,7 @@ object NativeEngine { @JvmStatic private fun HostPlatform.loadByPlatform( loader: ClassLoader, group: String, - workdir: File, + workdirProvider: () -> File, allCandidatePaths: Sequence, forceLoad: Boolean = !ImageInfo.inImageCode(), linux: (() -> List?)? = null, @@ -198,7 +198,14 @@ object NativeEngine { }.let { when (val target = (it?.invoke() ?: default?.invoke())) { null -> {} - else -> ensureLoadableFromNatives(group, target, workdir, loader, allCandidatePaths, forceLoad).also { result -> + else -> ensureLoadableFromNatives( + group, + target, + workdirProvider, + loader, + allCandidatePaths, + forceLoad, + ).also { result -> if (Statics.logging.isEnabled(DEBUG)) { Statics.logging.debug("Native library group $group (loaded=$result)") } @@ -209,13 +216,13 @@ object NativeEngine { // Load native tooling libraries, based on OS. @JvmStatic private fun HostPlatform.loadNativeTooling( - workdir: File, + workdirProvider: () -> File, allCandidatePaths: Sequence, loader: ClassLoader, ) = loadByPlatform( loader, "tools", - workdir, + workdirProvider, allCandidatePaths, default = { listOf(nativeLib("tools", "umbrella", os)) @@ -224,13 +231,13 @@ object NativeEngine { // Load native transport libraries, based on OS. @JvmStatic private fun HostPlatform.loadNativeTransport( - workdir: File, + workdirProvider: () -> File, allCandidatePaths: Sequence, loader: ClassLoader, ) = loadByPlatform( loader, "transport", - workdir, + workdirProvider, allCandidatePaths, linux = { listOf( nettyNative("transport", "transport_native_epoll", os), @@ -245,13 +252,13 @@ object NativeEngine { // Load native OpenSSL libraries. @JvmStatic private fun HostPlatform.loadNativeCrypto( - workdir: File, + workdirProvider: () -> File, allCandidatePaths: Sequence, loader: ClassLoader, ) = loadByPlatform( loader, "crypto", - workdir, + workdirProvider, allCandidatePaths, linux = { listOf(nettyNative("crypto", "tcnative_linux", os)) @@ -263,9 +270,11 @@ object NativeEngine { // Load natives from the CWD while it is swapped in. @Suppress("TooGenericExceptionCaught") - @JvmStatic private fun loadAllNatives( + @JvmStatic private fun loadApplicableNatives( platform: HostPlatform, - natives: File, + nativesProvider: () -> File, + server: Boolean, + tooling: Boolean, allCandidatePaths: Sequence, loader: ClassLoader, ) = platform.apply { @@ -273,14 +282,20 @@ object NativeEngine { val loadNatives: () -> Unit = if (ImageInfo.inImageCode() && staticJniMode) { // nothing to load { - loadNativeTransport(natives, allCandidatePaths, loader) + if (server) { + loadNativeTransport(nativesProvider, allCandidatePaths, loader) + } } } else { // trigger load of native libs { - loadNativeTransport(natives, allCandidatePaths, loader) - loadNativeCrypto(natives, allCandidatePaths, loader) - loadNativeTooling(natives, allCandidatePaths, loader) + if (server) { + loadNativeTransport(nativesProvider, allCandidatePaths, loader) + loadNativeCrypto(nativesProvider, allCandidatePaths, loader) + } + if (tooling) { + loadNativeTooling(nativesProvider, allCandidatePaths, loader) + } } } @@ -329,12 +344,19 @@ object NativeEngine { * * Prepare VM-level settings and trigger loading of critical native libraries. * - * @param workdir Working directory manager. + * @param workdirProvider Provider which yields a working directory manager. + * @param server Whether to initialize server components. + * @param tooling Whether to initialize tooling components. * @param extraProps Extra VM properties to set. */ @Suppress("UNUSED_PARAMETER") - @JvmStatic fun load(workdir: WorkdirManager, extraProps: List>) { - val natives = workdir.nativesDirectory() + @JvmStatic fun load( + workdirProvider: () -> WorkdirManager, + server: Boolean, + tooling: Boolean, + extraProps: List>, + ) { + val platform = HostPlatform.resolve() val separator = File.separator val libraryPath = System.getProperty("java.library.path", "") @@ -360,6 +382,7 @@ object NativeEngine { Path(it).resolve("lib") } val combinedFullPath = sequence { + val natives = workdirProvider.invoke().nativesDirectory() yield(natives.toPath()) yieldAll(libraryPath.split(separator).map { Path.of(it) }) yieldAll(libExecPaths) @@ -367,7 +390,14 @@ object NativeEngine { } // load all required native libs for current platform - loadAllNatives(platform, natives.toFile(), combinedFullPath, this::class.java.classLoader) + loadApplicableNatives( + platform, + { workdirProvider.invoke().nativesDirectory().toFile() }, + server = server, + tooling = tooling, + combinedFullPath, + this::class.java.classLoader, + ) // in jvm mode, force-load sqlite if (!ImageInfo.inImageCode()) { @@ -403,10 +433,22 @@ object NativeEngine { * Early in the static init process for Elide, this method is called to prepare and load native libraries and apply * VM-level settings as early as possible. * - * @param workdir Working directory manager. + * @param workdirProvider Provider which yields a working directory manager. + * @param server Whether to initialize server components. + * @param tooling Whether to initialize tooling components. * @param properties Provider of extra VM properties to set. */ - @JvmStatic fun boot(workdir: WorkdirManager, properties: () -> List>) { - load(workdir, properties.invoke()) + @JvmStatic inline fun boot( + noinline workdirProvider: () -> WorkdirManager, + server: Boolean = true, + tooling: Boolean = true, + properties: () -> List> + ) { + load( + workdirProvider, + server = server, + tooling = tooling, + properties.invoke(), + ) } } diff --git a/packages/cli/src/main/kotlin/elide/tool/engine/NativeUtil.kt b/packages/cli/src/main/kotlin/elide/tool/engine/NativeUtil.kt index 11316e8ab..611690eba 100644 --- a/packages/cli/src/main/kotlin/elide/tool/engine/NativeUtil.kt +++ b/packages/cli/src/main/kotlin/elide/tool/engine/NativeUtil.kt @@ -23,12 +23,13 @@ import java.net.URL import java.nio.file.Files import java.nio.file.Path import java.util.* -import kotlin.collections.ArrayList import elide.runtime.Logger import elide.tool.cli.Statics /** Utilities for loading and copied native libraries, inspired by Netty. */ internal object NativeUtil { + private const val COPY_BUFSIZE = 8192 + // Logger to use for warnings when unpacking native support libraries. @JvmStatic private val logger: Logger by lazy { Statics.logging } @@ -37,7 +38,7 @@ internal object NativeUtil { if (c != null) { try { c.close() - } catch (ignore: IOException) { + } catch (_: IOException) { // ignore } } @@ -104,9 +105,9 @@ internal object NativeUtil { } } - @Suppress("LongParameterList") + @Suppress("LongParameterList", "ReturnCount", "ThrowsCount", "TooGenericExceptionCaught") @JvmStatic internal fun loadOrCopy( - workdir: File, + workdirProvider: () -> File, path: String, libName: String, loader: ClassLoader, @@ -120,6 +121,7 @@ internal object NativeUtil { val index = libname.lastIndexOf('.') val prefix = libname.substring(0, index) val suffix = libname.substring(index) + val workdir = workdirProvider() val libTarget = workdir.resolve(prefix + suffix) // unless we are force-copying, we can skip remaining logic if the file already exists @@ -129,7 +131,7 @@ internal object NativeUtil { try { loadNativeLibrary(libName, false) return true to false - } catch (thr: Throwable) { + } catch (_: Throwable) { // ignore } } @@ -197,9 +199,9 @@ internal object NativeUtil { } } - `in` = url!!.openStream() + `in` = requireNotNull(url).openStream() out = FileOutputStream(libTarget) - val buffer = ByteArray(8192) + val buffer = ByteArray(COPY_BUFSIZE) var length: Int while (`in`.read(buffer).also { length = it } > 0) { out.write(buffer, 0, length) diff --git a/packages/cli/src/main/kotlin/elide/tool/err/PersistedError.kt b/packages/cli/src/main/kotlin/elide/tool/err/PersistedError.kt index 820b29518..814672dfc 100644 --- a/packages/cli/src/main/kotlin/elide/tool/err/PersistedError.kt +++ b/packages/cli/src/main/kotlin/elide/tool/err/PersistedError.kt @@ -49,7 +49,7 @@ import elide.tool.err.PersistedError.ErrorInfo.Companion.info val runtime: RuntimeInfo = RuntimeInfo(), val os: HostPlatform.OperatingSystem, val arch: HostPlatform.Architecture, - val args: List = Statics.args.get() ?: emptyList(), + val args: List = Statics.args.toList(), ) { companion object { /** diff --git a/packages/cli/src/main/kotlin/elide/tool/io/RuntimeWorkdirManager.kt b/packages/cli/src/main/kotlin/elide/tool/io/RuntimeWorkdirManager.kt index 10f3b0f1c..69ece23cc 100644 --- a/packages/cli/src/main/kotlin/elide/tool/io/RuntimeWorkdirManager.kt +++ b/packages/cli/src/main/kotlin/elide/tool/io/RuntimeWorkdirManager.kt @@ -20,8 +20,8 @@ import java.nio.file.Paths import java.util.* import java.util.concurrent.ConcurrentSkipListMap import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference import jakarta.inject.Provider +import kotlinx.atomicfu.atomic import kotlin.io.path.absolute import kotlin.io.path.exists import elide.annotations.Context @@ -36,7 +36,17 @@ import elide.tool.io.WorkdirManager.WorkdirHandle /** Main implementation of the runtime working directory manager. */ internal class RuntimeWorkdirManager : WorkdirManager { internal object SingletonManager { - @JvmStatic val singleton: AtomicReference = AtomicReference(null) + @JvmStatic val singleton = atomic(null) + + fun acquire(): RuntimeWorkdirManager { + val existing = singleton.value + if (existing == null) synchronized(this) { + val created = RuntimeWorkdirManager() + singleton.value = created + return created + } + return existing + } } internal companion object { @@ -78,11 +88,8 @@ internal class RuntimeWorkdirManager : WorkdirManager { } /** @return Created or acquired [RuntimeWorkdirManager] singleton. */ - @JvmStatic fun acquire(): RuntimeWorkdirManager = synchronized(this) { - if (SingletonManager.singleton.get() == null) { - SingletonManager.singleton.set(RuntimeWorkdirManager()) - } - SingletonManager.singleton.get() + @JvmStatic fun acquire(): RuntimeWorkdirManager { + return SingletonManager.acquire() } } diff --git a/packages/cli/src/main/resources/logback.scmo b/packages/cli/src/main/resources/logback.scmo index 60557cdfa..7479117a6 100644 Binary files a/packages/cli/src/main/resources/logback.scmo and b/packages/cli/src/main/resources/logback.scmo differ diff --git a/packages/cli/src/main/resources/logback.xml b/packages/cli/src/main/resources/logback.xml index 7b08e8b6d..e359659e2 100644 --- a/packages/cli/src/main/resources/logback.xml +++ b/packages/cli/src/main/resources/logback.xml @@ -68,7 +68,7 @@ - + diff --git a/packages/graalvm/api/graalvm.api b/packages/graalvm/api/graalvm.api index 30f59c4dc..2d8e15da9 100644 --- a/packages/graalvm/api/graalvm.api +++ b/packages/graalvm/api/graalvm.api @@ -477,7 +477,7 @@ public final class elide/runtime/gvm/internals/GVMInvocationBindings$DispatchSty } public final class elide/runtime/gvm/internals/IntrinsicsManager { - public fun (Ljava/util/List;)V + public fun (Ljava/util/stream/Stream;)V public final fun resolver ()Lelide/runtime/intrinsics/IntrinsicsResolver; } @@ -1561,13 +1561,13 @@ public final class elide/runtime/gvm/intrinsics/CachedIntrinsicsResolver$Compani public final class elide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver : elide/runtime/intrinsics/IntrinsicsResolver { public static final field Companion Lelide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver$Companion; - public synthetic fun (Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/util/stream/Stream;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun generate (Lelide/runtime/gvm/GuestLanguage;Z)Lkotlin/sequences/Sequence; - public static final fun of (Ljava/util/List;)Lelide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver; + public static final fun of (Ljava/util/stream/Stream;)Lelide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver; } public final class elide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver$Companion { - public final fun of (Ljava/util/List;)Lelide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver; + public final fun of (Ljava/util/stream/Stream;)Lelide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver; } public final class elide/runtime/gvm/intrinsics/ServiceIntrinsicsResolver : elide/runtime/intrinsics/IntrinsicsResolver { diff --git a/packages/graalvm/build.gradle.kts b/packages/graalvm/build.gradle.kts index 5064df4da..477cddcf2 100644 --- a/packages/graalvm/build.gradle.kts +++ b/packages/graalvm/build.gradle.kts @@ -821,31 +821,48 @@ afterEvaluate { ) } +val baseCargoFlags = listOfNotNull( + "--color=always", + "build", + "--target", + targetInfo.triple, +).plus( + cargoConfig?.let { + listOf("--config", it) + } ?: emptyList() +) + +val buildRustNativesForHostRelease by tasks.registering(Exec::class) { + workingDir(rootDir) + dependsOn("buildThirdPartyNatives") + + executable = "cargo" + args(baseCargoFlags.plus("--release")) + + outputs.upToDateWhen { true } + outputs.dir(targetDir) +} + val buildRustNativesForHost by tasks.registering(Exec::class) { workingDir(rootDir) dependsOn("buildThirdPartyNatives") executable = "cargo" - args(listOfNotNull( - "--color=always", - "build", - if (isRelease) "--release" else null, - "--target", - targetInfo.triple, - ).plus( - cargoConfig?.let { - listOf("--config", it) - } ?: emptyList() - )) + args(baseCargoFlags.plus(listOfNotNull(if (isRelease) "--release" else null))) outputs.upToDateWhen { true } outputs.dir(targetDir) } +val selectedRustNatives: String = if (isRelease) + buildRustNativesForHostRelease.name +else + buildRustNativesForHost.name + val natives by tasks.registering { group = "build" description = "Build natives via Make and Cargo" - dependsOn(buildThirdPartyNatives.name, buildRustNativesForHost.name) + dependsOn(buildThirdPartyNatives.name, selectedRustNatives) } listOf( diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/IntrinsicsManager.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/IntrinsicsManager.kt index c994eebba..7584b94fc 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/IntrinsicsManager.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/IntrinsicsManager.kt @@ -12,6 +12,7 @@ */ package elide.runtime.gvm.internals +import java.util.stream.Stream import elide.annotations.Context import elide.annotations.Singleton import elide.runtime.gvm.intrinsics.CompoundIntrinsicsResolver @@ -20,8 +21,8 @@ import elide.runtime.intrinsics.IntrinsicsResolver /** Resolves intrinsics for use with guest VMs. */ @Suppress("MnInjectionPoints") -@Context @Singleton public class IntrinsicsManager (resolvers: List) { - private val compound = CompoundIntrinsicsResolver.of(resolvers) +@Context @Singleton public class IntrinsicsManager (resolvers: Stream) { + private val compound by lazy { CompoundIntrinsicsResolver.of(resolvers) } private val filters: MutableList = mutableListOf() /** Resolver stub. */ diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/console/ConsoleIntrinsic.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/console/ConsoleIntrinsic.kt index e65fd0ea7..618f4c33e 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/console/ConsoleIntrinsic.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/console/ConsoleIntrinsic.kt @@ -17,8 +17,7 @@ package elide.runtime.gvm.internals.intrinsics.js.console import io.micronaut.core.annotation.ReflectiveAccess import java.time.Instant import java.util.* -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference +import kotlinx.atomicfu.atomic import elide.runtime.LogLevel import elide.runtime.Logger import elide.runtime.Logging @@ -49,21 +48,20 @@ internal class ConsoleIntrinsic : JavaScriptConsole, AbstractJsIntrinsic() { // Name of the primary console logger. private const val LOGGER_NAME: String = "gvm:js.console" - } - // Logger which receives console calls. - private val logging: Logger by lazy { Logging.named(LOGGER_NAME) } + // Logger which receives console calls. + private val logging: Logger by lazy { Logging.named(LOGGER_NAME) } - // Whether to intercept logs. - private val intercept: AtomicBoolean = AtomicBoolean(false) + // Whether to disable console streams. + private val disableStreams = System.getProperty("elide.disableStreams") == "true" + } // Interception logger (mostly for testing). - private var interceptor: AtomicReference = AtomicReference(null) + private var interceptor = atomic(null) // Set an interceptor which receives a mirror of all logging calls. internal fun setInterceptor(interceptor: Logger?) { - this.interceptor.set(interceptor) - this.intercept.set(interceptor != null) + this.interceptor.value = interceptor } /** @@ -194,10 +192,9 @@ internal class ConsoleIntrinsic : JavaScriptConsole, AbstractJsIntrinsic() { * @param args Set of arguments to format and include with the log message. */ private fun handleLog(level: LogLevel, args: Array) { + if (disableStreams) return val serializedArgs = args.toList().filterNotNull() - if (intercept.get()) { - interceptor.get().log(level, serializedArgs) - } + interceptor.value?.log(level, serializedArgs) logging.log(level, serializedArgs.map(this::formatLogComponent)) } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/BuiltinIntrinsicsResolver.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/BuiltinIntrinsicsResolver.kt index f1c8b5ae3..3b8ba9102 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/BuiltinIntrinsicsResolver.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/BuiltinIntrinsicsResolver.kt @@ -15,6 +15,8 @@ package elide.runtime.gvm.intrinsics import io.micronaut.context.annotation.Infrastructure +import java.util.stream.Stream +import kotlin.streams.asSequence import elide.annotations.Context import elide.annotations.Inject import elide.annotations.Singleton @@ -25,10 +27,11 @@ import elide.runtime.intrinsics.IntrinsicsResolver /** Resolve intrinsics known at build-time via annotation processing. */ @Context @Singleton @Infrastructure internal class BuiltinIntrinsicsResolver : IntrinsicsResolver { /** All candidate guest intrinsics. */ - @Inject lateinit var intrinsics: Collection + @Inject lateinit var intrinsicsStream: Stream /** @inheritDoc */ - override fun generate(language: GuestLanguage, internals: Boolean): Sequence = intrinsics - .asSequence() - .filter { it.supports(language) } + override fun generate(language: GuestLanguage, internals: Boolean): Sequence = + intrinsicsStream + .asSequence() + .filter { it.supports(language) } } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver.kt index 8fb531bec..a0562c73f 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/intrinsics/CompoundIntrinsicsResolver.kt @@ -12,20 +12,34 @@ */ package elide.runtime.gvm.intrinsics +import java.util.LinkedList +import java.util.stream.Stream +import kotlin.streams.asSequence import elide.runtime.gvm.GuestLanguage import elide.runtime.intrinsics.GuestIntrinsic import elide.runtime.intrinsics.IntrinsicsResolver /** Implementation of an intrinsics resolver which is backed by one or more foreign resolvers. */ public class CompoundIntrinsicsResolver private constructor ( - private val resolvers: List + private val resolvers: Stream ) : IntrinsicsResolver { public companion object { /** @return Compound intrinsics resolver which is backed by the provided [list]. */ - @JvmStatic public fun of(list: List): CompoundIntrinsicsResolver = + @JvmStatic public fun of(list: Stream): CompoundIntrinsicsResolver = CompoundIntrinsicsResolver(list) } - override fun generate(language: GuestLanguage, internals: Boolean): Sequence = - resolvers.asSequence().flatMap { it.resolve(language) } + private lateinit var cached: Collection + + override fun generate(language: GuestLanguage, internals: Boolean): Sequence { + if (!::cached.isInitialized) { + cached = LinkedList() + + return resolvers + .asSequence() + .flatMap { it.resolve(language) } + .onEach { cached += it } + } + return cached.asSequence() + } } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/node/zlib/NodeZlib.kt b/packages/graalvm/src/main/kotlin/elide/runtime/node/zlib/NodeZlib.kt index a49267722..9b96d4495 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/node/zlib/NodeZlib.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/node/zlib/NodeZlib.kt @@ -434,7 +434,7 @@ public data object ModernNodeZlibConstants : NodeZlibConstants { override fun provide(): NodeZlib = zlib override fun install(bindings: MutableIntrinsicBindings) { - bindings[ZLIB_MODULE_SYMBOL.asJsSymbol()] = provide() + bindings[ZLIB_MODULE_SYMBOL.asJsSymbol()] = ProxyExecutable { provide() } } } @@ -490,6 +490,10 @@ private class UnzipStream(private val wrapped: AbstractReadable<*>) : */ private class BrotliCompressStream(private val wrapped: WrappedOutputStream) : Writable by wrapped, BrotliCompress, CompressImpl() { + init { + Brotli4jLoader.ensureAvailability() + } + override fun close() { wrapped.close() } @@ -504,6 +508,10 @@ private class BrotliCompressStream(private val wrapped: WrappedOutputStream) : */ private class BrotliDecompressStream(private val wrapped: AbstractReadable<*>) : Readable by wrapped, BrotliDecompress, CompressImpl() { + init { + Brotli4jLoader.ensureAvailability() + } + override fun close() { wrapped.close() } @@ -519,10 +527,6 @@ private class BrotliDecompressStream(private val wrapped: AbstractReadable<*>) : * Implements the Node zlib module. */ @Singleton internal class NodeZlib @Inject constructor (private val exec: GuestExecutor) : ProxyObject, ZlibAPI { - init { - Brotli4jLoader.ensureAvailability() - } - private companion object { // Decompressor which is enabled for EOF awareness. private val compressorFactory by lazy { diff --git a/packages/terminal/src/main/java/org/fusesource/jansi/internal/JansiLoader.java b/packages/terminal/src/main/java/org/fusesource/jansi/internal/JansiLoader.java index c0114331e..d1b16c5a5 100644 --- a/packages/terminal/src/main/java/org/fusesource/jansi/internal/JansiLoader.java +++ b/packages/terminal/src/main/java/org/fusesource/jansi/internal/JansiLoader.java @@ -59,6 +59,7 @@ * usage: call {@link #initialize()} before using Jansi. */ public class JansiLoader { + private static boolean cleanupThread = false; private static boolean loaded = false; private static String nativeLibraryPath; @@ -72,7 +73,7 @@ public class JansiLoader { */ public static synchronized boolean initialize() { // only cleanup before the first extract - if (!loaded) { + if (!loaded && cleanupThread) { Thread cleanup = new Thread(JansiLoader::cleanup, "cleanup"); cleanup.setPriority(Thread.MIN_PRIORITY); cleanup.setDaemon(true); diff --git a/tools/elide-build/src/main/kotlin/elide/internal/conventions/Constants.kt b/tools/elide-build/src/main/kotlin/elide/internal/conventions/Constants.kt index 219569db9..874564690 100644 --- a/tools/elide-build/src/main/kotlin/elide/internal/conventions/Constants.kt +++ b/tools/elide-build/src/main/kotlin/elide/internal/conventions/Constants.kt @@ -220,18 +220,30 @@ public object Constants { /** Compiler args to include in all Kotlin targets. */ private val BaseCompilerArgs = listOf( - "-Xskip-prerelease-check", "-Xexpect-actual-classes", "-Xsuppress-version-warnings", + "-Xconsistent-data-class-copy-visibility", + "-Xvalue-classes", + "-Xinline-classes", + "-Xabi-stability=stable", ) /** Compiler args to include in Kotlin JVM targets. */ - internal val JvmCompilerArgs = BaseCompilerArgs.plus( + public val JvmCompilerArgs: List = BaseCompilerArgs.plus( listOf( "-no-stdlib", "-no-reflect", "-Xjvm-default=all", "-Xjsr305=strict", + "-Xvalidate-bytecode", + "-Xsam-conversions=indy", + "-Xno-receiver-assertions", + "-Xno-param-assertions", + "-Xlambdas=indy", + "-Xenhance-type-parameter-types-to-def-not-null", + "-Xemit-jvm-type-annotations", + "-Xassertions=jvm", + "-Xstring-concat=indy-with-constants", ), ) @@ -246,7 +258,6 @@ public object Constants { internal val KaptCompilerArgs = JvmCompilerArgs.plus( listOf( "-Xallow-unstable-dependencies", - "-Xemit-jvm-type-annotations", ), )