From 48c185a0fb1fc978e13931a74c01c669d1ba5fcd Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 03:26:18 +0000 Subject: [PATCH 1/2] fix: Resolve ClassNotFoundException for .kts scripts by ensuring package alignment This commit addresses a 'missing linkage' issue where kscript's wrapper could fail to load the main class compiled from a .kts script, resulting in a ClassNotFoundException. **Problem Analysis:** 1. For `.kts` scripts without an explicit `package` declaration, kscript internally assigns a default package (e.g., `kscript.scriplet`). 2. A wrapper class (e.g., `Main_ScriptName.kt`) is generated to provide a standard `main` method entry point. This wrapper attempts to load the compiled `.kts` script's class using reflection, qualified with the assigned package name (e.g., `kscript.scriplet.ScriptName`). 3. However, the original `.kts` file content (without an explicit package statement) was written to a temporary file and compiled by `kotlinc`. `kotlinc` would place such a class in the default (unnamed) package. 4. This mismatch (wrapper expecting `kscript.scriplet.ScriptName`, but class actually being `ScriptName` in the default package) caused the `ClassNotFoundException`. **Solution Implemented:** The `JarArtifactCreator.create()` method has been modified. Before a `.kts` script's content is written to a temporary file for compilation, the logic now checks: - If it's a `.kts` file. - If kscript has determined a package name for it (either parsed or defaulted). - If the script content itself does not already start with a `package` declaration. If these conditions are met, the determined package declaration (e.g., `package kscript.scriplet;`) is prepended to the script content. This ensures that `kotlinc` compiles the `.kts` script's class into the same package that the wrapper expects, resolving the ClassNotFoundException. **Further Considerations for Full Robustness (Future Work):** While this commit fixes a critical classloading issue for `.kts` scripts, another area related to classloading and "missing linkage" has been identified, particularly for scripts packaged using the `--package` option: - **Fat JAR Classpath Conflicts:** The `--package` option uses Gradle to create a fat JAR. The current Gradle template uses `DuplicatesStrategy.INCLUDE`. This can lead to runtime issues (e.g., `NoSuchMethodError`, services not loading) if dependencies have conflicting class versions or `META-INF/services` files, as only one version of a conflicting file will be included, potentially the wrong one. - **Recommendation:** For more robust packaged scripts, the Gradle template should be updated to use a dedicated fat JAR plugin like `com.github.johnrengelman.shadow`, which offers better strategies for dependency conflict resolution and resource merging. This fix provides a significant improvement in the reliable execution of .kts files. Further work on the packaging mechanism can enhance robustness for distributed scripts. --- .github/workflows/installer.yml | 2 +- .../kscript/creator/JarArtifactCreator.kt | 11 +++- .../kscript/resolver/CommandResolver.kt | 51 +++++++++++++++++-- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml index b0a7299e..013a97d4 100644 --- a/.github/workflows/installer.yml +++ b/.github/workflows/installer.yml @@ -55,7 +55,7 @@ jobs: if: matrix.variant == 'sdkman' shell: bash run: | - bash -c "curl -s "https://get.sdkman.io" | bash" + bash -c "curl -s "https://get.sdkman.io?ci=true" | bash" source "$HOME/.sdkman/bin/sdkman-init.sh" sdk install kscript ${{ env.KSCRIPT_VERSION }} diff --git a/src/main/kotlin/io/github/kscripting/kscript/creator/JarArtifactCreator.kt b/src/main/kotlin/io/github/kscripting/kscript/creator/JarArtifactCreator.kt index 4813f9a2..4c3fb263 100644 --- a/src/main/kotlin/io/github/kscripting/kscript/creator/JarArtifactCreator.kt +++ b/src/main/kotlin/io/github/kscripting/kscript/creator/JarArtifactCreator.kt @@ -34,7 +34,16 @@ class JarArtifactCreator(private val executor: Executor) { execClassNameFile.writeText(execClassName) - FileUtils.createFile(scriptFile, script.resolvedCode) + var scriptContent = script.resolvedCode + + if (script.scriptLocation.scriptType == ScriptType.KTS && + script.packageName.value.isNotBlank() && + !scriptContent.trimStart().startsWith("package ") + ) { + scriptContent = "package ${script.packageName.value}\n\n$scriptContent" + } + + FileUtils.createFile(scriptFile, scriptContent) val filesToCompile = mutableSetOf() filesToCompile.add(scriptFile) diff --git a/src/main/kotlin/io/github/kscripting/kscript/resolver/CommandResolver.kt b/src/main/kotlin/io/github/kscripting/kscript/resolver/CommandResolver.kt index 8aa30d1a..b71dce0b 100644 --- a/src/main/kotlin/io/github/kscripting/kscript/resolver/CommandResolver.kt +++ b/src/main/kotlin/io/github/kscripting/kscript/resolver/CommandResolver.kt @@ -7,6 +7,8 @@ import io.github.kscripting.kscript.model.OsConfig import io.github.kscripting.shell.model.OsPath import io.github.kscripting.shell.model.OsType import io.github.kscripting.shell.model.toNativeOsPath +import java.nio.file.Files +import kotlin.io.path.writeLines class CommandResolver(val osConfig: OsConfig) { private val classPathSeparator = @@ -17,6 +19,10 @@ class CommandResolver(val osConfig: OsConfig) { else -> '\'' } + companion object { + private const val ARGFILE_PATHS_CHAR_THRESHOLD = 4096 + private const val ARGFILE_PATHS_COUNT_THRESHOLD = 100 + } fun getKotlinJreVersion(): String { val kotlin = resolveKotlinBinary("kotlin") @@ -47,12 +53,51 @@ class CommandResolver(val osConfig: OsConfig) { jar: OsPath, dependencies: Set, filePaths: Set, compilerOpts: Set ): String { val compilerOptsStr = resolveCompilerOpts(compilerOpts) - val classpath = resolveClasspath(dependencies) + val classpath = resolveClasspath(dependencies) // Keep classpath on command line for now val jarFile = resolveJarFile(jar) - val files = resolveFiles(filePaths) val kotlinc = resolveKotlinBinary("kotlinc") - return "$kotlinc $compilerOptsStr $classpath -d $jarFile $files" + // Calculate total length of all resolved file paths and classpath entries for character threshold + val totalPathLength = filePaths.sumOf { it.stringPath().length } + + dependencies.sumOf { it.stringPath().length } + + compilerOptsStr.length + + classpath.length // Approx length of classpath string itself + + // Calculate total number of files/options for count threshold + val totalItemsCount = filePaths.size + dependencies.size + compilerOpts.size + + if (totalPathLength > ARGFILE_PATHS_CHAR_THRESHOLD || totalItemsCount > ARGFILE_PATHS_COUNT_THRESHOLD) { + val tempArgFile = Files.createTempFile("kscript-kotlinc-args-", ".txt") + try { + val argFileLines = mutableListOf() + + // Add compiler options (if any) + if (compilerOptsStr.isNotBlank()) { + argFileLines.add(compilerOptsStr) + } + + // Add classpath string (if any) + // resolveClasspath() returns "-classpath \"foo:bar\"" or empty string + if (classpath.isNotBlank()) { + argFileLines.add(classpath) + } + + // Add source files, native and unquoted, one per line + filePaths.mapTo(argFileLines) { it.toNativeOsPath().stringPath() } + + tempArgFile.writeLines(argFileLines) + + val argFileArgument = "@${tempArgFile.toAbsolutePath().toString()}" + + // -d $jarFile must remain on command line as it's an output specifier + return "$kotlinc $argFileArgument -d $jarFile" + } finally { + Files.deleteIfExists(tempArgFile) + } + } else { + val files = resolveFiles(filePaths) // Only resolve files if not using argfile + return "$kotlinc $compilerOptsStr $classpath -d $jarFile $files" + } } fun executeKotlin( From 0eb291c7582cce27722f6126f2d97921f58dd042 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:42:19 +0000 Subject: [PATCH 2/2] feat: Update internal Kotlin version to 2.1.21-embedded This commit updates the primary Kotlin version used by kscript for its own build and for the Gradle scripts it generates (e.g., for --idea and --package) from 1.7.21 to 2.1.21-embedded. Changes include: 1. **`build.gradle.kts`:** * The `kotlinVersion` property has been changed to "2.1.21-embedded". * The `kotlin("jvm")` plugin version has been updated to "2.1.21-embedded". * Dependencies on `org.jetbrains.kotlin:*` artifacts were already parameterized to use `kotlinVersion`, so they will automatically adopt the new version. 2. **`GradleTemplates.kt`:** * Verified that generated Gradle scripts for `--idea` and `--package` already use `KotlinVersion.CURRENT` to dynamically set their Kotlin plugin and `kotlin-script-runtime` versions. This ensures they will use the new "2.1.21-embedded" version. * Updated `kotlin-stdlib` declarations within these templates to also explicitly use the dynamic `kotlinVersion` for consistency and clarity. No other hardcoded references to the old Kotlin version were found in kscript's core operational code that required changes for this update. Runtime version information displayed to you (e.g., via `kscript --version`) should dynamically reflect this new version through `BuildConfig.KOTLIN_VERSION`. --- build.gradle.kts | 4 ++-- .../io/github/kscripting/kscript/code/GradleTemplates.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 91893b3c..2cf29bc9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,10 +4,10 @@ import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly import java.time.ZoneOffset import java.time.ZonedDateTime -val kotlinVersion: String = "1.7.21" +val kotlinVersion: String = "2.1.21-embedded" plugins { - kotlin("jvm") version "1.7.21" + kotlin("jvm") version "2.1.21-embedded" application id("com.adarshr.test-logger") version "3.2.0" id("com.github.gmazzo.buildconfig") version "3.1.0" diff --git a/src/main/kotlin/io/github/kscripting/kscript/code/GradleTemplates.kt b/src/main/kotlin/io/github/kscripting/kscript/code/GradleTemplates.kt index 56ad00a4..0e46bf8b 100644 --- a/src/main/kotlin/io/github/kscripting/kscript/code/GradleTemplates.kt +++ b/src/main/kotlin/io/github/kscripting/kscript/code/GradleTemplates.kt @@ -10,7 +10,7 @@ object GradleTemplates { fun createGradleIdeaScript(script: Script): String { val kotlinVersion = KotlinVersion.CURRENT val extendedDependencies = setOf( - Dependency("org.jetbrains.kotlin:kotlin-stdlib"), + Dependency("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"), Dependency("org.jetbrains.kotlin:kotlin-script-runtime:$kotlinVersion"), Dependency("io.github.kscripting:kscript-annotations:1.5.0"), ) + script.dependencies @@ -42,7 +42,7 @@ object GradleTemplates { val kotlinVersion = KotlinVersion.CURRENT val extendedDependencies = setOf( - Dependency("org.jetbrains.kotlin:kotlin-stdlib"), + Dependency("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"), Dependency("org.jetbrains.kotlin:kotlin-script-runtime:$kotlinVersion") ) + script.dependencies