Skip to content

Commit f074726

Browse files
committed
Fix making executable JARs from scala compiler output
1 parent 0421117 commit f074726

File tree

6 files changed

+61
-37
lines changed

6 files changed

+61
-37
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,4 @@ community-build/dotty-community-build-deps
101101
cs
102102

103103
# Coursier test product
104-
compiler/test-coursier/run/myfile.jar
104+
compiler/test-coursier/run/*.jar

compiler/src/dotty/tools/MainGenericRunner.scala

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,11 @@ object MainGenericRunner {
149149
val res = ObjectRunner.runAndCatch(newClasspath, settings.residualArgs.head, settings.residualArgs.drop(1)).flatMap {
150150
case ex: ClassNotFoundException if ex.getMessage == settings.residualArgs.head =>
151151
val file = settings.residualArgs.head
152-
def withJarInput[T](f: JarInputStream => T): T =
153-
val in = new JarInputStream(java.io.FileInputStream(file))
154-
try f(in)
155-
finally in.close()
156-
val manifest = withJarInput(s => Option(s.getManifest))
157-
manifest match
158-
case None => Some(IllegalArgumentException(s"Cannot find manifest in jar: $file"))
159-
case Some(f) =>
160-
f.getMainAttributes.get(Name.MAIN_CLASS) match
161-
case mainClass: String =>
162-
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mainClass, settings.residualArgs)
163-
case _ =>
164-
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $file"))
152+
Jar(file).mainClass match
153+
case Some(mc) =>
154+
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mc, settings.residualArgs)
155+
case None =>
156+
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $file"))
165157
case ex => Some(ex)
166158
}
167159
errorFn("", res)

compiler/src/dotty/tools/backend/jvm/GenBCode.scala

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ class GenBCode extends Phase {
8383
val sym = tree.symbol
8484
import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix
8585
val name = sym.fullName.stripModuleClassSuffix.toString
86-
// We strip module class suffix. Zinc relies on a class and its companion having the same name
87-
8886
if (sym.isStatic && !sym.is(Flags.Trait) && ctx.platform.hasMainMethod(sym)) {
8987
// If sym is an object, all main methods count, otherwise only @static ones count.
9088
_mainClassesBuffer += name
@@ -103,17 +101,11 @@ class GenBCode extends Phase {
103101
}
104102

105103
mainClass.map { mc =>
106-
import scala.util.Properties._
107-
import java.util.jar._
108-
import Attributes.Name._
109-
val manifest = new Manifest
110-
val attrs = manifest.getMainAttributes
111-
attrs.put(MANIFEST_VERSION, "1.0")
112-
attrs.put(ScalaCompilerVersion, versionNumberString)
113-
attrs.put(MAIN_CLASS, mc)
104+
val manifest = Jar.WManifest()
105+
manifest.mainClass = mc
114106
val file = jarArchive.subdirectoryNamed("META-INF").fileNamed("MANIFEST.MF")
115107
val os = file.output
116-
manifest.write(os)
108+
manifest.underlying.write(os)
117109
os.close()
118110
}
119111
end updateJarManifestWithMainClass

compiler/src/dotty/tools/io/Jar.scala

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Jar(file: File) {
4444
import Jar._
4545

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

4949
def mainClass: Option[String] = manifest.map(_(Name.MAIN_CLASS))
5050
/** The manifest-defined classpath String if available. */
@@ -72,6 +72,21 @@ class Jar(file: File) {
7272
case null => errorFn("No such entry: " + entry) ; null
7373
case x => x
7474
}
75+
76+
/**
77+
* Hack for Java reading MANIFEST.MF only if it is at the first entry of the JAR file and otherwise returns null.
78+
* https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/jar/JarInputStream.java#L74
79+
* Suprisingly, such jars still can be successfully runned jars via `java -jar path.jar` so it is only problem for Jar readers.
80+
*/
81+
private def findManifest(s: JarInputStream): Option[Manifest] =
82+
val entry = s.getNextEntry
83+
if entry != null && entry.getName != JarFile.MANIFEST_NAME then
84+
findManifest(s)
85+
else if entry == null then
86+
None
87+
else
88+
Some(Manifest(s))
89+
7590
override def toString: String = "" + file
7691
}
7792

compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class CoursierScalaTests:
3737
val testScriptArgs = Seq("a", "b", "c", "-repl", "-run", "-script", "-debug")
3838

