Skip to content

Commit 4ed3ba5

Browse files
authored
Merge pull request #13263 from BarkingBad/fix-executable-jars
Fix executable jars
2 parents 1524a5a + 9cde003 commit 4ed3ba5

File tree

9 files changed

+132
-26
lines changed

9 files changed

+132
-26
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: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import dotty.tools.runner.ObjectRunner
1414
import dotty.tools.dotc.config.Properties.envOrNone
1515
import java.util.jar._
1616
import java.util.jar.Attributes.Name
17+
import dotty.tools.io.Jar
1718

1819
enum ExecuteMode:
1920
case Guess
@@ -148,19 +149,11 @@ object MainGenericRunner {
148149
val res = ObjectRunner.runAndCatch(newClasspath, settings.residualArgs.head, settings.residualArgs.drop(1)).flatMap {
149150
case ex: ClassNotFoundException if ex.getMessage == settings.residualArgs.head =>
150151
val file = settings.residualArgs.head
151-
def withJarInput[T](f: JarInputStream => T): T =
152-
val in = new JarInputStream(java.io.FileInputStream(file))
153-
try f(in)
154-
finally in.close()
155-
val manifest = withJarInput(s => Option(s.getManifest))
156-
manifest match
157-
case None => Some(IllegalArgumentException(s"Cannot find manifest in jar: $file"))
158-
case Some(f) =>
159-
f.getMainAttributes.get(Name.MAIN_CLASS) match
160-
case mainClass: String =>
161-
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mainClass, settings.residualArgs)
162-
case _ =>
163-
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"))
164157
case ex => Some(ex)
165158
}
166159
errorFn("", res)

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class GenBCode extends Phase {
4242
superCallsMap.update(sym, old + calls)
4343
}
4444

45+
private val entryPoints = new mutable.HashSet[String]()
46+
def registerEntryPoint(s: String): Unit = entryPoints += s
47+
4548
private var myOutput: AbstractFile = _
4649

