-
Notifications
You must be signed in to change notification settings - Fork 83
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
Scala.js: Isolate linker in a separate classloader #629
Conversation
5471c47
to
8f89013
Compare
As a motivating example, here's the class paths:
|
build.sbt
Outdated
// This will work as long as mdoc has scala-js SBT plugin | ||
def scalajs = { | ||
val klass = | ||
Class.forName("org.scalajs.ir.ScalaJSVersions", true, getClass.getClassLoader()) | ||
val method = klass.getMethod("current") | ||
method.invoke(null).asInstanceOf[String] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any concern of sbt loading different plugins in different classloaders? I may have encountered this but never confirmed it.
🎉 😍 |
Since the ScalaJS toolchain is available for the same Scala version as the one you're using, I think you can write the API module in Scala if you want. We use Java in Mill only when the Worker implementation uses a different Scala binary version than the Plugin. In your case everything is on Scala 2.12 |
lazy val mdocJSWorkerClasspath = taskKey[Option[Seq[File]]]( | ||
"Optional classpath to use for Mdoc.js worker - " + | ||
"if not provided, the classpath will be formed by resolving the worker dependency" | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this separate task (not part of autoImport) to make mdoc's own tests to work - we can't introduce a dependency on mdoc-js-worker, and this would be reliable than placing dependsOn(jsWorker / publishLocal)
in all the possible situations where this module must be available.
MdocJSConfiguration( | ||
scalacOptions = options.options, | ||
compileClasspath = options.classpath, | ||
linkerClassPath = getJars(linkerDependency) ++ workerClasspath, | ||
moduleKind = options.moduleKind, | ||
jsLibraries = libraries | ||
).writeTo(props) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a strictly unnecessary refactor, but it's a small step towards my goal of weaning off mdoc's build of loading MdocPlugin at build time.
Main issue is that it forces 2.12 on all the modules that are used to build the site
It also causes some weird issues when compiling the build, i.e. I currently can't resolve the deprecation warnings because the build just fails to compile when ++2.12
is used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sbt on 2.13 would be so nice.
props.put( | ||
s"js-libraries", | ||
mdocJSLibraries.value.map(_.data).mkString(File.pathSeparator) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I were to guess, this setting was extracted from the block above because SBT disallows invoking the task in anonymous lambda, and not because it's a valid usecase to not have mdoc.js configuration but still use js-libraries?
@@ -1,7 +1,7 @@ | |||
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") | |||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") | |||
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") | |||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1") | |||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upgrade Mdoc's own Scala.js to 1.9.0
mdocJS := Some(jsdocs) | ||
mdocJS := Some(jsdocs), | ||
MdocPlugin.mdocJSWorkerClasspath := { | ||
val _ = (jsWorker / Compile / compile).value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dependency on classDirectory
is not enough to trigger compilation.
The user's build will not include this setting (mdocJSWorkerClasspath
) at all, it's here only because mdoc bootstraps itself
@@ -1,2 +1,2 @@ | |||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0") | |||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This version is intended to be updated with each new release of Scala.js
@@ -0,0 +1,2 @@ | |||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Frozen tests at this Scala.js version as it's the last one that was working in the old setup of the plugin, so we want to keep it here to make sure future changes won't break older versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Just some questions from me, but nothing blocking.
.fromClasspath(x.toSeq) | ||
.map(_._1) | ||
.flatMap(cache.cached), | ||
Duration.Inf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this not cause any issues to use Duration.Inf
? Maybe we should use CompletableFuture as the result value instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest the entire JsModifier process is very synchronous and futures are always waited on, even in the current main branch.
We can always change the interface as it is internal, so I wanted to keep it simple and avoid introducing potentially configurable timeouts because its brittle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok sure!
val linking = | ||
linker.link(virtualIrFiles ++ sjsir, Nil, LinkerOutput.apply(output), sjsLogger) | ||
Await.result(linking, Duration.Inf) | ||
val linkingReport = scalajsApi.get.link(sjsirFiles.toArray) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we match on the Option instead? Or maybe throw a better exception if scalajsApi is empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍 Just a few minor comments, feel free to ignore. Thank you for looking into this! It's hairy business to fiddle with classloaders, but it's really wonderful IMO that the JVM makes it possible at all
build.sbt
Outdated
@@ -395,22 +414,30 @@ lazy val plugin = project | |||
) | |||
.enablePlugins(ScriptedPlugin) | |||
|
|||
lazy val jsApi = | |||
project | |||
.in(file("mdoc-js-api")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we name this mdoc-js-interfaces
for consistency with mtags-interfaces? (and other artifacts using the -interfaces
suffix)
ScalaJSClassloader.create(linkerClasspath.entries.map(_.toURI.toURL()).toArray) | ||
scalajsApi = Some( | ||
loader | ||
.loadClass("mdoc.js.worker.ScalaJSWorker") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use service loaders here instead of manual reflection? Service loaders have several tooling benefits, for example they are automatically understood by native-image (although that's not relevant here). https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html
import java.net.URLClassLoader | ||
|
||
final class FilteringClassLoader(parent: ClassLoader) extends ClassLoader(parent) { | ||
private val parentPrefixes = List( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the loadClass
is a fairly hot path so I'd try to avoid List
here
private val parentPrefixes = List( | |
private val parentPrefixes = Array( |
MdocJSConfiguration( | ||
scalacOptions = options.options, | ||
compileClasspath = options.classpath, | ||
linkerClassPath = getJars(linkerDependency) ++ workerClasspath, | ||
moduleKind = options.moduleKind, | ||
jsLibraries = libraries | ||
).writeTo(props) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sbt on 2.13 would be so nice.
case Level.Error => Error | ||
} | ||
} | ||
def create(config: i.ScalajsConfig, logger: i.ScalajsLogger) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing result type
.newInstance(scalajsConfig(new ScalajsConfig, config), sjsLogger) | ||
.asInstanceOf[ScalajsWorkerApi] | ||
ServiceLoader | ||
.load(classOf[ScalajsWorkerProvider], loader) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Closes #601 #617 #590