-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Scala sh rewrite #13081
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
Scala sh rewrite #13081
Changes from 6 commits
4b2ac53
2c46c3e
630579b
216b70b
72c279f
0c340bc
df72930
12eeb9f
f3a72f2
314c3c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package dotty.tools | ||
|
||
|
||
import scala.annotation.tailrec | ||
import scala.io.Source | ||
import scala.util.Try | ||
import java.net.URLClassLoader | ||
import sys.process._ | ||
import java.io.File | ||
import java.lang.Thread | ||
import scala.annotation.internal.sharable | ||
|
||
enum ExecuteMode: | ||
case Guess | ||
case Script | ||
case Repl | ||
case Run | ||
|
||
case class Settings( | ||
verbose: Boolean = false, | ||
classPath: List[String] = List.empty, | ||
executeMode: ExecuteMode = ExecuteMode.Guess, | ||
exitCode: Int = 0, | ||
javaArgs: List[String] = List.empty, | ||
scalaArgs: List[String] = List.empty, | ||
residualArgs: List[String] = List.empty, | ||
scriptArgs: List[String] = List.empty, | ||
targetScript: String = "", | ||
save: Boolean = false, | ||
modeShouldBeRun: Boolean = false, | ||
) { | ||
def withExecuteMode(em: ExecuteMode): Settings = this.executeMode match | ||
case ExecuteMode.Guess => | ||
this.copy(executeMode = em) | ||
case _ => | ||
println(s"execute_mode==[$executeMode], attempted overwrite by [$em]") | ||
this.copy(exitCode = 1) | ||
end withExecuteMode | ||
|
||
def withScalaArgs(args: String*): Settings = | ||
this.copy(scalaArgs = scalaArgs.appendedAll(args.toList)) | ||
|
||
def withJavaArgs(args: String*): Settings = | ||
this.copy(javaArgs = javaArgs.appendedAll(args.toList)) | ||
|
||
def withResidualArgs(args: String*): Settings = | ||
this.copy(residualArgs = residualArgs.appendedAll(args.toList)) | ||
|
||
def withScriptArgs(args: String*): Settings = | ||
this.copy(scriptArgs = scriptArgs.appendedAll(args.toList)) | ||
|
||
def withTargetScript(file: String): Settings = | ||
Try(Source.fromFile(file)).toOption match | ||
case Some(_) => this.copy(targetScript = file) | ||
case None => | ||
println(s"not found $file") | ||
this.copy(exitCode = 2) | ||
end withTargetScript | ||
|
||
def withSave: Settings = | ||
this.copy(save = true) | ||
|
||
def withModeShouldBeRun: Settings = | ||
this.copy(modeShouldBeRun = true) | ||
} | ||
|
||
object MainGenericRunner { | ||
|
||
val classpathSeparator = File.pathSeparator | ||
|
||
@sharable val javaOption = raw"""-J(.*)""".r | ||
|
||
@tailrec | ||
def process(args: List[String], settings: Settings): Settings = args match | ||
case Nil => | ||
settings | ||
case "-run" :: tail => | ||
process(tail, settings.withExecuteMode(ExecuteMode.Run)) | ||
case ("-cp" | "-classpath" | "--class-path") :: cp :: tail => | ||
process(tail, settings.copy(classPath = settings.classPath.appended(cp))) | ||
case ("-version" | "--version") :: _ => | ||
settings.copy( | ||
executeMode = ExecuteMode.Repl, | ||
residualArgs = List("-version") | ||
) | ||
case ("-v" | "-verbose" | "--verbose") :: tail => | ||
process( | ||
tail, | ||
settings.copy( | ||
verbose = true, | ||
residualArgs = settings.residualArgs :+ "-verbose" | ||
) | ||
) | ||
case "-save" :: tail => | ||
process(tail, settings.withSave) | ||
case (o @ javaOption(striped)) :: tail => | ||
process(tail, settings.withJavaArgs(striped).withScalaArgs(o)) | ||
case arg :: tail => | ||
val line = Try(Source.fromFile(arg).getLines.toList).toOption.flatMap(_.headOption) | ||
if arg.endsWith(".scala") || arg.endsWith(".sc") || (line.nonEmpty && raw"#!.*scala".r.matches(line.get)) then | ||
settings | ||
.withExecuteMode(ExecuteMode.Script) | ||
.withTargetScript(arg) | ||
.withScriptArgs(tail*) | ||
else | ||
val newSettings = if arg.startsWith("-") then settings else settings.withModeShouldBeRun | ||
process(tail, newSettings.withResidualArgs(arg)) | ||
|
||
def main(args: Array[String]): Unit = | ||
val settings = process(args.toList, Settings()) | ||
if settings.exitCode != 0 then System.exit(settings.exitCode) | ||
|
||
def run(mode: ExecuteMode): Unit = mode match | ||
case ExecuteMode.Repl => | ||
val properArgs = | ||
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty)) | ||
++ settings.residualArgs | ||
repl.Main.main(properArgs.toArray) | ||
case ExecuteMode.Run => | ||
val properArgs = | ||
val newClasspath = settings.classPath ++ getClasspath :+ "." | ||
List("-classpath", newClasspath.mkString(classpathSeparator)).filter(Function.const(newClasspath.nonEmpty)) | ||
++ settings.residualArgs | ||
s"java ${settings.javaArgs.mkString(" ")} ${properArgs.mkString(" ")}".! // For now we collect classpath that coursier provides for convenience | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, that's not what we want to do here: we don't want to fork another JVM - we want to launch the main in a new classloader constructed with the given classpath. In scala/scala that's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the advice. I couldn't find proper entry from scala sources and didn't think about launching the main in a new classloader. Will change |
||
case ExecuteMode.Script => | ||
val properArgs = | ||
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty)) | ||
++ settings.residualArgs | ||
++ (if settings.save then List("-save") else Nil) | ||
++ List("-script", settings.targetScript) | ||
++ settings.scalaArgs | ||
++ settings.scriptArgs | ||
scripting.Main.main(properArgs.toArray) | ||
case ExecuteMode.Guess => | ||
if settings.modeShouldBeRun then | ||
run(ExecuteMode.Run) | ||
else | ||
run(ExecuteMode.Repl) | ||
|
||
run(settings.executeMode) | ||
|
||
private def getClasspath(cl: ClassLoader): Array[String] = cl match | ||
case null => Array() | ||
case u: URLClassLoader => u.getURLs.map(_.toURI.toString) ++ getClasspath(cl.getParent) | ||
case cl if cl.getClass.getName == "jdk.internal.loader.ClassLoaders$AppClassLoader" => | ||
// Required with JDK >= 9 | ||
sys.props.getOrElse("java.class.path", "") | ||
.split(File.pathSeparator) | ||
.filter(_.nonEmpty) | ||
case _ => getClasspath(cl.getParent) | ||
|
||
private def getClasspath: List[String] = | ||
getClasspath(Thread.currentThread().getContextClassLoader).toList | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package dotty | ||
package tools | ||
package coursier | ||
|
||
import java.io.File | ||
import java.nio.file.{Path, Paths, Files} | ||
import scala.sys.process._ | ||
import org.junit.Test | ||
import org.junit.BeforeClass | ||
import org.junit.Assert._ | ||
import scala.collection.mutable.ListBuffer | ||
|
||
import java.net.URLClassLoader | ||
import java.net.URL | ||
|
||
class CoursierScalaTests: | ||
|
||
private def scripts(path: String): Array[File] = { | ||
val dir = new File(getClass.getResource(path).getPath) | ||
assert(dir.exists && dir.isDirectory, "Couldn't load scripts dir") | ||
dir.listFiles | ||
} | ||
|
||
extension (f: File) private def absPath = | ||
f.getAbsolutePath.replace('\\', '/') | ||
|
||
extension (str: String) private def dropExtension = | ||
str.reverse.dropWhile(_ != '.').drop(1).reverse | ||
|
||
// classpath tests are managed by scripting.ClasspathTests.scala | ||
def testFiles = scripts("/scripting").filter { ! _.getName.startsWith("classpath") } | ||
|
||
// Cannot run tests in parallel, more info here: https://stackoverflow.com/questions/6345660/java-executing-bash-script-error-26-text-file-busy | ||
@Test def allTests = | ||
def scriptArgs() = | ||
val scriptPath = scripts("/scripting").find(_.getName == "showArgs.sc").get.absPath | ||
val testScriptArgs = Seq("a", "b", "c", "-repl", "-run", "-script", "-debug") | ||
|
||
val args = scriptPath +: testScriptArgs | ||
val output = CoursierScalaTests.csCmd(args*) | ||
val expectedOutput = List( | ||
"arg 0:[a]", | ||
"arg 1:[b]", | ||
"arg 2:[c]", | ||
"arg 3:[-repl]", | ||
"arg 4:[-run]", | ||
"arg 5:[-script]", | ||
"arg 6:[-debug]", | ||
) | ||
for (line, expect) <- output zip expectedOutput do | ||
printf("expected: %-17s\nactual : %s\n", expect, line) | ||
assertEquals(expectedOutput, output) | ||
scriptArgs() | ||
|
||
def version() = | ||
val output = CoursierScalaTests.csCmd("-version") | ||
assertTrue(output.mkString("\n").contains(sys.env("DOTTY_BOOTSTRAPPED_VERSION"))) | ||
version() | ||
|
||
def emptyArgsEqualsRepl() = | ||
val output = CoursierScalaTests.csCmd() | ||
assertTrue(output.mkString("\n").contains("Unable to create a system terminal")) // Scala attempted to create REPL so we can assume it is working | ||
emptyArgsEqualsRepl() | ||
|
||
def run() = | ||
val output = CoursierScalaTests.csCmd("-run", "-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile") | ||
assertEquals(output.mkString("\n"), "Hello") | ||
run() | ||
|
||
def notOnlyOptionsEqualsRun() = | ||
val output = CoursierScalaTests.csCmd("-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile") | ||
assertEquals(output.mkString("\n"), "Hello") | ||
notOnlyOptionsEqualsRun() | ||
|
||
def help() = | ||
val output = CoursierScalaTests.csCmd("-help") | ||
assertTrue(output.mkString("\n").contains("Usage: scala <options> <source files>")) | ||
help() | ||
|
||
def jar() = | ||
val source = new File(getClass.getResource("/run/myfile.scala").getPath) | ||
val output = CoursierScalaTests.csCmd("-save", source.absPath) | ||
assertEquals(output.mkString("\n"), "Hello") | ||
assertTrue(source.getParentFile.listFiles.find(_.getName == "myfile.jar").isDefined) | ||
jar() | ||
|
||
object CoursierScalaTests: | ||
|
||
def execCmd(command: String, options: String*): List[String] = | ||
val cmd = (command :: options.toList).toSeq.mkString(" ") | ||
val out = new ListBuffer[String] | ||
cmd.!(ProcessLogger(out += _, out += _)) | ||
out.toList | ||
|
||
def csCmd(options: String*): List[String] = | ||
val newOptions = options match | ||
case Nil => options | ||
case _ => "--" +: options | ||
execCmd("./cs", (s"""launch "org.scala-lang:scala3-compiler_3:${sys.env("DOTTY_BOOTSTRAPPED_VERSION")}" --main-class "dotty.tools.MainGenericRunner" --property "scala.usejavacp=true"""" +: newOptions)*) | ||
|
||
/** Get coursier script */ | ||
@BeforeClass def setup(): Unit = | ||
val ver = execCmd("uname").head.replace('L', 'l').replace('D', 'd') | ||
execCmd("curl", s"-fLo cs https://git.io/coursier-cli-$ver") #&& execCmd("chmod", "+x cs") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package run | ||
|
||
object myfile extends App: | ||
println("Hello") |
Uh oh!
There was an error while loading. Please reload this page.