Skip to content

Commit

Permalink
Scala.js: Isolate linker in a separate classloader (#629)
Browse files Browse the repository at this point in the history
- Remove Scala.js compiler dependency from mdoc
- Remove direct linker/linker-interface dependency
- Isolate linker in a separate class loader - this should address issues with conflicting IR versions on the classpath
  This introduces two new modules: jsWorkerApi (Java) and jsWorker (cross Scala)
- Auto-detect Scala.js version from the SBT plugin
- Mild cleanup of interactions with SJS linker inside the modifier
- Remove usage of deprecated linker APIs
- Updates Scala.js to 1.9.0
- Updates Scaal to 3.1.1
  • Loading branch information
keynmol authored Feb 24, 2022
1 parent 3120b1d commit 9014e2d
Show file tree
Hide file tree
Showing 26 changed files with 513 additions and 111 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- "'++2.12.12 test'"
- "'++2.12.15 test'"
- "'++2.13.8 test'"
- "'++3.1.0 test'"
- "'++3.1.1 test'"
- "scripted"
steps:
- uses: actions/checkout@v2
Expand Down
8 changes: 5 additions & 3 deletions bin/test-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ argumentsRest=${@:2}
suffix=${argumentsRest:-}

coursier fetch \
org.scalameta:mdoc_2.11:$version \
org.scalameta:mdoc_2.12:$version \
org.scalameta:mdoc_2.13:$version \
org.scalameta:mdoc_3:$version \
org.scalameta:mdoc-js_2.11:$version \
org.scalameta:mdoc-js_2.12:$version \
org.scalameta:mdoc-js_2.13:$version $suffix
org.scalameta:mdoc-js_2.13:$version \
org.scalameta:mdoc-js_3:$version \
org.scalameta:mdoc-js-interfaces:$version \
org.scalameta:mdoc-js-worker_2.12:$version \
org.scalameta:mdoc-js-worker_2.13:$version $suffix

coursier fetch \
"org.scalameta:sbt-mdoc;sbtVersion=1.0;scalaVersion=2.12:$version" \
Expand Down
73 changes: 56 additions & 17 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import scala.collection.mutable

def scala212 = "2.12.15"
def scala213 = "2.13.8"
def scala3 = "3.1.0"
def scala3 = "3.1.1"
def scala2Versions = List(scala212, scala213)
def allScalaVersions = scala2Versions :+ scala3

def scalajs = "1.7.1"
// This will work as long as mdoc has scala-js SBT plugin
def scalajs = MdocPlugin.detectScalaJSVersion
def scalajsBinaryVersion = "1"
def scalajsDom = "2.0.0"

Expand Down Expand Up @@ -108,6 +109,16 @@ lazy val sharedSettings = List(
)
)

lazy val sharedJavaSettings = List(
javacOptions ++= {
val version = System.getProperty("java.version")
if (version.startsWith("1.8"))
Seq()
else
Seq("--release", "8")
}
)

val V = new {
val scalameta = "4.4.35"
val munit = "0.7.29"
Expand Down Expand Up @@ -141,6 +152,7 @@ lazy val interfaces = project
"implNote:a:Implementation Note:"
)
)
.settings(sharedJavaSettings)

lazy val runtime = project
.settings(
Expand Down Expand Up @@ -352,7 +364,16 @@ lazy val unitJS = project
buildInfoKeys := Seq[BuildInfoKey](
"testsInputClassDirectory" -> (testsInput / Compile / classDirectory).value
),
mdocJS := Some(jsdocs)
mdocJS := Some(jsdocs),
MdocPlugin.mdocJSWorkerClasspath := {
val _ = (jsWorker / Compile / compile).value

val folders = Seq(
(jsWorker / Compile / classDirectory).value
) ++ (jsWorker / Compile / resourceDirectories).value

Some(folders)
}
)
.dependsOn(mdoc, js, testsInput, tests, unit)
.enablePlugins(BuildInfoPlugin, MdocPlugin)
Expand Down Expand Up @@ -383,7 +404,9 @@ lazy val plugin = project
publishLocal := {
publishLocal
.dependsOn(
(interfaces / publishLocal).dependsOn(localCrossPublish(List(scala212, scala213, scala3)))
(interfaces / publishLocal)
.dependsOn(jsApi / publishLocal)
.dependsOn(localCrossPublish(List(scala212, scala213, scala3)))
)
.value
},
Expand All @@ -395,22 +418,29 @@ lazy val plugin = project
)
.enablePlugins(ScriptedPlugin)

lazy val jsApi =
project
.in(file("mdoc-js-interfaces"))
.settings(moduleName := "mdoc-js-interfaces", crossPaths := false, autoScalaLibrary := false)
.settings(sharedJavaSettings)

