Skip to content

Commit f1f8b7b

Browse files
committed
refactor CliCommand.expandArg method; fix script.path regression, Windows classpath regression
1 parent 29c20d0 commit f1f8b7b

File tree

12 files changed

+106
-130
lines changed

12 files changed

+106
-130
lines changed

compiler/src/dotty/tools/MainGenericRunner.scala

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ object MainGenericRunner {
126126
case (o @ javaOption(striped)) :: tail =>
127127
process(tail, settings.withJavaArgs(striped).withScalaArgs(o))
128128
case (o @ scalaOption(_*)) :: tail =>
129-
val remainingArgs = (expandArg(o) ++ tail).toList
129+
val remainingArgs = (CommandLineParser.expandArg(o) ++ tail).toList
130130
process(remainingArgs, settings)
131131
case (o @ colorOption(_*)) :: tail =>
132132
process(tail, settings.withScalaArgs(o))
@@ -141,19 +141,6 @@ object MainGenericRunner {
141141
val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun
142142
process(tail, newSettings.withResidualArgs(arg))
143143

144-
// copy of method private to dotty.tools.dotc.config.CliCommand.distill()
145-
// TODO: make it available as a public method and remove this copy?
146-
def expandArg(arg: String): List[String] =
147-
def stripComment(s: String) = s takeWhile (_ != '#')
148-
val path = Paths.get(arg stripPrefix "@")
149-
if (!Files.exists(path))
150-
System.err.println(s"Argument file ${path.getFileName} could not be found")
151-
Nil
152-
else
153-
val lines = Files.readAllLines(path) // default to UTF-8 encoding
154-
val params = lines.asScala map stripComment mkString " "
155-
CommandLineParser.tokenize(params)
156-
157144
def main(args: Array[String]): Unit =
158145
val scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" "))
159146
val allArgs = scalaOpts ++ args
@@ -204,6 +191,8 @@ object MainGenericRunner {
204191
}
205192
errorFn("", res)
206193
case ExecuteMode.Script =>
194+
val targetScriptPath: String = settings.targetScript.toString.replace('\\', '/')
195+
System.setProperty("script.path", targetScriptPath)
207196
val properArgs =
208197
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty))
209198
++ settings.residualArgs

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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ 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+
125134
object CoursierScalaTests:
126135

127136
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/*
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
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) }
55
val path = Option(sys.props("script.path")) match {
66
case None => printf("no script.path property is defined\n")
77
case Some(path) =>
8-
printf("script.path: %s\n",path)
8+
printf("script.path: %s\n",path.norm)
99
assert(path.endsWith("scriptPath.sc"),s"actual path [$path]")
1010
}
11+
12+
extension(s: String)
13+
def norm: String = s.replace('\\', '/')

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ class BashScriptsTests:
8484
@Test def verifyScriptPathProperty =
8585
val scriptFile = testFiles.find(_.getName == "scriptPath.sc").get
8686
val expected = s"/${scriptFile.getName}"
87-
printf("===> verify valid system property script.path is reported by script [%s]\n", scriptFile.getName)
87+
System.err.printf("===> verify valid system property script.path is reported by script [%s]\n", scriptFile.getName)
8888
val (exitCode, stdout, stderr) = bashCommand(scriptFile.absPath)
8989
if exitCode == 0 && ! stderr.exists(_.contains("Permission denied")) then
9090
// var cmd = Array(bashExe, "-c", scriptFile.absPath)
9191
// val stdout = Process(cmd).lazyLines_!
92-
stdout.foreach { printf("######### [%s]\n", _) }
92+
stdout.foreach { System.err.printf("######### [%s]\n", _) }
9393
val valid = stdout.exists { _.endsWith(expected) }
9494
if valid then printf("# valid script.path reported by [%s]\n", scriptFile.getName)
9595
assert(valid, s"script ${scriptFile.absPath} did not report valid script.path value")
@@ -99,35 +99,39 @@ class BashScriptsTests:
9999
*/
100100
@Test def verifyScalaOpts =
101101
val scriptFile = testFiles.find(_.getName == "classpathReport.sc").get
102-
printf("===> verify valid system property script.path is reported by script [%s]\n", scriptFile.getName)
102+
printf("===> verify SCALA_OPTS -classpath setting in argument file seen by script [%s]\n", scriptFile.getName)
103103
val argsfile = createArgsFile() // avoid problems caused by drive letter
104104
val envPairs = List(("SCALA_OPTS", s"@$argsfile"))
105105
val (exitCode, stdout, stderr) = bashCommand(scriptFile.absPath, envPairs:_*)
106+
printf("\n")
106107
if exitCode != 0 || stderr.exists(_.contains("Permission denied")) then
107108
stderr.foreach { System.err.printf("stderr [%s]\n", _) }
108109
printf("unable to execute script, return value is %d\n", exitCode)
109110
else
110-
// val stdout: Seq[String] = Process(cmd, cwd, envPairs:_*).lazyLines_!.toList
111-
val expected = s"${cwd.toString}"
111+
val expected = cwd
112112
val List(line1: String, line2: String) = stdout.take(2)
113+
printf("line1 [%s]\n", line1)
113114
val valid = line2.dropWhile( _ != ' ').trim.startsWith(expected)
115+
val psep = if osname.startsWith("Windows") then ';' else ':'
116+
printf("line2 start [%s]\n", line2.take(100))
114117
if valid then printf(s"\n===> success: classpath begins with %s, as reported by [%s]\n", cwd, scriptFile.getName)
115-
assert(valid, s"script ${scriptFile.absPath} did not report valid java.class.path first entry")
118+
assert(valid, s"script ${scriptFile.getName} did not report valid java.class.path first entry")
116119

