Skip to content

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

Merged
merged 10 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:

- name: Cmd Tests
run: |
./project/scripts/sbt ";scala3-bootstrapped/compile ;scala3-bootstrapped/test;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test;sjsCompilerTests/test ;sbt-test/scripted scala2-compat/* ;configureIDE ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test"
./project/scripts/sbt ";scala3-bootstrapped/compile; scala3-bootstrapped/test;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test;sjsCompilerTests/test ;sbt-test/scripted scala2-compat/* ;configureIDE ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test; scala3-compiler-bootstrapped/scala3CompilerCoursierTest:test"
./project/scripts/bootstrapCmdTests

- name: MiMa
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,9 @@ community-build/dotty-community-build-deps

# Bloop
.bsp

# Coursier
cs

# Coursier test product
compiler/test-coursier/run/myfile.jar
154 changes: 154 additions & 0 deletions compiler/src/dotty/tools/MainGenericRunner.scala
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
Copy link
Member

Choose a reason for hiding this comment

The 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 ObjectRunner.runAndCatch if you want to mirror what's being done there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/scripting/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package dotty.tools.scripting

import java.io.File
import java.nio.file.{Path, Paths}
import dotty.tools.dotc.config.Properties.isWin
import dotty.tools.dotc.config.Properties.isWin

/** Main entry point to the Scripting execution engine */
object Main:
Expand Down Expand Up @@ -87,7 +87,7 @@ object Main:
case s if s.startsWith("./") => s.drop(2)
case s => s
}

// convert to absolute path relative to cwd.
def absPath: String = norm match
case str if str.isAbsolute => norm
Expand Down
105 changes: 105 additions & 0 deletions compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala
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")

4 changes: 4 additions & 0 deletions compiler/test-coursier/run/myfile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package run

object myfile extends App:
println("Hello")
6 changes: 6 additions & 0 deletions compiler/test/dotty/tools/utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def scripts(path: String): Array[File] = {
dir.listFiles
}

extension (f: File) def absPath =
f.getAbsolutePath.replace('\\', '/')

extension (str: String) def dropExtension =
str.reverse.dropWhile(_ != '.').drop(1).reverse

private def withFile[T](file: File)(action: Source => T): T =
resource(Source.fromFile(file, UTF_8.name))(action)

Expand Down
Loading