Skip to content

Commit a7ae389

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 0d6be65 commit a7ae389

File tree

7 files changed

+266
-50
lines changed

7 files changed

+266
-50
lines changed

Jenkinsfile

Lines changed: 8 additions & 9 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 \
@@ -381,8 +381,7 @@ def Tasks = [
381381
sbt ++2.11.12 compiler/publishLocal library/publishLocal \
382382
testInterface/publishLocal testBridge/publishLocal \
383383
jUnitPlugin/publishLocal jUnitRuntime/publishLocal &&
384-
sbt ++$toolsscala \
385-
ir/publishLocal logging/publishLocal \
384+
sbt ir/publishLocal logging/publishLocal \
386385
linkerInterface/publishLocal \
387386
linker/publishLocal jsEnvs/publishLocal \
388387
nodeJSEnv/publishLocal testAdapter/publishLocal \
@@ -455,7 +454,7 @@ allJavaVersions.each { javaVersion ->
455454
quickMatrix.add([task: "tools", scala: "2.11.12", java: javaVersion])
456455
}
457456
quickMatrix.add([task: "partestc", scala: "2.12.1", java: mainJavaVersion])
458-
quickMatrix.add([task: "sbtplugin-test", toolsscala: "2.12.8", java: mainJavaVersion])
457+
quickMatrix.add([task: "sbtplugin-test", java: mainJavaVersion])
459458

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

project/Build.scala

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

107+
scalaJSLinkerImpl := {
108+
val v = (scalaVersion in Build.linker).value
109+
assert(v.startsWith("2.12."), "The linker is configured with a Scala " +
110+
s"version ($v) that is not compatible with this build (2.12.x). ")
111+
LinkerImpl.default(Attributed.data((fullClasspath in (Build.linker, Runtime)).value))
112+
},
113+
107114
// Link source maps to GitHub sources
108115
addScalaJSCompilerOption(Def.setting {
109116
"mapSourceURI:" +
@@ -201,8 +208,8 @@ object Build {
201208
}
202209

203210
val commonSettings = Seq(
204-
scalaVersion := "2.12.8",
205211
organization := "org.scala-js",
212+
scalaVersion := "2.12.8",
206213
version := scalaJSVersion,
207214

208215
crossScalaVersions := Seq(
@@ -412,6 +419,10 @@ object Build {
412419
scalacOptions += "-Xfatal-warnings"
413420
)
414421

422+
val freezeVersionForToolsSettings = Def.settings(
423+
crossScalaVersions := Seq("2.12.8")
424+
)
425+
415426
private def publishToBintraySettings = Def.settings(
416427
publishTo := {
417428
val proj = bintrayProjectName.value
@@ -495,7 +506,7 @@ object Build {
495506
if (v < 8)
496507
throw new MessageOnlyException("This build requires JDK 8 or later. Aborting.")
497508
v
498-
}
509+
},
499510
)
500511

501512
lazy val root: Project = Project(id = "scalajs", base = file(".")).settings(
@@ -554,6 +565,7 @@ object Build {
554565

555566
lazy val irProject: Project = Project(id = "ir", base = file("ir")).settings(
556567
commonIrProjectSettings,
568+
freezeVersionForToolsSettings,
557569
libraryDependencies +=
558570
"com.novocode" % "junit-interface" % "0.9" % "test"
559571
)
@@ -638,7 +650,8 @@ object Build {
638650
)
639651

640652
lazy val logging: Project = (project in file("logging/jvm")).settings(
641-
commonLoggingSettings
653+
commonLoggingSettings,
654+
freezeVersionForToolsSettings,
642655
)
643656

644657
lazy val loggingJS: Project = (project in file("logging/js")).enablePlugins(
@@ -670,6 +683,7 @@ object Build {
670683

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

675689
lazy val linkerInterfaceJS: Project = (project in file("linker-interface/js")).settings(
@@ -711,6 +725,7 @@ object Build {
711725

712726
lazy val linker: Project = (project in file("linker/jvm")).settings(
713727
commonLinkerSettings,
728+
freezeVersionForToolsSettings,
714729
libraryDependencies ++= Seq(
715730
"com.google.javascript" % "closure-compiler" % "v20190513",
716731
"com.novocode" % "junit-interface" % "0.9" % "test"
@@ -815,7 +830,7 @@ object Build {
815830

816831
sbtJars.map(_.data -> docUrl).toMap
817832
}
818-
).dependsOn(linker, jsEnvs, nodeJSEnv, testAdapter)
833+
).dependsOn(linkerInterface, jsEnvs, nodeJSEnv, testAdapter)
819834

820835
lazy val delambdafySetting = {
821836
scalacOptions ++= (

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 eagery 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)