Skip to content

Fix executable jars #13263

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 7 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all 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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 6 additions & 13 deletions compiler/src/dotty/tools/MainGenericRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 31 additions & 1 deletion compiler/src/dotty/tools/backend/jvm/GenBCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 <jar>)", "")

val XmixinForceForwarders = ChoiceSetting(
name = "-Xmixin-force-forwarders",
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
54 changes: 54 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/CollectEntryPoints.scala
Original file line number Diff line number Diff line change
@@ -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 _ =>
}
}
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/io/Jar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class Jar(file: File) {
case null => errorFn("No such entry: " + entry) ; null
case x => x
}

override def toString: String = "" + file
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand All @@ -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 <options> <source files>"))
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] =
Expand All @@ -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 =
Expand Down