Skip to content
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

Fix executable jars #13263

Merged
merged 7 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
37 changes: 37 additions & 0 deletions compiler/src/dotty/tools/backend/jvm/GenBCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,47 @@ class GenBCode extends Phase {
// 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.")

updateJarManifestWithMainClass(units, jar)
jar.close()
case _ =>
}
}

private def updateJarManifestWithMainClass(units: List[CompilationUnit], jarArchive: JarArchive)(using Context): Unit =
val mainClass = Option.when(!ctx.settings.XmainClass.isDefault)(ctx.settings.XmainClass.value).orElse {
val _mainClassesBuffer = new mutable.HashSet[String]
units.map { unit =>
unit.tpdTree.foreachSubTree { tree =>
val sym = tree.symbol
import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix
val name = sym.fullName.stripModuleClassSuffix.toString
if (sym.isStatic && !sym.is(Flags.Trait) && ctx.platform.hasMainMethod(sym)) {
// If sym is an object, all main methods count, otherwise only @static ones count.
_mainClassesBuffer += name
}
}
}
_mainClassesBuffer.toList.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
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied from compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala I was thinking whether we could extract it for reuse rather than copy-paste, yet I wasn't sure where should I place it, maybe somewhere under dotty.tools.dotc.util

Copy link
Member

@smarter smarter Aug 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please feel free to extract any code for reuse yes, doesn't matter too much where it ends up :). However, having to fully traverse every single compilation unit just to get main method names out is going to be fairly expensive and should be avoided, especially for information that we should already know. Maybe ExtractAPI (or some other phase which is always run if we want this to work even outside of sbt) should collect this information and store it in the Context. In fact, we used to have a phase which was supposed to do that but I deleted it because it was dead code: 3ebf919

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 will look into that :D


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/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
17 changes: 16 additions & 1 deletion compiler/src/dotty/tools/io/Jar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Jar(file: File) {
import Jar._

lazy val jarFile: JarFile = new JarFile(file.jpath.toFile)
lazy val manifest: Option[Manifest] = withJarInput(s => Option(s.getManifest))
lazy val manifest: Option[Manifest] = withJarInput(s => Option(s.getManifest).orElse(findManifest(s)))

def mainClass: Option[String] = manifest.map(_(Name.MAIN_CLASS))
/** The manifest-defined classpath String if available. */
Expand Down Expand Up @@ -72,6 +72,21 @@ class Jar(file: File) {
case null => errorFn("No such entry: " + entry) ; null
case x => x
}

/**
* Hack for Java reading MANIFEST.MF only if it is at the first entry of the JAR file and otherwise returns null.
* https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/jar/JarInputStream.java#L74
* Suprisingly, such jars still can be successfully runned jars via `java -jar path.jar` so it is only problem for Jar readers.
*/
private def findManifest(s: JarInputStream): Option[Manifest] =
val entry = s.getNextEntry
if entry != null && entry.getName != JarFile.MANIFEST_NAME then
findManifest(s)
else if entry == null then
None
else
Some(Manifest(s))

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