Skip to content

Commit bfc96d6

Browse files
Use external binary to extract class name from stdin Java sources
1 parent 5ab04a5 commit bfc96d6

File tree

17 files changed

+269
-121
lines changed

17 files changed

+269
-121
lines changed

build.sc

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ object `cli-options` extends CliOptions
7171
object `build-macros` extends Cross[BuildMacros](Scala.mainVersions: _*)
7272
object options extends Cross[Options](Scala.mainVersions: _*)
7373
object scalaparse extends ScalaParse
74-
object javaparse extends JavaParse
7574
object directives extends Cross[Directives](Scala.mainVersions: _*)
7675
object core extends Cross[Core](Scala.mainVersions: _*)
7776
object `build-module` extends Cross[Build](Scala.mainVersions: _*)
@@ -400,6 +399,7 @@ class Core(val crossScalaVersion: String) extends BuildLikeModule {
400399
| def defaultGraalVMVersion = "${deps.graalVmVersion}"
401400
|
402401
| def scalaCliSigningVersion = "${Deps.signingCli.dep.version}"
402+
| def javaClassNameVersion = "${Deps.javaClassName.dep.version}"
403403
|
404404
| def libsodiumVersion = "${deps.libsodiumVersion}"
405405
| def libsodiumjniVersion = "${Deps.libsodiumjni.dep.version}"
@@ -498,34 +498,6 @@ trait ScalaParse extends SbtModule with ScalaCliPublishModule with ScalaCliCompi
498498
def scalaVersion = Scala.scala213
499499
}
500500