lazy val jsWorker =
project
.in(file("mdoc-js-worker"))
.dependsOn(jsApi)
.settings(
sharedSettings,
moduleName := "mdoc-js-worker",
libraryDependencies += ("org.scala-js" %% "scalajs-linker" % scalajs % Provided) cross CrossVersion.for3Use2_13
)

lazy val js = project
.in(file("mdoc-js"))
.dependsOn(jsApi)
.settings(
sharedSettings,
moduleName := "mdoc-js",
Compile / unmanagedSourceDirectories ++= multiScalaDirectories("js").value,
libraryDependencies ++= crossSetting(
scalaVersion.value,
if2 = List(
"org.scala-js" % "scalajs-compiler" % scalajs cross CrossVersion.full,
"org.scala-js" %% "scalajs-linker" % scalajs
),
if3 = List(
"org.scala-js" %% "scalajs-linker" % scalajs cross CrossVersion.for3Use2_13
)
)
Compile / unmanagedSourceDirectories ++= multiScalaDirectories("js").value
)
.dependsOn(mdoc)

Expand All @@ -434,6 +464,15 @@ lazy val docs = project
MdocPlugin.autoImport.mdoc := (Compile / run).evaluated,
mdocJS := Some(jsdocs),
mdocJSLibraries := (jsdocs / Compile / fullOptJS / webpack).value,
MdocPlugin.mdocJSWorkerClasspath := {
val _ = (jsWorker / Compile / compile).value

val folders = Seq(
(jsWorker / Compile / classDirectory).value
) ++ (jsWorker / Compile / resourceDirectories).value

Some(folders)
},
mdocVariables := {
val stableVersion: String =
version.value.replaceFirst("\\+.*", "")
Expand All @@ -456,7 +495,7 @@ def localCrossPublish(versions: List[String]): Def.Initialize[Task[Unit]] =
.reduceLeft(_ dependsOn _)

def localCrossPublishProjects(scalaV: String): Def.Initialize[Task[Unit]] = {
val projects = List(runtime, cli, mdoc, js).reverse
val projects = List(runtime, cli, mdoc, js, jsWorker).reverse
projects
.map(p => localCrossPublishProject(p, scalaV))
.reduceLeft(_ dependsOn _)
Expand All @@ -465,7 +504,7 @@ def localCrossPublishProjects(scalaV: String): Def.Initialize[Task[Unit]] = {
def localCrossPublishProject(ref: Project, scalaV: String): Def.Initialize[Task[Unit]] =
Def.task {
val versionValue = (ThisBuild / version).value
val projects = List(runtime, cli, mdoc, js)
val projects = List(runtime, cli, mdoc, js, jsWorker)
val setttings =
(ThisBuild / version := versionValue) ::
projects.map(p => p / scalaVersion := scalaV)
Expand Down
13 changes: 13 additions & 0 deletions mdoc-js-interfaces/src/main/java/LogLevel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package mdoc.js.interfaces;

public enum LogLevel {
Debug(1), Info(2), Warning(3), Error(4);

private final int order;
private LogLevel(int order) {
this.order = order;
}
public int getOrder() {
return order;
}
}
5 changes: 5 additions & 0 deletions mdoc-js-interfaces/src/main/java/ModuleType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package mdoc.js.interfaces;

public enum ModuleType {
NoModule, ESModule, CommonJSModule;
}
42 changes: 42 additions & 0 deletions mdoc-js-interfaces/src/main/java/ScalajsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package mdoc.js.interfaces;

public class ScalajsConfig {
public ModuleType moduleType;
public boolean fullOpt;
public boolean sourceMap;
public boolean batchMode;
public boolean closureCompiler;

public ScalajsConfig() {
}

public ScalajsConfig withModuleKind(ModuleType kind) {
if(kind == ModuleType.ESModule)
this.moduleType = ModuleType.ESModule;
else if (kind == ModuleType.NoModule)
this.moduleType = ModuleType.NoModule;
else if (kind == ModuleType.CommonJSModule)
this.moduleType = ModuleType.CommonJSModule;
return this;
}

public ScalajsConfig withOptimized(boolean enabled) {
this.fullOpt = enabled;
return this;
}

public ScalajsConfig withSourceMap(boolean enabled) {
this.sourceMap = enabled;
return this;
}

public ScalajsConfig withBatchMode(boolean enabled) {
this.batchMode = enabled;
return this;
}

public ScalajsConfig withClosureCompiler(boolean enabled) {
this.closureCompiler = enabled;
return this;
}
}
6 changes: 6 additions & 0 deletions mdoc-js-interfaces/src/main/java/ScalajsLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package mdoc.js.interfaces;

public interface ScalajsLogger{
public void log(LogLevel level, String message);
public void trace(Throwable ex);
}
15 changes: 15 additions & 0 deletions mdoc-js-interfaces/src/main/java/ScalajsWorkerApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mdoc.js.interfaces;

import java.nio.file.Path;

public interface ScalajsWorkerApi {

public interface IRFile {
}

public void cache(Path[] classPath);

public java.util.Map<String, byte[]> link(IRFile[] in);

public IRFile inMemory(String path, byte[] contents);
}
5 changes: 5 additions & 0 deletions mdoc-js-interfaces/src/main/java/ScalajsWorkerProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package mdoc.js.interfaces;

public interface ScalajsWorkerProvider {
public ScalajsWorkerApi create(ScalajsConfig config, ScalajsLogger logger);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mdoc.js.worker.ScalaJSWorkerProvider
92 changes: 92 additions & 0 deletions mdoc-js-worker/src/main/scala/ScalaJSWorker.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package mdoc.js.worker;

import mdoc.js.interfaces._
import java.nio.file.Path
import org.scalajs.linker.MemOutputDirectory
import scala.concurrent.ExecutionContext.Implicits.global
import org.scalajs.linker.StandardImpl
import org.scalajs.linker.interface.StandardConfig
import org.scalajs.linker.interface.ModuleKind
import ModuleType._
import org.scalajs.linker.PathIRContainer
import org.scalajs.linker.interface.IRFile
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import java.{util => ju}
import org.scalajs.logging.Logger
import org.scalajs.logging.Level
import org.scalajs.linker.standard.MemIRFileImpl
import org.scalajs.linker.interface.Semantics

class ScalaJSWorker(
config: ScalajsConfig,
logger: Logger
) extends ScalajsWorkerApi {
case class IFile(mem: IRFile) extends ScalajsWorkerApi.IRFile

val linker = {
val cfg =
StandardConfig()
.withSemantics {
if (config.fullOpt) Semantics.Defaults.optimized
else Semantics.Defaults
}
.withBatchMode(config.batchMode)
.withClosureCompilerIfAvailable(config.closureCompiler)
.withSourceMap(config.sourceMap)
.withModuleKind {
config.moduleType match {
case NoModule => ModuleKind.NoModule
case ESModule => ModuleKind.ESModule
case CommonJSModule => ModuleKind.CommonJSModule
}
}
StandardImpl.clearableLinker(cfg)
}

var cachedFiles = Seq.empty[org.scalajs.linker.interface.IRFile]

val cache = StandardImpl.irFileCache().newCache

override def cache(x: Array[Path]): Unit =
cachedFiles = Await.result(
PathIRContainer
.fromClasspath(x.toSeq)
.map(_._1)
.flatMap(cache.cached),
Duration.Inf
)

override def link(
in: Array[ScalajsWorkerApi.IRFile]
): ju.Map[String, Array[Byte]] = {
val mem = MemOutputDirectory()
val report = Await.result(
linker.link(
cachedFiles.toSeq ++
in.toSeq.collect { case IFile(o) =>
o
},
Seq.empty,
mem,
logger
),
Duration.Inf
)

val javaMap: ju.Map[String, Array[Byte]] = new ju.HashMap[String, Array[Byte]]

report.publicModules.foreach { m =>
mem.content(m.jsFileName).foreach { content =>
javaMap.put(m.jsFileName, content)
}
}

javaMap
}

override def inMemory(path: String, contents: Array[Byte]): ScalajsWorkerApi.IRFile = IFile(
new MemIRFileImpl(path, None, contents)
)

}
30 changes: 30 additions & 0 deletions mdoc-js-worker/src/main/scala/ScalaJSWorkerProvider.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mdoc.js.worker;

import mdoc.js.{interfaces => i}

import org.scalajs.{logging => sjslogging}

class ScalaJSWorkerProvider extends i.ScalajsWorkerProvider {
def mapping(level: sjslogging.Level): i.LogLevel = {
import i.LogLevel._
import sjslogging.Level

level match {
case Level.Debug => Debug
case Level.Info => Info
case Level.Warn => Warning
case Level.Error => Error
}
}
def create(config: i.ScalajsConfig, logger: i.ScalajsLogger): i.ScalajsWorkerApi = {
val wrappedLogger = new sjslogging.Logger {
def log(level: sjslogging.Level, message: => String) =
logger.log(mapping(level), message)

def trace(ex: => Throwable) =
logger.trace(ex)
}

new ScalaJSWorker(config, wrappedLogger)
}
}
Loading

0 comments on commit 9014e2d

Please sign in to comment.