Skip to content

Commit 1afb520

Browse files
authored
Merge pull request #1380 from Gedochao/fix-finding-main-classes-in-jars-2
Fix finding main classes in external jars
2 parents 29bc194 + 1884c18 commit 1afb520

File tree

7 files changed

+119
-19
lines changed

7 files changed

+119
-19
lines changed

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ package scala.build.internal
33
import org.objectweb.asm
44
import org.objectweb.asm.ClassReader
55

6+
import java.io.{ByteArrayInputStream, InputStream}
7+
import java.util.zip.ZipEntry
8+
9+
import scala.build.Inputs.{Element, resolve}
10+
import scala.build.internal.zip.WrappedZipInputStream
11+
612
object MainClass {
713

814
private def stringArrayDescriptor = "([Ljava/lang/String;)V"
@@ -37,26 +43,45 @@ object MainClass {
3743
if (foundMainClass) nameOpt else None
3844
}
3945

40-
def findInClass(path: os.Path): Iterator[String] = {
41-
val is = os.read.inputStream(path)
46+
def findInClass(path: os.Path): Iterator[String] =
47+
findInClass(os.read.inputStream(path))
48+
def findInClass(is: InputStream): Iterator[String] =
4249
try {
4350
val reader = new ClassReader(is)
4451
val checker = new MainMethodChecker
4552
reader.accept(checker, 0)
4653
checker.mainClassOpt.iterator
4754
}
4855
finally is.close()
56+
57+
def findInJar(path: os.Path): Iterator[String] = {
58+
val content = os.read.bytes(path)
59+
val jarInputStream = WrappedZipInputStream.create(new ByteArrayInputStream(content))
60+
jarInputStream.entries().flatMap(ent =>
61+
if !ent.isDirectory && ent.getName.endsWith(".class") then {
62+
val content = jarInputStream.readAllBytes()
63+
val inputStream = new ByteArrayInputStream(content)
64+
findInClass(inputStream)
65+
}
66+
else Iterator.empty
67+
)
4968
}
69+
5070
def find(output: os.Path): Seq[String] =
5171
output match {
5272
case o if os.isFile(o) && o.last.endsWith(".class") =>
5373
findInClass(o).toVector
74+
case o if os.isFile(o) && o.last.endsWith(".jar") =>
75+
findInJar(o).toVector
5476
case o if os.isDir(o) =>
5577
os.walk(o)
5678
.iterator
57-
.filter(os.isFile(_))
58-
.filter(_.last.endsWith(".class"))
59-
.flatMap(findInClass)
79+
.filter(os.isFile)
80+
.flatMap {
81+
case classFilePath if classFilePath.last.endsWith(".class") =>
82+
findInClass(classFilePath)
83+
case _ => Iterator.empty
84+
}
6085
.toVector
6186
case _ => Vector.empty
6287
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ final case class SharedOptions(
6565
@Name("classes")
6666
@Name("extraClasses")
6767
@Name("-classpath")
68+
@Name("-cp")
6869
@Name("classpath")
6970
@Name("classPath")
7071
@Name("extraClassPath")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class Default(
3535
val shouldDefaultToRun =
3636
args.remaining.nonEmpty || options.shared.snippet.executeScript.nonEmpty ||
3737
options.shared.snippet.executeScala.nonEmpty || options.shared.snippet.executeJava.nonEmpty ||
38-
(options.shared.extraJarsAndClasspath.nonEmpty && options.sharedRun.mainClass.mainClass.nonEmpty)
38+
(options.shared.extraJarsAndClassPath.nonEmpty && options.sharedRun.mainClass.mainClass.nonEmpty)
3939
if shouldDefaultToRun then RunOptions.parser else ReplOptions.parser
4040
}.parse(rawArgs) match
4141
case Left(e) => error(e)

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import dependency.AnyDependency
88
import dependency.parser.DependencyParser
99

1010
import java.io.{File, InputStream}
11+
import java.nio.file.Paths
1112

1213
import scala.build.EitherCps.{either, value}
1314
import scala.build.*
@@ -213,14 +214,8 @@ object SharedOptionsUtil extends CommandHelpers {
213214
runJmh = if (enableJmh) Some(true) else None
214215
),
215216
classPathOptions = bo.ClassPathOptions(
216-
extraClassPath = extraJarsAndClasspath
217-
.flatMap(_.split(File.pathSeparator).toSeq)
218-
.filter(_.nonEmpty)
219-
.map(os.Path(_, os.pwd)),
220-
extraCompileOnlyJars = extraCompileOnlyJars
221-
.flatMap(_.split(File.pathSeparator).toSeq)
222-
.filter(_.nonEmpty)
223-
.map(os.Path(_, os.pwd)),
217+
extraClassPath = extraJarsAndClassPath,
218+
extraCompileOnlyJars = extraCompileOnlyClassPath,
224219
extraRepositories = dependencies.repository.map(_.trim).filter(_.nonEmpty),
225220
extraDependencies = ShadowingSeq.from(
226221
SharedOptionsUtil.parseDependencies(
@@ -242,8 +237,28 @@ object SharedOptionsUtil extends CommandHelpers {
242237
)
243238
}
244239

245-
def extraJarsAndClasspath: List[String] =
246-
extraJars ++ scalac.scalacOption.toScalacOptShadowingSeq.getScalacOption("-classpath")
240+
extension (rawClassPath: List[String]) {
241+
def extractedClassPath: List[os.Path] =
242+
rawClassPath
243+
.flatMap(_.split(File.pathSeparator).toSeq)
244+
.filter(_.nonEmpty)
245+
.distinct
246+
.map(os.Path(_, os.pwd))
247+
.flatMap {
248+
case cp if os.isDir(cp) =>
249+
val jarsInTheDirectory =
250+
os.walk(cp)
251+
.filter(p => os.isFile(p) && p.last.endsWith(".jar"))
252+
List(cp) ++ jarsInTheDirectory // .jar paths have to be passed directly, unlike .class
253+
case cp => List(cp)
254+
}
255+
}
256+
257+
def extraJarsAndClassPath: List[os.Path] =
258+
(extraJars ++ scalac.scalacOption.toScalacOptShadowingSeq.getScalacOption("-classpath"))
259+
.extractedClassPath
260+
261+
def extraCompileOnlyClassPath: List[os.Path] = extraCompileOnlyJars.extractedClassPath
247262

248263
def globalInteractiveWasSuggested: Option[Boolean] =
249264
configDb.get(Keys.globalInteractiveWasSuggested) match {
@@ -380,7 +395,7 @@ object SharedOptionsUtil extends CommandHelpers {
380395
scalaSnippetList = allScalaSnippets,
381396
javaSnippetList = allJavaSnippets,
382397
enableMarkdown = v.markdown.enableMarkdown,
383-
extraClasspathWasPassed = v.extraJarsAndClasspath.nonEmpty
398+
extraClasspathWasPassed = v.extraJarsAndClassPath.nonEmpty
384399
)
385400

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

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2341,6 +2341,65 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
23412341
}
23422342
}
23432343

2344+
test("run main class from a jar even when no explicit inputs are passed") {
2345+
val expectedOutput = "Hello"
2346+
TestInputs(
2347+
os.rel / "Main.scala" -> s"""object Main extends App { println("$expectedOutput") }"""
2348+
).fromRoot { (root: os.Path) =>
2349+
// first, package the code to a jar with a main class
2350+
val jarPath = os.rel / "Main.jar"
2351+
os.proc(
2352+
TestUtil.cli,
2353+
"package",
2354+
".",
2355+
"--library",
2356+
"-o",
2357+
jarPath,
2358+
extraOptions
2359+
).call(cwd = root)
2360+
2361+
// next, run while relying on the jar instead of passing inputs
2362+
val runRes = os.proc(
2363+
TestUtil.cli,
2364+
"run",
2365+
"-classpath",
2366+
jarPath,
2367+
extraOptions
2368+
).call(cwd = root)
2369+
expect(runRes.out.trim == expectedOutput)
2370+
}
2371+
}
2372+
2373+
test("run main class from a jar in a directory even when no explicit inputs are passed") {
2374+
val expectedOutput = "Hello"
2375+
TestInputs(
2376+
os.rel / "Main.scala" -> s"""object Main extends App { println("$expectedOutput") }"""
2377+
).fromRoot { (root: os.Path) =>
2378+
// first, package the code to a jar with a main class
2379+
val jarParentDirectory = os.rel / "out"
2380+
val jarPath = jarParentDirectory / "Main.jar"
2381+
os.proc(
2382+
TestUtil.cli,
2383+
"package",
2384+
".",
2385+
"--library",
2386+
"-o",
2387+
jarPath,
2388+
extraOptions
2389+
).call(cwd = root)
2390+
2391+
// next, run while relying on the jar instead of passing inputs
2392+
val runRes = os.proc(
2393+
TestUtil.cli,
2394+
"run",
2395+
"-cp",
2396+
jarParentDirectory,
2397+
extraOptions
2398+
).call(cwd = root)
2399+
expect(runRes.out.trim == expectedOutput)
2400+
}
2401+
}
2402+
23442403
if (actualScalaVersion.startsWith("3"))
23452404
test("should throw exception for code compiled by scala 3.1.3") {
23462405
val exceptionMsg = "Throw exception in Scala"

website/docs/reference/cli-options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ Show help for scalac. This is an alias for --scalac-option -help
12321232

12331233
### `--extra-jars`
12341234

1235-
Aliases: `--jar`, `--jars`, `--extra-jar`, `--class`, `--extra-class`, `--classes`, `--extra-classes`, `-classpath`, `--classpath`, `--class-path`, `--extra-class-path`
1235+
Aliases: `--jar`, `--jars`, `--extra-jar`, `--class`, `--extra-class`, `--classes`, `--extra-classes`, `-classpath`, `-cp`, `--classpath`, `--class-path`, `--extra-class-path`
12361236

12371237
Add extra JARs and compiled classes to the class path
12381238

website/docs/reference/scala-command/cli-options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ Show help for scalac. This is an alias for --scalac-option -help
642642

643643
### `--extra-jars`
644644

645-
Aliases: `--jar`, `--jars`, `--extra-jar`, `--class`, `--extra-class`, `--classes`, `--extra-classes`, `-classpath`, `--classpath`, `--class-path`, `--extra-class-path`
645+
Aliases: `--jar`, `--jars`, `--extra-jar`, `--class`, `--extra-class`, `--classes`, `--extra-classes`, `-classpath`, `-cp`, `--classpath`, `--class-path`, `--extra-class-path`
646646

647647
Add extra JARs and compiled classes to the class path
648648

0 commit comments

Comments
 (0)