Skip to content

Commit

Permalink
Use external binary to extract class name from stdin Java sources
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed Jun 6, 2022
1 parent 5ab04a5 commit bfc96d6
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 121 deletions.
32 changes: 2 additions & 30 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ object `cli-options` extends CliOptions
object `build-macros` extends Cross[BuildMacros](Scala.mainVersions: _*)
object options extends Cross[Options](Scala.mainVersions: _*)
object scalaparse extends ScalaParse
object javaparse extends JavaParse
object directives extends Cross[Directives](Scala.mainVersions: _*)
object core extends Cross[Core](Scala.mainVersions: _*)
object `build-module` extends Cross[Build](Scala.mainVersions: _*)
Expand Down Expand Up @@ -400,6 +399,7 @@ class Core(val crossScalaVersion: String) extends BuildLikeModule {
| def defaultGraalVMVersion = "${deps.graalVmVersion}"
|
| def scalaCliSigningVersion = "${Deps.signingCli.dep.version}"
| def javaClassNameVersion = "${Deps.javaClassName.dep.version}"
|
| def libsodiumVersion = "${deps.libsodiumVersion}"
| def libsodiumjniVersion = "${Deps.libsodiumjni.dep.version}"
Expand Down Expand Up @@ -498,34 +498,6 @@ trait ScalaParse extends SbtModule with ScalaCliPublishModule with ScalaCliCompi
def scalaVersion = Scala.scala213
}

trait JavaParse extends SbtModule with ScalaCliPublishModule with ScalaCliCompile {
def ivyDeps = super.ivyDeps() ++ Agg(Deps.scala3Compiler(scalaVersion()))

// pin scala3-library suffix, so that 2.13 modules can have us as moduleDep fine
def mandatoryIvyDeps = T {
super.mandatoryIvyDeps().map { dep =>
val isScala3Lib =
dep.dep.module.organization.value == "org.scala-lang" &&
dep.dep.module.name.value == "scala3-library" &&
(dep.cross match {
case _: CrossVersion.Binary => true
case _ => false
})
if (isScala3Lib)
dep.copy(
dep = dep.dep.withModule(
dep.dep.module.withName(
coursier.ModuleName(dep.dep.module.name.value + "_3")
)
),
cross = CrossVersion.empty(dep.cross.platformed)
)
else dep
}
}
def scalaVersion = Scala.scala3
}

