Skip to content

fix for #10761 ; expand wildcard classpath entries #11633

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

Merged
merged 15 commits into from
Apr 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/core/MacroClassLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dotty.tools.dotc.core
import dotty.tools.dotc.core.Contexts._
import dotty.tools.dotc.util.Property
import dotty.tools.dotc.reporting.trace
import dotty.tools.io.ClassPath

import scala.collection.mutable

Expand All @@ -20,7 +21,8 @@ object MacroClassLoader {
ctx.setProperty(MacroClassLoaderKey, makeMacroClassLoader(using ctx))

private def makeMacroClassLoader(using Context): ClassLoader = trace("new macro class loader") {
val urls = ctx.settings.classpath.value.split(java.io.File.pathSeparatorChar).map(cp => java.nio.file.Paths.get(cp).toUri.toURL)
val entries = ClassPath.expandPath(ctx.settings.classpath.value, expandStar=true)
val urls = entries.map(cp => java.nio.file.Paths.get(cp).toUri.toURL).toArray
val out = ctx.settings.outputDir.value.jpath.toUri.toURL // to find classes in case of suspended compilation
new java.net.URLClassLoader(urls :+ out, getClass.getClassLoader)
}
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/io/ClassPath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ object ClassPath {
dir.list.filter(x => filt(x.name) && (x.isDirectory || isJarOrZip(x))).map(_.path).toList

if (pattern == "*") lsDir(Directory("."))
else if (pattern.endsWith(wildSuffix)) lsDir(Directory(pattern dropRight 2))
// On Windows the JDK supports forward slash or backslash in classpath entries
else if (pattern.endsWith(wildSuffix) || pattern.endsWith("/*")) lsDir(Directory(pattern dropRight 2))
else if (pattern.contains('*')) {
try {
val regexp = ("^" + pattern.replace("""\*""", """.*""") + "$").r
Expand Down
17 changes: 8 additions & 9 deletions compiler/src/dotty/tools/scripting/Main.scala
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ object Main:
All arguments afterwards are script arguments.*/
private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean, Boolean) =
val (leftArgs, rest) = args.splitAt(args.indexOf("-script"))
assert(rest.size >= 2,s"internal error: rest == Array(${rest.mkString(",")})")
assert(rest.size >= 2, s"internal error: rest == Array(${rest.mkString(",")})")

val file = File(rest(1))
val scriptArgs = rest.drop(2)
Expand All @@ -32,10 +32,12 @@ object Main:
def main(args: Array[String]): Unit =
val (compilerArgs, scriptFile, scriptArgs, saveJar, invokeFlag) = distinguishArgs(args)
val driver = ScriptingDriver(compilerArgs, scriptFile, scriptArgs)
try driver.compileAndRun { (outDir:Path, classpath:String, mainClass: String) =>
try driver.compileAndRun { (outDir:Path, classpathEntries:Seq[Path], mainClass: String) =>
// write expanded classpath to java.class.path property, so called script can see it
sys.props("java.class.path") = classpathEntries.map(_.toString).mkString(pathsep)
if saveJar then
// write a standalone jar to the script parent directory
writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClass)
writeJarfile(outDir, scriptFile, scriptArgs, classpathEntries, mainClass)
invokeFlag
}
catch
Expand All @@ -47,10 +49,7 @@ object Main:
throw e.getCause

private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String],
classpath:String, mainClassName: String): Unit =

val javaClasspath = sys.props("java.class.path")
val runtimeClasspath = s"${classpath}$pathsep$javaClasspath"
classpathEntries:Seq[Path], mainClassName: String): Unit =

val jarTargetDir: Path = Option(scriptFile.toPath.toAbsolutePath.getParent) match {
case None => sys.error(s"no parent directory for script file [$scriptFile]")
Expand All @@ -60,7 +59,7 @@ object Main:
def scriptBasename = scriptFile.getName.takeWhile(_!='.')
val jarPath = s"$jarTargetDir/$scriptBasename.jar"

val cpPaths = runtimeClasspath.split(pathsep).map(_.toUrl)
val cpPaths = classpathEntries.map { _.toString.toUrl }

import java.util.jar.Attributes.Name
val cpString:String = cpPaths.distinct.mkString(" ")
Expand Down Expand Up @@ -92,7 +91,7 @@ object Main:
// convert to absolute path relative to cwd.
def absPath: String = norm match
case str if str.isAbsolute => norm
case _ => Paths.get(userDir,norm).toString.norm
case _ => Paths.get(userDir, norm).toString.norm

def toUrl: String = Paths.get(absPath).toUri.toURL.toString

Expand Down
21 changes: 12 additions & 9 deletions compiler/src/dotty/tools/scripting/ScriptingDriver.scala
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dotty.tools.scripting

import java.nio.file.{ Files, Path }
import java.nio.file.{ Files, Paths, Path }
import java.io.File
import java.net.{ URL, URLClassLoader }
import java.lang.reflect.{ Modifier, Method }
Expand All @@ -10,14 +10,14 @@ import scala.jdk.CollectionConverters._
import dotty.tools.dotc.{ Driver, Compiler }
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx }
import dotty.tools.dotc.config.CompilerCommand
import dotty.tools.io.{ PlainDirectory, Directory }
import dotty.tools.io.{ PlainDirectory, Directory, ClassPath }
import dotty.tools.dotc.reporting.Reporter
import dotty.tools.dotc.config.Settings.Setting._