117-
lazy val cwd = Paths.get(dotty.tools.dotc.config.Properties.userDir).toFile
120+
lazy val cwd: String = Paths.get(".").toAbsolutePath.normalize.toString.norm
118121

119122
def createArgsFile(): String =
120123
val utfCharset = java.nio.charset.StandardCharsets.UTF_8.name
121-
val text = s"-classpath ${cwd.absPath}"
124+
val text = s"-classpath $cwd"
122125
val path = Files.createTempFile("scriptingTest", ".args")
123126
Files.write(path, text.getBytes(utfCharset))
124-
path.toFile.getAbsolutePath.replace('\\', '/')
127+
path.toFile.getAbsolutePath.norm
125128

126-
extension (str: String) def dropExtension: String =
127-
str.reverse.dropWhile(_ != '.').drop(1).reverse
129+
extension(str: String)
130+
def norm: String = str.replace('\\', '/')
131+
def dropExtension: String = str.reverse.dropWhile(_ != '.').drop(1).reverse
128132

129133
extension(f: File) def absPath: String =
130-
f.getAbsolutePath.replace('\\', '/')
134+
f.getAbsolutePath.norm
131135

132136
lazy val osname = Option(sys.props("os.name")).getOrElse("").toLowerCase
133137

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

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -13,104 +13,29 @@ import scala.sys.process._
1313
import scala.jdk.CollectionConverters._
1414
import dotty.tools.dotc.config.Properties._
1515

16-
/** Runs all tests contained in `compiler/test-resources/scripting/` */
16+
/** Test java command line generated by bin/scala and bin/scalac */
1717
class ClasspathTests:
1818
val packBinDir = "dist/target/pack/bin"
19-
val scalaCopy = makeTestableScriptCopy("scala")
20-
val scalacCopy = makeTestableScriptCopy("scalac")
21-
val commonCopy = makeTestableScriptCopy("common")
19+
val packLibDir = "dist/target/pack/lib"
2220

2321
// only interested in classpath test scripts
24-
def testFiles = scripts("/scripting").filter { _.getName.matches("classpath.*[.]sc") }
2522
val testScriptName = "classpathReport.sc"
26-
def testScript = testFiles.find { _.getName == testScriptName } match
23+
val testScript = scripts("/scripting").find { _.getName.matches(testScriptName) } match
2724
case None => sys.error(s"test script not found: ${testScriptName}")
2825
case Some(file) => file
2926

30-
def getScriptPath(scriptName: String): Path = Paths.get(s"$packBinDir/$scriptName")
31-
3227
def exists(scriptPath: Path): Boolean = Files.exists(scriptPath)
3328
def packBinScalaExists:Boolean = exists(Paths.get(s"$packBinDir/scala"))
3429