trait Scala3Runtime extends SbtModule with ScalaCliPublishModule with ScalaCliCompile {
def ivyDeps = super.ivyDeps()
def scalaVersion = Scala.scala3
Expand Down Expand Up @@ -561,7 +533,6 @@ class Build(val crossScalaVersion: String) extends BuildLikeModule {
def moduleDeps = Seq(
options(),
scalaparse,
javaparse,
directives(),
`scala-cli-bsp`,
`test-runner`(),
Expand All @@ -578,6 +549,7 @@ class Build(val crossScalaVersion: String) extends BuildLikeModule {
def ivyDeps = super.ivyDeps() ++ Agg(
Deps.asm,
Deps.collectionCompat,
Deps.javaClassName,
Deps.jsoniterCore,
Deps.nativeTestRunner,
Deps.osLib,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package scala.build.internal;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;

/**
* This makes [[JavaParserProxyMaker.get]] provide a [[JavaParserProxyBinary]]
* rather than a [[JavaParserProxyJvm]], from native launchers.
*
* See [[JavaParserProxyMaker]] for more details.
*/
@TargetClass(className = "scala.build.internal.JavaParserProxyMaker")
public final class JavaParserProxyMakerSubst {
@Substitute
public JavaParserProxy get(
Object archiveCache,
scala.Option<String> javaClassNameVersionOpt,
scala.build.Logger logger
) {
return new JavaParserProxyBinary(archiveCache, logger, javaClassNameVersionOpt);
}
}
4 changes: 3 additions & 1 deletion modules/build/src/main/scala/scala/build/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ object Build {
CrossSources.forInputs(
inputs,
Sources.defaultPreprocessors(
options.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper)
options.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper),
options.archiveCache,
options.internal.javaClassNameVersionOpt
),
logger
)
Expand Down
23 changes: 21 additions & 2 deletions modules/build/src/main/scala/scala/build/Sources.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package scala.build

import coursier.cache.ArchiveCache
import coursier.util.Task

import scala.build.internal.CodeWrapper
import scala.build.options.{BuildOptions, Scope}
import scala.build.preprocessing.*
Expand Down Expand Up @@ -69,10 +72,26 @@ object Sources {
topWrapperLen: Int
)

def defaultPreprocessors(codeWrapper: CodeWrapper): Seq[Preprocessor] =
/** The default preprocessor list.
*
* @param codeWrapper
* used by the Scala script preprocessor to "wrap" user code
* @param archiveCache
* used from native launchers by the Java preprocessor, to download a java-class-name binary,
* used to infer the class name of unnamed Java sources (like stdin)
* @param javaClassNameVersionOpt
* if using a java-class-name binary, the version we should download. If empty, the default
* version is downloaded.
* @return
*/
def defaultPreprocessors(
codeWrapper: CodeWrapper,
archiveCache: ArchiveCache[Task],
javaClassNameVersionOpt: Option[String]
): Seq[Preprocessor] =
Seq(
ScriptPreprocessor(codeWrapper),
JavaPreprocessor,
JavaPreprocessor(archiveCache, javaClassNameVersionOpt),
ScalaPreprocessor,
DataPreprocessor
)
Expand Down
4 changes: 3 additions & 1 deletion modules/build/src/main/scala/scala/build/bsp/BspImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ final class BspImpl(
CrossSources.forInputs(
inputs,
Sources.defaultPreprocessors(
buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper)
buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper),
buildOptions.archiveCache,
buildOptions.internal.javaClassNameVersionOpt
),
persistentLogger
).left.map((_, Scope.Main))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package scala.build.internal

import scala.build.errors.BuildException

/** Helper to get class names from Java sources
*
* See [[JavaParserProxyJvm]] for the implementation that runs things in memory using
* java-class-name from the class path, and [[JavaParserProxyBinary]] for the implementation that
* downloads and runs a java-class-name binary.
*/
trait JavaParserProxy {

/** Extracts the class name of a Java source, using the dotty Java parser.
*
* @param content
* the Java source to extract a class name from
* @return
* either some class name (if one was found) or none (if none was found), or a
* [[BuildException]]
*/
def className(content: Array[Byte]): Either[BuildException, Option[String]]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package scala.build.internal

import coursier.cache.ArchiveCache
import coursier.util.Task

import scala.build.EitherCps.{either, value}
import scala.build.Logger
import scala.build.errors.BuildException
import scala.util.Properties

/** Downloads and runs java-class-name as an external binary. */
class JavaParserProxyBinary(
archiveCache: ArchiveCache[Task],
javaClassNameVersionOpt: Option[String],
logger: Logger
) extends JavaParserProxy {

/** For internal use only
*
* Passing archiveCache as an Object, to work around issues with higher-kind type params from
* Java code.
*/
def this(
archiveCache: Object,
logger: Logger,
javaClassNameVersionOpt: Option[String]
) =
this(archiveCache.asInstanceOf[ArchiveCache[Task]], javaClassNameVersionOpt, logger)

def className(content: Array[Byte]): Either[BuildException, Option[String]] = either {

val platformSuffix = FetchExternalBinary.platformSuffix()
val version = javaClassNameVersionOpt.getOrElse(Constants.javaClassNameVersion)
val (tag, changing) =
if (version == "latest") ("nightly", true)
else ("v" + version, false)
val ext = if (Properties.isWin) ".zip" else ".gz"
val url =
s"https://github.com/scala-cli/java-class-name/releases/download/$tag/java-class-name-$platformSuffix$ext"

val binary =
value(FetchExternalBinary.fetch(url, changing, archiveCache, logger, "java-class-name"))

val source =
os.temp(content, suffix = ".java", perms = if (Properties.isWin) null else "rw-------")
val output =
try {
logger.debug(s"Running $binary $source")
val res = os.proc(binary, source).call()
res.out.text().trim
}
finally os.remove(source)

if (output.isEmpty) None
else Some(output)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package scala.build.internal

import scala.build.errors.BuildException
import scala.cli.javaclassname.JavaParser

/** A [[JavaParserProxy]] that relies on java-class-name in the class path, rather than downloading
* it and running it as an external binary.
*
* Should be used from Scala CLI when it's run on the JVM.
*/
class JavaParserProxyJvm extends JavaParserProxy {
override def className(content: Array[Byte]): Either[BuildException, Option[String]] =
Right(JavaParser.parseRootPublicClassName(content))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package scala.build.internal

import scala.build.Logger

/** On the JVM, provides [[JavaParserProxyJvm]] as [[JavaParserProxy]] instance.
*
* From native launchers, [[JavaParserProxyMakerSubst]] takes over this, and gives
* [[JavaParserProxyBinary]] instead.
*
* That way, no reference to [[JavaParserProxyJvm]] remains in the native call graph, and that
* class and those it pulls (the java-class-name classes, which includes parts of the dotty parser)
* are not embedded the native launcher.
*
* Note that this is a class and not an object, to make it easier to write substitutions for that
* in Java.
*/
class JavaParserProxyMaker {
def get(
archiveCache: Object, // Actually a ArchiveCache[Task], but having issues with the higher-kind type param from Java…
javaClassNameVersionOpt: Option[String],
logger: Logger
): JavaParserProxy =
new JavaParserProxyJvm
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
package scala.build.preprocessing

import com.virtuslab.using_directives.custom.model.UsingDirectiveKind
import coursier.cache.ArchiveCache
import coursier.util.Task

import java.nio.charset.StandardCharsets

import scala.build.EitherCps.{either, value}
import scala.build.errors.BuildException
import scala.build.internal.JavaParser
import scala.build.internal.JavaParserProxyMaker
import scala.build.options.BuildRequirements
import scala.build.preprocessing.ExtractedDirectives.from
import scala.build.preprocessing.ScalaPreprocessor._
import scala.build.{Inputs, Logger}

case object JavaPreprocessor extends Preprocessor {
/** Java source preprocessor.
*
* Doesn't modify Java sources. This only extracts using directives from them, and for unnamed
* sources (like stdin), tries to infer a class name from the sources themselves.
*
* @param archiveCache
* when using a java-class-name external binary to infer a class name (see [[JavaParserProxy]]),
* a cache to download that binary with
* @param javaClassNameVersionOpt
* when using a java-class-name external binary to infer a class name (see [[JavaParserProxy]]),
* this forces the java-class-name version to download
*/
final case class JavaPreprocessor(
archiveCache: ArchiveCache[Task],
javaClassNameVersionOpt: Option[String]
) extends Preprocessor {
def preprocess(
input: Inputs.SingleElement,
logger: Logger
Expand Down Expand Up @@ -45,27 +62,39 @@ case object JavaPreprocessor extends Preprocessor {
))
})
case v: Inputs.VirtualJavaFile =>
val relPath =
if (v.isStdin) {
val fileName = JavaParser.parseRootPublicClassName(v.content).map(
_ + ".java"
).getOrElse("stdin.java")
os.sub / fileName
}
else v.subPath
val content = new String(v.content, StandardCharsets.UTF_8)
val s = PreprocessedSource.InMemory(
originalPath = Left(v.source),
relPath = relPath,
code = content,
ignoreLen = 0,
options = None,
requirements = None,
scopedRequirements = Nil,
mainClassOpt = None,
scopePath = v.scopePath
)
Some(Right(Seq(s)))
val res = either {
val relPath =
if (v.isStdin) {
val classNameOpt = value {
(new JavaParserProxyMaker)
.get(
archiveCache,
javaClassNameVersionOpt,
logger
)
.className(v.content)
}
val fileName = classNameOpt
.map(_ + ".java")
.getOrElse("stdin.java")
os.sub / fileName
}
else v.subPath
val content = new String(v.content, StandardCharsets.UTF_8)
val s = PreprocessedSource.InMemory(
originalPath = Left(v.source),
relPath = relPath,
code = content,
ignoreLen = 0,
options = None,
requirements = None,
scopedRequirements = Nil,
mainClassOpt = None,
scopePath = v.scopePath
)
Seq(s)
}
Some(res)

case _ => None
}
Expand Down
Loading

0 comments on commit bfc96d6

Please sign in to comment.