All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.kotlin_script.KotlinScript.kt Maven / Gradle / Ivy

package kotlin_script

import com.github.ajalt.mordant.terminal.Terminal
import java.io.File
import java.io.IOException
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermissions
import java.util.jar.Attributes
import java.util.jar.Manifest
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.concurrent.thread
import kotlin.io.path.*
import kotlin.system.exitProcess

class KotlinScript(
    private val javaHome: Path = findJavaHome(),
    private val mavenRepoUrl: String = System.getenv("M2_CENTRAL_REPO")
        ?.takeIf { v -> v.isNotBlank() }
        ?.trim()
        ?: "https://repo1.maven.org/maven2",
    private val mavenRepoCache: Path? = System.getenv("M2_LOCAL_MIRROR")
        ?.takeIf { v -> v.isNotBlank() }
        ?.trim()
        ?.let { Paths.get(it) },
    private val localRepo: Path = System.getenv("M2_LOCAL_REPO")
        ?.takeIf { v -> v.isNotBlank() }
        ?.trim()
        ?.let { Paths.get(it) }
        ?: userHome.resolve(".m2/repository"),
    private val progress: Boolean = false,
    private val trace: Boolean = false,
    private val force: Boolean = false,
) {
    private val javaVersion =
        System.getProperty("java.vm.specification.version") ?: "1.8"

    private val kotlinJvmTarget = when (javaVersion) {
        in supportedJavaVersions -> javaVersion
        else -> supportedJavaVersions.last()
    }

    private val p = Progress(
        t = if (progress) Terminal() else null,
        trace = trace
    )

    private val resolver = Resolver(
        mavenRepoUrl = mavenRepoUrl,
        mavenRepoCache = mavenRepoCache,
        localRepo = localRepo,
        p = p
    )

    private fun defaultDependencyVersion(d: Dependency): String {
        return when (d.groupId) {
            KOTLIN_GROUP_ID -> KOTLIN_VERSION
            else -> error("no default version for $d")
        }
    }

    private fun kotlinCompilerArgs(
        compilerDependencies: List,
        compilerPlugins: List
    ): Array {
        val cp = compilerDependencies.joinToString(File.pathSeparator) { f ->
            //TODO use correct quoting
            f.toAbsolutePath().toString()
        }
        return arrayOf(
            (javaHome / "bin" / "java").absolutePathString(),
            "-Djava.awt.headless=true",
            "-cp", cp,
            KOTLIN_COMPILER_MAIN,
            *compilerPlugins.map { p ->
                "-Xplugin=${p.absolutePathString()}"
            }.toTypedArray(),
            "-jvm-target", kotlinJvmTarget,
            "-no-reflect",
            "-no-stdlib"
        )
    }

    private fun ZipOutputStream.writeFileTree(start: Path) {
        val startFullPath = start.toUri().path
        Files.walkFileTree(start, object : SimpleFileVisitor() {
            override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes) =
                FileVisitResult.CONTINUE.also {
                    val fullPath = dir.toUri().path
                    val entryName = fullPath.removePrefix(startFullPath)
                    if (entryName.isNotEmpty()) {
                        putNextEntry(ZipEntry(entryName))
                        closeEntry()
                    }
                }
            override fun visitFile(file: Path, attrs: BasicFileAttributes) =
                FileVisitResult.CONTINUE.also {
                    val fullPath = file.toUri().path
                    val entryName = fullPath.removePrefix(startFullPath)
                    if (entryName.isNotEmpty()) {
                        putNextEntry(ZipEntry(entryName))
                        file.inputStream().use { `in` ->
                            `in`.copyTo(this@writeFileTree)
                        }
                        closeEntry()
                    }
                }
        })
    }

    fun jarCachePath(metaData: MetaData): Path =
        localRepo / metaData.jarCachePath(kotlinJvmTarget)

    fun compile(script: Script): MetaData {
        val scriptFileName = script.path.name
        val scriptFileArgs = when (script.path.extension) {
            "kt" -> listOf(scriptFileName)
            else -> emptyList()
        }
        val addClassPath = listOf(kotlinStdlibDependency)
        val metaData = parseMetaData(KOTLIN_SCRIPT_VERSION, script).let { md ->
            md.copy(dep = addClassPath + md.dep.map { d ->
                if (d.version.isBlank()) {
                    d.copy(version = defaultDependencyVersion(d))
                } else {
                    d
                }
            })
        }

        val compilerDependencies = mutableMapOf()
        val resolvedDependencies = mutableMapOf()

        val targetFile = jarCachePath(metaData)
        if (!force && targetFile.isReadable()) {
            // only need to fetch runtime dependencies
            resolver.resolveLibs(
                emptyList(),
                metaData.dep
                    .filterNot { d -> d.scope == Scope.Plugin }
                    .map { d ->
                        if (d.version.isBlank()) {
                            d.copy(version = defaultDependencyVersion(d))
                        } else {
                            d
                        }
                    },
                compilerDependencies,
                resolvedDependencies
            )
            return metaData
        }

        // copy script to temp dir
        val tmp = createTempDirectory(script.path.name)
        Runtime.getRuntime().addShutdownHook(thread(start = false) {
            cleanup(tmp)
        })
        val maxDepth = metaData.inc.maxOfOrNull { inc ->
            // e.g. ../../../common/util.kt -> 2
            inc.path.indexOfLast { component -> component.name == ".." }
        } ?: -1
        val scriptTmpSubPath = when {
            maxDepth >= 0 -> {
                // e.g. maxDepth = 2
                // /work/kotlin_script/src/main/kotlin/main.kt
                // -> src/main/kotlin/main.kt
                val nameCount = script.path.nameCount
                val scriptSubPath = script.path.subpath(
                    nameCount - maxDepth - 2,
                    nameCount
                )
                scriptSubPath
            }

            else -> script.path.fileName
        }
        val scriptTmpPath = tmp / scriptTmpSubPath
        val scriptTmpParent = scriptTmpPath.parent
        val incArgs = p.withProgress("initializing $tmp") {
            if (tmp != scriptTmpParent && !scriptTmpParent.exists()) {
                scriptTmpParent.createDirectories()
            }
            scriptTmpPath.outputStream().use { out ->
                out.write(metaData.mainScript.data)
            }

            // copy inc to temp dir
            metaData.inc.map { inc ->
                val tmpIncFile = scriptTmpParent / inc.path
                val tmpIncParent = tmpIncFile.parent
                if (tmp != tmpIncParent) {
                    tmpIncParent.createDirectories()
                }
                tmpIncFile.outputStream().use { out ->
                    out.write(inc.data)
                }
                inc.path.pathString
            }
        }

        // call compiler
        val (rc, compilerErrors) = if (scriptFileArgs.isNotEmpty()
                || incArgs.isNotEmpty()) {
            resolver.resolveLibs(
                compilerClassPath,
                metaData.dep.map { d ->
                    if (d.version.isBlank()) {
                        d.copy(version = defaultDependencyVersion(d))
                    } else {
                        d
                    }
                },
                compilerDependencies,
                resolvedDependencies
            )
            val kotlinCompilerArgs = kotlinCompilerArgs(
                compilerDependencies.map { (_, f) -> f.toAbsolutePath() },
                resolvedDependencies
                    .filter { (d, _) -> d.scope == Scope.Plugin }
                    .map { (_, f) -> f.toAbsolutePath() }
            )
            val compileClassPath = resolvedDependencies
                .filter { (d, _) -> d.scope == Scope.Compile }
                .map { (_, f) -> f.toAbsolutePath() }
            val compileClassPathArgs = when {
                compileClassPath.isEmpty() -> emptyList()
                else -> listOf(
                    "-cp",
                    compileClassPath.joinToString(File.pathSeparator)
                )
            }
            val compilerArgs: List = listOf(
                *kotlinCompilerArgs,
                *metaData.compilerArgs.toTypedArray(),
                *compileClassPathArgs.toTypedArray(),
                "-d", tmp.toAbsolutePath().toString(),
                *scriptFileArgs.toTypedArray(),
                *incArgs.toTypedArray()
            )
            p.trace(*compilerArgs.toTypedArray())
            p.withProgress("compiling ${(scriptFileArgs + incArgs).first()}") {
                val compilerLog = scriptTmpParent / "kotlin_script.log"
                val compilerProcess = ProcessBuilder(compilerArgs)
                    .directory(scriptTmpParent.toFile())
                    .redirectErrorStream(true)
                    .redirectOutput(compilerLog.toFile())
                    .start()
                compilerProcess.outputStream.close()
                val rc = compilerProcess.waitFor()
                val compilerErrors = compilerLog.readText()
                rc to compilerErrors
            }
        } else {
            // only need to fetch runtime dependencies
            resolver.resolveLibs(
                emptyList(),
                metaData.dep
                    .filterNot { d -> d.scope == Scope.Plugin }
                    .map { d ->
                        if (d.version.isBlank()) {
                            d.copy(version = defaultDependencyVersion(d))
                        } else {
                            d
                        }
                    },
                compilerDependencies,
                resolvedDependencies
            )
            0 to ""
        }

        if (rc != 0) {
            System.err.println(compilerErrors)
            exitProcess(rc)
        }

        // embed metadata into jar
        p.trace("write", (tmp / "kotlin_script.metadata").absolutePathString())
        metaData.storeToFile(tmp / "kotlin_script.metadata")
        val manifestFile = tmp.resolve(MANIFEST_PATH)
        val manifest = when {
            manifestFile.exists() ->
                manifestFile.inputStream().use { `in` ->
                    Manifest(`in`)
                }
            else -> Manifest()
        }
        manifest.mainAttributes.apply {
            Attributes.Name.MANIFEST_VERSION.let { key ->
                if (!contains(key)) put(key, "1.0")
            }
        }
        manifestFile.parent?.createDirectories()
        manifestFile.outputStream().use { out ->
            manifest.write(out)
        }

        targetFile.parent?.createDirectories()
        val permissions = PosixFilePermissions.asFileAttribute(
            setOf(
                PosixFilePermission.OWNER_READ,
                PosixFilePermission.OWNER_WRITE
            )
        )
        try {
            Files.createFile(targetFile, permissions)
        } catch (_: UnsupportedOperationException) {
        } catch (ex: java.nio.file.FileAlreadyExistsException) {
            targetFile.setPosixFilePermissions(permissions.value())
        }
        p.trace("write", targetFile.absolutePathString())
        targetFile.outputStream().use { out ->
            ZipOutputStream(out).use { zout ->
                zout.writeFileTree(tmp)
                zout.finish()
            }
        }

        cleanup(tmp)

        return metaData
    }

    companion object {
        private const val KOTLIN_VERSION = "2.1.0"
        private const val KOTLIN_GROUP_ID = "org.jetbrains.kotlin"

        private const val KOTLIN_SCRIPT_VERSION = "$KOTLIN_VERSION.26"

        private val kotlinStdlibDependency = Dependency(
            groupId = KOTLIN_GROUP_ID,
            artifactId = "kotlin-stdlib",
            version = KOTLIN_VERSION,
            sha256 = "d6f91b7b0f306cca299fec74fb7c34e4874d6f5ec5b925a0b4de21901e119c3f",
            size = 1690048
        )

        private val compilerClassPath = listOf(
            // BEGIN_COMPILER_CLASS_PATH
            kotlinStdlibDependency,
            Dependency(
                groupId = KOTLIN_GROUP_ID,
                artifactId = "kotlin-compiler-embeddable",
                version = KOTLIN_VERSION,
                sha256 = "c1b139a6f251c3b99e92befa326cb75d93a001d74c3ac601155a8cdb0d253783",
                size = 58801389
            ),
            Dependency(
                groupId = "org.jetbrains.kotlin",
                artifactId = "kotlin-reflect",
                version = "1.6.10",
                sha256 = "3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203",
                size = 3038560
            ),
            Dependency(
                groupId = "org.jetbrains",
                artifactId = "annotations",
                version = "13.0",
                sha256 = "ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478",
                size = 17536
            ),
            Dependency(
                groupId = "org.jetbrains.kotlinx",
                artifactId = "kotlinx-coroutines-core-jvm",
                version = "1.6.4",
                sha256 = "c24c8bb27bb320c4a93871501a7e5e0c61607638907b197aef675513d4c820be",
                size = 1476653
            ),
            Dependency(
                groupId = "org.jetbrains.intellij.deps",
                artifactId = "trove4j",
                version = "1.0.20200330",
                sha256 = "c5fd725bffab51846bf3c77db1383c60aaaebfe1b7fe2f00d23fe1b7df0a439d",
                size = 572985
            ),
            Dependency(
                groupId = KOTLIN_GROUP_ID,
                artifactId = "kotlin-script-runtime",
                version = KOTLIN_VERSION,
                sha256 = "15a2b82119e9f145ea028029bd31166584648a419157c20948c124fa33d40e50",
                size = 43363
            ),
            Dependency(
                groupId = KOTLIN_GROUP_ID,
                artifactId = "kotlin-daemon-embeddable",
                version = KOTLIN_VERSION,
                sha256 = "6aa581bd53c3500e380e4bb6b2407f6d233910012f425349c2ed5a8ddbe29eac",
                size = 346166
            ),
            // END_COMPILER_CLASS_PATH
        )

        private val userHome = Paths.get(System.getProperty("user.home")
            ?: error("user.home system property not set"))

        private val supportedJavaVersions = listOf(
            "1.8",
            *(9 .. 22).map(Int::toString).toTypedArray()
        )

        private const val KOTLIN_COMPILER_MAIN =
                "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler"

        private const val MANIFEST_PATH = "META-INF/MANIFEST.MF"

        private fun cleanup(dir: Path) {
            if (!dir.exists()) {
                return
            }
            try {
                Files.walkFileTree(dir, object : SimpleFileVisitor() {
                    override fun postVisitDirectory(dir: Path, exc: IOException?) =
                            FileVisitResult.CONTINUE.also { Files.delete(dir) }

                    override fun visitFile(file: Path, attrs: BasicFileAttributes) =
                            FileVisitResult.CONTINUE.also { Files.delete(file) }
                })
            } catch (ex: Throwable) {
                System.err.println("warning: exception on cleanup: $ex")
            }
        }

        private fun findJavaHome(): Path {
            val javaHome = Path(System.getProperty("java.home"))
            return javaHome.parent?.takeIf { p ->
                // detekt jdk
                javaHome.endsWith("jre") && (p / "bin" / "java").isExecutable()
            } ?: javaHome
        }

        @JvmStatic
        @JvmName("compileScript")
        internal fun compileScript(
            scriptFile: Path,
            scriptData: ByteArray,
            scriptFileSha256: String,
            scriptMetadata: Path
        ): Path {
            val flags = System.getProperty("kotlin_script.flags") ?: ""
            val script = Script(scriptFile, "sha256=$scriptFileSha256", scriptData)
            val kotlinScript = KotlinScript(
                progress = "-P" in flags,
                trace = "-x" in flags,
                force = "-f" in flags,
            )
            val metaData = kotlinScript.compile(script)
            metaData.storeToFile(scriptMetadata)
            return kotlinScript.jarCachePath(metaData)
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy