Skip to content

Commit 7b6852f

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 c006423 commit 7b6852f

File tree

7 files changed

+251
-39
lines changed

7 files changed

+251
-39
lines changed

project/Build.scala

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

108+
scalaJSLinkerImpl := {
109+
val cp = (fullClasspath in (Build.linker.v("2.12"), Runtime)).value
110+
LinkerImpl.default(Attributed.data(cp))
111+
},
112+
108113
// Link source maps to GitHub sources
109114
addScalaJSCompilerOption(Def.setting {
110115
"mapSourceURI:" +
@@ -858,7 +863,7 @@ object Build {
858863

859864
sbtJars.map(_.data -> docUrl).toMap
860865
}
861-
).dependsOn(linker.v("2.12"), jsEnvs.v("2.12"), nodeJSEnv.v("2.12"), testAdapter.v("2.12"))
866+
).dependsOn(linkerInterface.v("2.12"), jsEnvs.v("2.12"), nodeJSEnv.v("2.12"), testAdapter.v("2.12"))
862867

863868
lazy val delambdafySetting = {
864869
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 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+
}

sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPlugin.scala

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,17 @@ object ScalaJSPlugin extends AutoPlugin {
5757

5858
// All our public-facing keys
5959

60-
val scalaJSIRCache = SettingKey[IRFileCache.Cache](
61-
"scalaJSIRCache",
62-
"Scala.js internal: Task to access a cache.", KeyRanks.Invisible)
60+
val scalaJSIRCacheBox = SettingKey[CacheBox[IRFileCache.Cache]](
61+
"scalaJSIRCacheBox",
62+
"Scala.js internal: CacheBox for a cache.", KeyRanks.Invisible)
6363

64-
/** Persisted instance of the Scala.js linker.
64+
val scalaJSGlobalIRCacheBox = SettingKey[CacheBox[IRFileCache]](
65+
"scalaJSGlobalIRCacheBox",
66+
"Scala.js internal: CacheBox for the global cache.", KeyRanks.Invisible)
67+
68+
/** Instance of the Scala.js linker.
6569
*
66-
* This setting must be scoped per project, configuration, and stage task
70+
* This task must be scoped per project, configuration, and stage task
6771
* (`fastOptJS` or `fullOptJS`).
6872
*
6973
* If a task uses the `link` method of the `ClearableLinker`, it must be
@@ -82,9 +86,22 @@ object ScalaJSPlugin extends AutoPlugin {
8286
* }.tag(usesLinkerTag)
8387
* }.value,
8488
* }}}
89+
*
90+
* Do not set this value. Instead, set [[scalaJSLinkerImpl]]. This will
91+
* automatically set up the correct caching behavior.
8592
*/
86-
val scalaJSLinker = SettingKey[ClearableLinker]("scalaJSLinker",
87-
"Persisted instance of the Scala.js linker", KeyRanks.Invisible)
93+
val scalaJSLinker = TaskKey[ClearableLinker]("scalaJSLinker",
94+
"Access task for a Scala.js linker. Use this if you want to use the linker.",
95+
KeyRanks.Invisible)
96+
97+
val scalaJSLinkerImpl = TaskKey[LinkerImpl]("scalaJSLinkerImpl",
98+
"Implementation of the Scala.js linker to use: By default, this is " +
99+
"reflectively loading the standard linker implementation. Users may " +
100+
"set this to provide custom linker implementations.",
101+
KeyRanks.Invisible)
102+
103+
val scalaJSLinkerBox = SettingKey[CacheBox[ClearableLinker]]("scalaJSLinkerBox",
104+
"Scala.js internal: CacheBox for a Scala.js linker", KeyRanks.Invisible)
88105

89106
/** A tag to indicate that a task is using the value of [[scalaJSLinker]]
90107
* and its `link` method.
@@ -177,19 +194,40 @@ object ScalaJSPlugin extends AutoPlugin {
177194

178195
scalaJSLinkerConfig := StandardConfig(),
179196

197+
scalaJSLinkerImpl := {
198+
val s = streams.value
199+
val log = s.log
200+
val retrieveDir = s.cacheDirectory / "scalajs-linker" / scalaJSVersion
201+
val lm = {
202+
import sbt.librarymanagement.ivy._
203+
val ivyConfig = InlineIvyConfiguration()
204+
.withResolvers(Vector(Resolver.defaultLocal, Resolver.mavenCentral))
205+
.withLog(log)
206+
IvyDependencyResolution(ivyConfig)
207+
}
208+
lm.retrieve(
209+
"org.scala-js" % "scalajs-linker_2.12" % scalaJSVersion,
210+
scalaModuleInfo = None, retrieveDir, log)
211+
.fold(w => throw w.resolveException, LinkerImpl.default _)
212+
},
213+
214+
scalaJSGlobalIRCacheBox := new CacheBox,
215+
180216
jsEnv := new NodeJSEnv(),
181217

182218
// Clear the IR cache stats every time a sequence of tasks ends
183219
onComplete := {
184-
import ScalaJSPluginInternal.globalIRCache
185-
186220
val prev = onComplete.value
221+
val globalIRCacheBox = scalaJSGlobalIRCacheBox.value
187222

188223
{ () =>
189224
prev()
190225
ScalaJSPluginInternal.closeAllTestAdapters()
191-
sLog.value.debug("Global IR cache stats: " + globalIRCache.stats.logLine)
192-
globalIRCache.clearStats()
226+
227+
for (irCache <- globalIRCacheBox) {
228+
sLog.value.debug("Global IR cache stats: " + irCache.stats.logLine)
229+
irCache.clearStats()
230+
}
193231
}
194232
},
195233

0 commit comments

Comments
 (0)