Skip to content

Commit 8e66c55

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 8e66c55

File tree

7 files changed

+316
-98
lines changed

7 files changed

+316
-98
lines changed

Jenkinsfile

Lines changed: 58 additions & 58 deletions
Large diffs are not rendered by default.

project/Build.scala

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,17 @@ object ExposedValues extends AutoPlugin {
4646
}
4747
}
4848

49+
val jvmScalaVersion = SettingKey[String]("jvmScalaVersion")
50+
val jsScalaVersion = SettingKey[String]("jsScalaVersion")
51+
4952
val CheckedBehavior = org.scalajs.linker.interface.CheckedBehavior
5053

5154
type NodeJSEnvForcePolyfills = build.NodeJSEnvForcePolyfills
5255
}
5356
}
5457

58+
import ExposedValues.autoImport.{jvmScalaVersion, jsScalaVersion}
59+
5560
object MyScalaJSPlugin extends AutoPlugin {
5661
override def requires: Plugins = ScalaJSPlugin
5762

@@ -97,13 +102,22 @@ object MyScalaJSPlugin extends AutoPlugin {
97102
*/
98103
crossVersion := CrossVersion.binary,
99104

105+
scalaVersion := jsScalaVersion.value,
106+
100107
scalaJSLinkerConfig ~= (_.withCheckIR(true)),
101108

102109
wantSourceMaps := true,
103110

104111
jsEnv := new NodeJSEnv(
105112
NodeJSEnv.Config().withSourceMap(wantSourceMaps.value)),
106113

114+
scalaJSLinkerImpl := {
115+
val v = (scalaVersion in Build.linker).value
116+
assert(v.startsWith("2.12."), "The linker is configured with a Scala " +
117+
s"version ($v) that is not compatible with this build (2.12.x). ")
118+
LinkerImpl.default(Attributed.data((fullClasspath in (Build.linker, Runtime)).value))
119+
},
120+
107121
// Link source maps to GitHub sources
108122
addScalaJSCompilerOption(Def.setting {
109123
"mapSourceURI:" +
@@ -201,7 +215,7 @@ object Build {
201215
}
202216

203217
val commonSettings = Seq(
204-
scalaVersion := "2.12.8",
218+
scalaVersion := jvmScalaVersion.value,
205219
organization := "org.scala-js",
206220
version := scalaJSVersion,
207221

@@ -495,7 +509,10 @@ object Build {
495509
if (v < 8)
496510
throw new MessageOnlyException("This build requires JDK 8 or later. Aborting.")
497511
v
498-
}
512+
},
513+
514+
jvmScalaVersion in Global := "2.12.8",
515+
jsScalaVersion in Global := "2.12.8",
499516
)
500517

501518
lazy val root: Project = Project(id = "scalajs", base = file(".")).settings(
@@ -815,7 +832,7 @@ object Build {
815832

816833
sbtJars.map(_.data -> docUrl).toMap
817834
}
818-
).dependsOn(linker, jsEnvs, nodeJSEnv, testAdapter)
835+
).dependsOn(linkerInterface, jsEnvs, nodeJSEnv, testAdapter)
819836

820837
lazy val delambdafySetting = {
821838
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 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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
)
81+
82+
override def loadClass(name: String, resolve: Boolean): Class[_] = {
83+
if (parentPrefixes.exists(name.startsWith _))
84+
super.loadClass(name, resolve)
85+
else
86+
null
87+
}
88+
}
89+
90+
private final class Reflect(loader: ClassLoader) extends LinkerImpl {
91+
private def loadMethod(clazz: String, method: String, result: Class[_], params: Class[_]*): Method = {
92+
val m = Class.forName("org.scalajs.linker." + clazz, true, loader).getMethod(method, params: _*)
93+
require(Modifier.isStatic(m.getModifiers()))
94+
require(result.isAssignableFrom(m.getReturnType()))
95+
m
96+
}
97+
98+
private def invoke[T](method: Method, args: AnyRef*): T =
99+
method.invoke(null, args: _*).asInstanceOf[T]
100+
101+
/* We load everything eagery to fail immediately, not only when the methods
102+
* are invoked.
103+
*/
104+
private val clearableLinkerMethod =
105+
loadMethod("StandardImpl", "clearableLinker", classOf[ClearableLinker], classOf[StandardConfig])
106+
107+
private val irFileCacheMethod =
108+
loadMethod("StandardImpl", "irFileCache", classOf[IRFileCache])
109+
110+
private val irContainersMethod = {
111+
loadMethod("PathIRContainer", "fromClasspath", classOf[Future[_]],
112+
classOf[Seq[Path]], classOf[ExecutionContext])
113+
}
114+
115+
private val outputFileMethod =
116+
loadMethod("PathOutputFile", "atomic", classOf[LinkerOutput.File], classOf[Path])
117+
118+
def clearableLinker(cfg: StandardConfig): ClearableLinker =
119+
invoke(clearableLinkerMethod, cfg)
120+
121+
def irFileCache(): IRFileCache =
122+
invoke(irFileCacheMethod)
123+
124+
def irContainers(classpath: Seq[Path])(
125+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])] = {
126+
invoke(irContainersMethod, classpath, ec)
127+
}
128+
129+
def outputFile(path: Path): LinkerOutput.File =
130+
invoke(outputFileMethod, path)
131+
}
132+
}

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

