diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f5216a1f9dc..8f0d96f8052e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.gitignore b/.gitignore index 0ba8e15fd3f3..20675f9397f0 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,9 @@ community-build/dotty-community-build-deps # Bloop .bsp + +# Coursier +cs + +# Coursier test product +compiler/test-coursier/run/myfile.jar diff --git a/compiler/src/dotty/tools/MainGenericRunner.scala b/compiler/src/dotty/tools/MainGenericRunner.scala new file mode 100644 index 000000000000..eac96c799310 --- /dev/null +++ b/compiler/src/dotty/tools/MainGenericRunner.scala @@ -0,0 +1,156 @@ +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 +import dotty.tools.dotc.util.ClasspathFromClassloader +import dotty.tools.runner.ObjectRunner +import dotty.tools.dotc.config.Properties.envOrNone + +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 + @sharable val scalaOption = raw"""@.*""".r + @sharable val colorOption = raw"""-color:.*""".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 (o @ scalaOption(_*)) :: tail => + process(tail, settings.withScalaArgs(o)) + case (o @ colorOption(_*)) :: tail => + process(tail, settings.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 scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" ")) + val allArgs = scalaOpts ++ args + val settings = process(allArgs.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 scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator) + val newClasspath = (settings.classPath ++ scalaClasspath :+ ".").map(File(_).toURI.toURL) + errorFn("", ObjectRunner.runAndCatch(newClasspath, settings.residualArgs.head, settings.residualArgs.drop(1))) + 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) + + + def errorFn(str: String, e: Option[Throwable] = None, isFailure: Boolean = true): Boolean = { + if (str.nonEmpty) Console.err.println(str) + e.foreach(_.printStackTrace()) + !isFailure + } +} diff --git a/compiler/src/dotty/tools/runner/ObjectRunner.scala b/compiler/src/dotty/tools/runner/ObjectRunner.scala new file mode 100644 index 000000000000..112b55f2e464 --- /dev/null +++ b/compiler/src/dotty/tools/runner/ObjectRunner.scala @@ -0,0 +1,48 @@ +package dotty.tools +package runner + +import java.net.URL +import scala.util.control.NonFatal +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.UndeclaredThrowableException +import java.util.concurrent.ExecutionException + +/** + * This is a copy implementation from scala/scala scala.tools.nsc.CommonRunner trait + */ +trait CommonRunner { + /** Run a given object, specified by name, using a + * specified classpath and argument list. + * + * @throws java.lang.ClassNotFoundException + * @throws java.lang.NoSuchMethodException + * @throws java.lang.reflect.InvocationTargetException + */ + def run(urls: Seq[URL], objectName: String, arguments: Seq[String]): Unit = { + import RichClassLoader._ + ScalaClassLoader.fromURLsParallelCapable(urls).run(objectName, arguments) + } + + /** Catches any non-fatal exception thrown by run (in the case of InvocationTargetException, + * unwrapping it) and returns it in an Option. + */ + def runAndCatch(urls: Seq[URL], objectName: String, arguments: Seq[String]): Option[Throwable] = + try { run(urls, objectName, arguments) ; None } + catch { case NonFatal(e) => Some(rootCause(e)) } + + private def rootCause(x: Throwable): Throwable = x match { + case _: InvocationTargetException | + _: ExceptionInInitializerError | + _: UndeclaredThrowableException | + _: ExecutionException + if x.getCause != null => + rootCause(x.getCause) + case _ => x + } +} + +/** An object that runs another object specified by name. + * + * @author Lex Spoon + */ +object ObjectRunner extends CommonRunner diff --git a/compiler/src/dotty/tools/runner/ScalaClassLoader.scala b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala new file mode 100644 index 000000000000..71c332a48846 --- /dev/null +++ b/compiler/src/dotty/tools/runner/ScalaClassLoader.scala @@ -0,0 +1,242 @@ +package dotty.tools +package runner + +import java.lang.invoke.{MethodHandles, MethodType} + +import scala.language.implicitConversions +import java.lang.{ClassLoader => JClassLoader} +import java.lang.reflect.Modifier +import java.net.{URLClassLoader => JURLClassLoader} +import java.net.URL + +import scala.annotation.tailrec +import scala.util.control.Exception.catching +import scala.reflect.{ClassTag, classTag} +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.UndeclaredThrowableException +import dotty.tools.repl.AbstractFileClassLoader +import dotty.tools.io.AbstractFile +import dotty.tools.io.Streamable +import scala.annotation.internal.sharable + +trait HasClassPath { + def classPathURLs: Seq[URL] +} + +final class RichClassLoader(private val self: JClassLoader) extends AnyVal { + /** Executing an action with this classloader as context classloader */ + def asContext[T](action: => T): T = { + val saved = Thread.currentThread.getContextClassLoader + try { ScalaClassLoader.setContext(self) ; action } + finally ScalaClassLoader.setContext(saved) + } + + /** Load and link a class with this classloader */ + def tryToLoadClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = false) + /** Load, link and initialize a class with this classloader */ + def tryToInitializeClass[T <: AnyRef](path: String): Option[Class[T]] = tryClass(path, initialize = true) + + private def tryClass[T <: AnyRef](path: String, initialize: Boolean): Option[Class[T]] = + catching(classOf[ClassNotFoundException], classOf[SecurityException]) opt + Class.forName(path, initialize, self).asInstanceOf[Class[T]] + + /** Create an instance of a class with this classloader */ + def create(path: String): AnyRef = + tryToInitializeClass[AnyRef](path).map(_.getConstructor().newInstance()).orNull + + /** Create an instance with ctor args, or invoke errorFn before throwing. */ + def create[T <: AnyRef : ClassTag](path: String, errorFn: String => Unit)(args: AnyRef*): T = { + def fail(msg: String) = error(msg, new IllegalArgumentException(msg)) + def error(msg: String, e: Throwable) = { errorFn(msg) ; throw e } + try { + val clazz = Class.forName(path, /*initialize =*/ true, /*loader =*/ self) + if (classTag[T].runtimeClass isAssignableFrom clazz) { + val ctor = { + val maybes = clazz.getConstructors filter (c => c.getParameterCount == args.size && + (c.getParameterTypes zip args).forall { case (k, a) => k isAssignableFrom a.getClass }) + if (maybes.size == 1) maybes.head + else fail(s"Constructor must accept arg list (${args map (_.getClass.getName) mkString ", "}): ${path}") + } + (ctor.newInstance(args: _*)).asInstanceOf[T] + } else { + errorFn(s"""Loader for ${classTag[T]}: [${show(classTag[T].runtimeClass.getClassLoader)}] + |Loader for ${clazz.getName}: [${show(clazz.getClassLoader)}]""".stripMargin) + fail(s"Not a ${classTag[T]}: ${path}") + } + } catch { + case e: ClassNotFoundException => + error(s"Class not found: ${path}", e) + case e @ (_: LinkageError | _: ReflectiveOperationException) => + error(s"Unable to create instance: ${path}: ${e.toString}", e) + } + } + + /** The actual bytes for a class file, or an empty array if it can't be found. */ + def classBytes(className: String): Array[Byte] = classAsStream(className) match { + case null => Array() + case stream => Streamable.bytes(stream) + } + + /** An InputStream representing the given class name, or null if not found. */ + def classAsStream(className: String) = self.getResourceAsStream { + if (className endsWith ".class") className + else s"${className.replace('.', '/')}.class" // classNameToPath + } + + /** Run the main method of a class to be loaded by this classloader */ + def run(objectName: String, arguments: Seq[String]): Unit = { + val clsToRun = tryToInitializeClass(objectName) getOrElse ( + throw new ClassNotFoundException(objectName) + ) + val method = clsToRun.getMethod("main", classOf[Array[String]]) + if (!Modifier.isStatic(method.getModifiers)) + throw new NoSuchMethodException(objectName + ".main is not static") + + try asContext(method.invoke(null, Array(arguments.toArray: AnyRef): _*)) // !!! : AnyRef shouldn't be necessary + catch unwrapHandler({ case ex => throw ex }) + } + + @tailrec + def unwrapThrowable(x: Throwable): Throwable = x match { + case _: InvocationTargetException | // thrown by reflectively invoked method or constructor + _: ExceptionInInitializerError | // thrown when running a static initializer (e.g. a scala module constructor) + _: UndeclaredThrowableException | // invocation on a proxy instance if its invocation handler's `invoke` throws an exception + _: ClassNotFoundException | // no definition for a class instantiated by name + _: NoClassDefFoundError // the definition existed when the executing class was compiled, but can no longer be found + if x.getCause != null => + unwrapThrowable(x.getCause) + case _ => x + } + // Transforms an exception handler into one which will only receive the unwrapped + // exceptions (for the values of wrap covered in unwrapThrowable.) + def unwrapHandler[T](pf: PartialFunction[Throwable, T]): PartialFunction[Throwable, T] = + pf.compose({ case ex => unwrapThrowable(ex) }) + + def show(cl: ClassLoader): String = { + import scala.reflect.Selectable.reflectiveSelectable + + @tailrec + def isAbstractFileClassLoader(clazz: Class[_]): Boolean = { + if (clazz == null) return false + if (clazz == classOf[AbstractFileClassLoader]) return true + isAbstractFileClassLoader(clazz.getSuperclass) + } + def inferClasspath(cl: ClassLoader): String = cl match { + case cl: java.net.URLClassLoader if cl.getURLs != null => + (cl.getURLs mkString ",") + case cl if cl != null && isAbstractFileClassLoader(cl.getClass) => + cl.asInstanceOf[{val root: AbstractFile}].root.canonicalPath + case null => + val loadBootCp = (flavor: String) => scala.util.Properties.propOrNone(flavor + ".boot.class.path") + loadBootCp("sun") orElse loadBootCp("java") getOrElse "" + case _ => + "" + } + cl match { + case null => s"primordial classloader with boot classpath [${inferClasspath(cl)}]" + case _ => s"$cl of type ${cl.getClass} with classpath [${inferClasspath(cl)}] and parent being ${show(cl.getParent)}" + } + } +} + +object RichClassLoader { + implicit def wrapClassLoader(loader: ClassLoader): RichClassLoader = new RichClassLoader(loader) +} + +/** A wrapper around java.lang.ClassLoader to lower the annoyance + * of java reflection. + */ +trait ScalaClassLoader extends JClassLoader { + private def wrap = new RichClassLoader(this) + /** Executing an action with this classloader as context classloader */ + def asContext[T](action: => T): T = wrap.asContext(action) + + /** Load and link a class with this classloader */ + def tryToLoadClass[T <: AnyRef](path: String): Option[Class[T]] = wrap.tryToLoadClass[T](path) + /** Load, link and initialize a class with this classloader */ + def tryToInitializeClass[T <: AnyRef](path: String): Option[Class[T]] = wrap.tryToInitializeClass(path) + + /** Create an instance of a class with this classloader */ + def create(path: String): AnyRef = wrap.create(path) + + /** Create an instance with ctor args, or invoke errorFn before throwing. */ + def create[T <: AnyRef : ClassTag](path: String, errorFn: String => Unit)(args: AnyRef*): T = + wrap.create[T](path, errorFn)(args: _*) + + /** The actual bytes for a class file, or an empty array if it can't be found. */ + def classBytes(className: String): Array[Byte] = wrap.classBytes(className) + + /** An InputStream representing the given class name, or null if not found. */ + def classAsStream(className: String) = wrap.classAsStream(className) + + /** Run the main method of a class to be loaded by this classloader */ + def run(objectName: String, arguments: Seq[String]): Unit = wrap.run(objectName, arguments) +} + + +/** Methods for obtaining various classloaders. + * appLoader: the application classloader. (Also called the java system classloader.) + * extLoader: the extension classloader. + * bootLoader: the boot classloader. + * contextLoader: the context classloader. + */ +object ScalaClassLoader { + /** Returns loaders which are already ScalaClassLoaders unaltered, + * and translates java.net.URLClassLoaders into scala URLClassLoaders. + * Otherwise creates a new wrapper. + */ + implicit def apply(cl: JClassLoader): ScalaClassLoader = cl match { + case cl: ScalaClassLoader => cl + case cl: JURLClassLoader => new URLClassLoader(cl.getURLs.toSeq, cl.getParent) + case _ => new JClassLoader(cl) with ScalaClassLoader + } + def contextLoader = apply(Thread.currentThread.getContextClassLoader) + def appLoader = apply(JClassLoader.getSystemClassLoader) + def setContext(cl: JClassLoader) = Thread.currentThread.setContextClassLoader(cl) + + class URLClassLoader(urls: Seq[URL], parent: JClassLoader) + extends JURLClassLoader(urls.toArray, parent) + with ScalaClassLoader + with HasClassPath { + private[this] var classloaderURLs: Seq[URL] = urls + def classPathURLs: Seq[URL] = classloaderURLs + + /** Override to widen to public */ + override def addURL(url: URL) = { + classloaderURLs :+= url + super.addURL(url) + } + override def close(): Unit = { + super.close() + classloaderURLs = null + } + } + + def fromURLs(urls: Seq[URL], parent: ClassLoader = null): URLClassLoader = { + new URLClassLoader(urls, if (parent == null) bootClassLoader else parent) + } + + def fromURLsParallelCapable(urls: Seq[URL], parent: ClassLoader = null): JURLClassLoader = { + new JURLClassLoader(urls.toArray, if (parent == null) bootClassLoader else parent) + } + + /** True if supplied class exists in supplied path */ + def classExists(urls: Seq[URL], name: String): Boolean = + (fromURLs(urls) tryToLoadClass name).isDefined + + /** Finding what jar a clazz or instance came from */ + def originOfClass(x: Class[_]): Option[URL] = + Option(x.getProtectionDomain.getCodeSource) flatMap (x => Option(x.getLocation)) + + @sharable private[this] val bootClassLoader: ClassLoader = { + if (!scala.util.Properties.isJavaAtLeast("9")) null + else { + try { + MethodHandles.lookup().findStatic(classOf[ClassLoader], "getPlatformClassLoader", MethodType.methodType(classOf[ClassLoader])).invoke().asInstanceOf[ClassLoader] + } catch { + case _: Throwable => + null + } + } + } +} diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index 97d0b91d4440..3a86155f1fe9 100755 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -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: @@ -13,6 +13,8 @@ object Main: assert(rest.size >= 2, s"internal error: rest == Array(${rest.mkString(",")})") val file = File(rest(1)) + // write script path to script.path property, so called script can see it + sys.props("script.path") = file.toPath.toAbsolutePath.toString val scriptArgs = rest.drop(2) var saveJar = false var invokeFlag = true // by default, script main method is invoked @@ -87,7 +89,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 diff --git a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala new file mode 100644 index 000000000000..e80f187321df --- /dev/null +++ b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala @@ -0,0 +1,113 @@ +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 scriptPath() = + val scriptPath = scripts("/scripting").find(_.getName == "scriptPath.sc").get.absPath + val args = scriptPath + val output = CoursierScalaTests.csCmd(args) + assertTrue(output.mkString("\n").startsWith("script.path:")) + assertTrue(output.mkString("\n").endsWith("scriptPath.sc")) + scriptPath() + + 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 ")) + 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") + diff --git a/compiler/test-coursier/run/myfile.scala b/compiler/test-coursier/run/myfile.scala new file mode 100644 index 000000000000..c9ed2cfb1683 --- /dev/null +++ b/compiler/test-coursier/run/myfile.scala @@ -0,0 +1,4 @@ +package run + +object myfile extends App: + println("Hello") diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala index 6d5677f0e8f8..720a9dffd80d 100644 --- a/compiler/test/dotty/tools/utils.scala +++ b/compiler/test/dotty/tools/utils.scala @@ -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) diff --git a/dist/bin/scala b/dist/bin/scala old mode 100644 new mode 100755 index 3c6720d9b94f..b1e3d4e66e3e --- a/dist/bin/scala +++ b/dist/bin/scala @@ -28,205 +28,11 @@ fi source "$PROG_HOME/bin/common" -# This script operates in one of 3 usage modes: -# script -# repl -# run -# execute_mode replaces mutually exclusive booleans: -# execute_repl=false -# execute_run=false -# execute_script=false -setExecuteMode () { - case "${execute_mode-}" in - "") execute_mode="$1" ; shift ;; - *) echo "execute_mode==[${execute_mode-}], attempted overwrite by [$1]" 1>&2 - exit 1 - ;; - esac -} +# exec here would prevent onExit from being called, leaving terminal in unusable state +compilerJavaClasspathArgs +[ -z "${ConEmuPID-}" -o -n "${cygwin-}" ] && export MSYSTEM= PWD= # workaround for #12405 +eval "\"$JAVACMD\"" "-classpath \"$jvm_cp_args\"" "dotty.tools.MainGenericRunner" "-classpath \"$jvm_cp_args\"" "$@" +scala_exit_status=$? -with_compiler=false # to add compiler jars to repl classpath -let class_path_count=0 || true # count classpath args, warning if more than 1 -save_compiled=false # to save compiled script jar in script directory -CLASS_PATH="" || true # scala classpath - -# Little hack to check if all arguments are options -all_params="$*" -truncated_params="${*#-}" -# options_indicator != 0 if at least one parameter is not an option -options_indicator=$(( ${#all_params} - ${#truncated_params} - $# )) - -[ -n "${SCALA_OPTS-}" ] && set -- $SCALA_OPTS "$@" - -while [[ $# -gt 0 ]]; do - case "$1" in - -repl) - setExecuteMode 'repl' - shift - ;; - -run) - setExecuteMode 'run' - shift - ;; - -cp | -classpath) - CLASS_PATH="$2${PSEP}" - let class_path_count+=1 - shift - shift - ;; - -cp*|-classpath*) # partial fix for #10761 - # hashbang can combine args, e.g. "-classpath 'lib/*'" - CLASS_PATH="${1#* *}${PSEP}" - let class_path_count+=1 - shift - ;; - -with-compiler) - with_compiler=true - shift - ;; - @*|-color:*) - addScala "${1}" - shift - ;; - -save|-savecompiled) - save_compiled=true - addScala "$1" - shift - ;; - -compile-only) - addScala "$1" - shift - ;; - -version) - # defer to scalac, then exit - shift - eval "\"$PROG_HOME/bin/scalac\" -version" - scala_exit_status=$? - onExit - ;; - -J*) - addJava "${1:2}" - addScala "${1}" - shift ;; - -v|-verbose) - verbose=true - addScala "-verbose" - shift ;; - -run) - setExecuteMode 'run' - shift ;; - - *) - # script if extension .scala or .sc, or if has scala hashbang line - # no -f test, issue meaningful error message (file not found) - if [[ "$1" == *.scala || "$1" == *.sc ]]; then - setExecuteMode 'script' # execute_script=true - - # -f test needed before we examine the hashbang line - elif [[ (-f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'`) ]]; then - setExecuteMode 'script' # execute_script=true - fi - - if [ "${execute_mode-}" == 'script' ]; then - target_script="$1" - shift - if [ ! -f $target_script ]; then - # likely a typo or missing script file, quit early - echo "not found: $target_script" 1>&2 - scala_exit_status=2 - onExit - fi - # all are script args - while [[ $# -gt 0 ]]; do - addScript "${1}" - shift - done - else - # all unrecognized args appearing prior to a script name - addResidual "$1" - shift - fi - ;; - - esac -done - -#[ -n "${dump_args}" ] && dumpArgs ; exit 2 -if [ -z "${execute_mode-}" ]; then - # no script was specified, set run or repl mode - if [[ $options_indicator -eq 0 ]]; then - setExecuteMode 'repl' - else - setExecuteMode 'run' - fi -fi - -[ -n "${script_trace-}" ] && set -x - -case "${execute_mode-}" in -script) - if [ "$CLASS_PATH" ]; then - script_cp_arg="-classpath '$CLASS_PATH'" - fi - setScriptName="-Dscript.path=$target_script" - target_jar="${target_script%.*}.jar" - if [[ $save_compiled == true && "$target_jar" -nt "$target_script" ]]; then - eval "\"$JAVACMD\"" $setScriptName -jar "$target_jar" "${script_args[@]}" - scala_exit_status=$? - else - [[ $save_compiled == true ]] && rm -f $target_jar - PROG_NAME=$ScriptingMain - compilerJavaClasspathArgs # initialize jvm_cp_args with toolchain classpath - scripting_string="-script $target_script ${script_args[@]}" - # use eval instead of exec, to insure that onExit is subsequently called - - # $script_cp_arg must be the first argument to $ScriptingMain - # $scripting_string must be last - eval "\"$JAVACMD\"" \ - ${JAVA_OPTS:-$default_java_opts} \ - "${java_args[@]}" \ - "-classpath \"$jvm_cp_args\"" \ - -Dscala.usejavacp=true \ - "$setScriptName" \ - "$ScriptingMain" \ - ${script_cp_arg-} \ - "${scala_args[@]}" \ - "${residual_args[@]}" \ - "${scripting_string-}" # must be the last arguments - scala_exit_status=$? - fi - ;; - -repl) - if [ "$CLASS_PATH" ]; then - repl_cparg="-classpath \"$CLASS_PATH\"" - fi - eval "\"$PROG_HOME/bin/scalac\" ${repl_cparg-} ${scalac_options[@]} -repl ${residual_args[@]}" - scala_exit_status=$? - ;; - -run) - run_cparg="$DOTTY_LIB$PSEP$SCALA_LIB" - if [ -z "$CLASS_PATH" ]; then - run_cparg+="$PSEP." - else - run_cparg+="$PSEP$CLASS_PATH" - fi - if [ "$class_path_count" -gt 1 ]; then - echo "warning: multiple classpaths are found, scala only use the last one." - fi - if [ $with_compiler == true ]; then - run_cparg+="$PSEP$DOTTY_COMP$PSEP$TASTY_CORE$PSEP$DOTTY_INTF$PSEP$SCALA_ASM$PSEP$DOTTY_STAGING$PSEP$DOTTY_TASTY_INSPECTOR" - fi - # exec here would prevent onExit from being called, leaving terminal in unusable state - eval "\"$JAVACMD\"" "-classpath \"$run_cparg\"" "${java_args[@]}" "${residual_args[@]}" - scala_exit_status=$? - ;; - -*) - echo "warning: command option is not correct." - ;; -esac - onExit diff --git a/project/Build.scala b/project/Build.scala index 9527c1449782..d5ecd0d7db0c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -759,7 +759,24 @@ object Build { if (mode == NonBootstrapped) nonBootstrapedDottyCompilerSettings else bootstrapedDottyCompilerSettings lazy val `scala3-compiler` = project.in(file("compiler")).asDottyCompiler(NonBootstrapped) + + lazy val Scala3CompilerCoursierTest = config("scala3CompilerCoursierTest") extend Test lazy val `scala3-compiler-bootstrapped` = project.in(file("compiler")).asDottyCompiler(Bootstrapped) + .configs(Scala3CompilerCoursierTest) + .settings( + inConfig(Scala3CompilerCoursierTest)(Defaults.testSettings), + Scala3CompilerCoursierTest / scalaSource := baseDirectory.value / "test-coursier", + Scala3CompilerCoursierTest / fork := true, + Scala3CompilerCoursierTest / envVars := Map("DOTTY_BOOTSTRAPPED_VERSION" -> dottyVersion), + Scala3CompilerCoursierTest / unmanagedClasspath += (Scala3CompilerCoursierTest / scalaSource).value, + Scala3CompilerCoursierTest / test := ((Scala3CompilerCoursierTest / test) dependsOn ( + publishLocal, // Had to enumarate all deps since calling `scala3-bootstrap` / publishLocal will lead to recursive dependency => stack overflow + `scala3-interfaces` / publishLocal, + dottyLibrary(Bootstrapped) / publishLocal, + tastyCore(Bootstrapped) / publishLocal, + ), + ).value, + ) def dottyCompiler(implicit mode: Mode): Project = mode match { case NonBootstrapped => `scala3-compiler`