35-
// create edited copy of [dist/bin/scala] and [dist/bin/scalac] for scalacEchoTest
36-
def makeTestableScriptCopy(scriptName: String): Path =
37-
val scriptPath: Path = getScriptPath(scriptName)
38-
val scriptCopy: Path = getScriptPath(s"$scriptName-copy")
39-
if Files.exists(scriptPath) then
40-
val lines = Files.readAllLines(scriptPath).asScala.map {
41-
_.replaceAll("/scalac", "/scalac-copy").
42-
replaceAll("/common", "/common-copy").
43-
replaceFirst("^ *eval(.*JAVACMD.*)", "echo $1")
44-
}
45-
val bytes = (lines.mkString("\n")+"\n").getBytes
46-
Files.write(scriptCopy, bytes)
47-
48-
scriptCopy
49-
50-
/*
51-
* verify java command line generated by scalac.
52-
*/
53-
@Test def scalacEchoTest =
54-
val relpath = testScript.toPath.relpath.norm
55-
printf("===> scalacEchoTest for script [%s]\n", relpath)
56-
printf("bash is [%s]\n", bashExe)
57-
58-
if packBinScalaExists then
59-
val bashCmdline = s"SCALA_OPTS= ${scalaCopy.norm} -classpath '$wildcardEntry' $relpath"
60-
61-
// ask [dist/bin/scalac] to echo generated command line so we can verify some things
62-
val cmd = Array(bashExe, "-c", bashCmdline)
63-
64-
//cmd.foreach { printf("[%s]\n", _) }
65-
66-
val javaCommandLine = exec(cmd:_*).mkString(" ").split(" ").filter { _.trim.nonEmpty }
67-
printf("\n==================== isWin[%s], cygwin[%s], mingw[%s], msys[%s]\n", isWin, cygwin, mingw, msys)
68-
javaCommandLine.foreach { printf("java-command[%s]\n", _) }
69-
70-
val output = scala.collection.mutable.Queue(javaCommandLine:_*)
71-
output.dequeueWhile( _ != "dotty.tools.scripting.Main")
72-
73-
def consumeNext = if output.isEmpty then "" else output.dequeue()
74-
75-
// assert that we found "dotty.tools.scripting.Main"
76-
val str = consumeNext
77-
if str != "dotty.tools.scripting.Main" then
78-
79-
assert(str == "dotty.tools.scripting.Main", s"found [$str]")
80-
val mainArgs = output.copyToArray(Array.ofDim[String](output.length))
81-
82-
// display command line starting with "dotty.tools.scripting.Main"
83-
output.foreach { line =>
84-
printf("%s\n", line)
85-
}
86-
87-
// expecting -classpath next
88-
assert(consumeNext.replaceAll("'", "") == "-classpath")
89-
90-
// 2nd arg to scripting.Main is 'lib/*', with semicolon added if Windows jdk
91-
92-
// PR #10761: verify that [dist/bin/scala] -classpath processing adds $psep to wildcard if Windows
93-
val classpathValue = consumeNext
94-
printf("classpath value [%s]\n", classpathValue)
95-
assert( !winshell || classpathValue.contains(psep) )
96-
97-
// expecting -script next
98-
assert(consumeNext.replaceAll("'", "") == "-script")
99-
100-
// PR #10761: verify that Windows jdk did not expand single wildcard classpath to multiple file paths
101-
if javaCommandLine.last != relpath then
102-
printf("last: %s\nrelp: %s\n", javaCommandLine.last, relpath)
103-
assert(javaCommandLine.last == relpath, s"unexpected output passed to scripting.Main")
104-
10530
/*
10631
* verify classpath reported by called script.
10732
*/
108-
@Test def hashbangClasspathVerifyTest =
33+
@Test def hashbangClasspathVerifyTest = {
10934
val relpath = testScript.toPath.relpath.norm
11035
printf("===> hashbangClasspathVerifyTest for script [%s]\n", relpath)
11136
printf("bash is [%s]\n", bashExe)
11237

113-
if false && packBinScalaExists then
38+
if packBinScalaExists then
11439
val bashCmdline = s"SCALA_OPTS= $relpath"
11540
val cmd = Array(bashExe, "-c", bashCmdline)
11641

@@ -123,14 +48,15 @@ class ClasspathTests:
12348
val scriptCp = findTaggedLine("classpath", scriptOutput)
12449

12550
val hashbangClasspathJars = scriptCp.split(psep).map { _.getName }.sorted.distinct
126-
val packlibJars = listJars(s"$scriptCwd/dist/target/pack/lib").sorted.distinct
51+
val packlibJars = listJars(s"$scriptCwd/$packLibDir").sorted.distinct
12752

12853
// verify that the classpath set in the hashbang line is effective
12954
if hashbangClasspathJars.size != packlibJars.size then
13055
printf("%d test script jars in classpath\n", hashbangClasspathJars.size)
13156
printf("%d jar files in dist/target/pack/lib\n", packlibJars.size)
13257

13358
assert(hashbangClasspathJars.size == packlibJars.size)
59+
}
13460

13561

13662
//////////////// end of tests ////////////////

dist/bin/scala

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,39 @@ fi
2828

2929
source "$PROG_HOME/bin/common"
3030

31+
case `uname` in
32+
CYG*|MINGW*|MSYS*) PSEP=';' ;;
33+
*) PSEP=':' ;;
34+
esac
35+
36+
ARGS=()
37+
while [ $# -gt 0 ]; do
38+
case "$1" in
39+
-cp | -classpath) # partial fix for #10761
40+
# passed as two arguments, e.g. '-classpath' 'lib/*'
41+
ARGS+=($1)
42+
ARGS+=("$2${PSEP}")
43+
shift
44+
shift
45+
;;
46+
-cp*|-classpath*) # partial fix for #10761
47+
# passed as a single argument, e.g. '-classpath lib/*'
48+
# (hashbang line can glom args together)
49+
ARGS+=('-classpath')
50+
ARGS+=("\"${1#* *}${PSEP}\"")
51+
shift
52+
;;
53+
*)
54+
ARGS+=($1)
55+
shift
56+
;;
57+
esac
58+
done
59+
3160
# exec here would prevent onExit from being called, leaving terminal in unusable state
3261
compilerJavaClasspathArgs
3362
[ -z "${ConEmuPID-}" -o -n "${cygwin-}" ] && export MSYSTEM= PWD= # workaround for #12405
34-
eval "\"$JAVACMD\"" "-classpath \"$jvm_cp_args\"" "dotty.tools.MainGenericRunner" "-classpath \"$jvm_cp_args\"" "$@"
63+
eval "\"$JAVACMD\"" "-classpath \"$jvm_cp_args\"" "dotty.tools.MainGenericRunner" "-classpath \"$jvm_cp_args\"" "${ARGS[@]}"
3564
scala_exit_status=$?
3665

3766

0 commit comments

Comments
 (0)