Skip to content

Commit 411b9f0

Browse files
authored
Merge pull request #13562 from philwalk/argument-file-fix-13552
insert compiler libraries head of user-specified classpath - fix for 13552
2 parents ee8d2ab + c9f21b8 commit 411b9f0

File tree

11 files changed

+120
-132
lines changed

11 files changed

+120
-132
lines changed

compiler/src/dotty/tools/MainGenericRunner.scala

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import java.util.jar._
1616
import java.util.jar.Attributes.Name
1717
import dotty.tools.io.Jar
1818
import dotty.tools.runner.ScalaClassLoader
19+
import java.nio.file.{Files, Paths, Path}
20+
import scala.collection.JavaConverters._
21+
import dotty.tools.dotc.config.CommandLineParser
1922

2023
enum ExecuteMode:
2124
case Guess
@@ -102,7 +105,18 @@ object MainGenericRunner {
102105
case "-run" :: fqName :: tail =>
103106
process(tail, settings.withExecuteMode(ExecuteMode.Run).withTargetToRun(fqName))
104107
case ("-cp" | "-classpath" | "--class-path") :: cp :: tail =>
105-
process(tail, settings.copy(classPath = settings.classPath.appended(cp)))
108+
val globdir = cp.replaceAll("[\\/][^\\/]*$", "") // slash/backslash agnostic
109+
val (tailargs, cpstr) = if globdir.nonEmpty && classpathSeparator != ";" || cp.contains(classpathSeparator) then
110+
(tail, cp)
111+
else
112+
// combine globbed classpath entries into a classpath
113+
val jarfiles = cp :: tail
114+
val cpfiles = jarfiles.takeWhile( f => f.startsWith(globdir) && ((f.toLowerCase.endsWith(".jar") || f.endsWith(".zip"))) )
115+
val tailargs = jarfiles.drop(cpfiles.size)
116+
(tailargs, cpfiles.mkString(classpathSeparator))
117+
118+
process(tailargs, settings.copy(classPath = settings.classPath ++ cpstr.split(classpathSeparator).filter(_.nonEmpty)))
119+
106120
case ("-version" | "--version") :: _ =>
107121
settings.copy(
108122
executeMode = ExecuteMode.Repl,
@@ -123,7 +137,8 @@ object MainGenericRunner {
123137
case (o @ javaOption(striped)) :: tail =>
124138
process(tail, settings.withJavaArgs(striped).withScalaArgs(o))
125139
case (o @ scalaOption(_*)) :: tail =>
126-
process(tail, settings.withScalaArgs(o))
140+
val remainingArgs = (CommandLineParser.expandArg(o) ++ tail).toList
141+
process(remainingArgs, settings)
127142
case (o @ colorOption(_*)) :: tail =>
128143
process(tail, settings.withScalaArgs(o))
129144
case arg :: tail =>
@@ -143,6 +158,13 @@ object MainGenericRunner {
143158
val settings = process(allArgs.toList, Settings())
144159
if settings.exitCode != 0 then System.exit(settings.exitCode)
145160

161+
def removeCompiler(cp: Array[String]) =
162+
if (!settings.compiler) then // Let's remove compiler from the classpath
163+
val compilerLibs = Seq("scala3-compiler", "scala3-interfaces", "tasty-core", "scala-asm", "scala3-staging", "scala3-tasty-inspector")
164+
cp.filterNot(c => compilerLibs.exists(c.contains))
165+
else
166+
cp
167+
146168
def run(settings: Settings): Unit = settings.executeMode match
147169
case ExecuteMode.Repl =>
148170
val properArgs =
@@ -151,7 +173,7 @@ object MainGenericRunner {
151173
repl.Main.main(properArgs.toArray)
152174

153175
case ExecuteMode.PossibleRun =>
154-
val newClasspath = (settings.classPath :+ ".").map(File(_).toURI.toURL)
176+
val newClasspath = (settings.classPath :+ ".").flatMap(_.split(classpathSeparator).filter(_.nonEmpty)).map(File(_).toURI.toURL)
155177
import dotty.tools.runner.RichClassLoader._
156178
val newClassLoader = ScalaClassLoader.fromURLsParallelCapable(newClasspath)
157179
val targetToRun = settings.possibleEntryPaths.to(LazyList).find { entryPath =>
@@ -166,15 +188,7 @@ object MainGenericRunner {
166188
run(settings.withExecuteMode(ExecuteMode.Repl))
167189
case ExecuteMode.Run =>
168190
val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
169-
170-
def removeCompiler(cp: Array[String]) =
171-
if (!settings.compiler) then // Let's remove compiler from the classpath
172-
val compilerLibs = Seq("scala3-compiler", "scala3-interfaces", "tasty-core", "scala-asm", "scala3-staging", "scala3-tasty-inspector")
173-
cp.filterNot(c => compilerLibs.exists(c.contains))
174-
else
175-
cp
176-
val newClasspath = (settings.classPath ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
177-
191+
val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
178192
val res = ObjectRunner.runAndCatch(newClasspath, settings.targetToRun, settings.residualArgs).flatMap {
179193
case ex: ClassNotFoundException if ex.getMessage == settings.targetToRun =>
180194
val file = settings.targetToRun
@@ -187,14 +201,30 @@ object MainGenericRunner {
187201
}
188202
errorFn("", res)
189203
case ExecuteMode.Script =>
190-
val properArgs =
191-
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty))
192-
++ settings.residualArgs
193-
++ (if settings.save then List("-save") else Nil)
194-
++ List("-script", settings.targetScript)
195-
++ settings.scalaArgs
196-
++ settings.scriptArgs
197-
scripting.Main.main(properArgs.toArray)
204+
val targetScript = Paths.get(settings.targetScript).toFile
205+
val targetJar = settings.targetScript.replaceAll("[.][^\\/]*$", "")+".jar"
206+
val precompiledJar = Paths.get(targetJar).toFile
207+
def mainClass = Jar(targetJar).mainClass.getOrElse("") // throws exception if file not found
208+
val jarIsValid = precompiledJar.isFile && mainClass.nonEmpty && precompiledJar.lastModified >= targetScript.lastModified
209+
if jarIsValid then
210+
// precompiledJar exists, is newer than targetScript, and manifest defines a mainClass
211+
sys.props("script.path") = targetScript.toPath.toAbsolutePath.normalize.toString
212+
val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
213+
val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
214+
val mc = mainClass
215+
if mc.nonEmpty then
216+
ObjectRunner.runAndCatch(newClasspath :+ File(targetJar).toURI.toURL, mc, settings.scriptArgs)
217+
else
218+
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $precompiledJar"))
219+
else
220+
val properArgs =
221+
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty))
222+
++ settings.residualArgs
223+
++ (if settings.save then List("-save") else Nil)
224+
++ settings.scalaArgs
225+
++ List("-script", settings.targetScript)
226+
++ settings.scriptArgs
227+
scripting.Main.main(properArgs.toArray)
198228
case ExecuteMode.Guess =>
199229
if settings.modeShouldBePossibleRun then
200230
run(settings.withExecuteMode(ExecuteMode.PossibleRun))

compiler/src/dotty/tools/dotc/config/CliCommand.scala

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import Settings._
77
import core.Contexts._
88
import Properties._
99

10-
import scala.PartialFunction.cond
11-
import scala.collection.JavaConverters._
10+
import scala.PartialFunction.cond
1211

1312
trait CliCommand:
1413

@@ -42,24 +41,10 @@ trait CliCommand:
4241

4342
/** Distill arguments into summary detailing settings, errors and files to main */
4443
def distill(args: Array[String], sg: Settings.SettingGroup)(ss: SettingsState = sg.defaultState)(using Context): ArgsSummary =
45-
/**
46-
* Expands all arguments starting with @ to the contents of the
47-
* file named like each argument.
48-
*/
49-
def expandArg(arg: String): List[String] =
50-
def stripComment(s: String) = s takeWhile (_ != '#')
51-
val path = Paths.get(arg stripPrefix "@")
52-
if (!Files.exists(path))
53-
report.error(s"Argument file ${path.getFileName} could not be found")
54-
Nil
55-
else
56-
val lines = Files.readAllLines(path) // default to UTF-8 encoding
57-
val params = lines.asScala map stripComment mkString " "
58-
CommandLineParser.tokenize(params)
5944

6045
// expand out @filename to the contents of that filename
6146
def expandedArguments = args.toList flatMap {
62-
case x if x startsWith "@" => expandArg(x)
47+
case x if x startsWith "@" => CommandLineParser.expandArg(x)
6348
case x => List(x)
6449
}
6550

compiler/src/dotty/tools/dotc/config/CommandLineParser.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package dotty.tools.dotc.config
33
import scala.annotation.tailrec
44
import scala.collection.mutable.ArrayBuffer
55
import java.lang.Character.isWhitespace
6+
import java.nio.file.{Files, Paths}
7+
import scala.collection.JavaConverters._
68

79
/** A simple enough command line parser.
810
*/
@@ -93,4 +95,19 @@ object CommandLineParser:
9395

9496
def tokenize(line: String): List[String] = tokenize(line, x => throw new ParseException(x))
9597

98+
/**
99+
* Expands all arguments starting with @ to the contents of the
100+
* file named like each argument.
101+
*/
102+
def expandArg(arg: String): List[String] =
103+
def stripComment(s: String) = s takeWhile (_ != '#')
104+
val path = Paths.get(arg stripPrefix "@")
105+
if (!Files.exists(path))
106+
System.err.println(s"Argument file ${path.getFileName} could not be found")
107+
Nil
108+
else
109+
val lines = Files.readAllLines(path) // default to UTF-8 encoding
110+
val params = lines.asScala map stripComment mkString " "
111+
tokenize(params)
112+
96113
class ParseException(msg: String) extends RuntimeException(msg)

compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ class CoursierScalaTests:
122122
assertTrue(output.mkString("\n").contains("Unable to create a system terminal")) // Scala attempted to create REPL so we can assume it is working
123123
replWithArgs()
124124

125+
def argumentFile() =
126+
// verify that an arguments file is accepted
127+
// verify that setting a user classpath does not remove compiler libraries from the classpath.
128+
// arguments file contains "-classpath .", adding current directory to classpath.
129+
val source = new File(getClass.getResource("/run/myfile.scala").getPath)
130+
val argsFile = new File(getClass.getResource("/run/myargs.txt").getPath)
131+
val output = CoursierScalaTests.csScalaCmd(s"@$argsFile", source.absPath)
132+
assertEquals(output.mkString("\n"), "Hello")
133+
argumentFile()
134+
125135
object CoursierScalaTests:
126136

127137
def execCmd(command: String, options: String*): List[String] =

compiler/test-coursier/run/myargs.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-classpath .
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!dist/target/pack/bin/scala @compiler/test-resources/scripting/cpArgumentsFile.txt
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+
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
#!dist/target/pack/bin/scala -classpath 'dist/target/pack/lib/*'
1+
#!dist/target/pack/bin/scala -classpath dist/target/pack/lib/*
22

33
import java.nio.file.Paths
44

55
def main(args: Array[String]): Unit =
6-
val cwd = Paths.get(".").toAbsolutePath.toString.replace('\\', '/').replaceAll("/$", "")
6+
val cwd = Paths.get(".").toAbsolutePath.normalize.toString.norm
77
printf("cwd: %s\n", cwd)
8-
printf("classpath: %s\n", sys.props("java.class.path"))
8+
printf("classpath: %s\n", sys.props("java.class.path").norm)
9+
10+
extension(s: String)
11+
def norm: String = s.replace('\\', '/')
912

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-classpath dist/target/pack/lib/*

compiler/test-resources/scripting/scriptPath.sc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env scala
1+
#!dist/target/pack/bin/scala
22

33
def main(args: Array[String]): Unit =
44
args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) }
@@ -17,3 +17,6 @@
1717
System.err.printf("sun.java.command: %s\n", sys.props("sun.java.command"))
1818
System.err.printf("first 5 PATH entries:\n%s\n",pathEntries.take(5).mkString("\n"))
1919
}
20+
21+
extension(s: String)
22+
def norm: String = s.replace('\\', '/')

compiler/test/dotty/tools/scripting/BashScriptsTests.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class BashScriptsTests:
5858

5959
/* verify `dist/bin/scala` non-interference with command line args following script name */
6060
@Test def verifyScalaArgs =
61-
val commandline = (Seq(scalaPath, showArgsScript) ++ testScriptArgs).mkString(" ")
61+
val commandline = (Seq("SCALA_OPTS= ", scalaPath, showArgsScript) ++ testScriptArgs).mkString(" ")
6262
val (validTest, exitCode, stdout, stderr) = bashCommand(commandline)
6363
if validTest then
6464
var fail = false
@@ -78,7 +78,7 @@ class BashScriptsTests:
7878
*/
7979
@Test def verifyScriptPathProperty =
8080
val scriptFile = testFiles.find(_.getName == "scriptPath.sc").get
81-
val expected = s"/${scriptFile.getName}"
81+
val expected = s"${scriptFile.getName}"
8282
printf("===> verify valid system property script.path is reported by script [%s]\n", scriptFile.getName)
8383
printf("calling scriptFile: %s\n", scriptFile)
8484
val (validTest, exitCode, stdout, stderr) = bashCommand(scriptFile.absPath)
@@ -100,11 +100,12 @@ class BashScriptsTests:
100100
if validTest then
101101
val expected = s"${workingDirectory.toString}"
102102
val List(line1: String, line2: String) = stdout.take(2)
103+
printf("line1 [%s]\n", line1)
103104
val valid = line2.dropWhile( _ != ' ').trim.startsWith(expected)
104105
if valid then printf(s"\n===> success: classpath begins with %s, as reported by [%s]\n", workingDirectory, scriptFile.getName)
105106
assert(valid, s"script ${scriptFile.absPath} did not report valid java.class.path first entry")
106107

107-
def existingPath: String = envOrElse("PATH","").norm
108+
def existingPath: String = envOrElse("PATH", "").norm
108109
def adjustedPath = s"$javaHome/bin$psep$scalaHome/bin$psep$existingPath"
109110
def pathEntries = adjustedPath.split(psep).toList
110111

@@ -114,12 +115,12 @@ class BashScriptsTests:
114115
val path = Files.createTempFile("scriptingTest", ".args")
115116
val text = s"-classpath ${workingDirectory.absPath}"
116117
Files.write(path, text.getBytes(utfCharset))
117-
path.toFile.getAbsolutePath.replace('\\', '/')
118+
path.toFile.getAbsolutePath.norm
118119

119120
def fixHome(s: String): String =
120121
s.startsWith("~") match {
121122
case false => s
122-
case true => s.replaceFirst("~",userHome)
123+
case true => s.replaceFirst("~", userHome)
123124
}
124125

125126
extension(s: String) {
@@ -145,7 +146,7 @@ class BashScriptsTests:
145146
def absPath: String = f.getAbsolutePath.norm
146147
}
147148

148-
lazy val psep: String = propOrElse("path.separator","")
149+
lazy val psep: String = propOrElse("path.separator", "")
149150
lazy val osname = propOrElse("os.name", "").toLowerCase
150151

151152
lazy val scalacPath = s"$workingDirectory/dist/target/pack/bin/scalac".norm
@@ -162,7 +163,7 @@ class BashScriptsTests:
162163
// else, SCALA_HOME if defined
163164
// else, not defined
164165
lazy val scalaHome =
165-
if scalacPath.isFile then scalacPath.replaceAll("/bin/scalac","")
166+
if scalacPath.isFile then scalacPath.replaceAll("/bin/scalac", "")
166167
else envOrElse("SCALA_HOME", "").norm
167168

168169
lazy val javaHome = envOrElse("JAVA_HOME", "").norm
@@ -171,7 +172,7 @@ class BashScriptsTests:
171172
("JAVA_HOME", javaHome),
172173
("SCALA_HOME", scalaHome),
173174
("PATH", adjustedPath),
174-
).filter { case (name,valu) => valu.nonEmpty }
175+
).filter { case (name, valu) => valu.nonEmpty }
175176

176177
lazy val whichBash: String =
177178
var whichBash = ""
@@ -182,7 +183,7 @@ class BashScriptsTests:
182183

183184
whichBash
184185

185-
def bashCommand(cmdstr: String, additionalEnvPairs:List[(String, String)] = Nil): (Boolean, Int, Seq[String], Seq[String]) = {
186+
def bashCommand(cmdstr: String, additionalEnvPairs: List[(String, String)] = Nil): (Boolean, Int, Seq[String], Seq[String]) = {
186187
var (stdout, stderr) = (List.empty[String], List.empty[String])
187188
if bashExe.toFile.exists then
188189
val cmd = Seq(bashExe, "-c", cmdstr)

0 commit comments

Comments
 (0)