3939
val args = scriptPath +: testScriptArgs
40-
val output = CoursierScalaTests.csCmd(args*)
40+
val output = CoursierScalaTests.csScalaCmd(args*)
4141
val expectedOutput = List(
4242
"arg 0:[a]",
4343
"arg 1:[b]",
@@ -55,49 +55,68 @@ class CoursierScalaTests:
5555
def scriptPath() =
5656
val scriptPath = scripts("/scripting").find(_.getName == "scriptPath.sc").get.absPath
5757
val args = scriptPath
58-
val output = CoursierScalaTests.csCmd(args)
58+
val output = CoursierScalaTests.csScalaCmd(args)
5959
assertTrue(output.mkString("\n").startsWith("script.path:"))
6060
assertTrue(output.mkString("\n").endsWith("scriptPath.sc"))
6161
scriptPath()
6262

6363
def version() =
64-
val output = CoursierScalaTests.csCmd("-version")
64+
val output = CoursierScalaTests.csScalaCmd("-version")
6565
assertTrue(output.mkString("\n").contains(sys.env("DOTTY_BOOTSTRAPPED_VERSION")))
6666
version()
6767

6868
def emptyArgsEqualsRepl() =
69-
val output = CoursierScalaTests.csCmd()
69+
val output = CoursierScalaTests.csScalaCmd()
7070
assertTrue(output.mkString("\n").contains("Unable to create a system terminal")) // Scala attempted to create REPL so we can assume it is working
7171
emptyArgsEqualsRepl()
7272

7373
def run() =
74-
val output = CoursierScalaTests.csCmd("-run", "-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile")
74+
val output = CoursierScalaTests.csScalaCmd("-run", "-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile")
7575
assertEquals(output.mkString("\n"), "Hello")
7676
run()
7777

7878
def notOnlyOptionsEqualsRun() =
79-
val output = CoursierScalaTests.csCmd("-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile")
79+
val output = CoursierScalaTests.csScalaCmd("-classpath", scripts("/run").head.getParentFile.getParent, "run.myfile")
8080
assertEquals(output.mkString("\n"), "Hello")
8181
notOnlyOptionsEqualsRun()
8282

8383
def help() =
84-
val output = CoursierScalaTests.csCmd("-help")
84+
val output = CoursierScalaTests.csScalaCmd("-help")
8585
assertTrue(output.mkString("\n").contains("Usage: scala <options> <source files>"))
8686
help()
8787

8888
def jar() =
8989
val source = new File(getClass.getResource("/run/myfile.scala").getPath)
90-
val output = CoursierScalaTests.csCmd("-save", source.absPath)
90+
val output = CoursierScalaTests.csScalaCmd("-save", source.absPath)
9191
assertEquals(output.mkString("\n"), "Hello")
9292
assertTrue(source.getParentFile.listFiles.find(_.getName == "myfile.jar").isDefined)
9393
jar()
9494

9595
def runThatJar() =
9696
val source = new File(getClass.getResource("/run/myfile.jar").getPath)
97-
val output = CoursierScalaTests.csCmd(source.absPath)
97+
val output = CoursierScalaTests.csScalaCmd(source.absPath)
9898
assertEquals(output.mkString("\n"), "Hello")
9999
runThatJar()
100100

101+
def compileFilesToJarAndRun() =
102+
val source = new File(getClass.getResource("/run/myfile.scala").getPath)
103+
val prefix = source.getParent
104+
105+
val o1source = Paths.get(prefix, "automain.jar").toAbsolutePath.toString
106+
val output1 = CoursierScalaTests.csScalaCompilerCmd("-d", o1source, source.absPath)
107+
assertEquals(output1.mkString("\n"), "")
108+
109+
val o2source = Paths.get(prefix, "custommain.jar").toAbsolutePath.toString
110+
val output2 = CoursierScalaTests.csScalaCompilerCmd("-d", o2source, "-Xmain-class", "run.myfile", source.absPath)
111+
assertEquals(output2.mkString("\n"), "")
112+
113+
val output3 = CoursierScalaTests.csScalaCmd(o1source)
114+
assertEquals(output3.mkString("\n"), "Hello")
115+
116+
val output4 = CoursierScalaTests.csScalaCmd(o2source)
117+
assertEquals(output4.mkString("\n"), "Hello")
118+
compileFilesToJarAndRun()
119+
101120
object CoursierScalaTests:
102121

103122
def execCmd(command: String, options: String*): List[String] =
@@ -106,11 +125,17 @@ object CoursierScalaTests:
106125
cmd.!(ProcessLogger(out += _, out += _))
107126
out.toList
108127

109-
def csCmd(options: String*): List[String] =
128+
def csScalaCmd(options: String*): List[String] =
129+
csCmd("dotty.tools.MainGenericRunner", options*)
130+
131+
def csScalaCompilerCmd(options: String*): List[String] =
132+
csCmd("dotty.tools.dotc.Main", options*)
133+
134+
private def csCmd(entry: String, options: String*): List[String] =
110135
val newOptions = options match
111136
case Nil => options
112137
case _ => "--" +: options
113-
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)*)
138+
execCmd("./cs", (s"""launch "org.scala-lang:scala3-compiler_3:${sys.env("DOTTY_BOOTSTRAPPED_VERSION")}" --main-class "$entry" --property "scala.usejavacp=true"""" +: newOptions)*)
114139

115140
/** Get coursier script */
116141
@BeforeClass def setup(): Unit =

hihi.jar

-3.24 KB
Binary file not shown.

0 commit comments

Comments
 (0)