import sys.process._

class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
def compileAndRun(pack:(Path, String, String) => Boolean = null): Unit =
def compileAndRun(pack:(Path, Seq[Path], String) => Boolean = null): Unit =
val outDir = Files.createTempDirectory("scala3-scripting")
setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh) match
case Some((toCompile, rootCtx)) =>
Expand All @@ -28,11 +28,13 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
throw ScriptingException("Errors encountered during compilation")

try
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile)
val classpath = s"${ctx.settings.classpath.value}${pathsep}${sys.props("java.class.path")}"
val classpathEntries: Seq[Path] = ClassPath.expandPath(classpath, expandStar=true).map { Paths.get(_) }
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, classpathEntries, scriptFile)
val invokeMain: Boolean =
Option(pack) match
case Some(func) =>
func(outDir, ctx.settings.classpath.value, mainClass)
func(outDir, classpathEntries, mainClass)
case None =>
true
end match
Expand All @@ -52,11 +54,12 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
target.delete()
end deleteFile

private def detectMainClassAndMethod(outDir: Path, classpath: String,
private def detectMainClassAndMethod(outDir: Path, classpathEntries: Seq[Path],
scriptFile: File): (String, Method) =
val outDirURL = outDir.toUri.toURL
val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL)
val cl = URLClassLoader(classpathUrls :+ outDirURL)

val classpathUrls = (classpathEntries :+ outDir).map { _.toUri.toURL }

val cl = URLClassLoader(classpathUrls.toArray)

def collectMainMethods(target: File, path: String): List[(String, Method)] =
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
Expand Down
9 changes: 9 additions & 0 deletions compiler/test-resources/scripting/classpathReport.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!dist/target/pack/bin/scala -classpath 'dist/target/pack/lib/*'

import java.nio.file.Paths

def main(args: Array[String]): Unit =
val cwd = Paths.get(".").toAbsolutePath.toString.replace('\\', '/').replaceAll("/$", "")
printf("cwd: %s\n", cwd)
printf("classpath: %s\n", sys.props("java.class.path"))

70 changes: 70 additions & 0 deletions compiler/test/dotty/tools/io/ClasspathTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dotty.tools.io

import org.junit.Test

import java.io.File
import dotty.tools.io.AbstractFile
import java.nio.file.{Files, Paths}
import java.nio.file.StandardCopyOption._
import java.nio.file.attribute.PosixFilePermissions
import dotty.tools.io.{ PlainDirectory, Directory, ClassPath }

class ClasspathTest {

def pathsep = sys.props("path.separator")

//
// Cope with wildcard classpath entries, exercised with -classpath <cp>
//
// Verify that Windows users not forced to use backslash in classpath.
//
@Test def testWildcards(): Unit =
val outDir = Files.createTempDirectory("classpath-test")
try
val compilerLib = "dist/target/pack/lib"
val libdir = Paths.get(compilerLib).toFile
if libdir.exists then
val libjarFiles = libdir.listFiles.toList.take(5)
try
for src <- libjarFiles do
val dest = Paths.get(s"$outDir/${src.getName}")
printf("copy: %s\n", Files.copy(src.toPath, dest))

val cp = Seq(s"$outDir/*", "not-a-real-directory/*").mkString(pathsep).replace('\\', '/')

val libjars = libjarFiles.map { _.getName }.toSet

// expand wildcard classpath entries, ignoring invalid entries
val entries = ClassPath.expandPath(cp).map { Paths.get(_).toFile.getName }

// require one-to-one matches
assert(libjars == entries.toSet)

printf("%d entries\n", entries.size)
printf("%d libjars\n", libjars.size)

for entry <- libjars do
printf("libdir[%s]\n", entry)

for entry <- entries do
printf("expand[%s]\n", entry)

// verify that expanded classpath has expected jar names
for jar <- libjars do
assert(entries.contains(jar))

catch
case _:NullPointerException => // no test if unable to copy jars to outDir


finally
deleteFile(outDir.toFile)


private def deleteFile(target: File): Unit =
if target.isDirectory then
for member <- target.listFiles.toList
do deleteFile(member)
target.delete()
end deleteFile
}
Loading