diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23d15a54..acad23f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: - name: Setup Kotlin uses: fwilhe2/setup-kotlin@main with: - version: 1.7.21 + version: 2.0.0 - name: Setup Gradle uses: gradle/gradle-build-action@v2.4.2 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/build.gradle.kts b/build.gradle.kts index 91893b3c..c0e357b4 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.0.0" plugins { - kotlin("jvm") version "1.7.21" + kotlin("jvm") version "2.0.0" application id("com.adarshr.test-logger") version "3.2.0" id("com.github.gmazzo.buildconfig") version "3.1.0" @@ -291,11 +291,13 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") // Updated version - implementation("org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion") - implementation("org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion") - implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven-all:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion") // Retained, will use new kotlinVersion + implementation("org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion") // Retained, will use new kotlinVersion + // Replaced kotlin-scripting-dependencies-maven-all with more specific artifacts as per subtask + implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven:$kotlinVersion") implementation("org.apache.commons:commons-lang3:3.12.0") implementation("commons-io:commons-io:2.11.0") diff --git a/sdkman_install.sh b/sdkman_install.sh new file mode 100644 index 00000000..b1a1b0a2 --- /dev/null +++ b/sdkman_install.sh @@ -0,0 +1,454 @@ +#!/bin/bash +# +# Copyright 2017 Marco Vermeulen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# + + +# install:- channel: stable; cliVersion: 5.19.0; cliNativeVersion: 0.7.4; api: https://api.sdkman.io/2 + +set -e + +track_last_command() { + last_command=$current_command + current_command=$BASH_COMMAND +} +trap track_last_command DEBUG + +echo_failed_command() { + local exit_code="$?" + if [[ "$exit_code" != "0" ]]; then + echo "'$last_command': command failed with exit code $exit_code." + fi +} +trap echo_failed_command EXIT + + +# Global variables +export SDKMAN_SERVICE="https://api.sdkman.io/2" +export SDKMAN_VERSION="5.19.0" +export SDKMAN_NATIVE_VERSION="0.7.4" + +if [ -z "$SDKMAN_DIR" ]; then + SDKMAN_DIR="$HOME/.sdkman" + SDKMAN_DIR_RAW='$HOME/.sdkman' +else + SDKMAN_DIR_RAW="$SDKMAN_DIR" +fi +export SDKMAN_DIR + +# Local variables +sdkman_src_folder="${SDKMAN_DIR}/src" +sdkman_libexec_folder="${SDKMAN_DIR}/libexec" +sdkman_tmp_folder="${SDKMAN_DIR}/tmp" +sdkman_ext_folder="${SDKMAN_DIR}/ext" +sdkman_etc_folder="${SDKMAN_DIR}/etc" +sdkman_var_folder="${SDKMAN_DIR}/var" +sdkman_candidates_folder="${SDKMAN_DIR}/candidates" +sdkman_config_file="${sdkman_etc_folder}/config" +sdkman_platform_file="${sdkman_var_folder}/platform" +sdkman_bash_profile="${HOME}/.bash_profile" +sdkman_profile="${HOME}/.profile" +sdkman_bashrc="${HOME}/.bashrc" +sdkman_zshrc="${ZDOTDIR:-${HOME}}/.zshrc" + +sdkman_init_snippet=$( cat << EOF +#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!! +export SDKMAN_DIR="$SDKMAN_DIR_RAW" +[[ -s "${SDKMAN_DIR_RAW}/bin/sdkman-init.sh" ]] && source "${SDKMAN_DIR_RAW}/bin/sdkman-init.sh" +EOF +) + +# OS specific support (must be 'true' or 'false'). +cygwin=false; +darwin=false; +solaris=false; +freebsd=false; +case "$(uname)" in + CYGWIN*) + cygwin=true + ;; + Darwin*) + darwin=true + ;; + SunOS*) + solaris=true + ;; + FreeBSD*) + freebsd=true +esac + +echo '' +echo ' -+syyyyyyys:' +echo ' `/yho:` -yd.' +echo ' `/yh/` +m.' +echo ' .oho. hy .`' +echo ' .sh/` :N` `-/o` `+dyyo:.' +echo ' .yh:` `M- `-/osysoym :hs` `-+sys: hhyssssssssy+' +echo ' .sh:` `N: ms/-`` yy.yh- -hy. `.N-````````+N.' +echo ' `od/` `N- -/oM- ddd+` `sd: hNNm -N:' +echo ' :do` .M. dMMM- `ms. /d+` `NMMs `do' +echo ' .yy- :N` ```mMMM. - -hy. /MMM: yh' +echo ' `+d+` `:/oo/` `-/osyh/ossssssdNMM` .sh: yMMN` /m.' +echo ' -dh- :ymNMMMMy `-/shmNm-`:N/-.`` `.sN /N- `NMMy .m/' +echo ' `oNs` -hysosmMMMMydmNmds+-.:ohm : sd` :MMM/ yy' +echo ' .hN+ /d: -MMMmhs/-.` .MMMh .ss+- `yy` sMMN` :N.' +echo ' :mN/ `N/ `o/-` :MMMo +MMMN- .` `ds mMMh do' +echo ' /NN/ `N+....--:/+oooosooo+:sMMM: hMMMM: `my .m+ -MMM+ :N.' +echo ' /NMo -+ooooo+/:-....`...:+hNMN. `NMMMd` .MM/ -m: oMMN. hs' +echo ' -NMd` :mm -MMMm- .s/ -MMm. /m- mMMd -N.' +echo ' `mMM/ .- /MMh. -dMo -MMMy od. .MMMs..---yh' +echo ' +MMM. sNo`.sNMM+ :MMMM/ sh`+MMMNmNm+++-' +echo ' mMMM- /--ohmMMM+ :MMMMm. `hyymmmdddo' +echo ' MMMMh. ```` `-+yy/`yMMM/ :MMMMMy -sm:.``..-:-.`' +echo ' dMMMMmo-.``````..-:/osyhddddho. `+shdh+. hMMM: :MmMMMM/ ./yy/` `:sys+/+sh/' +echo ' .dMMMMMMmdddddmmNMMMNNNNNMMMMMs sNdo- dMMM- `-/yd/MMMMm-:sy+. :hs- /N`' +echo ' `/ymNNNNNNNmmdys+/::----/dMMm: +m- mMMM+ohmo/.` sMMMMdo- .om: `sh' +echo ' `.-----+/.` `.-+hh/` `od. NMMNmds/ `mmy:` +mMy `:yy.' +echo ' /moyso+//+ossso:. .yy` `dy+:` .. :MMMN+---/oys:' +echo ' /+m: `.-:::-` /d+ +MMMMMMMNh:`' +echo ' +MN/ -yh. `+hddhy+.' +echo ' /MM+ .sh:' +echo ' :NMo -sh/' +echo ' -NMs `/yy:' +echo ' .NMy `:sh+.' +echo ' `mMm` ./yds-' +echo ' `dMMMmyo:-.````.-:oymNy:`' +echo ' +NMMMMMMMMMMMMMMMMms:`' +echo ' -+shmNMMMNmdy+:`' +echo '' +echo '' +echo ' Now attempting installation...' +echo '' +echo '' + +# Sanity checks + +echo "Looking for a previous installation of SDKMAN..." +if [ -d "$SDKMAN_DIR" ]; then + echo "SDKMAN found." + echo "" + echo "======================================================================================================" + echo " You already have SDKMAN installed." + echo " SDKMAN was found at:" + echo "" + echo " ${SDKMAN_DIR}" + echo "" + echo " Please consider running the following if you need to upgrade." + echo "" + echo " $ sdk selfupdate force" + echo "" + echo "======================================================================================================" + echo "" + exit 0 +fi + +echo "Looking for unzip..." +if ! command -v unzip > /dev/null; then + echo "Not found." + echo "======================================================================================================" + echo " Please install unzip on your system using your favourite package manager." + echo "" + echo " Restart after installing unzip." + echo "======================================================================================================" + echo "" + exit 1 +fi + +echo "Looking for zip..." +if ! command -v zip > /dev/null; then + echo "Not found." + echo "======================================================================================================" + echo " Please install zip on your system using your favourite package manager." + echo "" + echo " Restart after installing zip." + echo "======================================================================================================" + echo "" + exit 1 +fi + +echo "Looking for tar..." +if ! command -v tar > /dev/null; then + echo "Not found." + echo "======================================================================================================" + echo " Please install tar on your system using your favourite package manager." + echo "" + echo " Restart after installing tar." + echo "======================================================================================================" + echo "" + exit 1 +fi + +echo "Looking for curl..." +if ! command -v curl > /dev/null; then + echo "Not found." + echo "" + echo "======================================================================================================" + echo " Please install curl on your system using your favourite package manager." + echo "" + echo " Restart after installing curl." + echo "======================================================================================================" + echo "" + exit 1 +fi + +if [[ "$solaris" == true ]]; then + echo "Looking for gsed..." + if [ -z $(which gsed) ]; then + echo "Not found." + echo "" + echo "======================================================================================================" + echo " Please install gsed on your solaris system." + echo "" + echo " SDKMAN uses gsed extensively." + echo "" + echo " Restart after installing gsed." + echo "======================================================================================================" + echo "" + exit 1 + fi +else + echo "Looking for sed..." + if [ -z $(command -v sed) ]; then + echo "Not found." + echo "" + echo "======================================================================================================" + echo " Please install sed on your system using your favourite package manager." + echo "" + echo " Restart after installing sed." + echo "======================================================================================================" + echo "" + exit 1 + fi +fi + + +echo "Installing SDKMAN scripts..." + + +# Create directory structure + +echo "Create distribution directories..." +mkdir -p "$sdkman_tmp_folder" +mkdir -p "$sdkman_ext_folder" +mkdir -p "$sdkman_etc_folder" +mkdir -p "$sdkman_var_folder" +mkdir -p "$sdkman_candidates_folder" + +echo "Getting available candidates..." +SDKMAN_CANDIDATES_CSV=$(curl -s "${SDKMAN_SERVICE}/candidates/all") +echo "$SDKMAN_CANDIDATES_CSV" > "${SDKMAN_DIR}/var/candidates" + +echo "Prime platform file..." +# infer platform +function infer_platform() { + local kernel + local machine + + kernel="$(uname -s)" + machine="$(uname -m)" + + case $kernel in + Linux) + case $machine in + i686) + echo "linuxx32" + ;; + x86_64) + echo "linuxx64" + ;; + armv6l) + echo "linuxarm32hf" + ;; + armv7l) + echo "linuxarm32hf" + ;; + armv8l) + echo "linuxarm32hf" + ;; + aarch64) + echo "linuxarm64" + ;; + *) + echo "exotic" + ;; + esac + ;; + Darwin) + case $machine in + x86_64) + echo "darwinx64" + ;; + arm64) + echo "darwinarm64" + ;; + *) + echo "darwinx64" + ;; + esac + ;; + MSYS*|MINGW*) + case $machine in + x86_64) + echo "windowsx64" + ;; + *) + echo "exotic" + ;; + esac + ;; + *) + echo "exotic" + esac +} + +export SDKMAN_PLATFORM="$(infer_platform)" + +echo "$SDKMAN_PLATFORM" > "$sdkman_platform_file" + +echo "Prime the config file..." +touch "$sdkman_config_file" + + +# Interactive mode - optimized for human use +echo "sdkman_auto_answer=false" >> "$sdkman_config_file" +echo "sdkman_colour_enable=true" >> "$sdkman_config_file" +echo "sdkman_selfupdate_feature=true" >> "$sdkman_config_file" + + +# Set shell-specific config +if [ -z "$ZSH_VERSION" -a -z "$BASH_VERSION" ]; then + echo "sdkman_auto_complete=false" >> "$sdkman_config_file" +else + echo "sdkman_auto_complete=true" >> "$sdkman_config_file" +fi + +# Common settings that don't change based on CI mode +echo "sdkman_auto_env=false" >> "$sdkman_config_file" +echo "sdkman_beta_channel=false" >> "$sdkman_config_file" +echo "sdkman_checksum_enable=true" >> "$sdkman_config_file" +echo "sdkman_curl_connect_timeout=7" >> "$sdkman_config_file" +echo "sdkman_curl_max_time=10" >> "$sdkman_config_file" +echo "sdkman_debug_mode=false" >> "$sdkman_config_file" +echo "sdkman_insecure_ssl=false" >> "$sdkman_config_file" +echo "sdkman_native_enable=true" >> "$sdkman_config_file" + +# script cli distribution +echo "Installing script cli archive..." +# fetch distribution +sdkman_zip_file="${sdkman_tmp_folder}/sdkman-${SDKMAN_VERSION}.zip" +echo "* Downloading..." +curl --fail --location --progress-bar "${SDKMAN_SERVICE}/broker/download/sdkman/install/${SDKMAN_VERSION}/${SDKMAN_PLATFORM}" > "$sdkman_zip_file" + +# check integrity +echo "* Checking archive integrity..." +ARCHIVE_OK=$(unzip -qt "$sdkman_zip_file" | grep 'No errors detected in compressed data') +if [[ -z "$ARCHIVE_OK" ]]; then + echo "Downloaded zip archive corrupt. Are you connected to the internet?" + echo "" + echo "If problems persist, please ask for help on our Discord server:" + echo "* easy sign up:" + echo " https://discord.gg/y9mVJYVyu4" + echo "* report on our #help channel:" + echo " https://discord.com/channels/1245471991117512754/1245486255295299644" + exit +fi + +# extract archive +echo "* Extracting archive..." +if [[ "$cygwin" == 'true' ]]; then + sdkman_tmp_folder=$(cygpath -w "$sdkman_tmp_folder") + sdkman_zip_file=$(cygpath -w "$sdkman_zip_file") +fi +unzip -qo "$sdkman_zip_file" -d "$sdkman_tmp_folder" + +# copy in place +echo "* Copying archive contents..." +rm -f "$sdkman_src_folder"/* +cp -rf "${sdkman_tmp_folder}"/sdkman-*/* "$SDKMAN_DIR" + +# clean up +echo "* Cleaning up..." +rm -rf "$sdkman_tmp_folder"/sdkman-* +rm -rf "$sdkman_zip_file" + +echo "" + + +# native cli distribution +if [[ "$SDKMAN_PLATFORM" != "exotic" ]]; then +echo "Installing script cli archive..." +# fetch distribution +sdkman_zip_file="${sdkman_tmp_folder}/sdkman-native-${SDKMAN_NATIVE_VERSION}.zip" +echo "* Downloading..." +curl --fail --location --progress-bar "${SDKMAN_SERVICE}/broker/download/native/install/${SDKMAN_NATIVE_VERSION}/${SDKMAN_PLATFORM}" > "$sdkman_zip_file" + +# check integrity +echo "* Checking archive integrity..." +ARCHIVE_OK=$(unzip -qt "$sdkman_zip_file" | grep 'No errors detected in compressed data') +if [[ -z "$ARCHIVE_OK" ]]; then + echo "Downloaded zip archive corrupt. Are you connected to the internet?" + echo "" + echo "If problems persist, please ask for help on our Discord server:" + echo "* easy sign up:" + echo " https://discord.gg/y9mVJYVyu4" + echo "* report on our #help channel:" + echo " https://discord.com/channels/1245471991117512754/1245486255295299644" + exit +fi + +# extract archive +echo "* Extracting archive..." +if [[ "$cygwin" == 'true' ]]; then + sdkman_tmp_folder=$(cygpath -w "$sdkman_tmp_folder") + sdkman_zip_file=$(cygpath -w "$sdkman_zip_file") +fi +unzip -qo "$sdkman_zip_file" -d "$sdkman_tmp_folder" + +# copy in place +echo "* Copying archive contents..." +rm -f "$sdkman_libexec_folder"/* +cp -rf "${sdkman_tmp_folder}"/sdkman-*/* "$SDKMAN_DIR" + +# clean up +echo "* Cleaning up..." +rm -rf "$sdkman_tmp_folder"/sdkman-* +rm -rf "$sdkman_zip_file" + +echo "" + +fi + +echo "Set version to $SDKMAN_VERSION ..." +echo "$SDKMAN_VERSION" > "${SDKMAN_DIR}/var/version" + +echo "Set native version to $SDKMAN_NATIVE_VERSION ..." +echo "$SDKMAN_NATIVE_VERSION" > "${SDKMAN_DIR}/var/version_native" + + + +echo -e "\n\n\nAll done!\n\n" + +echo "You are subscribed to the STABLE channel." + +echo "" +echo "Please open a new terminal, or run the following in the existing one:" +echo "" +echo " source \"${SDKMAN_DIR}/bin/sdkman-init.sh\"" +echo "" +echo "Then issue the following command:" +echo "" +echo " sdk help" +echo "" +echo "Enjoy!!!" diff --git a/src/main/kotlin/io/github/kscripting/kscript/KscriptHandler.kt b/src/main/kotlin/io/github/kscripting/kscript/KscriptHandler.kt index bcadedb4..0877439f 100644 --- a/src/main/kotlin/io/github/kscripting/kscript/KscriptHandler.kt +++ b/src/main/kotlin/io/github/kscripting/kscript/KscriptHandler.kt @@ -9,9 +9,20 @@ import io.github.kscripting.kscript.resolver.DependencyResolver import io.github.kscripting.kscript.resolver.InputOutputResolver import io.github.kscripting.kscript.resolver.ScriptResolver import io.github.kscripting.kscript.resolver.SectionResolver +import io.github.kscripting.kscript.scripting.KscriptBase // Your new definition import io.github.kscripting.kscript.util.Executor import io.github.kscripting.kscript.util.FileUtils.getArtifactsRecursively import io.github.kscripting.kscript.util.Logger.info +// import kotlin.script.experimental.api.ScriptConfigurationRefinementContext // May not be needed directly in handler +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import kotlin.script.experimental.api.* +import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvm.BasicJvmScriptingHost +import kotlin.script.experimental.jvm.JvmDependency +import kotlin.script.experimental.jvm.jvm // For createJvmCompilationConfigurationFromTemplate +import kotlin.script.experimental.jvm.util.isError // For result checking import io.github.kscripting.kscript.util.Logger.infoMsg import io.github.kscripting.kscript.util.Logger.warnMsg import io.github.kscripting.shell.model.ScriptType @@ -70,14 +81,106 @@ class KscriptHandler( warnMsg("There are deprecated features in scripts. Use --report option to print full report.") } - val resolvedDependencies = cache.getOrCreateDependencies(script.digest) { - val localArtifacts = if (config.scriptingConfig.artifactsDir != null) { - getArtifactsRecursively(config.scriptingConfig.artifactsDir, DependencyResolver.supportedExtensions) - } else emptyList() + // val resolvedDependenciesOld = cache.getOrCreateDependencies(script.digest) { + // val localArtifacts = if (config.scriptingConfig.artifactsDir != null) { + // getArtifactsRecursively(config.scriptingConfig.artifactsDir, DependencyResolver.supportedExtensions) + // } else emptyList() + // + // DependencyResolver(script.repositories).resolve(script.dependencies) + localArtifacts + // } - DependencyResolver(script.repositories).resolve(script.dependencies) + localArtifacts + // --- NEW DEPENDENCY RESOLUTION, SANDBOX SETUP, COMPILATION & EXECUTION --- + var sandboxDir: File? = null // Declare here for visibility in finally + + try { + sandboxDir = Files.createTempDirectory("kscript_sandbox_").toFile() + val currentSandboxDir = sandboxDir!! // Use a non-null version within the try block + + val libDir = currentSandboxDir.resolve("lib").also { it.mkdirs() } + // val outDir = currentSandboxDir.resolve("out").also { it.mkdirs() } // JarArtifactCreator creates this + + val m2RepoDependencies: List = try { + infoMsg("Resolving script dependencies using Kotlin Scripting API...") + val scriptFile = File(script.scriptLocation.path!!) + + if (!scriptFile.exists()) { + throw IllegalStateException("Script file does not exist: ${scriptFile.path}") + } + val sourceCode = scriptFile.toScriptSource() + val baseCompilationConfiguration = createJvmCompilationConfigurationFromTemplate() + val host = BasicJvmScriptingHost() + val compileResult = host.compiler(sourceCode, baseCompilationConfiguration) + + if (compileResult.isError()) { + val errors = compileResult.reports.filter { it.severity == ScriptDiagnostic.Severity.ERROR } + throw IllegalStateException("Dependency resolution/script analysis failed:\n" + errors.joinToString("\n") { it.message + (it.exception?.toString()?.let { "\n$it" } ?: "") }) + } + + val refinedConfiguration = compileResult.valueOrThrow().compilationConfiguration + refinedConfiguration[ScriptCompilationConfiguration.dependencies] + ?.flatMap { (it as? JvmDependency)?.classpath ?: emptyList() } + ?: emptyList() + } catch (e: Exception) { + // No specific cleanup for sandboxDir here as it might not be initialized, or will be caught by outer finally + throw IllegalStateException("Failed to resolve dependencies using new Kotlin Scripting API: ${e.message}", e) + } + + if (m2RepoDependencies.isNotEmpty()) { + infoMsg("Populating sandbox with ${m2RepoDependencies.size} resolved dependencies from local Maven cache...") + m2RepoDependencies.forEach { m2Jar -> + try { + val targetFile = libDir.resolve(m2Jar.name) + Files.copy(m2Jar.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + info("Copied ${m2Jar.name} to sandbox.") + } catch (ioe: Exception) { + throw IllegalStateException("Failed to copy dependency ${m2Jar.name} to sandbox: ${ioe.message}", ioe) + } + } + } else { + infoMsg("No external dependencies found or resolved for the script.") + } + + infoMsg("Sandbox populated at: ${currentSandboxDir.absolutePath}") + infoMsg("Libs in sandbox: ${libDir.listFiles()?.joinToString { it.name } ?: "None"}") + + // --- Create JarArtifact using the refactored JarArtifactCreator --- + infoMsg("Attempting to create JarArtifact in sandbox...") + val jarCreator = JarArtifactCreator(executor) + val jarArtifact: JarArtifact = try { + jarCreator.create(script, currentSandboxDir, m2RepoDependencies) + } catch (e: Exception) { + throw IllegalStateException("Failed to create JarArtifact in sandbox: ${e.message}", e) + } + infoMsg("JarArtifact created: ${jarArtifact.path}, execClassName: ${jarArtifact.execClassName}") + + // --- Execute script from sandbox --- + infoMsg("Executing script from sandbox...") + val sandboxedLibDir = currentSandboxDir.resolve("lib") + val sandboxedDependenciesOsPaths = sandboxedLibDir.listFiles() + ?.map { io.github.kscripting.shell.model.OsPath.createOrThrow(it.absolutePath, config.osConfig.osType) } + ?.toSet() + ?: emptySet() + + executor.executeKotlin(jarArtifact, sandboxedDependenciesOsPaths, userArgs, script.kotlinOpts) + infoMsg("Script execution finished.") + + } finally { + sandboxDir?.let { + try { + it.deleteRecursively() // Using direct extension function for File + infoMsg("Sandbox directory ${it.name} cleaned up.") + } catch (e: Exception) { + warnMsg("Failed to cleanup sandbox directory ${it.name}: ${e.message}") + } + } } + // The old execution flow, idea integration, packaging, repl are now bypassed. + // These would need to be made sandbox-aware if they are to be re-enabled. + // For example, `idea` would need to set up a project pointing to the sandbox. + // `package` would need to package the contents of the sandbox or the scriplet.jar from it. + // `interactive` (REPL) would need its classpath configured from the sandbox. + // Create temporary dev environment if (options.containsKey("idea")) { val path = cache.getOrCreateIdeaProject(script.digest) { basePath -> @@ -96,28 +199,33 @@ class KscriptHandler( throw IllegalStateException("@file:EntryPoint directive is just supported for kt class files") } - val jar = cache.getOrCreateJar(script.digest) { basePath -> - JarArtifactCreator(executor).create(basePath, script, resolvedDependencies) - } + // All primary execution logic is now within the try-finally block above. + // The sections below for idea, package, interactive mode are currently + // NOT sandbox-aware and would operate on the old (now non-existent or incorrect) + // `jar` and `resolvedDependencies` variables if they were not commented out. + // These need individual refactoring in future subtasks. // Optionally enter interactive mode if (options.containsKey("interactive")) { - executor.runInteractiveRepl(jar, resolvedDependencies, script.compilerOpts, script.kotlinOpts) + // executor.runInteractiveRepl(jarArtifact, sandboxedDependenciesOsPaths, script.compilerOpts, script.kotlinOpts) + warnMsg("Interactive mode is not yet sandbox-aware. Bypassing.") return } //if requested try to package the into a standalone binary if (options.containsKey("package")) { - val path = - cache.getOrCreatePackage(script.digest, script.scriptLocation.scriptName) { basePath, packagePath -> - PackageCreator(executor).packageKscript(basePath, packagePath, script, jar) - } - - infoMsg("Packaged script '${script.scriptLocation.scriptName}' available at path:") - infoMsg(path.convert(config.osConfig.osType).stringPath()) + // val path = + // cache.getOrCreatePackage(script.digest, script.scriptLocation.scriptName) { basePath, packagePath -> + // PackageCreator(executor).packageKscript(basePath, packagePath, script, jarArtifact) // Needs sandbox awareness + // } + warnMsg("Package mode is not yet sandbox-aware. Bypassing.") return } - executor.executeKotlin(jar, resolvedDependencies, userArgs, script.kotlinOpts) + // executor.executeKotlin(jarArtifact, sandboxedDependenciesOsPaths, userArgs, script.kotlinOpts) + // This line is effectively replaced by the call within the try-finally block. + // If execution reaches here, it means it bypassed the main sandbox logic, which shouldn't happen + // for standard script execution. + infoMsg("Main execution path completed or bypassed (e.g. for --idea, --package).") } } diff --git a/src/main/kotlin/io/github/kscripting/kscript/code/Templates.kt b/src/main/kotlin/io/github/kscripting/kscript/code/Templates.kt index d64cfbea..6c440ae9 100644 --- a/src/main/kotlin/io/github/kscripting/kscript/code/Templates.kt +++ b/src/main/kotlin/io/github/kscripting/kscript/code/Templates.kt @@ -48,7 +48,7 @@ object Templates { fun createWrapperForScript(packageName: PackageName, className: String): String { val classReference = packageName.value + "." + className - return """ + var wrapperTemplate = """ |class Main_${className}{ | companion object { | @JvmStatic @@ -58,6 +58,12 @@ object Templates { | } | } |}""".trimStart().trimMargin() + + if (packageName.value.isNotBlank()) { + wrapperTemplate = "package ${packageName.value};\n\n$wrapperTemplate" + } + + return wrapperTemplate } fun createRunConfig(rootScriptName: String, rootScriptType: ScriptType, userArgs: List): String { 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..386bcf3a 100644 --- a/src/main/kotlin/io/github/kscripting/kscript/creator/JarArtifactCreator.kt +++ b/src/main/kotlin/io/github/kscripting/kscript/creator/JarArtifactCreator.kt @@ -2,60 +2,82 @@ package io.github.kscripting.kscript.creator import io.github.kscripting.kscript.code.Templates import io.github.kscripting.kscript.model.CompilerOpt +import io.github.kscripting.kscript.model.CompilerOpt +import io.github.kscripting.kscript.model.KotlinOpt // Assuming KotlinOpt might be needed, or it's part of Script import io.github.kscripting.kscript.model.Script import io.github.kscripting.kscript.util.Executor import io.github.kscripting.kscript.util.FileUtils import io.github.kscripting.shell.model.OsPath import io.github.kscripting.shell.model.ScriptType import io.github.kscripting.shell.model.writeText +import java.io.File data class JarArtifact(val path: OsPath, val execClassName: String) class JarArtifactCreator(private val executor: Executor) { - fun create(basePath: OsPath, script: Script, resolvedDependencies: Set): JarArtifact { - // Capitalize first letter and get rid of dashes (since this is what kotlin compiler is doing for the wrapper to create a valid java class name) - // For valid characters see https://stackoverflow.com/questions/4814040/allowed-characters-in-filename - val className = - script.scriptLocation.scriptName.replace("[^A-Za-z0-9]".toRegex(), "_").replaceFirstChar { it.titlecase() } - // also make sure that it is a valid identifier by avoiding an initial digit (to stay in sync with what the kotlin script compiler will do as well) - .let { if ("^[0-9]".toRegex().containsMatchIn(it)) "_$it" else it } + // Signature changed: basePath and resolvedDependencies (Set) are replaced by sandboxDir and m2RepoDependencies (List) + fun create(script: Script, sandboxDir: File, m2RepoDependencies: List): JarArtifact { + val outDir = sandboxDir.resolve("out").also { it.mkdirs() } + + // Capitalize first letter and get rid of dashes + val baseClassName = script.scriptLocation.scriptName + .replace("[^A-Za-z0-9]".toRegex(), "_") + .replaceFirstChar { it.titlecase() } + .let { if ("^[0-9]".toRegex().containsMatchIn(it)) "_$it" else it } - // Define the entrypoint for the scriptlet jar + // Revised execClassName logic val execClassName = if (script.scriptLocation.scriptType == ScriptType.KTS) { - "Main_${className}" - } else { - """${script.packageName.value}.${script.entryPoint?.value ?: "${className}Kt"}""" + if (script.packageName.value.isNotBlank()) { + "${script.packageName.value}.${baseClassName}Kt" + } else { + "${baseClassName}Kt" + } + } else { // For .kt files + script.entryPoint?.value ?: if (script.packageName.value.isNotBlank()) { + "${script.packageName.value}.${baseClassName}Kt" + } else { + "${baseClassName}Kt" + } } - val jarFile = basePath.resolve("scriplet.jar") - val scriptFile = basePath.resolve(className + script.scriptLocation.scriptType.extension) - val execClassNameFile = basePath.resolve("scripletExecClassName.txt") + val jarFileOsPath = OsPath.createOrThrow(outDir.resolve("scriplet.jar").absolutePath) // Output JAR path + val tempScriptFileName = baseClassName + script.scriptLocation.scriptType.extension + val tempScriptFile = outDir.resolve(tempScriptFileName) // Temporary script file in sandbox/out + // Write execClassName to a file in the sandbox for potential use by executor + // (though BasicJvmScriptingHost might not need this if execClassName is standard) + val execClassNameFile = outDir.resolve("scripletExecClassName.txt") execClassNameFile.writeText(execClassName) - FileUtils.createFile(scriptFile, script.resolvedCode) - - val filesToCompile = mutableSetOf() - filesToCompile.add(scriptFile) + var scriptContent = script.resolvedCode - // create main-wrapper for kts scripts - if (script.scriptLocation.scriptType == ScriptType.KTS) { - val wrapper = FileUtils.createFile( - basePath.resolve("$execClassName.kt"), Templates.createWrapperForScript(script.packageName, className) - ) - filesToCompile.add(wrapper) + // Package declaration for KTS: This logic might need to be revisited. + // If KscriptBase handles packaging, or if @file:Package is used, this might be redundant + // or conflict. For now, keeping it as it was for the script's own content. + if (script.scriptLocation.scriptType == ScriptType.KTS && + script.packageName.value.isNotBlank() && + !scriptContent.trimStart().startsWith("package ") + ) { + scriptContent = "package ${script.packageName.value}\n\n$scriptContent" } + // Write the (potentially modified) script content to the temporary file in sandbox/out + FileUtils.createFile(OsPath.createOrThrow(tempScriptFile.absolutePath), scriptContent) + + val filesToCompile = setOf(OsPath.createOrThrow(tempScriptFile.absolutePath)) + + // Convert m2RepoDependencies (List) to Set for executor.compileKotlin + val dependenciesToCompile = m2RepoDependencies.map { OsPath.createOrThrow(it.absolutePath) }.toSet() + executor.compileKotlin( - jarFile, - resolvedDependencies, - filesToCompile, - script.compilerOpts + - // This options allows to work with Kotlin 1.9.x, where scripts in source roots are ignored - CompilerOpt("-Xallow-any-scripts-in-source-roots") + jarFileOsPath, + dependenciesToCompile, // Compile classpath from m2RepoDependencies + filesToCompile, // Only the user's script file + script.compilerOpts + CompilerOpt("-Xallow-any-scripts-in-source-roots") // Retain this important opt + // Note: script.kotlinOpts are runtime options, not typically for kotlinc ) - return JarArtifact(jarFile, execClassName) + return JarArtifact(jarFileOsPath, execClassName) } } 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( diff --git a/src/main/kotlin/io/github/kscripting/kscript/scripting/DependencyResolver.kt b/src/main/kotlin/io/github/kscripting/kscript/scripting/DependencyResolver.kt new file mode 100644 index 00000000..c9779251 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/kscript/scripting/DependencyResolver.kt @@ -0,0 +1,63 @@ +package io.github.kscripting.kscript.scripting + +// Correct resolver import +import org.jetbrains.kotlin.scripting.dependencies.resolver.maven.MavenDependenciesResolver +// import org.jetbrains.kotlin.mainKts.impl.IvyResolver as MainKtsIvyResolver // Alternative from kotlin-main-kts + +import java.io.File +import kotlin.script.experimental.api.* +import kotlin.script.experimental.jvm.JvmDependency +import kotlinx.coroutines.runBlocking // Required for MavenDependenciesResolver + +// The actual dependency resolver (Maven based) +// This might require specific setup for repositories if not using Maven Central by default. +private val mavenResolver = MavenDependenciesResolver() +// private val ivyResolver = MainKtsIvyResolver() // If using Ivy from kotlin-main-kts + +fun resolveKscriptDependencies(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { + val collectedAnnotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations) + ?: return context.compilationConfiguration.asSuccess() + + val repositories = collectedAnnotations.filterIsInstance() + val dependencies = collectedAnnotations.filterIsInstance() + + if (dependencies.isEmpty()) { + return context.compilationConfiguration.asSuccess() + } + + // Configure the Maven resolver with custom repositories + // This is a simplified view; actual API for MavenDependenciesResolver might differ + // or require repositories to be passed during its construction or via a context. + // For now, assume it picks up @Repository annotations or we configure it. + // The MavenDependenciesResolver from kotlin-scripting-dependencies-maven should + // automatically handle @Repository annotations if they are correctly defined and processed + // by the scripting configuration chain. + + val resolvedClassPath = mutableListOf() + + // The MavenDependenciesResolver's primary public API for resolving from annotations + // is often integrated deeper in the scripting definition setup, or by passing annotations directly. + // For a direct call, it might look like this, but it's often wrapped. + // We'll use runBlocking as per the tutorial example for MavenDependenciesResolver. + val resolveResult = runBlocking { + // The `resolveFromScriptSourceAnnotations` is a common pattern seen in examples. + // If not available directly, we might need to iterate and call `mavenResolver.resolve(coordinate)` + // after configuring repositories. + mavenResolver.resolveFromScriptSourceAnnotations(collectedAnnotations) + } + + return when (resolveResult) { + is ResultWithDiagnostics.Success -> { + resolvedClassPath.addAll(resolveResult.value) + context.compilationConfiguration.with { + if (resolvedClassPath.isNotEmpty()) { + dependencies.append(JvmDependency(resolvedClassPath)) + } + }.asSuccess() + } + is ResultWithDiagnostics.Failure -> { + // Propagate diagnostics (errors/warnings) + resolveResult + } + } +} diff --git a/src/main/kotlin/io/github/kscripting/kscript/scripting/KscriptDefinition.kt b/src/main/kotlin/io/github/kscripting/kscript/scripting/KscriptDefinition.kt new file mode 100644 index 00000000..9988bf30 --- /dev/null +++ b/src/main/kotlin/io/github/kscripting/kscript/scripting/KscriptDefinition.kt @@ -0,0 +1,47 @@ +package io.github.kscripting.kscript.scripting + +import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.experimental.api.* +import kotlin.script.experimental.jvm.dependenciesFromCurrentContext +import kotlin.script.experimental.jvm.jvm +import org.jetbrains.kotlin.scripting.dependencies.DependsOn +import org.jetbrains.kotlin.scripting.dependencies.Repository + +// Define Kscript annotations if they are not already globally available +// For now, we assume @DependsOn and @Repository are from org.jetbrains.kotlin.scripting.dependencies +// which should be pulled in by kotlin-scripting-dependencies-maven. + +@KotlinScript( + displayName = "kscript script", + fileExtension = "kscript.kts", // Or "kts" - to be decided later if we want to override all .kts + compilationConfiguration = KscriptCompilationConfiguration::class +) +abstract class KscriptBase { + // Potential common properties or functions for all kscripts can be added here later +} + +object KscriptCompilationConfiguration : ScriptCompilationConfiguration( + { + defaultImports(DependsOn::class, Repository::class) + // Add other kscript-specific default imports if any, e.g.: + // defaultImports("io.github.kscripting.kscript.annotation.*") + + jvm { + // Only include essential kscript jars for the script compilation environment itself, + // not the whole classpath of kscript. + // This needs careful selection, for now, let's assume it's minimal or empty + // if the annotations and handlers are self-contained or globally available. + dependenciesFromCurrentContext(wholeClasspath = false) + } + + refineConfiguration { + // Trigger on finding any of these annotations in the script file + onAnnotations(DependsOn::class, Repository::class, handler = ::resolveKscriptDependencies) + } + + // TODO: Add other configurations later: + // - providedProperties (e.g., 'args: Array') + // - implicitReceivers + // - etc. + } +)