4750
private def outputDir(using Context): AbstractFile = {
@@ -61,17 +64,44 @@ class GenBCode extends Phase {
6164

6265

6366
override def runOn(units: List[CompilationUnit])(using Context): List[CompilationUnit] = {
67+
outputDir match
68+
case jar: JarArchive =>
69+
updateJarManifestWithMainClass(jar, entryPoints.toList)
70+
case _ =>
6471
try super.runOn(units)
65-
finally myOutput match {
72+
finally outputDir match {
6673
case jar: JarArchive =>
6774
if (ctx.run.suspendedUnits.nonEmpty)
6875
// If we close the jar the next run will not be able to write on the jar.
6976
// But if we do not close it we cannot use it as part of the macro classpath of the suspended files.
7077
report.error("Can not suspend and output to a jar at the same time. See suspension with -Xprint-suspension.")
78+
7179
jar.close()
7280
case _ =>
7381
}
7482
}
83+
84+
private def updateJarManifestWithMainClass(jarArchive: JarArchive, entryPoints: List[String])(using Context): Unit =
85+
val mainClass = Option.when(!ctx.settings.XmainClass.isDefault)(ctx.settings.XmainClass.value).orElse {
86+
entryPoints match
87+
case List(mainClass) =>
88+
Some(mainClass)
89+
case Nil =>
90+
report.warning("No Main-Class designated or discovered.")
91+
None
92+
case mcs =>
93+
report.warning(s"No Main-Class due to multiple entry points:\n ${mcs.mkString("\n ")}")
94+
None
95+
}
96+
mainClass.map { mc =>
97+
val manifest = Jar.WManifest()
98+
manifest.mainClass = mc
99+
val file = jarArchive.subdirectoryNamed("META-INF").fileNamed("MANIFEST.MF")
100+
val os = file.output
101+
manifest.underlying.write(os)
102+
os.close()
103+
}
104+
end updateJarManifestWithMainClass
75105
}
76106

77107
object GenBCode {

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ class Compiler {
132132
new RestoreScopes, // Repair scopes rendered invalid by moving definitions in prior phases of the group
133133
new SelectStatic, // get rid of selects that would be compiled into GetStatic
134134
new sjs.JUnitBootstrappers, // Generate JUnit-specific bootstrapper classes for Scala.js (not enabled by default)
135+
new CollectEntryPoints, // Collect all entry points and save them in the context
135136
new CollectSuperCalls, // Find classes that are called with super
136137
new RepeatableAnnotations) :: // Aggregate repeatable annotations
137138
Nil

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ private sealed trait XSettings:
156156
val Xsemanticdb: Setting[Boolean] = BooleanSetting("-Xsemanticdb", "Store information in SemanticDB.", aliases = List("-Ysemanticdb"))
157157
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"))
158158
val XcheckMacros: Setting[Boolean] = BooleanSetting("-Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros"))
159+
val XmainClass: Setting[String] = StringSetting("-Xmain-class", "path", "Class for manifest's Main-Class entry (only useful with -d <jar>)", "")
159160

160161
val XmixinForceForwarders = ChoiceSetting(
161162
name = "-Xmixin-force-forwarders",

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,7 @@ class Definitions {
10181018
else ArrayType.appliedTo(elem :: Nil)
10191019
def unapply(tp: Type)(using Context): Option[Type] = tp.dealias match {
10201020
case AppliedType(at, arg :: Nil) if at.isRef(ArrayType.symbol) => Some(arg)
1021+
case JavaArrayType(tp) if ctx.erasedTypes => Some(tp)
10211022
case _ => None
10221023
}
10231024
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dotty.tools.dotc.transform
2+
3+
import dotty.tools.dotc.ast.tpd
4+
import dotty.tools.dotc.core.Contexts.Context
5+
import dotty.tools.dotc.core.Types
6+
import dotty.tools.dotc.transform.MegaPhase._
7+
import java.io.{File => _}
8+
9+
import dotty.tools.dotc.core._
10+
import SymDenotations._
11+
import Contexts._
12+
import Types._
13+
import Symbols._
14+
import Phases._
15+
import dotty.tools.dotc.util.SourcePosition
16+
import Decorators._
17+
import StdNames.nme
18+
import dotty.tools.io.JarArchive
19+
import dotty.tools.backend.jvm.GenBCode
20+
21+
/**
22+
* Small phase to be run to collect main classes and store them in the context.
23+
* The general rule to run this phase is:
24+
* - The output of compilation is JarArchive
25+
* - There is no `-Xmain-class` defined
26+
*
27+
* The following flags affect this phase:
28+
* -d path.jar
29+
* -Xmain-class
30+
*/
31+
class CollectEntryPoints extends MiniPhase:
32+
def phaseName: String = "Collect entry points"
33+
34+
override def isRunnable(using Context): Boolean =
35+
def forceRun = ctx.settings.XmainClass.isDefault && ctx.settings.outputDir.value.isInstanceOf[JarArchive]
36+
super.isRunnable && forceRun
37+
38+
override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree =
39+
getEntryPoint(tree).map(registerEntryPoint)
40+
tree
41+
42+
private def getEntryPoint(tree: tpd.TypeDef)(using Context): Option[String] =
43+
val sym = tree.symbol
44+
import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix
45+
val name = sym.fullName.stripModuleClassSuffix.toString
46+
Option.when(sym.isStatic && !sym.is(Flags.Trait) && ctx.platform.hasMainMethod(sym))(name)
47+
48+
private def registerEntryPoint(s: String)(using Context) = {
49+
genBCodePhase match {
50+
case genBCodePhase: GenBCode =>
51+
genBCodePhase.registerEntryPoint(s)
52+
case _ =>
53+
}
54+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class Jar(file: File) {
7272
case null => errorFn("No such entry: " + entry) ; null
7373
case x => x
7474
}
75+
7576
override def toString: String = "" + file
7677
}
7778

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 =

0 commit comments

Comments
 (0)