diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ClassPathTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ClassPathTest.kt index ad63a39ee83..ff7e425c8a1 100644 --- a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ClassPathTest.kt +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ClassPathTest.kt @@ -13,19 +13,50 @@ import java.io.FileOutputStream import java.net.URLClassLoader import java.util.jar.JarEntry import java.util.jar.JarOutputStream +import java.util.jar.Manifest +import kotlin.script.experimental.jvm.util.classPathFromTypicalResourceUrls import kotlin.script.experimental.jvm.util.classpathFromClassloader class ClassPathTest : TestCase() { + lateinit var tempDir: File + + override fun setUp() { + tempDir = createTempDir(ClassPathTest::class.simpleName!!) + super.setUp() + } + + override fun tearDown() { + super.tearDown() + tempDir.deleteRecursively() + } + @Test fun testExtractFromFat() { - val collection = createTempFile("col", ".jar").apply { createCollectionJar(emulatedCollectionFiles, "BOOT-INF") } + val collection = createTempFile("col", ".jar", directory = tempDir).apply { createCollectionJar(emulatedCollectionFiles, "BOOT-INF") } val cl = URLClassLoader(arrayOf(collection.toURI().toURL()), null) val cp = classpathFromClassloader(cl, true) Assert.assertTrue(cp != null && cp.isNotEmpty()) testUnpackedCollection(cp!!, emulatedCollectionFiles) } + + @Test + fun testDetectClasspathFromResources() { + val root1 = createTempDir("root1", directory = tempDir) + val jar = createTempFile("jar1", ".jar", directory = tempDir).apply { createJarWithManifest() } + val cl = URLClassLoader( + (emulatedClasspath.map { File(root1, it).apply { mkdirs() }.toURI().toURL() } + + jar.toURI().toURL()).toTypedArray(), + null + ) + val cp = cl.classPathFromTypicalResourceUrls().toList() + + Assert.assertTrue(cp.contains(jar.canonicalFile)) + for (el in emulatedClasspath) { + Assert.assertTrue(cp.contains(File(root1, el).canonicalFile)) + } + } } private val emulatedCollectionFiles = arrayOf( @@ -33,6 +64,11 @@ private val emulatedCollectionFiles = arrayOf( "lib/c-d.jar" ) +private val emulatedClasspath = arrayOf( + "module1/classes/kotlin/main/", + "module2/classes/java/test/" +) + fun File.createCollectionJar(fileNames: Array, infDirName: String) { FileOutputStream(this).use { fileStream -> val jarStream = JarOutputStream(fileStream) @@ -59,4 +95,11 @@ fun testUnpackedCollection(classpath: List, fileNames: Array) { Assert.assertTrue(cpClasses.size == 1) classes.checkFiles(cpClasses.first().parentFile) jars.checkFiles(cpJars.first().parentFile.parentFile) -} \ No newline at end of file +} + +fun File.createJarWithManifest() { + FileOutputStream(this).use { fileStream -> + val jarStream = JarOutputStream(fileStream, Manifest()) + jarStream.finish() + } +} diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/pathUtil.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/pathUtil.kt index 61d83be0490..998f3993ad2 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/pathUtil.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/pathUtil.kt @@ -100,7 +100,7 @@ internal fun URL.toFileOrNull() = else File(file).canonicalFile } -internal fun URL.toContainingFileOrNull(): File? = +internal fun URL.toContainingJarOrNull(): File? = if (protocol == "jar") { (openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull() - } else toFileOrNull() + } else null diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt index 2612a3fe4c4..0fbb4bae629 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt @@ -8,11 +8,13 @@ package kotlin.script.experimental.jvm.util import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException +import java.net.JarURLConnection import java.net.URL import java.net.URLClassLoader import java.util.jar.JarInputStream import kotlin.reflect.KClass -import kotlin.script.experimental.jvm.impl.toContainingFileOrNull +import kotlin.script.experimental.jvm.impl.toContainingJarOrNull +import kotlin.script.experimental.jvm.impl.toFileOrNull import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClass import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClassByName import kotlin.script.templates.standard.ScriptTemplateWithArgs @@ -67,7 +69,7 @@ fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollectio // if cache dir is specified, find all jar collections (spring boot fat jars and WARs so far, and unpack it accordingly val jarCollections = JAR_COLLECTIONS_KEY_PATHS.asSequence().flatMap { currentClassLoader.getResources(it).asSequence() } .mapNotNull { - it.toContainingFileOrNull()?.takeIf { file -> + it.toContainingJarOrNull()?.takeIf { file -> // additionally mark/check processed collection jars since unpacking is expensive file.extension in validJarCollectionFilesExtensions && processedJars.add(file) } @@ -80,7 +82,7 @@ fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollectio } else -> { classLoader.classPathFromGetUrlsMethodOrNull() - ?: classLoader.classPathFromManifestResourceUrls() + ?: classLoader.classPathFromTypicalResourceUrls() } } classPath @@ -88,7 +90,8 @@ fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollectio .toList().takeIf { it.isNotEmpty() } } -internal fun URL.toValidClasspathFileOrNull(): File? = toContainingFileOrNull()?.takeIf { it.isValidClasspathFile() } +internal fun URL.toValidClasspathFileOrNull(): File? = + (toContainingJarOrNull() ?: toFileOrNull())?.takeIf { it.isValidClasspathFile() } internal fun File.isValidClasspathFile(): Boolean = isDirectory || (isFile && extension in validClasspathFilesExtensions) @@ -105,8 +108,31 @@ private fun ClassLoader.classPathFromGetUrlsMethodOrNull(): Sequence? { } } -internal fun ClassLoader.classPathFromManifestResourceUrls(): Sequence = - getResources(JAR_MANIFEST_RESOURCE_NAME).asSequence().distinct().mapNotNull { it.toValidClasspathFileOrNull() } +internal fun ClassLoader.rawClassPathFromKeyResourcePath(keyResourcePath: String): Sequence { + var keyResourcePathDepth = -1 + return getResources(keyResourcePath).asSequence().mapNotNull { url -> + if (url.protocol == "jar") { + (url.openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull() + } else url.toFileOrNull()?.let { file -> + if (keyResourcePathDepth < 0) { + keyResourcePathDepth = keyResourcePath.trim('/').count { it == '/' } + } + var root = file + for (i in 1..keyResourcePathDepth) { + root = root.parentFile + } + root + } + } +} + +fun ClassLoader.classPathFromTypicalResourceUrls(): Sequence = +// roots without manifest cases are detected in some test scenarios +// manifests without containing directory entries are detected in some optimized jars, e.g. after proguard +// TODO: investigate whether getting resources with empty name works in all situations + (rawClassPathFromKeyResourcePath("") + rawClassPathFromKeyResourcePath(JAR_MANIFEST_RESOURCE_NAME)) + .distinct() + .filter { it.isValidClasspathFile() } private fun File.unpackJarCollection(rootTempDir: File?): Sequence { val targetDir = createTempDir(nameWithoutExtension, directory = rootTempDir)