Skip to content

fix: Resolve ClassNotFoundException for .kts scripts by ensuring pack… #427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

jnorthrup
Copy link

…age 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.

…age 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.
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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant