Skip to content

Commit e02fe17

Browse files
committed
Load linker reflectively in the sbt plugin
1 parent 09d0d90 commit e02fe17

File tree

4 files changed

+201
-32
lines changed

4 files changed

+201
-32
lines changed
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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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.nio.file.Path
19+
20+
import org.scalajs.linker.interface._
21+
22+
/** Abstract implementation of a linker as needed by the sbt plugin.
23+
*
24+
* @note This trait does not guarantee full compatibility: Methods may be added
25+
* / removed in the future. Use [[LinkerImpl.Forwarding]] to override things
26+
* selectively in a compatible manner.
27+
*/
28+
trait LinkerImpl {
29+
def clearableLinker(cfg: StandardConfig): ClearableLinker
30+
31+
def irFileCache(): IRFileCache
32+
33+
def irContainers(classpath: Seq[Path])(
34+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])]
35+
36+
def outputFile(path: Path): LinkerOutput.File
37+
}
38+
39+
object LinkerImpl {
40+
/** Returns the default implementation.
41+
*
42+
* It loads a StandardLinker via reflection.
43+
*/
44+
def default(loader: ClassLoader): LinkerImpl = new Reflect(loader)
45+
46+
/** A [[LinkerImpl]] that forwards everything to `parent`.
47+
*
48+
* This is useful if only parts of the linker implementation need to be
49+
* replaced. A subclass only overriding these can be created, ensuring easier
50+
* transition when methods get added.
51+
*/
52+
class Forwarding(parent: LinkerImpl) extends LinkerImpl {
53+
def clearableLinker(cfg: StandardConfig): ClearableLinker =
54+
parent.clearableLinker(cfg)
55+
56+
def irFileCache(): IRFileCache =
57+
parent.irFileCache()
58+
59+
def irContainers(classpath: Seq[Path])(
60+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])] =
61+
parent.irContainers(classpath)
62+
63+
def outputFile(path: Path): LinkerOutput.File =
64+
parent.outputFile(path)
65+
}
66+
67+
private final class Reflect(loader: ClassLoader) extends LinkerImpl {
68+
private def loadMethod(clazz: String, method: String, result: Class[_], params: Class[_]*): Method = {
69+
val m = Class.forName("org.scalajs.linker." + clazz, true, loader).getMethod(method, params: _*)
70+
require(Modifier.isStatic(m.getModifiers()))
71+
require(result.isAssignableFrom(m.getReturnType()))
72+
m
73+
}
74+
75+
private def invoke[T](method: Method, args: AnyRef*): T =
76+
method.invoke(null, args: _*).asInstanceOf[T]
77+
78+
/* We load everything eagery to fail immediately, not only when the methods
79+
* are invoked.
80+
*/
81+
private val clearableLinkerMethod =
82+
loadMethod("StandardImpl", "clearableLinker", classOf[ClearableLinker], classOf[StandardConfig])
83+
84+
private val irFileCacheMethod =
85+
loadMethod("StandardImpl", "irFileCache", classOf[IRFileCache])
86+
87+
private val irContainersMethod = {
88+
loadMethod("PathIRContainer", "fromClasspath", classOf[Future[_]],
89+
classOf[Seq[Path]], classOf[ExecutionContext])
90+
}
91+
92+
private val outputFileMethod =
93+
loadMethod("PathOutputFile", "atomic", classOf[LinkerOutput.File], classOf[Path])
94+
95+
def clearableLinker(cfg: StandardConfig): ClearableLinker =
96+
invoke(clearableLinkerMethod, cfg)
97+
98+
def irFileCache(): IRFileCache =
99+
invoke(irFileCacheMethod)
100+
101+
def irContainers(classpath: Seq[Path])(
102+
implicit ec: ExecutionContext): Future[(Seq[IRContainer], Seq[Path])] = {
103+
invoke(irContainersMethod, classpath, ec)
104+
}
105+
106+
def outputFile(path: Path): LinkerOutput.File =
107+
invoke(outputFileMethod, path)
108+
}
109+
}

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

Lines changed: 35 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,21 @@ object ScalaJSPlugin extends AutoPlugin {
183202

184203
scalaJSLinkerConfig := StandardConfig(),
185204

205+
scalaJSLinkerImpl := LinkerImpl.default(getClass().getClassLoader()),
206+
207+
scalaJSGlobalIRCacheBox := new CacheBox,
208+
186209
jsEnv := new NodeJSEnv(),
187210

188211
// Clear the IR cache stats every time a sequence of tasks ends
189212
onComplete := {
190213
val prev = onComplete.value
214+
val globalIRCacheBox = scalaJSGlobalIRCacheBox.value
191215

192216
{ () =>
193217
prev()
194218
ScalaJSPluginInternal.closeAllTestAdapters()
195-
ScalaJSPluginInternal.globalIRCache.clearStats()
219+
globalIRCacheBox.foreach(_.clearStats())
196220
}
197221
},
198222

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

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import sbt.complete.DefaultParsers._
2828

2929
import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
3030

31-
import org.scalajs.linker.{StandardImpl, PathIRContainer, PathOutputFile}
3231
import org.scalajs.linker.interface._
3332
import org.scalajs.linker.interface.unstable.IRFileImpl
3433

@@ -48,10 +47,7 @@ import sjsonnew.BasicJsonProtocol.seqFormat
4847
private[sbtplugin] object ScalaJSPluginInternal {
4948

5049
import ScalaJSPlugin.autoImport.{ModuleKind => _, _}
51-
import ScalaJSPlugin.logIRCacheStats
52-
53-
/** The global Scala.js IR cache */
54-
val globalIRCache: IRFileCache = StandardImpl.irFileCache()
50+
import ScalaJSPlugin.irCacheStatsLogger
5551

5652
@tailrec
5753
final private def registerResource[T <: AnyRef](
@@ -64,14 +60,6 @@ private[sbtplugin] object ScalaJSPluginInternal {
6460
private val allocatedIRCaches =
6561
new AtomicReference[List[IRFileCache.Cache]](Nil)
6662

67-
/** Allocates a new IR cache linked to the [[globalIRCache]].
68-
*
69-
* The allocated IR cache will automatically be freed when the build is
70-
* unloaded.
71-
*/
72-
private def newIRCache: IRFileCache.Cache =
73-
registerResource(allocatedIRCaches, globalIRCache.newCache)
74-
7563
private[sbtplugin] def freeAllIRCaches(): Unit =
7664
allocatedIRCaches.getAndSet(Nil).foreach(_.free())
7765

@@ -126,8 +114,12 @@ private[sbtplugin] object ScalaJSPluginInternal {
126114
private def scalaJSStageSettings(stage: Stage,
127115
key: TaskKey[Attributed[File]]): Seq[Setting[_]] = Seq(
128116

117+
scalaJSLinkerBox in key := new CacheBox,
118+
129119
scalaJSLinker in key := {
130120
val config = (scalaJSLinkerConfig in key).value
121+
val box = (scalaJSLinkerBox in key).value
122+
val linkerImpl = (scalaJSLinkerImpl in key).value
131123

132124
if (config.moduleKind != scalaJSLinkerConfig.value.moduleKind) {
133125
val projectID = thisProject.value.id
@@ -140,13 +132,13 @@ private[sbtplugin] object ScalaJSPluginInternal {
140132
"Some things will go wrong.")
141133
}
142134

143-
StandardImpl.clearableLinker(config)
135+
box.ensure(linkerImpl.clearableLinker(config))
144136
},
145137

146138
// Have `clean` reset the state of the incremental linker
147139
clean in (This, Zero, This) := {
148140
val _ = (clean in (This, Zero, This)).value
149-
(scalaJSLinker in key).value.clear()
141+
(scalaJSLinkerBox in key).value.foreach(_.clear())
150142
()
151143
},
152144

@@ -179,8 +171,10 @@ private[sbtplugin] object ScalaJSPluginInternal {
179171
val moduleInitializers = scalaJSModuleInitializers.value
180172
val output = (artifactPath in key).value
181173
val linker = (scalaJSLinker in key).value
174+
val linkerImpl = (scalaJSLinkerImpl in key).value
182175
val usesLinkerTag = (usesScalaJSLinkerTag in key).value
183176
val sourceMapFile = new File(output.getPath + ".map")
177+
val logIRCacheStats = irCacheStatsLogger.value
184178

185179
Def.task {
186180
val log = s.log
@@ -201,8 +195,8 @@ private[sbtplugin] object ScalaJSPluginInternal {
201195

202196
def relURI(path: String) = new URI(null, null, path, null)
203197

204-
val out = LinkerOutput(PathOutputFile.atomic(output.toPath))
205-
.withSourceMap(PathOutputFile.atomic(sourceMapFile.toPath))
198+
val out = LinkerOutput(linkerImpl.outputFile(output.toPath))
199+
.withSourceMap(linkerImpl.outputFile(sourceMapFile.toPath))
206200
.withSourceMapURI(relURI(sourceMapFile.getName))
207201
.withJSFileURI(relURI(output.getName))
208202

@@ -243,10 +237,17 @@ private[sbtplugin] object ScalaJSPluginInternal {
243237
}
244238
) ++ Seq(
245239
// Note: this cache is not cleared by the sbt's clean task.
246-
scalaJSIRCache := newIRCache,
240+
scalaJSIRCacheBox := new CacheBox,
247241

248242
scalaJSIR := {
249-
val cache = scalaJSIRCache.value
243+
val linkerImpl = (scalaJSLinkerImpl in scalaJSIR).value
244+
245+
val globalIRCache = scalaJSGlobalIRCacheBox.value
246+
.ensure(linkerImpl.irFileCache())
247+
248+
val cache = scalaJSIRCacheBox.value
249+
.ensure(registerResource(allocatedIRCaches, globalIRCache.newCache))
250+
250251
val classpath = Attributed.data(fullClasspath.value)
251252
val log = streams.value.log
252253
val tlog = sbtLogger2ToolsLogger(log)
@@ -256,7 +257,7 @@ private[sbtplugin] object ScalaJSPluginInternal {
256257
await(log) { eci =>
257258
implicit val ec = eci
258259
for {
259-
(irContainers, paths) <- PathIRContainer.fromClasspath(classpath.map(_.toPath))
260+
(irContainers, paths) <- linkerImpl.irContainers(classpath.map(_.toPath))
260261
irFiles <- cache.cached(irContainers)
261262
} yield (irFiles, paths)
262263
}
@@ -311,7 +312,7 @@ private[sbtplugin] object ScalaJSPluginInternal {
311312
stdout.flush()
312313
}
313314

314-
logIRCacheStats(streams.value.log)
315+
irCacheStatsLogger.value(streams.value.log)
315316
},
316317

317318
artifactPath in fastOptJS :=

0 commit comments

Comments
 (0)