Skip to content

Commit 880242e

Browse files
authored
Merge pull request #1369 from Gedochao/backwards-compat-classpat-and-main-class-inputs
Make inputs optional when `-classpath` and `--main-class` are passed
2 parents bce41cb + fe791de commit 880242e

File tree

9 files changed

+134
-31
lines changed

9 files changed

+134
-31
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ object Build {
5757
def outputOpt: Some[os.Path] = Some(output)
5858
def dependencyClassPath: Seq[os.Path] = sources.resourceDirs ++ artifacts.classPath
5959
def fullClassPath: Seq[os.Path] = Seq(output) ++ dependencyClassPath
60-
def foundMainClasses(): Seq[String] = MainClass.find(output)
60+
def foundMainClasses(): Seq[String] =
61+
MainClass.find(output) ++ options.classPathOptions.extraClassPath.flatMap(MainClass.find)
6162
def retainedMainClass(
6263
mainClasses: Seq[String],
6364
commandString: String,

modules/build/src/main/scala/scala/build/Inputs.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,11 @@ object Inputs {
302302
directories: Directories,
303303
forcedWorkspace: Option[os.Path],
304304
enableMarkdown: Boolean,
305-
allowRestrictedFeatures: Boolean
305+
allowRestrictedFeatures: Boolean,
306+
extraClasspathWasPassed: Boolean
306307
): Inputs = {
307308

308-
assert(validElems.nonEmpty)
309+
assert(extraClasspathWasPassed || validElems.nonEmpty)
309310

310311
val (inferredWorkspace, inferredNeedsHash, workspaceOrigin) = {
311312
val settingsFiles = projectSettingsFiles(validElems)
@@ -490,7 +491,8 @@ object Inputs {
490491
acceptFds: Boolean,
491492
forcedWorkspace: Option[os.Path],
492493
enableMarkdown: Boolean,
493-
allowRestrictedFeatures: Boolean
494+
allowRestrictedFeatures: Boolean,
495+
extraClasspathWasPassed: Boolean
494496
): Either[BuildException, Inputs] = {
495497
val validatedArgs: Seq[Either[String, Seq[Element]]] =
496498
validateArgs(args, cwd, download, stdinOpt, acceptFds)
@@ -504,15 +506,16 @@ object Inputs {
504506
val validElems = validatedArgsAndSnippets.collect {
505507
case Right(elem) => elem
506508
}.flatten
507-
assert(validElems.nonEmpty)
509+
assert(extraClasspathWasPassed || validElems.nonEmpty)
508510

509511
Right(forValidatedElems(
510512
validElems,
511513
baseProjectName,
512514
directories,
513515
forcedWorkspace,
514516
enableMarkdown,
515-
allowRestrictedFeatures
517+
allowRestrictedFeatures,
518+
extraClasspathWasPassed
516519
))
517520
}
518521
else
@@ -533,10 +536,11 @@ object Inputs {
533536
acceptFds: Boolean = false,
534537
forcedWorkspace: Option[os.Path] = None,
535538
enableMarkdown: Boolean = false,
536-
allowRestrictedFeatures: Boolean
539+
allowRestrictedFeatures: Boolean,
540+
extraClasspathWasPassed: Boolean
537541
): Either[BuildException, Inputs] =
538542
if (
539-
args.isEmpty && scriptSnippetList.isEmpty && scalaSnippetList.isEmpty && javaSnippetList.isEmpty
543+
args.isEmpty && scriptSnippetList.isEmpty && scalaSnippetList.isEmpty && javaSnippetList.isEmpty && !extraClasspathWasPassed
540544
)
541545
defaultInputs().toRight(new InputsException(
542546
"No inputs provided (expected files with .scala, .sc, .java or .md extensions, and / or directories)."
@@ -555,7 +559,8 @@ object Inputs {
555559
acceptFds,
556560
forcedWorkspace,
557561
enableMarkdown,
558-
allowRestrictedFeatures
562+
allowRestrictedFeatures,
563+
extraClasspathWasPassed
559564
)
560565

561566
def default(): Option[Inputs] =

modules/build/src/main/scala/scala/build/internal/MainClass.scala

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package scala.build.internal
22

33
import org.objectweb.asm
4+
import org.objectweb.asm.ClassReader
45

56
object MainClass {
67

@@ -36,20 +37,27 @@ object MainClass {
3637
if (foundMainClass) nameOpt else None
3738
}
3839

40+
def findInClass(path: os.Path): Iterator[String] = {
41+
val is = os.read.inputStream(path)
42+
try {
43+
val reader = new ClassReader(is)
44+
val checker = new MainMethodChecker
45+
reader.accept(checker, 0)
46+
checker.mainClassOpt.iterator
47+
}
48+
finally is.close()
49+
}
3950
def find(output: os.Path): Seq[String] =
40-
os.walk(output)
41-
.iterator
42-
.filter(os.isFile(_))
43-
.filter(_.last.endsWith(".class"))
44-
.flatMap { path =>
45-
val is = os.read.inputStream(path)
46-
try {
47-
val reader = new asm.ClassReader(is)
48-
val checker = new MainMethodChecker
49-
reader.accept(checker, 0)
50-
checker.mainClassOpt.iterator
51-
}
52-
finally is.close()
53-
}
54-
.toVector
51+
output match {
52+
case o if os.isFile(o) && o.last.endsWith(".class") =>
53+
findInClass(o).toVector
54+
case o if os.isDir(o) =>
55+
os.walk(o)
56+
.iterator
57+
.filter(os.isFile(_))
58+
.filter(_.last.endsWith(".class"))
59+
.flatMap(findInClass)
60+
.toVector
61+
case _ => Vector.empty
62+
}
5563
}

modules/build/src/test/scala/scala/build/tests/TestInputs.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ final case class TestInputs(
4343
tmpDir,
4444
Directories.under(tmpDir / ".data"),
4545
forcedWorkspace = forcedWorkspaceOpt.map(_.resolveFrom(tmpDir)),
46-
allowRestrictedFeatures = true
46+
allowRestrictedFeatures = true,
47+
extraClasspathWasPassed = false
4748
)
4849
res match {
4950
case Left(err) => throw new Exception(err)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ object Clean extends ScalaCommand[CleanOptions] {
1717
options.directories.directories,
1818
defaultInputs = () => Inputs.default(),
1919
forcedWorkspace = options.workspace.forcedWorkspaceOpt,
20-
allowRestrictedFeatures = ScalaCli.allowRestrictedFeatures
20+
allowRestrictedFeatures = ScalaCli.allowRestrictedFeatures,
21+
extraClasspathWasPassed = false
2122
) match {
2223
case Left(message) =>
2324
System.err.println(message)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ class Default(
3434
{
3535
val shouldDefaultToRun =
3636
args.remaining.nonEmpty || options.shared.snippet.executeScript.nonEmpty ||
37-
options.shared.snippet.executeScala.nonEmpty || options.shared.snippet.executeJava.nonEmpty
37+
options.shared.snippet.executeScala.nonEmpty || options.shared.snippet.executeJava.nonEmpty ||
38+
(options.shared.extraJarsAndClasspath.nonEmpty && options.sharedRun.mainClass.mainClass.nonEmpty)
3839
if shouldDefaultToRun then RunOptions.parser else ReplOptions.parser
3940
}.parse(rawArgs) match
4041
case Left(e) => error(e)

modules/cli/src/main/scala/scala/cli/commands/util/SharedOptionsUtil.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ object SharedOptionsUtil extends CommandHelpers {
6161
scriptSnippetList: List[String],
6262
scalaSnippetList: List[String],
6363
javaSnippetList: List[String],
64-
enableMarkdown: Boolean = false
64+
enableMarkdown: Boolean = false,
65+
extraClasspathWasPassed: Boolean = false
6566
): Either[BuildException, Inputs] = {
6667
val resourceInputs = resourceDirs
6768
.map(os.Path(_, Os.pwd))
@@ -84,7 +85,8 @@ object SharedOptionsUtil extends CommandHelpers {
8485
acceptFds = !Properties.isWin,
8586
forcedWorkspace = forcedWorkspaceOpt,
8687
enableMarkdown = enableMarkdown,
87-
allowRestrictedFeatures = ScalaCli.allowRestrictedFeatures
88+
allowRestrictedFeatures = ScalaCli.allowRestrictedFeatures,
89+
extraClasspathWasPassed = extraClasspathWasPassed
8890
)
8991
maybeInputs.map { inputs =>
9092
val forbiddenDirs =
@@ -367,7 +369,8 @@ object SharedOptionsUtil extends CommandHelpers {
367369
scriptSnippetList = allScriptSnippets,
368370
scalaSnippetList = allScalaSnippets,
369371
javaSnippetList = allJavaSnippets,
370-
enableMarkdown = v.markdown.enableMarkdown
372+
enableMarkdown = v.markdown.enableMarkdown,
373+
extraClasspathWasPassed = v.extraJarsAndClasspath.nonEmpty
371374
)
372375

373376
def allScriptSnippets: List[String] = v.snippet.scriptSnippet ++ v.snippet.executeScript

modules/integration/src/test/scala/scala/cli/integration/DefaultTests.scala

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class DefaultTests extends ScalaCliSuite {
66
test("running scala-cli with no args should default to repl") {
77
TestInputs.empty.fromRoot { root =>
88
val res = os.proc(TestUtil.cli, "--repl-dry-run").call(cwd = root, mergeErrIntoOut = true)
9-
expect(res.out.trim() == "Dry run, not running REPL.")
9+
expect(res.out.trim() == replDryRunOutput)
1010
}
1111
}
1212
test("running scala-cli with no args should not accept run-only options") {
@@ -94,11 +94,66 @@ class DefaultTests extends ScalaCliSuite {
9494
}
9595
}
9696

97+
test("default to the run sub-command if -classpath and --main-class are passed") {
98+
val expectedOutput = "Hello"
99+
val mainClassName = "Main"
100+
TestInputs(
101+
os.rel / s"$mainClassName.scala" -> s"""object $mainClassName extends App { println("$expectedOutput") }"""
102+
).fromRoot { (root: os.Path) =>
103+
val compilationOutputDir = os.rel / "compilationOutput"
104+
// first, precompile to an explicitly specified output directory with -d
105+
os.proc(
106+
TestUtil.cli,
107+
".",
108+
"-d",
109+
compilationOutputDir
110+
).call(cwd = root)
111+
112+
// next, run while relying on the pre-compiled class instead of passing inputs
113+
val runRes = os.proc(
114+
TestUtil.cli,
115+
"--main-class",
116+
mainClassName,
117+
"-classpath",
118+
(os.rel / compilationOutputDir).toString
119+
).call(cwd = root)
120+
expect(runRes.out.trim == expectedOutput)
121+
}
122+
}
123+
124+
test("default to the repl sub-command if -classpath is passed, but --main-class isn't") {
125+
val expectedOutput = "Hello"
126+
val mainClassName = "Main"
127+
TestInputs(
128+
os.rel / s"$mainClassName.scala" -> s"""object $mainClassName extends App { println("$expectedOutput") }"""
129+
).fromRoot { (root: os.Path) =>
130+
val compilationOutputDir = os.rel / "compilationOutput"
131+
// first, precompile to an explicitly specified output directory with -d
132+
os.proc(
133+
TestUtil.cli,
134+
".",
135+
"-d",
136+
compilationOutputDir
137+
).call(cwd = root)
138+
139+
// next, run the repl while relying on the pre-compiled classes
140+
val runRes = os.proc(
141+
TestUtil.cli,
142+
"--repl-dry-run",
143+
"-classpath",
144+
(os.rel / compilationOutputDir).toString
145+
).call(cwd = root, mergeErrIntoOut = true)
146+
expect(runRes.out.trim == replDryRunOutput)
147+
}
148+
}
149+
97150
private def unrecognizedArgMessage(argName: String) =
98151
s"""
99152
|Unrecognized argument: $argName
100153
|
101154
|To list all available options, run
102155
| ${Console.BOLD}${TestUtil.detectCliPath} --help${Console.RESET}
103156
|""".stripMargin.trim
157+
158+
private lazy val replDryRunOutput = "Dry run, not running REPL."
104159
}

modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,34 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
23132313
}
23142314
}
23152315

2316+
test("run main class from -classpath even when no explicit inputs are passed") {
2317+
val expectedOutput = "Hello"
2318+
TestInputs(
2319+
os.rel / "Main.scala" -> s"""object Main extends App { println("$expectedOutput") }"""
2320+
).fromRoot { (root: os.Path) =>
2321+
val compilationOutputDir = os.rel / "compilationOutput"
2322+
// first, precompile to an explicitly specified output directory with -d
2323+
os.proc(
2324+
TestUtil.cli,
2325+
"compile",
2326+
".",
2327+
"-d",
2328+
compilationOutputDir,
2329+
extraOptions
2330+
).call(cwd = root)
2331+
2332+
// next, run while relying on the pre-compiled class instead of passing inputs
2333+
val runRes = os.proc(
2334+
TestUtil.cli,
2335+
"run",
2336+
"-classpath",
2337+
(os.rel / compilationOutputDir).toString,
2338+
extraOptions
2339+
).call(cwd = root)
2340+
expect(runRes.out.trim == expectedOutput)
2341+
}
2342+
}
2343+
23162344
if (actualScalaVersion.startsWith("3"))
23172345
test("should throw exception for code compiled by scala 3.1.3") {
23182346
val exceptionMsg = "Throw exception in Scala"

0 commit comments

Comments
 (0)