Lines changed: 48 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,19 @@ 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+
"Factory for a Scala.js linker", KeyRanks.Invisible)
99+
100+
val scalaJSLinkerBox = SettingKey[CacheBox[ClearableLinker]]("scalaJSLinkerBox",
101+
"CacheBox for a Scala.js linker", KeyRanks.Invisible)
88102

89103
/** A tag to indicate that a task is using the value of [[scalaJSLinker]]
90104
* and its `link` method.
@@ -172,9 +186,14 @@ object ScalaJSPlugin extends AutoPlugin {
172186
import autoImport._
173187

174188
/** Logs the current statistics about the global IR cache. */
175-
def logIRCacheStats(logger: Logger): Unit = {
176-
import ScalaJSPluginInternal.globalIRCache
177-
logger.debug("Global IR cache stats: " + globalIRCache.stats.logLine)
189+
val irCacheStatsLogger: Def.Initialize[Logger => Unit] = Def.setting {
190+
val globalIRCacheBox = scalaJSGlobalIRCacheBox.value
191+
192+
logger: Logger => {
193+
globalIRCacheBox.foreach { globalIRCache =>
194+
logger.debug("Global IR cache stats: " + globalIRCache.stats.logLine)
195+
}
196+
}
178197
}
179198

180199
override def globalSettings: Seq[Setting[_]] = {
@@ -183,16 +202,34 @@ object ScalaJSPlugin extends AutoPlugin {
183202

184203
scalaJSLinkerConfig := StandardConfig(),
185204

205+
scalaJSLinkerImpl := {
206+
val s = streams.value
207+
val log = s.log
208+
val retrieveDir = s.cacheDirectory / "scalajs-linker" / scalaJSVersion
209+
val lm = {
210+
import sbt.librarymanagement.ivy._
211+
val ivyConfig = InlineIvyConfiguration().withLog(log)
212+
IvyDependencyResolution(ivyConfig)
213+
}
214+
lm.retrieve(
215+
"org.scala-js" % "scalajs-linker_2.12" % scalaJSVersion,
216+
scalaModuleInfo = None, retrieveDir, log)
217+
.fold(w => throw w.resolveException, LinkerImpl.default _)
218+
},
219+
220+
scalaJSGlobalIRCacheBox := new CacheBox,
221+
186222
jsEnv := new NodeJSEnv(),
187223

188224
// Clear the IR cache stats every time a sequence of tasks ends
189225
onComplete := {
190226
val prev = onComplete.value
227+
val globalIRCacheBox = scalaJSGlobalIRCacheBox.value
191228

192229
{ () =>
193230
prev()
194231
ScalaJSPluginInternal.closeAllTestAdapters()
195-
ScalaJSPluginInternal.globalIRCache.clearStats()
232+
globalIRCacheBox.foreach(_.clearStats())
196233
}
197234
},
198235

0 commit comments

Comments
 (0)