501-
trait JavaParse extends SbtModule with ScalaCliPublishModule with ScalaCliCompile {
502-
def ivyDeps = super.ivyDeps() ++ Agg(Deps.scala3Compiler(scalaVersion()))
503-
504-
// pin scala3-library suffix, so that 2.13 modules can have us as moduleDep fine
505-
def mandatoryIvyDeps = T {
506-
super.mandatoryIvyDeps().map { dep =>
507-
val isScala3Lib =
508-
dep.dep.module.organization.value == "org.scala-lang" &&
509-
dep.dep.module.name.value == "scala3-library" &&
510-
(dep.cross match {
511-
case _: CrossVersion.Binary => true
512-
case _ => false
513-
})
514-
if (isScala3Lib)
515-
dep.copy(
516-
dep = dep.dep.withModule(
517-
dep.dep.module.withName(
518-
coursier.ModuleName(dep.dep.module.name.value + "_3")
519-
)
520-
),
521-
cross = CrossVersion.empty(dep.cross.platformed)
522-
)
523-
else dep
524-
}
525-
}
526-
def scalaVersion = Scala.scala3
527-
}
528-
529501
trait Scala3Runtime extends SbtModule with ScalaCliPublishModule with ScalaCliCompile {
530502
def ivyDeps = super.ivyDeps()
531503
def scalaVersion = Scala.scala3
@@ -561,7 +533,6 @@ class Build(val crossScalaVersion: String) extends BuildLikeModule {
561533
def moduleDeps = Seq(
562534
options(),
563535
scalaparse,
564-
javaparse,
565536
directives(),
566537
`scala-cli-bsp`,
567538
`test-runner`(),
@@ -578,6 +549,7 @@ class Build(val crossScalaVersion: String) extends BuildLikeModule {
578549
def ivyDeps = super.ivyDeps() ++ Agg(
579550
Deps.asm,
580551
Deps.collectionCompat,
552+
Deps.javaClassName,
581553
Deps.jsoniterCore,
582554
Deps.nativeTestRunner,
583555
Deps.osLib,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package scala.build.internal;
2+
3+
import com.oracle.svm.core.annotate.Substitute;
4+
import com.oracle.svm.core.annotate.TargetClass;
5+
6+
/**
7+
* This makes [[JavaParserProxyMaker.get]] provide a [[JavaParserProxyBinary]]
8+
* rather than a [[JavaParserProxyJvm]], from native launchers.
9+
*
10+
* See [[JavaParserProxyMaker]] for more details.
11+
*/
12+
@TargetClass(className = "scala.build.internal.JavaParserProxyMaker")
13+
public final class JavaParserProxyMakerSubst {
14+
@Substitute
15+
public JavaParserProxy get(
16+
Object archiveCache,
17+
scala.Option<String> javaClassNameVersionOpt,
18+
scala.build.Logger logger
19+
) {
20+
return new JavaParserProxyBinary(archiveCache, logger, javaClassNameVersionOpt);
21+
}
22+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ object Build {
152152
CrossSources.forInputs(
153153
inputs,
154154
Sources.defaultPreprocessors(
155-
options.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper)
155+
options.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper),
156+
options.archiveCache,
157+
options.internal.javaClassNameVersionOpt
156158
),
157159
logger
158160
)

modules/build/src/main/scala/scala/build/Sources.scala

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package scala.build
22

3+
import coursier.cache.ArchiveCache
4+
import coursier.util.Task
5+
36
import scala.build.internal.CodeWrapper
47
import scala.build.options.{BuildOptions, Scope}
58
import scala.build.preprocessing.*
@@ -69,10 +72,26 @@ object Sources {
6972
topWrapperLen: Int
7073
)
7174

72-
def defaultPreprocessors(codeWrapper: CodeWrapper): Seq[Preprocessor] =
75+
/** The default preprocessor list.
76+
*
77+
* @param codeWrapper
78+
* used by the Scala script preprocessor to "wrap" user code
79+
* @param archiveCache
80+
* used from native launchers by the Java preprocessor, to download a java-class-name binary,
81+
* used to infer the class name of unnamed Java sources (like stdin)
82+
* @param javaClassNameVersionOpt
83+
* if using a java-class-name binary, the version we should download. If empty, the default
84+
* version is downloaded.
85+
* @return
86+
*/
87+
def defaultPreprocessors(
88+
codeWrapper: CodeWrapper,
89+
archiveCache: ArchiveCache[Task],
90+
javaClassNameVersionOpt: Option[String]
91+
): Seq[Preprocessor] =
7392
Seq(
7493
ScriptPreprocessor(codeWrapper),
75-
JavaPreprocessor,
94+
JavaPreprocessor(archiveCache, javaClassNameVersionOpt),
7695
ScalaPreprocessor,
7796
DataPreprocessor
7897
)

modules/build/src/main/scala/scala/build/bsp/BspImpl.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ final class BspImpl(
6161
CrossSources.forInputs(
6262
inputs,
6363
Sources.defaultPreprocessors(
64-
buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper)
64+
buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper),
65+
buildOptions.archiveCache,
66+
buildOptions.internal.javaClassNameVersionOpt
6567
),
6668
persistentLogger
6769
).left.map((_, Scope.Main))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package scala.build.internal
2+
3+
import scala.build.errors.BuildException
4+
5+
/** Helper to get class names from Java sources
6+
*
7+
* See [[JavaParserProxyJvm]] for the implementation that runs things in memory using
8+
* java-class-name from the class path, and [[JavaParserProxyBinary]] for the implementation that
9+
* downloads and runs a java-class-name binary.
10+
*/
11+
trait JavaParserProxy {
12+
13+
/** Extracts the class name of a Java source, using the dotty Java parser.
14+
*
15+
* @param content
16+
* the Java source to extract a class name from
17+
* @return
18+
* either some class name (if one was found) or none (if none was found), or a
19+
* [[BuildException]]
20+
*/
21+
def className(content: Array[Byte]): Either[BuildException, Option[String]]
22+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package scala.build.internal
2+
3+
import coursier.cache.ArchiveCache
4+
import coursier.util.Task
5+
6+
import scala.build.EitherCps.{either, value}
7+
import scala.build.Logger
8+
import scala.build.errors.BuildException
9+
import scala.util.Properties
10+
11+
/** Downloads and runs java-class-name as an external binary. */
12+
class JavaParserProxyBinary(
13+
archiveCache: ArchiveCache[Task],
14+
javaClassNameVersionOpt: Option[String],
15+
logger: Logger
16+
) extends JavaParserProxy {
17+
18+
/** For internal use only
19+
*
20+
* Passing archiveCache as an Object, to work around issues with higher-kind type params from
21+
* Java code.
22+
*/
23+
def this(
24+
archiveCache: Object,
25+
logger: Logger,
26+
javaClassNameVersionOpt: Option[String]
27+
) =
28+
this(archiveCache.asInstanceOf[ArchiveCache[Task]], javaClassNameVersionOpt, logger)
29+
30+
def className(content: Array[Byte]): Either[BuildException, Option[String]] = either {
31+
32+
val platformSuffix = FetchExternalBinary.platformSuffix()
33+
val version = javaClassNameVersionOpt.getOrElse(Constants.javaClassNameVersion)
34+
val (tag, changing) =
35+
if (version == "latest") ("nightly", true)
36+
else ("v" + version, false)
37+
val ext = if (Properties.isWin) ".zip" else ".gz"
38+
val url =
39+
s"https://github.com/scala-cli/java-class-name/releases/download/$tag/java-class-name-$platformSuffix$ext"
40+
41+
val binary =
42+
value(FetchExternalBinary.fetch(url, changing, archiveCache, logger, "java-class-name"))
43+
44+
val source =
45+
os.temp(content, suffix = ".java", perms = if (Properties.isWin) null else "rw-------")
46+
val output =
47+
try {
48+
logger.debug(s"Running $binary $source")
49+
val res = os.proc(binary, source).call()
50+
res.out.text().trim
51+
}
52+
finally os.remove(source)
53+
54+
if (output.isEmpty) None
55+
else Some(output)
56+
}
57+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package scala.build.internal
2+
3+
import scala.build.errors.BuildException
4+
import scala.cli.javaclassname.JavaParser
5+
6+
/** A [[JavaParserProxy]] that relies on java-class-name in the class path, rather than downloading
7+
* it and running it as an external binary.
8+
*
9+
* Should be used from Scala CLI when it's run on the JVM.
10+
*/
11+
class JavaParserProxyJvm extends JavaParserProxy {
12+
override def className(content: Array[Byte]): Either[BuildException, Option[String]] =
13+
Right(JavaParser.parseRootPublicClassName(content))
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package scala.build.internal
2+
3+
import scala.build.Logger
4+
5+
/** On the JVM, provides [[JavaParserProxyJvm]] as [[JavaParserProxy]] instance.
6+
*
7+
* From native launchers, [[JavaParserProxyMakerSubst]] takes over this, and gives
8+
* [[JavaParserProxyBinary]] instead.
9+
*
10+
* That way, no reference to [[JavaParserProxyJvm]] remains in the native call graph, and that
11+
* class and those it pulls (the java-class-name classes, which includes parts of the dotty parser)
12+
* are not embedded the native launcher.
13+
*
14+
* Note that this is a class and not an object, to make it easier to write substitutions for that
15+
* in Java.
16+
*/
17+
class JavaParserProxyMaker {
18+
def get(
19+
archiveCache: Object, // Actually a ArchiveCache[Task], but having issues with the higher-kind type param from Java…
20+
javaClassNameVersionOpt: Option[String],
21+
logger: Logger
22+
): JavaParserProxy =
23+
new JavaParserProxyJvm
24+
}

modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
package scala.build.preprocessing
22

33
import com.virtuslab.using_directives.custom.model.UsingDirectiveKind
4+
import coursier.cache.ArchiveCache
5+
import coursier.util.Task
46

57
import java.nio.charset.StandardCharsets
68

79
import scala.build.EitherCps.{either, value}
810
import scala.build.errors.BuildException
9-
import scala.build.internal.JavaParser
11+
import scala.build.internal.JavaParserProxyMaker
1012
import scala.build.options.BuildRequirements
1113
import scala.build.preprocessing.ExtractedDirectives.from
1214
import scala.build.preprocessing.ScalaPreprocessor._
1315
import scala.build.{Inputs, Logger}
1416

15-
case object JavaPreprocessor extends Preprocessor {
17+
/** Java source preprocessor.
18+
*
19+
* Doesn't modify Java sources. This only extracts using directives from them, and for unnamed
20+
* sources (like stdin), tries to infer a class name from the sources themselves.
21+
*
22+
* @param archiveCache
23+
* when using a java-class-name external binary to infer a class name (see [[JavaParserProxy]]),
24+
* a cache to download that binary with
25+
* @param javaClassNameVersionOpt
26+
* when using a java-class-name external binary to infer a class name (see [[JavaParserProxy]]),
27+
* this forces the java-class-name version to download
28+
*/
29+
final case class JavaPreprocessor(
30+
archiveCache: ArchiveCache[Task],
31+
javaClassNameVersionOpt: Option[String]
32+
) extends Preprocessor {
1633
def preprocess(
1734
input: Inputs.SingleElement,
1835
logger: Logger
@@ -45,27 +62,39 @@ case object JavaPreprocessor extends Preprocessor {
4562
))
4663
})
4764
case v: Inputs.VirtualJavaFile =>
48-
val relPath =
49-
if (v.isStdin) {
50-
val fileName = JavaParser.parseRootPublicClassName(v.content).map(
51-
_ + ".java"
52-
).getOrElse("stdin.java")
53-
os.sub / fileName
54-
}
55-
else v.subPath
56-
val content = new String(v.content, StandardCharsets.UTF_8)
57-
val s = PreprocessedSource.InMemory(
58-
originalPath = Left(v.source),
59-
relPath = relPath,
60-
code = content,
61-
ignoreLen = 0,
62-
options = None,
63-
requirements = None,
64-
scopedRequirements = Nil,
65-
mainClassOpt = None,
66-
scopePath = v.scopePath
67-
)
68-
Some(Right(Seq(s)))
65+
val res = either {
66+
val relPath =
67+
if (v.isStdin) {
68+
val classNameOpt = value {
69+
(new JavaParserProxyMaker)
70+
.get(
71+
archiveCache,
72+
javaClassNameVersionOpt,
73+
logger
74+
)
75+
.className(v.content)
76+
}
77+
val fileName = classNameOpt
78+
.map(_ + ".java")
79+
.getOrElse("stdin.java")
80+
os.sub / fileName
81+
}
82+
else v.subPath
83+
val content = new String(v.content, StandardCharsets.UTF_8)
84+
val s = PreprocessedSource.InMemory(
85+
originalPath = Left(v.source),
86+
relPath = relPath,
87+
code = content,
88+
ignoreLen = 0,
89+
options = None,
90+
requirements = None,
91+
scopedRequirements = Nil,
92+
mainClassOpt = None,
93+
scopePath = v.scopePath
94+
)
95+
Seq(s)
96+
}
97+
Some(res)
6998

7099
case _ => None
71100
}

0 commit comments

Comments
 (0)