Skip to content

Commit 586d011

Browse files
committed
Fix scala-js#3661: Load linker reflectively
This gives us the following advantages: - Allows us to give resources to the linker that the build itself compiles first (unblocks scala-js#3537). - Hides linker dependencies away from other sbt plugins. Notably the Google Closure Compiler and Guava (fixes scala-js#3193).
1 parent 3a59592 commit 586d011

File tree

8 files changed

+279
-52
lines changed

8 files changed

+279
-52
lines changed

Jenkinsfile

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def Tasks = [
298298
"bootstrap": '''
299299
setJavaVersion $java
300300
npm install &&
301-
sbt ++$scala linker/test &&
301+
sbt ++$scala! linker/test &&
302302
sbt ++$scala irJS/test linkerJS/test &&
303303
sbt 'set scalaJSStage in Global := FullOptStage' \
304304
'set scalaJSStage in testSuite := FastOptStage' \
@@ -315,14 +315,14 @@ def Tasks = [
315315
"tools": '''
316316
setJavaVersion $java
317317
npm install &&
318-
sbt ++$scala ir/test logging/compile linkerInterface/compile \
318+
sbt ++$scala! ir/test logging/compile linkerInterface/compile \
319319
linker/compile jsEnvs/test nodeJSEnv/test testAdapter/test \
320320
ir/mimaReportBinaryIssues logging/mimaReportBinaryIssues \
321321
linkerInterface/mimaReportBinaryIssues linker/mimaReportBinaryIssues \
322322
jsEnvs/mimaReportBinaryIssues jsEnvsTestKit/mimaReportBinaryIssues \
323323
nodeJSEnv/mimaReportBinaryIssues \
324324
testAdapter/mimaReportBinaryIssues &&
325-
sbt ++$scala ir/compile:doc logging/compile:doc \
325+
sbt ++$scala! ir/compile:doc logging/compile:doc \
326326
linkerInterface/compile:doc \
327327
linker/compile:doc jsEnvs/compile:doc \
328328
jsEnvsTestKit/compile:doc nodeJSEnv/compile:doc \
@@ -332,7 +332,7 @@ def Tasks = [
332332
"tools-sbtplugin": '''
333333
setJavaVersion $java
334334
npm install &&
335-
sbt ++$scala ir/test logging/compile linkerInterface/compile \
335+
sbt ++$scala! ir/test logging/compile linkerInterface/compile \
336336
linker/compile jsEnvs/test nodeJSEnv/test testAdapter/test \
337337
sbtPlugin/package \
338338
ir/mimaReportBinaryIssues logging/mimaReportBinaryIssues \
@@ -341,7 +341,7 @@ def Tasks = [
341341
nodeJSEnv/mimaReportBinaryIssues \
342342
testAdapter/mimaReportBinaryIssues \
343343
sbtPlugin/mimaReportBinaryIssues &&
344-
sbt ++$scala library/scalastyle javalanglib/scalastyle javalib/scalastyle \
344+
sbt ++$scala! library/scalastyle javalanglib/scalastyle javalib/scalastyle \
345345
ir/scalastyle compiler/scalastyle \
346346
compiler/test:scalastyle \
347347
logging/scalastyle logging/test:scalastyle \
@@ -359,7 +359,7 @@ def Tasks = [
359359
jUnitPlugin/scalastyle jUnitRuntime/scalastyle \
360360
jUnitTestOutputsJVM/scalastyle jUnitTestOutputsJVM/test:scalastyle \
361361
jUnitTestOutputsJS/scalastyle jUnitTestOutputsJS/test:scalastyle &&
362-
sbt ++$scala ir/compile:doc logging/compile:doc \
362+
sbt ++$scala! ir/compile:doc logging/compile:doc \
363363
linkerInterface/compile:doc \
364364
linker/compile:doc jsEnvs/compile:doc \
365365
jsEnvsTestKit/compile:doc nodeJSEnv/compile:doc \
@@ -382,8 +382,7 @@ def Tasks = [
382382
sbt ++2.11.12 compiler/publishLocal library/publishLocal \
383383
testInterface/publishLocal testBridge/publishLocal \
384384
jUnitPlugin/publishLocal jUnitRuntime/publishLocal &&
385-
sbt ++$toolsscala \
386-
ir/publishLocal logging/publishLocal \
385+
sbt ir/publishLocal logging/publishLocal \
387386
linkerInterface/publishLocal \
388387
linker/publishLocal jsEnvs/publishLocal \
389388
nodeJSEnv/publishLocal testAdapter/publishLocal \
@@ -403,19 +402,19 @@ def Tasks = [
403402
"partest-noopt": '''
404403
setJavaVersion $java
405404
npm install &&
406-
sbt ++$scala package "partestSuite/testOnly -- --showDiff"
405+
sbt ++$scala! package "partestSuite/testOnly -- --showDiff"
407406
''',
408407

409408
"partest-fastopt": '''
410409
setJavaVersion $java
411410
npm install &&
412-
sbt ++$scala package "partestSuite/testOnly -- --fastOpt --showDiff"
411+
sbt ++$scala! package "partestSuite/testOnly -- --fastOpt --showDiff"
413412
''',
414413

415414
"partest-fullopt": '''
416415
setJavaVersion $java
417416
npm install &&
418-
sbt ++$scala package "partestSuite/testOnly -- --fullOpt --showDiff"
417+
sbt ++$scala! package "partestSuite/testOnly -- --fullOpt --showDiff"
419418
'''
420419
]
421420

@@ -456,7 +455,7 @@ allJavaVersions.each { javaVersion ->
456455
quickMatrix.add([task: "tools", scala: "2.11.12", java: javaVersion])
457456
}
458457
quickMatrix.add([task: "partestc", scala: "2.12.1", java: mainJavaVersion])
459-
quickMatrix.add([task: "sbtplugin-test", toolsscala: "2.12.8", java: mainJavaVersion])
458+
quickMatrix.add([task: "sbtplugin-test", java: mainJavaVersion])
460459

461460
// The 'full' matrix
462461
def fullMatrix = quickMatrix.clone()

project/Build.scala

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ object MyScalaJSPlugin extends AutoPlugin {
105105
jsEnv := new NodeJSEnv(
106106
NodeJSEnv.Config().withSourceMap(wantSourceMaps.value)),
107107

108+
scalaJSLinkerImpl := {
109+
val v = (scalaVersion in Build.linker).value
110+
assert(v.startsWith("2.12."), "The linker is configured with a Scala " +
111+
s"version ($v) that is not compatible with this build (2.12.x). ")
112+
LinkerImpl.default(Attributed.data((fullClasspath in (Build.linker, Runtime)).value))
113+
},
114+
108115
// Link source maps to GitHub sources
109116
addScalaJSCompilerOption(Def.setting {
110117
"mapSourceURI:" +
@@ -413,6 +420,10 @@ object Build {
413420
scalacOptions += "-Xfatal-warnings"
414421
)
415422

423+
val freezeVersionForToolsSettings = Def.settings(
424+
crossScalaVersions := Seq("2.12.8")
425+
)
426+
416427
private def publishToBintraySettings = Def.settings(
417428
publishTo := {
418429
val proj = bintrayProjectName.value
@@ -555,6 +566,7 @@ object Build {
555566

556567
lazy val irProject: Project = Project(id = "ir", base = file("ir")).settings(
557568
commonIrProjectSettings,
569+
freezeVersionForToolsSettings,
558570
libraryDependencies +=
559571
"com.novocode" % "junit-interface" % "0.9" % "test"
560572
)
@@ -639,7 +651,8 @@ object Build {
639651
)
640652

641653
lazy val logging: Project = (project in file("logging/jvm")).settings(
642-
commonLoggingSettings
654+
commonLoggingSettings,
655+
freezeVersionForToolsSettings,
643656
)
644657

645658
lazy val loggingJS: Project = (project in file("logging/js")).enablePlugins(
@@ -671,6 +684,7 @@ object Build {
671684

672685
lazy val linkerInterface: Project = (project in file("linker-interface/jvm")).settings(
673686
commonLinkerInterfaceSettings,
687+
freezeVersionForToolsSettings,
674688
).dependsOn(irProject, logging)
675689

676690
lazy val linkerInterfaceJS: Project = (project in file("linker-interface/js")).settings(
@@ -712,6 +726,7 @@ object Build {
712726

713727
lazy val linker: Project = (project in file("linker/jvm")).settings(
714728
commonLinkerSettings,
729+
freezeVersionForToolsSettings,
715730
libraryDependencies ++= Seq(
716731
"com.google.javascript" % "closure-compiler" % "v20190513",
717732
"com.novocode" % "junit-interface" % "0.9" % "test"
@@ -733,6 +748,7 @@ object Build {
733748

734749
lazy val jsEnvs: Project = (project in file("js-envs")).settings(
735750
commonSettings,
751+
freezeVersionForToolsSettings,
736752
publishSettings,
737753
fatalWarningsSettings,
738754
name := "Scala.js JS Envs",
@@ -743,6 +759,7 @@ object Build {
743759

744760
lazy val jsEnvsTestKit: Project = (project in file("js-envs-test-kit")).settings(
745761
commonSettings,
762+
freezeVersionForToolsSettings,
746763
publishSettings,
747764
fatalWarningsSettings,
748765
name := "Scala.js JS Envs Test Kit",
@@ -757,6 +774,7 @@ object Build {
757774

758775
lazy val nodeJSEnv: Project = (project in file("nodejs-env")).settings(
759776
commonSettings,
777+
freezeVersionForToolsSettings,
760778
publishSettings,
761779
fatalWarningsSettings,
762780
name := "Scala.js Node.js env",
@@ -771,6 +789,7 @@ object Build {
771789

772790
lazy val testAdapter = (project in file("test-adapter")).settings(
773791
commonSettings,
792+
freezeVersionForToolsSettings,
774793
publishSettings,
775794
fatalWarningsSettings,
776795
name := "Scala.js sbt test adapter",
@@ -848,7 +867,7 @@ object Build {
848867

849868
sbtJars.map(_.data -> docUrl).toMap
850869
}
851-
).dependsOn(linker, jsEnvs, nodeJSEnv, testAdapter)
870+
).dependsOn(linkerInterface, jsEnvs, nodeJSEnv, testAdapter)
852871

853872
lazy val delambdafySetting = {
854873
scalacOptions ++= (
@@ -1307,6 +1326,7 @@ object Build {
13071326

13081327
lazy val jUnitTestOutputsJVM = (project in file("junit-test/output-jvm")).settings(
13091328
commonJUnitTestOutputsSettings,
1329+
freezeVersionForToolsSettings,
13101330
name := "Tests for Scala.js JUnit output in JVM.",
13111331
libraryDependencies ++= Seq(
13121332
"org.scala-sbt" % "test-interface" % "1.0" % "test",
@@ -1336,6 +1356,7 @@ object Build {
13361356

13371357
lazy val jUnitAsyncJVM = (project in file("junit-async/jvm")).settings(
13381358
commonSettings,
1359+
freezeVersionForToolsSettings,
13391360
name := "Scala.js internal JUnit async JVM support",
13401361
publishArtifact in Compile := false
13411362
)

project/build.sbt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "1.0.0")
66

77
addSbtPlugin("org.portable-scala" % "sbt-platform-deps" % "1.0.0")
88

9-
libraryDependencies += "com.google.javascript" % "closure-compiler" % "v20190513"
10-
119
libraryDependencies += "com.google.jimfs" % "jimfs" % "1.1"
1210

1311
libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit.pgm" % "3.2.0.201312181205-r"
@@ -20,8 +18,6 @@ unmanagedSourceDirectories in Compile ++= {
2018
root / "logging/jvm/src/main/scala",
2119
root / "linker-interface/shared/src/main/scala",
2220
root / "linker-interface/jvm/src/main/scala",
23-
root / "linker/shared/src/main/scala",
24-
root / "linker/jvm/src/main/scala",
2521
root / "js-envs/src/main/scala",
2622
root / "nodejs-env/src/main/scala",
2723
root / "test-adapter/src/main/scala",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Scala.js (https://www.scala-js.org/)
3+
*
4+
* Copyright EPFL.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (https://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package org.scalajs.sbtplugin
14+
15+
/** A CacheBox is a support class to abuse an sbt setting as a cache.
16+
*
17+
* A CacheBox is needed, once the cached result needs to depend on a task,
18+
* since then it cannot simply be made a setting anymore.
19+
*/
20+
private[sbtplugin] final class CacheBox[T] {
21+
private[this] var value: T = _
22+
23+
def ensure(f: => T): T = synchronized {
24+
if (value == null) {
25+
value = f
26+
}
27+
value
28+
}
29+
30+
def foreach(f: T => Unit): Unit = synchronized {
31+
if (value != null) {
32+
f(value)
33+
}
34+
}
35+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Scala.js (https://www.scala-js.org/)
3+
*
4+
* Copyright EPFL.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (https://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package org.scalajs.sbtplugin
14+
15+
import scala.concurrent._
16+
17+
import java.lang.reflect.{Method, Modifier}
18+
import java.io.File
19+
import java.net.URLClassLoader
20+
import java.nio.file.Path
21+
22+
import org.scalajs.linker.interface._
23+
24+
/** Abstract implementation of a linker as needed by the sbt plugin.
25+
*
26+
* @note This trait does not guarantee full compatibility: Methods may be added
27+
* / removed in the future. Use [[LinkerImpl.Forwarding]] to override things
28+
* selectively in a compatible manner.
29+
*/
30+
trait LinkerImpl {
31+
def clearableLinker(cfg: StandardConfig): ClearableLinker
32+
33+
def irFileCache(): IRFileCache
34+
35+
def irContainers(classpath: Seq[Path])(
36+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])]
37+
38+
def outputFile(path: Path): LinkerOutput.File
39+
}
40+
41+
object LinkerImpl {
42+
/** Returns the default implementation.
43+
*
44+
* It loads a StandardLinker via reflection.
45+
*/
46+
def default(files: Seq[File]): LinkerImpl = {
47+
val urls = files.map(_.toURI.toURL).toArray
48+
val loader = new URLClassLoader(urls, new FilteringClassLoader(getClass.getClassLoader))
49+
new Reflect(loader)
50+
}
51+
52+
/** A [[LinkerImpl]] that forwards everything to `parent`.
53+
*
54+
* This is useful if only parts of the linker implementation need to be
55+
* replaced. A subclass only overriding these can be created, ensuring easier
56+
* transition when methods get added.
57+
*/
58+
class Forwarding(parent: LinkerImpl) extends LinkerImpl {
59+
def clearableLinker(cfg: StandardConfig): ClearableLinker =
60+
parent.clearableLinker(cfg)
61+
62+
def irFileCache(): IRFileCache =
63+
parent.irFileCache()
64+
65+
def irContainers(classpath: Seq[Path])(
66+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])] =
67+
parent.irContainers(classpath)
68+
69+
def outputFile(path: Path): LinkerOutput.File =
70+
parent.outputFile(path)
71+
}
72+
73+
private final class FilteringClassLoader(parent: ClassLoader)
74+
extends ClassLoader(parent) {
75+
private val parentPrefixes = List(
76+
"java.",
77+
"scala.",
78+
"org.scalajs.linker.interface.",
79+
"org.scalajs.logging.",
80+
"org.scalajs.ir."
81+
)
82+
83+
override def loadClass(name: String, resolve: Boolean): Class[_] = {
84+
if (parentPrefixes.exists(name.startsWith _))
85+
super.loadClass(name, resolve)
86+
else
87+
null
88+
}
89+
}
90+
91+
private final class Reflect(loader: ClassLoader) extends LinkerImpl {
92+
private def loadMethod(clazz: String, method: String, result: Class[_], params: Class[_]*): Method = {
93+
val m = Class.forName("org.scalajs.linker." + clazz, true, loader).getMethod(method, params: _*)
94+
require(Modifier.isStatic(m.getModifiers()))
95+
require(result.isAssignableFrom(m.getReturnType()))
96+
m
97+
}
98+
99+
private def invoke[T](method: Method, args: AnyRef*): T =
100+
method.invoke(null, args: _*).asInstanceOf[T]
101+
102+
/* We load everything eagerly to fail immediately, not only when the methods
103+
* are invoked.
104+
*/
105+
private val clearableLinkerMethod =
106+
loadMethod("StandardImpl", "clearableLinker", classOf[ClearableLinker], classOf[StandardConfig])
107+
108+
private val irFileCacheMethod =
109+
loadMethod("StandardImpl", "irFileCache", classOf[IRFileCache])
110+
111+
private val irContainersMethod = {
112+
loadMethod("PathIRContainer", "fromClasspath", classOf[Future[_]],
113+
classOf[Seq[Path]], classOf[ExecutionContext])
114+
}
115+
116+
private val outputFileMethod =
117+
loadMethod("PathOutputFile", "atomic", classOf[LinkerOutput.File], classOf[Path])
118+
119+
def clearableLinker(cfg: StandardConfig): ClearableLinker =
120+
invoke(clearableLinkerMethod, cfg)
121+
122+
def irFileCache(): IRFileCache =
123+
invoke(irFileCacheMethod)
124+
125+
def irContainers(classpath: Seq[Path])(
126+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])] = {
127+
invoke(irContainersMethod, classpath, ec)
128+
}
129+
130+
def outputFile(path: Path): LinkerOutput.File =
131+
invoke(outputFileMethod, path)
132+
}
133+
}

0 commit comments

Comments
 (0)