Skip to content

Commit f7b5594

Browse files
authored
Merge pull request #11633 from philwalk/scripting-classpath-wildcard-fix-10761
fix for #10761 ; expand wildcard classpath entries
2 parents b7d2a12 + cb64423 commit f7b5594

File tree

9 files changed

+366
-46
lines changed

9 files changed

+366
-46
lines changed

compiler/src/dotty/tools/dotc/core/MacroClassLoader.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dotty.tools.dotc.core
33
import dotty.tools.dotc.core.Contexts._
44
import dotty.tools.dotc.util.Property
55
import dotty.tools.dotc.reporting.trace
6+
import dotty.tools.io.ClassPath
67

78
import scala.collection.mutable
89

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

2223
private def makeMacroClassLoader(using Context): ClassLoader = trace("new macro class loader") {
23-
val urls = ctx.settings.classpath.value.split(java.io.File.pathSeparatorChar).map(cp => java.nio.file.Paths.get(cp).toUri.toURL)
24+
val entries = ClassPath.expandPath(ctx.settings.classpath.value, expandStar=true)
25+
val urls = entries.map(cp => java.nio.file.Paths.get(cp).toUri.toURL).toArray
2426
val out = ctx.settings.outputDir.value.jpath.toUri.toURL // to find classes in case of suspended compilation
2527
new java.net.URLClassLoader(urls :+ out, getClass.getClassLoader)
2628
}

compiler/src/dotty/tools/io/ClassPath.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ object ClassPath {
132132
dir.list.filter(x => filt(x.name) && (x.isDirectory || isJarOrZip(x))).map(_.path).toList
133133

134134
if (pattern == "*") lsDir(Directory("."))
135-
else if (pattern.endsWith(wildSuffix)) lsDir(Directory(pattern dropRight 2))
135+
// On Windows the JDK supports forward slash or backslash in classpath entries
136+
else if (pattern.endsWith(wildSuffix) || pattern.endsWith("/*")) lsDir(Directory(pattern dropRight 2))
136137
else if (pattern.contains('*')) {
137138
try {
138139
val regexp = ("^" + pattern.replace("""\*""", """.*""") + "$").r

compiler/src/dotty/tools/scripting/Main.scala

100644100755
+8-9
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ object Main:
1010
All arguments afterwards are script arguments.*/
1111
private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean, Boolean) =
1212
val (leftArgs, rest) = args.splitAt(args.indexOf("-script"))
13-
assert(rest.size >= 2,s"internal error: rest == Array(${rest.mkString(",")})")
13+
assert(rest.size >= 2, s"internal error: rest == Array(${rest.mkString(",")})")
1414

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

4951
private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String],
50-
classpath:String, mainClassName: String): Unit =
51-
52-
val javaClasspath = sys.props("java.class.path")
53-
val runtimeClasspath = s"${classpath}$pathsep$javaClasspath"
52+
classpathEntries:Seq[Path], mainClassName: String): Unit =
5453

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

63-
val cpPaths = runtimeClasspath.split(pathsep).map(_.toUrl)
62+
val cpPaths = classpathEntries.map { _.toString.toUrl }
6463

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

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

compiler/src/dotty/tools/scripting/ScriptingDriver.scala

100644100755
+12-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package dotty.tools.scripting
22

3-
import java.nio.file.{ Files, Path }
3+
import java.nio.file.{ Files, Paths, Path }
44
import java.io.File
55
import java.net.{ URL, URLClassLoader }
66
import java.lang.reflect.{ Modifier, Method }
@@ -10,14 +10,14 @@ import scala.jdk.CollectionConverters._
1010
import dotty.tools.dotc.{ Driver, Compiler }
1111
import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx }
1212
import dotty.tools.dotc.config.CompilerCommand
13-
import dotty.tools.io.{ PlainDirectory, Directory }
13+
import dotty.tools.io.{ PlainDirectory, Directory, ClassPath }
1414
import dotty.tools.dotc.reporting.Reporter
1515
import dotty.tools.dotc.config.Settings.Setting._
1616

1717
import sys.process._
1818

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

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

55-
private def detectMainClassAndMethod(outDir: Path, classpath: String,
57+
private def detectMainClassAndMethod(outDir: Path, classpathEntries: Seq[Path],
5658
scriptFile: File): (String, Method) =
57-
val outDirURL = outDir.toUri.toURL
58-
val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL)
59-
val cl = URLClassLoader(classpathUrls :+ outDirURL)
59+
60+
val classpathUrls = (classpathEntries :+ outDir).map { _.toUri.toURL }
61+
62+
val cl = URLClassLoader(classpathUrls.toArray)
6063

6164
def collectMainMethods(target: File, path: String): List[(String, Method)] =
6265
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!dist/target/pack/bin/scala -classpath 'dist/target/pack/lib/*'
2+
3+
import java.nio.file.Paths
4+
5+
def main(args: Array[String]): Unit =
6+
val cwd = Paths.get(".").toAbsolutePath.toString.replace('\\', '/').replaceAll("/$", "")
7+
printf("cwd: %s\n", cwd)
8+
printf("classpath: %s\n", sys.props("java.class.path"))
9+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package dotty.tools.io
2+
3+
import org.junit.Test
4+
5+
import java.io.File
6+
import dotty.tools.io.AbstractFile
7+
import java.nio.file.{Files, Paths}
8+
import java.nio.file.StandardCopyOption._
9+
import java.nio.file.attribute.PosixFilePermissions
10+
import dotty.tools.io.{ PlainDirectory, Directory, ClassPath }
11+
12+
class ClasspathTest {
13+
14+
def pathsep = sys.props("path.separator")
15+
16+
//
17+
// Cope with wildcard classpath entries, exercised with -classpath <cp>
18+
//
19+
// Verify that Windows users not forced to use backslash in classpath.
20+
//
21+
@Test def testWildcards(): Unit =
22+
val outDir = Files.createTempDirectory("classpath-test")
23+
try
24+
val compilerLib = "dist/target/pack/lib"
25+
val libdir = Paths.get(compilerLib).toFile
26+
if libdir.exists then
27+
val libjarFiles = libdir.listFiles.toList.take(5)
28+
try
29+
for src <- libjarFiles do
30+
val dest = Paths.get(s"$outDir/${src.getName}")
31+
printf("copy: %s\n", Files.copy(src.toPath, dest))
32+
33+
val cp = Seq(s"$outDir/*", "not-a-real-directory/*").mkString(pathsep).replace('\\', '/')
34+
35+
val libjars = libjarFiles.map { _.getName }.toSet
36+
37+
// expand wildcard classpath entries, ignoring invalid entries
38+
val entries = ClassPath.expandPath(cp).map { Paths.get(_).toFile.getName }
39+
40+
// require one-to-one matches
41+
assert(libjars == entries.toSet)
42+
43+
printf("%d entries\n", entries.size)
44+
printf("%d libjars\n", libjars.size)
45+
46+
for entry <- libjars do
47+
printf("libdir[%s]\n", entry)
48+
49+
for entry <- entries do
50+
printf("expand[%s]\n", entry)
51+
52+
// verify that expanded classpath has expected jar names
53+
for jar <- libjars do
54+
assert(entries.contains(jar))
55+
56+
catch
57+
case _:NullPointerException => // no test if unable to copy jars to outDir
58+
59+
60+
finally
61+
deleteFile(outDir.toFile)
62+
63+
64+
private def deleteFile(target: File): Unit =
65+
if target.isDirectory then
66+
for member <- target.listFiles.toList
67+
do deleteFile(member)
68+
target.delete()
69+
end deleteFile
70+
}

0 commit comments

Comments
 (0)