Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use external binary to extract class name from stdin Java sources #1068

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 8 additions & 36 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 All @@ -414,8 +414,8 @@ class Core(val crossScalaVersion: String) extends BuildLikeModule {

class Directives(val crossScalaVersion: String) extends BuildLikeModule {
def moduleDeps = Seq(
`options`(),
`core`()
options(),
core()
)
def scalacOptions = T {
super.scalacOptions() ++ asyncScalacOptions(scalaVersion())
Expand Down Expand Up @@ -470,7 +470,7 @@ class Directives(val crossScalaVersion: String) extends BuildLikeModule {

class Options(val crossScalaVersion: String) extends BuildLikeModule {
def moduleDeps = Seq(
`core`(),
core(),
`build-macros`()
)
def scalacOptions = T {
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 All @@ -550,7 +522,7 @@ class Scala3Graal(val crossScalaVersion: String) extends BuildLikeModule {
}
}

trait Scala3GraalProcessor extends ScalaModule {
trait Scala3GraalProcessor extends ScalaModule with ScalaCliPublishModule {
def moduleDeps = Seq(`scala3-graal`(Scala.scala3))
def scalaVersion = Scala.scala3
def finalMainClass = "scala.cli.graal.CoursierCacheProcessor"
Expand All @@ -559,10 +531,9 @@ trait Scala3GraalProcessor extends ScalaModule {
class Build(val crossScalaVersion: String) extends BuildLikeModule {
def millSourcePath = super.millSourcePath / os.up / "build"
def moduleDeps = Seq(
`options`(),
options(),
scalaparse,
javaparse,
`directives`(),
directives(),
`scala-cli-bsp`,
`test-runner`(),
`tasty-lib`()
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
}
Loading