Skip to content

Commit 3c38733

Browse files
authored
Merge pull request #1095 from Gedochao/favor-non-script-main-class
Favor non-script main classes & add an option for listing main classes in context
2 parents c24e37c + cd4ca47 commit 3c38733

File tree

10 files changed

+387
-57
lines changed

10 files changed

+387
-57
lines changed

modules/build/src/main/scala/scala/build/Build.scala

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,66 @@ object Build {
4949
generatedSources: Seq[GeneratedSource],
5050
isPartial: Boolean
5151
) extends Build {
52-
def success: Boolean = true
53-
def successfulOpt: Some[this.type] = Some(this)
54-
def outputOpt: Some[os.Path] = Some(output)
55-
def fullClassPath: Seq[os.Path] = Seq(output) ++ sources.resourceDirs ++ artifacts.classPath
56-
def foundMainClasses(): Seq[String] =
57-
MainClass.find(output)
58-
def retainedMainClass(logger: Logger): Either[MainClassError, String] = {
59-
lazy val foundMainClasses0 = foundMainClasses()
52+
def success: Boolean = true
53+
def successfulOpt: Some[this.type] = Some(this)
54+
def outputOpt: Some[os.Path] = Some(output)
55+
def fullClassPath: Seq[os.Path] = Seq(output) ++ sources.resourceDirs ++ artifacts.classPath
56+
def foundMainClasses(): Seq[String] = MainClass.find(output)
57+
def retainedMainClass(
58+
mainClasses: Seq[String],
59+
logger: Logger
60+
): Either[MainClassError, String] = {
6061
val defaultMainClassOpt = sources.defaultMainClass
61-
.filter(name => foundMainClasses0.contains(name))
62+
.filter(name => mainClasses.contains(name))
6263
def foundMainClass =
63-
if (foundMainClasses0.isEmpty) Left(new NoMainClassFoundError)
64-
else if (foundMainClasses0.length == 1) Right(foundMainClasses0.head)
65-
else
66-
options.interactive.chooseOne(
67-
"Found several main classes. Which would you like to run?",
68-
foundMainClasses0.toList
69-
).toRight {
70-
new SeveralMainClassesFoundError(
71-
::(foundMainClasses0.head, foundMainClasses0.tail.toList),
72-
Nil
73-
)
74-
}
64+
mainClasses match {
65+
case Seq() => Left(new NoMainClassFoundError)
66+
case Seq(mainClass) => Right(mainClass)
67+
case _ =>
68+
val scriptInferredMainClasses =
69+
sources.inMemory.map(im => im.originalPath.map(_._1))
70+
.flatMap {
71+
case Right(originalRelPath) if originalRelPath.toString.endsWith(".sc") =>
72+
Some {
73+
originalRelPath
74+
.toString
75+
.replace(".", "_")
76+
.replace("/", ".")
77+
}
78+
case Left(stdin @ "stdin") => Some(s"${stdin}_sc")
79+
case _ => None
80+
}
81+
val filteredMainClasses =
82+
mainClasses.filter(!scriptInferredMainClasses.contains(_))
83+
if (filteredMainClasses.length == 1) {
84+
val pickedMainClass = filteredMainClasses.head
85+
if (scriptInferredMainClasses.nonEmpty) {
86+
val firstScript = scriptInferredMainClasses.head
87+
val scriptsString = scriptInferredMainClasses.mkString(", ")
88+
logger.message(
89+
s"Running $pickedMainClass. Also detected script main classes: $scriptsString"
90+
)
91+
logger.message(
92+
s"You can run any one of them by passing option --main-class, i.e. --main-class $firstScript"
93+
)
94+
logger.message(
95+
"All available main classes can always be listed by passing option --list-main-classes"
96+
)
97+
}
98+
Right(pickedMainClass)
99+
}
100+
else options.interactive
101+
.chooseOne(
102+
"Found several main classes. Which would you like to run?",
103+
mainClasses.toList
104+
)
105+
.toRight {
106+
new SeveralMainClassesFoundError(
107+
::(mainClasses.head, mainClasses.tail.toList),
108+
Nil
109+
)
110+
}
111+
}
75112

76113
defaultMainClassOpt match {
77114
case Some(cls) => Right(cls)

modules/cli-options/src/main/scala/scala/cli/commands/MainClassOptions.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ final case class MainClassOptions(
88
@HelpMessage("Specify which main class to run")
99
@ValueDescription("main-class")
1010
@Name("M")
11-
mainClass: Option[String] = None
11+
mainClass: Option[String] = None,
12+
13+
@Group("Entrypoint")
14+
@HelpMessage("List main classes available in the current context")
15+
@Name("mainClassList")
16+
@Name("listMainClass")
17+
@Name("listMainClasses")
18+
mainClassLs: Option[Boolean] = None
1219
)
1320
// format: on
1421

modules/cli/src/main/scala/scala/cli/commands/Package.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ object Package extends ScalaCommand[PackageOptions] {
212212
.map(_.stripSuffix("_sc"))
213213
.map(_ + extension)
214214
}
215-
.orElse(build.retainedMainClass(logger).map(_.stripSuffix("_sc") + extension).toOption)
215+
.orElse(build.retainedMainClass(build.foundMainClasses(), logger).map(
216+
_.stripSuffix("_sc") + extension
217+
).toOption)
216218
.orElse(build.sources.paths.collectFirst(_._1.baseName + extension))
217219
.getOrElse(defaultName)
218220
val destPath = os.Path(dest, Os.pwd)
@@ -237,7 +239,7 @@ object Package extends ScalaCommand[PackageOptions] {
237239
def mainClass: Either[BuildException, String] =
238240
build.options.mainClass match {
239241
case Some(cls) => Right(cls)
240-
case None => build.retainedMainClass(logger)
242+
case None => build.retainedMainClass(build.foundMainClasses(), logger)
241243
}
242244

243245
val packageOptions = build.options.notForBloopOptions.packageOptions

modules/cli/src/main/scala/scala/cli/commands/Run.scala

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig}
1010
import scala.build.options.{BuildOptions, JavaOpt, Platform}
1111
import scala.build.{Build, BuildThreads, Inputs, Logger, Positioned}
1212
import scala.cli.CurrentParams
13+
import scala.cli.commands.util.MainClassOptionsUtil._
1314
import scala.cli.commands.util.SharedOptionsUtil._
1415
import scala.cli.internal.ProcUtil
1516
import scala.util.Properties
@@ -65,31 +66,43 @@ object Run extends ScalaCommand[RunOptions] {
6566
def maybeRun(
6667
build: Build.Successful,
6768
allowTerminate: Boolean
68-
): Either[BuildException, (Process, CompletableFuture[_])] = either {
69-
val process = value(maybeRunOnce(
70-
build,
71-
programArgs,
72-
logger,
73-
allowExecve = allowTerminate,
74-
jvmRunner = build.artifacts.hasJvmRunner
75-
))
69+
): Either[BuildException, Option[(Process, CompletableFuture[_])]] = either {
70+
val potentialMainClasses = build.foundMainClasses()
71+
if (options.mainClass.mainClassLs.contains(true))
72+
value {
73+
options.mainClass
74+
.maybePrintMainClasses(potentialMainClasses, shouldExit = allowTerminate)
75+
.map(_ => None)
76+
}
77+
else {
78+
val process = value {
79+
maybeRunOnce(
80+
build,
81+
programArgs,
82+
logger,
83+
allowExecve = allowTerminate,
84+
jvmRunner = build.artifacts.hasJvmRunner,
85+
potentialMainClasses
86+
)
87+
}
7688

77-
val onExitProcess = process.onExit().thenApply { p1 =>
78-
val retCode = p1.exitValue()
79-
if (retCode != 0)
80-
if (allowTerminate)
81-
sys.exit(retCode)
82-
else {
83-
val red = Console.RED
84-
val lightRed = "\u001b[91m"
85-
val reset = Console.RESET
86-
System.err.println(
87-
s"${red}Program exited with return code $lightRed$retCode$red.$reset"
88-
)
89-
}
90-
}
89+
val onExitProcess = process.onExit().thenApply { p1 =>
90+
val retCode = p1.exitValue()
91+
if (retCode != 0)
92+
if (allowTerminate)
93+
sys.exit(retCode)
94+
else {
95+
val red = Console.RED
96+
val lightRed = "\u001b[91m"
97+
val reset = Console.RESET
98+
System.err.println(
99+
s"${red}Program exited with return code $lightRed$retCode$red.$reset"
100+
)
101+
}
102+
}
91103

92-
(process, onExitProcess)
104+
Some((process, onExitProcess))
105+
}
93106
}
94107

95108
val cross = options.compileCross.cross.getOrElse(false)
@@ -126,6 +139,7 @@ object Run extends ScalaCommand[RunOptions] {
126139
if (proc.isAlive) ProcUtil.forceKillProcess(proc, logger)
127140
val maybeProcess = maybeRun(s, allowTerminate = false)
128141
.orReport(logger)
142+
.flatten
129143
if (options.watch.restart)
130144
processOpt = maybeProcess
131145
else
@@ -154,7 +168,7 @@ object Run extends ScalaCommand[RunOptions] {
154168
builds.main match {
155169
case s: Build.Successful =>
156170
val (process, onExit) = maybeRun(s, allowTerminate = true)
157-
.orExit(logger)
171+
.orExit(logger).getOrElse(sys.exit(1))
158172
ProcUtil.waitForProcess(process, onExit)
159173
case _: Build.Failed =>
160174
System.err.println("Compilation failed")
@@ -168,7 +182,8 @@ object Run extends ScalaCommand[RunOptions] {
168182
args: Seq[String],
169183
logger: Logger,
170184
allowExecve: Boolean,
171-
jvmRunner: Boolean
185+
jvmRunner: Boolean,
186+
potentialMainClasses: Seq[String]
172187
): Either[BuildException, Process] = either {
173188

174189
val mainClassOpt = build.options.mainClass.filter(_.nonEmpty) // trim it too?
@@ -178,7 +193,7 @@ object Run extends ScalaCommand[RunOptions] {
178193
}
179194
val mainClass = mainClassOpt match {
180195
case Some(cls) => cls
181-
case None => value(build.retainedMainClass(logger))
196+
case None => value(build.retainedMainClass(potentialMainClasses, logger))
182197
}
183198
val verbosity = build.options.internal.verbosity.getOrElse(0).toString
184199

modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ object Publish extends ScalaCommand[PublishOptions] {
384384

385385
val mainJar = {
386386
val mainClassOpt = build.options.mainClass.orElse {
387-
build.retainedMainClass(logger) match {
387+
val potentialMainClasses = build.foundMainClasses()
388+
build.retainedMainClass(potentialMainClasses, logger) match {
388389
case Left(_: NoMainClassFoundError) => None
389390
case Left(err) =>
390391
logger.debug(s"Error while looking for main class: $err")
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package scala.cli.commands.util
2+
3+
import scala.build.errors.{MainClassError, NoMainClassFoundError}
4+
import scala.cli.commands.MainClassOptions
5+
6+
object MainClassOptionsUtil {
7+
implicit class MainClassOptionsOps(v: MainClassOptions) {
8+
def maybePrintMainClasses(
9+
mainClasses: Seq[String],
10+
shouldExit: Boolean = true
11+
): Either[MainClassError, Unit] =
12+
v.mainClassLs match {
13+
case Some(true) if mainClasses.nonEmpty =>
14+
println(mainClasses.mkString(" "))
15+
if (shouldExit) sys.exit(0)
16+
else Right(())
17+
case Some(true) => Left(new NoMainClassFoundError)
18+
case _ => Right(())
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)