diff --git a/.gitignore b/.gitignore index 20675f9397f0..17a292e4c6a7 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,4 @@ community-build/dotty-community-build-deps cs # Coursier test product -compiler/test-coursier/run/myfile.jar +compiler/test-coursier/run/*.jar diff --git a/compiler/src/dotty/tools/MainGenericRunner.scala b/compiler/src/dotty/tools/MainGenericRunner.scala index ec8fb8f9cfd5..5221e8b642f5 100644 --- a/compiler/src/dotty/tools/MainGenericRunner.scala +++ b/compiler/src/dotty/tools/MainGenericRunner.scala @@ -14,6 +14,7 @@ import dotty.tools.runner.ObjectRunner import dotty.tools.dotc.config.Properties.envOrNone import java.util.jar._ import java.util.jar.Attributes.Name +import dotty.tools.io.Jar enum ExecuteMode: case Guess @@ -148,19 +149,11 @@ object MainGenericRunner { val res = ObjectRunner.runAndCatch(newClasspath, settings.residualArgs.head, settings.residualArgs.drop(1)).flatMap { case ex: ClassNotFoundException if ex.getMessage == settings.residualArgs.head => val file = settings.residualArgs.head - def withJarInput[T](f: JarInputStream => T): T = - val in = new JarInputStream(java.io.FileInputStream(file)) - try f(in) - finally in.close() - val manifest = withJarInput(s => Option(s.getManifest)) - manifest match - case None => Some(IllegalArgumentException(s"Cannot find manifest in jar: $file")) - case Some(f) => - f.getMainAttributes.get(Name.MAIN_CLASS) match - case mainClass: String => - ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mainClass, settings.residualArgs) - case _ => - Some(IllegalArgumentException(s"No main class defined in manifest in jar: $file")) + Jar(file).mainClass match + case Some(mc) => + ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mc, settings.residualArgs) + case None => + Some(IllegalArgumentException(s"No main class defined in manifest in jar: $file")) case ex => Some(ex) } errorFn("", res) diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index 0c3c24916da5..fdb6a6218670 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala @@ -42,6 +42,9 @@ class GenBCode extends Phase { superCallsMap.update(sym, old + calls) } + private val entryPoints = new mutable.HashSet[String]() + def registerEntryPoint(s: String): Unit = entryPoints += s + private var myOutput: AbstractFile = _ private def outputDir(using Context): AbstractFile = { @@ -61,17 +64,44 @@ class GenBCode extends Phase { override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = { + outputDir match + case jar: JarArchive => + updateJarManifestWithMainClass(jar, entryPoints.toList) + case _ => try super.runOn(units) - finally myOutput match { + finally outputDir match { case jar: JarArchive => if (ctx.run.suspendedUnits.nonEmpty) // If we close the jar the next run will not be able to write on the jar. // But if we do not close it we cannot use it as part of the macro classpath of the suspended files. report.error("Can not suspend and output to a jar at the same time. See suspension with -Xprint-suspension.") + jar.close() case _ => } } + + private def updateJarManifestWithMainClass(jarArchive: JarArchive, entryPoints: List[String])(using Context): Unit = + val mainClass = Option.when(!ctx.settings.XmainClass.isDefault)(ctx.settings.XmainClass.value).orElse { + entryPoints match + case List(mainClass) => + Some(mainClass) + case Nil => + report.warning("No Main-Class designated or discovered.") + None + case mcs => + report.warning(s"No Main-Class due to multiple entry points:\n ${mcs.mkString("\n ")}") + None + } + mainClass.map { mc => + val manifest = Jar.WManifest() + manifest.mainClass = mc + val file = jarArchive.subdirectoryNamed("META-INF").fileNamed("MANIFEST.MF") + val os = file.output + manifest.underlying.write(os) + os.close() + } + end updateJarManifestWithMainClass } object GenBCode { diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 911628209971..4481b4e98ab4 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -130,6 +130,7 @@ class Compiler { new RestoreScopes, // Repair scopes rendered invalid by moving definitions in prior phases of the group new SelectStatic, // get rid of selects that would be compiled into GetStatic new sjs.JUnitBootstrappers, // Generate JUnit-specific bootstrapper classes for Scala.js (not enabled by default) + new CollectEntryPoints, // Collect all entry points and save them in the context new CollectSuperCalls, // Find classes that are called with super new RepeatableAnnotations) :: // Aggregate repeatable annotations Nil diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index f3a2d1f2f31f..e89c8b97aea8 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -156,6 +156,7 @@ private sealed trait XSettings: val Xsemanticdb: Setting[Boolean] = BooleanSetting("-Xsemanticdb", "Store information in SemanticDB.", aliases = List("-Ysemanticdb")) val Xtarget: Setting[String] = ChoiceSetting("-Xtarget", "target", "Emit bytecode for the specified version of the Java platform. This might produce bytecode that will break at runtime. When on JDK 9+, consider -release as a safer alternative.", ScalaSettings.supportedTargetVersions, "", aliases = List("--Xtarget")) val XcheckMacros: Setting[Boolean] = BooleanSetting("-Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros")) + val XmainClass: Setting[String] = StringSetting("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d )", "") val XmixinForceForwarders = ChoiceSetting( name = "-Xmixin-force-forwarders", diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 7c1c2494d323..3fe5449c7586 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1018,6 +1018,7 @@ class Definitions { else ArrayType.appliedTo(elem :: Nil) def unapply(tp: Type)(using Context): Option[Type] = tp.dealias match { case AppliedType(at, arg :: Nil) if at.isRef(ArrayType.symbol) => Some(arg) + case JavaArrayType(tp) if ctx.erasedTypes => Some(tp) case _ => None } } diff --git a/compiler/src/dotty/tools/dotc/transform/CollectEntryPoints.scala b/compiler/src/dotty/tools/dotc/transform/CollectEntryPoints.scala new file mode 100644 index 000000000000..b92c57f1fb5e --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/CollectEntryPoints.scala @@ -0,0 +1,54 @@ +package dotty.tools.dotc.transform + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Types +import dotty.tools.dotc.transform.MegaPhase._ +import java.io.{File => _} + +import dotty.tools.dotc.core._ +import SymDenotations._ +import Contexts._ +import Types._ +import Symbols._ +import Phases._ +import dotty.tools.dotc.util.SourcePosition +import Decorators._ +import StdNames.nme +import dotty.tools.io.JarArchive +import dotty.tools.backend.jvm.GenBCode + +/** + * Small phase to be run to collect main classes and store them in the context. + * The general rule to run this phase is: + * - The output of compilation is JarArchive + * - There is no `-Xmain-class` defined + * + * The following flags affect this phase: + * -d path.jar + * -Xmain-class + */ +class CollectEntryPoints extends MiniPhase: + def phaseName: String = "Collect entry points" + + override def isRunnable(using Context): Boolean = + def forceRun = ctx.settings.XmainClass.isDefault && ctx.settings.outputDir.value.isInstanceOf[JarArchive] + super.isRunnable && forceRun + + override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree = + getEntryPoint(tree).map(registerEntryPoint) + tree + + private def getEntryPoint(tree: tpd.TypeDef)(using Context): Option[String] = + val sym = tree.symbol + import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix + val name = sym.fullName.stripModuleClassSuffix.toString + Option.when(sym.isStatic && !sym.is(Flags.Trait) && ctx.platform.hasMainMethod(sym))(name) + + private def registerEntryPoint(s: String)(using Context) = { + genBCodePhase match { + case genBCodePhase: GenBCode => + genBCodePhase.registerEntryPoint(s) + case _ => + } + } diff --git a/compiler/src/dotty/tools/io/Jar.scala b/compiler/src/dotty/tools/io/Jar.scala index 063de2ca8137..6baaec175b14 100644 --- a/compiler/src/dotty/tools/io/Jar.scala +++ b/compiler/src/dotty/tools/io/Jar.scala @@ -72,6 +72,7 @@ class Jar(file: File) { case null => errorFn("No such entry: " + entry) ; null case x => x } + override def toString: String = "" + file } diff --git a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala index 5a09b9ab3a9b..a9c9809b1be7 100644 --- a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala +++ b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala @@ -37,7 +37,7 @@ class CoursierScalaTests: val testScriptArgs = Seq("a", "b", "c", "-repl", "-run", "-script", "-debug") val args = scriptPath +: testScriptArgs - val output = CoursierScalaTests.csCmd(args*) + val output = CoursierScalaTests.csScalaCmd(args*) val expectedOutput = List( "arg 0:[a]", "arg 1:[b]", @@ -55,49 +55,68 @@ class CoursierScalaTests: def scriptPath() = val scriptPath = scripts("/scripting").find(_.getName == "scriptPath.sc").get.absPath val args = scriptPath - val output = CoursierScalaTests.csCmd(args) + val output = CoursierScalaTests.csScalaCmd(args) assertTrue(output.mkString("\n").startsWith("script.path:")) assertTrue(output.mkString("\n").endsWith("scriptPath.sc")) scriptPath() def version() = - val output = CoursierScalaTests.csCmd("-version") + val output = CoursierScalaTests.csScalaCmd("-version") assertTrue(output.mkString("\n").contains(sys.env("DOTTY_BOOTSTRAPPED_VERSION"))) version() def emptyArgsEqualsRepl() = - val output = CoursierScalaTests.csCmd() + val output = CoursierScalaTests.csScalaCmd() 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") + val output = CoursierScalaTests.csScalaCmd("-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") + val output = CoursierScalaTests.csScalaCmd("-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile") assertEquals(output.mkString("\n"), "Hello") notOnlyOptionsEqualsRun() def help() = - val output = CoursierScalaTests.csCmd("-help") + val output = CoursierScalaTests.csScalaCmd("-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) + val output = CoursierScalaTests.csScalaCmd("-save", source.absPath) assertEquals(output.mkString("\n"), "Hello") assertTrue(source.getParentFile.listFiles.find(_.getName == "myfile.jar").isDefined) jar() def runThatJar() = val source = new File(getClass.getResource("/run/myfile.jar").getPath) - val output = CoursierScalaTests.csCmd(source.absPath) + val output = CoursierScalaTests.csScalaCmd(source.absPath) assertEquals(output.mkString("\n"), "Hello") runThatJar() + def compileFilesToJarAndRun() = + val source = new File(getClass.getResource("/run/myfile.scala").getPath) + val prefix = source.getParent + + val o1source = Paths.get(prefix, "automain.jar").toAbsolutePath.toString + val output1 = CoursierScalaTests.csScalaCompilerCmd("-d", o1source, source.absPath) + assertEquals(output1.mkString("\n"), "") + + val o2source = Paths.get(prefix, "custommain.jar").toAbsolutePath.toString + val output2 = CoursierScalaTests.csScalaCompilerCmd("-d", o2source, "-Xmain-class", "run.myfile", source.absPath) + assertEquals(output2.mkString("\n"), "") + + val output3 = CoursierScalaTests.csScalaCmd(o1source) + assertEquals(output3.mkString("\n"), "Hello") + + val output4 = CoursierScalaTests.csScalaCmd(o2source) + assertEquals(output4.mkString("\n"), "Hello") + compileFilesToJarAndRun() + object CoursierScalaTests: def execCmd(command: String, options: String*): List[String] = @@ -106,11 +125,17 @@ object CoursierScalaTests: cmd.!(ProcessLogger(out += _, out += _)) out.toList - def csCmd(options: String*): List[String] = + def csScalaCmd(options: String*): List[String] = + csCmd("dotty.tools.MainGenericRunner", options*) + + def csScalaCompilerCmd(options: String*): List[String] = + csCmd("dotty.tools.dotc.Main", options*) + + private def csCmd(entry: String, 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)*) + execCmd("./cs", (s"""launch "org.scala-lang:scala3-compiler_3:${sys.env("DOTTY_BOOTSTRAPPED_VERSION")}" --main-class "$entry" --property "scala.usejavacp=true"""" +: newOptions)*) /** Get coursier script */ @BeforeClass def setup(): Unit =